mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 15:20:58 +00:00
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/
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# Phase 06 — Stub modules (wordle / loldle / misc)
|
||||
|
||||
## Context Links
|
||||
- Plan: [plan.md](plan.md)
|
||||
- Phase 04: [module framework](phase-04-module-framework.md)
|
||||
|
||||
## Overview
|
||||
- **Priority:** P2
|
||||
- **Status:** pending
|
||||
- **Description:** three stub modules proving the plugin system. Each registers one `public`, one `protected`, and one `private` command. All commands are slash commands; private ones are simply absent from `setMyCommands` and `/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 `/help` rendering.
|
||||
- Each stub module demonstrates DB usage via `getJSON` / `putJSON` in one handler — proves `createStore` namespacing + 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
|
||||
- `wordle` module:
|
||||
- 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)"`.
|
||||
- `loldle` module:
|
||||
- public `/loldle` → `"Loldle stub."`
|
||||
- protected `/lstats` → `"loldle stats stub"`.
|
||||
- private `/ggwp` → `"gg well played (stub)"`.
|
||||
- `misc` module:
|
||||
- public `/ping` → `"pong"` + `await db.putJSON("last_ping", { at: Date.now() })`.
|
||||
- protected `/mstats` → `const last = await db.getJSON("last_ping");` → echoes `last.at` or `"never"`.
|
||||
- private `/fortytwo` → `"The answer."` (slash-command regex excludes bare numbers, so we spell it).
|
||||
|
||||
### 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):
|
||||
```js
|
||||
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.js`
|
||||
- `src/modules/loldle/index.js`
|
||||
- `src/modules/misc/index.js`
|
||||
|
||||
### Modify
|
||||
- none (static map in `src/modules/index.js` already lists all four — from phase 04)
|
||||
|
||||
### Delete
|
||||
- none
|
||||
|
||||
## Implementation Steps
|
||||
1. Create `src/modules/wordle/index.js` per shape above (`/wordle`, `/wstats`, `/konami`).
|
||||
2. Create `src/modules/loldle/index.js` (`/loldle`, `/lstats`, `/ggwp`).
|
||||
3. Create `src/modules/misc/index.js` (`/ping`, `/mstats`, `/fortytwo`) including the `last_ping` `putJSON` / `getJSON` demonstrating DB usage.
|
||||
4. Verify `src/modules/index.js` static map includes all three (added in phase-04).
|
||||
5. `wrangler dev` smoke test: send each command via a mocked Telegram update; verify routing and KV writes land in the preview namespace with prefix `wordle:` / `loldle:` / `misc:`.
|
||||
6. Lint clean.
|
||||
|
||||
## Todo List
|
||||
- [ ] `wordle/index.js`
|
||||
- [ ] `loldle/index.js`
|
||||
- [ ] `misc/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.
|
||||
- `/help` output shows three stub sections (wordle, loldle, misc) with 2 commands each (public + protected), plus `util` section.
|
||||
- `/help` does NOT list `/konami`, `/ggwp`, `/fortytwo`.
|
||||
- Telegram's `/` menu (after `scripts/register.js` runs) shows `/wordle`, `/loldle`, `/ping`, `/info`, `/help` — nothing else.
|
||||
- Typing `/konami` in Telegram invokes the handler. Typing `/Konami` does NOT (Telegram + grammY match case-sensitively).
|
||||
- `/ping` writes `last_ping` via `putJSON`; subsequent `/mstats` reads it back via `getJSON`.
|
||||
- Removing `wordle` from `MODULES` (`MODULES="util,loldle,misc"`) successfully boots without loading wordle; `/wordle` becomes 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.
|
||||
- `/ping` timestamp is server time — no sensitive data.
|
||||
|
||||
## Next Steps
|
||||
- Phase 07 adds `scripts/register.js` to run `setWebhook` + `setMyCommands` at deploy time.
|
||||
- Phase 08 tests the full routing flow with these stubs as fixtures.
|
||||
Reference in New Issue
Block a user