TelegramConnector
in package
implements
Connector, ProvidesActions, ProvidesModules, Editing, Media, Avatars
Telegram, as far as the bridge is concerned.
Owns the TelegramPHP client and the long poll, and hands the core
Incoming messages and Room descriptions like any other
connector. Nothing above this knows what a file_id is.
Telegram::run() would call Loop::run() itself, so the client is started
through start() — TelegramPHP's loop-free entry point — and Discord::run()
is left to drive the loop.
What it can do that Twitch cannot
Telegram can rewrite a message it already sent and can carry a picture rather than a link to one, so this implements Editing and Media. The relay asks rather than assuming, because IRC can do neither and no amount of wishing makes it.
The token in the URL
Every Bot API URL contains the bot token, and so does every file URL. That
is Telegram's design, not a mistake to be worked around, and it means such a
URL must never be logged, never be put in an exception message that might
be, and never be posted into Discord. Files are downloaded and re-uploaded
instead — see describe(), which hands the core null for the URL
every time — and every error leaving this class passes through
TelegramText::redacted().
Tags
Table of Contents
Interfaces
- Connector
- One network the bot bridges Discord with.
- ProvidesActions
- A connector that brings commands of its own.
- ProvidesModules
- A connector that brings Discord-side features an {@see \Bridge\Command\Action} cannot express.
- Editing
- A connector whose network lets a message already sent be rewritten.
- Media
- A connector whose network can carry a picture rather than a link to one, in both directions.
- Avatars
- A connector that can say what somebody looks like.
Constants
- NAME : mixed = 'telegram'
- The name this connector is addressed by, in the store and in chat.
- UPDATES : mixed = ['message', 'edited_message', 'channel_post', '...
- The updates the bridge acts on; nothing else is worth the bandwidth.
- AVATAR_CACHE : mixed = 1000
- How many people's pictures to remember.
- AVATAR_TTL : mixed = 3600.0
- How long whether someone has a picture is believed; they rarely change it.
- REMEMBER_CAPTIONS : mixed = 500
- How many sent photos to remember, so an edit knows to rewrite a caption.
- USERPIC : mixed = 'https://t.me/i/userpic/320/'
- Where t.me serves a public profile picture, by username, with no token.
Properties
- $adapter : TelegramAdapter|null
- $avatars : array<string, PromiseInterface, 1: float}>
- Each person's picture URL, or `null` for none, by user id, with when it was looked up. The promise itself is kept, so people who speak at once share one lookup.
- $bot : Bot
- $captioned : array<string, true>
- $clientOptions : array<string|int, mixed>
- $config : TelegramConfig
- $gateway : TelegramGateway|null
- $handlers : array<int, callable(Incoming): void>
- $seen : array<string, true>
- $telegram : Telegram
Methods
- __construct() : mixed
- actions() : array<int, Action>
- Every command this connector defines.
- avatarFor() : PromiseInterface<string|int, string|null>
- The sender's public t.me picture, when there is one to show.
- boot() : void
- Builds the client and registers everything that must exist before the first message arrives. Called once, when the connector is added.
- edit() : PromiseInterface
- Rewrites a relayed message in place.
- fetchMedia() : PromiseInterface<string|int, array{filename: string, content: string}|null>
- Downloads a file somebody sent in Telegram, for re-uploading to Discord.
- getConfig() : TelegramConfig
- getGateway() : TelegramGateway
- The gateway, for a caller that needs its pacing rather than the raw client — sending a photo, most obviously.
- getTelegram() : Telegram
- joined() : array<int, string>
- Every bridged chat the bot could see when it last looked.
- label() : string
- What to call the network in front of a human: `Twitch`, `Telegram`.
- modules() : array<int, Module>
- The buttons a panel carries, which are component interactions rather than commands and so cannot be actions.
- name() : string
- A short, lowercase, stable name — `twitch`, `telegram`.
- normalise() : string|null
- Turns however somebody referred to a room into the form this connector addresses it by, or `null` when it is not a room reference at all.
- onIncoming() : void
- Registers the handler every inbound message is passed to.
- queued() : int
- How many outbound messages are waiting on this connector's own pacing.
- relay() : PromiseInterface<string|int, string|null>
- Renders a Discord message for this network and says it.
- resolve() : PromiseInterface<string|int, Room|null>
- Looks a chat up, by id or `@username`.
- send() : PromiseInterface
- Sends text, escaped unless `html` says it is already Telegram HTML.
- sendMedia() : PromiseInterface<string|int, string|null>
- Sends a picture or file into a room.
- start() : PromiseInterface<string|int, mixed>
- Identifies the bot and starts the long poll.
- stop() : void
- Stops the long poll. The loop is Discord's, and keeps running.
- surface() : Surface
- Telegram takes 4096 characters and renders its own HTML, not markdown.
-
sync()
: array{join: list
, part: list } - Telegram has nothing to join: the bot is in a chat because somebody added it, and it cannot let itself in.
- author() : string
- caption() : string
- A photo's caption: the relayed text, or at least who sent it.
- describe() : Media|null
- What a Telegram message was carrying, as the core's own {@see Attachment}.
- dispatch() : void
- Turns a TelegramPHP message into the core's own shape, running it as a command first if it is one.
- firstPhoto() : Media|null
- The first picture Telegram was handed by URL — the one a photo was sent as.
- isGif() : bool
- Whether an image is a GIF: by the type the sender's platform reported, or by its name when it reported none.
- isGone() : bool
- Whether an error means the chat is not there for this bot: it does not exist, or the bot was removed from it.
- messageId() : string|null
- The message id out of whatever TelegramPHP handed back.
- publicLink() : string|null
- The message's public page on t.me, when its chat has a public username.
- quoted() : string|null
- A one-line quote of what a Telegram message was replying to.
- raw() : array<string, mixed>
- A message as plain arrays, all the way down.
Constants
NAME
The name this connector is addressed by, in the store and in chat.
public
mixed
NAME
= 'telegram'
UPDATES
The updates the bridge acts on; nothing else is worth the bandwidth.
public
mixed
UPDATES
= ['message', 'edited_message', 'channel_post', 'edited_channel_post']
AVATAR_CACHE
How many people's pictures to remember.
private
mixed
AVATAR_CACHE
= 1000
AVATAR_TTL
How long whether someone has a picture is believed; they rarely change it.
private
mixed
AVATAR_TTL
= 3600.0
REMEMBER_CAPTIONS
How many sent photos to remember, so an edit knows to rewrite a caption.
private
mixed
REMEMBER_CAPTIONS
= 500
USERPIC
Where t.me serves a public profile picture, by username, with no token.
private
mixed
USERPIC
= 'https://t.me/i/userpic/320/'
Properties
$adapter
private
TelegramAdapter|null
$adapter
= null
$avatars
Each person's picture URL, or `null` for none, by user id, with when it was looked up. The promise itself is kept, so people who speak at once share one lookup.
private
array<string, PromiseInterface, 1: float}>
$avatars
= []
$bot
private
Bot
$bot
$captioned
private
array<string, true>
$captioned
= []
"chat:message" for everything sent as a photo, whose text is a caption.
$clientOptions read-only
private
array<string|int, mixed>
$clientOptions
= []
$config read-only
private
TelegramConfig
$config
$gateway
private
TelegramGateway|null
$gateway
= null
$handlers
private
array<int, callable(Incoming): void>
$handlers
= []
$seen
private
array<string, true>
$seen
= []
Chats the bot could last see, for the startup check.
$telegram
private
Telegram
$telegram
Methods
__construct()
public
__construct(TelegramConfig $config[, array<string, mixed> $clientOptions = [] ]) : mixed
Parameters
- $config : TelegramConfig
- $clientOptions : array<string, mixed> = []
-
Extra TelegramPHP options, merged over the ones built from the config — a different HTTP driver, or a fake one in tests.
actions()
Every command this connector defines.
public
actions() : array<int, Action>
Return values
array<int, Action>avatarFor()
The sender's public t.me picture, when there is one to show.
public
avatarFor(Incoming $message) : PromiseInterface<string|int, string|null>
The Bot API hands out profile photos only as file URLs carrying the bot token, which must never reach Discord. t.me serves the same picture without one, keyed by @username, to anyone the owner lets see it. So it is used when the sender has a username and the Bot API says they have a photo at all; without that check, a username with no photo would show t.me's placeholder. The URL is never fetched here: a connector holds no HTTP client of its own, and Discord does the fetching.
A failed lookup is not remembered, so the next message tries again.
Parameters
- $message : Incoming
Return values
PromiseInterface<string|int, string|null>boot()
Builds the client and registers everything that must exist before the first message arrives. Called once, when the connector is added.
public
boot(Bot $bot) : void
Parameters
- $bot : Bot
edit()
Rewrites a relayed message in place.
public
edit(string $target, string $messageId, Outgoing $message) : PromiseInterface
A photo's text is its caption, which is a different call with a quarter of the room, so what was sent as a photo is remembered.
Parameters
- $target : string
- $messageId : string
- $message : Outgoing
Return values
PromiseInterfacefetchMedia()
Downloads a file somebody sent in Telegram, for re-uploading to Discord.
public
fetchMedia(Media $media) : PromiseInterface<string|int, array{filename: string, content: string}|null>
The bytes, never the URL: the URL has the token in it. Declined up front when the file is known to be too big for Discord, and after the download when it turns out to be — Telegram often omits the size.
Parameters
- $media : Media
Return values
PromiseInterface<string|int, array{filename: string, content: string}|null>getConfig()
public
getConfig() : TelegramConfig
Return values
TelegramConfiggetGateway()
The gateway, for a caller that needs its pacing rather than the raw client — sending a photo, most obviously.
public
getGateway() : TelegramGateway
Tags
Return values
TelegramGatewaygetTelegram()
public
getTelegram() : Telegram
Return values
Telegramjoined()
Every bridged chat the bot could see when it last looked.
public
joined() : array<int, string>
Derived from what the API answered rather than from the configuration: the point of the startup check is to catch the two disagreeing.
Return values
array<int, string>label()
What to call the network in front of a human: `Twitch`, `Telegram`.
public
label() : string
Return values
stringmodules()
The buttons a panel carries, which are component interactions rather than commands and so cannot be actions.
public
modules() : array<int, Module>
Return values
array<int, Module>name()
A short, lowercase, stable name — `twitch`, `telegram`.
public
name() : string
It is not only a label. It keys this connector's bridges in Store, and it is the top-level slash command the connector publishes, so changing it strands both.
Return values
stringnormalise()
Turns however somebody referred to a room into the form this connector addresses it by, or `null` when it is not a room reference at all.
public
normalise(string $input) : string|null
twitch.tv/Foo and #Foo are both foo; t.me/name is @name. The
result is what gets stored, so it must be stable — normalising one way on
Monday and another on Tuesday orphans every bridge made before the change.
Parameters
- $input : string
Return values
string|nullonIncoming()
Registers the handler every inbound message is passed to.
public
onIncoming(callable $handler) : void
The connector is responsible for setting Incoming::$own on anything the bot itself said. A bridge that repeats its own output is an infinite loop that gets the account banned from both networks within minutes, and only the connector can recognise its own voice.
Parameters
- $handler : callable
queued()
How many outbound messages are waiting on this connector's own pacing.
public
queued() : int
Return values
intrelay()
Renders a Discord message for this network and says it.
public
relay(string $target, Outgoing $message) : PromiseInterface<string|int, string|null>
The rendering is the connector's because the answer is: IRC cannot carry a newline, Telegram's HTML mode needs exactly three characters escaped, and the limits are 500 and 4096. MessageText holds the parts that are the same everywhere — resolving mentions, budgeting attachment links, truncating on a character boundary — so a connector writes only what is genuinely its own.
Resolves to null when there was nothing worth relaying, which is not a
failure: an embed-only message says nothing a chat can repeat.
Parameters
- $target : string
- $message : Outgoing
Return values
PromiseInterface<string|int, string|null> —The id of what was said, when the network has them.
resolve()
Looks a chat up, by id or `@username`.
public
resolve(string $target) : PromiseInterface<string|int, Room|null>
The room is keyed by the numeric id whichever was typed: a username can be changed or given away, and a bridge stored under one would follow it.
Not cached. Nothing calls this per message, and a cached "not found"
would keep refusing a group for as long as the bot runs — including the
one somebody has just added it to, which is exactly when link is
retried.
Parameters
- $target : string
Return values
PromiseInterface<string|int, Room|null>send()
Sends text, escaped unless `html` says it is already Telegram HTML.
public
send(string $target, string $text[, array{html?: bool, reply_to?: int|string|null} $options = [] ]) : PromiseInterface
Parameters
- $target : string
- $text : string
- $options : array{html?: bool, reply_to?: int|string|null} = []
Return values
PromiseInterfacesendMedia()
Sends a picture or file into a room.
public
sendMedia(string $target, Media $media[, Outgoing|null $message = null ]) : PromiseInterface<string|int, string|null>
The whole Outgoing comes with it rather than a finished caption, because composing one is the connector's business: the caption limit is not the message limit on any network that has both, and the escaping is the connector's own.
Rejects when the network will not take it — too large, wrong type, a link it cannot reach — and the relay falls back to sending it as a link in the text, which every network can carry.
Parameters
Return values
PromiseInterface<string|int, string|null> —The id of what was sent, when the network has them.
start()
Identifies the bot and starts the long poll.
public
start() : PromiseInterface<string|int, mixed>
The listeners go on first, so nothing that arrives in the first poll is missed. A failure — a revoked token, no route to Telegram — is handed back redacted, and the core reports this connector as down without taking the others with it.
Return values
PromiseInterface<string|int, mixed>stop()
Stops the long poll. The loop is Discord's, and keeps running.
public
stop() : void
surface()
Telegram takes 4096 characters and renders its own HTML, not markdown.
public
surface() : Surface
Return values
Surfacesync()
Telegram has nothing to join: the bot is in a chat because somebody added it, and it cannot let itself in.
public
sync(Links $links) : array{join: list, part: list}
So there is no diff to apply — but the chats are checked, because a bridge to a group the bot was removed from looks exactly like a quiet one and the startup check is the only thing that will say otherwise.
Parameters
- $links : Links
Return values
array{join: listauthor()
private
author(Message $message) : string
Parameters
- $message : Message
Return values
stringcaption()
A photo's caption: the relayed text, or at least who sent it.
private
caption(Outgoing $message) : string
Parameters
- $message : Outgoing
Return values
stringdescribe()
What a Telegram message was carrying, as the core's own {@see Attachment}.
private
describe(array<string, mixed> $raw) : Media|null
The URL is always null, and that is the important part: turning a
file_id into a link means asking the Bot API, and the link it returns
has the bot token in its path. Handing the core null means the relay
names the file — or asks fetchMedia() for the bytes — rather than
publishing a credential into a Discord channel.
Parameters
- $raw : array<string, mixed>
Return values
Media|nulldispatch()
Turns a TelegramPHP message into the core's own shape, running it as a command first if it is one.
private
dispatch(Message $message, bool $edited) : void
The chat title is remembered on the way past, so a listing can name a group that has since been renamed without going and asking.
Parameters
- $message : Message
- $edited : bool
firstPhoto()
The first picture Telegram was handed by URL — the one a photo was sent as.
private
static firstPhoto(Outgoing $message) : Media|null
Parameters
- $message : Outgoing
Return values
Media|nullisGif()
Whether an image is a GIF: by the type the sender's platform reported, or by its name when it reported none.
private
static isGif(Media $media) : bool
Parameters
- $media : Media
Return values
boolisGone()
Whether an error means the chat is not there for this bot: it does not exist, or the bot was removed from it.
private
static isGone(Throwable $e) : bool
Parameters
- $e : Throwable
Return values
boolmessageId()
The message id out of whatever TelegramPHP handed back.
private
static messageId(mixed $message) : string|null
Parameters
- $message : mixed
Return values
string|nullpublicLink()
The message's public page on t.me, when its chat has a public username.
private
static publicLink(array<string, mixed> $raw) : string|null
t.me/<chat>/<id> opens for anyone and previews the picture, so a
network that can only carry text still gets something to click. A private
chat's link (t.me/c/…) opens only for its members, so it gets none.
Parameters
- $raw : array<string, mixed>
Return values
string|nullquoted()
A one-line quote of what a Telegram message was replying to.
private
quoted(Message $message) : string|null
Discord shows a reply as a link to the original; there is no such link across the bridge, so the context has to be carried in the text or it is lost entirely.
Parameters
- $message : Message
Return values
string|nullraw()
A message as plain arrays, all the way down.
private
static raw(Message $message) : array<string, mixed>
jsonSerialize() is one level deep: the sizes of a photo come back as
PhotoSize parts, not arrays, and Media::describe() — which reads
arrays — finds no file in them at all. Encoding runs every nested
part's own serialisation.
Parameters
- $message : Message