PhoenixKit.Integrations.Telegram.ChatLink (phoenix_kit v2.29.1)

Copy Markdown View Source

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

capturable_chats(updates)

@spec capturable_chats([map()]) :: [map()]

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.

capture(mode, existing, existing_meta, chats)

@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.

group_id?(id)

@spec group_id?(String.t()) :: boolean()

Whether a stored id denotes a group/supergroup.

kind(arg1)

@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.

merge(mode, existing, chats)

@spec merge(String.t(), [String.t()], [map()]) :: {[String.t()], [String.t()]}

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".

normalize_chat_id(value)

@spec normalize_chat_id(String.t()) :: {:ok, String.t()} | :error

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.

prune_meta(meta, ids)

@spec prune_meta(map(), [String.t()]) :: map()

Metadata for the given ids only — everything else is dropped.