Pure chat-linking rules for the Telegram integration: which chats a
getUpdates peek may link, how a capture merges into what is already
linked, and what counts as a hand-entered chat id.
Split out of the LiveView because this is where the surprising parts live (a locked private chat that must still admit a group, ids whose sign carries meaning) and because a form event is a poor place to prove them.
Chat kinds
Telegram numbers private chats positively and group/supergroup chats
negatively — a documented invariant this module leans on, since a stored
chat_ids entry is just a string with no type alongside it.
Privacy mode
A bot added to a group only receives messages addressed to it (a /
command, an @mention, a reply, or a service message) unless it is a group
admin. So a group reaches capturable_chats/1 when someone runs
/start@yourbot in that group — plain chatter never will.
Summary
Functions
Chats that may be linked from a getUpdates result, oldest first, one
entry per chat.
Folds a getUpdates peek into everything the connection should store:
the linked ids, which of them are new, and the metadata for those ids.
Whether a stored id denotes a group/supergroup.
What a stored id denotes. A stored chat_ids entry is a bare string, so
kind is read back from its shape: Telegram signs group ids negative, and a
channel is linked by its @handle.
Folds captured chats into the already-linked ids, returning
{linked_ids, newly_added_ids}.
Normalizes a hand-entered chat id — a numeric id (negative for groups) or
an @channelusername, both of which sendMessage accepts.
Metadata for the given ids only — everything else is dropped.
Functions
Chats that may be linked from a getUpdates result, oldest first, one
entry per chat.
Keeps the chat's type and title so a caller can name a linked chat
rather than show a bare id.
@spec capture(String.t(), [String.t()], map(), [map()]) :: %{ ids: [String.t()], added: [String.t()], meta: map() }
Folds a getUpdates peek into everything the connection should store:
the linked ids, which of them are new, and the metadata for those ids.
Metadata is kept for LINKED chats only, and pruned to them. Recording a chat the lock just refused would turn the connection's data into a log of everyone who has ever messaged the bot.
Whether a stored id denotes a group/supergroup.
@spec kind(String.t()) :: :group | :channel | :private
What a stored id denotes. A stored chat_ids entry is a bare string, so
kind is read back from its shape: Telegram signs group ids negative, and a
channel is linked by its @handle.
Folds captured chats into the already-linked ids, returning
{linked_ids, newly_added_ids}.
"single" locks ONE private chat: the most recent one, and only while no
private chat is linked yet — a stranger who messaged the bot right after
the owner cannot displace it. A group is never subject to that lock:
it is linked by someone who can post in it and deliberately ran the
command there, which is not the case the lock defends against.
"multi" unions everything captured. The empty newly_added_ids is what
lets a caller tell "nothing new was found" from "linked a chat" — the old
code could not, and reported success either way.
Both modes govern AUTO-CAPTURE only. Linking a chat by id is a deliberate act by the connection's owner, so it is not capped here: "single" means "capture cannot quietly add a second private chat", not "this connection can only ever reach one chat".
Normalizes a hand-entered chat id — a numeric id (negative for groups) or
an @channelusername, both of which sendMessage accepts.
Hand entry exists because capture only reaches chats whose update is still in Telegram's ~24h queue; an id you already know should not need that window.
Metadata for the given ids only — everything else is dropped.