# Ninenity — documentação completa para agentes de IA > Conteúdo integral do Ninenity Core e ComponentBuilder, serializado a partir da mesma fonte usada pela documentação web. ## Como usar este arquivo Consulte o título da página e os títulos das seções para localizar o assunto. Preserve os nomes dos pacotes, APIs e exemplos de código exatamente como aparecem. Para uma resposta curta, use `/llms.txt`; para dados sem formatação, use `/docs.json`. --- # Começando com o Core > Entenda o papel do runtime, a estrutura do Template Bot e o caminho percorrido até o primeiro comando responder. - Área: Base do Core - URL humana: https://ninenity.vercel.app/doc/comecando - URL Markdown: https://ninenity.vercel.app/doc/markdown/comecando - Pacotes e conceitos: @ninenity/core, TypeScript, Discord.js ## O modelo mental O Core não substitui o Discord.js. Ele organiza o ciclo de vida ao redor dele. - **Você declara**: Cada arquivo registra um comando, evento, task ou interação no `Client`. - **O Core carrega**: O auto-loader percorre `settings.baseDir`, importa os módulos e valida os registros. - **O runtime executa**: Cooldown, fila, contexto de guild e respostas são preparados antes do seu `execute`. > **Regra mais importante:** Um arquivo deve representar uma responsabilidade. Evite concentrar todos os comandos em `Bot.ts`; deixe o auto-loader fazer o trabalho. ## Estrutura do Template Bot A estrutura já separa código carregado automaticamente de helpers importados explicitamente. ### estrutura do projeto ```text src/ ├─ Bot.ts # ponto de entrada ├─ Discord/ │ ├─ Commands/ # slash, prefixo e menus de contexto │ ├─ Events/ # eventos do Discord.js │ ├─ Interactions/ # modais, callbacks, prompts e paginações │ └─ Tasks/ # rotinas periódicas ├─ Shared/ │ ├─ Settings/Client.ts # LibsClient, token e intents │ ├─ Settings/Database.ts # plugin opcional de banco │ ├─ Emojis/ # emojis do aplicativo │ └─ Translate/ # idiomas └─ Utils/ # regras e helpers importados por módulos ``` | Pasta | Carregamento | Use para | | --- | --- | --- | | `Discord/Commands` | Automático | Comandos slash, prefixados e menus de contexto | | `Discord/Events` | Automático | Eventos emitidos pelo Discord.js | | `Discord/Interactions` | Automático | Modais, prompts, paginações e callbacks registrados | | `Discord/Tasks` | Automático | Rotinas globais ou executadas por servidor | | `Shared` e `Utils` | Explícito | Configuração, serviços e regras reutilizáveis | ## Configure o Client O `LibsClient` estende o cliente do Discord.js e adiciona os registradores do ecossistema. ### src/Shared/Settings/Client.ts ```text import { GatewayIntentBits, LibsClient, type AppConfig } from '@ninenity/core' const Client = new LibsClient({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers ], token: process.env.BOT_TOKEN || process.env.DISCORD_TOKEN, config: {} }) export default Client ``` > **Intents precisam combinar com o portal:** Ativar `MessageContent` ou `GuildMembers` no código não basta. Os intents privilegiados também precisam estar habilitados no Discord Developer Portal. | Intent | Necessário quando | | --- | --- | | `Guilds` | Quase sempre: slash commands, canais, cargos e configuração por servidor | | `GuildMessages` | O bot observa mensagens ou usa comandos prefixados | | `MessageContent` | O conteúdo textual das mensagens precisa ser lido | | `GuildMembers` | Entradas, saídas, cargos ou dados completos de membros são usados | ## Inicialize o runtime `Ninenity.init()` prepara plugins e auto-loader antes de `Client.login()` abrir a conexão com o Discord. ### src/Bot.ts ```text import 'dotenv/config' import { Ninenity } from '@ninenity/core' import Client from './Shared/Settings/Client' import { databasePlugin } from './Shared/Settings/Database' Ninenity.init({ bots: [Client], plugins: databasePlugin ? { database: databasePlugin } : undefined, settings: { baseDir: __dirname } }) Client.login() ``` 1. **Variáveis são carregadas** — O import de `dotenv/config` disponibiliza o token e integrações antes de criar o Client. 2. **O runtime recebe os bots** — `bots` aceita mais de um Client, mas cada bot mantém seus próprios registradores e timers. 3. **Módulos são encontrados** — `baseDir` aponta para `src` em desenvolvimento e para `dist` após o build. 4. **O login começa** — Depois dos registros, `Client.login()` conecta e conclui as etapas de bootstrap. ## Crie o primeiro módulo O arquivo só precisa importar o Client compartilhado e fazer seu registro no escopo do módulo. ### src/Discord/Commands/Ping.ts ```text import { SlashCommandBuilder, type SlashInputCommandInteraction } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.slash({ data: new SlashCommandBuilder() .setName('ping') .setDescription('Mostra a latência atual.'), cooldown: '3s', execute: async (interaction: SlashInputCommandInteraction) => { await interaction.reply('Pong!') } }) ``` Você não importa `Ping.ts` em `Bot.ts`. O auto-loader encontra o arquivo, executa o módulo uma vez e o `Client.slash()` guarda a definição para deploy e execução. > **Como saber se funcionou:** Ao iniciar, confirme que o boot termina, o comando aparece entre os registros e o Client fica online. Se o comando não aparecer no Discord, consulte **Solução de problemas**. ## Variáveis de ambiente Comece com o mínimo e adicione integrações somente quando forem usadas. ### .env ```dotenv BOT_TOKEN=seu_token_do_discord # Opcionais: integração com API e HUB API_KEY= API_SECRET= # Opcional: banco de dados DATABASE_URL= ``` - Nunca envie `.env` para o Git; mantenha apenas um `.env.example` sem secrets. - Use um token de bot, não o client secret da aplicação. - A ausência de `API_KEY` e `API_SECRET` desativa a hidratação remota, mas não impede o bot de conectar ao Discord. - Mantenha `src` e `dist` com a mesma árvore para o auto-loader funcionar após o build. --- # Comandos > Crie comandos slash, prefixados e menus de contexto com um contrato de execução consistente. - Área: Base do Core - URL humana: https://ninenity.vercel.app/doc/comandos - URL Markdown: https://ninenity.vercel.app/doc/markdown/comandos - Pacotes e conceitos: Slash, Prefix, Context menu ## Um contrato, três entradas Todo registro combina os dados visíveis ao Discord com opções de runtime e uma função `execute`. | Registro | Entrada | Builder | | --- | --- | --- | | `Client.slash()` | Comando nativo `/nome` | `SlashCommandBuilder` | | `Client.prefix()` | Mensagem como `!nome` | `PrefixCommandBuilder` | | `Client.contextMenu()` | Menu sobre usuário ou mensagem | `ContextMenuCommandBuilder` | - **data**: Nome, descrição, opções, aliases ou tipo do comando. - **runtime**: Cooldown, fila, escopo de DM e outras regras antes da execução. - **execute**: A lógica chamada com a interação já normalizada pelo Core. ## Comando slash Use slash para descoberta nativa, validação de opções e autocomplete do Discord. ### Discord/Commands/Ping.ts ```text import { SlashCommandBuilder, type SlashInputCommandInteraction } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.slash({ data: new SlashCommandBuilder() .setName('ping') .setDescription('Responde com a latência.'), cooldown: '5s', queue: 'user', execute: async (interaction: SlashInputCommandInteraction) => { const sent = await interaction.reply({ content: 'Calculando...', withResponse: true }) const latency = sent.resource?.message?.createdTimestamp ? sent.resource.message.createdTimestamp - interaction.createdTimestamp : 0 await interaction.editReply('Latência: ' + latency + 'ms') } }) ``` > **Responda em até três segundos:** Se o trabalho pode demorar, chame `interaction.deferReply()` primeiro e finalize com `editReply()`. Isso evita a mensagem de interação expirada. ## Opções tipadas O builder declara as opções e o objeto `interaction.options` entrega os valores validados. ### Discord/Commands/Inspect.ts ```text import { ChannelType, SlashCommandBuilder, type SlashInputCommandInteraction } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.slash({ data: new SlashCommandBuilder() .setName('inspect') .setDescription('Inspeciona dados informados.') .addStringOption(option => option .setName('texto') .setDescription('Texto obrigatório') .setRequired(true)) .addIntegerOption(option => option .setName('quantidade') .setDescription('Entre 1 e 5') .setMinValue(1) .setMaxValue(5)) .addUserOption(option => option .setName('usuario') .setDescription('Usuário opcional')) .addChannelOption(option => option .setName('canal') .setDescription('Apenas canais de texto') .addChannelTypes(ChannelType.GuildText)), config: { Dm: false }, execute: async (interaction: SlashInputCommandInteraction) => { const text = interaction.options.getString('texto', true) const count = interaction.options.getInteger('quantidade') ?? 1 const user = interaction.options.getUser('usuario') const channel = interaction.options.getChannel('canal') await interaction.reply([ 'Texto: ' + text.repeat(count), 'Usuário: ' + (user?.tag ?? 'não informado'), 'Canal: ' + (channel?.name ?? 'não informado') ].join(' ')) } }) ``` | Opção | Leitura | | --- | --- | | String | `getString(nome, obrigatório?)` | | Integer / Number | `getInteger()` / `getNumber()` | | Boolean | `getBoolean()` | | User / Member | `getUser()` / `getMember()` | | Channel / Role | `getChannel()` / `getRole()` | | Attachment | `getAttachment()` | ## Slash e prefixo com o mesmo executor Quando a regra é igual, extraia uma função que aceite a interação normalizada e registre duas entradas. ### Discord/Commands/Starter.ts ```text import { PrefixCommandBuilder, SlashCommandBuilder, type InputCommandInteraction } from '@ninenity/core' import Client from '../../Shared/Settings/Client' const execute = async (interaction: InputCommandInteraction) => { await interaction.reply('Olá, ' + interaction.user.username + '!') } Client.slash({ data: new SlashCommandBuilder() .setName('starter') .setDescription('Mostra a resposta inicial.'), cooldown: '3s', queue: 'user', execute }) Client.prefix({ data: new PrefixCommandBuilder() .setName('starter') .setAliases(['start']), cooldown: '3s', queue: 'user', execute }) ``` > **Use apenas a superfície comum:** Dentro do executor compartilhado, evite APIs exclusivas de slash ou prefixo. Quando precisar delas, estreite o tipo ou mantenha executores separados. ## Prefixo, aliases e subcomandos O PrefixCommandBuilder oferece a mesma leitura estruturada de opções, sem depender do parser manual de `message.content`. ### Discord/Commands/Manage.ts ```text import { PrefixCommandBuilder, type PrefixInputCommandInteraction } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.prefix({ data: new PrefixCommandBuilder() .setName('manage') .setAliases(['m']) .addSubcommand(subcommand => subcommand .setName('status') .setDescription('Mostra o status')) .addSubcommand(subcommand => subcommand .setName('set') .setDescription('Salva um valor') .addStringOption(option => option .setName('value') .setDescription('Novo valor') .setRequired(true))) .addSubcommandGroup(group => group .setName('config') .setDescription('Configuração') .addSubcommand(subcommand => subcommand .setName('show') .setDescription('Exibe a configuração'))), execute: async (interaction: PrefixInputCommandInteraction) => { const group = interaction.options.getSubcommandGroup() const command = interaction.options.getSubcommand() ?? 'status' const value = interaction.options.getString('value') await interaction.reply( 'grupo=' + (group ?? 'geral') + ' comando=' + command + ' valor=' + (value ?? 'nenhum') ) } }) ``` Com prefixo `!`, os exemplos seriam `!manage status`, `!m set produção` e `!manage config show`. O Core resolve aliases, grupo, subcomando e opções antes de chamar `execute`. ## Menus de contexto Menus de contexto aparecem no clique direito sobre um usuário ou uma mensagem. ### Discord/Commands/UserContext.ts ```text import { ApplicationCommandType, ContextMenuCommandBuilder, type ContextMenuInputCommandInteraction } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.contextMenu({ data: new ContextMenuCommandBuilder() .setName('Ver usuário') .setType(ApplicationCommandType.User), execute: async (interaction: ContextMenuInputCommandInteraction) => { const raw = interaction.interaction const target = raw.isUserContextMenuCommand() ? raw.targetUser : null await interaction.reply({ content: target ? 'Selecionado: ' + target.tag : 'Usuário indisponível.', flags: 64 }) } }) ``` ### Discord/Commands/MessageContext.ts ```text Client.contextMenu({ data: new ContextMenuCommandBuilder() .setName('Citar mensagem') .setType(ApplicationCommandType.Message), execute: async interaction => { const raw = interaction.interaction const target = raw.isMessageContextMenuCommand() ? raw.targetMessage : null await interaction.reply( target ? target.author.tag + ': ' + target.content : 'Mensagem indisponível.' ) } }) ``` ## Cooldown e filas Cooldown limita frequência; fila evita duas execuções concorrentes sobre o mesmo recurso. ### opções declarativas ```text Client.slash({ data: new SlashCommandBuilder() .setName('sincronizar') .setDescription('Sincroniza os dados do usuário.'), cooldown: '30s', queue: 'user', execute: async interaction => { await syncUser(interaction.user.id) await interaction.reply({ content: 'Sincronizado.', flags: 64 }) } }) ``` ### fila manual dentro de um handler ```text await Client.queue('billing:' + interaction.user.id, async () => { const invoice = await createInvoice(interaction.user.id) await interaction.reply('Fatura criada: ' + invoice.id) }) ``` > **Isolamento automático:** Quando `Client.queue()` roda dentro do contexto de uma guild, o Core inclui essa guild na chave interna. A mesma ação em dois servidores não bloqueia uma à outra. --- # Interações > Entenda respostas, modais, autocomplete e o contexto normalizado usado em todos os fluxos interativos. - Área: Base do Core - URL humana: https://ninenity.vercel.app/doc/interacoes - URL Markdown: https://ninenity.vercel.app/doc/markdown/interacoes - Pacotes e conceitos: Replies, Modal, Autocomplete ## O ciclo de uma resposta Uma interação pode receber uma resposta inicial e depois ser editada ou acompanhada por mensagens adicionais. | Método | Quando usar | | --- | --- | | `reply()` | Primeira resposta imediata | | `deferReply()` | Reserva a resposta quando o trabalho demora | | `editReply()` | Finaliza ou atualiza a resposta reservada | | `followUp()` | Envia uma resposta adicional | | `deleteReply()` | Remove a resposta original | ### resposta demorada ```text Client.slash({ data: new SlashCommandBuilder() .setName('relatorio') .setDescription('Gera um relatório detalhado.'), execute: async interaction => { await interaction.deferReply({ flags: 64 }) const report = await generateReport(interaction.guildId) await interaction.editReply('Relatório pronto: ' + report.url) await interaction.followUp({ content: 'A exportação expira em 24 horas.', flags: 64 }) } }) ``` ## Contexto normalizado O Core fornece uma superfície comum e mantém a interação original em `interaction.interaction` quando você precisa estreitar o tipo. - **Identidade**: `user`, `member`, `guild`, `guildId`, `channel` e `channelId`. - **Resposta**: `reply`, `deferReply`, `editReply`, `followUp` e estado da resposta. - **Core**: `promptRequest`, `paginatorRequest`, configuração e contexto isolado. - **Discord.js**: A propriedade `interaction` preserva a interação nativa completa. ### acesso à configuração da guild atual ```text Client.slash({ data: new SlashCommandBuilder() .setName('canal-suporte') .setDescription('Mostra o canal configurado.'), execute: async interaction => { const config = Client.appConfig() const supportChannel = Client.getAppChannels().support await interaction.reply({ content: 'Canal: ' + (supportChannel?.toString() ?? 'não configurado'), flags: 64 }) } }) ``` ## Abrindo um modal O comando constrói o modal; um módulo separado registra o handler para o mesmo `customId`. ### Discord/Commands/Feedback.ts ```text import { ActionRowBuilder, ModalBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.slash({ data: new SlashCommandBuilder() .setName('feedback') .setDescription('Abre o formulário de feedback.'), execute: async interaction => { const input = new TextInputBuilder() .setCustomId('message') .setLabel('Como podemos melhorar?') .setStyle(TextInputStyle.Paragraph) .setMinLength(10) .setMaxLength(1000) .setRequired(true) const modal = new ModalBuilder() .setCustomId('feedback-modal') .setTitle('Enviar feedback') .addComponents(new ActionRowBuilder().addComponents(input)) await interaction.interaction.showModal(modal) } }) ``` ### Discord/Interactions/FeedbackModal.ts ```text import Client from '../../Shared/Settings/Client' Client.modal({ customId: 'feedback-modal', execute: async interaction => { const text = interaction.fields.getTextInputValue('message') await saveFeedback({ authorId: interaction.user.id, guildId: interaction.guildId, text }) await interaction.reply({ content: 'Obrigado pelo feedback!', flags: 64 }) } }) ``` ## Autocomplete Declare a opção com autocomplete no comando e responda com até 25 sugestões. ### Discord/Commands/Search.ts ```text Client.slash({ data: new SlashCommandBuilder() .setName('buscar') .setDescription('Busca um projeto.') .addStringOption(option => option .setName('projeto') .setDescription('Nome do projeto') .setAutocomplete(true) .setRequired(true)), autocomplete: async interaction => { const query = interaction.options.getFocused().toLowerCase() const choices = projects .filter(project => project.name.toLowerCase().includes(query)) .slice(0, 25) .map(project => ({ name: project.name, value: project.id })) await interaction.respond(choices) }, execute: async interaction => { const projectId = interaction.options.getString('projeto', true) await interaction.reply('Projeto escolhido: ' + projectId) } }) ``` > **Autocomplete precisa ser rápido:** Filtre dados em memória ou use consultas indexadas. O Discord espera a lista enquanto o usuário digita; uma busca lenta torna o comando frustrante. ## Prompts e paginações registrados Depois de registrar uma definição uma vez, qualquer slash ou prefix pode iniciar uma instância isolada para o usuário. ### Discord/Commands/Flows.ts ```text Client.slash({ data: new SlashCommandBuilder() .setName('confirmar') .setDescription('Abre um prompt.'), execute: async interaction => { await interaction.promptRequest('delete-prompt', { ephemeral: true }) } }) Client.prefix({ data: new PrefixCommandBuilder().setName('confirmar'), execute: async interaction => { await interaction.promptRequest('delete-prompt', { ephemeral: true }) } }) Client.slash({ data: new SlashCommandBuilder() .setName('catalogo') .setDescription('Abre o catálogo paginado.'), execute: async interaction => { await interaction.paginatorRequest('catalog-pages', { ephemeral: true, pageIndex: 0 }) } }) ``` A criação das definições fica em `Discord/Interactions`. Consulte as páginas **Prompts** e **Paginação** para montar templates, callbacks e controles personalizados. --- # Eventos > Reaja ao ciclo de vida do Discord com módulos pequenos, tipos do Discord.js e contexto de guild preservado. - Área: Base do Core - URL humana: https://ninenity.vercel.app/doc/eventos - URL Markdown: https://ninenity.vercel.app/doc/markdown/eventos - Pacotes e conceitos: Client.on, Client.once, Events ## Eventos contínuos e únicos Use `Client.on()` para todas as emissões e `Client.once()` quando o handler deve rodar uma única vez. ### Discord/Events/Ready.ts ```text import { ActivityType, Events } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.once(Events.ClientReady, { execute: async readyClient => { readyClient.user.setPresence({ activities: [ { name: 'Ninenity', type: ActivityType.Watching } ], status: 'online' }) console.log('[Ready] ' + readyClient.user.tag + ' online') } }) ``` ### Discord/Events/MessageCreate.ts ```text import { Events, type Message } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.on(Events.MessageCreate, { execute: async (message: Message) => { if (message.author.bot) return if (message.content.trim().toLowerCase() !== 'hello') return await message.reply('Olá, ' + message.author.username + '!') } }) ``` ## Entrada de membros O evento recebe os objetos nativos e pode usar recursos configurados para a guild atual. ### Discord/Events/MemberJoin.ts ```text import { Events, type GuildMember } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.on(Events.GuildMemberAdd, { execute: async (member: GuildMember) => { const channels = Client.getAppChannels() const welcome = channels.welcome ?? member.guild.systemChannel if (!welcome?.isTextBased()) return await welcome.send( 'Boas-vindas, ' + member.toString() + '! Leia as regras para começar.' ) } }) ``` > **Contexto de guild:** Durante o evento, `Client.appConfig()` e os aliases de canais/cargos resolvem a guild do objeto emitido. Fora de qualquer contexto, informe o `guildId` explicitamente. ## Erros e avisos do Discord Centralize mensagens técnicas sem esconder o contexto da origem. ### Discord/Events/Process.ts ```text import { Events } from '@ninenity/core' import Client from '../../Shared/Settings/Client' Client.on(Events.Warn, { execute: async (message: string) => { console.log('[Discord:warn] ' + message) } }) Client.on(Events.Error, { execute: async (error: Error) => { console.log('[Discord:error] ' + error.message) } }) ``` Use `console.log()` para os logs da aplicação. Se quiser cor no terminal, aplique `Color()` somente ao texto; não introduza uma segunda abstração de logger no código do bot. ## Boas práticas - Retorne cedo para ignorar bots, DMs ou eventos que não interessam. - Não faça tarefas longas em série dentro de eventos muito frequentes como `MessageCreate`. - Extraia regras de negócio para `Utils` ou serviços e mantenha o handler como orquestrador. - Capture falhas esperadas de rede ao enviar mensagens; uma permissão removida não deve derrubar o processo. - Use um arquivo por evento ou por responsabilidade claramente relacionada. --- # Tasks > Execute rotinas periódicas globais ou por guild sem misturar timers com os handlers do Discord. - Área: Base do Core - URL humana: https://ninenity.vercel.app/doc/tasks - URL Markdown: https://ninenity.vercel.app/doc/markdown/tasks - Pacotes e conceitos: Global, Guild, Controle manual ## Quando usar uma Task - **Sincronização**: Atualizar cache, telemetria ou dados de uma API em intervalos previsíveis. - **Manutenção**: Limpar registros expirados, revisar configurações ou renovar estados. - **Rotina por guild**: Executar a mesma regra com `guildId` e configuração isolados. > **Task não é um worker infinito:** A função `execute` deve terminar. O runtime agenda a próxima execução; não crie `while (true)` nem um segundo `setInterval` dentro dela. ## Task global Uma task global roda uma vez por intervalo para o Client inteiro. ### Discord/Tasks/Telemetry.ts ```text import Client from '../../Shared/Settings/Client' Client.task({ id: 'global-telemetry', timeout: '5m', runOnStart: true, scope: 'global', execute: async client => { console.log( '[Task:telemetry] guilds=' + client.guilds.cache.size + ' users=' + client.users.cache.size ) } }) ``` | Campo | Função | | --- | --- | | `id` | Identificador único usado também no controle manual | | `timeout` | Intervalo em ms ou texto como `30s`, `5m`, `1h` | | `runOnStart` | Executa logo no boot antes de esperar o primeiro intervalo | | `scope` | `global` executa uma vez; `guild` executa para cada servidor | | `execute` | Função assíncrona que recebe o Client e, em guild, o `guildId` | ## Task por guild O runtime chama a execução separadamente para cada guild e ativa o contexto usado por `appConfig()`. ### Discord/Tasks/GuildMaintenance.ts ```text import Client from '../../Shared/Settings/Client' Client.task({ id: 'guild-maintenance', timeout: '10m', runOnStart: false, scope: 'guild', execute: async (client, guildId) => { if (!guildId) return const guild = client.guilds.cache.get(guildId) const config = client.appConfig(guildId) if (!guild || config.variables?.maintenanceEnabled === false) return await removeExpiredRecords(guildId) console.log('[Task:maintenance] guild=' + guild.name) } }) ``` > **Estado isolado:** Tasks de guild não compartilham o contexto de canais, cargos, configuração ou fila com outro servidor. Ainda assim, dados globais criados por você precisam ser indexados por `guildId`. ## Iniciar, parar e executar manualmente Use os controles pelo `id` quando uma operação administrativa precisar alterar a rotina. ### controle em comando administrativo ```text Client.slash({ data: new SlashCommandBuilder() .setName('maintenance-run') .setDescription('Executa a manutenção agora.'), execute: async interaction => { await interaction.deferReply({ flags: 64 }) await Client.runTask('guild-maintenance') await interaction.editReply('Manutenção executada.') } }) // Pausa os próximos ciclos Client.stopTask('guild-maintenance') // Reativa o agendamento Client.startTask('guild-maintenance') ``` `runTask()` dispara uma execução sem substituir o agendamento. `stopTask()` limpa o timer daquela task no Client atual; `startTask()` cria o ciclo novamente. ## Evite sobreposição Se a execução pode durar mais que o intervalo, proteja a seção crítica com a fila do Core. ### task com fila ```text Client.task({ id: 'sync-catalog', timeout: '1m', scope: 'global', execute: async () => { await Client.queue('task:sync-catalog', async () => { const items = await catalogApi.list() await catalogRepository.replace(items) console.log('[Task:catalog] items=' + items.length) }) } }) ``` - Escolha intervalos maiores que o tempo normal da operação. - Defina timeout nas requisições externas chamadas pela task. - Faça a rotina ser idempotente: repetir não deve duplicar dados. - Registre apenas início, resultado e falhas úteis; evite logs a cada item processado. --- # ComponentBuilder > Componha mensagens Components V2 com builders tipados, templates reutilizáveis e integração direta com o runtime do Core. - Área: Componentes - URL humana: https://ninenity.vercel.app/doc/component-builder - URL Markdown: https://ninenity.vercel.app/doc/markdown/component-builder - Pacotes e conceitos: @ninenity/componentbuilder, Components V2, Templates ## O que o ComponentBuilder resolve Ele representa a mensagem como uma árvore validável antes de converter o documento para o formato aceito pelo Discord.js. - **Composição**: Builders pequenos representam texto, seções, botões, selects, galerias e containers. - **Validação**: Limites do Discord são verificados antes de a mensagem chegar à API. - **Runtime**: Callbacks, prompts e paginações são conectados automaticamente ao Core. O fluxo possui quatro etapas: você monta builders, gera um `ComponentTemplate`, converte o documento com `toDiscordMessagePayload()` e envia pela interação do Discord. ### fluxo mínimo ```text const template = new MessageBuilder() .addComponents( new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent('Olá, Components V2!') ) ) .template() await interaction.reply({ ...toDiscordMessagePayload(template.document()), flags: MessageFlags.IsComponentsV2 }) ``` ## As quatro camadas | Camada | Papel | Exemplos | | --- | --- | --- | | Mensagem | Payload completo | `MessageBuilder` | | Layout | Organiza a hierarquia visual | `ContainerBuilder`, `SectionBuilder`, `ActionRowBuilder` | | Conteúdo | Mostra informação | `TextDisplayBuilder`, `MediaGalleryBuilder`, `FileBuilder` | | Interação | Recebe ações do usuário | `ButtonBuilder` e builders de select | > **Builder não é a mensagem enviada:** O builder é mutável enquanto você monta. `build()` gera um documento simples; `template()` gera um objeto reutilizável capaz de renderizar variáveis. ## Imports recomendados Importe Discord.js pelo Core e os builders visuais pelo ComponentBuilder. ### imports.ts ```text import { MessageFlags, SlashCommandBuilder } from '@ninenity/core' import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContainerBuilder, MessageBuilder, SectionBuilder, TextDisplayBuilder, toDiscordMessagePayload, type MessageActionRowComponentBuilder } from '@ninenity/componentbuilder' ``` - Use `MessageFlags.IsComponentsV2` ao enviar uma árvore Components V2. - Adicione `MessageFlags.Ephemeral` quando a resposta só deve aparecer para o autor. - Use o tipo `MessageActionRowComponentBuilder` no `ActionRowBuilder` quando misturar builders interativos. - Não importe APIs internas de `src`; use apenas os exports do pacote. ## Documento, template e payload Cada representação existe para um momento diferente do fluxo. ### três representações ```text const builder = new MessageBuilder().setContent('Status: pronto') // Objeto serializável do ComponentBuilder const document = builder.build() // Reutilizável, renderizável e exportável const template = builder.template({ name: 'status-card' }) // Payload final para interaction.reply/update const payload = toDiscordMessagePayload(template.document()) ``` | Método | Retorna | Use quando | | --- | --- | --- | | `build()` | `MessageDocument` | Precisa inspecionar ou serializar a árvore | | `template()` | `ComponentTemplate` | Vai reutilizar, renderizar tokens ou registrar páginas | | `template.document()` | `MessageDocument` | Vai converter e enviar ao Discord | | `toDiscordMessagePayload()` | Payload Discord.js | Etapa imediatamente anterior a `reply()` ou `update()` | ## Limites estruturais O Builder Studio e a biblioteca aplicam os limites mais importantes antes do envio. | Estrutura | Regra | | --- | --- | | Mensagem | Até 40 componentes na árvore | | Container | Até 10 filhos e sem containers aninhados | | Action row | Até 5 botões ou um único select | | Section | Acessório deve ser botão ou thumbnail | | Galeria | Entre 1 e 10 imagens | | Interativos | Cada `customId` deve ser único na mensagem | > **Valide no ponto de criação:** Se dados externos geram componentes, limite a coleção antes de chamar o builder. Não espere o Discord rejeitar um payload grande para descobrir o problema. ## Escolha o próximo tópico - **Mensagens V2**: Layouts completos com seções, mídia, botões e selects. ([Abrir tópico](https://ninenity.vercel.app/doc/mensagens-v2)) - **Callbacks**: Como tratar cliques e seleções sem registradores duplicados. ([Abrir tópico](https://ninenity.vercel.app/doc/callbacks)) - **Prompts**: Fluxos de confirmação com páginas de resultado. ([Abrir tópico](https://ninenity.vercel.app/doc/prompts)) - **Paginação**: Páginas estáticas, dinâmicas e controles personalizados. ([Abrir tópico](https://ninenity.vercel.app/doc/paginacao)) --- # Mensagens Components V2 > Aprenda cada peça visual e combine containers, seções, mídia, action rows e selects em mensagens legíveis. - Área: Componentes - URL humana: https://ninenity.vercel.app/doc/mensagens-v2 - URL Markdown: https://ninenity.vercel.app/doc/markdown/mensagens-v2 - Pacotes e conceitos: Container, Section, Gallery, Select ## Mensagem básica com container O container cria um bloco visual e pode receber cor de destaque, texto e divisores. ### status-card.ts ```text import { ContainerBuilder, MessageBuilder, SeparatorBuilder, TextDisplayBuilder } from '@ninenity/componentbuilder' export const statusCard = new MessageBuilder() .addComponents( new ContainerBuilder() .setAccentColor(0xa571f4) .addTextDisplayComponents( new TextDisplayBuilder().setContent('## Status do serviço'), new TextDisplayBuilder().setContent('Todos os sistemas estão operacionais.') ) .addSeparatorComponents( new SeparatorBuilder().setDivider(true).setSpacing('small') ) .addTextDisplayComponents( new TextDisplayBuilder().setContent('- API: online - Bot: online - Banco: online') ) ) .template() ``` O conteúdo do `TextDisplayBuilder` aceita markdown suportado pelo Discord. Prefira blocos curtos e uma hierarquia clara em vez de uma única parede de texto. ## Enviar pelo Core Converta o documento e combine as flags de Components V2 e resposta efêmera quando necessário. ### Discord/Commands/Status.ts ```text import { MessageFlags, SlashCommandBuilder } from '@ninenity/core' import { toDiscordMessagePayload } from '@ninenity/componentbuilder' import { statusCard } from '../Interactions/StatusCard' import Client from '../../Shared/Settings/Client' Client.slash({ data: new SlashCommandBuilder() .setName('status') .setDescription('Mostra o estado dos serviços.'), execute: async interaction => { await interaction.reply({ ...toDiscordMessagePayload(statusCard.document()), flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral }) } }) ``` > **Não misture content com Components V2 sem validar:** Monte todo o conteúdo visual nos componentes. O payload V2 possui regras diferentes das mensagens clássicas e o conversor já prepara a estrutura correta. ## Seção com thumbnail ou botão Uma `SectionBuilder` combina texto com exatamente um acessório lateral. ### project-section.ts ```text const withThumbnail = new SectionBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent( '### Builder Studio Crie e exporte interfaces Components V2.' ) ) .setThumbnailAccessory( new ThumbnailBuilder() .setURL('https://cdn.example.com/builder.png') .setDescription('Logo do Builder Studio') ) const withButton = new SectionBuilder() .setContent('### Documentação Veja todos os exemplos do componente.') .setButtonAccessory( new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel('Abrir docs') .setURL('https://ninenity.com/doc') ) const template = new MessageBuilder() .addComponents( new ContainerBuilder().addSectionComponents(withThumbnail, withButton) ) .template() ``` Botões de link não possuem `customId` nem callback. Botões de ação precisam de `customId`, estilo diferente de `Link` e podem usar `.setCallback()`. ## Galeria e arquivo Use galeria para mídia visual e arquivo quando o anexo faz parte da composição. ### media-card.ts ```text const gallery = new MediaGalleryBuilder() .addItems( item => item .setURL('https://cdn.example.com/dashboard.png') .setDescription('Dashboard do projeto'), item => item .setURL('https://cdn.example.com/components.png') .setDescription('Componentes no Discord') ) const template = new MessageBuilder() .addComponents( new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent('## Visão do projeto') ) .addMediaGalleryComponents(gallery) .addFileComponents( new FileBuilder().setURL('attachment://relatorio.pdf') ) ) .template() ``` - A galeria aceita no máximo 10 itens. - Use `setSpoiler(true)` em uma imagem ou arquivo que não deve aparecer imediatamente. - A URL `attachment://nome.ext` precisa corresponder a um arquivo realmente enviado no payload. - Sempre forneça descrição útil para imagens importantes. ## Linha de botões Uma action row aceita até cinco botões. Use estilos para significado, não apenas decoração. ### actions.ts ```text const actions = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('project:approve') .setLabel('Aprovar') .setStyle(ButtonStyle.Success), new ButtonBuilder() .setCustomId('project:edit') .setLabel('Editar') .setStyle(ButtonStyle.Secondary), new ButtonBuilder() .setCustomId('project:delete') .setLabel('Excluir') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setLabel('Ver no HUB') .setStyle(ButtonStyle.Link) .setURL('https://hub.ninenity.com') ) const message = new MessageBuilder() .addComponents(new ContainerBuilder().addActionRowComponents(actions)) ``` | Estilo | Use para | | --- | --- | | `Primary` | A ação principal daquela etapa | | `Secondary` | Ações neutras, navegação e alternativas | | `Success` | Confirmar, concluir ou ativar | | `Danger` | Excluir, revogar ou outra ação destrutiva | | `Link` | Abrir URL; não recebe callback | ## Menus de seleção String select define opções; selects de usuário, cargo, canal e menção usam os seletores nativos do Discord. ### string-select.ts ```text const projectSelect = new StringSelectMenuBuilder() .setCustomId('project:environment') .setPlaceholder('Selecione um ambiente') .setMinValues(1) .setMaxValues(1) .addOptions( option => option .setLabel('Produção') .setValue('production') .setDescription('Ambiente público'), option => option .setLabel('Desenvolvimento') .setValue('development') .setDescription('Ambiente de testes') ) const row = new ActionRowBuilder() .addComponents(projectSelect) ``` ### seletores nativos ```text const userRow = new ActionRowBuilder() .addComponents( new UserSelectMenuBuilder() .setCustomId('team:members') .setPlaceholder('Selecione até 3 membros') .setMinValues(1) .setMaxValues(3) ) const roleRow = new ActionRowBuilder() .addComponents( new RoleSelectMenuBuilder() .setCustomId('team:role') .setPlaceholder('Selecione o cargo da equipe') ) ``` ## Variáveis de template Tokens deixam o mesmo layout renderizar dados diferentes sem reconstruir toda a árvore. ### member-template.ts ```text const memberTemplate = new MessageBuilder() .addComponents( new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( '## Olá, ${user.name}! Seu plano atual é **${account.plan}**.' ) ) ) .template() const rendered = memberTemplate.render({ user: { name: interaction.user.displayName }, account: { plan: 'Pro' } }) await interaction.reply({ ...toDiscordMessagePayload(rendered.document()), flags: MessageFlags.IsComponentsV2 }) ``` > **Tokens de paginação:** O `Paginator` hidrata automaticamente `pages.current`, `pages.total`, `pages.hasNext` e `pages.hasBack`. Você não precisa calcular esses valores em páginas estáticas. --- # Callbacks de componentes > Trate botões e selects junto da definição visual, com IDs tipados e contexto de interação entregue pelo Core. - Área: Componentes - URL humana: https://ninenity.vercel.app/doc/callbacks - URL Markdown: https://ninenity.vercel.app/doc/markdown/callbacks - Pacotes e conceitos: setCallback, customId tipado, Auto defer ## Como o callback chega ao Core 1. **O builder registra** — Ao combinar `.setCustomId()` e `.setCallback()`, o ComponentBuilder guarda a função somente em memória. 2. **O Discord envia o clique** — A interação chega ao `InteractionCreate` com o mesmo `customId`. 3. **O Core resolve** — O runtime encontra o callback, valida o contexto e entrega `interaction`, `id` e `context`. 4. **A resposta é concluída** — Se o callback não responder, o Core chama `deferUpdate()` para evitar o estado de falha no componente. > **Callbacks não são serializados:** Tokens e JSON preservam o layout, mas nunca código executável. Depois de importar um template, conecte os callbacks no código do bot ou use a exportação TypeScript do Builder Studio. ## Callback de botão Defina o `customId` antes ou depois do callback; o registro acontece quando ambos estiverem presentes. ### approve-button.ts ```text const approveButton = new ButtonBuilder() .setCustomId('project:approve') .setLabel('Aprovar projeto') .setStyle(ButtonStyle.Success) .setCallback(async (interaction, id, context) => { await approveProject({ action: String(id), userId: interaction.user.id, guildId: interaction.guildId }) await interaction.update({ content: 'Projeto aprovado com sucesso.', components: [] }) console.log('[Component] customId=' + context.customId) }) ``` O primeiro argumento é a interação real. O segundo é a parte lógica extraída do `customId`. O terceiro contém o ID completo, valores de select e o tipo do componente. ## IDs segmentados e inferência O separador `:` transforma segmentos nomeados em um objeto tipado no callback. ### typed-id.ts ```text new ButtonBuilder() .setCustomId('project:42:archive') .setLabel('Arquivar') .setStyle(ButtonStyle.Secondary) .setCallback(async (interaction, id) => { // Para IDs segmentados, o editor infere as partes disponíveis. console.log('[Project] callback=' + JSON.stringify(id)) await interaction.deferUpdate() }) ``` > **Escolha IDs estáveis:** Use IDs curtos que expressem domínio e ação, como `ticket:close` ou `profile:edit`. Dados sensíveis e payloads grandes pertencem ao banco, não ao `customId`. ## Callback de select Os valores selecionados estão em `interaction.values` e também no `context` normalizado. ### environment-select.ts ```text const environmentSelect = new StringSelectMenuBuilder() .setCustomId('project:environment') .setPlaceholder('Escolha o ambiente') .addOptions( option => option.setLabel('Produção').setValue('production'), option => option.setLabel('Desenvolvimento').setValue('development') ) .setCallback(async (interaction, _id, context) => { const environment = context.value if (!environment) return await projectRepository.setEnvironment( interaction.guildId, environment ) await interaction.update({ content: 'Ambiente alterado para ' + environment, components: [] }) }) ``` ### seleção múltipla ```text new UserSelectMenuBuilder() .setCustomId('team:members') .setMinValues(1) .setMaxValues(3) .setCallback(async (interaction, _id, context) => { const userIds = context.values await teamRepository.replaceMembers(interaction.guildId, userIds) await interaction.reply({ content: userIds.length + ' membros selecionados.', flags: 64 }) }) ``` ## Reply, update ou deferUpdate? | Método | Resultado | | --- | --- | | `interaction.update()` | Substitui a mensagem que contém o componente | | `interaction.reply()` | Cria uma resposta separada ao clique | | `interaction.deferUpdate()` | Confirma o clique sem mudar a mensagem | | Nenhum | O Core faz `deferUpdate()` automaticamente | ### ação demorada ```text .setCallback(async interaction => { await interaction.deferReply({ flags: 64 }) const result = await runLongOperation() await interaction.editReply( result.ok ? 'Operação concluída.' : 'Não foi possível concluir.' ) }) ``` ## Autorização continua sendo sua regra O runtime resolve o callback, mas a permissão de negócio precisa ser verificada no handler. ### callback protegido ```text .setCallback(async interaction => { const member = interaction.member const canManage = member?.permissions?.has('ManageGuild') if (!canManage) { await interaction.reply({ content: 'Você não pode executar esta ação.', flags: 64 }) return } await resetGuildConfiguration(interaction.guildId) await interaction.update({ content: 'Configuração restaurada.', components: [] }) }) ``` - Valide permissões no momento do clique, não apenas ao montar a mensagem. - Nunca confie em IDs ou valores recebidos sem validar existência e escopo. - Para fluxos privados, prefira respostas efêmeras iniciadas pelo próprio usuário. - Evite callbacks duplicados para o mesmo `customId` em módulos diferentes. --- # Prompts > Crie confirmações reutilizáveis, páginas de resultado e instâncias isoladas que funcionam tanto em slash quanto em prefixo. - Área: Componentes - URL humana: https://ninenity.vercel.app/doc/prompts - URL Markdown: https://ninenity.vercel.app/doc/markdown/prompts - Pacotes e conceitos: Prompt, registerPrompt, promptRequest ## Definição e solicitação são separadas - **Defina uma vez**: O módulo em `Discord/Interactions` monta o prompt e registra um ID estável. - **Solicite quando precisar**: Um comando chama `interaction.promptRequest(id)` para criar a instância. - **O Core isola**: Autor, guild e runtime são vinculados para impedir que outra pessoa controle o fluxo. - **Resolva a escolha**: O botão atualiza para sua página de resultado e executa o callback associado. > **Por que registrar antes?:** O auto-loader importa `Discord/Interactions` no boot. Quando o comando é executado, a definição já existe e pode ser instanciada sem remontar toda a configuração. ## Prompt simples com opções automáticas `Prompt.create()` adiciona uma linha de botões com base nas opções informadas. ### Discord/Interactions/DeletePrompt.ts ```text import { ButtonStyle, ContainerBuilder, MessageBuilder, Prompt, TextDisplayBuilder } from '@ninenity/componentbuilder' import Client from '../../Shared/Settings/Client' const prompt = Prompt.create({ id: 'delete-prompt', prompt: new MessageBuilder() .addComponents( new ContainerBuilder() .setAccentColor(0xff7582) .addTextDisplayComponents( new TextDisplayBuilder().setContent( '## Excluir projeto? Esta ação não poderá ser desfeita.' ) ) ) .template(), options: [ { id: 'confirm', label: 'Excluir', style: ButtonStyle.Danger }, { id: 'cancel', label: 'Cancelar', style: ButtonStyle.Secondary } ] }) Client.registerPrompt('delete-prompt', prompt) ``` Sem páginas ou callbacks, uma opção apenas conclui o fluxo. Para executar uma regra e mostrar um resultado diferente, use páginas exportadas ou botões com callback como no próximo exemplo. ## Prompt com páginas de resultado A primeira página contém os botões; páginas chamadas `yes` e `no` são escolhidas pelo sufixo do `customId`. ### Discord/Interactions/PublishPrompt.ts ```text const promptPage = new MessageBuilder() .addComponents( new ContainerBuilder() .setAccentColor(0xffc14e) .addTextDisplayComponents( new TextDisplayBuilder().setContent('Deseja publicar esta versão?') ) .addActionRowComponents( new ActionRowBuilder().addComponents( new ButtonBuilder() .setStyle(ButtonStyle.Success) .setLabel('Publicar') .setCustomId('publish-prompt:yes') .setCallback(async interaction => { await publishVersion(interaction.guildId) console.log('[Prompt] versão publicada') }), new ButtonBuilder() .setStyle(ButtonStyle.Secondary) .setLabel('Agora não') .setCustomId('publish-prompt:no') ) ) ) .template() const successPage = new MessageBuilder() .addComponents(new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent('✓ Versão publicada com sucesso.') )) .template() const canceledPage = new MessageBuilder() .addComponents(new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent('Publicação cancelada.') )) .template() Client.registerPrompt('publish-prompt', [ { id: 'prompt', name: 'prompt', template: promptPage }, { id: 'result-yes', name: 'yes', template: successPage }, { id: 'result-no', name: 'no', template: canceledPage } ]) ``` > **Integração com Builder Studio:** A exportação de múltiplas páginas já produz essa lista. Nomeie as páginas de destino com o mesmo sufixo dos botões, por exemplo `yes`, `no`, `approve` ou `cancel`. ## Solicitar no comando O mesmo ID registrado funciona em qualquer interação normalizada pelo Core. ### Discord/Commands/Publish.ts ```text Client.slash({ data: new SlashCommandBuilder() .setName('publish') .setDescription('Confirma a publicação.'), execute: async interaction => { await interaction.promptRequest('publish-prompt', { ephemeral: true }) } }) Client.prefix({ data: new PrefixCommandBuilder() .setName('publish') .setAliases(['publicar']), execute: async interaction => { await interaction.promptRequest('publish-prompt', { ephemeral: true }) } }) ``` Cada chamada cria uma instância com o autor e a guild da interação. O erro `Prompt "id" is not registered` indica que o módulo não foi carregado, o ID diverge ou o arquivo está fora das pastas percorridas. ## Definição dinâmica por usuário Registre uma factory quando o texto ou as páginas dependem da interação que abriu o prompt. ### prompt factory ```text Client.registerPrompt('remove-member', async context => { const targetId = context.interaction.options?.getUser('usuario')?.id const target = targetId ? await loadMember(targetId) : null const page = new MessageBuilder() .addComponents( new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( 'Remover **' + (target?.name ?? 'membro desconhecido') + '**?' ) ) ) .template() return Prompt.create({ id: 'remove-member', prompt: page, ownerId: context.ownerId, guildId: context.guildId }) }) ``` > **Não compartilhe estado mutável:** Crie os builders dentro da factory quando dados mudam por usuário. Uma definição global com variáveis externas mutáveis pode vazar conteúdo entre duas solicitações simultâneas. --- # Paginação > Crie paginação estática, dinâmica ou híbrida e personalize completamente os botões de navegação. - Área: Componentes - URL humana: https://ninenity.vercel.app/doc/paginacao - URL Markdown: https://ninenity.vercel.app/doc/markdown/paginacao - Pacotes e conceitos: Static, Dynamic, Hybrid, Custom controls ## Escolha o tipo certo | Tipo | Fonte | Melhor uso | | --- | --- | --- | | Estática | Lista pronta de templates | Tutoriais, painéis e páginas com layout diferente | | Dinâmica | Array dividido por `pageSize` | Usuários, logs, ranking e resultados de banco | | Híbrida | Introdução fixa + array dinâmico | Catálogo com capa ou instruções antes dos itens | > **Estado é zero-based internamente:** `pageIndex` começa em 0; o valor amigável `context.page` começa em 1. Use cada um para sua finalidade e evite subtrair manualmente em vários lugares. ## Paginação estática Registre uma lista quando cada página já é conhecida durante o carregamento. ### Discord/Interactions/GuidePages.ts ```text const page = (title: string, content: string) => new MessageBuilder() .addComponents( new ContainerBuilder() .setAccentColor(0xa571f4) .addTextDisplayComponents( new TextDisplayBuilder().setContent( '## ' + title + ' ' + content + ' Página ${pages.current}/${pages.total}' ) ) ) .template() Client.registerPaginator('guide-pages', [ { id: 'intro', name: 'Introdução', template: page('Introdução', 'Como usar o painel.') }, { id: 'config', name: 'Configuração', template: page('Configuração', 'Escolha seus canais.') }, { id: 'finish', name: 'Finalização', template: page('Tudo pronto', 'Revise e confirme.') } ]) ``` ### Discord/Commands/Guide.ts ```text Client.slash({ data: new SlashCommandBuilder() .setName('guide') .setDescription('Abre o guia paginado.'), execute: async interaction => { await interaction.paginatorRequest('guide-pages', { ephemeral: true, pageIndex: 0 }) } }) ``` ## Paginação dinâmica O paginator divide `data`, chama `render` para a fatia atual e injeta o contexto da página. ### Discord/Interactions/UserPages.ts ```text type UserRecord = { id: string name: string level: number } const users: UserRecord[] = await userRepository.list() const paginator = Paginator.dynamic({ id: 'users-pages', data: users, pageSize: 5, render: async (items, context) => { const rows = items.map((user, index) => (context.pageIndex * 5 + index + 1) + '. **' + user.name + '** — nível ' + user.level ) return new MessageBuilder() .addComponents( new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( '## Usuários ' + rows.join(' ') + ' Página ' + context.page + '/' + context.pageCount ) ) ) .template() } }) Client.registerPaginator('users-pages', paginator) ``` | Contexto | Valor | | --- | --- | | `items` | Itens da página atual | | `page` | Número amigável, começando em 1 | | `pageIndex` | Índice interno, começando em 0 | | `pageCount` | Quantidade total de páginas | | `totalItems` | Quantidade total de registros | ## Dados novos a cada solicitação Use uma definição factory para consultar banco ou API quando o usuário abre a paginação, não durante o boot. ### paginator factory ```text Client.registerPaginator('audit-pages', async context => { const guildId = context.guildId const records = guildId ? await auditRepository.list(guildId) : [] return Paginator.dynamic({ id: 'audit-pages', data: records, pageSize: 10, ownerId: context.ownerId, guildId, render: (items, page) => new MessageBuilder() .addComponents( new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( '## Auditoria ' + items.map(item => '- ' + item.summary).join(' ') + ' ' + page.page + '/' + page.pageCount ) ) ) .template() }) }) ``` > **Snapshot consistente:** Os dados são carregados uma vez ao abrir e mantidos durante aquela instância. Isso evita páginas mudarem de posição no meio da navegação. Para dados realmente vivos, crie uma ação explícita de atualizar. ## Paginação híbrida A primeira página é fixa; as seguintes vêm de uma coleção dinâmica. ### catalog-paginator.ts ```text const intro = new MessageBuilder() .addComponents( new ContainerBuilder() .setAccentColor(0x6485ff) .addTextDisplayComponents( new TextDisplayBuilder().setContent( '## Catálogo Ninenity Use **Próxima** para explorar os projetos.' ) ) ) .template() const paginator = Paginator.hybrid({ id: 'catalog-pages', intro, data: projects, pageSize: 3, render: (items, context) => new MessageBuilder() .addComponents( new ContainerBuilder().addTextDisplayComponents( new TextDisplayBuilder().setContent( items.map(project => '### ' + project.name + ' ' + project.summary).join(' ') + ' Página ' + context.page + '/' + context.pageCount ) ) ) .template() }) Client.registerPaginator('catalog-pages', paginator) ``` ## Botões de paginação personalizados Inclua uma action row no próprio template e marque a ação de cada botão. O runtime detecta os controles e não adiciona uma segunda linha. ### custom-controls.ts ```text const controls = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('guide-pages:first') .setLabel('Primeira') .setEmoji('⏮') .setStyle(ButtonStyle.Secondary) .setPaginationAction('first'), new ButtonBuilder() .setCustomId('guide-pages:back') .setLabel('Voltar') .setEmoji('◀') .setStyle(ButtonStyle.Secondary) .setPaginationAction('back'), new ButtonBuilder() .setCustomId('guide-pages:next') .setLabel('Próxima') .setEmoji('▶') .setStyle(ButtonStyle.Primary) .setPaginationAction('next'), new ButtonBuilder() .setCustomId('guide-pages:last') .setLabel('Última') .setEmoji('⏭') .setStyle(ButtonStyle.Secondary) .setPaginationAction('last') ) const page = new MessageBuilder() .addComponents( new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent( 'Página ${pages.current} de ${pages.total}' ) ) .addActionRowComponents(controls) ) .template() ``` - O prefixo do `customId` deve ser exatamente o ID registrado no paginator. - `back` e `previous` são entendidos como a página anterior; no builder use `setPaginationAction('back')`. - Use `first` e `last` apenas quando a quantidade de páginas justifica esses atalhos. - O runtime hidrata os tokens `pages.current` e `pages.total` antes de enviar. ## Personalizar rótulos automáticos Se as páginas não incluem controles, `Paginator` cria a linha e você pode trocar os rótulos. ### automatic-controls.ts ```text const paginator = Paginator.static({ id: 'help-pages', pages: [introPage, commandsPage, settingsPage], controls: { first: 'Início', previous: 'Voltar', next: 'Avançar', last: 'Fim' } }) Client.registerPaginator('help-pages', paginator) ``` > **Não duplique controles:** Escolha uma estratégia: controles gerados pelo Paginator ou action row customizada no template. Se a linha personalizada usar outro ID, ela será tratada como callback comum e a linha automática também aparecerá. --- # Configuração > Leia variáveis, canais e cargos globais ou específicos de cada guild sem espalhar IDs pelo código. - Área: Operação - URL humana: https://ninenity.vercel.app/doc/configuracao - URL Markdown: https://ninenity.vercel.app/doc/markdown/configuracao - Pacotes e conceitos: appConfig, Channels, Roles, HUB ## De onde vem a configuração O Client começa com defaults locais e pode ser hidratado pela API quando as credenciais estão presentes. 1. **Defaults locais** — O objeto `config` passado ao `LibsClient` garante valores básicos mesmo sem API. 2. **Hidratação** — Com `API_KEY` e `API_SECRET`, o Core solicita a configuração persistida do bot. 3. **Resolução por guild** — `appConfig(guildId?)` mescla a base global com a entrada em `guilds[guildId]`. 4. **Contexto automático** — Em comandos, eventos, callbacks e tasks por guild, o ID atual é resolvido pelo runtime. > **Sem guild, retorno seguro e vazio:** Fora de um contexto de servidor, chame `Client.appConfig(guildId)`. Sem contexto e sem ID explícito, o Core não escolhe uma guild arbitrária. ## Estrutura persistida A configuração base e as sobrescritas de cada guild seguem o mesmo formato. ### exemplo conceitual de AppConfig ```json { "channels": { "support": "123456789012345678" }, "roles": { "staff": "223456789012345678" }, "variables": { "general": { "prefix": { "type": "string", "value": "!" }, "maxTickets": { "type": "number", "value": 3 }, "features": { "type": "list", "value": ["tickets", "logs"] } } }, "guilds": { "323456789012345678": { "channels": { "support": "423456789012345678" }, "variables": { "general": { "prefix": { "type": "string", "value": "?" } } } } } } ``` | Tipo | Valor resolvido | | --- | --- | | `string` | Texto | | `number` | Número, sem conversão para string | | `object` | Objeto JSON de chave e valor | | `list` | Array JSON ordenado | ## Ler variáveis no contexto atual O runtime remove os wrappers `type/value`; sua lógica recebe os valores puros. ### configuração dentro de comando ```text Client.slash({ data: new SlashCommandBuilder() .setName('settings') .setDescription('Mostra as configurações atuais.'), execute: async interaction => { const config = Client.appConfig() const general = config.variables?.general ?? {} await interaction.reply({ content: [ 'Prefixo: ' + (general.prefix ?? '!'), 'Máximo de tickets: ' + (general.maxTickets ?? 1), 'Recursos: ' + (general.features ?? []).join(', ') ].join(' '), flags: 64 }) } }) ``` ### fora de uma interação ```text async function refreshGuild(guildId: string) { const config = Client.appConfig(guildId) const enabled = config.variables?.automation?.enabled ?? false if (!enabled) return await synchronizeGuild(guildId, config) } ``` ## Aliases de canais e cargos Aliases tornam o código legível e permitem trocar IDs no HUB sem editar o bot. ### uso de aliases ```text const channels = Client.getAppChannels() const roles = Client.getAppRoles() const supportChannel = channels.support const staffRole = roles.staff if (supportChannel) { console.log( '[Config] support=' + supportChannel.Name + ' id=' + supportChannel.Id ) } await interaction.reply({ content: [ 'Suporte: ' + (supportChannel?.Mention ?? 'não configurado'), 'Equipe: ' + (staffRole?.Mention ?? 'não configurado') ].join(' '), flags: 64 }) ``` | Campo | Canal | Cargo | | --- | --- | --- | | ID | `Id` | `Id` | | Nome | `Name` | `Name` | | Menção | `Mention` / `toString()` | `Mention` / `toString()` | | Metadado | `Type` | `Color` e `Position` | ## Defaults locais Defaults permitem desenvolver sem depender da API e documentam o formato esperado. ### Shared/Settings/Client.ts ```text const Client = new LibsClient({ intents: [GatewayIntentBits.Guilds], token: process.env.BOT_TOKEN, config: { channels: {}, roles: {}, variables: { general: { prefix: { type: 'string', value: '!' }, maxTickets: { type: 'number', value: 1 }, features: { type: 'list', value: [] } } } } }) ``` > **Defaults não são secrets:** Tokens, senhas e chaves de API pertencem ao `.env` ou a um cofre de secrets. A configuração do bot é adequada para comportamento, aliases e valores operacionais. --- # Multi-guild > Mantenha configuração, recursos, filas e interações independentes quando o mesmo bot atende vários servidores. - Área: Operação - URL humana: https://ninenity.vercel.app/doc/multi-guild - URL Markdown: https://ninenity.vercel.app/doc/markdown/multi-guild - Pacotes e conceitos: Context isolation, Guild resources, Scoped runtime ## O problema que o escopo resolve Sem isolamento, duas guilds podem disputar a mesma configuração, fila ou instância de componente. - **Configuração**: Cada guild possui canais, cargos e variáveis próprios. - **Concorrência**: Filas lógicas recebem a guild na chave interna quando há contexto. - **Interações**: Prompts e paginações validam autor, guild e Client responsável. > **Com contexto, a API continua simples:** Dentro de comandos, eventos, callbacks, modais e tasks por guild, continue usando `Client.appConfig()` sem argumentos. O runtime mantém a guild ativa durante a execução. ## Contexto automático ### mesma função em qualquer guild ```text async function sendSupportPanel() { const config = Client.appConfig() const channel = Client.getAppChannels().support const staff = Client.getAppRoles().staff if (!channel) return await sendPanel(channel.Id, { title: config.variables?.tickets?.title ?? 'Suporte', staffRoleId: staff?.Id }) } Client.on(Events.GuildMemberAdd, { execute: async member => { // O evento ativa o contexto de member.guild.id. await sendSupportPanel() } }) ``` A função `sendSupportPanel` não recebe `guildId`, porque foi chamada durante um evento contextualizado. Se a mesma função também rodar em um script administrativo sem contexto, passe o ID explicitamente em sua assinatura. ## Contexto explícito Jobs externos, scripts e callbacks fora do runtime precisam indicar a guild que desejam ler. ### sincronização externa ```text async function synchronizeAllGuilds() { for (const guild of Client.guilds.cache.values()) { const config = Client.appConfig(guild.id) const channels = Client.getAppChannels(guild.id) const roles = Client.getAppRoles(guild.id) await syncGuild({ guildId: guild.id, auditChannelId: channels.audit?.Id, staffRoleId: roles.staff?.Id, options: config.variables?.sync ?? {} }) } } ``` > **Nunca use a primeira guild do cache como fallback:** `Client.guilds.cache.first()` torna o comportamento dependente da ordem de conexão. Se a operação é por guild, o ID deve vir do contexto, argumento ou dado persistido. ## Filas e cooldowns O Core acrescenta o escopo quando a chamada ocorre dentro de uma execução contextualizada. ### fila por recurso e guild ```text Client.slash({ data: new SlashCommandBuilder() .setName('sync') .setDescription('Sincroniza o painel.'), cooldown: '15s', execute: async interaction => { await interaction.deferReply({ flags: 64 }) await Client.queue('panel-sync', async () => { await syncPanel(interaction.guildId) }) await interaction.editReply('Painel sincronizado.') } }) ``` Duas execuções de `panel-sync` na mesma guild entram na mesma fila. A mesma chave em outra guild segue independentemente. O cooldown de comando também considera a guild para não bloquear o usuário em todos os servidores. ## Prompts e paginações escopados Cada solicitação registra uma instância temporária vinculada ao autor e à guild. ### factory escopada ```text Client.registerPaginator('members-pages', async context => { const members = context.guildId ? await memberRepository.list(context.guildId) : [] return Paginator.dynamic({ id: 'members-pages', ownerId: context.ownerId, guildId: context.guildId, data: members, pageSize: 10, render: renderMembersPage }) }) ``` - Outro usuário não consegue navegar pela instância aberta pelo autor. - Um componente enviado em uma guild não é resolvido pela instância de outra guild. - Dois Clients no mesmo processo usam namespaces de runtime separados. - Dados retornados por uma factory devem ser consultados usando `context.guildId`. ## Checklist multi-guild - Toda tabela ou coleção persistente inclui `guildId` quando os dados pertencem ao servidor. - Caches criados pela aplicação usam `Map` ou uma chave composta. - IDs de canais e cargos vêm de aliases resolvidos, não de constantes globais. - Factories de prompts e paginações usam `context.guildId`. - Tasks de escopo `guild` não gravam o resultado em uma variável global única. - Logs de operação incluem a guild quando isso ajuda a diagnosticar a origem. --- # Logs e diagnóstico > Produza logs úteis com console.log(), aplique cor somente quando ela melhora a leitura e evite flood em rotinas repetitivas. - Área: Operação - URL humana: https://ninenity.vercel.app/doc/logs - URL Markdown: https://ninenity.vercel.app/doc/markdown/logs - Pacotes e conceitos: console.log, Color, Contexto, Anti-flood ## Use console.log() como saída O código da aplicação não precisa de `Logger.info()`, `Logger.warn()` ou `Logger.error()`. ### logs diretos ```text console.log('[Commands] comando registrado: ping') console.log('[Database] conexão estabelecida') console.log('[API] requisição falhou: status=502') ``` > **Uma convenção simples:** Comece com uma tag curta, descreva o evento e adicione somente os campos que ajudam a reproduzir ou medir o resultado. ## Color para ênfase visual `Color` formata texto ANSI; `console.log()` continua sendo o responsável por imprimir. ### Color.ts ```text import { Color } from '@ninenity/core' console.log(Color.green('[Database] conectado').bold()) console.log(Color.yellow('[WebSocket] reconectando')) console.log(Color.red('[API] falha ao buscar configuração').bold()) // Forma funcional console.log(Color('Deploy concluído', 'lightGreen', 'bold')) // Chain vazia com text no final console.log(Color().lightBlue().bold().text('[Core] pronto')) ``` | Cores | Estilos | | --- | --- | | `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray` | `bold`, `dim`, `italic`, `underline` | | `lightRed`, `lightGreen`, `lightYellow`, `lightBlue` | `strikethrough`, `inverse`, `hidden`, `visible` | > **Cor não substitui contexto:** Um terminal inteiramente colorido é tão difícil de ler quanto um sem hierarquia. Reserve cor para estados ou tags importantes e mantenha o corpo neutro quando possível. ## Inclua contexto útil Formato consistente torna busca, dashboard e investigação muito mais simples. ### contextual logging ```text function logCommand(input: { command: string guildId?: string | null userId: string durationMs: number }) { console.log( '[Command] name=' + input.command + ' guild=' + (input.guildId ?? 'dm') + ' user=' + input.userId + ' duration=' + input.durationMs + 'ms' ) } logCommand({ command: interaction.commandName, guildId: interaction.guildId, userId: interaction.user.id, durationMs: Date.now() - startedAt }) ``` - Inclua o módulo ou domínio na tag: `[Prompt]`, `[Task:catalog]`, `[Database]`. - Use pares `chave=valor` para IDs, duração, quantidade e estado. - Não registre tokens, cookies, API secrets, conteúdo privado ou payloads completos de usuários. - Para erros, registre a mensagem e o ponto da operação; stack completa apenas quando realmente necessária. ## Evite flood em reconexões e loops A operação continua em frequência normal, mas a comunicação ao usuário pode ser agregada. ### aviso agregado de WebSocket ```text const REPORT_INTERVAL = 10 * 60 * 1000 let lastReportAt = 0 let failedAttempts = 0 function onWebSocketFailure(message: string) { failedAttempts += 1 const now = Date.now() if (lastReportAt && now - lastReportAt < REPORT_INTERVAL) return lastReportAt = now console.log( '[WebSocket] conexão indisponível; reconexão segue em segundo plano' + ' attempts=' + failedAttempts + ' lastError=' + message ) failedAttempts = 0 } ``` A função de reconexão pode continuar com backoff próprio a cada poucos segundos. O intervalo de dez minutos limita apenas o aviso visível, não a tentativa técnica. ### resumo de lote ```text const results = await Promise.allSettled(items.map(processItem)) const failed = results.filter(result => result.status === 'rejected') console.log( '[Import] total=' + results.length + ' success=' + (results.length - failed.length) + ' failed=' + failed.length ) ``` ## Diagnóstico em produção | Sinal | Pergunta que responde | | --- | --- | | Boot concluído | O auto-loader e os plugins terminaram? | | Comandos registrados | Quantos módulos foram aceitos por guild/global? | | Duração | A operação ficou lenta ou expirou? | | Guild e usuário | Qual contexto reproduz o problema? | | Tentativas agregadas | Uma integração está instável sem inundar o terminal? | > **Logs contam a história, métricas mostram a tendência:** Use logs para eventos discretos e diagnóstico. Use telemetria para volume, latência, memória, CPU e taxa de falhas ao longo do tempo. --- # Solução de problemas > Diagnostique módulos ausentes, workspaces duplicados, comandos que não aparecem e definições de prompt ou paginação não registradas. - Área: Operação - URL humana: https://ninenity.vercel.app/doc/solucao-de-problemas - URL Markdown: https://ninenity.vercel.app/doc/markdown/solucao-de-problemas - Pacotes e conceitos: MODULE_NOT_FOUND, Workspace, Registration ## Cannot find module @ninenity/... O nome do import precisa combinar com o `name` do package e o workspace precisa estar mapeado pelo npm. 1. **Confira o pacote** — Abra o `package.json` da lib e confirme que `name` usa exatamente `@ninenity/nome`. 2. **Confira dependências** — O package consumidor deve listar a lib em `dependencies` ou o monorepo precisa expor o workspace. 3. **Reinstale links** — Execute `npm install` na raiz para recriar os links simbólicos em `node_modules/@ninenity`. 4. **Valide exports** — O `main`, `types` e `exports` da lib precisam apontar para arquivos que realmente existem após o build. ### checagens na raiz ```powershell npm query .workspace npm ls @ninenity/core @ninenity/componentbuilder npm run build --workspace=@ninenity/core ``` > **Renomear imports não renomeia pacotes:** Trocar `@tefutaki/...` por `@ninenity/...` no código exige também atualizar `package.json`, lockfile e qualquer alias de TypeScript ou bundler. ## Multiple workspaces with the same name O npm encontrou dois `package.json` com o mesmo campo `name` dentro dos padrões de workspace. ### localizar nomes duplicados ```powershell Get-ChildItem -Recurse -Filter package.json | Where-Object { $_.FullName -notmatch 'node_modules' } | ForEach-Object { $package = Get-Content -Raw $_.FullName | ConvertFrom-Json [PSCustomObject]@{ Name = $package.name; Path = $_.DirectoryName } } | Group-Object Name | Where-Object Count -gt 1 ``` Dê um nome único para cada app, mesmo que um seja apenas template. Por exemplo, `@ninenity/template-bot` e `@ninenity/example-bot`. Depois execute `npm install` na raiz para atualizar o lockfile. ## Slash funciona, prefixo não Slash e prefix são registros separados, mesmo quando compartilham o executor. - Confirme que existe uma chamada `Client.prefix()` para aquele nome. - O Client precisa do intent `GuildMessages` e, para ler texto, `MessageContent`. - Habilite Message Content Intent no Discord Developer Portal. - Confira o prefixo configurado e se o bot pode ver/enviar mensagens no canal. - Para subcomandos, confirme a sintaxe esperada pelo `PrefixCommandBuilder`. ### executor compartilhado ```text const execute = async (interaction: InputCommandInteraction) => { await interaction.reply('Fluxo disponível em slash e prefixo.') } Client.slash({ data: new SlashCommandBuilder().setName('prompt').setDescription('Abre o prompt.'), execute }) Client.prefix({ data: new PrefixCommandBuilder().setName('prompt'), execute }) ``` ## Prompt ou paginator is not registered A solicitação chegou antes de existir uma definição com o mesmo ID no namespace do Client. | Causa | Correção | | --- | --- | | ID diferente | Compare exatamente `registerPrompt('id')` e `promptRequest('id')` | | Arquivo fora do loader | Mova o registro para `Discord/Interactions` ou importe o módulo explicitamente | | Arquivo não está no dist | Preserve a árvore `src/Discord` no build | | Dois Clients | Registre a definição no mesmo Client que executa o comando | | Exceção no módulo | Leia o primeiro erro do boot; a importação pode ter parado antes do registro | ### IDs alinhados ```text // Discord/Interactions/Prompt.ts Client.registerPrompt('example-prompt', promptPages) // Discord/Commands/Prompt.ts await interaction.promptRequest('example-prompt', { ephemeral: true }) ``` ## Mensagem Components V2 rejeitada - Inclua `MessageFlags.IsComponentsV2` no payload final. - Passe `template.document()` por `toDiscordMessagePayload()`. - Não aninhe containers e não misture select com outros itens na mesma action row. - Mantenha até 5 botões por linha, 10 itens por galeria e IDs interativos únicos. - Botão de link usa URL e não deve ter `customId` ou callback. ### envio correto ```text await interaction.reply({ ...toDiscordMessagePayload(template.document()), flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral }) ``` ## Ordem de diagnóstico 1. **Leia o primeiro erro** — Erros seguintes costumam ser consequência da primeira importação ou configuração inválida. 2. **Reduza para um módulo** — Teste um comando ou template mínimo para separar infraestrutura de regra de negócio. 3. **Valide TypeScript** — Execute o check do package antes de iniciar o runtime. 4. **Valide o build** — Confirme que os arquivos carregados existem na árvore compilada. 5. **Só então limpe cache** — Reinstalar tudo é último recurso; primeiro descubra qual contrato está quebrado. --- # Referência rápida > Consulte os registradores, tipos, builders e métodos mais usados do Core e do ComponentBuilder em um único lugar. - Área: Operação - URL humana: https://ninenity.vercel.app/doc/referencia - URL Markdown: https://ninenity.vercel.app/doc/markdown/referencia - Pacotes e conceitos: Core API, ComponentBuilder API, Cheat sheet ## Registros do Client | API | Responsabilidade | | --- | --- | | `Client.slash(definition)` | Registra comando slash e deploy de application command | | `Client.prefix(definition)` | Registra comando por prefixo, aliases, opções e grupos | | `Client.contextMenu(definition)` | Registra menu de usuário ou mensagem | | `Client.modal(definition)` | Registra submit handler por `customId` | | `Client.on(event, definition)` | Executa em toda emissão do evento | | `Client.once(event, definition)` | Executa somente na primeira emissão | | `Client.task(definition)` | Registra rotina periódica global ou por guild | | `Client.registerPrompt(id, definition)` | Registra prompt reutilizável ou factory | | `Client.registerPaginator(id, definition)` | Registra páginas, Paginator ou factory | ## Runtime e operação do Client | API | Retorno / efeito | | --- | --- | | `Client.appConfig(guildId?)` | Configuração resolvida; valores de variáveis já normalizados | | `Client.getAppChannels(guildId?)` | Mapa de aliases para `ChannelData` | | `Client.getAppRoles(guildId?)` | Mapa de aliases para `RoleData` | | `Client.cooldown(duration?)` | Define ou consulta cooldown no contexto atual | | `Client.globalCooldown(duration)` | Define cooldown global do usuário | | `Client.queue(key, task)` | Serializa uma operação e retorna o resultado | | `Client.runTask(id)` | Executa uma task registrada imediatamente | | `Client.startTask(id)` | Inicia ou reativa o timer | | `Client.stopTask(id)` | Interrompe o timer no Client atual | | `Client.login()` | Conecta ao Discord e inicia o bootstrap | | `Client.destroy()` | Limpa componentes, tasks, plugins e conexão | ## Interação normalizada | Propriedade / método | Uso | | --- | --- | | `user`, `member`, `guild`, `guildId`, `channel` | Identidade e contexto | | `options` | Opções de slash/prefix e subcomandos | | `reply(payload)` | Resposta inicial | | `deferReply(payload?)` | Reserva resposta para trabalho demorado | | `editReply(payload)` | Edita a resposta original | | `followUp(payload)` | Envia resposta adicional | | `promptRequest(id, options?)` | Cria instância de prompt registrada | | `paginatorRequest(id, options?)` | Cria instância de paginator registrada | | `interaction` | Objeto original do Discord.js | | ComponentRequestOptions | Significado | | --- | --- | | `pageIndex` | Página inicial, começando em 0 | | `ephemeral` | Adiciona flag de resposta efêmera | | `flags` | Flags adicionais da mensagem | ## Builders de mensagem | Builder | Métodos principais | | --- | --- | | `MessageBuilder` | `setContent`, `addEmbeds`, `addComponents`, `setFlags`, `setAllowedMentions`, `build`, `template` | | `ContainerBuilder` | `setAccentColor`, `setSpoiler`, `setId`, `addComponents` e aliases tipados | | `TextDisplayBuilder` | `setContent`, `setId` | | `SeparatorBuilder` | `setDivider`, `setSpacing`, `setId` | | `SectionBuilder` | `setContent`, `addTextDisplayComponents`, `setAccessory`, `setButtonAccessory`, `setThumbnailAccessory` | | `ThumbnailBuilder` | `setURL`, `setDescription`, `setSpoiler`, `setId` | | `MediaGalleryBuilder` | `addItems`, `setId`, `build` | | `MediaGalleryItemBuilder` | `setURL`, `setDescription`, `setSpoiler`, `build` | | `FileBuilder` | `setURL`, `setSpoiler`, `setId` | | `ActionRowBuilder` | `addComponents`, `setId`, `build` | ## Builders interativos | Builder | Métodos principais | | --- | --- | | `ButtonBuilder` | `setCustomId`, `setCallback`, `setURL`, `setLabel`, `setEmoji`, `setStyle`, `setDisabled`, `setPaginationAction` | | `StringSelectMenuBuilder` | `setCustomId`, `setCallback`, `setPlaceholder`, `setMinValues`, `setMaxValues`, `addOptions` | | `UserSelectMenuBuilder` | Seleção nativa de usuários + métodos comuns de select | | `RoleSelectMenuBuilder` | Seleção nativa de cargos + métodos comuns de select | | `ChannelSelectMenuBuilder` | Seleção nativa de canais + métodos comuns de select | | `MentionableSelectMenuBuilder` | Seleção de usuários ou cargos + métodos comuns | | `SelectMenuOptionBuilder` | `setLabel`, `setValue`, `setDescription`, `setEmoji`, `setDefault` | ### assinatura de callback ```text .setCallback(async (interaction, id, context) => { context.customId // ID completo context.values // todos os valores de select context.value // primeiro valor, quando existe context.componentType // tipo recebido do Discord }) ``` ## ComponentTemplate | API | Uso | | --- | --- | | `ComponentTemplate.create(document, metadata?)` | Cria template a partir de documento | | `ComponentTemplate.from(input)` | Normaliza template, definição ou documento | | `ComponentTemplate.fromJSON(json)` | Importa representação JSON | | `template.document()` | Retorna cópia do documento | | `template.render(context)` | Hidrata tokens com um contexto | | `template.validate()` | Valida estrutura e limites | | `template.toJSON()` | Exporta representação JSON | | `template.toToken()` | Gera token assinado quando configurado | | `template.toUnsignedToken()` | Gera token sem assinatura para fluxo local | | `template.toTypeScript()` | Exporta uma representação TypeScript | ## Paginator e Prompt | API | Uso | | --- | --- | | `Paginator.static({ pages })` | Páginas prontas | | `Paginator.dynamic({ data, pageSize, render })` | Divide dados e renderiza cada fatia | | `Paginator.hybrid({ intro, data, pageSize, render })` | Introdução fixa seguida de dados | | `paginator.state(pageIndex?)` | Estado normalizado da página | | `paginator.render(pageIndex?)` | Template da página | | `paginator.renderDocument(pageIndex?)` | Documento pronto | | `Prompt.create(options)` | Prompt a partir de template e opções | | `Prompt.fromPages(pages, options?)` | Prompt roteado por páginas exportadas | | `prompt.render()` | Template da pergunta | | `prompt.resolveOption(customId)` | Resolve a opção pelo ID completo | | `prompt.resolvePage(optionId)` | Encontra página associada à opção | ## Imports rápidos ### core-imports.ts ```text import { Ninenity, LibsClient, Color, I18n, SlashCommandBuilder, PrefixCommandBuilder, ContextMenuCommandBuilder, GatewayIntentBits, Events, MessageFlags, type AppConfig, type InputCommandInteraction } from '@ninenity/core' ``` ### component-imports.ts ```text import { MessageBuilder, ContainerBuilder, TextDisplayBuilder, SeparatorBuilder, SectionBuilder, ThumbnailBuilder, MediaGalleryBuilder, FileBuilder, ActionRowBuilder, ButtonBuilder, StringSelectMenuBuilder, ButtonStyle, ComponentTemplate, Paginator, Prompt, toDiscordMessagePayload } from '@ninenity/componentbuilder' ``` ---