Files
miti99bot/README.md
tiennm99 0f0dc1c108 docs: update architecture and README for trading module
Add trading module section (§13) to architecture.md covering commands,
data model, price sources, and file layout. Update file trees, test
counts (56→110), and module registry snippet in both docs.
2026-04-14 15:24:01 +07:00

183 lines
7.7 KiB
Markdown

# miti99bot
[My Telegram bot](https://t.me/miti99bot) — a plug-n-play bot framework for Cloudflare Workers.
Modules are added or removed via a single `MODULES` env var. Each module registers its own commands with three visibility levels (public / protected / private). Data lives in Cloudflare KV behind a thin `KVStore` interface, so swapping the backend later is a one-file change.
## Why
- **Drop-in modules.** Write a single file, list the folder name in `MODULES`, redeploy. No registration boilerplate, no manual command wiring.
- **Three visibility levels out of the box.** Public commands show in Telegram's `/` menu and `/help`; protected show only in `/help`; private are hidden slash-command easter eggs. One namespace, loud conflict detection.
- **Storage is swappable.** Modules talk to a small `KVStore` interface — Cloudflare KV today, a different backend tomorrow, with a one-file change.
- **Zero admin surface.** No in-Worker `/admin/*` routes, no admin secret. `setWebhook` + `setMyCommands` run at deploy time from a local node script.
- **Tested.** 110 vitest unit tests cover registry, storage, dispatcher, help renderer, validators, HTML escaping, and the trading module.
## How a request flows
```
Telegram sends update
POST /webhook ◄── grammY validates X-Telegram-Bot-Api-Secret-Token (401 on miss)
getBot(env) ──► first call only: installDispatcher(bot, env)
│ │
│ ├── loadModules(env.MODULES.split(","))
│ ├── per module: init({ db: createStore(name, env), env })
│ ├── build publicCommands / protectedCommands / privateCommands
│ │ + unified allCommands map (conflict check)
│ └── for each entry: bot.command(name, handler)
bot.handleUpdate(update) ──► grammY routes /cmd → registered handler
handler reads/writes via db.getJSON / db.putJSON (auto-prefixed as "module:key")
ctx.reply(...) → response back to Telegram
```
## Architecture snapshot
```
src/
├── index.js # fetch handler: POST /webhook + GET / health
├── bot.js # memoized grammY Bot, lazy dispatcher install
├── db/
│ ├── kv-store-interface.js # JSDoc typedefs (the contract)
│ ├── cf-kv-store.js # Cloudflare KV implementation
│ └── create-store.js # per-module prefixing factory
├── modules/
│ ├── index.js # static import map — register new modules here
│ ├── registry.js # load, validate, build command tables
│ ├── dispatcher.js # wires every command via bot.command()
│ ├── validate-command.js
│ ├── util/ # /info, /help (fully implemented)
│ ├── trading/ # fake paper trading — crypto, stocks, forex, gold
│ ├── wordle/ # stub — proves plugin system
│ ├── loldle/ # stub
│ └── misc/ # stub
└── util/
└── escape-html.js
scripts/
├── register.js # post-deploy: setWebhook + setMyCommands
└── stub-kv.js
```
## Command visibility
| Level | In `/` menu | In `/help` | Callable |
|---|---|---|---|
| `public` | yes | yes | yes |
| `protected` | **no** | yes | yes |
| `private` | **no** | **no** | yes (hidden slash command — easter egg) |
All three are slash commands. Private commands are just hidden from both surfaces. They're not access control — anyone who knows the name can invoke them.
Command names must match `^[a-z0-9_]{1,32}$` (Telegram's slash-command limit). Conflict detection is unified across all visibility levels — two modules cannot register the same command name no matter the visibility. Registry build throws at load time.
## Prereqs
- Node.js ≥ 20.6 (for `node --env-file`)
- A Cloudflare account with Workers + KV
- A Telegram bot token from [@BotFather](https://t.me/BotFather)
## Setup
1. **Install dependencies**
```bash
npm install
```
2. **Create KV namespaces** (production + preview)
```bash
npx wrangler kv namespace create miti99bot-kv
npx wrangler kv namespace create miti99bot-kv --preview
```
Paste the returned IDs into `wrangler.toml` under `[[kv_namespaces]]`, replacing both `REPLACE_ME` placeholders.
3. **Set Worker runtime secrets** (stored in Cloudflare, used by the deployed Worker)
```bash
npx wrangler secret put TELEGRAM_BOT_TOKEN
npx wrangler secret put TELEGRAM_WEBHOOK_SECRET
```
`TELEGRAM_WEBHOOK_SECRET` can be any high-entropy string — e.g. `openssl rand -hex 32`. It gates incoming webhook requests; grammY validates it on every update.
4. **Create `.dev.vars`** for local development
```bash
cp .dev.vars.example .dev.vars
# fill in the same TELEGRAM_BOT_TOKEN + TELEGRAM_WEBHOOK_SECRET values
```
Used by `wrangler dev`. Gitignored.
5. **Create `.env.deploy`** for the post-deploy register script
```bash
cp .env.deploy.example .env.deploy
# fill in: token, webhook secret, WORKER_URL (known after first deploy), MODULES
```
Gitignored. The `TELEGRAM_BOT_TOKEN` and `TELEGRAM_WEBHOOK_SECRET` values MUST match what you set via `wrangler secret put` — mismatch means every incoming webhook returns 401.
## Local dev
```bash
npm run dev # wrangler dev — runs the Worker at http://localhost:8787
npm run lint # biome check
npm test # vitest
```
The local `wrangler dev` server exposes `GET /` (health) and `POST /webhook`. For end-to-end testing you'd ngrok/cloudflared the local port and point a test bot's `setWebhook` at it — but pure unit tests (`npm test`) cover the logic seams without Telegram.
## Deploy
Single command, idempotent:
```bash
npm run deploy
```
That runs `wrangler deploy` followed by `scripts/register.js`, which calls Telegram's `setWebhook` + `setMyCommands` using values from `.env.deploy`.
First-time deploy flow:
1. Run `wrangler deploy` once to learn the `*.workers.dev` URL printed at the end.
2. Paste it into `.env.deploy` as `WORKER_URL`.
3. Preview the register payloads without calling Telegram:
```bash
npm run register:dry
```
4. Run the real thing:
```bash
npm run deploy
```
Subsequent deploys: just `npm run deploy`.
## Adding a module
See [`docs/adding-a-module.md`](docs/adding-a-module.md) for the full guide.
TL;DR:
1. Create `src/modules/<name>/index.js` with a default export `{ name, commands, init? }`.
2. Add a line to `src/modules/index.js` static map.
3. Add `<name>` to `MODULES` in both `wrangler.toml` `[vars]` and `.env.deploy`.
4. `npm test` + `npm run deploy`.
## Troubleshooting
| Symptom | Cause |
|---|---|
| 401 on every webhook | `TELEGRAM_WEBHOOK_SECRET` differs between `wrangler secret` and `.env.deploy`. |
| `/help` is missing a module's section | Module has no public or protected commands — private-only modules are hidden. |
| Module loads but no commands respond | `MODULES` does not list the module. Check `wrangler.toml` AND `.env.deploy`. |
| `command conflict: /foo ...` at deploy | Two modules register the same command name. Rename one. |
| `npm run register` exits `missing env: X` | Add `X` to `.env.deploy`. |
| `--env-file` flag not recognized | Node < 20.6. Upgrade Node. |
## Further reading
- [`docs/architecture.md`](docs/architecture.md) — deeper dive: cold-start, module lifecycle, DB namespacing, deploy flow, design tradeoffs.
- [`docs/adding-a-module.md`](docs/adding-a-module.md) — step-by-step guide to authoring a new module.
- `plans/260411-0853-telegram-bot-plugin-framework/` — full phased implementation plan (9 phase files + researcher reports).