DiscordPHP-BridgeBot Documentation

TelegramConnector
in package
implements Connector, ProvidesActions, ProvidesModules, Editing, Media, Avatars

FinalYes

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
author

Valithor Obsidion valithor@valgorithms.com

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

$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 = []

$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 = []

$seen

private array<string, true> $seen = []

Chats the bot could last see, for the startup check.

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.

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
PromiseInterface

fetchMedia()

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>

getGateway()

The gateway, for a caller that needs its pacing rather than the raw client — sending a photo, most obviously.

public getGateway() : TelegramGateway
Tags
throws
LogicException

before the connector has started.

Return values
TelegramGateway

joined()

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
string

modules()

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
string

normalise()

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|null

onIncoming()

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
int

relay()

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
PromiseInterface

sendMedia()

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
$target : string
$media : Media
$message : Outgoing|null = null
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

sync()

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: list, part: list}

author()

private author(Message $message) : string
Parameters
$message : Message
Return values
string

caption()

A photo's caption: the relayed text, or at least who sent it.

private caption(Outgoing $message) : string
Parameters
$message : Outgoing
Return values
string

describe()

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|null

dispatch()

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|null

isGif()

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
bool

isGone()

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
bool

messageId()

The message id out of whatever TelegramPHP handed back.

private static messageId(mixed $message) : string|null
Parameters
$message : mixed
Return values
string|null

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|null

quoted()

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|null

raw()

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
Return values
array<string, mixed>
On this page

Search results