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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- Parser: retain bounded voice-file metadata and topic close/reopen service
events for host-owned transcription and session lifecycle handling.

## 0.6.6 - 2026-09-05

- Ingress: skip a crashing update and advance the polling offset so later
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,11 @@ mix test

Optional live Telegram smoke tests should use separate credentials and should
not be required in regular CI.

### Host-owned voice and topic lifecycle

Parser events optionally include `voice` (Telegram file ID, duration, MIME type
and size) and `topic_event: :closed | :reopened`. They remain non-text events;
the parser does not download files, transcribe audio or grant access. Hosts must
validate the Telegram user and conversation before acting on this metadata.
The Bot API does not provide a corresponding documented topic-deleted event.
13 changes: 13 additions & 0 deletions lib/genswarms/telegram/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,25 @@ defmodule Genswarms.Telegram.Parser do
}
|> maybe_put(:replied_to, replied_to(message))
|> maybe_put(:photo, photo_file_id(message))
|> maybe_put(:voice, voice_metadata(message))
|> maybe_put(:topic_event, topic_event(message))

{:ok, maybe_pending_message_ids(event, update)}
end

defp parse_message(_update, _message, _source), do: :ignore

# Preserve metadata only. Admission, downloads, transcription and session
# lifecycle policy belong to the host, not the generic Telegram parser.
defp voice_metadata(%{"voice" => %{"file_id" => id} = voice}) when is_binary(id),
do: Map.take(voice, ["file_id", "file_unique_id", "duration", "mime_type", "file_size"])

defp voice_metadata(_), do: nil

defp topic_event(%{"forum_topic_closed" => data}) when is_map(data), do: :closed
defp topic_event(%{"forum_topic_reopened" => data}) when is_map(data), do: :reopened
defp topic_event(_), do: nil

# Host-generated `inject_update` batches may represent several original
# Telegram messages as one synthetic transport event. Preserve their real
# ids as explicit metadata; normal Telegram updates never carry this field.
Expand Down
53 changes: 53 additions & 0 deletions test/parser_session_metadata_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule Genswarms.Telegram.ParserSessionMetadataTest do
use ExUnit.Case, async: true
alias Genswarms.Telegram.Parser

defp update(fields) do
%{
"update_id" => 1,
"message" =>
Map.merge(
%{
"message_id" => 4,
"chat" => %{"id" => -100, "type" => "supergroup"},
"is_topic_message" => true,
"message_thread_id" => 7,
"from" => %{"id" => 42}
},
fields
)
}
end

test "voice preserves download metadata without treating audio as trusted text" do
voice = %{
"file_id" => "voice-file",
"duration" => 9,
"file_size" => 300,
"unexpected" => "ignored"
}

assert {:ok, event} = Parser.parse_update(update(%{"voice" => voice}))
assert event.type == :non_text
assert event.conversation_id == "tg:-100:7"
assert event.identity.user_id == 42
assert event.voice == Map.delete(voice, "unexpected")
assert event.text == ""
end

test "topic close and reopen remain non-text service events" do
for {field, expected} <- [
{"forum_topic_closed", :closed},
{"forum_topic_reopened", :reopened}
] do
assert {:ok, event} = Parser.parse_update(update(%{field => %{}}))
assert event.topic_event == expected
assert event.type == :non_text
assert event.conversation_id == "tg:-100:7"
end

assert {:ok, event} = Parser.parse_update(update(%{"text" => "hello"}))
refute Map.has_key?(event, :topic_event)
refute Map.has_key?(event, :voice)
end
end
Loading