Skip to content

feat(sender): honour link_preview_options on the send path - #41

Merged
jmlago merged 3 commits into
genlayerlabs:mainfrom
rasca:feat/per-message-link-preview
Sep 5, 2026
Merged

jmlago merged 3 commits into
genlayerlabs:mainfrom
rasca:feat/per-message-link-preview

Conversation

@rasca

@rasca rasca commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The problem

A link preview is a transport-wide constant, not a per-message decision.

build_send_message/1 sets disable_web_page_preview: true unconditionally (delivery/core.ex:15), and the sender's send/reply path builds its attrs from a fixed key set with no preview key (objects/sender.ex:1350-1365). So no message this transport sends can carry a preview, and nothing a caller puts in the message map changes it.

The asymmetry that makes this look unintended: edits already accept the option. build_edit_message_text/1 does maybe_put(:link_preview_options, option(attrs, :link_preview_options)) (delivery/edits.ex:25) and the sender passes msg["link_preview_options"] straight through on the edit path (sender.ex:1516). A message can be edited into having a preview it could never be sent with.

The change

Two small edits, mirroring what the edit builder already does:

  • build_send_message/1 reads link_preview_options from attrs via the existing option/2 + optional_map/2 helpers.
  • The sender's do_send_text/5 passes msg["link_preview_options"] into attrs.

The default is unchanged. With no option the payload still carries disable_web_page_preview: true — the right default for an agent quoting a URL mid-thread, and the wrong one for an operator broadcast. The point is that those two can now differ per message.

Details worth a reviewer's eye:

  • An explicit option wins and the deprecated flag is then omitted — Bot API 7.0 replaced disable_web_page_preview with link_preview_options, so sending both would be contradictory.
  • A non-object option raises ArgumentError through the existing optional_map/2 rather than being silently dropped.
  • Read by atom or string key, through option/2 — same idiom as the edit builder.
  • Applied to every chunk of a chunked message: chunking is the transport's own detail, not something the caller asked for, and Telegram previews at most the first link of each message it actually sends, so a chunk with no link is unaffected. (Happy to scope it to the first chunk instead if you'd rather — reply_markup already has idx == last precedent for chunk-position logic.)
  • Sender.build_send_body/5 (sender.ex:3886) still hardcodes the flag. It is not on the send path — nothing in the package calls it — so I left it alone rather than widen the diff. Say the word and I'll include it.

Motivation

An operator broadcast carrying a link to an X post reached its groups with no preview card. The link was live, but the post it pointed at was invisible until someone clicked.

The only host-side workaround is to route those targets to a different sender object — i.e. fork the egress path and re-implement the throttle, chunking, delivery-effects audit, dead-recipient classification and retry/backoff this package already gets right, all for one boolean.

Validation

  • 7 new tests in test/sender_link_preview_test.exs: the unchanged default, an explicit option (on and off), the extra option fields (url, prefer_large_media), string-key reading, the raise on a non-object, and both halves of the sender send path.
  • Full suite 242/242 green.
  • No new compiler warnings (3 before, 3 after — all pre-existing).

A link preview was a transport-wide constant, not a per-message decision.
build_send_message/1 set disable_web_page_preview: true unconditionally
(delivery/core.ex) and the sender's send/reply path built its attrs from a
fixed key set with no preview key (objects/sender.ex), so NO message this
transport sent could carry a preview and nothing a caller put in the message
map changed it.

The asymmetry that makes this look unintended: EDITS already accept the
option — build_edit_message_text/1 does maybe_put(:link_preview_options, …)
and the sender passes msg["link_preview_options"] straight through on the edit
path. A message could therefore be edited into having a preview it could never
be sent with.

The default is unchanged. With no option the payload still carries
disable_web_page_preview: true — the right default for an agent that quotes a
URL mid-thread, and the wrong one for an operator broadcast; the point is that
those two can now differ per message. An explicit link_preview_options wins and
the deprecated flag is then omitted (Bot API 7.0 replaced
disable_web_page_preview with link_preview_options, so sending both would be
contradictory). A non-object option raises ArgumentError via the existing
optional_map/2 rather than being silently dropped, and the option is read by
atom or string key through option/2 — the same idiom the edit builder uses.

Applied to every chunk of a chunked message: chunking is the transport's own
implementation detail, not something the caller asked for, and Telegram
previews at most the first link of each message it actually sends, so a chunk
with no link is unaffected.

Motivation: an operator broadcast carrying a link to an X post reached its
groups with no preview card — the link was live, but the post it pointed at was
invisible until someone clicked. The only host-side workaround is to route
those targets to a different sender object, i.e. to fork the egress path and
re-implement the throttle, chunking, delivery-effects audit, dead-recipient
classification and retry/backoff this package already gets right, for one
boolean.

Validated: 7 new tests in test/sender_link_preview_test.exs cover the unchanged
default, an explicit option (on and off), the extra option fields, string-key
reading, the raise on a non-object, and both sender send-path halves. Full
suite 242/242 green; no new compiler warnings (3 before, 3 after).
@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 3020feec-58f9-448a-9110-393d14cbd88e

📥 Commits

Reviewing files that changed from the base of the PR and between 30cfb5c and bf252d4.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • lib/genswarms/telegram/delivery/core.ex
  • lib/genswarms/telegram/objects/sender.ex
  • test/sender_link_preview_test.exs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The new test's Effects module implemented only the configured /2 form, so
compiling the test suite warned 'function before_send/1 required by behaviour
Genswarms.Telegram.DeliveryEffects is not implemented'. Same shape the other
sender tests use. No behaviour change; 242/242 still green.
@rasca

rasca commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Self-review follow-up, pushed as c4c9d67:

Correction to the PR body. I claimed "no new compiler warnings (3 before, 3 after)" — that count was from mix compile, which does not compile the test tree. mix test did warn on my new file:

warning: function before_send/1 required by behaviour Genswarms.Telegram.DeliveryEffects
is not implemented (in module Genswarms.Telegram.SenderLinkPreviewTest.Effects)

The test's Effects module implemented only the configured /2 form. Fixed by adding the /1 clause, the same shape SenderCoalesceTest uses. My test file is now named in no warning; the 3 pre-existing library warnings are untouched, and the suite is still 242/242.

No change to lib/ — the library diff is unchanged from the first commit.

@jmlago
jmlago merged commit 249f908 into genlayerlabs:main Sep 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants