← Back to PRs

#19991: feat(telegram): callback direct mode with dedupe, button state, and tap intercept

by li-yifei open 2026-02-18 11:11 View on GitHub →
channel: telegram size: XL
## Summary Adds a new `callback` config block under `channels.telegram` for fine-grained control over Telegram callback query handling. ### New features - **`callback.enabled`** — enables direct callback mode (callback queries handled natively instead of being forwarded as synthetic messages) - **`callback.tapIntercept`** — middleware-level early intercept with ack support (`ackText`, `ackAlert`) - **`callback.forwardUnhandled`** — controls whether unmatched callbacks fall through to message processing - **`callback.dedupeWindowMs`** — deduplicates rapid repeated clicks within a time window (capped at 60s) - **`callback.buttonStateMode`** — visual feedback after click: `off` / `mark-clicked` (✅) / `disable-clicked` (sentinel) ### Fixes included - **C1**: Fixed double `answerCallbackQuery` when `tapIntercept=true` + `ackText` configured — ack now happens in the middleware with the correct params, handler skips if already answered - **M1**: `forwardUnhandled=false` now has schema validation requiring `enabled=true`; JSDoc clarified - **N2**: Incoming clicks on `disable-clicked` buttons (sentinel prefix `ocb:clicked`) are detected and early-returned - **R2**: `dedupeWindowMs` capped at 60,000ms to prevent memory accumulation ### Test coverage - tap intercept + ackText: verifies single answerCallbackQuery call with correct params - disable-sentinel detection: verifies early return without processMessage - All `buttonStateMode` enum values tested (`off`, `mark-clicked`, `disable-clicked`) - Regression: legacy mode (`enabled=false`) still forwards callbacks as messages ### Config example ```json5 { channels: { telegram: { callback: { enabled: true, tapIntercept: true, ackText: "Got it!", forwardUnhandled: false, dedupeWindowMs: 3000, buttonStateMode: "disable-clicked" } } } } ``` <!-- greptile_comment --> <h3>Greptile Summary</h3> Adds a new `callback` config block under `channels.telegram` for fine-grained control over Telegram callback query handling, including direct callback mode, tap intercept middleware, dedupe, and post-click button state editing. - **Config & Schema**: New `TelegramCallbackConfig` type and Zod schema with validation (e.g., `forwardUnhandled=false` requires `enabled=true`). Well-structured with proper `strict()` and `superRefine`. - **Tap Intercept**: Middleware in `bot.ts` answers callback queries early to prevent Telegram retries; the downstream handler skips its own ack when tap intercept is on. However, the middleware doesn't distinguish disabled sentinel buttons from regular callbacks, so `ackText` is shown for already-disabled buttons. - **Dedupe**: `callbackDedupeCache` with lazy cleanup on each callback and 60s cap. Correctly gated behind `callbackDirectEnabled`. - **Button State Mode**: `buildEditedCallbackKeyboard` treats `mark-clicked` and `disable-clicked` identically (both replace callback_data with sentinel and keep only the clicked button). The `mode` parameter is `void`-ed. The JSDoc documents different behaviors for each mode that don't match the implementation. - **Model buttons**: Removes checkmark (✓) from current model display — `currentModel` is no longer used in `buildModelsKeyboard` but remains in the `ModelsKeyboardParams` type and is still passed at the call site (dead parameter). - **Tests**: Comprehensive coverage for new features including tap intercept + ackText, sentinel detection, all `buttonStateMode` values, dedupe, and legacy mode regression. <h3>Confidence Score: 3/5</h3> - Mostly safe to merge but has a UX-level logic issue with tap intercept acking disabled buttons, and a config behavior mismatch between mark-clicked and disable-clicked modes. - The PR is well-structured with good test coverage and clear config validation. Score of 3 reflects two issues: (1) the tap intercept middleware doesn't filter out sentinel/disabled callbacks, causing ackText to show on disabled buttons when both features are combined, and (2) the mark-clicked vs disable-clicked config options are documented as distinct behaviors but implemented identically, which could confuse users. - `src/telegram/bot.ts` (tap intercept middleware doesn't filter sentinel callbacks), `src/telegram/bot-handlers.ts` (buildEditedCallbackKeyboard ignores mode parameter) <sub>Last reviewed commit: f980f11</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs