/** * Generated-image dispatcher driven by an interactive button tap. * * Replaces the bibliafala `verse_images/handler.js` shape — parse the button * id → check the per-user daily cap → look up R2 (or render-then-upload) → * send the image with a caption → record usage — with a renderer-agnostic * wrapper. The app supplies: * * - `encode` / `decode` — how button args are serialized into the button id * - `cacheKey` — R2 path computed from args (stable + safe) * - `render` — the actual image producer (PNG bytes / stream) * - `caption` — text to caption the sent image * * Everything else (button-prefix routing, cap enforcement, R2 lookup, send, * failure messages, usage recording) is the framework's job. * * Wiring with the Agent: * * const verseImages = new ButtonImageDispatcher({ * prefix: 'imggen_', * encode: ({ book, chapter, verse }) => `${book}_${chapter}_${verse}`, * decode: (s) => { * const [book, c, v] = s.split('_'); * if (!book || !c || !v) return null; * return { book, chapter: Number(c), verse: Number(v) }; * }, * cacheKey: ({ book, chapter, verse }) => `verse/${book}-${chapter}-${verse}.png`, * render: async (args) => ({ body: await renderVersePng(args), contentType: 'image/png' }), * caption: ({ book, chapter, verse }) => `📖 ${book} ${chapter}:${verse}`, * cache, * client: agent.client, * usage, * feature: 'verse_image', * dailyMax: 5, * }); * * agent.buttonPrefix(verseImages.prefix, async ({ user, buttonId }) => { * await verseImages.handle(user.whatsapp, buttonId); * }); * * The dispatcher always responds — successfully with the image, or with a * fallback text message — so the user never sees a button that did nothing. */ import type { R2Cache, ProducerResult } from './r2_cache.js'; import type { WhatsAppClient } from '../client/whatsapp.js'; import type { UsageCounter } from '../usage/usage_counter.js'; export interface ButtonImageDispatcherOptions { /** Button-id prefix. Must end with the delimiter you separate the suffix on (typically '_'). */ prefix: string; /** Encode args → the suffix portion (the dispatcher prepends `prefix`). */ encode: (args: Args) => string; /** Decode a suffix back to args. Return `null` to mean "malformed". */ decode: (suffix: string) => Args | null; /** R2 path for the rendered output. Stable per-args; used as the cache key. */ cacheKey: (args: Args) => string; /** Produce the image bytes. Called only on cache miss. */ render: (args: Args) => Promise; /** Caption text. Sync or async. */ caption: (args: Args, ctx: { url: string; fromCache: boolean; }) => string | Promise; cache: R2Cache; client: WhatsAppClient; /** Optional per-user daily cap, enforced via UsageCounter. */ usage?: UsageCounter | null; /** Required when `usage` is set: the `feature` key in feature_usage. */ feature?: string; /** Required when `usage` is set: the daily allowance. */ dailyMax?: number; /** * Message sent when the user has hit the daily cap. Replaces the default * 'You\'ve hit today\'s limit.' string. Return null/empty to send nothing. */ capExceededText?: (args: Args) => string | null; /** * Message sent when rendering or sending throws. Replaces the default. * Return null/empty to send nothing. */ errorText?: (args: Args, err: unknown) => string | null; /** * Hook fired once an image has been successfully delivered. Fires AFTER * the usage row has been recorded. Failures here are caught + logged. */ onSuccess?: (args: Args, info: { whatsapp: string; url: string; fromCache: boolean; }) => void | Promise; } export type DispatchImageReason = 'sent' | 'invalid_button' | 'daily_cap' | 'render_failed' | 'send_failed'; export interface DispatchImageResult { sent: boolean; reason: DispatchImageReason; fromCache?: boolean; url?: string; } export declare class ButtonImageDispatcher { readonly prefix: string; readonly encode: (args: Args) => string; readonly decode: (suffix: string) => Args | null; readonly cacheKey: (args: Args) => string; readonly render: (args: Args) => Promise; readonly caption: (args: Args, ctx: { url: string; fromCache: boolean; }) => string | Promise; readonly cache: R2Cache; readonly client: WhatsAppClient; readonly usage: UsageCounter | null; readonly feature: string | null; readonly dailyMax: number; readonly capExceededText: ((args: Args) => string | null) | null; readonly errorText: ((args: Args, err: unknown) => string | null) | null; readonly onSuccess: ((args: Args, info: { whatsapp: string; url: string; fromCache: boolean; }) => void | Promise) | null; constructor(opts: ButtonImageDispatcherOptions); /** Build the full button id for `args`. Use when wiring the offer button. */ buttonIdFor(args: Args): string; /** Parse a full button id back to args. Returns null on prefix mismatch or decode failure. */ parseButtonId(id: string): Args | null; /** * Full dispatch flow. Always responds to the user (success or fallback). * Never throws — failures are caught and surfaced in `reason`. */ handle(whatsapp: string, buttonId: string): Promise; } //# sourceMappingURL=button_image_dispatcher.d.ts.map