mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 19:22:09 +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/
5.5 KiB
5.5 KiB
Phase 06 — Stub modules (wordle / loldle / misc)
Context Links
- Plan: plan.md
- Phase 04: module framework
Overview
- Priority: P2
- Status: pending
- Description: three stub modules proving the plugin system. Each registers one
public, oneprotected, and oneprivatecommand. All commands are slash commands; private ones are simply absent fromsetMyCommandsand/help. Handlers are one-liners that echo or reply.
Key Insights
- Stubs are NOT feature-complete games. Their job: exercise the plugin loader, visibility levels, registry, dispatcher, and
/helprendering. - Each stub module demonstrates DB usage via
getJSON/putJSONin one handler — provescreateStorenamespacing + JSON helpers work end-to-end. - Private commands follow the same slash-name rules as public/protected (
[a-z0-9_]{1,32}). They're hidden, not text-matched.
Requirements
Functional
wordlemodule:- public
/wordle→"Wordle stub — real game TBD." - protected
/wstats→await db.getJSON("stats")→ returns"games played: ${stats?.gamesPlayed ?? 0}". - private
/konami→"⬆⬆⬇⬇⬅➡⬅➡BA — secret wordle mode unlocked (stub)".
- public
loldlemodule:- public
/loldle→"Loldle stub." - protected
/lstats→"loldle stats stub". - private
/ggwp→"gg well played (stub)".
- public
miscmodule:- public
/ping→"pong"+await db.putJSON("last_ping", { at: Date.now() }). - protected
/mstats→const last = await db.getJSON("last_ping");→ echoeslast.ator"never". - private
/fortytwo→"The answer."(slash-command regex excludes bare numbers, so we spell it).
- public
Non-functional
- Each stub's
index.js< 80 LOC. - No additional utilities — stubs use only what phase-03/04/05 provide.
Architecture
src/modules/
├── wordle/
│ └── index.js
├── loldle/
│ └── index.js
└── misc/
└── index.js
Each index.js exports { name, commands, init } per the contract defined in phase 04. init stashes the injected db on a module-scope let so handlers can reach it.
Example shape (pseudo):
let db;
export default {
name: "wordle",
init: async ({ db: store }) => { db = store; },
commands: [
{ name: "wordle", visibility: "public", description: "Play wordle",
handler: async (ctx) => ctx.reply("Wordle stub — real game TBD.") },
{ name: "wstats", visibility: "protected", description: "Wordle stats",
handler: async (ctx) => {
const stats = await db.getJSON("stats");
await ctx.reply(`games played: ${stats?.gamesPlayed ?? 0}`);
} },
{ name: "konami", visibility: "private", description: "Easter egg — retro code",
handler: async (ctx) => ctx.reply("⬆⬆⬇⬇⬅➡⬅➡BA — secret wordle mode unlocked (stub)") },
],
};
Related Code Files
Create
src/modules/wordle/index.jssrc/modules/loldle/index.jssrc/modules/misc/index.js
Modify
- none (static map in
src/modules/index.jsalready lists all four — from phase 04)
Delete
- none
Implementation Steps
- Create
src/modules/wordle/index.jsper shape above (/wordle,/wstats,/konami). - Create
src/modules/loldle/index.js(/loldle,/lstats,/ggwp). - Create
src/modules/misc/index.js(/ping,/mstats,/fortytwo) including thelast_pingputJSON/getJSONdemonstrating DB usage. - Verify
src/modules/index.jsstatic map includes all three (added in phase-04). wrangler devsmoke test: send each command via a mocked Telegram update; verify routing and KV writes land in the preview namespace with prefixwordle:/loldle:/misc:.- Lint clean.
Todo List
wordle/index.jsloldle/index.jsmisc/index.js- Verify KV writes are correctly prefixed (check via
wrangler kv key list --preview) - Manual webhook smoke test for all 9 commands
- Lint clean
Success Criteria
- With
MODULES="util,wordle,loldle,misc"the bot loads all four modules on cold start. /helpoutput shows three stub sections (wordle, loldle, misc) with 2 commands each (public + protected), plusutilsection./helpdoes NOT list/konami,/ggwp,/fortytwo.- Telegram's
/menu (afterscripts/register.jsruns) shows/wordle,/loldle,/ping,/info,/help— nothing else. - Typing
/konamiin Telegram invokes the handler. Typing/Konamidoes NOT (Telegram + grammY match case-sensitively). /pingwriteslast_pingviaputJSON; subsequent/mstatsreads it back viagetJSON.- Removing
wordlefromMODULES(MODULES="util,loldle,misc") successfully boots without loading wordle;/wordlebecomes unknown command (grammY default: silently ignored).
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Stub handlers accidentally leak into production behavior | Low | Low | Stubs clearly labeled in reply text |
| Private command name too guessable | Low | Low | These are stubs; real easter eggs can pick obscure names |
| DB write fails silently | Low | Med | Handlers await writes; errors propagate to grammY error handler |
Security Considerations
- Stubs do NOT read user input for DB keys — they use fixed keys. Avoids injection.
/pingtimestamp is server time — no sensitive data.
Next Steps
- Phase 07 adds
scripts/register.jsto runsetWebhook+setMyCommandsat deploy time. - Phase 08 tests the full routing flow with these stubs as fixtures.