2026-02-14 17:38:02 +00:00

214 lines
6.3 KiB
PHP

<?php
/**
* An example that registers a slash command with an autocomplete callback.
*
* Type "/roll" in chat, the option "sides" will trigger the autocomplete callback, listing available options.
*
* Run this example bot from main directory using command:
* php examples/command.php
*/
declare(strict_types = 1);
use Discord\Builders\CommandBuilder;
use Discord\Builders\MessageBuilder;
use Discord\Discord;
use Discord\Helpers\Collection;
use Discord\Parts\Interactions\ApplicationCommand;
use Discord\Parts\Interactions\ApplicationCommandAutocomplete;
use Discord\Parts\Interactions\Command\Choice;
use Discord\Parts\Interactions\Command\Command;
use Discord\Parts\Interactions\Command\Option;
use Discord\Parts\Interactions\Request\Option as RequestOption;
use Discord\Repository\Guild\GuildCommandRepository;
use Discord\Repository\Interaction\GlobalCommandRepository;
use Discord\WebSockets\Intents;
require_once __DIR__.'/../vendor/autoload.php';
ini_set('memory_limit', -1);
/**
* Class to handle the command callbacks
*
* We're going to roll dice
*/
class DiceRollHandler
{
public const NAME = 'roll';
public function __construct(
protected Discord $discord,
) {
// noop
}
/**
* Create the command to be saved.
*
* @return Command The command
*/
public function buildCommand(GlobalCommandRepository|GuildCommandRepository $repository): Command
{
// an option "sides"
$sides = (new Option($this->discord))
->setType(Option::INTEGER)
->setName('sides')
->setDescription('sides on the die')
->setAutoComplete(true);
// the command "roll"
return CommandBuilder::new()
->setType(Command::CHAT_INPUT)
->setName(static::NAME)
->setDescription('rolls an n-sided die')
->addOption($sides)
->create($repository);
}
/**
* Register a global slash command.
*
* @param string|null $reason Reason for registering the command.
* @param bool $update Whether to update the command if it already exists.
*
* @return static
*/
public function register(GlobalCommandRepository|GuildCommandRepository $repository, ?string $reason = null, bool $update = false): static
{
// If the the command was created successfully you don't need to create it again
if (! $update && $repository->get('name', static::NAME)) {
return $this;
}
$this->buildCommand($repository)->save($reason);
return $this;
}
/**
* Attempt to delete the command.
*
* @param string|null $reason Reason for deleting the command.
*
* @return static
*/
public function delete(?string $reason = null): static
{
$repository = $this->discord->application->commands;
$command = $repository->get('name', static::NAME);
if ($command) {
$repository->delete($command, $reason);
}
return $this;
}
/**
* Add listener(s) for the command and possible subcommands.
*
* @return static
*/
public function listen(): static
{
$registeredCommand = $this->discord->listenCommand(DiceRollHandler::NAME, $this->execute(...), $this->autocomplete(...));
// you may register different handlers for each subcommand here
# foreach(['subcommand1', 'subcommand2', /*...*/] as $subcommand){
# $registeredCommand->addSubCommand($subcommand, $this->execute(...), $this->autocomplete(...));
# }
return $this;
}
/**
* The command callback.
*
* @param ApplicationCommand $interaction The interaction object.
*/
public function execute(ApplicationCommand $interaction, Collection $params): void
{
$sides = ($interaction->data->options->get('name', 'sides')?->value ?? 20);
// sanity check
if (! in_array($sides, [4, 6, 8, 10, 12, 20], true)) {
$sides = 20;
}
$message = sprintf('%s rolled %s with a %s-sided die', $interaction->user, random_int(1, $sides), $sides);
// respond to the command with an interaction message
$interaction->respondWithMessage((new MessageBuilder)->setContent($message));
}
/**
* The autocomplete callback.
*
* Must return array to trigger a response.
*
* @param ApplicationCommandAutocomplete $interaction The interaction object.
*
* @return array<Choice>|null An array of Choice objects or null to not respond.
*/
public function autocomplete(ApplicationCommandAutocomplete $interaction): array|null
{
// respond if the desired option is focused
/** @var ?RequestOption */
$option = $interaction->data->options->get('name', 'sides');
if ($option && $option->focused) {
// the dataset, e.g. fetched from a database (25 results max)
$dataset = [4, 6, 8, 10, 12, 20];
$choices = [];
foreach ($dataset as $sides) {
$choices[] = new Choice($this->discord, ['name' => sprintf('%s-sided', $sides), 'value' => $sides]);
}
return $choices;
}
return null;
}
}
/**
* Invoke the discord client
*
* MESSAGE_CONTENT, GUILD_MEMBERS and GUILD_PRESENCES are privileged
*
* @see https://dis.gd/mcfaq
*/
$dc = new Discord([
// https://discord.com/developers/applications/<APP_ID>>/bot
'token' => 'YOUR_DISCORD_BOT_TOKEN',
'intents' => (Intents::getDefaultIntents() | Intents::MESSAGE_CONTENT),
]);
/**
* When the bot is ready
*
* IMPORTANT: Avoid calling freshen() multiple times to prevent rate limiting
*/
$dc->on('init', function (Discord $discord): void
{
echo "Bot is ready!\n";
// invoke the command handler
$commandHandler = new DiceRollHandler($discord);
// Can be GuildCommandRepository for guild-specific commands
$repository = $discord->application->commands;
// Freshen the repository prior to registering to avoid overwriting other commands
//$repository->freshen()->then(fn ($repository) =>
$commandHandler->register(repository: $repository, reason: 'Initial command registration', update: false);
//);
// add a listener for the command
$commandHandler->listen();
});
$dc->run();