From f4928c3947290fa02b57ab5d6f8c1768a48449f8 Mon Sep 17 00:00:00 2001 From: jmlago Date: Sat, 5 Sep 2026 23:14:37 +0100 Subject: [PATCH] feat(parser): expose voice and topic lifecycle metadata --- CHANGELOG.md | 5 +++ README.md | 8 ++++ lib/genswarms/telegram/parser.ex | 13 +++++++ test/parser_session_metadata_test.exs | 53 +++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 test/parser_session_metadata_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f8194b..f0b2217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index aa90f49..d7a70ee 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/genswarms/telegram/parser.ex b/lib/genswarms/telegram/parser.ex index 521b854..e30d2e6 100644 --- a/lib/genswarms/telegram/parser.ex +++ b/lib/genswarms/telegram/parser.ex @@ -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. diff --git a/test/parser_session_metadata_test.exs b/test/parser_session_metadata_test.exs new file mode 100644 index 0000000..7df26f0 --- /dev/null +++ b/test/parser_session_metadata_test.exs @@ -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