Files
miti99bot/plans/260411-0853-telegram-bot-plugin-framework/phase-05-util-module.md
tiennm99 c4314f21df feat: scaffold plug-n-play telegram bot on cloudflare workers
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/
2026-04-11 09:49:06 +07:00

6.0 KiB

Phase 05 — util module (/info, /help)

Overview

  • Priority: P1
  • Status: pending
  • Description: fully-implemented util module with two public commands. /info reports chat/thread/sender IDs. /help iterates the registry and prints public+protected commands grouped by module.

Key Insights

  • /help is a renderer over the registry — it does NOT hold its own command metadata. Single source of truth = registry.
  • Forum topics: message_thread_id may 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 small escapeHtml() util.
  • /help must access the registry. Use an exported getter from src/modules/dispatcher.js or src/modules/registry.js that 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

  • /info replies with:
    chat id: 123
    thread id: 456  (or "n/a" if undefined)
    sender id: 789
    
    Plain text, no parse mode needed.
  • /help output 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.

Create

  • src/modules/util/index.js
  • src/modules/util/info-command.js
  • src/modules/util/help-command.js
  • src/util/escape-html.js (shared escaper)

Modify

  • src/modules/registry.js — add getCurrentRegistry() exported getter

Delete

  • none

Implementation Steps

  1. src/util/escape-html.js:
    export function escapeHtml(s) {
      return String(s)
        .replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .replaceAll('"', "&quot;");
    }
    
  2. src/modules/registry.js:
    • Add module-scope let currentRegistry = null;
    • buildRegistry assigns to it before returning.
    • export function getCurrentRegistry() { if (!currentRegistry) throw new Error("registry not built yet"); return currentRegistry; }
  3. 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}``.
  4. 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; Map preserves it).
      • For each entry, push "/" + cmd.name + " — " + escapeHtml(cmd.description) + (visibility === "protected" ? " (protected)" : "") under its module name.
      • Iterate reg.modules in 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" });
  5. src/modules/util/index.js:
    • import info from "./info-command.js"; import help from "./help-command.js";
    • export default { name: "util", commands: [info, help] };
  6. Add util to wrangler.toml MODULES default if not already: MODULES = "util,wordle,loldle,misc".
  7. Lint.

Todo List

  • escape-html.js
  • getCurrentRegistry() in registry.js
  • info-command.js
  • help-command.js renderer
  • util/index.js
  • Manual smoke test via wrangler dev
  • Lint clean

Success Criteria

  • /info in a 1:1 chat shows chat id + "thread id: n/a" + sender id.
  • /info in a forum topic shows a real thread id.
  • /help shows util section with both commands, and stub module sections (after phase-06).
  • /help does 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.
  • /info reveals 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 /help output.
  • Phase 08 tests /help rendering against a synthetic registry.