SlashAdapter
in package
Registers actions that carry a {@see Slash} spec as Discord slash commands.
The surface that reuses the most: an action's handler is not touched at all.
Discord's typed options are rendered back into exactly the text the prefix
form would have produced — a channel picker becomes <#id> — so
/twitch link target:x and !twitch link x run the same code down to the
argument indices.
The shape of the tree
One command per qualifier, and the action's group decides how deep it sits:
/twitch link (no group)
/twitch channel title (group: channel)
Discord caps a command at 25 options and allows exactly one level of sub-command group, which is the whole reason groups exist — a connector with thirty commands does not fit flat. What it does not do is let the qualifier be dropped; a chat surface does that, this one does not.
Slash commands matter beyond convenience. Message Content is a privileged intent; a server that has not granted it gets no prefix commands at all, but slash commands keep working, because Discord delivers the arguments rather than the bot reading them out of a message.
Tags
Table of Contents
Constants
- MAX_DESCRIPTION : mixed = 100
- And on a description.
- MAX_OPTIONS : mixed = 25
- Discord's cap on options — sub-commands and groups included.
Properties
Methods
- __construct() : mixed
- register() : void
- Defines the commands with Discord, and starts listening for them.
- argumentsFor() : Arguments
- Turns an interaction's typed options into the arguments a prefix handler expects.
- build() : CommandBuilder
- Assembles one qualifier's actions into a single command definition.
- builder() : MessageBuilder
- Every outbound message: clamped, and pinging nobody.
- clampDescription() : string
- contextFor() : PromiseInterface<string|int, Context>
- Builds the invocation context, resolving which room this Discord channel acts on. Mirrors {@see DiscordAdapter::contextFor()}.
- define() : void
- describe() : string
- Discord caps a description at 100 characters and rejects an empty one.
- describeQualifier() : string
- fail() : void
- invoke() : void
- leafOptions() : mixed
- Walks the interaction down to the invoked leaf's own options.
- logFailure() : void
- lowestAccess() : Access
- option() : Option
- publish() : bool
- Creates the command, updates it when this build defines something different, and leaves it alone otherwise.
- rendered() : string|MessageBuilder|null
- reply() : void
- settle() : PromiseInterface<string|int, string|MessageBuilder|null>
- Normalises a handler's return into a promise of what to send.
- subCommand() : Option
- tree() : array<string, array<int, Action>>
- Every slash-capable action, grouped by the command it belongs under.
Constants
MAX_DESCRIPTION
And on a description.
public
mixed
MAX_DESCRIPTION
= 100
MAX_OPTIONS
Discord's cap on options — sub-commands and groups included.
public
mixed
MAX_OPTIONS
= 25
Properties
$bot read-only
private
Bot
$bot
$registry read-only
private
ActionRegistry
$registry
Methods
__construct()
public
__construct(Bot $bot, ActionRegistry $registry) : mixed
Parameters
- $bot : Bot
- $registry : ActionRegistry
register()
Defines the commands with Discord, and starts listening for them.
public
register() : void
argumentsFor()
Turns an interaction's typed options into the arguments a prefix handler expects.
private
argumentsFor(Action $action, Interaction $interaction) : Arguments
Each value is recorded twice: by name, and positionally in declaration
order. That is what lets one handler read get(0) and another
named('target') against the same invocation.
Positional recording stops at the first option the user omitted. Carrying
on would shift every later value down an index, so a handler reading
get(1) would silently receive what was meant for get(2) — the kind of
bug that bans the wrong person.
Parameters
- $action : Action
- $interaction : Interaction
Return values
Argumentsbuild()
Assembles one qualifier's actions into a single command definition.
private
build(string $qualifier, array<int, Action> $actions) : CommandBuilder
Parameters
- $qualifier : string
- $actions : array<int, Action>
Return values
CommandBuilderbuilder()
Every outbound message: clamped, and pinging nobody.
private
builder(string $text) : MessageBuilder
Parameters
- $text : string
Return values
MessageBuilderclampDescription()
private
clampDescription(string $description) : string
Parameters
- $description : string
Return values
stringcontextFor()
Builds the invocation context, resolving which room this Discord channel acts on. Mirrors {@see DiscordAdapter::contextFor()}.
private
contextFor(Action $action, Interaction $interaction, Access $access, Arguments $arguments, bool $isPublic) : PromiseInterface<string|int, Context>
Parameters
- $action : Action
- $interaction : Interaction
- $access : Access
- $arguments : Arguments
- $isPublic : bool
Return values
PromiseInterface<string|int, Context>define()
private
define(array<string, array<int, Action>> $tree, GlobalCommandRepository $repo) : void
Parameters
- $tree : array<string, array<int, Action>>
- $repo : GlobalCommandRepository
describe()
Discord caps a description at 100 characters and rejects an empty one.
private
describe(Action $action) : string
Parameters
- $action : Action
Return values
stringdescribeQualifier()
private
describeQualifier(string $qualifier, array<int, Action> $actions) : string
Parameters
- $qualifier : string
- $actions : array<int, Action>
Return values
stringfail()
private
fail(Interaction $interaction, Action $action, Throwable $e) : void
Parameters
- $interaction : Interaction
- $action : Action
- $e : Throwable
invoke()
private
invoke(Action $action, Interaction $interaction) : void
Parameters
- $action : Action
- $interaction : Interaction
leafOptions()
Walks the interaction down to the invoked leaf's own options.
private
leafOptions(Action $action, Interaction $interaction) : mixed
Discord nests these the same way the command is declared: a group holds a sub-command, which holds the values. Reading the top level instead would find the sub-command's name where a value was expected.
Parameters
- $action : Action
- $interaction : Interaction
logFailure()
private
logFailure(Action $action, Throwable $e) : void
Parameters
- $action : Action
- $e : Throwable
lowestAccess()
private
lowestAccess(array<int, Action> $actions) : Access
Parameters
- $actions : array<int, Action>
Return values
Accessoption()
private
option(SlashOption $option) : Option
Parameters
- $option : SlashOption
Return values
Optionpublish()
Creates the command, updates it when this build defines something different, and leaves it alone otherwise.
private
publish(string $qualifier, array<int, Action> $actions, GlobalCommandRepository $repo) : bool
Neither extreme works. Skipping whatever is already there means a command keeps the shape it had the first time it was published — add a sub-command, restart, and it is routed in code but never offered by Discord, so the handler cannot be reached. Upserting on every boot reaches Discord but costs one rate-limited write per command per restart to republish definitions that did not change, and a global command takes up to an hour to propagate, so rewriting it restarts that clock for nothing.
CommandSync tells the two apart, leniently enough that the fields
Discord adds on the way back — id, version, defaults it fills in, key
order — do not read as a change.
Parameters
- $qualifier : string
- $actions : array<int, Action>
- $repo : GlobalCommandRepository
Return values
bool —Whether a write was issued.
rendered()
private
static rendered(mixed $value) : string|MessageBuilder|null
Parameters
- $value : mixed
Return values
string|MessageBuilder|nullreply()
private
reply(Interaction $interaction, string|MessageBuilder $reply) : void
Parameters
- $interaction : Interaction
- $reply : string|MessageBuilder
settle()
Normalises a handler's return into a promise of what to send.
private
settle(mixed $result) : PromiseInterface<string|int, string|MessageBuilder|null>
A handler may answer with a string, or with a MessageBuilder when a sentence will not do — a Components v2 panel with buttons that stay live, most obviously. A builder only renders here, so an action that returns one must declare itself Discord-only; a chat has nothing to do with it.
Parameters
- $result : mixed
Return values
PromiseInterface<string|int, string|MessageBuilder|null>subCommand()
private
subCommand(Action $action) : Option
Parameters
- $action : Action
Return values
Optiontree()
Every slash-capable action, grouped by the command it belongs under.
private
tree() : array<string, array<int, Action>>