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:
2026-04-11 09:49:06 +07:00
parent e76ad8c0ee
commit c4314f21df
51 changed files with 6928 additions and 1 deletions

View File

@@ -0,0 +1,44 @@
/**
* @file KVStore interface — JSDoc typedefs only, no runtime code.
*
* This is the contract every storage backend must satisfy. modules receive
* a prefixed `KVStore` (via {@link module:db/create-store}) and must NEVER
* touch the underlying binding. to swap Cloudflare KV for a different
* backend (D1, Upstash Redis, ...) in the future, implement this interface
* in a new file and change the one import in create-store.js — no module
* code changes required.
*/
/**
* @typedef {Object} KVStorePutOptions
* @property {number} [expirationTtl] seconds — value auto-deletes after this many seconds.
*/
/**
* @typedef {Object} KVStoreListOptions
* @property {string} [prefix] additional prefix (appended AFTER the module namespace).
* @property {number} [limit]
* @property {string} [cursor] pagination cursor from a previous list() call.
*/
/**
* @typedef {Object} KVStoreListResult
* @property {string[]} keys — module namespace already stripped.
* @property {string} [cursor] — present if more pages available.
* @property {boolean} done — true when list_complete.
*/
/**
* @typedef {Object} KVStore
* @property {(key: string) => Promise<string|null>} get
* @property {(key: string, value: string, opts?: KVStorePutOptions) => Promise<void>} put
* @property {(key: string) => Promise<void>} delete
* @property {(opts?: KVStoreListOptions) => Promise<KVStoreListResult>} list
* @property {(key: string) => Promise<any|null>} getJSON
* returns null on missing key OR malformed JSON (logs a warning — does not throw).
* @property {(key: string, value: any, opts?: KVStorePutOptions) => Promise<void>} putJSON
* throws if value is undefined or contains a cycle.
*/
// JSDoc-only module. No runtime exports.
export {};