Skip to content

Latest commit

 

History

History
230 lines (182 loc) · 10.7 KB

ARCHITECTURE.md

File metadata and controls

230 lines (182 loc) · 10.7 KB

Arquitetura

Visão geral

O Codiconomy é um bot Discord, executado por Bun, escrito em TypeScript e integrado ao Discord por discord.js 14. O ponto de entrada é index.ts. Ele é o composition root: valida a configuração, abre o SQLite, monta o AppContext (banco, relógio, RNG, notificador, scheduler), cria o registry de comandos, registra os comandos na guild e só então faz login.

Princípios:

  • validar configuração cedo e falhar sem iniciar parcialmente o cliente;
  • registrar comandos da guild configurada antes do login;
  • manter o registry explícito e previsível;
  • regras de economia puras em src/shared-domain, sem Discord nem banco;
  • toda mudança de saldo passa por postTransaction (livro-razão de partidas dobradas); nunca há UPDATE direto de saldo;
  • relógio e RNG sempre injetados no domínio; Date.now() e Math.random() são proibidos fora das implementações de sistema (systemClock, systemRng);
  • valores monetários e XP sempre inteiros (centavos / milésimos);
  • logs JSON estruturados, sem segredos;
  • usar o mínimo de intents (Guilds).

Direção de dependências

index.ts
  -> interactions/handler.ts -> commands/types.ts          (despacho)
  -> commands/index.ts -> commands/<comando> -> commands/support
  -> jobs/{scheduler,handlers,maintenance}
  -> shared/{env,logger,notifier,rate-limit}
  -> database/*                -> drizzle-orm/bun-sqlite -> bun:sqlite

commands/<x>  -> database/<x>.ts, views/*, shared-domain/*
jobs/handlers -> database/*, views/*, shared-domain/*
views/*       -> shared-domain/*, discord.js (somente builders)
database/*    -> shared-domain/*  (regras puras; nunca o contrário)
shared-domain/* -> (sem dependências externas, exceto messages -> discord.js)

O bootstrap orquestra, mas não concentra regras de domínio. SQL, Drizzle e bun:sqlite ficam restritos a src/database.

Startup e ciclo de vida

  1. Cria o logger e registra discord.startup.started.
  2. Lê DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_GUILD_ID e SQLITE_DATABASE_PATH (nada de segredos nos logs).
  3. Abre uma conexão SQLite (openDatabase), faz SELECT 1 e assertMigrated (falha com instrução clara se as migrations não foram aplicadas com bun run db:migrate).
  4. Monta o AppContext e bootstrapDatabase: cria a temporada 1 na primeira execução, agenda o fim da temporada, semeia as manchetes iniciais, garante as notícias da janela atual e o job da próxima virada, e encerra quizzes deixados active por um restart.
  5. Inicia a manutenção (reconciliação do razão, de hora em hora).
  6. Cria o Client com GatewayIntentBits.Guilds e instala ClientReady e InteractionCreate.
  7. Registra os comandos por REST na guild antes do login.
  8. Faz login. Em ClientReady, o scheduler inicia: a primeira varredura executa tudo o que venceu enquanto o bot esteve offline.

SIGINT/SIGTERM e falhas de startup param manutenção e scheduler, fecham o banco e destroem o cliente de forma idempotente.

Interações

src/interactions/handler.ts recebe todo InteractionCreate e roteia:

Tipo Destino
Slash command command.execute
Autocomplete command.autocomplete
Botão ou modal command.component, pelo prefixo do customId (<comando>:…)

Antes de executar, aplica o rate limit por usuário (8 interações / 10 s, em memória). Registra discord.interaction.received, discord.command.execution.{started,completed,failed} e discord.interaction.completed com IDs e durationMs. Falhas respondem com uma mensagem genérica e efêmera, sem vazar estado interno. O handler de customId usa a convenção quiz:a:<sessão>:<posição>:<alternativa>, blackjack:<ação>:<sessão> e admin:qcreate|qedit:<id>.

Módulos

src/commands

Um diretório por comando, com createXCommand(ctx: AppContext). O registry (index.ts) lista todos explicitamente. commands/support concentra o guard requirePlayer (servidor, temporada ativa, cadastro, preso) e os helpers de resposta (replyError, readAmount). commands/admin divide o /admin por grupo (parametro, quiz-categoria, quiz-pergunta, noticia, evento, canal, temporada, cofre, dia). Comandos respondem pela interação (reply/editReply/followUp); mensagens fora de interação usam o Notifier.

src/shared

env.ts (configuração), logger.ts (JSON estruturado), notifier.ts (DMs, anúncios e edição de mensagens antigas; falhas são logadas, nunca propagadas) e rate-limit.ts.

src/shared-domain

Regras puras e testáveis com relógio/RNG falsos:

Pasta Conteúdo
money formatação/parse de CD$, percentuais
rng Rng injetável (systemRng, semente, sequência)
time Clock, dia do bot (janelas, virada, mudança pendente), cooldown, duração
params catálogo tipado, grupos, resolvedor admin > evento > notícia > padrão
ledger contas, validação de partida dobrada e construtores de lançamentos
skills barras, curva, XP por ação
work, quiz, crime, prison, investments, news, season regras de cada área
casino roleta, blackjack, caça-níqueis, corrida
messages createEmbed, sendEmbeds, Color, formatDiscordTimestamp

src/views

Builders de embeds e componentes (blackjack, corrida, quiz, notícias) usados tanto por comandos quanto por jobs. Dependem só do domínio e de discord.js.

src/jobs

  • scheduler.ts: varredura a cada 15 s + nudge (timer) para jobs que devem disparar perto do horário; as varreduras nunca se sobrepõem.
  • handlers.ts: um handler por kind de job; efeitos de Discord voltam em after, executados depois do commit.
  • maintenance.ts, race-animation.ts.

src/database

Única camada que conhece SQLite e Drizzle. Funções recebem um Executor (cliente ou transação). runInTransaction abre BEGIN IMMEDIATE (ou savepoint se já houver transação). Nunca use await nem chame o Discord dentro de uma transação. Módulos:

Módulo Responsabilidade
index.ts conexão única, tipos, runInTransaction, assertMigrated
ledger.ts postTransaction, saldos, extrato, reconcile
members.ts, users.ts identidade e /cadastrar
seasons.ts, season-lifecycle.ts, ranking.ts temporadas, wipe, ranking
settings.ts, params.ts, cooldowns.ts, audit.ts dia, parâmetros, cooldowns genéricos, auditoria
jobs.ts, day.ts, bootstrap.ts scheduler persistente, virada do dia, startup
bank.ts, work.ts, quiz.ts, crime.ts, prison.ts, invest.ts, casino.ts, race.ts, skills.ts, news.ts casos de uso

Modelo de dados

Os modelos Drizzle ficam em src/database/models/ e são exportados por src/database/schema/index.ts. Há uma única migration init.

Tabela Notas
users identidade Discord + membro (uma guild), registered_at
seasons, season_rankings no máx. uma active (índice único parcial)
accounts por temporada/dono/tipo; CHECK (balance >= 0) exceto emissao, conversao, cassino; frozen
ledger_transactions, ledger_entries idempotency_key único; amount <> 0; append-only
skills, skill_events CHECK (xp_milli BETWEEN 0 AND 100000) + trilha de auditoria
cooldowns (discord_id, action) -> available_at
parameter_overrides, admin_audit, settings configuração e auditoria
scheduled_jobs kind, due_at, idempotency_key único, done_at, attempts
quiz_* categorias, perguntas, sessões e perguntas da sessão (prazo, resposta, pagamento)
news_templates, daily_news, events notícias (com snapshot por janela) e eventos
criminal_state, prison_terms risco, última janela com tentativa, penas
investments principal, taxa congelada, vencimento, status
casino_rounds, blackjack_sessions, horse_races, horse_bets jogos e auditoria de RTP

Livro-razão

postTransaction({ key, kind, entries, now, … }):

  1. Se a chave já existe, devolve a transação original (replayed: true) sem efeito.
  2. Valida inteiros e soma zero por moeda (limpo e sujo nunca se compensam).
  3. Resolve contas de usuário (devem existir; seasonId explícito por conta só para virada de temporada) e do sistema (criadas sob demanda).
  4. Recusa débito em conta frozen (exceto allowFrozenDebit, usado na fiança) e saldo insuficiente.
  5. Grava a transação, as linhas e atualiza o cache accounts.balance, tudo na mesma transação SQLite.

reconcile() verifica soma zero por moeda, por transação e a igualdade entre o saldo em cache e a soma das linhas de cada conta; roda de hora em hora (ledger.reconcile.failed no log) e em toda a suíte de testes.

Scheduler persistente

scheduled_jobs guarda o que precisa acontecer no futuro. processDueJobs executa cada job vencido em uma transação própria: o efeito e o done_at são gravados juntos, então um job é aplicado exatamente uma vez, mesmo com o bot offline ou duas varreduras simultâneas. Uma falha reverte o efeito, incrementa attempts e tenta de novo; após 5 tentativas o job é encerrado com o erro.

Kind Chave Efeito
day.turn day:turn:{janela} risco, notícias, anúncio, próximo turno
prison.release prison:release:{termo} solta e avisa por DM
investment.mature investment:mature:{id} paga principal + rendimento
blackjack.timeout blackjack:timeout:{id} encerra a mão (parar)
race.start race:start:{id} larga a corrida
season.end season:end:{id} encerra a temporada

Registry e registro de comandos

const registry = createCommandRegistry(context); // AppContext

Para adicionar um comando é preciso importá-lo e incluí-lo na lista de createCommandRegistry. Não há descoberta automática. O REST.put publica a lista inteira na guild a cada startup, antes do login.

Mensagens, embeds e cores

createEmbed normaliza EmbedBuilder/dados em um EmbedBuilder; sendEmbeds envia a um canal com channel.send. A primeira resposta de um slash command usa a interação; sendEmbeds não a substitui.

Configuração, segurança e intents

Segredos e IDs vêm do ambiente validado por env.ts. O token é usado só para REST e login. O único intent é Guilds; não há intents privilegiados nem listeners de mensagens. DMs usam o REST do discord.js sem intent extra. O acesso administrativo é calculado de interaction.memberPermissions (Administrador ou Gerenciar Servidor).

Extensões futuras que exijam novos intents, tipos de interação ou módulos devem ser deliberadas e atualizar este documento.