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,29 @@
/**
* @file fake-modules — fixture modules + a helper that builds an import-map
* shape compatible with `loadModules(env, importMap)`.
*
* Using dependency injection (instead of `vi.mock`) sidesteps path-resolution
* flakiness on Windows and keeps tests fully deterministic.
*/
/**
* @param {Record<string, import("../../src/modules/registry.js").BotModule>} modules
*/
export function makeFakeImportMap(modules) {
/** @type {Record<string, () => Promise<{default: any}>>} */
const map = {};
for (const [name, mod] of Object.entries(modules)) {
map[name] = async () => ({ default: mod });
}
return map;
}
export const noopHandler = async () => {};
export function makeCommand(name, visibility, description = "fixture command") {
return { name, visibility, description, handler: noopHandler };
}
export function makeModule(name, commands, init) {
return init ? { name, commands, init } : { name, commands };
}