Skip to content

Commit

Permalink
✨ Add persistent interaction routing support to commands (Fixes #79)
Browse files Browse the repository at this point in the history
  • Loading branch information
Log1x committed Jun 5, 2024
1 parent 3d506dc commit 91ea28a
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 24 deletions.
10 changes: 9 additions & 1 deletion src/Commands/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ public static function make(Laracord $bot): self
return new static($bot);
}

/**
* The command interaction routes.
*/
public function interactions(): array
{
return [];
}

/**
* Build an embed for use in a Discord message.
*
Expand All @@ -106,7 +114,7 @@ public static function make(Laracord $bot): self
*/
public function message($content = '')
{
return $this->bot()->message($content);
return $this->bot()->message($content)->routePrefix($this->getName());
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/Commands/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Laracord\Commands;

use Laracord\Commands\Contracts\Command as CommandContract;
use Laracord\Discord\Message;

abstract class Command extends AbstractCommand implements CommandContract
{
Expand Down
134 changes: 112 additions & 22 deletions src/Laracord.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Laracord;

use Discord\DiscordCommandClient as Discord;
use Discord\Parts\Interactions\Interaction;
use Discord\WebSockets\Event as DiscordEvent;
use Discord\WebSockets\Intents;
use Exception;
use Illuminate\Contracts\Foundation\Application;
Expand Down Expand Up @@ -113,6 +115,11 @@ class Laracord
*/
protected array $services = [];

/**
* The bot interaction routes.
*/
protected array $interactions = [];

/**
* The console input stream.
*
Expand Down Expand Up @@ -195,7 +202,8 @@ public function boot(): void
->registerEvents()
->bootServices()
->bootHttpServer()
->registerSlashCommands();
->registerSlashCommands()
->handleInteractions();

$this->afterBoot();

Expand Down Expand Up @@ -229,7 +237,7 @@ public function boot(): void
/**
* Boot the Discord client.
*/
public function bootDiscord(): void
protected function bootDiscord(): void
{
$this->discord = new Discord([
'token' => $this->getToken(),
Expand All @@ -243,7 +251,7 @@ public function bootDiscord(): void
/**
* Register the input and output streams.
*/
public function registerStream(): self
protected function registerStream(): self
{
if (windows_os()) {
return $this;
Expand All @@ -264,7 +272,7 @@ public function registerStream(): self
/**
* Handle the input stream.
*/
public function handleStream(string $data): void
protected function handleStream(string $data): void
{
$command = trim($data);

Expand Down Expand Up @@ -393,6 +401,8 @@ protected function registerCommands(): self
);

$this->registeredCommands[] = $command;

$this->registerInteractions($command->getName(), $command->interactions());
}

return $this;
Expand All @@ -401,7 +411,7 @@ protected function registerCommands(): self
/**
* Handle the bot slash commands.
*/
protected function registerSlashCommands()
protected function registerSlashCommands(): self
{
$existing = cache()->get('laracord.slash-commands');

Expand Down Expand Up @@ -527,15 +537,18 @@ protected function registerSlashCommands()
}

if ($registered->isEmpty()) {
return;
return $this;
}

$registered->each(fn ($command, $name) => $this->discord()->listenCommand(
$name,
fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandle($interaction))
));
$registered->each(function ($command, $name) {
$this->discord()->listenCommand($name, fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandle($interaction)));

$this->registerInteractions($name, $command['state']->interactions());
});

$this->registeredCommands = array_merge($this->registeredCommands, $registered->pluck('state')->all());

return $this;
}

/**
Expand Down Expand Up @@ -586,10 +599,26 @@ public function unregisterSlashCommand(string $id, ?string $guildId = null): voi
$this->discord()->application->commands->delete($id)->done();
}

/**
* Register the interaction routes.
*/
protected function registerInteractions(string $name, array $routes = []): void
{
$routes = collect($routes)
->mapWithKeys(fn ($value, $route) => ["{$name}@{$route}" => $value])
->all();

if (! $routes) {
return;
}

$this->interactions = array_merge($this->interactions, $routes);
}

/**
* Register the Discord events.
*/
public function registerEvents(): self
protected function registerEvents(): self
{
foreach ($this->getEvents() as $event) {
$this->handleSafe($event, function () use ($event) {
Expand All @@ -611,7 +640,7 @@ public function registerEvents(): self
/**
* Boot the bot services.
*/
public function bootServices(): self
protected function bootServices(): self
{
foreach ($this->getServices() as $service) {
$this->handleSafe($service, function () use ($service) {
Expand All @@ -631,24 +660,62 @@ public function bootServices(): self
}

/**
* Safely handle the provided callback.
* Handle the interaction routes.
*/
public function handleSafe(string $name, callable $callback): mixed
protected function handleInteractions(): self
{
try {
return $callback();
} catch (Throwable $e) {
$this->console()->error("An error occurred in <fg=red>{$name}</>.");
$this->console()->outputComponents()->bulletList([$e->getMessage()]);
}
$this->discord()->on(DiscordEvent::INTERACTION_CREATE, function (Interaction $interaction) {
$id = $interaction->data->custom_id;

return null;
$handlers = collect($this->getInteractions())
->partition(fn ($route, $name) => ! Str::contains($name, '{'));

$static = $handlers[0];
$dynamic = $handlers[1];

if ($route = $static->get($id)) {
return $this->handleSafe($id, fn () => $route($interaction));
}

if (! $route) {
$route = $dynamic->first(fn ($route, $name) => Str::before($name, ':') === Str::before($id, ':'));
}

if (! $route) {
return;
}

$parameters = [];
$requiredParameters = [];

if (Str::contains($id, ':')) {
$parameters = explode(':', Str::after($id, ':'));
}

$routeName = $dynamic->keys()->first(fn ($name) => Str::before($name, ':') === Str::before($id, ':'));

if ($routeName && preg_match_all('/\{(.*?)\}/', $routeName, $matches)) {
$requiredParameters = $matches[1];
}

foreach ($requiredParameters as $index => $param) {
if (! Str::endsWith($param, '?') && (! isset($parameters[$index]) || $parameters[$index] === '')) {
$this->console()->error("Missing required parameter `{$param}` for interaction route `{$routeName}`.");

return;
}
}

$this->handleSafe($id, fn () => $route($interaction, ...$parameters));
});

return $this;
}

/**
* Boot the HTTP server.
*/
public function bootHttpServer(): self
protected function bootHttpServer(): self
{
if ($this->httpServer) {
return $this;
Expand Down Expand Up @@ -871,6 +938,14 @@ public function getSlashCommands(): array
return $this->slashCommands = $slashCommands;
}

/**
* Get the bot interaction routes.
*/
public function getInteractions(): array
{
return $this->interactions;
}

/**
* Extract classes from the provided application path.
*/
Expand Down Expand Up @@ -1026,6 +1101,21 @@ public function getApplication(): Application
return $this->app;
}

/**
* Safely handle the provided callback.
*/
public function handleSafe(string $name, callable $callback): mixed
{
try {
return $callback();
} catch (Throwable $e) {
$this->console()->error("An error occurred in <fg=red>{$name}</>.");
$this->console()->outputComponents()->bulletList([$e->getMessage()]);
}

return null;
}

/**
* Build an embed for use in a Discord message.
*
Expand Down

0 comments on commit 91ea28a

Please sign in to comment.