Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
# Changelog

## Unreleased

## 0.6.6 - Unreleased

- Sender/Delivery: `link_preview_options` is now honoured on the SEND path, so
a link preview is a per-message decision instead of a transport-wide one.
`build_send_message/1` hardcoded `disable_web_page_preview: true` and the
sender built its attrs from a fixed key set with no preview key, so no
message this transport sent could ever carry a preview and nothing a caller
put in the message map changed it — while `build_edit_message_text/1` already
accepted the option, meaning a message could be *edited* into having a
preview it could never be *sent* with. Same option name and shape as the edit
builder; read by atom or string key. **The default is unchanged**: with no
option the payload still carries `disable_web_page_preview: true`, which is
the right default for an agent quoting a URL mid-thread and the wrong one for
an operator broadcast — those now differ per message. An explicit option wins
and the deprecated flag is then omitted (Bot API 7.0 replaced
`disable_web_page_preview` with `link_preview_options`); a non-object option
raises rather than being silently dropped. Applied to every chunk of a
chunked message, since chunking is the transport's own detail and Telegram
previews at most the first link of each message it actually sends.
- Curl transport removes its private temporary directory after cleaning up the
request files, instead of leaving one empty directory per request.

Expand Down
17 changes: 15 additions & 2 deletions lib/genswarms/telegram/delivery/core.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,29 @@ defmodule Genswarms.Telegram.Delivery.Core do
base = %{
chat_id: ConversationId.chat_id(cid),
text: Format.to_html(text),
parse_mode: "HTML",
disable_web_page_preview: true
parse_mode: "HTML"
}

base
|> put_link_preview(attrs)
|> maybe_put_thread(cid)
|> maybe_put(:reply_parameters, reply_parameters(attrs))
|> maybe_put(:reply_markup, reply_markup_from_attrs(attrs))
end

# Previews stay OFF unless the caller asks for them — the default every send
# has always had, and the right one for an agent that quotes a URL mid-thread.
# An explicit `link_preview_options` (Bot API 7.0, which deprecated
# `disable_web_page_preview`) wins and the legacy flag is then omitted, so the
# two never travel together. Same option, same name and same shape that
# `build_edit_message_text/1` already accepts.
defp put_link_preview(payload, attrs) do
case optional_map(option(attrs, :link_preview_options), :link_preview_options) do
nil -> Map.put(payload, :disable_web_page_preview, true)
options -> Map.put(payload, :link_preview_options, options)
end
end

def build_plain_message(%{conversation_id: cid, text: text} = attrs) do
validate_conversation_id!(cid)

Expand Down
10 changes: 10 additions & 0 deletions lib/genswarms/telegram/objects/sender.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,7 @@ defmodule Genswarms.Telegram.Objects.Sender do
reply_to = validate_reply_tag(cid, Map.get(msg, "reply_to_message_id"), state)
reply_attrs = reply_attrs(reply_to, msg)
reply_markup = message_reply_markup(msg)
link_preview = Map.get(msg, "link_preview_options")
photo = photo_for_text(Map.get(msg, "photo"), text)

{results, state} =
Expand All @@ -1360,6 +1361,15 @@ defmodule Genswarms.Telegram.Objects.Sender do

attrs = if idx == 0, do: Map.merge(attrs, reply_attrs), else: attrs

# Per-message, and deliberately on EVERY chunk rather than only the
# first: chunking is our implementation detail, not something the
# caller asked for, and Telegram previews at most the first link of
# each message it actually sends. A chunk with no link is unaffected.
attrs =
if link_preview,
do: Map.put(attrs, :link_preview_options, link_preview),
else: attrs

payload =
case {idx, photo} do
{0, photo} when is_binary(photo) and photo != "" ->
Expand Down
142 changes: 142 additions & 0 deletions test/sender_link_preview_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
defmodule Genswarms.Telegram.SenderLinkPreviewTest do
@moduledoc """
Link previews are per-message, and OFF unless the caller asks.

Every send used to carry a hardcoded `disable_web_page_preview: true`, with
no key a caller could set to change it — while EDITS already accepted
`link_preview_options`. So a message could be edited into having a preview it
could never be sent with. These tests pin both halves of the fix: the default
is unchanged, and an explicit option reaches the payload.
"""
use ExUnit.Case, async: true

alias Genswarms.Telegram.Delivery
alias Genswarms.Telegram.Objects.Sender

defmodule Effects do
@behaviour Genswarms.Telegram.DeliveryEffects

# The behaviour requires the /1 arity even when only the /2 (configured)
# form is used — omitting it warns at compile time.
@impl true
def before_send(_payload), do: :ok

@impl true
def before_send(payload, %{test_pid: pid}) do
send(pid, {:payload, payload})
:ok
end

@impl true
def after_send(_payload, _result), do: :ok
@impl true
def delivery_failed(_payload, _reason), do: :ok
@impl true
def redact_outbound(text, _meta), do: text
end

@cid "tg:-100777:0"

defp booted_state do
{:ok, state} =
Sender.init(%{
dry_run: true,
send_sources: [:host],
delivery_effects: {Effects, %{test_pid: self()}}
})

state
end

defp send!(msg) do
{:noreply, _state} = Sender.handle_message(:host, Map.put(msg, "action", "send"), booted_state())
assert_receive {:payload, payload}
payload
end

# ── the builder ───────────────────────────────────────────────────────────

test "no option: the long-standing default, previews off" do
payload = Delivery.build_send_message(%{conversation_id: @cid, text: "see https://x.com/a/1"})

assert payload.disable_web_page_preview == true
refute Map.has_key?(payload, :link_preview_options)
end

test "an explicit option wins, and the deprecated flag is then omitted" do
payload =
Delivery.build_send_message(%{
conversation_id: @cid,
text: "see https://x.com/a/1",
link_preview_options: %{is_disabled: false}
})

assert payload.link_preview_options == %{is_disabled: false}

# Bot API 7.0 deprecated disable_web_page_preview in favour of
# link_preview_options; sending both would be contradictory.
refute Map.has_key?(payload, :disable_web_page_preview)
end

test "the option can also turn a preview OFF explicitly, and carries its own fields" do
payload =
Delivery.build_send_message(%{
conversation_id: @cid,
text: "x",
link_preview_options: %{is_disabled: true}
})

assert payload.link_preview_options == %{is_disabled: true}

large =
Delivery.build_send_message(%{
conversation_id: @cid,
text: "x",
link_preview_options: %{url: "https://example.com", prefer_large_media: true}
})

assert large.link_preview_options == %{url: "https://example.com", prefer_large_media: true}
end

test "the option is read by string key too (option/2, as the edit builder does)" do
payload =
Delivery.build_send_message(%{
:conversation_id => @cid,
:text => "x",
"link_preview_options" => %{"is_disabled" => false}
})

assert payload.link_preview_options == %{"is_disabled" => false}
end

test "a non-object option is refused rather than silently dropped" do
assert_raise ArgumentError, fn ->
Delivery.build_send_message(%{
conversation_id: @cid,
text: "x",
link_preview_options: "yes please"
})
end
end

# ── the sender's send path ────────────────────────────────────────────────

test "send: the message's option reaches the payload" do
payload =
send!(%{
"conversation_id" => @cid,
"text" => "Read this: https://x.com/genlayer/status/1",
"link_preview_options" => %{"is_disabled" => false}
})

assert payload.link_preview_options == %{"is_disabled" => false}
refute Map.has_key?(payload, :disable_web_page_preview)
end

test "send: without the option nothing changes for existing hosts" do
payload = send!(%{"conversation_id" => @cid, "text" => "plain https://x.com/a/1"})

assert payload.disable_web_page_preview == true
refute Map.has_key?(payload, :link_preview_options)
end
end
Loading