DiscordPHP-BridgeBot Documentation

TwitchConnector
in package
implements Connector, ProvidesActions, Avatars

FinalYes

Twitch, as far as the bridge is concerned.

Owns the TwitchPHP client, the IRC membership, the Helix dispatcher and the device-code recovery, and hands the core Incoming messages and Room descriptions like any other connector. Nothing above this knows what IRC is.

Twitch::run() would call Loop::run() itself, so the client is started through bootstrap() and Discord::run() is left to drive the loop.

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.
Avatars
A connector that can say what somebody looks like.

Constants

NAME  : mixed = 'twitch'
The name this connector is addressed by, in the store and in chat.
SCOPES  : array<int, string> = [ // Reading and speaking in chat: the relay, a...
Exactly the scopes the built-in commands need — no more.
USER_CACHE  : mixed = 1000
How many user rows to keep. Every new chatter is one, for their avatar.

Properties

$adapter  : TwitchAdapter|null
$bot  : Bot
$config  : TwitchConfig
$dispatcher  : RepositoryDispatcher
$gateway  : TwitchGateway|null
$handlers  : array<int, callable(Incoming): void>
$pending  : array<string, PromiseInterface<string|int, array{id: string, login: string, display_name: string, description: string, avatar: string}|null>>
$twitch  : Twitch
$users  : array<string, array{id: string, login: string, display_name: string, description: string, avatar: string}|null>

Methods

__construct()  : mixed
actions()  : array<int, Action>
Every command this connector defines.
avatarFor()  : PromiseInterface<string|int, string|null>
A chatter's profile picture, for the Discord copy of what they said.
boot()  : void
Builds the client and registers everything that must exist before the first message arrives. Called once, when the connector is added.
getConfig()  : TwitchConfig
getDispatcher()  : RepositoryDispatcher
getTwitch()  : Twitch
joined()  : array<int, string>
Every room the connector is currently in.
label()  : string
What to call the network in front of a human: `Twitch`, `Telegram`.
lookupUser()  : PromiseInterface<string|int, array{id: string, login: string, display_name: string, description: string, avatar: string}|null>
One Helix user row by login, cached — including a user that does not exist, so a typo is not re-queried on every relayed message.
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 channel up by login.
send()  : PromiseInterface
Says something in a room, exactly as given.
start()  : PromiseInterface<string|int, mixed>
Connects, and starts answering commands in chat.
stop()  : void
Disconnects cleanly, for shutdown.
surface()  : Surface
Twitch chat takes 500 characters of flat text on a single line.
sync()  : array{join: list, part: list}
Joins and leaves whatever the routing table now says, and reports what it did.
dispatch()  : void
Turns a TwitchPHP chat message into the core's own shape.
promptForDeviceCode()  : void
reauthorizer()  : DeviceCodeReauthorizer
The device-code reauthorizer, and the prompt that reaches a human.
warnIfCannotRefresh()  : void
warnIfTokenIsSomeoneElse()  : void
Says so when the token belongs to a different account than TWITCH_NICK.

Constants

NAME

The name this connector is addressed by, in the store and in chat.

public mixed NAME = 'twitch'

SCOPES

Exactly the scopes the built-in commands need — no more.

public array<int, string> SCOPES = [ // Reading and speaking in chat: the relay, and every command reply. 'chat:read', 'chat:edit', // `title`, `game`, `tags`, and `marker`. 'channel:manage:broadcast', // `followers`. 'moderator:read:followers', // `clip`. 'clips:edit', // `ban`, `unban`, `timeout`. 'moderator:manage:banned_users', // `clear`. 'moderator:manage:chat_messages', // `announce`. 'moderator:manage:announcements', // `shoutout`. 'moderator:manage:shoutouts', // `slow`, `subonly`, `emoteonly`, `followersonly`. 'moderator:manage:chat_settings', // `vip`, `unvip`. 'channel:manage:vips', // `mod`, `unmod`. 'channel:manage:moderators', // `raid`, `unraid`. 'channel:manage:raids', // `commercial`. 'channel:edit:commercial', ]

Each line names what stops working without it. Nothing is requested speculatively: every extra scope makes the consent screen longer and scarier, and grants the bot authority it has no code to use.

Two families, and the difference decides what the bot can act on:

  • channel:* requires the token to belong to the broadcaster, or to an account they have added as a channel editor. These only ever work on the channel that authorized the bot.
  • moderator:* works on any channel where the bot account is a moderator, which is what lets one bot moderate many channels.

So title, vip, mod, raid and commercial act on the authorizing account's own channel, while ban, timeout, clear, announce and the chat modes work anywhere the bot is modded.

ApiActions can reach endpoints beyond this list. That is deliberate and not a reason to widen it: an uncovered call fails with a MissingScopeException naming the scope it wanted, which is a better outcome than holding every permission on the chance somebody types one.

Verified against the Twitch OpenAPI description rather than the docs pages.

USER_CACHE

How many user rows to keep. Every new chatter is one, for their avatar.

private mixed USER_CACHE = 1000

Properties

$pending

private array<string, PromiseInterface<string|int, array{id: string, login: string, display_name: string, description: string, avatar: string}|null>> $pending = []

Lookups in flight, so a burst from one chatter is one request.

$users

private array<string, array{id: string, login: string, display_name: string, description: string, avatar: string}|null> $users = []

login => cached user row, negatives included.

Methods

avatarFor()

A chatter's profile picture, for the Discord copy of what they said.

public avatarFor(Incoming $message) : PromiseInterface<string|int, string|null>

Helix's profile_image_url is a public CDN link with nothing secret in it. Never rejects: a missing avatar is not worth losing the message over.

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

joined()

Every room the connector is currently in.

public joined() : array<int, string>

The startup check compares this against what was restored from disk: a join that silently failed leaves a bridge that works in one direction only, which is invisible from the configuration alone.

Return values
array<int, string>

label()

What to call the network in front of a human: `Twitch`, `Telegram`.

public label() : string
Return values
string

lookupUser()

One Helix user row by login, cached — including a user that does not exist, so a typo is not re-queried on every relayed message.

public lookupUser(string $login) : PromiseInterface<string|int, array{id: string, login: string, display_name: string, description: string, avatar: string}|null>

Resolves null only when Twitch says there is no such user. A lookup that failed rejects: it is not proof of absence, and a moderation command must not treat it as one.

Parameters
$login : string
Return values
PromiseInterface<string|int, array{id: string, login: string, display_name: string, description: string, avatar: string}|null>

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 channel up by login.

public resolve(string $target) : PromiseInterface<string|int, Room|null>

The room is keyed by login — what IRC joins and what the store has held since the single-platform bot — and carries the Helix user id as its Room::apiId(), which is what every Helix call wants as broadcaster_id.

Parameters
$target : string
Return values
PromiseInterface<string|int, Room|null>

send()

Says something in a room, exactly as given.

public send(string $target, string $text[, array<string|int, mixed> $options = [] ]) : PromiseInterface

For text this bot composed itself — a command's reply, a notice. Relayed chat goes through relay() instead, which is the one that still has to be rendered.

Parameters
$target : string
$text : string
$options : array<string|int, mixed> = []

Connector-specific extras — a reply target, a file to attach. Unknown keys are ignored.

Return values
PromiseInterface

start()

Connects, and starts answering commands in chat.

public start() : PromiseInterface<string|int, mixed>

bootstrap() resolves once the token is usable and IRC is up; anything that needs a live client happens inside it. A rejection is handed back to the core, which reports this connector as down without taking the others with it.

Return values
PromiseInterface<string|int, mixed>

surface()

Twitch chat takes 500 characters of flat text on a single line.

public surface() : Surface

lines: false is not a stylistic note — a newline in a PRIVMSG is an IRC injection, so the core must never assume one can be sent.

Return values
Surface

sync()

Joins and leaves whatever the routing table now says, and reports what it did.

public sync(Links $links) : array{join: list, part: list}

A diff rather than "rejoin everything", so one server's change does not blink every other server's bridge offline.

Parameters
$links : Links
Return values
array{join: list, part: list}

dispatch()

Turns a TwitchPHP chat message into the core's own shape.

private dispatch(ChatMessage $message) : void

The gateway has already dropped the bridge's own lines, so everything that reaches here was typed by a person — the host included, since the bot speaks as the host's account. Their messages are relayed and their commands answered like anyone else's, at the rank Twitch gives them.

Parameters
$message : ChatMessage

promptForDeviceCode()

private promptForDeviceCode(array<string, mixed> $device) : void
Parameters
$device : array<string, mixed>

reauthorizer()

The device-code reauthorizer, and the prompt that reaches a human.

private reauthorizer() : DeviceCodeReauthorizer

Device-code is the one grant OAuth::form() allows without a client secret, so this is the only route back to a working token when none is configured.

The prompt goes two places: the log, always, and a DM to the bot's owner when one is set. A bot that needs re-authorization and only whispers it into a logfile stays down until somebody looks.

Return values
DeviceCodeReauthorizer

warnIfTokenIsSomeoneElse()

Says so when the token belongs to a different account than TWITCH_NICK.

private warnIfTokenIsSomeoneElse() : void

Twitch sends chat as whoever the token belongs to, whatever nick the connection claims — so a token approved while logged in as the streamer makes the relay speak as the streamer. Nothing fails, which is what makes it worth saying: the first sign is a relayed message under the wrong name.

On this page

Search results