feat: isolated Telegram Bot API integration with localized key normalization#736
Conversation
|
Maintainer review-assist for bounty #62: This looks like a useful Telegram-core hardening path because it keeps the key-normalization work localized to the Telegram integration instead of changing the general Prism/Lens runtime, and it adds concrete read/webhook surfaces ( Before treating it as a complete #62 closure, I would gate on three items:
No local tests run here; this is based on the visible PR diff at |
…l-Finance#62) 1. Apply Lux.Integrations.Telegram.normalize_keys/1 to all messaging, media, and interactive Telegram prism handlers so string-keyed LLM/JSON input (e.g. %{"chat_id" => 1, "text" => "hi"}) works on every prism, matching the pattern already used by the webhook prisms. 2. Guard ListUpdates/GetFile/GetWebhookInfo focus so a missing bot token fails locally with {:error, :missing_token} instead of issuing a live request to the unsubstituted ":token" URL, consistent with Client.request/3. 3. Add regression tests: string-keyed handler tests for SendMessage, SendPhoto, and AnswerCallbackQuery, plus a missing-token lens test for all three lenses. Scoped to the Telegram integration; no core framework (prism.ex/lens.ex) changes.
|
Thanks for the detailed review-assist @MyTH-zyxeon — all three gating items are now addressed in ✅ Review fixes — addressing @MyTH-zyxeon's review-assist (commit
|
| Metric | Before | After |
|---|---|---|
| Telegram prisms applying key normalization | 2 / 30 (webhook only) | 30 / 30 |
SendMessage.handler(%{"chat_id" => 1, "text" => "hi"}) |
{:error, "Missing or invalid chat_id"} |
{:ok, %{sent: true, …}} |
| Read lens with no token configured | live GET to …/bot:token/getUpdates |
{:error, :missing_token} (local, no network) |
| Client ↔ lens missing-token parity | inconsistent | identical {:error, :missing_token} |
| Telegram regression tests added | — | +5 cases (3 string-key handlers, 1 lens missing-token × 3 lenses) |
| Telegram unit suite (OTP 27.2 / Elixir 1.18.1) | — | 296 tests, 0 failures |
Core framework files touched (prism.ex/lens.ex) |
0 | 0 |
Design — read-lens missing-token guard
flowchart LR
A["Lens.focus/2"] --> B{"get_token/0"}
B -- "token present" --> C["super/2: authenticate + HTTP request"]
C --> D["after_focus → {:ok, result}"]
B -- "no token" --> E["{:error, :missing_token} (local, no request)"]
- Item 1 reuses the existing recursive, struct-safe
normalize_keys/1(String.to_existing_atom/1, so the atom table cannot grow from untrusted input) as a per-handler choke point — identical to the webhook-prism pattern, so the read/write surfaces stay consistent. - Item 2 keeps reads (lenses) and writes (client) returning the same
{:error, :missing_token}contract, localized viadefoverridablesolens.exstays untouched.
|
/claim #62 |
1 similar comment
|
/claim #62 |
Summary
This PR implements a strictly scoped, production-grade, and secure Telegram Bot API integration for the Lux multi-agent framework, fully adhering to native patterns and avoiding any changes to core framework files (
prism.ex,lens.ex).Core Enhancements
Localized Parameter Key Normalization:
%{"chat_id" => 123}).Lux.Integrations.Telegram.normalize_keys/1helper usingString.to_existing_atom/1(preventing atom leaks / DoS attacks) and ignoring Elixir structs.Hardened Bot Token URL Substitution:
~r/(^|\/)bot(?::token|(?=\/|$))(\/?)/./bot:token/or/bot/), preventing corruption of custom URLs containingsomebot.Resilient HTTP Client:
retry_afterheaders/body fields.request_many/2) for ordered message flows.File.stream!/3.Webhooks & Rich Update Parsing:
Plug.Crypto.secure_compare/2for constant-time comparison of incoming secret tokens.%Lux.Integrations.Telegram.Update{}parser that categorizes incoming updates (messages, callback queries, inline queries, channel posts) and safely normalizes their structure.Lenses & Prisms (R1 Compliance):
ListUpdates(getUpdates),GetWebhookInfo(getWebhookInfo),GetFile(getFile). These are purely declarative and useafter_focus/1callbacks without overriding thefocus/2macro.SetWebhook(setWebhook) andDeleteWebhook(deleteWebhook) implementing clean handler logic.Verification & Test Coverage
✅ Review fixes — addressing @MyTH-zyxeon's review-assist (commit
979ce21)The review-assist for bounty #62 gated on three items. All are resolved, kept strictly scoped to the Telegram integration (still no
prism.ex/lens.exchanges).1. Key normalization was only wired into the new webhook prisms
Issue raised:
normalize_keys/1was used by the new webhook prisms/lenses, but the pre-existing messaging/media/interactive prisms still read atom keys directly — e.g.SendMessage.handler/2didMap.fetch(params, :chat_id)— so the most basic string-keyed LLM/JSON input (%{"chat_id" => 123, "text" => "hi"}) still failed on the send path.Fix: Applied
Lux.Integrations.Telegram.normalize_keys/1as the first line of every messaging, media, and interactive handler — the exact one-line pattern the webhook prisms already established. No new abstraction, no core change.2. Missing-token behavior was inconsistent between client and lenses
Issue raised:
Client.request/3returns{:error, :missing_token}, butTelegram.add_auth/1left the lens URL with the literalbot:tokenplaceholder when no token was configured — soListUpdates/GetFile/GetWebhookInfocould issue a live request againsthttps://api.telegram.org/bot:token/…instead of failing locally.Fix: Each read lens now guards
focus/2: with no token it returns{:error, :missing_token}(the same shape the client returns) with no network call, and delegates tosuper/2unchanged when a token is present. Implemented withdefoverridable focus: 2, so the guard lives entirely inside the Telegram lenses (theLux.Lensmacro only exposesafter_focus/before_focusas overridable).3. Acceptance should cover the existing send/edit/delete/media/interactive surface
Fix: Added regression tests for the string-key path across all three families —
SendMessage(messaging),SendPhoto(media),AnswerCallbackQuery(interactive) — plus amissing_token_testcovering all three lenses.Before / after
SendMessage.handler(%{"chat_id" => 1, "text" => "hi"}){:error, "Missing or invalid chat_id"}{:ok, %{sent: true, …}}…/bot:token/getUpdates{:error, :missing_token}(local, no network){:error, :missing_token}prism.ex/lens.ex)Design — read-lens missing-token guard
flowchart LR A["Lens.focus/2"] --> B{"get_token/0"} B -- "token present" --> C["super/2: authenticate + HTTP request"] C --> D["after_focus → {:ok, result}"] B -- "no token" --> E["{:error, :missing_token} (local, no request)"]normalize_keys/1(String.to_existing_atom/1, so the atom table cannot grow from untrusted input) as a per-handler choke point — identical to the webhook-prism pattern, so the read/write surfaces stay consistent.{:error, :missing_token}contract, localized viadefoverridablesolens.exstays untouched.