mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 13:21:31 +00:00
grammY-based bot with a module plugin system loaded from the MODULES env var. Three command visibility levels (public/protected/private) share a unified command namespace with conflict detection at registry build. - 4 initial modules (util, wordle, loldle, misc); util fully implemented, others are stubs proving the plugin system end-to-end - util: /info (chat/thread/sender ids) + /help (pure renderer over the registry, HTML parse mode, escapes user-influenced strings) - KVStore interface with CFKVStore and a per-module prefixing factory; getJSON/putJSON convenience helpers; other backends drop in via one file - Webhook at POST /webhook with secret-token validation via grammY's webhookCallback; no admin HTTP surface - Post-deploy register script (npm run deploy = wrangler deploy && node --env-file=.env.deploy scripts/register.js) for setWebhook and setMyCommands; --dry-run flag for preview - 56 vitest unit tests across 7 suites covering registry, db wrapper, dispatcher, help renderer, validators, and HTML escaper - biome for lint + format; phased implementation plan under plans/
6.0 KiB
6.0 KiB
Phase 05 — util module (/info, /help)
Context Links
- Plan: plan.md
- Phase 04: module framework
Overview
- Priority: P1
- Status: pending
- Description: fully-implemented
utilmodule with two public commands./inforeports chat/thread/sender IDs./helpiterates the registry and prints public+protected commands grouped by module.
Key Insights
/helpis a renderer over the registry — it does NOT hold its own command metadata. Single source of truth = registry.- Forum topics:
message_thread_idmay be absent for normal chats. Output "n/a" rather than omitting, so debug users know the field was checked. - Parse mode: HTML (decision locked). Easier escaping than MarkdownV2. Only 4 chars to escape:
&,<,>,". Write a smallescapeHtml()util. /helpmust access the registry. Use an exported getter fromsrc/modules/dispatcher.jsorsrc/modules/registry.jsthat returns the currently-built registry. The util module reads it inside its handler — not at module load time — so the registry exists by then.
Requirements
Functional
/inforeplies with:Plain text, no parse mode needed.chat id: 123 thread id: 456 (or "n/a" if undefined) sender id: 789/helpoutput grouped by module:<b>util</b> /info — Show chat/thread/sender IDs /help — Show this help <b>wordle</b> /wordle — Play wordle /wstats — Stats (protected) ...- Modules with zero visible commands omitted entirely.
- Private commands skipped.
- Protected commands appended with
" (protected)"suffix so users understand the distinction. - Module order: insertion order of
env.MODULES. - Sent with
parse_mode: "HTML".
- Both commands are public visibility.
Non-functional
src/modules/util/index.js< 150 LOC.- No new deps.
Architecture
src/modules/util/
├── index.js # module default export
├── info-command.js # /info handler
├── help-command.js # /help handler + HTML renderer
Split by command file for clarity. Each command file < 80 LOC.
Registry access: src/modules/registry.js exports getCurrentRegistry() returning the memoized instance (set by buildRegistry). /help calls this at handler time.
Related Code Files
Create
src/modules/util/index.jssrc/modules/util/info-command.jssrc/modules/util/help-command.jssrc/util/escape-html.js(shared escaper)
Modify
src/modules/registry.js— addgetCurrentRegistry()exported getter
Delete
- none
Implementation Steps
src/util/escape-html.js:export function escapeHtml(s) { return String(s) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); }src/modules/registry.js:- Add module-scope
let currentRegistry = null; buildRegistryassigns to it before returning.export function getCurrentRegistry() { if (!currentRegistry) throw new Error("registry not built yet"); return currentRegistry; }
- Add module-scope
src/modules/util/info-command.js:- Exports
{ name: "info", visibility: "public", description: "Show chat/thread/sender IDs", handler }. - Handler reads
ctx.chat?.id,ctx.message?.message_thread_id,ctx.from?.id. - Reply:
\chat id: ${chatId}\nthread id: ${threadId ?? "n/a"}\nsender id: ${senderId}``.
- Exports
src/modules/util/help-command.js:- Exports
{ name: "help", visibility: "public", description: "Show this help", handler }. - Handler:
const reg = getCurrentRegistry();- Build
Map<moduleName, string[]>of lines. - Iterate
reg.publicCommands+reg.protectedCommands(in insertion order;Mappreserves it). - For each entry, push
"/" + cmd.name + " — " + escapeHtml(cmd.description) + (visibility === "protected" ? " (protected)" : "")under its module name. - Iterate
reg.modulesin order; for each with non-empty lines, emit<b>${escapeHtml(moduleName)}</b>\n+ lines joined by\n+ blank line. await ctx.reply(text, { parse_mode: "HTML" });
- Exports
src/modules/util/index.js:import info from "./info-command.js"; import help from "./help-command.js";export default { name: "util", commands: [info, help] };
- Add
utiltowrangler.tomlMODULESdefault if not already:MODULES = "util,wordle,loldle,misc". - Lint.
Todo List
escape-html.jsgetCurrentRegistry()in registry.jsinfo-command.jshelp-command.jsrendererutil/index.js- Manual smoke test via
wrangler dev - Lint clean
Success Criteria
/infoin a 1:1 chat shows chat id + "thread id: n/a" + sender id./infoin a forum topic shows a real thread id./helpshowsutilsection with both commands, and stub module sections (after phase-06)./helpdoes NOT show private commands.- Protected commands show
(protected)suffix. - HTML injection attempt in module description (e.g.
<script>) renders literally.
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Registry not built before handler fires | Low | Med | getCurrentRegistry() throws with clear error; dispatcher ensures build before bot handles updates |
/help output exceeds Telegram 4096-char limit |
Low (at this scale) | Low | Phase-09 mentions future pagination; current scale is fine |
| Module descriptions contain raw HTML | Med | Med | escapeHtml all descriptions + module names |
Missing message_thread_id crashes |
Low | Low | ?? "n/a" fallback |
Security Considerations
- Escape ALL user-influenced strings (module names, descriptions) — even though modules are trusted code, future-proofing against dynamic registration.
/inforeveals sender id — that's the point. Document in help text that it's a debugging tool.
Next Steps
- Phase 06 adds the stub modules that populate the
/helpoutput. - Phase 08 tests
/helprendering against a synthetic registry.