DiscordPHP-BridgeBot Documentation

ChatRelay
in package

FinalYes

The relay itself: Discord chat out to every connector, and every connector's chat back into Discord.

The hard requirement here is that nothing the bot says can come back to it. A bridge that repeats its own output is an infinite loop that gets the account banned from both networks within minutes, so each direction drops its own traffic as early as it can — see shouldRelayFromDiscord() and Incoming::$own, which the connector sets because only it can recognise its own voice.

With more than one connector installed a single Discord message may go to several networks at once, and a single message from one network still fans out to every Discord channel following that room, across unrelated servers. Both directions are bounded — the first by how many connectors are installed, the second by OutboundPacer. A channel bridged to two networks also carries each network's chat to the other, directly, since the Discord copy of it is a webhook message and never relayed onward.

Edits

An edit is only ever applied as an edit. A network that cannot rewrite a message it sent — IRC — never hears about one, because the only alternative is posting the message a second time, and a second copy of everything somebody corrects a typo in is worse than the typo.

That matters more than it sounds, because Discord calls a lot of things an edit. When a message containing a link is unfurled into an embed, Discord sends MESSAGE_UPDATE with the content unchanged. Treating that as an edit would relay every message with a link in it twice. So an update is acted on only when the text or the attachments actually changed.

Tags
author

Valithor Obsidion valithor@discordphp.org

Table of Contents

Constants

REMEMBER  : mixed = 500
How many relayed messages to remember for editing, per direction.

Properties

$bot  : Bot
$commands  : array<string, ChatDispatcher>
$delivered  : MessageMap
$seen  : MessageMap
$sent  : MessageMap

Methods

__construct()  : mixed
attach()  : void
Attaches both directions. Call once; nothing arrives until a connector starts.
fingerprint()  : string
What a relayed message looked like, for telling a real edit from an update that changed nothing a chat can see.
isDiscordCommand()  : bool
Whether a Discord message is addressed to the bot rather than to the channel.
shouldRelayFromDiscord()  : bool
Whether a Discord message is real conversation worth relaying.
acrossNetworks()  : void
Hands a message on to every *other* network bridged to the same Discord channels, so a channel bridged to Twitch and Telegram is one conversation across all three rather than two that only Discord can see.
attachments()  : array<int, Media>
Each attachment as a {@see Media}.
authorName()  : string
bridgedConnectors()  : array<int, Connector, 1: string}>
Every connector this Discord channel is bridged through, and the room on each.
channelNames()  : array<string, string>
channelsFor()  : array<int, Channel>
Every cached Discord channel following the room a message came from.
compose()  : Outgoing
composeFromNetwork()  : Outgoing
A message from one network as another should receive it.
deliver()  : void
describeMedia()  : string
A file, as a link when there is a safe one and as a name otherwise.
editInDiscord()  : bool
Rewrites every Discord copy of an edited message, returning whether any were remembered.
fetch()  : PromiseInterface<string|int, array{filename: string, content: string}|null>
fromConnector()  : void
fromDiscord()  : void
fromDiscordEdit()  : void
An edit of something already relayed, and only that.
incomingKey()  : string
Where a message from a network is remembered. Room and id together, because a message id is only unique within its room on some networks — Telegram numbers each chat from one.
mirrorableIn()  : Media|null
The one attachment worth copying across as a file.
photoIn()  : Media|null
The one picture worth handing to the network itself, if there is one.
relayPhoto()  : PromiseInterface<string|int, string|null>
Hands the picture to a network that can carry it, and falls back to a link in the text when it will not take it.
relayTo()  : void
renderForDiscord()  : string|null
One incoming message as Discord should show it: the quoted reply, the text, and whatever files came with it.
roleNames()  : array<string, string>
suffix()  : string
Appended to a relayed name, so it is never mistaken for a Discord account.
userNames()  : array<string, string>

Constants

REMEMBER

How many relayed messages to remember for editing, per direction.

public mixed REMEMBER = 500

Properties

$delivered read-only

private MessageMap $delivered

"connector:room:id" => list of Discord copies it became

$seen read-only

private MessageMap $seen

"discordId" => fingerprint of what was relayed, to tell an edit from an unfurl

$sent read-only

private MessageMap $sent

"connector:discordId" => the id it was given on that network

Methods

__construct()

public __construct(Bot $bot[, int $remember = self::REMEMBER ]) : mixed
Parameters
$bot : Bot
$remember : int = self::REMEMBER

attach()

Attaches both directions. Call once; nothing arrives until a connector starts.

public attach() : void

fingerprint()

What a relayed message looked like, for telling a real edit from an update that changed nothing a chat can see.

public static fingerprint(Message $message) : string
Parameters
$message : Message
Return values
string

isDiscordCommand()

Whether a Discord message is addressed to the bot rather than to the channel.

public isDiscordCommand(string $content) : bool

Only a registered command counts. Chat is full of ! — "yes!!!", or another bot's !drop — and treating all of it as a command would quietly stop relaying a slice of ordinary conversation.

Parameters
$content : string
Return values
bool

shouldRelayFromDiscord()

Whether a Discord message is real conversation worth relaying.

public shouldRelayFromDiscord(Message $message) : bool

Four rejections, each for its own reason:

  • Webhook messages. Every connector's chat is delivered into Discord through a webhook, so this single check is what stops the loop. It has to come first and it has to be unconditional.
  • Bot authors. Two bridges sharing a channel would otherwise ping-pong forever, and a bot's output is rarely what a chat wants to read.
  • The bot's own messages, which is belt-and-braces: command replies are sent as the bot and would otherwise be echoed out, having already been said there.
  • Commands. !twitch title something is an instruction to the bot, not a remark; relaying it would put every command into the stream's chat.
Parameters
$message : Message
Return values
bool

acrossNetworks()

Hands a message on to every *other* network bridged to the same Discord channels, so a channel bridged to Twitch and Telegram is one conversation across all three rather than two that only Discord can see.

private acrossNetworks(Connector $source, Incoming $incoming) : void

Discord cannot make this hop itself: the copy it receives arrives through a webhook, and webhook messages are dropped unconditionally because that is what stops the loop. So it is made here, from the original.

Each room gets the message once however many channels lead to it, and an edit follows the same rule as everywhere else — applied as an edit where the network can and the copy is remembered, otherwise not at all.

Parameters
$source : Connector
$incoming : Incoming

attachments()

Each attachment as a {@see Media}.

private attachments(Message $message) : array<int, Media>

url rather than proxy_url: both are signed and both expire, but url is the canonical one, and the proxy adds nothing for a recipient who is going to open it in a browser.

Parameters
$message : Message
Return values
array<int, Media>

authorName()

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

bridgedConnectors()

Every connector this Discord channel is bridged through, and the room on each.

private bridgedConnectors(string $channelId) : array<int, Connector, 1: string}>
Parameters
$channelId : string
Return values
array<int, Connector, 1: string}>

channelNames()

private channelNames(Message $message) : array<string, string>
Parameters
$message : Message
Return values
array<string, string>

channelsFor()

Every cached Discord channel following the room a message came from.

private channelsFor(Connector $connector, Incoming $incoming) : array<int, Channel>
Parameters
$connector : Connector
$incoming : Incoming
Return values
array<int, Channel>

compose()

private compose(Message $message, bool $edited) : Outgoing
Parameters
$message : Message
$edited : bool
Return values
Outgoing

composeFromNetwork()

A message from one network as another should receive it.

private composeFromNetwork(Connector $source, Incoming $incoming, string $key) : Outgoing

Named with the network it came from, as a Discord copy is. A file only the source network can open — a Telegram photo has no URL that does not carry the bot token — goes as a link to the public page showing it when there is one, and is named otherwise.

Parameters
$source : Connector
$incoming : Incoming
$key : string
Return values
Outgoing

deliver()

private deliver(Connector $connector, Channel $channel, Incoming $incoming, string|null $text, string|null $avatar, array{filename: string, content: string}|null $file) : void
Parameters
$connector : Connector
$channel : Channel
$incoming : Incoming
$text : string|null
$avatar : string|null
$file : array{filename: string, content: string}|null

describeMedia()

A file, as a link when there is a safe one and as a name otherwise.

private describeMedia(Media $item) : string

A connector hands over null for the URL when the only link it could produce carries a credential — a Telegram file URL has the bot token in its path — so this never leaks one into a channel. A public page showing the file is the next best thing: reached when a file was too big to copy across, or the copy failed.

Parameters
$item : Media
Return values
string

editInDiscord()

Rewrites every Discord copy of an edited message, returning whether any were remembered.

private editInDiscord(Connector $connector, Incoming $incoming) : bool

When none are — the original was relayed before the last restart, or so long ago it fell out of memory — the edit is relayed as a new message rather than lost, which on a network that sends whole messages as edits is the only way the correction reaches anyone.

Parameters
$connector : Connector
$incoming : Incoming
Return values
bool

fetch()

private fetch(Connector $connector, Media $media) : PromiseInterface<string|int, array{filename: string, content: string}|null>
Parameters
$connector : Connector
$media : Media
Return values
PromiseInterface<string|int, array{filename: string, content: string}|null>

fromDiscord()

private fromDiscord(Message $message) : void
Parameters
$message : Message

fromDiscordEdit()

An edit of something already relayed, and only that.

private fromDiscordEdit(object $message) : void

Three things are not edits and are dropped: a partial update with no message part (a pin, a flag change), an update to a message this bot never relayed, and an update whose content did not change — which is what an unfurling link looks like.

Parameters
$message : object

incomingKey()

Where a message from a network is remembered. Room and id together, because a message id is only unique within its room on some networks — Telegram numbers each chat from one.

private static incomingKey(Connector $connector, Incoming $incoming) : string
Parameters
$connector : Connector
$incoming : Incoming
Return values
string

mirrorableIn()

The one attachment worth copying across as a file.

private mirrorableIn(Connector $connector, Incoming $incoming) : Media|null

Only one, and only from a connector that can fetch it: a second upload per message multiplies the bytes by every channel it fans out to.

Parameters
$connector : Connector
$incoming : Incoming
Return values
Media|null

photoIn()

The one picture worth handing to the network itself, if there is one.

private photoIn(Outgoing $outgoing) : Media|null

Only the first: sending several means a media group, which is a different call on every network that has one, and mixing files with images is not something they agree on. The rest relay as links in the text.

Parameters
$outgoing : Outgoing
Return values
Media|null

relayPhoto()

Hands the picture to a network that can carry it, and falls back to a link in the text when it will not take it.

private relayPhoto(Media|Connector $connector, string $target, Media $photo, Outgoing $outgoing) : PromiseInterface<string|int, string|null>

A network that can carry the picture should, because then it is actually there — a relayed link to Discord's CDN expires in about a day, so it is dead by the time anyone reads the logs.

Parameters
$connector : Media|Connector
$target : string
$photo : Media
$outgoing : Outgoing
Return values
PromiseInterface<string|int, string|null>

renderForDiscord()

One incoming message as Discord should show it: the quoted reply, the text, and whatever files came with it.

private renderForDiscord(Incoming $incoming, bool $sourceHasLines[, Media|null $mirrored = null ]) : string|null
Parameters
$incoming : Incoming
$sourceHasLines : bool
$mirrored : Media|null = null

An attachment that is being uploaded, so is not named as well.

Return values
string|null

roleNames()

private roleNames(Message $message) : array<string, string>
Parameters
$message : Message
Return values
array<string, string>

suffix()

Appended to a relayed name, so it is never mistaken for a Discord account.

private suffix(Connector $connector) : string
Parameters
$connector : Connector
Return values
string

userNames()

private userNames(Message $message) : array<string, string>
Parameters
$message : Message
Return values
array<string, string>
On this page

Search results