diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index a22dce0..de897d3 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -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. * @@ -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()); } /** diff --git a/src/Commands/Command.php b/src/Commands/Command.php index 616079a..16f8941 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -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 { diff --git a/src/Laracord.php b/src/Laracord.php index f77af7c..2fbc1e6 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -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; @@ -113,6 +115,11 @@ class Laracord */ protected array $services = []; + /** + * The bot interaction routes. + */ + protected array $interactions = []; + /** * The console input stream. * @@ -195,7 +202,8 @@ public function boot(): void ->registerEvents() ->bootServices() ->bootHttpServer() - ->registerSlashCommands(); + ->registerSlashCommands() + ->handleInteractions(); $this->afterBoot(); @@ -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(), @@ -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; @@ -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); @@ -393,6 +401,8 @@ protected function registerCommands(): self ); $this->registeredCommands[] = $command; + + $this->registerInteractions($command->getName(), $command->interactions()); } return $this; @@ -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'); @@ -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; } /** @@ -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) { @@ -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) { @@ -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 {$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; @@ -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. */ @@ -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 {$name}."); + $this->console()->outputComponents()->bulletList([$e->getMessage()]); + } + + return null; + } + /** * Build an embed for use in a Discord message. *