From 6f0ded6ccce8606b9e89be00a276fc6c9e0f2e0c Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 5 Feb 2025 23:11:35 -0600 Subject: [PATCH 001/146] =?UTF-8?q?=E2=9E=95=20Add=20`illuminate/log`=20to?= =?UTF-8?q?=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 1b362aa..52d808f 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "illuminate/encryption": "^11.0", "illuminate/hashing": "^11.0", "illuminate/http": "^11.0", + "illuminate/log": "^11.0", "illuminate/queue": "^11.0", "illuminate/routing": "^11.0", "illuminate/session": "^11.0", From 2aa52667aec4cdaaa2b251c5a836b97b43623dd5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 5 Feb 2025 23:11:52 -0600 Subject: [PATCH 002/146] =?UTF-8?q?=F0=9F=94=A7=20Create=20a=20default=20`?= =?UTF-8?q?logging.php`=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 132 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 config/logging.php diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..c698631 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laracord.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laracord.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laracord.log'), + ], + + ], + +]; From 70faca10cf83e4994fcd975227aae9110ec612f7 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 5 Feb 2025 23:12:23 -0600 Subject: [PATCH 003/146] =?UTF-8?q?=F0=9F=94=A7=20Remove=20deprecated=20`d?= =?UTF-8?q?iscord.php`=20configuration=20=F0=9F=94=A7=20Add=20missing=20Di?= =?UTF-8?q?scordPHP=20options=20to=20the=20`discord.php`=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/discord.php | 76 ++-------------------------------------------- 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/config/discord.php b/config/discord.php index 9f7277a..4de2fd3 100644 --- a/config/discord.php +++ b/config/discord.php @@ -4,19 +4,6 @@ return [ - /* - |-------------------------------------------------------------------------- - | Discord Bot Description - |-------------------------------------------------------------------------- - | - | Here you may specify the description of your Discord bot. This will be - | used when the bot is mentioned in chat, or when you run the "servers" - | command. Change this to anything you like. - | - */ - - 'description' => env('DISCORD_BOT_DESCRIPTION', 'The Laracord Discord Bot.'), - /* |-------------------------------------------------------------------------- | Discord Token @@ -74,6 +61,8 @@ 'options' => [ 'loadAllMembers' => true, + 'storeMessages' => false, + 'retrieveBans' => false, ], /* @@ -121,65 +110,4 @@ // ], - /* - |-------------------------------------------------------------------------- - | Additional Commands - |-------------------------------------------------------------------------- - | - | Here you may specify any additional commands for the Discord bot. These - | commands will be loaded in addition to the commands automatically loaded - | in your project. By default, the Laracord-provided help command is - | is registered here. - | - */ - - 'commands' => [ - Laracord\Commands\HelpCommand::class, - ], - - /* - |-------------------------------------------------------------------------- - | Additional Context Menus - |-------------------------------------------------------------------------- - | - | Here you may specify any additional context menus for the Discord bot. - | These context menus will be loaded in addition to the context menus - | automatically loaded in your project. - | - */ - - 'menus' => [ - // - ], - - /* - |-------------------------------------------------------------------------- - | Additional Services - |-------------------------------------------------------------------------- - | - | Here you may specify any additional services to run asynchronously - | alongside the Discord bot. These services will be loaded in addition - | to the services automatically loaded from your project. - | - */ - - 'services' => [ - // - ], - - /* - |-------------------------------------------------------------------------- - | Additional Events - |-------------------------------------------------------------------------- - | - | Here you may specify any additional events to listen for in your - | Discord bot. These events will be registered in addition to the - | events automatically registered from your project. - | - */ - - 'events' => [ - // - ], - ]; From 8caee212a5f8d361a1ed2e886d5bd40273aa83b4 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 5 Feb 2025 23:12:51 -0600 Subject: [PATCH 004/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Cre?= =?UTF-8?q?ate=20a=20global=20`laracord()`=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers.php b/src/helpers.php index 7c830b6..0eb6e71 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,5 +1,15 @@ Date: Wed, 5 Feb 2025 23:14:51 -0600 Subject: [PATCH 005/146] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20the=20`CanAsync?= =?UTF-8?q?`=20trait=20to=20`HasAsync`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Concerns/{CanAsync.php => HasAsync.php} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/Concerns/{CanAsync.php => HasAsync.php} (75%) diff --git a/src/Concerns/CanAsync.php b/src/Concerns/HasAsync.php similarity index 75% rename from src/Concerns/CanAsync.php rename to src/Concerns/HasAsync.php index f1cd4c3..2cebaaf 100644 --- a/src/Concerns/CanAsync.php +++ b/src/Concerns/HasAsync.php @@ -3,9 +3,10 @@ namespace Laracord\Concerns; use Exception; +use React\EventLoop\LoopInterface; use React\Promise\Promise; -trait CanAsync +trait HasAsync { /** * Perform an asynchronous operation. @@ -13,14 +14,13 @@ trait CanAsync public static function handleAsync(callable $callback): Promise { return new Promise(function ($resolve, $reject) use ($callback) { - if (! $loop = app('bot')?->getLoop()) { - throw new Exception('The Laracord event loop is not available.'); + if (! $loop = app(LoopInterface::class)) { + throw new Exception('The event loop is not available.'); } $loop->futureTick(function () use ($callback, $resolve, $reject) { try { - $result = $callback(); - $resolve($result); + $resolve($callback()); } catch (Exception $e) { $reject($e); } From dc033cf650c6257e10236a9a43a98becb80d54de Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 00:07:04 -0600 Subject: [PATCH 006/146] =?UTF-8?q?=F0=9F=9A=9A=20Move=20the=20`ResolvesUs?= =?UTF-8?q?er`=20trait=20=F0=9F=8E=A8=20Utilize=20the=20`Laracord`=20facad?= =?UTF-8?q?e=20when=20fetching=20token=20while=20resolving=20a=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/AdminCommand.php | 2 +- .../{ => Commands}/Concerns/ResolvesUser.php | 15 +++------------ src/Console/Commands/TokenMakeCommand.php | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) rename src/Console/{ => Commands}/Concerns/ResolvesUser.php (83%) diff --git a/src/Console/Commands/AdminCommand.php b/src/Console/Commands/AdminCommand.php index 1598df4..85301f9 100644 --- a/src/Console/Commands/AdminCommand.php +++ b/src/Console/Commands/AdminCommand.php @@ -2,7 +2,7 @@ namespace Laracord\Console\Commands; -use Laracord\Console\Concerns\ResolvesUser; +use Laracord\Console\Commands\Concerns\ResolvesUser; class AdminCommand extends Command { diff --git a/src/Console/Concerns/ResolvesUser.php b/src/Console/Commands/Concerns/ResolvesUser.php similarity index 83% rename from src/Console/Concerns/ResolvesUser.php rename to src/Console/Commands/Concerns/ResolvesUser.php index 0f2d218..2ecbfdf 100644 --- a/src/Console/Concerns/ResolvesUser.php +++ b/src/Console/Commands/Concerns/ResolvesUser.php @@ -1,10 +1,11 @@ getUserModel()::where('discord_id', $user)->first(); if (! $model) { - $token = $this->getBotClass()::make($this)->getToken(); + $token = Laracord::getToken(); $request = Http::withHeaders([ 'Authorization' => "Bot {$token}", @@ -56,16 +57,6 @@ protected function resolveUser(?string $user = null) return $model; } - /** - * Get the bot class. - */ - protected function getBotClass(): string - { - $class = Str::start($this->app->getNamespace(), '\\').'Bot'; - - return class_exists($class) ? $class : 'Laracord'; - } - /** * Get the user model class. */ diff --git a/src/Console/Commands/TokenMakeCommand.php b/src/Console/Commands/TokenMakeCommand.php index 995273f..ace3ce5 100644 --- a/src/Console/Commands/TokenMakeCommand.php +++ b/src/Console/Commands/TokenMakeCommand.php @@ -3,7 +3,7 @@ namespace Laracord\Console\Commands; use Illuminate\Support\Collection; -use Laracord\Console\Concerns\ResolvesUser; +use Laracord\Console\Commands\Concerns\ResolvesUser; class TokenMakeCommand extends Command { From ddfcbe7c41f1a588b2c8db7f2ab8854276f9ec81 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:28:36 -0600 Subject: [PATCH 007/146] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20`Http\Server`?= =?UTF-8?q?=20to=20`Http\HttpServer`=20=F0=9F=94=8A=20Utilize=20the=20logg?= =?UTF-8?q?er=20instead=20of=20console=20for=20logs=20=F0=9F=A7=91?= =?UTF-8?q?=E2=80=8D=F0=9F=92=BB=20Improve=20HTTP=20server=20middleware=20?= =?UTF-8?q?implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/{Server.php => HttpServer.php} | 70 +++++++------------------ 1 file changed, 20 insertions(+), 50 deletions(-) rename src/Http/{Server.php => HttpServer.php} (73%) diff --git a/src/Http/Server.php b/src/Http/HttpServer.php similarity index 73% rename from src/Http/Server.php rename to src/Http/HttpServer.php index d451493..ad1961e 100644 --- a/src/Http/Server.php +++ b/src/Http/HttpServer.php @@ -3,35 +3,25 @@ namespace Laracord\Http; use Exception; -use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Http\Kernel; +use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; use Laracord\Laracord; use Psr\Http\Message\ServerRequestInterface; -use React\Http\HttpServer; +use React\Http\HttpServer as Server; use React\Http\Message\Response; use React\Socket\SocketServer; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Throwable; -class Server +class HttpServer { - /** - * The application instance. - */ - protected Application $app; - - /** - * The Laracord instance. - */ - protected Laracord $bot; - /** * The HTTP server instance. */ - protected ?HttpServer $server = null; + protected ?Server $server = null; /** * The socket server instance. @@ -51,10 +41,9 @@ class Server /** * Create a new server instance. */ - public function __construct(Laracord $bot) + public function __construct(protected Laracord $bot) { - $this->bot = $bot; - $this->app = $bot->getApplication(); + // } /** @@ -97,21 +86,19 @@ public function shutdown(): void $this->booted = false; - $this->bot->console()->log('The HTTP server has been shutdown'); + $this->bot->logger->info('The HTTP server has been shutdown'); } /** * Retrieve the HTTP server instance. - * - * @return \React\Http\HttpServer */ - public function getServer() + public function getServer(): Server { if ($this->server) { return $this->server; } - return $this->server = new HttpServer($this->bot->getLoop(), function (ServerRequestInterface $request) { + return $this->server = new Server($this->bot->getLoop(), function (ServerRequestInterface $request) { $headers = $request->getHeaders(); $request = Request::create( @@ -126,12 +113,17 @@ public function getServer() $request->headers->replace($headers); - $this->app->instance('request', $request); + $this->bot->app->instance('request', $request); - /** @var \Laracord\Http\Kernel $kernel */ - $kernel = $this->app->make(Kernel::class); + $this->bot->withMiddleware(function (Middleware $middleware) { + $middleware + ->use([\Laracord\Http\Middleware\FlushState::class]) + ->api([\Laracord\Http\Middleware\AuthorizeToken::class]) + ->alias(['auth' => \Laracord\Http\Middleware\AuthorizeToken::class]); + }); - $kernel = $this->attachMiddleware($kernel); + /** @var \Laracord\Http\Kernel $kernel */ + $kernel = $this->bot->app->make(Kernel::class); try { $kernel->terminate($request, $response = $kernel->handle($request)); @@ -158,7 +150,7 @@ protected function handleError(Throwable $e): Response $response = Str::finish($response, ": {$e->getMessage()}"); } - $this->bot->console()->error($e->getMessage()); + report($e); return new Response( 500, @@ -167,32 +159,10 @@ protected function handleError(Throwable $e): Response ); } - /** - * Attach the middleware to the kernel. - */ - protected function attachMiddleware(Kernel $kernel): Kernel - { - if ($this->bot->prependMiddleware()) { - foreach ($this->bot->prependMiddleware() as $middleware) { - $kernel->prependMiddleware($middleware); - } - } - - if ($this->bot->middleware()) { - foreach ($this->bot->middleware() as $middleware) { - $kernel->pushMiddleware($middleware); - } - } - - return $kernel; - } - /** * Retrieve the socket server instance. - * - * @return \React\Socket\SocketServer */ - public function getSocket() + public function getSocket(): SocketServer { return $this->socket; } From 4c18cc6d5f8b51d8ecc96082e9a16ad5ff42612f Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:30:06 -0600 Subject: [PATCH 008/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20applicat?= =?UTF-8?q?ion=20command=20logic=20to=20a=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasApplicationCommands.php | 286 ++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 src/Bot/Concerns/HasApplicationCommands.php diff --git a/src/Bot/Concerns/HasApplicationCommands.php b/src/Bot/Concerns/HasApplicationCommands.php new file mode 100644 index 0000000..edf97cb --- /dev/null +++ b/src/Bot/Concerns/HasApplicationCommands.php @@ -0,0 +1,286 @@ +get('laracord.application-commands', []); + + if (! $existing) { + $existing[] = $this->discord->application->commands->freshen(); + + foreach ($this->discord->guilds as $guild) { + $existing[] = $guild->commands->freshen(); + } + + $existing = all($existing)->then(fn ($commands) => collect($commands) + ->flatMap(fn ($command) => $command->toArray()) + ->map(fn ($command) => collect($command->getCreatableAttributes()) + ->merge([ + 'id' => $command->id, + 'guild_id' => $command->guild_id ?? null, + 'dm_permission' => $command->guild_id ? null : ($command->dm_permission ?? false), + 'default_permission' => $command->default_permission ?? true, + ]) + ->all() + ) + ->map(fn ($command) => array_merge($command, [ + 'options' => json_decode(json_encode($command['options'] ?? []), true), + ])) + ->filter(fn ($command) => ! blank($command)) + ->keyBy('name') + ); + + $existing = await($existing); + + cache()->forever('laracord.application-commands', $existing); + } + + $existing = collect($existing); + + $registered = collect($this->slashCommands) + ->merge($this->contextMenus) + ->filter(fn ($command) => $command->isEnabled()) + ->mapWithKeys(function ($command) { + $attributes = $command->create()->getCreatableAttributes(); + + $attributes = collect($attributes) + ->merge([ + 'guild_id' => $command->getGuild() ?? null, + 'dm_permission' => ! $command->getGuild() ? $command->canDirectMessage() : null, + 'nsfw' => $command->isNsfw(), + ]) + ->sortKeys() + ->all(); + + return [$command::class => [ + 'state' => $command, + 'attributes' => $attributes, + ]]; + }); + + $created = $registered->reject(fn ($command, $name) => $existing->has($name))->filter(); + $deleted = $existing->reject(fn ($command, $name) => $registered->has($name))->filter(); + + $updated = $registered + ->map(function ($command) { + $attributes = collect($command['attributes']) + ->reject(fn ($value) => blank($value)) + ->all(); + + return array_merge($command, ['attributes' => $attributes]); + }) + ->filter(function ($command, $name) use ($existing, $normalize) { + if (! $existing->has($name)) { + return false; + } + + $current = collect($existing->get($name)) + ->forget('id') + ->reject(fn ($value) => blank($value)); + + $attributes = collect($command['attributes']) + ->reject(fn ($value) => blank($value)); + + $keys = collect($current->keys()) + ->merge($attributes->keys()) + ->unique(); + + foreach ($keys as $key) { + $attribute = $current->get($key); + $value = $attributes->get($key); + + $attribute = $normalize($attribute); + $value = $normalize($value); + + if ($attribute === $value) { + continue; + } + + return true; + } + + return false; + }) + ->each(function ($command) use ($existing) { + $state = $existing->get($command['state']->getName()); + + $current = Arr::get($command, 'attributes.guild_id'); + $existing = Arr::get($state, 'guild_id'); + + if ($current && ! $existing) { + $this->unregisterApplicationCommand($state['id']); + } + + if ((! $current && $existing) || $current !== $existing) { + $this->unregisterApplicationCommand($state['id'], $existing); + } + }); + + if ($updated->isNotEmpty()) { + $this->logger->warning("Updating {$updated->count()} application command(s)."); + + $updated->each(function ($command) { + $state = $command['state']; + + $this->registerApplicationCommand($state); + }); + } + + if ($deleted->isNotEmpty()) { + $this->logger->warning("Deleting {$deleted->count()} application command(s)."); + + $deleted->each(fn ($command) => $this->unregisterApplicationCommand($command['id'], $command['guild_id'] ?? null)); + } + + if ($created->isNotEmpty()) { + $this->logger->info("Creating {$created->count()} new application command(s)."); + + $created->each(fn ($command) => $this->registerApplicationCommand($command['state'])); + } + + if ($registered->isEmpty()) { + return $this; + } + + $registered->each(function ($command, $name) { + $this->registerInteractions($name, $command['state']->interactions()); + + if ($command['state'] instanceof ContextMenu) { + $menu = $command['state']; + + $this->discord->listenCommand( + $name, + fn ($interaction) => rescue(fn () => $menu->maybeHandle($interaction)) + ); + + $this->contextMenus[$menu::class] = $menu; + + return; + } + + $subcommands = collect($command['state']->getRegisteredOptions()) + ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND) + ->map(fn (Option $subcommand) => [$name, $subcommand->name]); + + $subcommandGroups = collect($command['state']->getRegisteredOptions()) + ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND_GROUP) + ->flatMap(fn (Option $group) => collect($group->options) + ->filter(fn (Option $subcommand) => $subcommand->type === Option::SUB_COMMAND) + ->map(fn (Option $subcommand) => [$name, $group->name, $subcommand->name]) + ); + + $subcommands = $subcommands->merge($subcommandGroups); + + if ($subcommands->isNotEmpty()) { + $subcommands->each(function ($names) use ($command) { + $this->discord->listenCommand( + $names, + fn ($interaction) => rescue(fn () => $command['state']->maybeHandle($interaction)), + fn ($interaction) => rescue(fn () => $command['state']->maybeHandleAutocomplete($interaction)) + ); + }); + + return; + } + + $this->discord->listenCommand( + $name, + fn ($interaction) => rescue(fn () => $command['state']->maybeHandle($interaction)), + fn ($interaction) => rescue(fn () => $command['state']->maybeHandleAutocomplete($interaction)) + ); + }); + + $this->slashCommands = [ + ...$this->slashCommands, + ...$registered->pluck('state')->reject(fn ($command) => $command instanceof ContextMenu)->all(), + ]; + + return $this; + } + + /** + * Register the specified application command. + */ + protected function registerApplicationCommand(ApplicationCommand $command): void + { + cache()->forget('laracord.application-commands'); + + if ($command->getGuild()) { + $guild = $this->discord->guilds->get('id', $command->getGuild()); + + if (! $guild) { + $this->logger->warning("The {$command->getName()} command failed to register because the guild {$command->getGuild()} could not be found."); + + return; + } + + $guild->commands->save($command->create()); + + return; + } + + $this->discord->application->commands->save($command->create()); + } + + /** + * Unregister the specified application command. + */ + protected function unregisterApplicationCommand(string $id, ?string $guildId = null): void + { + cache()->forget('laracord.application-commands'); + + if ($guildId) { + $guild = $this->discord->guilds->get('id', $guildId); + + if (! $guild) { + $this->logger->warning("The command with ID {$id} failed to unregister because the guild {$guildId} could not be found."); + + return; + } + + $guild->commands->delete($id); + + return; + } + + $this->discord->application->commands->delete($id); + } +} From c172f8882db517b3b87440ffc83e94401fb599c7 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:31:32 -0600 Subject: [PATCH 009/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20command?= =?UTF-8?q?=20logic=20to=20a=20trait=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB?= =?UTF-8?q?=20Implement=20command=20component=20discovery=20=F0=9F=8E=A8?= =?UTF-8?q?=20Improve=20command=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasCommands.php | 179 +++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/Bot/Concerns/HasCommands.php diff --git a/src/Bot/Concerns/HasCommands.php b/src/Bot/Concerns/HasCommands.php new file mode 100644 index 0000000..fbe7ca0 --- /dev/null +++ b/src/Bot/Concerns/HasCommands.php @@ -0,0 +1,179 @@ +prefixes) { + return $this->prefixes; + } + + $prefixes = collect(config('discord.prefix', '!')) + ->map(fn ($prefix) => Str::of($prefix)->replace(['@mention', '@self'], (string) $this->discord->user)->trim()->toString()) + ->reject(fn ($prefix) => Str::startsWith($prefix, '/')) + ->filter(); + + if ($prefixes->isEmpty()) { + throw new Exception('You must provide a valid command prefix.'); + } + + return $this->prefixes = $prefixes; + } + + /** + * Retrieve the primary prefix. + */ + public function getPrefix(): string + { + return $this->getPrefixes()->first(); + } + + /** + * Boot the chat commands. + */ + protected function bootCommands(): self + { + $this->handleCommands(); + + return $this; + } + + /** + * Handle the chat commands. + */ + protected function handleCommands(): void + { + $this->discord->on('message', function (Message $message) { + if ($message->author->id === $this->discord->id) { + return; + } + + $prefix = $this->getPrefixes()->first(fn ($prefix) => Str::startsWith($message->content, $prefix)); + + if (! $prefix) { + return; + } + + $parts = Str::of($message->content) + ->after($prefix) + ->trim() + ->explode(' '); + + $command = $parts->shift(); + + if (! $command) { + return; + } + + $command = $this->getCommand($command); + + if (! $command) { + return; + } + + rescue(fn () => $command->maybeHandle($message, $parts->all())); + }); + } + + /** + * Register a command. + */ + public function registerCommand(Command|string $command): self + { + if (is_string($command)) { + $command = $command::make(); + } + + if (! is_subclass_of($command, Command::class)) { + throw new InvalidArgumentException("Class [{$command}] is not a valid command."); + } + + if (! $command->isEnabled()) { + return $this; + } + + $this->commands[$command::class] = $command; + + $this->commandMap[$command->getName()] = $command::class; + + foreach ($command->getAliases() as $alias) { + $this->commandMap[$alias] = $command::class; + } + + $this->registerInteractions($command->getName(), $command->interactions()); + + return $this; + } + + /** + * Register multiple commands. + */ + public function registerCommands(array $commands): self + { + foreach ($commands as $command) { + $this->registerCommand($command); + } + + return $this; + } + + /** + * Discover commands in a path. + */ + public function discoverCommands(string $in, string $for): self + { + foreach ($this->discover(Command::class, $in, $for) as $command) { + $this->registerCommand($command); + } + + return $this; + } + + /** + * Get the registered commands. + */ + public function getCommands(): array + { + return $this->commands; + } + + /** + * Get a registered command by name. + */ + public function getCommand(string $name): ?Command + { + $command = $this->commandMap[$name] ?? null; + + if (! $command) { + return null; + } + + return $this->commands[$command] ?? null; + } +} From 76965057d236217797ce6c670731a2ae1acece7f Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:32:23 -0600 Subject: [PATCH 010/146] =?UTF-8?q?=E2=9C=A8=20Implement=20component=20dis?= =?UTF-8?q?covery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasComponents.php | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Bot/Concerns/HasComponents.php diff --git a/src/Bot/Concerns/HasComponents.php b/src/Bot/Concerns/HasComponents.php new file mode 100644 index 0000000..5bb56f7 --- /dev/null +++ b/src/Bot/Concerns/HasComponents.php @@ -0,0 +1,61 @@ +exists($directory)) && (! str($directory)->contains('*'))) { + return []; + } + + $namespace = str($namespace); + $discovered = []; + + foreach ($filesystem->allFiles($directory) as $file) { + $variableNamespace = $namespace->contains('*') ? str_ireplace( + ['\\'.$namespace->before('*'), $namespace->after('*')], + ['', ''], + str_replace([DIRECTORY_SEPARATOR], ['\\'], (string) str($file->getPath())->after(base_path())), + ) : null; + + if (is_string($variableNamespace)) { + $variableNamespace = (string) str($variableNamespace)->before('\\'); + } + + $class = (string) $namespace + ->append('\\', $file->getRelativePathname()) + ->replace('*', $variableNamespace ?? '') + ->replace([DIRECTORY_SEPARATOR, '.php'], ['\\', '']); + + if (! class_exists($class)) { + continue; + } + + if ((new ReflectionClass($class))->isAbstract()) { + continue; + } + + if (! is_subclass_of($class, $baseClass)) { + continue; + } + + $discovered[] = $class; + } + + return $discovered; + } +} From 81b6285255c6107c7d8af38febc05d56691a288e Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:33:17 -0600 Subject: [PATCH 011/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Movecontext=20m?= =?UTF-8?q?enu=20logic=20to=20a=20trait=20=F0=9F=A7=91=E2=80=8D?= =?UTF-8?q?=F0=9F=92=BB=20Implement=20context=20menu=20component=20discove?= =?UTF-8?q?ry=20=F0=9F=8E=A8=20Improve=20context=20menu=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasContextMenus.php | 74 ++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/Bot/Concerns/HasContextMenus.php diff --git a/src/Bot/Concerns/HasContextMenus.php b/src/Bot/Concerns/HasContextMenus.php new file mode 100644 index 0000000..09d9236 --- /dev/null +++ b/src/Bot/Concerns/HasContextMenus.php @@ -0,0 +1,74 @@ +contextMenus[$menu::class] = $menu; + + return $this; + } + + /** + * Register multiple context menus. + */ + public function registerContextMenus(array $menus): self + { + foreach ($menus as $menu) { + $this->registerContextMenu($menu); + } + + return $this; + } + + /** + * Discover context menus in a path. + */ + public function discoverContextMenus(string $in, string $for): self + { + foreach ($this->discover(ContextMenu::class, $in, $for) as $menu) { + $this->registerContextMenu($menu); + } + + return $this; + } + + /** + * Get the registered context menus. + */ + public function getContextMenus(): array + { + return $this->contextMenus; + } + + /** + * Get a registered context menu by name. + */ + public function getContextMenu(string $name): ?ContextMenu + { + return $this->contextMenus[$name] ?? collect($this->contextMenus)->first(fn (ContextMenu $menu): bool => $menu->getName() === $name); + } +} From 7cc62abbffdae6aac8fc519c7da787590af42268 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:35:02 -0600 Subject: [PATCH 012/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20event=20?= =?UTF-8?q?logic=20to=20a=20trait=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Im?= =?UTF-8?q?plement=20event=20component=20discovery=20=F0=9F=8E=A8=20Improv?= =?UTF-8?q?e=20event=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasEvents.php | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/Bot/Concerns/HasEvents.php diff --git a/src/Bot/Concerns/HasEvents.php b/src/Bot/Concerns/HasEvents.php new file mode 100644 index 0000000..8836033 --- /dev/null +++ b/src/Bot/Concerns/HasEvents.php @@ -0,0 +1,92 @@ +events as $event) { + if (! $event->isEnabled()) { + continue; + } + + $this->events[$event::class] = $event->register(); + + $this->logger->info("The {$event->getName()} event has been registered to {$event->getHandler()}."); + } + + return $this; + } + + /** + * Register an event. + */ + public function registerEvent(Event|string $event): self + { + if (is_string($event)) { + $event = $event::make(); + } + + if (! is_subclass_of($event, Event::class)) { + $class = $event::class; + + throw new InvalidArgumentException("Class [{$class}] is not a valid event."); + } + + $this->events[$event::class] = $event; + + return $this; + } + + /** + * Register multiple events. + */ + public function registerEvents(array $events): self + { + foreach ($events as $event) { + $this->registerEvent($event); + } + + return $this; + } + + /** + * Discover events in a path. + */ + public function discoverEvents(string $in, string $for): self + { + foreach ($this->discover(Event::class, $in, $for) as $event) { + $this->registerEvent($event); + } + + return $this; + } + + /** + * Get the registered events. + */ + public function getEvents(): array + { + return $this->events; + } + + /** + * Get a registered event by name. + */ + public function getEvent(string $name): ?Event + { + return $this->events[$name] ?? null; + } +} From 063d405b72ff80b069e3a37a8b003454f5edf31a Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:36:05 -0600 Subject: [PATCH 013/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20service?= =?UTF-8?q?=20logic=20to=20a=20trait=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB?= =?UTF-8?q?=20Implement=20service=20component=20discovery=20=F0=9F=8E=A8?= =?UTF-8?q?=20Improve=20service=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasServices.php | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/Bot/Concerns/HasServices.php diff --git a/src/Bot/Concerns/HasServices.php b/src/Bot/Concerns/HasServices.php new file mode 100644 index 0000000..31d231f --- /dev/null +++ b/src/Bot/Concerns/HasServices.php @@ -0,0 +1,92 @@ +services as $service) { + if (! $service->isEnabled()) { + continue; + } + + $this->services[$service::class] = $service->boot(); + + $this->logger->info("The {$service->getName()} service has been booted."); + } + + return $this; + } + + /** + * Register a service. + */ + public function registerService(Service|string $service): self + { + if (is_string($service)) { + $service = $service::make(); + } + + if (! is_subclass_of($service, Service::class)) { + $class = $service::class; + + throw new InvalidArgumentException("Class [{$class}] is not a valid service."); + } + + $this->services[$service::class] = $service; + + return $this; + } + + /** + * Register multiple services. + */ + public function registerServices(array $services): self + { + foreach ($services as $service) { + $this->registerService($service); + } + + return $this; + } + + /** + * Discover services in a path. + */ + public function discoverServices(string $in, string $for): self + { + foreach ($this->discover(Service::class, $in, $for) as $service) { + $this->registerService($service); + } + + return $this; + } + + /** + * Get the registered services. + */ + public function getServices(): array + { + return $this->services; + } + + /** + * Get a registered service by name. + */ + public function getService(string $name): ?Service + { + return $this->services[$name] ?? collect($this->services)->first(fn (Service $service): bool => $service->getName() === $name); + } +} From 81424160dc45395ee7710c7fc4c0f4896fbf6928 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:37:15 -0600 Subject: [PATCH 014/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20slash=20?= =?UTF-8?q?command=20logic=20to=20a=20trait=20=F0=9F=A7=91=E2=80=8D?= =?UTF-8?q?=F0=9F=92=BB=20Implement=20slash=20command=20component=20discov?= =?UTF-8?q?ery=20=F0=9F=8E=A8=20Improve=20slash=20command=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasSlashCommands.php | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/Bot/Concerns/HasSlashCommands.php diff --git a/src/Bot/Concerns/HasSlashCommands.php b/src/Bot/Concerns/HasSlashCommands.php new file mode 100644 index 0000000..7847afe --- /dev/null +++ b/src/Bot/Concerns/HasSlashCommands.php @@ -0,0 +1,74 @@ +slashCommands[$command::class] = $command; + + return $this; + } + + /** + * Register multiple slash commands. + */ + public function registerSlashCommands(array $commands): self + { + foreach ($commands as $command) { + $this->registerSlashCommand($command); + } + + return $this; + } + + /** + * Discover slash commands in a path. + */ + public function discoverSlashCommands(string $in, string $for): self + { + foreach ($this->discover(SlashCommand::class, $in, $for) as $command) { + $this->registerSlashCommand($command); + } + + return $this; + } + + /** + * Get a registered slash command by name. + */ + public function getSlashCommand(string $name): ?SlashCommand + { + return $this->slashCommands[$name] ?? collect($this->slashCommands)->first(fn (SlashCommand $command): bool => $command->getName() === $name); + } + + /** + * Get the registered slash commands. + */ + public function getSlashCommands(): array + { + return $this->slashCommands; + } +} From 1ebc9125956c502eaf0ee3ac2c376f76f8fbaa9c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:38:22 -0600 Subject: [PATCH 015/146] =?UTF-8?q?=E2=9C=A8=20Add=20interaction=20middlew?= =?UTF-8?q?are=20support=20=F0=9F=8F=97=EF=B8=8F=20Move=20interaction=20lo?= =?UTF-8?q?gic=20to=20a=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasInteractions.php | 159 +++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/Bot/Concerns/HasInteractions.php diff --git a/src/Bot/Concerns/HasInteractions.php b/src/Bot/Concerns/HasInteractions.php new file mode 100644 index 0000000..fc18ca5 --- /dev/null +++ b/src/Bot/Concerns/HasInteractions.php @@ -0,0 +1,159 @@ +mapWithKeys(fn ($value, $route) => ["{$name}@{$route}" => $value]) + ->all(); + + if (! $routes) { + return; + } + + $this->interactions = [ + ...$this->interactions, + ...$routes, + ]; + } + + /** + * Get the registered interactions. + */ + public function getInteractions(): array + { + return $this->interactions; + } + + /** + * Register an interaction middleware. + */ + public function registerInteractionMiddleware(string|Middleware $middleware): self + { + if (is_string($middleware)) { + if (! class_exists($middleware)) { + throw new InvalidArgumentException("Middleware class [{$middleware}] does not exist."); + } + + if (! is_subclass_of($middleware, Middleware::class)) { + throw new InvalidArgumentException("Middleware class [{$middleware}] must implement the Middleware interface."); + } + } + + $this->interactionMiddleware[] = $middleware; + + return $this; + } + + /** + * Register multiple interaction middleware. + */ + public function registerInteractionMiddlewares(array $middlewares): self + { + foreach ($middlewares as $middleware) { + $this->registerInteractionMiddleware($middleware); + } + + return $this; + } + + /** + * Get the interaction middleware. + */ + public function getInteractionMiddleware(): array + { + return $this->resolveCommandMiddleware($this->interactionMiddleware); + } + + /** + * Process the interaction through its middleware stack. + */ + protected function processInteractionMiddleware(Interaction $interaction, callable $handler): mixed + { + $context = new Context(source: $interaction); + + return (new Pipeline($this->app)) + ->send($context) + ->through($this->getInteractionMiddleware()) + ->then(fn (Context $context) => $handler($context->source)); + } + + /** + * Handle the interaction routes. + */ + protected function handleInteractions(): self + { + $this->discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction) { + $id = $interaction->data->custom_id; + + $handlers = collect($this->getInteractions()) + ->partition(fn ($route, $name) => ! Str::contains($name, '{')); + + $static = $handlers[0]; + $dynamic = $handlers[1]; + + if ($route = $static->get($id)) { + return rescue(fn () => $this->processInteractionMiddleware($interaction, fn ($interaction) => $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->bot->logger->error("Missing required parameter `{$param}` for interaction route `{$routeName}`."); + + return; + } + } + + rescue(fn () => $this->processInteractionMiddleware( + $interaction, + fn ($interaction) => $route($interaction, ...$parameters) + )); + }); + + return $this; + } +} From d6a6f3fed0286ee1558e19fd9ea1ad64e7bb1604 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:41:05 -0600 Subject: [PATCH 016/146] =?UTF-8?q?=E2=9C=A8=20Add=20middleware=20to=20com?= =?UTF-8?q?mands=20(Fixes=20#121)=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Im?= =?UTF-8?q?prove=20command=20cooldown=20support=20(Fixes=20#18)=20?= =?UTF-8?q?=F0=9F=8E=A8=20Improve=20abstract=20command=20structure/types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/AbstractCommand.php | 145 +++++++++++++------------------ 1 file changed, 62 insertions(+), 83 deletions(-) diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index eb967c0..58ea1cc 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -6,40 +6,13 @@ use Discord\Parts\Interactions\Command\Command; use Discord\Parts\User\User; use Illuminate\Support\Str; +use Laracord\Concerns\HasHandler; use Laracord\Discord\Concerns\HasModal; -use Laracord\Laracord; +use Laracord\HasLaracord; abstract class AbstractCommand { - use HasModal; - - /** - * The bot instance. - * - * @var \Laracord\Laracord - */ - protected $bot; - - /** - * The console instance. - * - * @var \Laracord\Console\Commands\Command - */ - protected $console; - - /** - * The Discord instance. - * - * @var \Discord\DiscordCommandClient - */ - protected $discord; - - /** - * The user instance. - * - * @var \App\Models\User - */ - protected $user; + use HasHandler, HasLaracord, HasModal; /** * The command name. @@ -79,6 +52,13 @@ abstract class AbstractCommand */ protected $admin = false; + /** + * The command cooldown in seconds. + * + * @var int + */ + protected $cooldown = 0; + /** * Determines whether the command should be displayed in the commands list. * @@ -94,23 +74,29 @@ abstract class AbstractCommand protected $enabled = true; /** - * Create a new command instance. - * - * @return void + * The command cooldown cache. + */ + protected array $cooldowns = []; + + /** + * The middleware to be applied to the command. + */ + protected array $middleware = []; + + /** + * Get the middleware for the command. */ - public function __construct(Laracord $bot) + public function getMiddleware(): array { - $this->bot = $bot; - $this->console = $bot->console(); - $this->discord = $bot->discord(); + return $this->bot()->resolveCommandMiddleware($this->middleware); } /** * Make a new command instance. */ - public static function make(Laracord $bot): self + public static function make(): self { - return new static($bot); + return new static; } /** @@ -129,23 +115,20 @@ public function interactions(): array */ public function message($content = '') { - return $this->bot()->message($content)->routePrefix($this->getName()); + return $this->bot->message($content)->routePrefix($this->getName()); } /** * Determine if the Discord user is an admin. - * - * @param string|\Discord\Parts\User\User $user - * @return bool */ - public function isAdmin($user) + public function isAdmin(User|string $user): bool { if (! $user instanceof User) { - $user = $this->discord()->users->get('id', $user); + $user = $this->discord->users->get('id', $user); } - if ($this->bot()->getAdmins()) { - return in_array($user->id, $this->bot()->getAdmins()); + if ($this->bot->getAdmins()) { + return in_array($user->id, $this->bot->getAdmins()); } return $this->getUser($user)->is_admin; @@ -185,7 +168,7 @@ public function getUser($user) throw new Exception('The user model could not be found.'); } - return $this->user = $model::firstOrCreate(['discord_id' => $user->id], [ + return $model::firstOrCreate(['discord_id' => $user->id], [ 'discord_id' => $user->id, 'username' => $user->username, ]) ?? null; @@ -193,34 +176,28 @@ public function getUser($user) /** * Retrieve the command name. - * - * @return string */ - public function getName() + public function getName(): string { return $this->name; } /** * Retrieve the command signature. - * - * @return string */ - public function getSignature() + public function getSignature(): string { return $this->getName(); } /** * Retrieve the full command syntax. - * - * @return string */ - public function getSyntax() + public function getSyntax(): string { $command = $this->getSignature(); - if (! empty($this->usage)) { + if (filled($this->usage)) { $command .= " `{$this->usage}`"; } @@ -229,10 +206,8 @@ public function getSyntax() /** * Retrieve the command description. - * - * @return string */ - public function getDescription() + public function getDescription(): string { return $this->description; } @@ -256,41 +231,45 @@ public function getGuild(): ?string } /** - * Retrieve the bot instance. - * - * @return \Laracord\Laracord + * Determine if the command requires admin permissions. */ - public function bot() + public function isAdminCommand(): bool { - return $this->bot; + return $this->admin; } /** - * Retrieve the console instance. - * - * @return \Laracord\Console\Commands\Command + * Determine if the user is on cooldown. */ - public function console() + public function isOnCooldown(User $user, Guild $guild): bool { - return $this->console; - } + if ($this->getCooldown() === 0) { + return false; + } - /** - * Retrieve the Discord instance. - * - * @return \Discord\DiscordCommandClient - */ - public function discord() - { - return $this->discord; + $key = "{$user->id}.{$guild->id}"; + + if (! isset($this->cooldowns[$key])) { + $this->cooldowns[$key] = time(); + + return false; + } + + if (time() - $this->cooldowns[$key] < $this->cooldown) { + return true; + } + + $this->cooldowns[$key] = time(); + + return false; } /** - * Determine if the command requires admin permissions. + * Retrieve the command cooldown. */ - public function isAdminCommand(): bool + public function getCooldown(): int { - return $this->admin; + return $this->cooldown; } /** From 22f3de0cb95c128567e829af35afaea144d3654b Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:43:47 -0600 Subject: [PATCH 017/146] =?UTF-8?q?=F0=9F=9B=82=20Move=20the=20permission?= =?UTF-8?q?=20denied=20message=20into=20a=20future=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/ApplicationCommand.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Commands/ApplicationCommand.php b/src/Commands/ApplicationCommand.php index e0f27ca..fd2b2b7 100644 --- a/src/Commands/ApplicationCommand.php +++ b/src/Commands/ApplicationCommand.php @@ -2,6 +2,7 @@ namespace Laracord\Commands; +use Discord\Parts\Interactions\Interaction; use Discord\Parts\Permissions\RolePermission; abstract class ApplicationCommand extends AbstractCommand @@ -41,4 +42,16 @@ public function isNsfw(): bool { return $this->nsfw; } + + /** + * Handle the denied command. + */ + public function handleDenied(Interaction $interaction): void + { + $this + ->message('You do not have permission to use this command.') + ->title('Permission Denied') + ->error() + ->reply($interaction, ephemeral: true); + } } From 150d60924f0d675276b38e598775fc547073bd33 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:46:46 -0600 Subject: [PATCH 018/146] =?UTF-8?q?=E2=9C=A8=20Add=20sharding=20configurat?= =?UTF-8?q?ion=20support=20=F0=9F=8F=97=EF=B8=8F=20Move=20Discord=20logic?= =?UTF-8?q?=20to=20a=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasDiscord.php | 178 ++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/Bot/Concerns/HasDiscord.php diff --git a/src/Bot/Concerns/HasDiscord.php b/src/Bot/Concerns/HasDiscord.php new file mode 100644 index 0000000..7e4fa4a --- /dev/null +++ b/src/Bot/Concerns/HasDiscord.php @@ -0,0 +1,178 @@ +discord = new Discord($this->getOptions()); + $this->admins = config('discord.admins', $this->admins); + } + + /** + * Get the bot name. + */ + public function getName(): string + { + if ($this->name) { + return $this->name; + } + + return $this->name = config('app.name'); + } + + /** + * Set the bot token. + */ + public function setToken(string $token): self + { + $this->token = $token; + + return $this; + } + + /** + * Get the bot token. + */ + public function getToken(): string + { + if ($this->token) { + return $this->token; + } + + $token = config('discord.token'); + + if (! $token) { + throw new Exception('You must provide a Discord bot token.'); + } + + return $this->token = $token; + } + + /** + * Get the bot intents. + */ + public function getIntents(): ?int + { + if ($this->intents) { + return $this->intents; + } + + return $this->intents = config('discord.intents', Intents::getDefaultIntents()); + } + + /** + * Set the bot shard ID. + */ + public function setShard(int $id, int $count): self + { + $this->shardId = $id; + $this->shardCount = $count; + + return $this; + } + + /** + * Get the bot options. + */ + public function getOptions(): array + { + if ($this->options) { + return $this->options; + } + + $options = [ + 'token' => $this->getToken(), + 'intents' => $this->getIntents(), + 'logger' => $this->getLogger(), + 'loop' => $this->getLoop(), + ]; + + if ($this->shardId && $this->shardCount) { + $options = [ + ...$options, + 'shardId' => $this->shardId, + 'shardCount' => $this->shardCount, + ]; + } + + return $this->options = [ + ...config('discord.options', []), + ...$options, + ]; + } + + /** + * Get the Discord admins. + */ + public function getAdmins(): array + { + return $this->admins; + } + + /** + * Retrieve the Discord instance. + */ + public function discord(): ?Discord + { + return $this->discord; + } + + /** + * Build a mesage for Discord. + */ + public function message(string $content = ''): Message + { + return Message::make($this) + ->content($content); + } +} From 5d11f783a2d8c879e27acbf086f1e95b083f77fc Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:48:57 -0600 Subject: [PATCH 019/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Imp?= =?UTF-8?q?rove=20HTTP=20server=20middleware=20support=20=E2=9C=A8=20Add?= =?UTF-8?q?=20configuration=20methods=20for=20routing/middleware=20?= =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20HTTP=20server=20boot/configurati?= =?UTF-8?q?on=20logic=20to=20a=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHttpServer.php | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/Bot/Concerns/HasHttpServer.php diff --git a/src/Bot/Concerns/HasHttpServer.php b/src/Bot/Concerns/HasHttpServer.php new file mode 100644 index 0000000..d670f70 --- /dev/null +++ b/src/Bot/Concerns/HasHttpServer.php @@ -0,0 +1,88 @@ +app->make('router'); + + if (! is_null($callback)) { + $callback($router); + } + + return $this; + } + + /** + * The HTTP middleware. + */ + public function withMiddleware(?callable $callback = null): self + { + /** @var \Laracord\Http\Kernel $kernel */ + $kernel = $this->app->make(Kernel::class); + + $middleware = new Middleware; + + if (! is_null($callback)) { + $callback($middleware); + } + + $kernel->setGlobalMiddleware($middleware->getGlobalMiddleware()); + $kernel->setMiddlewareGroups($middleware->getMiddlewareGroups()); + $kernel->setMiddlewareAliases($middleware->getMiddlewareAliases()); + + if ($priorities = $middleware->getMiddlewarePriority()) { + $kernel->setMiddlewarePriority($priorities); + } + + return $this; + } + + /** + * Boot the HTTP server. + */ + protected function bootHttpServer(): self + { + if ($this->httpServer) { + return $this; + } + + rescue(function () { + $this->app->booted(function () { + $this->app['router']->getRoutes()->refreshNameLookups(); + $this->app['router']->getRoutes()->refreshActionLookups(); + }); + + $this->httpServer = HttpServer::make($this)->boot(); + + if ($this->httpServer->isBooted()) { + $this->logger->info("HTTP server started on {$this->httpServer->getAddress()}."); + } + }); + + return $this; + } + + /** + * Get the HTTP server instance. + */ + public function httpServer(): ?HttpServer + { + return $this->httpServer; + } +} From 81b6b56e8657ffd2a6382c552c246563e8bdec8c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 05:52:36 -0600 Subject: [PATCH 020/146] =?UTF-8?q?=E2=9C=A8=20Create=20a=20custom=20conso?= =?UTF-8?q?le=20implementation=20using=20React=20streams=20=E2=9C=A8=20Imp?= =?UTF-8?q?lement=20`Prompts`=20for=20interacting=20with=20the=20bot=20con?= =?UTF-8?q?sole=20while=20running=20=F0=9F=8F=97=EF=B8=8F=20Move=20console?= =?UTF-8?q?=20logic=20to=20a=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasConsole.php | 141 ++++++++++++++++++++ src/Console/Console.php | 223 ++++++++++++++++++++++++++++++++ src/Console/Prompts/Prompt.php | 16 +++ 3 files changed, 380 insertions(+) create mode 100644 src/Bot/Concerns/HasConsole.php create mode 100644 src/Console/Console.php create mode 100644 src/Console/Prompts/Prompt.php diff --git a/src/Bot/Concerns/HasConsole.php b/src/Bot/Concerns/HasConsole.php new file mode 100644 index 0000000..95e0928 --- /dev/null +++ b/src/Bot/Concerns/HasConsole.php @@ -0,0 +1,141 @@ +console) { + return; + } + + $this->app->make(LoggerInterface::class)->pushHandler(new ConsoleHandler); + + $this->console = $this->app->make(Console::class); + + foreach ($this->getPrompts() as $prompt) { + $this->console->addCommand($prompt); + } + } + + /** + * Register a console prompt. + */ + public function registerPrompt(Prompt|string $prompt): self + { + if (is_string($prompt)) { + $prompt = $prompt::make(); + } + + if (! is_subclass_of($prompt, Prompt::class)) { + throw new InvalidArgumentException("Class [{$prompt}] is not a valid prompt."); + } + + $this->prompts[$prompt::class] = $prompt; + + return $this; + } + + /** + * Register multiple console prompts. + */ + public function registerPrompts(array $prompts): self + { + foreach ($prompts as $prompt) { + $this->registerPrompt($prompt); + } + + return $this; + } + + /** + * Print the registered commands to console. + */ + public function showCommands(): self + { + if (! $this->showCommands) { + return $this; + } + + $this->console->table( + ['Command', 'Description'], + collect($this->commands)->map(fn ($command) => [ + $command->getSignature(), + $command->getDescription(), + ])->all() + ); + + return $this; + } + + /** + * Show the invite link if the bot is not in any guilds. + */ + public function showInvite(bool $force = false): self + { + if (! $force && (! $this->showInvite || $this->discord->guilds->count() > 0)) { + return $this; + } + + if (! $force) { + $this->logger->warning("{$this->getName()} is currently not in any guilds."); + } + + $query = Arr::query([ + 'client_id' => $this->discord->id, + 'permissions' => 281600, + 'scope' => 'bot applications.commands', + ]); + + $invite = "https://discord.com/oauth2/authorize?{$query}"; + + $this->logger->info("You can invite {$this->getName()} using the following link: {$invite}"); + + return $this; + } + + /** + * Get the registered prompts. + */ + public function getPrompts(): array + { + return $this->prompts; + } + + /** + * Get the console instance. + */ + public function console(): ?Console + { + return $this->console; + } +} diff --git a/src/Console/Console.php b/src/Console/Console.php new file mode 100644 index 0000000..d2a0470 --- /dev/null +++ b/src/Console/Console.php @@ -0,0 +1,223 @@ + '; + + /** + * The registered commands. + */ + protected array $commands = []; + + /** + * The command aliases. + */ + protected array $aliases = []; + + /** + * The resolved commands. + */ + protected array $resolvedCommands = []; + + /** + * Initialize the console instance. + */ + public function __construct( + public readonly DuplexStreamInterface $stdio, + public readonly Container $laravel, + ConsoleOutputInterface $output, + InputInterface $input, + ) { + if (! $output instanceof OutputStyle) { + $output = new OutputStyle($input, $output); + } + + $this->components = $this->laravel->make(Factory::class, ['output' => $this->output]); + + $this->output = $output; + $this->input = $input; + + $this->stdio->on('data', fn (string $data) => $this->handle(trim($data))); + $this->stdio->on('end', fn () => $this->handle('shutdown')); + } + + /** + * Close the console instance. + */ + public function __destruct() + { + $this->stdio->close(); + } + + /** + * Make a new console instance. + */ + public static function make(): self + { + return app('bot.console'); + } + + /** + * Handle the console input. + */ + public function handle(string $command): void + { + if (empty($command)) { + $this->showPrompt(); + + return; + } + + [$command, $input] = explode(' ', $command, 2) + [1 => '']; + + $input = new StringInput($input); + + try { + $command = $this->resolveCommand($command); + } catch (InvalidArgumentException $e) { + logger()->error($e->getMessage()); + + $this->showPrompt(); + + return; + } + + $command->run($input, $this->output); + + $this->showPrompt(); + } + + /** + * Resolve the the specified command. + */ + protected function resolveCommand($command): Command + { + if (isset($this->resolvedCommands[$command])) { + return $this->resolvedCommands[$command]; + } + + if (isset($this->aliases[$command])) { + $command = $this->aliases[$command]; + } + + $instance = $this->commands[$command] ?? null; + + if (is_null($instance)) { + throw new InvalidArgumentException("Command not found: {$command}"); + } + + return $this->resolvedCommands[$command] = $instance; + } + + /** + * Add a command to the console. + */ + public function addCommand(Command|string $signature, ?Closure $command = null, string $description = '', array $aliases = []): void + { + if ($signature instanceof Command) { + $command = $signature; + $name = $command->getName(); + $aliases = $command->getAliases(); + + $command->setLaravel($this->laravel); + } elseif (is_null($command)) { + throw new InvalidArgumentException('If command is a string, a callable must be provided.'); + } + + if ($command instanceof Closure) { + $command = tap(new ClosureCommand($signature, $command), fn ($command) => $command + ->setDescription($description) + ->setAliases($aliases) + ->setLaravel($this->laravel) + ); + + $name = $command->getName(); + } + + $this->commands[$name] = $command; + + foreach ($aliases as $alias) { + $this->aliases[$alias] = $name; + } + } + + /** + * Get all available commands. + * + * @return \Symfony\Component\Console\Command\Command[] + */ + public function getCommands(): array + { + return $this->commands; + } + + /** + * Show the prompt. + */ + public function showPrompt(): void + { + $this->output->write($this->prompt); + } + + /** + * Returns true if the stream supports colorization. + * + * @copyright Fabien Potencier + * + * @see https://github.com/symfony/symfony/blob/b61353801c0229d67b7bfeef4b56b270b1f818eb/src/Symfony/Component/Console/Output/StreamOutput.php#L90-L121 + */ + public function hasColorSupport(): bool + { + // Follow https://no-color.org/ + if (isset($_SERVER['NO_COLOR']) || getenv('NO_COLOR')) { + return false; + } + + // Detect msysgit/mingw and assume this is a tty because detection + // does not work correctly, see https://github.com/composer/composer/issues/9690 + if (is_resource($this->stdio) && ! @stream_isatty($this->stdio) && ! \in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { + return false; + } + + if (windows_os() && @sapi_windows_vt100_support($this->stdio)) { + return true; + } + + if (getenv('TERM_PROGRAM') === 'Hyper' || getenv('COLORTERM') || getenv('ANSICON') || getenv('ConEmuANSI') === 'ON') { + return true; + } + + if (($term = (string) getenv('TERM')) === 'dumb') { + return false; + } + + // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 + return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); + } +} diff --git a/src/Console/Prompts/Prompt.php b/src/Console/Prompts/Prompt.php new file mode 100644 index 0000000..576e54a --- /dev/null +++ b/src/Console/Prompts/Prompt.php @@ -0,0 +1,16 @@ + Date: Thu, 6 Feb 2025 05:57:23 -0600 Subject: [PATCH 021/146] =?UTF-8?q?=E2=9C=A8=20Implement=20command=20middl?= =?UTF-8?q?eware=20registration=20and=20resolving=20(Fixes=20#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasCommandMiddleware.php | 93 +++++++++++++++++++ src/Commands/Middleware/Context.php | 91 ++++++++++++++++++ src/Commands/Middleware/Middleware.php | 15 +++ src/Commands/Middleware/ThrottleCommands.php | 66 +++++++++++++ .../Commands/MakeCommandMiddlewareCommand.php | 46 +++++++++ .../Commands/stubs/command-middleware.stub | 22 +++++ 6 files changed, 333 insertions(+) create mode 100644 src/Bot/Concerns/HasCommandMiddleware.php create mode 100644 src/Commands/Middleware/Context.php create mode 100644 src/Commands/Middleware/Middleware.php create mode 100644 src/Commands/Middleware/ThrottleCommands.php create mode 100644 src/Console/Commands/MakeCommandMiddlewareCommand.php create mode 100644 src/Console/Commands/stubs/command-middleware.stub diff --git a/src/Bot/Concerns/HasCommandMiddleware.php b/src/Bot/Concerns/HasCommandMiddleware.php new file mode 100644 index 0000000..8176323 --- /dev/null +++ b/src/Bot/Concerns/HasCommandMiddleware.php @@ -0,0 +1,93 @@ +commandMiddleware[] = $middleware; + + return $this; + } + + /** + * Register multiple global command middleware. + */ + public function registerCommandMiddlewares(array $middlewares): self + { + foreach ($middlewares as $middleware) { + $this->registerCommandMiddleware($middleware); + } + + return $this; + } + + /** + * Get the global command middleware. + */ + public function getCommandMiddleware(): array + { + return $this->commandMiddleware; + } + + /** + * Parse middleware string to get the name and parameters. + */ + protected function parseMiddlewareString(string $middleware): array + { + [$name, $parameters] = array_pad(explode(':', $middleware, 2), 2, null); + + if (is_null($parameters)) { + return [$name, []]; + } + + return [$name, explode(',', $parameters)]; + } + + /** + * Get all middleware for a command, including global middleware. + */ + public function resolveCommandMiddleware(array $commandMiddleware = []): array + { + $resolveMiddleware = function ($middleware) { + if ($middleware instanceof Middleware) { + return $middleware; + } + + [$name, $parameters] = $this->parseMiddlewareString($middleware); + + if (empty($parameters)) { + return new $name; + } + + return new $name(...$parameters); + }; + + return array_map( + $resolveMiddleware, + array_merge($this->getCommandMiddleware(), $commandMiddleware) + ); + } +} diff --git a/src/Commands/Middleware/Context.php b/src/Commands/Middleware/Context.php new file mode 100644 index 0000000..0b2670f --- /dev/null +++ b/src/Commands/Middleware/Context.php @@ -0,0 +1,91 @@ +source instanceof Message; + } + + /** + * Determine if the context is from an interaction. + */ + public function isInteraction(): bool + { + return $this->source instanceof Interaction; + } + + /** + * Determine if the command is a slash command. + */ + public function isSlashCommand(): bool + { + return $this->command instanceof SlashCommand; + } + + /** + * Determine if the command is a context menu. + */ + public function isContextMenu(): bool + { + return $this->command instanceof ContextMenu; + } + + /** + * Determine if the command is a message command. + */ + public function isCommand(): bool + { + return $this->command instanceof Command; + } + + /** + * Determine if this is a raw interaction (no command). + */ + public function isRawInteraction(): bool + { + return $this->isInteraction() && $this->command === null; + } + + /** + * Get the user from the context. + */ + public function getUser() + { + if ($this->isMessage()) { + return $this->source->author; + } + + return $this->source->user ?? $this->source->member?->user; + } + + /** + * Get the guild ID from the context. + */ + public function getGuildId(): ?string + { + return $this->source->guild_id; + } +} diff --git a/src/Commands/Middleware/Middleware.php b/src/Commands/Middleware/Middleware.php new file mode 100644 index 0000000..6a551f3 --- /dev/null +++ b/src/Commands/Middleware/Middleware.php @@ -0,0 +1,15 @@ +resolveRequestSignature($context); + + if ($this->tooManyAttempts($key)) { + Message::content('You are being rate limited. Please try again later.') + ->error() + ->reply($context->source); + + return; + } + + $this->incrementAttempts($key); + + return $next($context); + } + + /** + * Resolve the unique request signature for the rate limiter. + */ + protected function resolveRequestSignature(Context $context): string + { + return sha1($context->getUser()->id.'|'.$context->getGuildId().'|'.class_basename($context->command ?? $context->source)); + } + + /** + * Determine if the user has too many attempts. + */ + protected function tooManyAttempts(string $key): bool + { + return Cache::get($key, 0) >= $this->maxAttempts; + } + + /** + * Increment the attempts for the given key. + */ + protected function incrementAttempts(string $key): void + { + $attempts = Cache::get($key, 0) + 1; + + Cache::put($key, $attempts, now()->addMinutes($this->decayMinutes)); + } +} diff --git a/src/Console/Commands/MakeCommandMiddlewareCommand.php b/src/Console/Commands/MakeCommandMiddlewareCommand.php new file mode 100644 index 0000000..99b7a3a --- /dev/null +++ b/src/Console/Commands/MakeCommandMiddlewareCommand.php @@ -0,0 +1,46 @@ +laravel->basePath(trim($relativePath, '/'))) + ? $customPath + : __DIR__.$relativePath; + } + + /** + * {@inheritdoc} + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace.'\Commands\Middleware'; + } +} diff --git a/src/Console/Commands/stubs/command-middleware.stub b/src/Console/Commands/stubs/command-middleware.stub new file mode 100644 index 0000000..6c18aa0 --- /dev/null +++ b/src/Console/Commands/stubs/command-middleware.stub @@ -0,0 +1,22 @@ + Date: Thu, 6 Feb 2025 05:57:33 -0600 Subject: [PATCH 022/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Cre?= =?UTF-8?q?ate=20a=20`make:prompt`=20command=20to=20create=20console=20pro?= =?UTF-8?q?mpts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/PromptMakeCommand.php | 46 ++++++++++++++++++++++ src/Console/Commands/stubs/prompt.stub | 32 +++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/Console/Commands/PromptMakeCommand.php create mode 100644 src/Console/Commands/stubs/prompt.stub diff --git a/src/Console/Commands/PromptMakeCommand.php b/src/Console/Commands/PromptMakeCommand.php new file mode 100644 index 0000000..f18dff4 --- /dev/null +++ b/src/Console/Commands/PromptMakeCommand.php @@ -0,0 +1,46 @@ +laravel->basePath(trim($relativePath, '/'))) + ? $customPath + : __DIR__.$relativePath; + } + + /** + * {@inheritdoc} + */ + protected function getDefaultNamespace($rootNamespace): string + { + return $rootNamespace.'\Console\Prompts'; + } +} diff --git a/src/Console/Commands/stubs/prompt.stub b/src/Console/Commands/stubs/prompt.stub new file mode 100644 index 0000000..78820b8 --- /dev/null +++ b/src/Console/Commands/stubs/prompt.stub @@ -0,0 +1,32 @@ + Date: Thu, 6 Feb 2025 05:57:59 -0600 Subject: [PATCH 023/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Imp?= =?UTF-8?q?rove=20the=20default=20component=20stubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/stubs/command.stub | 11 ++++------ src/Console/Commands/stubs/context-menu.stub | 18 +++++++--------- src/Console/Commands/stubs/event.stub | 4 ++-- src/Console/Commands/stubs/service.stub | 9 ++++++-- src/Console/Commands/stubs/slash-command.stub | 21 +++++++------------ 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/Console/Commands/stubs/command.stub b/src/Console/Commands/stubs/command.stub index 1b9ed01..7a24943 100644 --- a/src/Console/Commands/stubs/command.stub +++ b/src/Console/Commands/stubs/command.stub @@ -2,6 +2,7 @@ namespace {{ namespace }}; +use Discord\Parts\Channel\Message; use Discord\Parts\Interactions\Interaction; use Laracord\Commands\Command; @@ -37,19 +38,15 @@ class {{ class }} extends Command /** * Handle the command. - * - * @param \Discord\Parts\Channel\Message $message - * @param array $args - * @return void */ - public function handle($message, $args) + public function handle(Message $message, array $args): void { - return $this + $this ->message() ->title('{{ title }}') ->content('Hello world!') ->button('👋', route: 'wave') - ->send($message); + ->reply($message); } /** diff --git a/src/Console/Commands/stubs/context-menu.stub b/src/Console/Commands/stubs/context-menu.stub index 2f5dbc9..a9a9d6b 100644 --- a/src/Console/Commands/stubs/context-menu.stub +++ b/src/Console/Commands/stubs/context-menu.stub @@ -38,16 +38,14 @@ class {{ class }} extends ContextMenu /** * Handle the context menu interaction. */ - public function handle(Interaction $interaction, Message|User|null $target): mixed + public function handle(Interaction $interaction, Message|User|null $target): void { - $interaction->respondWithMessage( - $this - ->message() - ->title('{{ title }}') - ->content('Hello world!') - ->button('👋', route: 'wave') - ->build() - ); + $this + ->message() + ->title('{{ title }}') + ->content('Hello world!') + ->button('👋', route: 'wave') + ->reply($interaction); } /** @@ -59,4 +57,4 @@ class {{ class }} extends ContextMenu 'wave' => fn (Interaction $interaction) => $this->message('👋')->reply($interaction), ]; } -} +} \ No newline at end of file diff --git a/src/Console/Commands/stubs/event.stub b/src/Console/Commands/stubs/event.stub index 0f03934..3009106 100644 --- a/src/Console/Commands/stubs/event.stub +++ b/src/Console/Commands/stubs/event.stub @@ -17,8 +17,8 @@ class {{ class }} extends Event /** * Handle the event. */ - public function handle({{ attributes }}) + public function handle({{ attributes }}): void { - $this->console()->log('The {{ eventName }} event has fired!'); + $this->logger()->info('The {{ eventName }} event has fired!'); } } diff --git a/src/Console/Commands/stubs/service.stub b/src/Console/Commands/stubs/service.stub index 4abe148..c573510 100644 --- a/src/Console/Commands/stubs/service.stub +++ b/src/Console/Commands/stubs/service.stub @@ -11,11 +11,16 @@ class {{ class }} extends Service */ protected int $interval = 5; + /** + * Determine if the service handler should execute during boot. + */ + protected bool $eager = false; + /** * Handle the service. */ - public function handle(): mixed + public function handle(): void { - $this->console()->log('Hello world.'); + $this->logger()->info('Hello world.'); } } diff --git a/src/Console/Commands/stubs/slash-command.stub b/src/Console/Commands/stubs/slash-command.stub index 81e4467..6a78454 100644 --- a/src/Console/Commands/stubs/slash-command.stub +++ b/src/Console/Commands/stubs/slash-command.stub @@ -51,20 +51,15 @@ class {{ class }} extends SlashCommand /** * Handle the slash command. - * - * @param \Discord\Parts\Interactions\Interaction $interaction - * @return mixed */ - public function handle($interaction) + public function handle(Interaction $interaction): void { - $interaction->respondWithMessage( - $this - ->message() - ->title('{{ title }}') - ->content('Hello world!') - ->button('👋', route: 'wave') - ->build() - ); + $this + ->message() + ->title('{{ title }}') + ->content('Hello world!') + ->button('👋', route: 'wave') + ->reply($interaction); } /** @@ -76,4 +71,4 @@ class {{ class }} extends SlashCommand 'wave' => fn (Interaction $interaction) => $this->message('👋')->reply($interaction), ]; } -} +} \ No newline at end of file From 7d6bee6245091d44e034fdeeeb03fed905c11e6a Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:00:15 -0600 Subject: [PATCH 024/146] =?UTF-8?q?=F0=9F=9B=82=20Resolve=20the=20user=20d?= =?UTF-8?q?uring=20the=20authorize=20token=20middleware=20(Supersedes=20#1?= =?UTF-8?q?32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/Middleware/AuthorizeToken.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Http/Middleware/AuthorizeToken.php b/src/Http/Middleware/AuthorizeToken.php index fa42c3f..c33c1d6 100644 --- a/src/Http/Middleware/AuthorizeToken.php +++ b/src/Http/Middleware/AuthorizeToken.php @@ -27,9 +27,7 @@ public function handle(Request $request, Closure $next) return response()->json(['message' => 'You are not authorized.'], 403); } - $user = $token->tokenable; - - $request->merge(['user' => $user]); + $request->setUserResolver(static fn () => $token->tokenable); return $next($request); } From 79af752844788c8241ecfd6522dde804656715fd Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:00:46 -0600 Subject: [PATCH 025/146] =?UTF-8?q?=F0=9F=94=A5=20Remove=20deprecated=20HT?= =?UTF-8?q?TP=20kernel=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/Kernel.php | 52 +-------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/src/Http/Kernel.php b/src/Http/Kernel.php index 22fbda2..f7c4301 100644 --- a/src/Http/Kernel.php +++ b/src/Http/Kernel.php @@ -6,55 +6,5 @@ class Kernel extends HttpKernel { - /** - * The application's global HTTP middleware stack. - * - * These middleware are run during every request to your application. - * - * @var array - */ - protected $middleware = [ - \Laracord\Http\Middleware\FlushState::class, - \Illuminate\Http\Middleware\HandleCors::class, - \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, - \Laracord\Http\Middleware\TrimStrings::class, - \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, - ]; - - /** - * The application's route middleware groups. - * - * @var array> - */ - protected $middlewareGroups = [ - 'web' => [ - \Illuminate\Cookie\Middleware\EncryptCookies::class, - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], - - 'api' => [ - \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \Laracord\Http\Middleware\AuthorizeToken::class, - ], - ]; - - /** - * The application's middleware aliases. - * - * Aliases may be used instead of class names to conveniently assign middleware to routes and groups. - * - * @var array - */ - protected $middlewareAliases = [ - 'auth' => \Laracord\Http\Middleware\AuthorizeToken::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, - 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - ]; + // } From 6de3d7fc5822dcc07e6d4d28c81cc9964d079c5c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:02:32 -0600 Subject: [PATCH 026/146] =?UTF-8?q?=F0=9F=94=8A=20Implement=20proper=20log?= =?UTF-8?q?ging=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasLogger.php | 34 +++++++++ src/Console/Concerns/WithLog.php | 85 ++++++++++++++-------- src/Logging/ConsoleHandler.php | 33 +++++++++ src/Logging/Logger.php | 120 ++++++++++++++----------------- 4 files changed, 175 insertions(+), 97 deletions(-) create mode 100644 src/Bot/Concerns/HasLogger.php create mode 100755 src/Logging/ConsoleHandler.php diff --git a/src/Bot/Concerns/HasLogger.php b/src/Bot/Concerns/HasLogger.php new file mode 100644 index 0000000..9873631 --- /dev/null +++ b/src/Bot/Concerns/HasLogger.php @@ -0,0 +1,34 @@ +logger) { + return; + } + + $this->logger = $this->app->make(LoggerInterface::class); + } + + /** + * Get the logger instance. + */ + public function getLogger(): ?LogManager + { + return $this->logger; + } +} diff --git a/src/Console/Concerns/WithLog.php b/src/Console/Concerns/WithLog.php index bfda096..a660e17 100644 --- a/src/Console/Concerns/WithLog.php +++ b/src/Console/Concerns/WithLog.php @@ -2,60 +2,87 @@ namespace Laracord\Console\Concerns; +use DateTimeInterface; use Laracord\Console\Components\Log; +use Psr\Log\LogLevel; +use Stringable; trait WithLog { /** - * Send a message to the console. + * The output style implementation. + * + * @var \Illuminate\Console\OutputStyle */ - public function log(string $message, string $type = 'info'): void - { - $message = trim($message); + protected $output; - if (empty($message)) { - return; - } + /** + * The log level colors. + */ + protected array $colors = [ + LogLevel::EMERGENCY => 'red', + LogLevel::ALERT => 'red', + LogLevel::CRITICAL => 'red', + LogLevel::ERROR => 'red', + LogLevel::WARNING => 'yellow', + LogLevel::NOTICE => 'cyan', + LogLevel::INFO => 'blue', + LogLevel::DEBUG => 'green', + ]; - $color = match ($type) { - 'error' => 'red', - 'warn' => 'yellow', - default => 'blue', - }; + /** + * Render a log message. + */ + public function log($level, string|\Stringable $message, array $context = []): void + { + $message = trim($message); $timestamp = config('discord.timestamp'); $config = [ - 'bgColor' => $color, + 'bgColor' => $this->color($level), 'fgColor' => 'white', - 'title' => $type, + 'title' => $level, 'timestamp' => $timestamp ? now()->format($timestamp) : null, ]; - with(new Log($this->getOutput()))->render($config, $message); + with(new Log($this->output))->render($config, $this->interpolate($level, $message, $context)); } /** - * Send a warning log to console. - * - * @param string $string - * @param string|null $verbosity - * @return void + * Interpolate the message. */ - public function warn($string, $verbosity = null) + private function interpolate(string $level, string $message, array $context): string { - return $this->log($string, 'warn'); + $color = $this->color($level); + + $replacements = []; + + foreach ($context as $key => $val) { + $replacements["{{$key}}"] = $this->colorize($color, match (true) { + $val === null, is_scalar($val), $val instanceof Stringable => "{$val}", + $val instanceof DateTimeInterface => $val->format(DateTimeInterface::RFC3339), + is_object($val) => '[object '.$val::class.']', + default => '['.gettype($val).']', + }); + } + + return strtr($message, $replacements); } /** - * Send an error log to console. - * - * @param string $string - * @param string|null $verbosity - * @return void + * Retrieve the color for the log level. + */ + private function color(string $level): string + { + return $this->colors[$level] ?? $this->colors[LogLevel::INFO]; + } + + /** + * Colorize the message. */ - public function error($string, $verbosity = null) + private function colorize(string $color, string $message) { - return $this->log($string, 'error'); + return "{$message}"; } } diff --git a/src/Logging/ConsoleHandler.php b/src/Logging/ConsoleHandler.php new file mode 100755 index 0000000..36a6d68 --- /dev/null +++ b/src/Logging/ConsoleHandler.php @@ -0,0 +1,33 @@ +logger = $logger ?? Logger::make(); + } + + /** + * {@inheritdoc} + */ + protected function write(LogRecord $record): void + { + $this->logger->handle($record->message, $record->context, $record->level->getName()); + } +} diff --git a/src/Logging/Logger.php b/src/Logging/Logger.php index 4ad7183..84e0a6a 100644 --- a/src/Logging/Logger.php +++ b/src/Logging/Logger.php @@ -3,17 +3,20 @@ namespace Laracord\Logging; use Illuminate\Support\Str; -use LaravelZero\Framework\Commands\Command; +use Laracord\Console\Console; +use NunoMaduro\Collision\Writer; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Stringable; +use Throwable; +use Whoops\Exception\Inspector; class Logger implements LoggerInterface { /** * Log messages that should be ignored. - * - * @var array */ - protected $except = [ + protected array $except = [ 'sending heartbeat', 'received heartbeat', 'http not checking', @@ -22,17 +25,13 @@ class Logger implements LoggerInterface /** * The console instance. - * - * @var \LaravelZero\Framework\Commands\Command */ - protected $console; + protected Console $console; /** * Initialize the logger. - * - * @return void */ - public function __construct(Command $console) + public function __construct(Console $console) { $this->console = $console; } @@ -40,140 +39,125 @@ public function __construct(Command $console) /** * Make a new logger instance. */ - public static function make(Command $console): Logger + public static function make(?Console $console = null): static { - return new static($console); + return new static($console ?? app(Console::class)); } /** * {@inheritdoc} */ - public function emergency(string|\Stringable $message, array $context = []): void + public function emergency(string|Stringable $message, array $context = []): void { - $this->error($message, $context); + $this->handle($message, $context, LogLevel::EMERGENCY); } /** * {@inheritdoc} */ - public function alert(string|\Stringable $message, array $context = []): void + public function alert(string|Stringable $message, array $context = []): void { - $this->error($message, $context); + $this->handle($message, $context, LogLevel::ALERT); } /** * {@inheritdoc} */ - public function critical(string|\Stringable $message, array $context = []): void + public function critical(string|Stringable $message, array $context = []): void { - $this->error($message, $context); + $this->handle($message, $context, LogLevel::CRITICAL); } /** * {@inheritdoc} */ - public function error(string|\Stringable $message, array $context = []): void + public function error(string|Stringable $message, array $context = []): void { - $this->handle($message, $context, 'error'); + $this->handle($message, $context, LogLevel::ERROR); } /** * {@inheritdoc} */ - public function warning(string|\Stringable $message, array $context = []): void + public function warning(string|Stringable $message, array $context = []): void { - $this->handle($message, $context, 'warn'); + $this->handle($message, $context, LogLevel::WARNING); } /** * {@inheritdoc} */ - public function notice(string|\Stringable $message, array $context = []): void + public function notice(string|Stringable $message, array $context = []): void { - $this->info($message, $context); + $this->info($message, $context, LogLevel::NOTICE); } /** * {@inheritdoc} */ - public function info(string|\Stringable $message, array $context = []): void + public function info(string|Stringable $message, array $context = []): void { - $this->handle($message, $context); + $this->handle($message, $context, LogLevel::INFO); } /** * {@inheritdoc} */ - public function debug(string|\Stringable $message, array $context = []): void + public function debug(string|Stringable $message, array $context = []): void { if (app()->environment('production')) { return; } - $this->info($message, $context); + $this->handle($message, $context, LogLevel::DEBUG); } /** * {@inheritdoc} */ - public function log($level, string|\Stringable $message, array $context = []): void + public function log($level, string|Stringable $message, array $context = []): void { - $this->info($message, $context); + $this->handle($message, $context, $level); } /** * Handle the log message. */ - public function handle(string|\Stringable $message, array $context = [], string $type = 'info'): void + public function handle(string|Stringable $message, array $context = [], string $type = 'info'): void { - $type = match ($type) { - 'error' => 'error', - 'warn' => 'warn', - default => 'info', + $type = match (strtolower($type)) { + 'alert' => LogLevel::ALERT, + 'critical' => LogLevel::CRITICAL, + 'debug' => LogLevel::DEBUG, + 'emergency' => LogLevel::EMERGENCY, + 'error' => LogLevel::ERROR, + 'info' => LogLevel::INFO, + 'warning' => LogLevel::WARNING, + default => LogLevel::INFO, }; if (Str::of($message)->lower()->contains($this->except)) { return; } - $message = ucfirst($message); - - if (method_exists($this->console, 'log')) { - $this->console->log($message, $type); - } else { - $this->console->outputComponents()->{$type}($message); - } + if (isset($context['exception']) && $context['exception'] instanceof Throwable) { + tap(new Writer, fn (Writer $writer) => $writer->write(new Inspector($context['exception']))); - $this->handleContext($context, $type); - } - - /** - * Handle the log context. - * - * @return array - */ - protected function handleContext(array $context = [], string $type = 'info'): void - { - if (! Str::is('error', $type)) { return; } - $context = collect($context)->filter(); - - if ($context->isEmpty()) { - return; - } - - $type = match ($type) { - 'error' => 'red', - 'warn' => 'yellow', - default => 'blue', - }; + $message = ucfirst($message); - $context = $context - ->mapWithKeys(fn ($value, $key) => [Str::is('e', $key) ? 'Error' : $key => $value]) - ->map(fn ($value, $key) => sprintf('%s: %s', $type, Str::headline($key), $value)); + if (method_exists($this->console, 'log')) { + $this->console->log($type, $message, $context); + } else { + $component = match ($type) { + LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR => 'error', + LogLevel::WARNING => 'warn', + default => 'info', + }; - $this->console->outputComponents()->bulletList($context->all()); + $this->console->outputComponents()->{$component}($message); + } } } From be34332175f055f8808dfe451a1b408150c64ca7 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:03:00 -0600 Subject: [PATCH 027/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Mak?= =?UTF-8?q?e=20`logger()`=20available=20in=20the=20`HasLaracord`=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/HasLaracord.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/HasLaracord.php b/src/HasLaracord.php index 10ac438..e4fca01 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -41,6 +41,14 @@ public function console(): ConsoleCommand return $this->bot()->console(); } + /** + * Retrieve the logger instance. + */ + public function logger(): LogManager + { + return $this->bot()->getLogger(); + } + /** * Build an embed for use in a Discord message. * From 3050e917a8b4164047a599989cbd70436f0b56d5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:03:29 -0600 Subject: [PATCH 028/146] =?UTF-8?q?=F0=9F=9A=9A=20Move=20the=20`RouteServi?= =?UTF-8?q?ceProvider`=20to=20the=20`Http`=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{ => Http}/Providers/RouteServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ => Http}/Providers/RouteServiceProvider.php (94%) diff --git a/src/Providers/RouteServiceProvider.php b/src/Http/Providers/RouteServiceProvider.php similarity index 94% rename from src/Providers/RouteServiceProvider.php rename to src/Http/Providers/RouteServiceProvider.php index 151cb4f..c6ec7f7 100644 --- a/src/Providers/RouteServiceProvider.php +++ b/src/Http/Providers/RouteServiceProvider.php @@ -1,6 +1,6 @@ Date: Thu, 6 Feb 2025 06:04:39 -0600 Subject: [PATCH 029/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Upd?= =?UTF-8?q?ate=20the=20`Message`=20class=20to=20utilize=20the=20logger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index e3040fe..ea6758d 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -288,7 +288,7 @@ public function sendTo(mixed $user): ?PromiseInterface $member = $this->bot->discord()->users->get('id', $user); if (! $member) { - $this->bot->console()->error("Could not find user {$user} to send message"); + $this->bot->logger->error("Could not find user {$user} to send message"); return null; } @@ -301,7 +301,7 @@ public function sendTo(mixed $user): ?PromiseInterface } if (! $user instanceof User) { - $this->bot->console()->error('You must provide a valid Discord user.'); + $this->bot->logger->error('You must provide a valid Discord user.'); return null; } @@ -318,13 +318,13 @@ protected function handleWebhook(): ?PromiseInterface /** @var WebhookRepository $webhooks */ $webhooks = await($this->getChannel()->webhooks->freshen()); } catch (NoPermissionsException) { - $this->bot->console()->error("\nMissing permission to fetch channel webhooks."); + $this->bot->logger->error("\nMissing permission to fetch channel webhooks."); return null; } if (! $webhooks) { - $this->bot->console()->error('Failed to fetch channel webhooks.'); + $this->bot->logger->error('Failed to fetch channel webhooks.'); return null; } @@ -337,7 +337,7 @@ protected function handleWebhook(): ?PromiseInterface 'name' => $this->bot->discord()->username, ]))->then( fn (Webhook $webhook) => $webhook->execute($this->build()), - fn () => $this->bot->console()->error('Failed to create message webhook.') + fn () => $this->bot->logger->error('Failed to create message webhook.') ); } @@ -347,7 +347,7 @@ protected function handleWebhook(): ?PromiseInterface $webhook = $this->getChannel()->webhooks->get('url', $this->webhook); if (! $webhook) { - $this->bot->console()->error("Could not find webhook {$this->webhook} on channel to send message."); + $this->bot->logger->error("Could not find webhook {$this->webhook} on channel to send message."); return null; } @@ -612,7 +612,7 @@ public function file(string $input = '', ?string $filename = null): self public function filePath(string $path, ?string $filename = null): self { if (! file_exists($path)) { - $this->bot->console()->error("File {$path} does not exist"); + $this->bot->logger->error("File {$path} does not exist"); return $this; } @@ -997,7 +997,7 @@ public function select( try { $select = $select->{$key}($option); } catch (Throwable) { - $this->bot->console()->error("Invalid select menu option {$key}"); + $this->bot->logger->error("Invalid select menu option {$key}"); continue; } @@ -1095,7 +1095,7 @@ public function button( try { $button = $button->{$key}($option); } catch (Throwable) { - $this->bot->console()->error("Invalid button option {$key}"); + $this->bot->logger->error("Invalid button option {$key}"); continue; } From c66d73811a87c325f000eaafd13168abf0425e4c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:05:16 -0600 Subject: [PATCH 030/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Cre?= =?UTF-8?q?ate=20a=20`Message`=20facade=20to=20use=20the=20message=20build?= =?UTF-8?q?er=20from=20the=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Facades/Message.php | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/Discord/Facades/Message.php diff --git a/src/Discord/Facades/Message.php b/src/Discord/Facades/Message.php new file mode 100644 index 0000000..0fa4447 --- /dev/null +++ b/src/Discord/Facades/Message.php @@ -0,0 +1,88 @@ + Date: Thu, 6 Feb 2025 06:05:32 -0600 Subject: [PATCH 031/146] =?UTF-8?q?=F0=9F=94=A5=20Remove=20deprecated=20mi?= =?UTF-8?q?ddleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/Middleware/TrimStrings.php | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/Http/Middleware/TrimStrings.php diff --git a/src/Http/Middleware/TrimStrings.php b/src/Http/Middleware/TrimStrings.php deleted file mode 100644 index cfaea1e..0000000 --- a/src/Http/Middleware/TrimStrings.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ - protected $except = [ - 'current_password', - 'password', - 'password_confirmation', - ]; -} From 6066b668f16d3dce581ec8f14df069ccb34042d7 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:06:41 -0600 Subject: [PATCH 032/146] =?UTF-8?q?=E2=9C=A8=20Allow=20passing=20token/sha?= =?UTF-8?q?rding=20during=20boot=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Imp?= =?UTF-8?q?rove=20how=20Laracord=20boots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/BootCommand.php | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Console/Commands/BootCommand.php b/src/Console/Commands/BootCommand.php index 2c01427..1efefbe 100644 --- a/src/Console/Commands/BootCommand.php +++ b/src/Console/Commands/BootCommand.php @@ -2,7 +2,7 @@ namespace Laracord\Console\Commands; -use Illuminate\Support\Str; +use Laracord\Laracord; class BootCommand extends Command { @@ -12,6 +12,9 @@ class BootCommand extends Command * @var string */ protected $signature = 'bot:boot + {--token= : The Discord bot token} + {--shard-id= : The Discord bot shard ID} + {--shard-count= : The Discord bot shard count} {--no-migrate : Boot without running database migrations}'; /** @@ -23,27 +26,24 @@ class BootCommand extends Command /** * Execute the console command. - * - * @return mixed */ - public function handle() + public function handle(Laracord $bot): void { if (! $this->option('no-migrate')) { $this->callSilent('migrate', ['--force' => true]); } - $this->app->singleton('bot', fn () => $this->getClass()::make($this)); - - $this->app->make('bot')->boot(); - } + if ($this->option('token')) { + $bot->setToken($this->option('token')); + } - /** - * Get the bot class. - */ - protected function getClass(): string - { - $class = Str::start($this->app->getNamespace(), '\\').'Bot'; + if ($this->option('shard-id') && $this->option('shard-count')) { + $bot->setShard( + id: $this->option('shard-id'), + count: $this->option('shard-count') + ); + } - return class_exists($class) ? $class : 'Laracord'; + $bot->boot(); } } From abffcc74d6279093ab752eff023c012afda45916 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:07:20 -0600 Subject: [PATCH 033/146] =?UTF-8?q?=E2=9C=A8=20Add=20plugin=20support=20(F?= =?UTF-8?q?ixes=20#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasPlugins.php | 59 +++++++++++++++++++++++++++++++++ src/Contracts/Plugin.php | 13 ++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/Bot/Concerns/HasPlugins.php create mode 100644 src/Contracts/Plugin.php diff --git a/src/Bot/Concerns/HasPlugins.php b/src/Bot/Concerns/HasPlugins.php new file mode 100644 index 0000000..11a55e7 --- /dev/null +++ b/src/Bot/Concerns/HasPlugins.php @@ -0,0 +1,59 @@ + + */ + protected array $plugins = []; + + /** + * Register a plugin. + */ + public function plugin(Plugin $plugin): static + { + $plugin->register($this); + + $this->plugins[$plugin::class] = $plugin; + + return $this; + } + + /** + * Register multiple plugins. + * + * @param array $plugins + */ + public function plugins(array $plugins): static + { + foreach ($plugins as $plugin) { + $this->plugin($plugin); + } + + return $this; + } + + /** + * Get the registered plugins. + * + * @return array + */ + public function getPlugins(): array + { + return $this->plugins; + } + + /** + * Retrieve a registered plugin. + */ + public function getPlugin(string $plugin): ?Plugin + { + return $this->plugins[$plugin] ?? null; + } +} diff --git a/src/Contracts/Plugin.php b/src/Contracts/Plugin.php new file mode 100644 index 0000000..c180fe4 --- /dev/null +++ b/src/Contracts/Plugin.php @@ -0,0 +1,13 @@ + Date: Thu, 6 Feb 2025 06:12:20 -0600 Subject: [PATCH 034/146] =?UTF-8?q?=E2=9C=A8=20Add=20DI=20support=20to=20`?= =?UTF-8?q?handle`=20methods=20on=20components=20(Supersedes=20#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/Contracts/Command.php | 7 +------ src/Commands/Contracts/ContextMenu.php | 9 +-------- src/Commands/Contracts/SlashCommand.php | 7 +------ src/Concerns/HasHandler.php | 22 ++++++++++++++++++++++ src/Services/Contracts/Service.php | 7 +------ 5 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 src/Concerns/HasHandler.php diff --git a/src/Commands/Contracts/Command.php b/src/Commands/Contracts/Command.php index f07a934..72bf4f4 100644 --- a/src/Commands/Contracts/Command.php +++ b/src/Commands/Contracts/Command.php @@ -2,12 +2,7 @@ namespace Laracord\Commands\Contracts; -use Discord\Parts\Channel\Message; - interface Command { - /** - * Handle the command. - */ - public function handle(Message $message, array $args); + // } diff --git a/src/Commands/Contracts/ContextMenu.php b/src/Commands/Contracts/ContextMenu.php index d45c664..229dd45 100644 --- a/src/Commands/Contracts/ContextMenu.php +++ b/src/Commands/Contracts/ContextMenu.php @@ -2,14 +2,7 @@ namespace Laracord\Commands\Contracts; -use Discord\Parts\Channel\Message; -use Discord\Parts\Interactions\Interaction; -use Discord\Parts\User\User; - interface ContextMenu { - /** - * Handle the context menu interaction. - */ - public function handle(Interaction $interaction, Message|User|null $target): mixed; + // } diff --git a/src/Commands/Contracts/SlashCommand.php b/src/Commands/Contracts/SlashCommand.php index 2796534..6ee422e 100644 --- a/src/Commands/Contracts/SlashCommand.php +++ b/src/Commands/Contracts/SlashCommand.php @@ -2,12 +2,7 @@ namespace Laracord\Commands\Contracts; -use Discord\Parts\Interactions\Interaction; - interface SlashCommand { - /** - * Handle the slash command. - */ - public function handle(Interaction $interaction); + // } diff --git a/src/Concerns/HasHandler.php b/src/Concerns/HasHandler.php new file mode 100644 index 0000000..f7542bb --- /dev/null +++ b/src/Concerns/HasHandler.php @@ -0,0 +1,22 @@ + $this->bot->app->call([$this, 'handle'], $parameters)); + } +} diff --git a/src/Services/Contracts/Service.php b/src/Services/Contracts/Service.php index 1d03229..ffb1926 100644 --- a/src/Services/Contracts/Service.php +++ b/src/Services/Contracts/Service.php @@ -4,10 +4,5 @@ interface Service { - /** - * Handle the service. - * - * @return mixed - */ - public function handle(); + // } From fc6394be2c071a94a5cab462cc207d4db158287c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:12:36 -0600 Subject: [PATCH 035/146] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20eager?= =?UTF-8?q?=20handling=20services=20on=20boot=20(Supersedes=20#129)=20?= =?UTF-8?q?=F0=9F=8E=A8=20Change=20the=20`Service`=20class=20to=20the=20`H?= =?UTF-8?q?asLaracord`=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Services/Service.php | 97 +++++++--------------------------------- 1 file changed, 15 insertions(+), 82 deletions(-) diff --git a/src/Services/Service.php b/src/Services/Service.php index cbe4a1e..d06fae6 100644 --- a/src/Services/Service.php +++ b/src/Services/Service.php @@ -2,34 +2,14 @@ namespace Laracord\Services; -use Discord\DiscordCommandClient as Discord; -use Laracord\Console\Commands\BootCommand as Console; -use Laracord\Laracord; +use Laracord\Concerns\HasHandler; +use Laracord\HasLaracord; use Laracord\Services\Contracts\Service as ServiceContract; use Laracord\Services\Exceptions\InvalidServiceInterval; abstract class Service implements ServiceContract { - /** - * The bot instance. - * - * @var \Laracord\Laracord - */ - protected $bot; - - /** - * The console instance. - * - * @var \Laracord\Console\Commands\BootCommand - */ - protected $console; - - /** - * The Discord instance. - * - * @var \Discord\DiscordCommandClient; - */ - protected $discord; + use HasHandler, HasLaracord; /** * The service name. @@ -42,39 +22,23 @@ abstract class Service implements ServiceContract protected int $interval = 5; /** - * Determine if the service is enabled. - * - * @var bool + * Determine if the service handler should execute during boot. */ - protected $enabled = true; + protected bool $eager = false; /** - * Create a new service instance. - * - * @return void + * Determine if the service is enabled. */ - public function __construct(Laracord $bot) - { - $this->bot = $bot; - $this->console = $bot->console(); - $this->discord = $bot->discord(); - } + protected bool $enabled = true; /** * Make a new service instance. */ - public static function make(Laracord $bot): self + public static function make(): self { - return new static($bot); + return new static; } - /** - * Handle the service. - * - * @return mixed - */ - abstract public function handle(); - /** * Boot the service. */ @@ -84,9 +48,13 @@ public function boot(): self throw new InvalidServiceInterval($this->getName()); } + if ($this->eager) { + $this->resolveHandler(); + } + $this->bot->getLoop()->addPeriodicTimer( $this->getInterval(), - fn () => $this->bot->handleSafe($this->getName(), fn () => $this->handle()) + fn () => $this->resolveHandler() ); return $this; @@ -123,7 +91,7 @@ public function interval(int $interval): self */ public function getName(): string { - if ($this->name) { + if (filled($this->name)) { return $this->name; } @@ -137,39 +105,4 @@ public function isEnabled(): bool { return $this->enabled; } - - /** - * Get the Discord client. - */ - public function discord(): Discord - { - return $this->discord; - } - - /** - * Get the bot instance. - */ - public function bot(): Laracord - { - return $this->bot; - } - - /** - * Get the console instance. - */ - public function console(): Console - { - return $this->console; - } - - /** - * Build an embed for use in a Discord message. - * - * @param string $content - * @return \Laracord\Discord\Message - */ - public function message($content = '') - { - return $this->bot()->message($content); - } } From 576f5598e8d93690c675e996a416a4a1cd462fe8 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:13:08 -0600 Subject: [PATCH 036/146] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20unnecessary=20`?= =?UTF-8?q?WithLog`=20trait=20from=20`Console\Commands\Command`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/Command.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Console/Commands/Command.php b/src/Console/Commands/Command.php index f71ac66..4b50243 100644 --- a/src/Console/Commands/Command.php +++ b/src/Console/Commands/Command.php @@ -2,10 +2,9 @@ namespace Laracord\Console\Commands; -use Laracord\Console\Concerns\WithLog; use LaravelZero\Framework\Commands\Command as LaravelCommand; abstract class Command extends LaravelCommand { - use WithLog; + // } From 000a051fa7cd852a88555e08924be730f4171115 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:13:46 -0600 Subject: [PATCH 037/146] =?UTF-8?q?=F0=9F=8E=A8=20Change=20the=20`Event`?= =?UTF-8?q?=20class=20to=20the=20`HasLaracord`=20trait=20=F0=9F=94=8A=20Ut?= =?UTF-8?q?ilize=20the=20logger=20instead=20of=20logging=20to=20console=20?= =?UTF-8?q?directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Events/Event.php | 82 ++++---------------------------------------- 1 file changed, 7 insertions(+), 75 deletions(-) diff --git a/src/Events/Event.php b/src/Events/Event.php index 2fda648..f44446e 100644 --- a/src/Events/Event.php +++ b/src/Events/Event.php @@ -3,15 +3,15 @@ namespace Laracord\Events; use Composer\InstalledVersions; -use Discord\DiscordCommandClient as Discord; use Discord\WebSockets\Event as DiscordEvent; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; -use Laracord\Console\Commands\BootCommand as Console; -use Laracord\Laracord; +use Laracord\HasLaracord; abstract class Event { + use HasLaracord; + /** * The event name. * @@ -33,45 +33,12 @@ abstract class Event */ protected $enabled = true; - /** - * The bot instance. - * - * @var \Laracord\Laracord - */ - protected $bot; - - /** - * The console instance. - * - * @var \Laracord\Console\Commands\BootCommand - */ - protected $console; - - /** - * The Discord instance. - * - * @var \Discord\DiscordCommandClient; - */ - protected $discord; - - /** - * Create a new event instance. - * - * @return void - */ - public function __construct(Laracord $bot) - { - $this->bot = $bot; - $this->console = $bot->console(); - $this->discord = $bot->discord(); - } - /** * Make a new event instance. */ - public static function make(Laracord $bot): self + public static function make(): self { - return new static($bot); + return new static; } /** @@ -80,13 +47,13 @@ public static function make(Laracord $bot): self public function register(): self { if (! $this->getHandler() || ! array_key_exists($this->getHandler(), $this->getEvents())) { - $this->console()->error("The {$this->getName()} event handler {$this->getHandler()} is invalid."); + $this->logger()->error("The {$this->getName()} event handler {$this->getHandler()} is invalid."); return $this; } if (! method_exists($this, 'handle')) { - $this->console()->error("The {$this->getName()} event handler does not have a handle method."); + $this->logger()->error("The {$this->getName()} event handler does not have a handle method."); return $this; } @@ -96,41 +63,6 @@ public function register(): self return $this; } - /** - * Build an embed for use in a Discord message. - * - * @param string $content - * @return \Laracord\Discord\Message - */ - public function message($content = '') - { - return $this->bot()->message($content); - } - - /** - * Get the Discord client. - */ - public function discord(): Discord - { - return $this->discord; - } - - /** - * Get the bot instance. - */ - public function bot(): Laracord - { - return $this->bot; - } - - /** - * Get the console instance. - */ - public function console(): Console - { - return $this->console; - } - /** * Get the available events. */ From f83c3007e8fa52e7e342760cba9ffccb5c85caec Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:14:59 -0600 Subject: [PATCH 038/146] =?UTF-8?q?=F0=9F=A7=B1=20Handle=20command=20middl?= =?UTF-8?q?eware=20in=20slash=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/SlashCommand.php | 45 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Commands/SlashCommand.php b/src/Commands/SlashCommand.php index b0c4dc4..c7e36cd 100644 --- a/src/Commands/SlashCommand.php +++ b/src/Commands/SlashCommand.php @@ -8,9 +8,11 @@ use Discord\Parts\Interactions\Command\Command as DiscordCommand; use Discord\Parts\Interactions\Command\Option; use Discord\Parts\Interactions\Interaction; +use Illuminate\Pipeline\Pipeline; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Laracord\Commands\Contracts\SlashCommand as SlashCommandContract; +use Laracord\Commands\Middleware\Context; abstract class SlashCommand extends ApplicationCommand implements SlashCommandContract { @@ -62,29 +64,37 @@ public function create(): DiscordCommand ->filter() ->all(); - return new DiscordCommand($this->discord(), $command); + return new DiscordCommand($this->discord, $command); } /** - * Handle the slash command. - * - * @param \Discord\Parts\Interactions\Interaction $interaction - * @return mixed + * Process the command through its middleware stack. */ - abstract public function handle($interaction); + protected function processMiddleware(Interaction $interaction): mixed + { + $context = new Context( + source: $interaction, + options: $this->getOptions(), + command: $this, + ); + + return (new Pipeline($this->bot()->app)) + ->send($context) + ->through($this->getMiddleware()) + ->then(fn (Context $context) => $this->resolveHandler([ + 'interaction' => $context->source, + ])); + } /** * Maybe handle the slash command. - * - * @param \Discord\Parts\Interactions\Interaction $interaction - * @return mixed */ - public function maybeHandle($interaction) + public function maybeHandle(Interaction $interaction): void { if (! $this->isAdminCommand()) { $this->parseOptions($interaction); - $this->handle($interaction); + $this->processMiddleware($interaction); $this->clearOptions(); @@ -92,19 +102,14 @@ public function maybeHandle($interaction) } if ($this->isAdminCommand() && ! $this->isAdmin($interaction->member->user)) { - return $interaction->respondWithMessage( - $this - ->message('You do not have permission to run this command.') - ->title('Permission Denied') - ->error() - ->build(), - ephemeral: true - ); + $this->handleDenied($interaction); + + return; } $this->parseOptions($interaction); - $this->handle($interaction); + $this->processMiddleware($interaction); $this->clearOptions(); } From 8768d50448b7602d9270029e01869818ec0872d6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:16:05 -0600 Subject: [PATCH 039/146] =?UTF-8?q?=F0=9F=A7=B1=20Handle=20command=20middl?= =?UTF-8?q?eware=20in=20commands=20=F0=9F=8E=A8=20Improve=20the=20`Command?= =?UTF-8?q?`=20class=20docblocks/types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/Command.php | 90 ++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/src/Commands/Command.php b/src/Commands/Command.php index d21ba39..9837e32 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -2,8 +2,11 @@ namespace Laracord\Commands; +use Discord\Parts\Channel\Message; +use Illuminate\Pipeline\Pipeline; use Illuminate\Support\Str; use Laracord\Commands\Contracts\Command as CommandContract; +use Laracord\Commands\Middleware\Context; abstract class Command extends AbstractCommand implements CommandContract { @@ -14,20 +17,6 @@ abstract class Command extends AbstractCommand implements CommandContract */ protected $aliases = []; - /** - * The command cooldown. - * - * @var int - */ - protected $cooldown = 0; - - /** - * The command cooldown message. - * - * @var string - */ - protected $cooldownMessage = ''; - /** * The command usage. * @@ -37,12 +26,8 @@ abstract class Command extends AbstractCommand implements CommandContract /** * Maybe handle the command. - * - * @param \Discord\Parts\Channel\Message $message - * @param array $args - * @return mixed */ - public function maybeHandle($message, $args) + public function maybeHandle(Message $message, array $args): void { if (! $this->canDirectMessage() && ! $message->guild_id) { return; @@ -52,8 +37,12 @@ public function maybeHandle($message, $args) return; } + if ($this->isOnCooldown($message->author, $message->guild)) { + return; + } + if (! $this->isAdminCommand()) { - $this->handle($message, $args); + $this->processMiddleware($message, $args); return; } @@ -62,65 +51,50 @@ public function maybeHandle($message, $args) return; } - $this->handle($message, $args); + $this->processMiddleware($message, $args); } /** - * Handle the command. - * - * @param \Discord\Parts\Channel\Message $message - * @param array $args - * @return mixed - */ - abstract public function handle($message, $args); - - /** - * Retrieve the command cooldown. - * - * @return int + * Process the command through its middleware stack. */ - public function getCooldown() + protected function processMiddleware(Message $message, array $args): mixed { - return $this->cooldown; - } - - /** - * Retrieve the command cooldown message. - * - * @return string - */ - public function getCooldownMessage() - { - return $this->cooldownMessage; + $context = new Context( + source: $message, + args: $args, + command: $this + ); + + return (new Pipeline($this->bot()->app)) + ->send($context) + ->through($this->getMiddleware()) + ->then(fn (Context $context) => $this->resolveHandler([ + 'message' => $context->source, + 'args' => $context->args, + ])); } /** * Retrieve the command signature. - * - * @return string */ - public function getSignature() + public function getSignature(): string { return Str::start($this->getName(), $this->bot()->getPrefix()); } /** - * Retrieve the command usage. - * - * @return string + * Retrieve the command aliases. */ - public function getUsage() + public function getAliases(): array { - return $this->usage; + return $this->aliases; } /** - * Retrieve the command aliases. - * - * @return array + * Retrieve the command usage. */ - public function getAliases() + public function getUsage(): string { - return $this->aliases; + return $this->usage; } } From 38554a9985d020ac156ee69a9f17f51c30e82c75 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:16:34 -0600 Subject: [PATCH 040/146] =?UTF-8?q?=F0=9F=A7=B1=20Handle=20command=20middl?= =?UTF-8?q?eware=20in=20context=20menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/ContextMenu.php | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Commands/ContextMenu.php b/src/Commands/ContextMenu.php index 37de1fd..29721a4 100644 --- a/src/Commands/ContextMenu.php +++ b/src/Commands/ContextMenu.php @@ -2,11 +2,11 @@ namespace Laracord\Commands; -use Discord\Parts\Channel\Message; use Discord\Parts\Interactions\Command\Command as DiscordCommand; use Discord\Parts\Interactions\Interaction; -use Discord\Parts\User\User; +use Illuminate\Pipeline\Pipeline; use Laracord\Commands\Contracts\ContextMenu as ContextMenuContract; +use Laracord\Commands\Middleware\Context; abstract class ContextMenu extends ApplicationCommand implements ContextMenuContract { @@ -30,21 +30,33 @@ public function create(): DiscordCommand 'nsfw' => $this->isNsfw(), ])->reject(fn ($value) => blank($value)); - return new DiscordCommand($this->discord(), $menu->all()); + return new DiscordCommand($this->discord, $menu->all()); } /** - * Handle the context menu interaction. + * Process the command through its middleware stack. */ - abstract public function handle(Interaction $interaction, Message|User|null $target): mixed; + protected function processMiddleware(Interaction $interaction, mixed $target = null): mixed + { + $context = new Context( + source: $interaction, + target: $target, + command: $this, + ); + + return (new Pipeline($this->bot()->app)) + ->send($context) + ->through($this->getMiddleware()) + ->then(fn (Context $context) => $this->resolveHandler([ + 'interaction' => $context->source, + 'target' => $context->target, + ])); + } /** * Maybe handle the context menu interaction. - * - * @param \Discord\Parts\Interactions\Interaction $interaction - * @return mixed */ - public function maybeHandle($interaction) + public function maybeHandle(Interaction $interaction): void { $target = match ($this->getType()) { DiscordCommand::USER => $interaction->data->resolved->users?->first(), @@ -53,23 +65,18 @@ public function maybeHandle($interaction) }; if (! $this->isAdminCommand()) { - $this->handle($interaction, $target); + $this->processMiddleware($interaction, $target); return; } if ($this->isAdminCommand() && ! $this->isAdmin($interaction->member->user)) { - return $interaction->respondWithMessage( - $this - ->message('You do not have permission to run this command.') - ->title('Permission Denied') - ->error() - ->build(), - ephemeral: true - ); + $this->handleDenied($interaction); + + return; } - $this->handle($interaction, $target); + $this->processMiddleware($interaction, $target); } /** From 5f9960185e57520100bb1fc4e6109515445bd333 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:16:48 -0600 Subject: [PATCH 041/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20the=20default?= =?UTF-8?q?=20`!help`=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/HelpCommand.php | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Commands/HelpCommand.php b/src/Commands/HelpCommand.php index 957f33a..23a5080 100644 --- a/src/Commands/HelpCommand.php +++ b/src/Commands/HelpCommand.php @@ -2,6 +2,8 @@ namespace Laracord\Commands; +use Discord\Parts\Channel\Message; + class HelpCommand extends Command { /** @@ -27,28 +29,20 @@ class HelpCommand extends Command /** * The response title. - * - * @var string */ - protected $title = 'Command Help'; + protected string $title = 'Command Help'; /** * The response message. - * - * @var string */ - protected $message = 'Here is a list of all available commands.'; + protected string $message = 'Here is a list of all available commands.'; /** * Handle the command. - * - * @param \Discord\Parts\Channel\Message $message - * @param array $args - * @return mixed */ - public function handle($message, $args) + public function handle(Message $message, array $args): void { - $commands = collect($this->bot()->getRegisteredCommands()) + $commands = collect($this->bot->getCommands()) ->filter(fn ($command) => ! $command->isHidden()) ->filter(fn ($command) => $command->getGuild() ? $message->guild_id === $command->getGuild() : true) ->sortBy('name'); @@ -67,10 +61,10 @@ public function handle($message, $args) $fields[' '] = ''; } - return $this->message() + $this + ->message($this->message) ->title($this->title) - ->content($this->message) ->fields($fields) - ->send($message->channel); + ->reply($message); } } From 9590b54cf46d18bab566b09101299c49cbcf9729 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:17:20 -0600 Subject: [PATCH 042/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Move=20the=20lo?= =?UTF-8?q?op=20logic=20to=20a=20trait=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB?= =?UTF-8?q?=20Get=20the=20loop=20from=20the=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasLoop.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Bot/Concerns/HasLoop.php diff --git a/src/Bot/Concerns/HasLoop.php b/src/Bot/Concerns/HasLoop.php new file mode 100644 index 0000000..fcfb31e --- /dev/null +++ b/src/Bot/Concerns/HasLoop.php @@ -0,0 +1,22 @@ +app->make(LoopInterface::class); + } +} From cc1c5bf6e459d25d6d83f025480e87d9a426d48b Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:18:01 -0600 Subject: [PATCH 043/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20support=20for=20resolving=20hooks=20through=20the=20containe?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHooks.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/Bot/Concerns/HasHooks.php diff --git a/src/Bot/Concerns/HasHooks.php b/src/Bot/Concerns/HasHooks.php new file mode 100644 index 0000000..a4026c3 --- /dev/null +++ b/src/Bot/Concerns/HasHooks.php @@ -0,0 +1,18 @@ +app->call([$this, $hook]); + } +} From 27affa76624309994df60f1cda45a0c88f8b530a Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:18:27 -0600 Subject: [PATCH 044/146] =?UTF-8?q?=E2=9C=A8=20Create=20initial=20default?= =?UTF-8?q?=20console=20prompts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Prompts/ClearPrompt.php | 37 ++++++++++++++++++++++ src/Console/Prompts/ExitPrompt.php | 37 ++++++++++++++++++++++ src/Console/Prompts/HelpPrompt.php | 45 +++++++++++++++++++++++++++ src/Console/Prompts/InvitePrompt.php | 31 ++++++++++++++++++ src/Console/Prompts/RestartPrompt.php | 37 ++++++++++++++++++++++ src/Console/Prompts/StatusPrompt.php | 44 ++++++++++++++++++++++++++ 6 files changed, 231 insertions(+) create mode 100644 src/Console/Prompts/ClearPrompt.php create mode 100644 src/Console/Prompts/ExitPrompt.php create mode 100644 src/Console/Prompts/HelpPrompt.php create mode 100644 src/Console/Prompts/InvitePrompt.php create mode 100644 src/Console/Prompts/RestartPrompt.php create mode 100644 src/Console/Prompts/StatusPrompt.php diff --git a/src/Console/Prompts/ClearPrompt.php b/src/Console/Prompts/ClearPrompt.php new file mode 100644 index 0000000..e710e7c --- /dev/null +++ b/src/Console/Prompts/ClearPrompt.php @@ -0,0 +1,37 @@ +line("\033[2J\033[3J\033[H"); + } +} diff --git a/src/Console/Prompts/ExitPrompt.php b/src/Console/Prompts/ExitPrompt.php new file mode 100644 index 0000000..bf735d1 --- /dev/null +++ b/src/Console/Prompts/ExitPrompt.php @@ -0,0 +1,37 @@ +shutdown(); + } +} diff --git a/src/Console/Prompts/HelpPrompt.php b/src/Console/Prompts/HelpPrompt.php new file mode 100644 index 0000000..386329d --- /dev/null +++ b/src/Console/Prompts/HelpPrompt.php @@ -0,0 +1,45 @@ +getCommands()); + + $console->table( + ['Name', 'Description'], + $commands->map(fn ($command) => [ + $command->getName(), + $command->getDescription(), + ])->all(), + ); + } +} diff --git a/src/Console/Prompts/InvitePrompt.php b/src/Console/Prompts/InvitePrompt.php new file mode 100644 index 0000000..a9e3b2a --- /dev/null +++ b/src/Console/Prompts/InvitePrompt.php @@ -0,0 +1,31 @@ +showInvite(force: true); + } +} diff --git a/src/Console/Prompts/RestartPrompt.php b/src/Console/Prompts/RestartPrompt.php new file mode 100644 index 0000000..ee3bbbc --- /dev/null +++ b/src/Console/Prompts/RestartPrompt.php @@ -0,0 +1,37 @@ +restart(); + } +} diff --git a/src/Console/Prompts/StatusPrompt.php b/src/Console/Prompts/StatusPrompt.php new file mode 100644 index 0000000..42635af --- /dev/null +++ b/src/Console/Prompts/StatusPrompt.php @@ -0,0 +1,44 @@ +getUptime()->diffForHumans(null, true); + + $status = $bot->getStatus()->merge([ + 'user' => $bot->discord->users->count(), + 'guild' => $bot->discord->guilds->count(), + ])->mapWithKeys(fn ($count, $type) => [Str::plural($type, $count) => $count]); + + $status = $status + ->prepend($uptime, 'uptime') + ->prepend("{$bot->discord->username} ({$bot->discord->id})", 'bot') + ->map(fn ($count, $type) => Str::of($type)->title()->finish(": {$count}")->toString()); + + $console->outputComponents()->bulletList($status->all()); + } +} From b5ce7340eac4e2ca724eba8905d8230659dbb989 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:19:38 -0600 Subject: [PATCH 045/146] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Dramatically=20?= =?UTF-8?q?improve=20registration=20within=20the=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 154 +++++++++++++++++++++++++++----- 1 file changed, 134 insertions(+), 20 deletions(-) diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 2cd6401..f44e014 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -2,21 +2,35 @@ namespace Laracord; +use Illuminate\Console\Command; +use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Http\Kernel as KernelContract; +use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\ServiceProvider; +use Laracord\Console\Commands; +use Laracord\Console\Console; +use Laracord\Console\Prompts; +use Laracord\Discord\Message; use Laracord\Http\Kernel; use LaravelZero\Framework\Components\Database\Provider as DatabaseProvider; +use LaravelZero\Framework\Components\Log\Provider as LogProvider; +use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; +use React\Stream\CompositeStream; +use React\Stream\ReadableResourceStream; +use React\Stream\WritableResourceStream; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Finder\Finder; -class LaracordServiceProvider extends ServiceProvider +abstract class LaracordServiceProvider extends AggregateServiceProvider { /** - * The default providers. + * The provider class names. * * @var array */ @@ -30,10 +44,12 @@ class LaracordServiceProvider extends ServiceProvider \Illuminate\View\ViewServiceProvider::class, \Illuminate\Cookie\CookieServiceProvider::class, \Illuminate\Session\SessionServiceProvider::class, - \Laracord\Providers\RouteServiceProvider::class, + \Laracord\Http\Providers\RouteServiceProvider::class, \Intonate\TinkerZero\TinkerZeroServiceProvider::class, ]; + abstract public function bot(Laracord $bot): Laracord; + /** * Register any application services. * @@ -44,13 +60,27 @@ public function register() $this->mergeConfigs(); $this->createDirectories(); - foreach ($this->providers as $provider) { - $this->app->register($provider); - } + parent::register(); $this->registerDatabase(); + $this->registerLoop(); + $this->registerConsole(); + $this->registerLogger(); $this->app->singleton(KernelContract::class, Kernel::class); + + $this->app->singleton(Laracord::class, fn () => tap(Laracord::make($this->app), function (Laracord $bot) { + $this + ->registerDefaultComponents($bot) + ->registerDefaultPrompts($bot); + + $this->app->singleton(Message::class, fn () => Message::make($bot)); + + return $this->bot($bot); + })); + + $this->app->alias(Laracord::class, 'bot'); + $this->app->alias(Message::class, 'bot.message'); } /** @@ -61,23 +91,107 @@ public function register() public function boot() { $this->commands([ - Console\Commands\AdminCommand::class, - Console\Commands\BootCommand::class, - Console\Commands\ConsoleMakeCommand::class, - Console\Commands\ControllerMakeCommand::class, - Console\Commands\EventMakeCommand::class, - Console\Commands\KeyGenerateCommand::class, - Console\Commands\MakeCommand::class, - Console\Commands\MakeSlashCommand::class, - Console\Commands\MakeMenuCommand::class, - Console\Commands\ModelMakeCommand::class, - Console\Commands\ServiceMakeCommand::class, - Console\Commands\TokenMakeCommand::class, + Commands\AdminCommand::class, + Commands\BootCommand::class, + Commands\ConsoleMakeCommand::class, + Commands\ControllerMakeCommand::class, + Commands\EventMakeCommand::class, + Commands\KeyGenerateCommand::class, + Commands\MakeCommand::class, + Commands\MakeCommandMiddlewareCommand::class, + Commands\MakeSlashCommand::class, + Commands\MakeMenuCommand::class, + Commands\ModelMakeCommand::class, + Commands\ServiceMakeCommand::class, + Commands\TokenMakeCommand::class, ]); $this->registerMacros(); } + /** + * Register the event loop. + */ + protected function registerLoop(): void + { + $this->app->singleton(LoopInterface::class, fn () => Loop::get()); + $this->app->alias(LoopInterface::class, 'bot.loop'); + } + + /** + * Register the console. + */ + protected function registerConsole(): void + { + $this->app->singleton(Console::class, function () { + $loop = $this->app->make(LoopInterface::class); + + $console = new Console( + stdio: new CompositeStream( + new ReadableResourceStream(STDIN, $loop), + new WritableResourceStream(STDOUT, $loop), + ), + laravel: $this->app, + output: new ConsoleOutput, + input: new StringInput(''), + ); + + if ($console->hasColorSupport()) { + $console->getOutput()->setDecorated(true); + } + + // foreach ($this->app->make(ConsoleKernel::class)->all() as $command) { + // if ($command instanceof Command) { + // $console->addCommand($command); + // } + // } + + $this->app->instance('bot.console', $console); + + return $console; + }); + } + + /** + * Register the logger. + */ + protected function registerLogger(): void + { + $this->app->booting(fn () => $this->app->register(LogProvider::class)); + } + + /** + * Register the default components. + */ + protected function registerDefaultComponents(Laracord $bot): self + { + $bot + ->discoverCommands(in: app_path('Commands'), for: 'App\\Commands') + ->discoverSlashCommands(in: app_path('SlashCommands'), for: 'App\\SlashCommands') + ->discoverContextMenus(in: app_path('Menus'), for: 'App\\Menus') + ->discoverEvents(in: app_path('Events'), for: 'App\\Events') + ->discoverServices(in: app_path('Services'), for: 'App\\Services'); + + return $this; + } + + /** + * Register the default console prompts. + */ + protected function registerDefaultPrompts(Laracord $bot): self + { + $bot->registerPrompts([ + Prompts\ExitPrompt::class, + Prompts\HelpPrompt::class, + Prompts\InvitePrompt::class, + Prompts\RestartPrompt::class, + Prompts\StatusPrompt::class, + Prompts\ClearPrompt::class, + ]); + + return $this; + } + /** * Retrieve configuration files from the specified path. */ @@ -152,7 +266,7 @@ protected function registerMacros(): void Storage::macro('getAsync', fn (string $path) => Laracord::handleAsync(fn () => Storage::get($path))); Storage::macro('putAsync', fn (string $path, mixed $contents) => Laracord::handleAsync(fn () => Storage::put($path, $contents))); - Date::macro('toDiscord', function ($format = 'R') { + Date::macro('toDiscord', function (string $format = 'R') { $format = match ($format) { 't', 'T', 'd', 'D', 'f', 'F', 'R' => $format, default => 'R', From 0112f6df59952765ac6cd0d484892d3cad90d478 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:22:33 -0600 Subject: [PATCH 046/146] =?UTF-8?q?=E2=99=BB=20Refactor=20the=20`Laracord`?= =?UTF-8?q?=20class=20(Fixes=20#67)=20=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20?= =?UTF-8?q?Create=20a=20`Laracord`=20facade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Facades/Laracord.php | 93 +++ src/Laracord.php | 1306 ++------------------------------------ 2 files changed, 163 insertions(+), 1236 deletions(-) create mode 100644 src/Facades/Laracord.php diff --git a/src/Facades/Laracord.php b/src/Facades/Laracord.php new file mode 100644 index 0000000..a4cd2e0 --- /dev/null +++ b/src/Facades/Laracord.php @@ -0,0 +1,93 @@ +console = $console; - $this->app = $console->getLaravel(); - $this->admins = config('discord.admins', $this->admins); + set_rejection_handler(fn (Throwable $e) => report($e)); } /** - * Make the Bot instance. + * Make a new Laracord instance. */ - public static function make(ConsoleCommand $console): self + public static function make(Application $app): self { - return new static($console); + return new static($app); } /** @@ -213,25 +57,37 @@ public static function make(ConsoleCommand $console): self */ public function boot(): void { - set_rejection_handler(fn (Throwable $e) => $this->console()->error($e->getTraceAsString())); + if ($this->isBooted()) { + return; + } - $this->beforeBoot(); + $this->registerConsole(); + $this->registerLogger(); - $this->bootDiscord(); + rescue(fn () => $this->handleBoot()); - $this->registerStream(); + $this->booted = true; + } + + /** + * Handle the boot process. + */ + protected function handleBoot(): void + { + $this->registerDiscord(); - $this->registerCommands(); + $this->callHook('beforeBoot'); - $this->discord()->on('init', function () { + $this->discord->on('init', function () { $this - ->registerEvents() + ->bootApplicationCommands() + ->bootCommands() + ->bootEvents() ->bootServices() ->bootHttpServer() - ->registerApplicationCommands() ->handleInteractions(); - $this->afterBoot(); + $this->callHook('afterBoot'); $this->getLoop()->addTimer(1, function () { $status = $this @@ -241,84 +97,17 @@ public function boot(): void $status = Str::replaceLast(', ', ', and ', $status); - $this->console()->log("Successfully booted {$this->getName()} with {$status}."); + $this->logger->info("Successfully booted {$this->getName()} with {$status}."); $this ->showCommands() ->showInvite(); + + $this->console->newLine()->showPrompt(); }); }); - $this->discord()->run(); - } - - /** - * Boot the Discord client. - */ - protected function bootDiscord(): void - { - $this->discord = new Discord([ - 'token' => $this->getToken(), - 'prefixes' => $this->getPrefixes()->all(), - 'description' => $this->getDescription(), - 'discordOptions' => $this->getOptions(), - 'defaultHelpCommand' => false, - ]); - } - - /** - * Register the input and output streams. - */ - protected function registerStream(): self - { - if (windows_os()) { - return $this; - } - - if ($this->inputStream && $this->outputStream) { - return $this; - } - - $this->inputStream = new ReadableResourceStream(STDIN, $this->getLoop()); - $this->outputStream = new WritableResourceStream(STDOUT, $this->getLoop()); - - $this->inputStream->on('data', fn ($data) => $this->handleStream($data)); - - return $this; - } - - /** - * Handle the input stream. - */ - protected function handleStream(string $data): void - { - $command = trim($data); - - if (! $command) { - $this->outputStream->write('> '); - - return; - } - - $this->console()->newLine(); - - match ($command) { - 'shutdown', 'exit', 'quit', 'stop' => $this->shutdown(), - 'restart' => $this->restart(), - 'invite' => $this->showInvite(force: true), - 'commands' => $this->showCommands(), - 'status' => $this->showStatus(), - '?' => $this->console()->table(['Command', 'Description'], [ - ['shutdown', 'Shutdown the bot.'], - ['restart', 'Restart the bot.'], - ['invite', 'Show the invite link.'], - ['commands', 'Show the registered commands.'], - ['status', 'Show the bot status.'], - ]), - default => $this->console()->error("Unknown command: {$command}"), - }; - - $this->outputStream->write('> '); + $this->discord->run(); } /** @@ -326,7 +115,7 @@ protected function handleStream(string $data): void */ public function shutdown(int $code = 0): void { - $this->console()->log("Shutting down {$this->getName()}."); + $this->logger->info("Shutting down {$this->getName()}."); $this->httpServer()->shutdown(); $this->discord()->close(); @@ -339,999 +128,44 @@ public function shutdown(int $code = 0): void */ public function restart(): void { - $this->console()->log("{$this->getName()} is restarting."); + $this->logger->info("{$this->getName()} is restarting."); - $this->httpServer()->shutdown(); - $this->discord()->close(); + $this->discord->close(closeLoop: false); - $this->httpServer = null; $this->discord = null; - $this->registeredCommands = []; - $this->registeredContextMenus = []; - $this->registeredEvents = []; - $this->registeredServices = []; - - $this->boot(); - } - - /** - * Actions to run before booting the bot. - */ - public function beforeBoot(): void - { - // - } - - /** - * Actions to run after booting the bot. - */ - public function afterBoot(): void - { - // - } - - /** - * The HTTP routes. - */ - public function routes(): void - { - // - } - - /** - * The HTTP middleware. - */ - public function middleware(): array - { - return []; - } - - /** - * The prepended HTTP middleware. - */ - public function prependMiddleware(): array - { - return []; - } - - /** - * Register the bot commands. - */ - protected function registerCommands(): self - { - foreach ($this->getCommands() as $command) { - $command = $command::make($this); - - if (! $command->isEnabled()) { - continue; - } - - $options = [ - 'cooldown' => $command->getCooldown() ?: 0, - 'cooldownMessage' => $command->getCooldownMessage() ?: '', - 'description' => $command->getDescription() ?: '', - 'usage' => $command->getUsage() ?: '', - 'aliases' => $command->getAliases(), - ]; - - $this->discord->registerCommand( - $command->getName(), - fn ($message, $args) => $this->handleSafe($command->getName(), fn () => $command->maybeHandle($message, $args)), - $options - ); - - $this->registeredCommands[] = $command; - - $this->registerInteractions($command->getName(), $command->interactions()); - } - - return $this; - } - - /** - * Register the bot application commands. - */ - protected function registerApplicationCommands(): self - { - $normalize = function ($data) use (&$normalize) { - if (is_object($data)) { - $data = (array) $data; - } - - if (is_array($data)) { - ksort($data); - - return array_map($normalize, $data); - } - - return $data; - }; - - $existing = cache()->get('laracord.application-commands', []); - - if (! $existing) { - $existing[] = $this->discord->application->commands->freshen(); - - foreach ($this->discord->guilds as $guild) { - $existing[] = $guild->commands->freshen(); - } - - $existing = all($existing)->then(fn ($commands) => collect($commands) - ->flatMap(fn ($command) => $command->toArray()) - ->map(fn ($command) => collect($command->getCreatableAttributes()) - ->merge([ - 'id' => $command->id, - 'guild_id' => $command->guild_id ?? null, - 'dm_permission' => $command->guild_id ? null : ($command->dm_permission ?? false), - 'default_permission' => $command->default_permission ?? true, - ]) - ->all() - ) - ->map(fn ($command) => array_merge($command, [ - 'options' => json_decode(json_encode($command['options'] ?? []), true), - ])) - ->filter(fn ($command) => ! blank($command)) - ->keyBy('name') - ); - - $existing = await($existing); - - cache()->forever('laracord.application-commands', $existing); - } - - $existing = collect($existing); - - $registered = collect($this->getSlashCommands()) - ->merge($this->getContextMenus()) - ->map(fn ($command) => $command::make($this)) - ->filter(fn ($command) => $command->isEnabled()) - ->mapWithKeys(function ($command) { - $attributes = $command->create()->getCreatableAttributes(); - - $attributes = collect($attributes) - ->merge([ - 'guild_id' => $command->getGuild() ?? null, - 'dm_permission' => ! $command->getGuild() ? $command->canDirectMessage() : null, - 'nsfw' => $command->isNsfw(), - ]) - ->sortKeys() - ->all(); - - return [$command->getName() => [ - 'state' => $command, - 'attributes' => $attributes, - ]]; - }); - - $created = $registered->reject(fn ($command, $name) => $existing->has($name))->filter(); - $deleted = $existing->reject(fn ($command, $name) => $registered->has($name))->filter(); - - $updated = $registered - ->map(function ($command) { - $attributes = collect($command['attributes']) - ->reject(fn ($value) => blank($value)) - ->all(); - - return array_merge($command, ['attributes' => $attributes]); - }) - ->filter(function ($command, $name) use ($existing, $normalize) { - if (! $existing->has($name)) { - return false; - } - - $current = collect($existing->get($name)) - ->forget('id') - ->reject(fn ($value) => blank($value)); - - $attributes = collect($command['attributes']) - ->reject(fn ($value) => blank($value)); - - $keys = collect($current->keys()) - ->merge($attributes->keys()) - ->unique(); - - foreach ($keys as $key) { - $attribute = $current->get($key); - $value = $attributes->get($key); - - $attribute = $normalize($attribute); - $value = $normalize($value); - - if ($attribute === $value) { - continue; - } - - return true; - } - - return false; - }) - ->each(function ($command) use ($existing) { - $state = $existing->get($command['state']->getName()); - - $current = Arr::get($command, 'attributes.guild_id'); - $existing = Arr::get($state, 'guild_id'); - - if ($current && ! $existing) { - $this->unregisterApplicationCommand($state['id']); - } - - if ((! $current && $existing) || $current !== $existing) { - $this->unregisterApplicationCommand($state['id'], $existing); - } - }); - - if ($updated->isNotEmpty()) { - $this->console()->warn("Updating {$updated->count()} application command(s)."); - - $updated->each(function ($command) { - $state = $command['state']; - - $this->registerApplicationCommand($state); - }); - } - - if ($deleted->isNotEmpty()) { - $this->console()->warn("Deleting {$deleted->count()} application command(s)."); - - $deleted->each(fn ($command) => $this->unregisterApplicationCommand($command['id'], $command['guild_id'] ?? null)); - } - - if ($created->isNotEmpty()) { - $this->console()->log("Creating {$created->count()} new application command(s)."); - - $created->each(fn ($command) => $this->registerApplicationCommand($command['state'])); - } - - if ($registered->isEmpty()) { - return $this; - } - - $registered->each(function ($command, $name) { - $this->registerInteractions($name, $command['state']->interactions()); - - if ($command['state'] instanceof ContextMenu) { - $this->discord()->listenCommand( - $name, - fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandle($interaction)) - ); - - $this->registeredContextMenus[] = $command['state']; - - return; - } - - $subcommands = collect($command['state']->getRegisteredOptions()) - ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND) - ->map(fn (Option $subcommand) => [$name, $subcommand->name]); - - $subcommandGroups = collect($command['state']->getRegisteredOptions()) - ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND_GROUP) - ->flatMap(fn (Option $group) => collect($group->options) - ->filter(fn (Option $subcommand) => $subcommand->type === Option::SUB_COMMAND) - ->map(fn (Option $subcommand) => [$name, $group->name, $subcommand->name]) - ); - - $subcommands = $subcommands->merge($subcommandGroups); - - if ($subcommands->isNotEmpty()) { - $subcommands->each(function ($names) use ($command, $name) { - $this->discord()->listenCommand( - $names, - fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandle($interaction)), - fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandleAutocomplete($interaction)) - ); - }); - - return; - } - - $this->discord()->listenCommand( - $name, - fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandle($interaction)), - fn ($interaction) => $this->handleSafe($name, fn () => $command['state']->maybeHandleAutocomplete($interaction)) - ); - }); - - $this->registeredCommands = array_merge( - $this->registeredCommands, - $registered->pluck('state')->reject(fn ($command) => $command instanceof ContextMenu)->all() - ); - - return $this; - } - - /** - * Register the specified application command. - */ - public function registerApplicationCommand(ApplicationCommand $command): void - { - cache()->forget('laracord.application-commands'); - - if ($command->getGuild()) { - $guild = $this->discord()->guilds->get('id', $command->getGuild()); - - if (! $guild) { - $this->console()->warn("The {$command->getName()} command failed to register because the guild {$command->getGuild()} could not be found."); - - return; - } - - $guild->commands->save($command->create()); - - return; - } - - $this->discord()->application->commands->save($command->create()); - } - - /** - * Unregister the specified application command. - */ - public function unregisterApplicationCommand(string $id, ?string $guildId = null): void - { - cache()->forget('laracord.application-commands'); - - if ($guildId) { - $guild = $this->discord()->guilds->get('id', $guildId); - - if (! $guild) { - $this->console()->warn("The command with ID {$id} failed to unregister because the guild {$guildId} could not be found."); - - return; - } - - $guild->commands->delete($id); - - return; - } - - $this->discord()->application->commands->delete($id); + $this->handleBoot(); } /** - * Register the interaction routes. + * Retrieve the bot status collection. */ - protected function registerInteractions(string $name, array $routes = []): void + public function getStatus(): Collection { - $routes = collect($routes) - ->mapWithKeys(fn ($value, $route) => ["{$name}@{$route}" => $value]) - ->all(); - - if (! $routes) { - return; - } - - $this->registeredInteractions = array_merge($this->registeredInteractions, $routes); + return collect([ + 'command' => count($this->commands), + 'slash command' => count($this->slashCommands), + 'menu' => count($this->contextMenus), + 'event' => count($this->events), + 'service' => count($this->services), + 'interaction' => count($this->interactions), + 'route' => count(Route::getRoutes()->getRoutes()), + ])->filter()->mapWithKeys(fn ($count, $type) => [Str::plural($type, $count) => $count]); } /** - * Register the Discord events. + * Retrieve the bot uptime. */ - protected function registerEvents(): self - { - foreach ($this->getEvents() as $event) { - $this->handleSafe($event, function () use ($event) { - $event = $event::make($this); - - if (! $event->isEnabled()) { - return; - } - - $this->registeredEvents[] = $event->register(); - - $this->console()->log("The {$event->getName()} event has been registered to {$event->getHandler()}."); - }); - } - - return $this; - } - - /** - * Boot the bot services. - */ - protected function bootServices(): self - { - foreach ($this->getServices() as $service) { - $this->handleSafe($service, function () use ($service) { - $service = $service::make($this); - - if (! $service->isEnabled()) { - return; - } - - $this->registeredServices[] = $service->boot(); - - $this->console()->log("The {$service->getName()} service has been booted."); - }); - } - - return $this; - } - - /** - * Handle the interaction routes. - */ - protected function handleInteractions(): self - { - $this->discord()->on(DiscordEvent::INTERACTION_CREATE, function (Interaction $interaction) { - $id = $interaction->data->custom_id; - - $handlers = collect($this->getRegisteredInteractions()) - ->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. - */ - protected function bootHttpServer(): self - { - if ($this->httpServer) { - return $this; - } - - $this->handleSafe(Server::class, function () { - $this->routes(); - - $this->app->booted(function () { - $this->app['router']->getRoutes()->refreshNameLookups(); - $this->app['router']->getRoutes()->refreshActionLookups(); - }); - - $this->httpServer = Server::make($this)->boot(); - - if ($this->httpServer->isBooted()) { - $this->console()->log("HTTP server started on {$this->httpServer->getAddress()}."); - } - }); - - return $this; - } - - /** - * Print the registered commands to console. - */ - public function showCommands(): self - { - if (! $this->showCommands) { - return $this; - } - - $this->console()->table( - ['Command', 'Description'], - collect($this->getRegisteredCommands())->map(fn ($command) => [ - $command->getSignature(), - $command->getDescription(), - ])->toArray() - ); - - return $this; - } - - /** - * Print the bot status to console. - */ - public function showStatus(): self - { - $uptime = now()->createFromTimestamp(LARAVEL_START)->diffForHumans(); - - $status = $this->getStatus() - ->prepend($this->discord()->users->count(), 'user') - ->prepend($this->discord()->guilds->count(), 'guild') - ->mapWithKeys(fn ($count, $type) => [Str::plural($type, $count) => $count]) - ->prepend($uptime, 'uptime') - ->prepend("{$this->discord()->username} ({$this->discord()->id})", 'bot') - ->map(fn ($count, $type) => Str::of($type)->title()->finish(": {$count}")->toString()); - - $this->console()->line(' Status'); - $this->console()->outputComponents()->bulletList($status->all()); - - return $this; - } - - /** - * Show the invite link if the bot is not in any guilds. - */ - public function showInvite(bool $force = false): self - { - if (! $force && (! $this->showInvite || $this->discord()->guilds->count() > 0)) { - return $this; - } - - if (! $force) { - $this->console()->warn("{$this->getName()} is currently not in any guilds."); - } - - $query = Arr::query([ - 'client_id' => $this->discord()->id, - 'permissions' => 281600, - 'scope' => 'bot applications.commands', - ]); - - $this->console()->log("You can invite {$this->getName()} using the following link:"); - - $this->console()->outputComponents()->bulletList(["https://discord.com/api/oauth2/authorize?{$query}"]); - - return $this; - } - - /** - * Get the bot name. - */ - public function getName(): string - { - if ($this->name) { - return $this->name; - } - - return $this->name = config('app.name'); - } - - /** - * Get the bot description. - */ - public function getDescription(): string - { - if ($this->description) { - return $this->description; - } - - return $this->description = config('discord.description'); - } - - /** - * Get the bot token. - */ - public function getToken(): string - { - if ($this->token) { - return $this->token; - } - - $token = config('discord.token'); - - if (! $token) { - throw new Exception('You must provide a Discord bot token.'); - } - - return $this->token = $token; - } - - /** - * Get the bot intents. - */ - public function getIntents(): ?int - { - if ($this->intents) { - return $this->intents; - } - - return $this->intents = config('discord.intents', Intents::getDefaultIntents()); - } - - /** - * Get the bot options. - */ - public function getOptions(): array - { - if ($this->options) { - return $this->options; - } - - $defaultOptions = [ - 'intents' => $this->getIntents(), - 'logger' => $this->getLogger(), - 'loop' => $this->getLoop(), - ]; - - return $this->options = [ - ...config('discord.options', []), - ...$defaultOptions, - ]; - } - - /** - * Get the logger instance. - */ - public function getLogger(): Logger - { - return $this->logger ??= Logger::make($this->console); - } - - /** - * Get the Discord admins. - */ - public function getAdmins(): array - { - return $this->admins; - } - - /** - * Get the Discord events. - */ - public function getEvents(): array - { - if ($this->events) { - return $this->events; - } - - $events = $this->extractClasses($this->getEventPath()) - ->merge(config('discord.events', [])) - ->unique() - ->filter(fn ($event) => $this->handleSafe($event, fn () => is_subclass_of($event, Event::class) && ! (new ReflectionClass($event))->isAbstract())) - ->all(); - - return $this->events = $events; - } - - /** - * Get the bot services. - */ - public function getServices(): array - { - if ($this->services) { - return $this->services; - } - - $services = $this->extractClasses($this->getServicePath()) - ->merge(config('discord.services', [])) - ->unique() - ->filter(fn ($service) => $this->handleSafe($service, fn () => is_subclass_of($service, Service::class) && ! (new ReflectionClass($service))->isAbstract())) - ->all(); - - return $this->services = $services; - } - - /** - * Get the bot commands. - */ - public function getCommands(): array - { - if ($this->commands) { - return $this->commands; - } - - $commands = $this->extractClasses($this->getCommandPath()) - ->merge(config('discord.commands', [])) - ->unique() - ->filter(fn ($command) => $this->handleSafe($command, fn () => is_subclass_of($command, Command::class) && ! (new ReflectionClass($command))->isAbstract())) - ->all(); - - return $this->commands = $commands; - } - - /** - * Get the bot slash commands. - */ - public function getSlashCommands(): array - { - if ($this->slashCommands) { - return $this->slashCommands; - } - - $slashCommands = $this->extractClasses($this->getSlashCommandPath()) - ->merge(config('discord.commands', [])) - ->unique() - ->filter(fn ($command) => $this->handleSafe($command, fn () => is_subclass_of($command, SlashCommand::class) && ! (new ReflectionClass($command))->isAbstract())) - ->all(); - - return $this->slashCommands = $slashCommands; - } - - /** - * Get the bot context menus. - */ - public function getContextMenus(): array - { - if ($this->contextMenus) { - return $this->contextMenus; - } - - $contextMenus = $this->extractClasses($this->getContextMenuPath()) - ->merge(config('discord.menus', [])) - ->unique() - ->filter(fn ($contextMenu) => $this->handleSafe($contextMenu, fn () => is_subclass_of($contextMenu, ContextMenu::class) && ! (new ReflectionClass($contextMenu))->isAbstract())) - ->all(); - - return $this->contextMenus = $contextMenus; - } - - /** - * Extract classes from the provided application path. - */ - protected function extractClasses(string $path): Collection - { - if (! File::isDirectory($path)) { - return collect(); - } - - return collect(File::allFiles($path)) - ->map(function ($file) { - $relativePath = str_replace( - Str::finish(app_path(), DIRECTORY_SEPARATOR), - '', - $file->getPathname() - ); - - $folders = Str::beforeLast( - $relativePath, - DIRECTORY_SEPARATOR - ).DIRECTORY_SEPARATOR; - - $className = Str::after($relativePath, $folders); - - $class = app()->getNamespace().str_replace( - ['/', '.php'], - ['\\', ''], - $folders.$className - ); - - return $class; - }); - } - - /** - * Get the registered commands. - */ - public function getRegisteredCommands(): array - { - return $this->registeredCommands; - } - - /** - * Get the registered context menus. - */ - public function getRegisteredContextMenus(): array - { - return $this->registeredContextMenus; - } - - /** - * Get the registered events. - */ - public function getRegisteredEvents(): array - { - return $this->registeredEvents; - } - - /** - * Get the registered services. - */ - public function getRegisteredServices(): array - { - return $this->registeredServices; - } - - /** - * Get the registered interactions. - */ - public function getRegisteredInteractions(): array - { - return $this->registeredInteractions; - } - - /** - * Get a registered command by name. - */ - public function getCommand(string $name): Command|SlashCommand|null - { - return collect($this->getRegisteredCommands()) - ->first(fn ($command) => $command->getName() === $name); - } - - /** - * Get a registered context menu by name. - */ - public function getContextMenu(string $name): ?ContextMenu - { - return collect($this->getRegisteredContextMenus()) - ->first(fn ($contextMenu) => $contextMenu->getName() === $name); - } - - /** - * Get the path to the Discord commands. - */ - public function getCommandPath(): string - { - return app_path('Commands'); - } - - /** - * Get the path to the Discord slash commands. - */ - public function getSlashCommandPath(): string - { - return app_path('SlashCommands'); - } - - /** - * Get the path to the Discord context menus. - */ - public function getContextMenuPath(): string - { - return app_path('Menus'); - } - - /** - * Get the path to the Discord events. - */ - public function getEventPath(): string - { - return app_path('Events'); - } - - /** - * Get the path to the bot services. - */ - public function getServicePath(): string - { - return app_path('Services'); - } - - /** - * Retrieve the prefixes. - */ - public function getPrefixes(): Collection - { - if ($this->prefixes) { - return $this->prefixes; - } - - $prefixes = collect(config('discord.prefix', '!')) - ->filter() - ->reject(fn ($prefix) => Str::startsWith($prefix, '/')); - - if ($prefixes->isEmpty()) { - throw new Exception('You must provide a valid command prefix.'); - } - - return $this->prefixes = $prefixes; - } - - /** - * Retrieve the primary prefix. - */ - public function getPrefix(): string - { - return $this->getPrefixes()->first(); - } - - /** - * Get the event loop. - */ - public function getLoop(): LoopInterface - { - if ($this->loop) { - return $this->loop; - } - - return $this->loop = Loop::get(); - } - - /** - * Get the Discord instance. - */ - public function discord(): ?Discord - { - return $this->discord; - } - - /** - * Get the console instance. - */ - public function console(): ConsoleCommand - { - return $this->console; - } - - /** - * Get the HTTP server instance. - */ - public function httpServer(): ?Server - { - return $this->httpServer; - } - - /** - * Get the Application instance. - */ - public function getApplication(): Application - { - return $this->app; - } - - /** - * Retrieve the bot status collection. - */ - public function getStatus(): Collection - { - return collect([ - 'command' => count($this->getRegisteredCommands()), - 'menu' => count($this->getRegisteredContextMenus()), - 'event' => count($this->getRegisteredEvents()), - 'service' => count($this->getRegisteredServices()), - 'interaction' => count($this->getRegisteredInteractions()), - 'route' => count(Route::getRoutes()->getRoutes()), - ])->filter()->mapWithKeys(fn ($count, $type) => [Str::plural($type, $count) => $count]); - } - - /** - * Retrieve the bot uptime. - */ - public function getUptime(): Carbon + public function getUptime(): Carbon { return now()->createFromTimestamp(LARAVEL_START); } /** - * 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([ - sprintf('%s in %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), - ]); - } - - return null; - } - - /** - * Build an embed for use in a Discord message. - * - * @param string $content - * @return \Laracord\Discord\Message + * Determine if the bot is booted. */ - public function message($content = '') + public function isBooted(): bool { - return Message::make($this) - ->content($content); + return $this->booted; } } From 4520a7b502adcffbd585e9e58d49794e64730ce3 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 06:24:09 -0600 Subject: [PATCH 047/146] =?UTF-8?q?=F0=9F=9A=A8=20Run=20Pint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Facades/Laracord.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Facades/Laracord.php b/src/Facades/Laracord.php index a4cd2e0..ba39dd4 100644 --- a/src/Facades/Laracord.php +++ b/src/Facades/Laracord.php @@ -13,7 +13,6 @@ * @method static \Illuminate\Support\Collection getStatus() Retrieve the bot status collection * @method static \Carbon\Carbon getUptime() Retrieve the bot uptime * @method static bool isBooted() Determine if the bot is booted - * * @method static \Illuminate\Support\Collection getPrefixes() Retrieve the command prefixes * @method static string getPrefix() Retrieve the primary prefix * @method static self registerCommand(\Laracord\Commands\Command|string $command) Register a command @@ -21,52 +20,42 @@ * @method static self discoverCommands(string $in, string $for) Discover commands in a path * @method static array getCommands() Get the registered commands * @method static ?\Laracord\Commands\Command getCommand(string $name) Get a registered command by name - * * @method static self registerSlashCommand(\Laracord\Commands\SlashCommand|string $command) Register a slash command * @method static self registerSlashCommands(array $commands) Register multiple slash commands * @method static self discoverSlashCommands(string $in, string $for) Discover slash commands in a path * @method static ?\Laracord\Commands\SlashCommand getSlashCommand(string $name) Get a registered slash command by name * @method static array getSlashCommands() Get the registered slash commands - * * @method static self registerContextMenu(\Laracord\Commands\ContextMenu|string $menu) Register a context menu * @method static self registerContextMenus(array $menus) Register multiple context menus * @method static self discoverContextMenus(string $in, string $for) Discover context menus in a path * @method static array getContextMenus() Get the registered context menus * @method static ?\Laracord\Commands\ContextMenu getContextMenu(string $name) Get a registered context menu by name - * * @method static self registerEvent(\Laracord\Events\Event|string $event) Register an event * @method static self registerEvents(array $events) Register multiple events * @method static self discoverEvents(string $in, string $for) Discover events in a path * @method static array getEvents() Get the registered events * @method static ?\Laracord\Events\Event getEvent(string $name) Get a registered event by name - * * @method static self registerService(\Laracord\Services\Service|string $service) Register a service * @method static self registerServices(array $services) Register multiple services * @method static self discoverServices(string $in, string $for) Discover services in a path * @method static array getServices() Get the registered services * @method static ?\Laracord\Services\Service getService(string $name) Get a registered service by name - * * @method static self registerPrompt(\Laracord\Console\Prompts\Prompt|string $prompt) Register a console prompt * @method static self registerPrompts(array $prompts) Register multiple console prompts * @method static array getPrompts() Get the registered prompts * @method static ?\Laracord\Console\Console console() Get the console instance - * * @method static self registerCommandMiddleware(string|\Laracord\Commands\Middleware\Middleware $middleware) Register a global command middleware * @method static self registerCommandMiddlewares(array $middlewares) Register multiple global command middleware * @method static array getCommandMiddleware() Get the global command middleware - * * @method static self registerInteractionMiddleware(string|\Laracord\Commands\Middleware\Middleware $middleware) Register an interaction middleware * @method static self registerInteractionMiddlewares(array $middlewares) Register multiple interaction middleware * @method static array getInteractionMiddleware() Get the interaction middleware - * * @method static self withRoutes(?callable $callback = null) Register HTTP routes * @method static self withMiddleware(?callable $callback = null) Register HTTP middleware - * * @method static self plugin(\Laracord\Contracts\Plugin $plugin) Register a plugin * @method static self plugins(array $plugins) Register multiple plugins * @method static array getPlugins() Get the registered plugins * @method static ?\Laracord\Contracts\Plugin getPlugin(string $plugin) Retrieve a registered plugin - * * @method static string getName() Get the bot name * @method static self setToken(string $token) Set the bot token * @method static string getToken() Get the bot token @@ -76,7 +65,6 @@ * @method static array getAdmins() Get the Discord admins * @method static ?\Discord\Discord discord() Retrieve the Discord instance * @method static \Laracord\Discord\Message message(string $content = '') Build a message for Discord - * * @method static \React\EventLoop\LoopInterface getLoop() Get the event loop */ class Laracord extends Facade From 93f46e87bd9ec7c2791d04987fad191f6babdbc2 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 07:59:11 -0600 Subject: [PATCH 048/146] =?UTF-8?q?=F0=9F=94=A7=20Add=20missing=20`make:pr?= =?UTF-8?q?ompt`=20command=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index f44e014..6ceb843 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -99,9 +99,10 @@ public function boot() Commands\KeyGenerateCommand::class, Commands\MakeCommand::class, Commands\MakeCommandMiddlewareCommand::class, - Commands\MakeSlashCommand::class, Commands\MakeMenuCommand::class, + Commands\MakeSlashCommand::class, Commands\ModelMakeCommand::class, + Commands\PromptMakeCommand::class, Commands\ServiceMakeCommand::class, Commands\TokenMakeCommand::class, ]); From 39034cf8d0834a491725e65b885bac757d616c48 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 08:15:42 -0600 Subject: [PATCH 049/146] =?UTF-8?q?=F0=9F=A9=B9=20Change=20to=20proper=20`?= =?UTF-8?q?Discord`=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/HasLaracord.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HasLaracord.php b/src/HasLaracord.php index e4fca01..8bcf6dd 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -2,7 +2,7 @@ namespace Laracord; -use Discord\DiscordCommandClient as Discord; +use Discord\Discord; use Laracord\Console\Commands\Command as ConsoleCommand; use Laracord\Discord\Message; From 39b90da2bcf5c9709c413beffc3a4f8a77658a26 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 08:16:42 -0600 Subject: [PATCH 050/146] =?UTF-8?q?=F0=9F=A9=B9=20Change=20to=20proper=20`?= =?UTF-8?q?Console`=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/HasLaracord.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HasLaracord.php b/src/HasLaracord.php index 8bcf6dd..a542ef2 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -3,7 +3,7 @@ namespace Laracord; use Discord\Discord; -use Laracord\Console\Commands\Command as ConsoleCommand; +use Laracord\Console\Console; use Laracord\Discord\Message; trait HasLaracord @@ -36,7 +36,7 @@ public function discord(): Discord /** * Retrieve the console instance. */ - public function console(): ConsoleCommand + public function console(): Console { return $this->bot()->console(); } From 8230fe4856ee375e6b864a5d7f235b6aacdc7749 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 08:17:17 -0600 Subject: [PATCH 051/146] =?UTF-8?q?=F0=9F=A9=B9=20Add=20missing=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/HasLaracord.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/HasLaracord.php b/src/HasLaracord.php index a542ef2..6157501 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -3,6 +3,7 @@ namespace Laracord; use Discord\Discord; +use Illuminate\Log\LogManager; use Laracord\Console\Console; use Laracord\Discord\Message; From b385e49f2e942824ca2de41c22d824e9edf63f2c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 09:49:43 -0600 Subject: [PATCH 052/146] =?UTF-8?q?=F0=9F=A9=B9=20Add=20missing=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasConsole.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Bot/Concerns/HasConsole.php b/src/Bot/Concerns/HasConsole.php index 95e0928..be6ea94 100644 --- a/src/Bot/Concerns/HasConsole.php +++ b/src/Bot/Concerns/HasConsole.php @@ -2,6 +2,8 @@ namespace Laracord\Bot\Concerns; +use Illuminate\Support\Arr; +use InvalidArgumentException; use Laracord\Console\Console; use Laracord\Console\Prompts\Prompt; use Laracord\Logging\ConsoleHandler; From 7d9dbe2a9231df1c57ee2fe4f4d06eb929c7b5a1 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 10:21:57 -0600 Subject: [PATCH 053/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20sharding=20sup?= =?UTF-8?q?port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasDiscord.php | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Bot/Concerns/HasDiscord.php b/src/Bot/Concerns/HasDiscord.php index 7e4fa4a..c3f3632 100644 --- a/src/Bot/Concerns/HasDiscord.php +++ b/src/Bot/Concerns/HasDiscord.php @@ -4,7 +4,6 @@ use Discord\Discord; use Discord\WebSockets\Intents; -use Exception; use Laracord\Discord\Message; trait HasDiscord @@ -92,7 +91,9 @@ public function getToken(): string $token = config('discord.token'); if (! $token) { - throw new Exception('You must provide a Discord bot token.'); + $this->logger->error('You must provide a Discord bot token.'); + + exit(1); } return $this->token = $token; @@ -121,6 +122,30 @@ public function setShard(int $id, int $count): self return $this; } + /** + * Determine if the bot is a shard. + */ + public function isShard(): bool + { + return filled($this->shardId) && filled($this->shardCount); + } + + /** + * Get the current shard ID. + */ + public function getShardId(): ?int + { + return $this->shardId; + } + + /** + * Get the shard count. + */ + public function getShardCount(): ?int + { + return $this->shardCount; + } + /** * Get the bot options. */ @@ -137,7 +162,7 @@ public function getOptions(): array 'loop' => $this->getLoop(), ]; - if ($this->shardId && $this->shardCount) { + if ($this->isShard()) { $options = [ ...$options, 'shardId' => $this->shardId, From b1acc695edb5b772fb96ac18ca705c1c8a816b15 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 10:22:13 -0600 Subject: [PATCH 054/146] =?UTF-8?q?=F0=9F=8E=A8=20Disable=20the=20HTTP=20s?= =?UTF-8?q?erver=20when=20sharding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/BootCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/BootCommand.php b/src/Console/Commands/BootCommand.php index 1efefbe..287aa0a 100644 --- a/src/Console/Commands/BootCommand.php +++ b/src/Console/Commands/BootCommand.php @@ -37,11 +37,13 @@ public function handle(Laracord $bot): void $bot->setToken($this->option('token')); } - if ($this->option('shard-id') && $this->option('shard-count')) { + if (filled($this->option('shard-id')) && filled($this->option('shard-count'))) { $bot->setShard( id: $this->option('shard-id'), count: $this->option('shard-count') ); + + $bot->disableHttpServer(); } $bot->boot(); From bdcb29044a3a0793fde78d1d009be954a1d0d99b Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 10:22:35 -0600 Subject: [PATCH 055/146] =?UTF-8?q?=F0=9F=92=84=20Show=20the=20shard=20ID?= =?UTF-8?q?=20on=20boot=20if=20sharded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Laracord.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Laracord.php b/src/Laracord.php index 5e05a78..b4d72a7 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -97,7 +97,13 @@ protected function handleBoot(): void $status = Str::replaceLast(', ', ', and ', $status); - $this->logger->info("Successfully booted {$this->getName()} with {$status}."); + $name = "{$this->getName()}"; + + if ($this->isShard()) { + $name .= " (Shard {$this->getShardId()})"; + } + + $this->logger->info("Successfully booted {$name} with {$status}."); $this ->showCommands() From 0053ed76543a84bc351aac38eea1da8c473bcfe0 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 10:23:11 -0600 Subject: [PATCH 056/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Imp?= =?UTF-8?q?rove=20HTTP=20server=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHttpServer.php | 78 +++++++++++++++++++++++++++++- src/Http/HttpServer.php | 38 +++++---------- 2 files changed, 87 insertions(+), 29 deletions(-) diff --git a/src/Bot/Concerns/HasHttpServer.php b/src/Bot/Concerns/HasHttpServer.php index d670f70..ff6b0f2 100644 --- a/src/Bot/Concerns/HasHttpServer.php +++ b/src/Bot/Concerns/HasHttpServer.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Support\Str; use Laracord\Http\HttpServer; trait HasHttpServer @@ -13,6 +14,16 @@ trait HasHttpServer */ protected ?HttpServer $httpServer = null; + /** + * The HTTP server address. + */ + protected ?string $httpAddress = null; + + /** + * Determine if the HTTP server is enabled. + */ + protected bool $httpEnabled = true; + /** * The HTTP routes. */ @@ -58,7 +69,7 @@ public function withMiddleware(?callable $callback = null): self */ protected function bootHttpServer(): self { - if ($this->httpServer) { + if ($this->httpServer || ! $this->isHttpEnabled()) { return $this; } @@ -68,7 +79,9 @@ protected function bootHttpServer(): self $this->app['router']->getRoutes()->refreshActionLookups(); }); - $this->httpServer = HttpServer::make($this)->boot(); + $this->httpServer = HttpServer::make($this) + ->setAddress($this->getHttpAddress()) + ->boot(); if ($this->httpServer->isBooted()) { $this->logger->info("HTTP server started on {$this->httpServer->getAddress()}."); @@ -78,6 +91,67 @@ protected function bootHttpServer(): self return $this; } + /** + * Disable the HTTP server. + */ + public function disableHttpServer(): self + { + $this->httpEnabled = false; + + return $this; + } + + /** + * Determine if the HTTP server is enabled. + */ + public function isHttpEnabled(): bool + { + return $this->httpEnabled && $this->getHttpAddress(); + } + + /** + * Set the HTTP server address. + */ + public function setHttpAddress(string $address): self + { + $this->httpAddress = $address; + + return $this; + } + + /** + * Get the HTTP server address. + */ + public function getHttpAddress(): ?string + { + $this->httpAddress ??= config('discord.http'); + + if (! $this->httpAddress) { + return null; + } + + if (Str::startsWith($this->httpAddress, ':')) { + $this->httpAddress = Str::start($this->httpAddress, '0.0.0.0'); + } + + $host = Str::before($this->httpAddress, ':'); + $port = Str::after($this->httpAddress, ':'); + + if (! filter_var($host, FILTER_VALIDATE_IP)) { + $this->logger->error('Invalid HTTP server address'); + + return null; + } + + if ($port > 65535 || $port < 1) { + $this->logger->error('Invalid HTTP server port'); + + return null; + } + + return $this->httpAddress; + } + /** * Get the HTTP server instance. */ diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index ad1961e..876a56e 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -2,7 +2,6 @@ namespace Laracord\Http; -use Exception; use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Http\Request; @@ -168,36 +167,21 @@ public function getSocket(): SocketServer } /** - * Retrieve the server address. + * Set the server address. */ - public function getAddress(): ?string + public function setAddress(string $address): self { - if ($this->address) { - return $this->address; - } - - $address = config('discord.http'); - - if (! $address) { - return null; - } - - if (Str::startsWith($address, ':')) { - $address = Str::start($address, '0.0.0.0'); - } + $this->address = $address; - $host = Str::before($address, ':'); - $port = Str::after($address, ':'); - - if (! filter_var($host, FILTER_VALIDATE_IP)) { - throw new Exception('Invalid HTTP server address'); - } - - if ($port > 65535 || $port < 1) { - throw new Exception('Invalid HTTP server port'); - } + return $this; + } - return $this->address = $address; + /** + * Retrieve the server address. + */ + public function getAddress(): ?string + { + return $this->address; } /** From 46bb0fad9c24b2d6fff5246ae4020821f79231ef Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 15:24:40 -0600 Subject: [PATCH 057/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20Windows=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Console.php | 4 +-- src/LaracordServiceProvider.php | 45 ++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/Console/Console.php b/src/Console/Console.php index d2a0470..5479478 100644 --- a/src/Console/Console.php +++ b/src/Console/Console.php @@ -201,11 +201,11 @@ public function hasColorSupport(): bool // Detect msysgit/mingw and assume this is a tty because detection // does not work correctly, see https://github.com/composer/composer/issues/9690 - if (is_resource($this->stdio) && ! @stream_isatty($this->stdio) && ! \in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { + if (is_resource($this->stdio) && ! @stream_isatty($this->stdio) && ! in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { return false; } - if (windows_os() && @sapi_windows_vt100_support($this->stdio)) { + if (is_resource($this->stdio) && windows_os() && @sapi_windows_vt100_support($this->stdio)) { return true; } diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 6ceb843..742878d 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -22,7 +22,10 @@ use React\EventLoop\LoopInterface; use React\Stream\CompositeStream; use React\Stream\ReadableResourceStream; +use React\Stream\ReadableStreamInterface; +use React\Stream\ThroughStream; use React\Stream\WritableResourceStream; +use React\Stream\WritableStreamInterface; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Finder\Finder; @@ -127,11 +130,11 @@ protected function registerConsole(): void $this->app->singleton(Console::class, function () { $loop = $this->app->make(LoopInterface::class); + $stdin = $this->createInputStream($loop); + $stdout = $this->createOutputStream($loop); + $console = new Console( - stdio: new CompositeStream( - new ReadableResourceStream(STDIN, $loop), - new WritableResourceStream(STDOUT, $loop), - ), + stdio: new CompositeStream($stdin, $stdout), laravel: $this->app, output: new ConsoleOutput, input: new StringInput(''), @@ -153,6 +156,40 @@ protected function registerConsole(): void }); } + /** + * Create an input stream that works on both Windows and Unix systems. + */ + protected function createInputStream(LoopInterface $loop): ReadableStreamInterface + { + if (! windows_os()) { + return new ReadableResourceStream(STDIN, $loop); + } + + $stream = new ThroughStream; + + $stream->end(); + + return $stream; + } + + /** + * Create an output stream. + */ + protected function createOutputStream(LoopInterface $loop): WritableStreamInterface + { + if (! windows_os()) { + return new WritableResourceStream(STDOUT, $loop); + } + + $stream = new ThroughStream; + + $stream->on('data', function ($data) { + fwrite(STDOUT, $data); + }); + + return $stream; + } + /** * Register the logger. */ From 82a1fb2f27336085bbebd93c8e4fe39d61483393 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 6 Feb 2025 15:26:45 -0600 Subject: [PATCH 058/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20docblock=20wor?= =?UTF-8?q?ding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 742878d..aa52aa3 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -157,7 +157,7 @@ protected function registerConsole(): void } /** - * Create an input stream that works on both Windows and Unix systems. + * Create an input stream. */ protected function createInputStream(LoopInterface $loop): ReadableStreamInterface { From 28836709217bfb4cdf31d7f6dc87a18ebac1bac9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 04:57:53 -0600 Subject: [PATCH 059/146] =?UTF-8?q?=F0=9F=94=92=20Show=20an=20alert=20when?= =?UTF-8?q?=20using=20`--token`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/BootCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/BootCommand.php b/src/Console/Commands/BootCommand.php index 287aa0a..e888160 100644 --- a/src/Console/Commands/BootCommand.php +++ b/src/Console/Commands/BootCommand.php @@ -12,7 +12,7 @@ class BootCommand extends Command * @var string */ protected $signature = 'bot:boot - {--token= : The Discord bot token} + {--token= : The Discord bot token (insecure)} {--shard-id= : The Discord bot shard ID} {--shard-count= : The Discord bot shard count} {--no-migrate : Boot without running database migrations}'; @@ -34,6 +34,8 @@ public function handle(Laracord $bot): void } if ($this->option('token')) { + $this->components->alert('Using --token is insecure and not recommended.'); + $bot->setToken($this->option('token')); } From 6a4d204ef34674e3e348ca3500143d0376921139 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 05:26:26 -0600 Subject: [PATCH 060/146] =?UTF-8?q?=F0=9F=8E=A8=20Make=20`registerInteract?= =?UTF-8?q?ions`=20public?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasInteractions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bot/Concerns/HasInteractions.php b/src/Bot/Concerns/HasInteractions.php index fc18ca5..4063aab 100644 --- a/src/Bot/Concerns/HasInteractions.php +++ b/src/Bot/Concerns/HasInteractions.php @@ -24,7 +24,7 @@ trait HasInteractions /** * Register the interaction routes. */ - protected function registerInteractions(string $name, array $routes = []): void + public function registerInteractions(string $name, array $routes = []): void { $routes = collect($routes) ->mapWithKeys(fn ($value, $route) => ["{$name}@{$route}" => $value]) From 8895940064197eec2ad213466c71ae6e21a74e1f Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 05:41:14 -0600 Subject: [PATCH 061/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20the=20command?= =?UTF-8?q?=20middleware=20stub=20docblock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/stubs/command-middleware.stub | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Console/Commands/stubs/command-middleware.stub b/src/Console/Commands/stubs/command-middleware.stub index 6c18aa0..bc124ef 100644 --- a/src/Console/Commands/stubs/command-middleware.stub +++ b/src/Console/Commands/stubs/command-middleware.stub @@ -11,8 +11,6 @@ class {{ class }} implements Middleware /** * Handle the command. * - * @param \Laracord\Commands\Middleware\Context $context - * @param \Closure $next * @return mixed */ public function handle(Context $context, Closure $next) From b518c25187ee0e3986e91f4a2e634c5cd8e4b9b9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 06:01:03 -0600 Subject: [PATCH 062/146] =?UTF-8?q?=F0=9F=8E=A8=20Add=20missing=20return?= =?UTF-8?q?=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/SlashCommand.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Commands/SlashCommand.php b/src/Commands/SlashCommand.php index c7e36cd..d834995 100644 --- a/src/Commands/SlashCommand.php +++ b/src/Commands/SlashCommand.php @@ -235,10 +235,8 @@ public function autocomplete(): array /** * Retrieve the command signature. - * - * @return string */ - public function getSignature() + public function getSignature(): string { return Str::start($this->getName(), '/'); } From c279db9566b18bdc8fe453c4d8e928b776dde4bb Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 06:04:19 -0600 Subject: [PATCH 063/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20call=20to=20`disco?= =?UTF-8?q?rd()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/SlashCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/SlashCommand.php b/src/Commands/SlashCommand.php index d834995..6d89952 100644 --- a/src/Commands/SlashCommand.php +++ b/src/Commands/SlashCommand.php @@ -64,7 +64,7 @@ public function create(): DiscordCommand ->filter() ->all(); - return new DiscordCommand($this->discord, $command); + return new DiscordCommand($this->discord(), $command); } /** From 5d102120de611061bd102f521f35c67da799cc01 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 06:52:03 -0600 Subject: [PATCH 064/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20application=20comm?= =?UTF-8?q?and=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasApplicationCommands.php | 80 ++++++++++----------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/src/Bot/Concerns/HasApplicationCommands.php b/src/Bot/Concerns/HasApplicationCommands.php index edf97cb..a3b1c89 100644 --- a/src/Bot/Concerns/HasApplicationCommands.php +++ b/src/Bot/Concerns/HasApplicationCommands.php @@ -41,38 +41,33 @@ protected function bootApplicationCommands(): self return $data; }; - $existing = cache()->get('laracord.application-commands', []); + $existing = []; - if (! $existing) { - $existing[] = $this->discord->application->commands->freshen(); + $existing[] = $this->discord->application->commands->freshen(); - foreach ($this->discord->guilds as $guild) { - $existing[] = $guild->commands->freshen(); - } - - $existing = all($existing)->then(fn ($commands) => collect($commands) - ->flatMap(fn ($command) => $command->toArray()) - ->map(fn ($command) => collect($command->getCreatableAttributes()) - ->merge([ - 'id' => $command->id, - 'guild_id' => $command->guild_id ?? null, - 'dm_permission' => $command->guild_id ? null : ($command->dm_permission ?? false), - 'default_permission' => $command->default_permission ?? true, - ]) - ->all() - ) - ->map(fn ($command) => array_merge($command, [ - 'options' => json_decode(json_encode($command['options'] ?? []), true), - ])) - ->filter(fn ($command) => ! blank($command)) - ->keyBy('name') - ); - - $existing = await($existing); - - cache()->forever('laracord.application-commands', $existing); + foreach ($this->discord->guilds as $guild) { + $existing[] = $guild->commands->freshen(); } + $existing = all($existing)->then(fn ($commands) => collect($commands) + ->flatMap(fn ($command) => $command->toArray()) + ->map(fn ($command) => collect($command->getCreatableAttributes()) + ->merge([ + 'id' => $command->id, + 'guild_id' => $command->guild_id ?? null, + 'dm_permission' => $command->guild_id ? null : ($command->dm_permission ?? false), + 'default_permission' => $command->default_permission ?? true, + ]) + ->all() + ) + ->map(fn ($command) => array_merge($command, [ + 'options' => json_decode(json_encode($command['options'] ?? []), true), + ])) + ->filter(fn ($command) => filled($command)) + ->keyBy('name') + ); + + $existing = await($existing); $existing = collect($existing); $registered = collect($this->slashCommands) @@ -90,7 +85,7 @@ protected function bootApplicationCommands(): self ->sortKeys() ->all(); - return [$command::class => [ + return [$command->getName() => [ 'state' => $command, 'attributes' => $attributes, ]]; @@ -196,11 +191,13 @@ protected function bootApplicationCommands(): self return; } - $subcommands = collect($command['state']->getRegisteredOptions()) + $command = $command['state']; + + $subcommands = collect($command->getRegisteredOptions()) ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND) ->map(fn (Option $subcommand) => [$name, $subcommand->name]); - $subcommandGroups = collect($command['state']->getRegisteredOptions()) + $subcommandGroups = collect($command->getRegisteredOptions()) ->filter(fn (Option $option) => $option->type === Option::SUB_COMMAND_GROUP) ->flatMap(fn (Option $group) => collect($group->options) ->filter(fn (Option $subcommand) => $subcommand->type === Option::SUB_COMMAND) @@ -210,28 +207,25 @@ protected function bootApplicationCommands(): self $subcommands = $subcommands->merge($subcommandGroups); if ($subcommands->isNotEmpty()) { - $subcommands->each(function ($names) use ($command) { + $subcommands->each(fn ($names) => $this->discord->listenCommand( $names, - fn ($interaction) => rescue(fn () => $command['state']->maybeHandle($interaction)), - fn ($interaction) => rescue(fn () => $command['state']->maybeHandleAutocomplete($interaction)) - ); - }); + fn ($interaction) => rescue(fn () => $command->maybeHandle($interaction)), + fn ($interaction) => rescue(fn () => $command->maybeHandleAutocomplete($interaction)) + ) + ); return; } $this->discord->listenCommand( $name, - fn ($interaction) => rescue(fn () => $command['state']->maybeHandle($interaction)), - fn ($interaction) => rescue(fn () => $command['state']->maybeHandleAutocomplete($interaction)) + fn ($interaction) => rescue(fn () => $command->maybeHandle($interaction)), + fn ($interaction) => rescue(fn () => $command->maybeHandleAutocomplete($interaction)) ); - }); - $this->slashCommands = [ - ...$this->slashCommands, - ...$registered->pluck('state')->reject(fn ($command) => $command instanceof ContextMenu)->all(), - ]; + $this->slashCommands[$command::class] = $command; + }); return $this; } From 54f3d10f2b09766321d0f216c74f93d535484dc8 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 06:53:53 -0600 Subject: [PATCH 065/146] =?UTF-8?q?=F0=9F=9A=A8=20Run=20Pint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasApplicationCommands.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Bot/Concerns/HasApplicationCommands.php b/src/Bot/Concerns/HasApplicationCommands.php index a3b1c89..0d806e7 100644 --- a/src/Bot/Concerns/HasApplicationCommands.php +++ b/src/Bot/Concerns/HasApplicationCommands.php @@ -207,12 +207,11 @@ protected function bootApplicationCommands(): self $subcommands = $subcommands->merge($subcommandGroups); if ($subcommands->isNotEmpty()) { - $subcommands->each(fn ($names) => - $this->discord->listenCommand( - $names, - fn ($interaction) => rescue(fn () => $command->maybeHandle($interaction)), - fn ($interaction) => rescue(fn () => $command->maybeHandleAutocomplete($interaction)) - ) + $subcommands->each(fn ($names) => $this->discord->listenCommand( + $names, + fn ($interaction) => rescue(fn () => $command->maybeHandle($interaction)), + fn ($interaction) => rescue(fn () => $command->maybeHandleAutocomplete($interaction)) + ) ); return; @@ -235,8 +234,6 @@ protected function bootApplicationCommands(): self */ protected function registerApplicationCommand(ApplicationCommand $command): void { - cache()->forget('laracord.application-commands'); - if ($command->getGuild()) { $guild = $this->discord->guilds->get('id', $command->getGuild()); @@ -259,8 +256,6 @@ protected function registerApplicationCommand(ApplicationCommand $command): void */ protected function unregisterApplicationCommand(string $id, ?string $guildId = null): void { - cache()->forget('laracord.application-commands'); - if ($guildId) { $guild = $this->discord->guilds->get('id', $guildId); From 5f46e4e2675dabf1de130d8f694d3372aa16fff4 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 06:54:30 -0600 Subject: [PATCH 066/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20context=20menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/ContextMenu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/ContextMenu.php b/src/Commands/ContextMenu.php index 29721a4..84c8e1a 100644 --- a/src/Commands/ContextMenu.php +++ b/src/Commands/ContextMenu.php @@ -30,7 +30,7 @@ public function create(): DiscordCommand 'nsfw' => $this->isNsfw(), ])->reject(fn ($value) => blank($value)); - return new DiscordCommand($this->discord, $menu->all()); + return new DiscordCommand($this->discord(), $menu->all()); } /** From a9777cf088cd430f7dc0eae61fe3b21e03772075 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 06:58:15 -0600 Subject: [PATCH 067/146] =?UTF-8?q?=F0=9F=92=84=20Add=20slash=20commands?= =?UTF-8?q?=20to=20the=20commands=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasConsole.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Bot/Concerns/HasConsole.php b/src/Bot/Concerns/HasConsole.php index be6ea94..7c5dc56 100644 --- a/src/Bot/Concerns/HasConsole.php +++ b/src/Bot/Concerns/HasConsole.php @@ -88,9 +88,14 @@ public function showCommands(): self return $this; } + $commands = [ + ...$this->commands, + ...$this->slashCommands, + ]; + $this->console->table( ['Command', 'Description'], - collect($this->commands)->map(fn ($command) => [ + collect($commands)->map(fn ($command) => [ $command->getSignature(), $command->getDescription(), ])->all() From 011b0eca9a29339007c354a733e2596fc993cb77 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 08:36:36 -0600 Subject: [PATCH 068/146] =?UTF-8?q?=F0=9F=94=A7=20Resolve=20the=20storage?= =?UTF-8?q?=20path=20with=20`laracord=5Fpath()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index aa52aa3..bd6a2f3 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -60,6 +60,8 @@ abstract public function bot(Laracord $bot): Laracord; */ public function register() { + $this->app->useStoragePath(laracord_path('storage', basePath: false)); + $this->mergeConfigs(); $this->createDirectories(); From 89aee7f1255cceb6c64afd35289f67fdfa24b4e2 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 08:44:38 -0600 Subject: [PATCH 069/146] =?UTF-8?q?=F0=9F=94=A7=20Improve=20storage=20path?= =?UTF-8?q?=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/filesystems.php | 2 +- src/LaracordServiceProvider.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/filesystems.php b/config/filesystems.php index 082153f..8579a88 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -32,7 +32,7 @@ 'local' => [ 'driver' => 'local', - 'root' => env('STORAGE_PATH', laracord_path('storage', basePath: false)), + 'root' => storage_path(), 'throw' => false, ], diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index bd6a2f3..6244dfe 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -60,7 +60,9 @@ abstract public function bot(Laracord $bot): Laracord; */ public function register() { - $this->app->useStoragePath(laracord_path('storage', basePath: false)); + $storage = env('STORAGE_PATH', laracord_path('storage', basePath: false)); + + $this->app->useStoragePath($storage); $this->mergeConfigs(); $this->createDirectories(); From 39a82bbd6d4d41e28d444ce0a2c3ced2e2ee2f27 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 08:59:50 -0600 Subject: [PATCH 070/146] =?UTF-8?q?=E2=9C=A8=20Add=20hook=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHooks.php | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Bot/Concerns/HasHooks.php b/src/Bot/Concerns/HasHooks.php index a4026c3..314f2f0 100644 --- a/src/Bot/Concerns/HasHooks.php +++ b/src/Bot/Concerns/HasHooks.php @@ -5,14 +5,31 @@ trait HasHooks { /** - * Attempt to call the hook. + * The registered hooks. */ - protected function callHook(string $hook): void + protected array $hooks = []; + + /** + * Register a hook callback. + */ + public function registerHook(string $hook, callable $callback): self { - if (! method_exists($this, $hook)) { - return; + if (! isset($this->hooks[$hook])) { + $this->hooks[$hook] = []; } - $this->app->call([$this, $hook]); + $this->hooks[$hook][] = $callback; + + return $this; + } + + /** + * Call all registered callbacks for a hook. + */ + protected function callHook(string $hook): void + { + foreach ($this->hooks[$hook] ?? [] as $callback) { + $this->app->call($callback); + } } } From de47f9ce9d73db782fa1411149362c1bfe4909cf Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 10:43:04 -0600 Subject: [PATCH 071/146] =?UTF-8?q?=F0=9F=92=84=20Improve=20the=20token=20?= =?UTF-8?q?alert=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/BootCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/Commands/BootCommand.php b/src/Console/Commands/BootCommand.php index e888160..3bf2bdf 100644 --- a/src/Console/Commands/BootCommand.php +++ b/src/Console/Commands/BootCommand.php @@ -34,7 +34,7 @@ public function handle(Laracord $bot): void } if ($this->option('token')) { - $this->components->alert('Using --token is insecure and not recommended.'); + $this->components->alert('Using --token is insecure. Consider using --env instead.'); $bot->setToken($this->option('token')); } From 1e24a8a773a9b1ad670328d09b96a970c69119fd Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 12:27:01 -0600 Subject: [PATCH 072/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20user=20model?= =?UTF-8?q?=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasUserModel.php | 51 +++++++++++++++++++ src/Commands/AbstractCommand.php | 40 +++------------ .../Commands/Concerns/ResolvesUser.php | 22 ++------ src/Laracord.php | 3 +- 4 files changed, 62 insertions(+), 54 deletions(-) create mode 100644 src/Bot/Concerns/HasUserModel.php diff --git a/src/Bot/Concerns/HasUserModel.php b/src/Bot/Concerns/HasUserModel.php new file mode 100644 index 0000000..e9d407b --- /dev/null +++ b/src/Bot/Concerns/HasUserModel.php @@ -0,0 +1,51 @@ +userModel = $model; + + return $this; + } + + /** + * Get the user model class. + */ + public function getUserModel(): ?string + { + if ($this->userModel) { + return $this->userModel; + } + + $model = Str::start($this->app->getNamespace(), '\\').'Models\\User'; + + if (! class_exists($model)) { + return null; + } + + if (! is_subclass_of($model, Model::class)) { + throw new Exception('The user model must extend '.Model::class); + } + + return $this->userModel = $model; + } +} diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index 58ea1cc..d0b1a21 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -5,7 +5,7 @@ use Discord\Parts\Guild\Guild; use Discord\Parts\Interactions\Command\Command; use Discord\Parts\User\User; -use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Model; use Laracord\Concerns\HasHandler; use Laracord\Discord\Concerns\HasModal; use Laracord\HasLaracord; @@ -131,7 +131,11 @@ public function isAdmin(User|string $user): bool return in_array($user->id, $this->bot->getAdmins()); } - return $this->getUser($user)->is_admin; + if (! $this->bot->getUserModel()) { + return false; + } + + return $this->bot->getUserModel()::where(['discord_id' => $user->id])->first()?->is_admin ?? false; } /** @@ -142,38 +146,6 @@ public function canDirectMessage(): bool return $this->directMessage; } - /** - * Resolve a Discord user. - */ - public function resolveUser(string $username): ?User - { - return ! empty($username) ? $this->discord()->users->filter(function ($user) use ($username) { - $username = str_replace(['<', '@', '>'], '', strtolower($username)); - - return ($user->username === $username || $user->id === $username) && ! $user->bot; - })->first() : null; - } - - /** - * Get the command user. - * - * @param \Discord\Parts\User\User $user - * @return \App\Models\User|null - */ - public function getUser($user) - { - $model = Str::start(app()->getNamespace(), '\\').'Models\\User'; - - if (! class_exists($model)) { - throw new Exception('The user model could not be found.'); - } - - return $model::firstOrCreate(['discord_id' => $user->id], [ - 'discord_id' => $user->id, - 'username' => $user->username, - ]) ?? null; - } - /** * Retrieve the command name. */ diff --git a/src/Console/Commands/Concerns/ResolvesUser.php b/src/Console/Commands/Concerns/ResolvesUser.php index 2ecbfdf..ffe2325 100644 --- a/src/Console/Commands/Concerns/ResolvesUser.php +++ b/src/Console/Commands/Concerns/ResolvesUser.php @@ -2,9 +2,7 @@ namespace Laracord\Console\Commands\Concerns; -use Exception; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Str; use Laracord\Facades\Laracord; trait ResolvesUser @@ -23,7 +21,7 @@ protected function resolveUser(?string $user = null) } if (! is_numeric($user)) { - $model = $this->getUserModel()::where('username', $user)->first(); + $model = Laracord::getUserModel()::where('username', $user)->first(); if (! $model) { $this->components->error("The user {$user} does not exist."); @@ -32,7 +30,7 @@ protected function resolveUser(?string $user = null) } } - $model = $model ?? $this->getUserModel()::where('discord_id', $user)->first(); + $model = $model ?? Laracord::getUserModel()::where('discord_id', $user)->first(); if (! $model) { $token = Laracord::getToken(); @@ -49,25 +47,11 @@ protected function resolveUser(?string $user = null) $user = $request->json(); - $model = $this->getUserModel()::updateOrCreate(['discord_id' => $user['id']], [ + $model = Laracord::getUserModel()::updateOrCreate(['discord_id' => $user['id']], [ 'username' => $user['username'], ]); } return $model; } - - /** - * Get the user model class. - */ - protected function getUserModel(): string - { - $model = Str::start(app()->getNamespace(), '\\').'Models\\User'; - - if (! class_exists($model)) { - throw new Exception('The user model could not be found.'); - } - - return $model; - } } diff --git a/src/Laracord.php b/src/Laracord.php index b4d72a7..98f4707 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -29,7 +29,8 @@ class Laracord Concerns\HasLoop, Concerns\HasPlugins, Concerns\HasServices, - Concerns\HasSlashCommands; + Concerns\HasSlashCommands, + Concerns\HasUserModel; /** * The boot state. From 62b33b49e28d26285e10396f883dbcc0acbc1f59 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 12:27:54 -0600 Subject: [PATCH 073/146] =?UTF-8?q?=F0=9F=9A=A8=20Run=20Pint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/AbstractCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index d0b1a21..ca42e01 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -5,7 +5,6 @@ use Discord\Parts\Guild\Guild; use Discord\Parts\Interactions\Command\Command; use Discord\Parts\User\User; -use Illuminate\Database\Eloquent\Model; use Laracord\Concerns\HasHandler; use Laracord\Discord\Concerns\HasModal; use Laracord\HasLaracord; From e0e5345b1c2b2ac4301d01233c37ddfd0a26f26a Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 23:57:16 -0600 Subject: [PATCH 074/146] =?UTF-8?q?=E2=9C=A8=20Implement=20an=20initial=20?= =?UTF-8?q?non-blocking=20file=20logging=20channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 10 +++- src/Logging/LoggerHandler.php | 101 ++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/Logging/LoggerHandler.php diff --git a/config/logging.php b/config/logging.php index c698631..c786f8a 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,7 +54,7 @@ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', 'single')), + 'channels' => explode(',', env('LOG_STACK', 'react')), 'ignore_exceptions' => false, ], @@ -65,6 +65,14 @@ 'replace_placeholders' => true, ], + 'react' => [ + 'driver' => 'monolog', + 'handler' => Laracord\Logging\LoggerHandler::class, + 'with' => [ + 'path' => storage_path('logs/laracord.log'), + ], + ], + 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laracord.log'), diff --git a/src/Logging/LoggerHandler.php b/src/Logging/LoggerHandler.php new file mode 100644 index 0000000..f5dcc8e --- /dev/null +++ b/src/Logging/LoggerHandler.php @@ -0,0 +1,101 @@ +initializeStream(); + } + + /** + * Initialize the stream. + */ + protected function initializeStream(): void + { + $path = dirname($this->path); + + File::ensureDirectoryExists($path); + + $resource = fopen($this->path, 'a'); + + if ($resource === false) { + throw new RuntimeException("Could not open log file: {$this->path}"); + } + + $this->stream = new WritableResourceStream( + $resource, + Laracord::getLoop(), + ['write_buffer_size' => 64 * 1024] + ); + } + + /** + * Rotate the log file. + */ + protected function rotate(): void + { + if (! file_exists($this->path) || filesize($this->path) < $this->maxSize) { + return; + } + + $this->stream->end(); + + for ($i = $this->maxFiles - 1; $i >= 0; $i--) { + $oldFile = $i === 0 ? $this->path : "{$this->path}.{$i}"; + $newFile = "{$this->path}.".($i + 1); + + if (file_exists($oldFile)) { + if ($i === $this->maxFiles - 1) { + unlink($oldFile); + } else { + rename($oldFile, $newFile); + } + } + } + + $this->initializeStream(); + } + + /** + * {@inheritdoc} + */ + protected function write(LogRecord $record): void + { + $this->rotate(); + + $this->stream->write($record->formatted); + } + + /** + * Close the stream. + */ + public function close(): void + { + $this->stream->end(); + } +} From 04ea45786bb9f896644ba360f6c5a54b7d1afb15 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 7 Feb 2025 23:57:31 -0600 Subject: [PATCH 075/146] =?UTF-8?q?=E2=AC=86=20Bump=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 52d808f..4bc2d23 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "react/http": "^1.9", "react/promise": "^3.0", "symfony/psr-http-message-bridge": "^7.0", - "team-reflex/discord-php": "^10.1" + "team-reflex/discord-php": "^10.3" }, "require-dev": { "laravel/pint": "^1.15" From 2a2dc36ef2e5b4979e7c7cea731beff27c3506c6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 00:04:39 -0600 Subject: [PATCH 076/146] =?UTF-8?q?=F0=9F=94=A7=20Windows=20sucks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/logging.php b/config/logging.php index c786f8a..12db687 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,7 +54,7 @@ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', 'react')), + 'channels' => explode(',', env('LOG_STACK', windows_os() ? 'single' : 'react')), 'ignore_exceptions' => false, ], From 6f70695436da4b96545b92e241bf3a73036e3303 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 00:29:33 -0600 Subject: [PATCH 077/146] =?UTF-8?q?=E2=9E=95=20Add=20`illuminate/mail`=20t?= =?UTF-8?q?o=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 4bc2d23..38aac81 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "illuminate/hashing": "^11.0", "illuminate/http": "^11.0", "illuminate/log": "^11.0", + "illuminate/mail": "^11.0", "illuminate/queue": "^11.0", "illuminate/routing": "^11.0", "illuminate/session": "^11.0", From aed2edf0eaf519eff2db3bf4d15284896f43bebf Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 00:30:26 -0600 Subject: [PATCH 078/146] =?UTF-8?q?=F0=9F=94=A7=20Register=20the=20`MailSe?= =?UTF-8?q?rviceProvider`=20(Fixes=20#53)=20=F0=9F=94=A7=20Create=20a=20de?= =?UTF-8?q?fault=20`mail.php`=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/mail.php | 135 ++++++++++++++++++++++++++++++++ src/LaracordServiceProvider.php | 1 + 2 files changed, 136 insertions(+) create mode 100644 config/mail.php diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..7ed4812 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,135 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => env('MAIL_MARKDOWN_THEME', 'default'), + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 6244dfe..79d1476 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -47,6 +47,7 @@ abstract class LaracordServiceProvider extends AggregateServiceProvider \Illuminate\View\ViewServiceProvider::class, \Illuminate\Cookie\CookieServiceProvider::class, \Illuminate\Session\SessionServiceProvider::class, + \Illuminate\Mail\MailServiceProvider::class, \Laracord\Http\Providers\RouteServiceProvider::class, \Intonate\TinkerZero\TinkerZeroServiceProvider::class, ]; From 15b17f836d4d48c6c547521ce41d40b610a1691a Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 00:52:57 -0600 Subject: [PATCH 079/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Imp?= =?UTF-8?q?rove=20logging=20on=20Windows=20=F0=9F=94=87=20Only=20write=20i?= =?UTF-8?q?nfo=20level=20and=20above=20to=20file=20logging=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 21 ++++--- src/Logging/LoggerHandler.php | 113 ++++++++++++++++++++++++++++++---- 2 files changed, 111 insertions(+), 23 deletions(-) diff --git a/config/logging.php b/config/logging.php index 12db687..aba8e60 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,28 +54,29 @@ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', windows_os() ? 'single' : 'react')), + 'channels' => explode(',', env('LOG_STACK', 'laracord')), 'ignore_exceptions' => false, ], - 'single' => [ - 'driver' => 'single', - 'path' => storage_path('logs/laracord.log'), - 'level' => env('LOG_LEVEL', 'debug'), - 'replace_placeholders' => true, - ], - - 'react' => [ + 'laracord' => [ 'driver' => 'monolog', 'handler' => Laracord\Logging\LoggerHandler::class, 'with' => [ 'path' => storage_path('logs/laracord.log'), + 'level' => env('LOG_LEVEL', 'info'), ], ], + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + 'daily' => [ 'driver' => 'daily', - 'path' => storage_path('logs/laracord.log'), + 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'days' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true, diff --git a/src/Logging/LoggerHandler.php b/src/Logging/LoggerHandler.php index f5dcc8e..bb0119b 100644 --- a/src/Logging/LoggerHandler.php +++ b/src/Logging/LoggerHandler.php @@ -7,6 +7,7 @@ use Monolog\Handler\AbstractProcessingHandler; use Monolog\Level; use Monolog\LogRecord; +use React\EventLoop\TimerInterface; use React\Stream\WritableResourceStream; use RuntimeException; @@ -15,7 +16,22 @@ class LoggerHandler extends AbstractProcessingHandler /** * The stream instance. */ - protected WritableResourceStream $stream; + protected ?WritableResourceStream $stream = null; + + /** + * The file resource. (Windows only) + */ + protected $handle = null; + + /** + * The buffer of messages to write (Windows only). + */ + protected array $buffer = []; + + /** + * The timer for flushing the buffer (Windows only). + */ + protected ?TimerInterface $flushTimer = null; /** * Create a new logger handler instance. @@ -24,6 +40,7 @@ public function __construct( protected string $path, protected int $maxSize = 10485760, protected int $maxFiles = 5, + protected float $flushInterval = 1, mixed $level = Level::Debug, bool $bubble = true, ) { @@ -41,6 +58,32 @@ protected function initializeStream(): void File::ensureDirectoryExists($path); + windows_os() + ? $this->initializeWindowsStream() + : $this->initializeUnixStream(); + } + + /** + * Initialize the stream for Windows systems. + */ + protected function initializeWindowsStream(): void + { + $this->handle = fopen($this->path, 'a'); + + if ($this->handle === false) { + throw new RuntimeException("Could not open log file: {$this->path}"); + } + + stream_set_blocking($this->handle, false); + + $this->flushTimer = Laracord::getLoop()->addPeriodicTimer($this->flushInterval, fn () => $this->flush()); + } + + /** + * Initialize the stream for Unix-like systems. + */ + protected function initializeUnixStream(): void + { $resource = fopen($this->path, 'a'); if ($resource === false) { @@ -54,6 +97,34 @@ protected function initializeStream(): void ); } + /** + * Flush the buffer to disk (Windows only). + */ + protected function flush(): void + { + if (empty($this->buffer)) { + return; + } + + $this->rotate(); + + if (! flock($this->handle, LOCK_EX | LOCK_NB)) { + return; + } + + try { + foreach ($this->buffer as $message) { + fwrite($this->handle, $message); + } + + fflush($this->handle); + } finally { + flock($this->handle, LOCK_UN); + } + + $this->buffer = []; + } + /** * Rotate the log file. */ @@ -63,19 +134,21 @@ protected function rotate(): void return; } - $this->stream->end(); + windows_os() + ? fclose($this->handle) + : $this->stream->end(); for ($i = $this->maxFiles - 1; $i >= 0; $i--) { - $oldFile = $i === 0 ? $this->path : "{$this->path}.{$i}"; - $newFile = "{$this->path}.".($i + 1); - - if (file_exists($oldFile)) { - if ($i === $this->maxFiles - 1) { - unlink($oldFile); - } else { - rename($oldFile, $newFile); - } + $existing = $i === 0 ? $this->path : "{$this->path}.{$i}"; + $new = "{$this->path}.".($i + 1); + + if (! file_exists($existing)) { + continue; } + + $i === $this->maxFiles - 1 + ? unlink($existing) + : rename($existing, $new); } $this->initializeStream(); @@ -88,7 +161,9 @@ protected function write(LogRecord $record): void { $this->rotate(); - $this->stream->write($record->formatted); + windows_os() + ? $this->buffer[] = $record->formatted + : $this->stream->write($record->formatted); } /** @@ -96,6 +171,18 @@ protected function write(LogRecord $record): void */ public function close(): void { - $this->stream->end(); + if (! windows_os()) { + $this->stream->end(); + + return; + } + + if ($this->flushTimer) { + Laracord::getLoop()->cancelTimer($this->flushTimer); + } + + $this->flush(); + + fclose($this->handle); } } From 260a40c6121120b137a1abed85b419ca116b7850 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 00:54:31 -0600 Subject: [PATCH 080/146] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20`LoggerHandler`?= =?UTF-8?q?=20to=20`LoggingHandler`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 2 +- src/Logging/{LoggerHandler.php => LoggingHandler.php} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Logging/{LoggerHandler.php => LoggingHandler.php} (98%) diff --git a/config/logging.php b/config/logging.php index aba8e60..1c94030 100644 --- a/config/logging.php +++ b/config/logging.php @@ -60,7 +60,7 @@ 'laracord' => [ 'driver' => 'monolog', - 'handler' => Laracord\Logging\LoggerHandler::class, + 'handler' => Laracord\Logging\LoggingHandler::class, 'with' => [ 'path' => storage_path('logs/laracord.log'), 'level' => env('LOG_LEVEL', 'info'), diff --git a/src/Logging/LoggerHandler.php b/src/Logging/LoggingHandler.php similarity index 98% rename from src/Logging/LoggerHandler.php rename to src/Logging/LoggingHandler.php index bb0119b..f72f023 100644 --- a/src/Logging/LoggerHandler.php +++ b/src/Logging/LoggingHandler.php @@ -11,7 +11,7 @@ use React\Stream\WritableResourceStream; use RuntimeException; -class LoggerHandler extends AbstractProcessingHandler +class LoggingHandler extends AbstractProcessingHandler { /** * The stream instance. From 38e070973acb5b9a1820f0b30237529cf8ed4419 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 01:11:40 -0600 Subject: [PATCH 081/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20support=20for=20overriding=20the=20denied=20application=20co?= =?UTF-8?q?mmand=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/ApplicationCommand.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Commands/ApplicationCommand.php b/src/Commands/ApplicationCommand.php index fd2b2b7..d07076d 100644 --- a/src/Commands/ApplicationCommand.php +++ b/src/Commands/ApplicationCommand.php @@ -2,11 +2,17 @@ namespace Laracord\Commands; +use Closure; use Discord\Parts\Interactions\Interaction; use Discord\Parts\Permissions\RolePermission; abstract class ApplicationCommand extends AbstractCommand { + /** + * The denied handler callback. + */ + protected static ?Closure $deniedHandler = null; + /** * The permissions required to use the command. * @@ -43,11 +49,25 @@ public function isNsfw(): bool return $this->nsfw; } + /** + * Set a handler for denied commands. + */ + public static function deniedHandler(Closure $handler): void + { + static::$deniedHandler = $handler; + } + /** * Handle the denied command. */ public function handleDenied(Interaction $interaction): void { + if (static::$deniedHandler) { + call_user_func(static::$deniedHandler, $interaction); + + return; + } + $this ->message('You do not have permission to use this command.') ->title('Permission Denied') From 59c9d5ab9d7a60219fc745be80ecaa1c4ceff4eb Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 01:12:04 -0600 Subject: [PATCH 082/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20support=20for=20overriding=20the=20`!help`=20command=20strin?= =?UTF-8?q?gs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/HelpCommand.php | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Commands/HelpCommand.php b/src/Commands/HelpCommand.php index 23a5080..28d0a6b 100644 --- a/src/Commands/HelpCommand.php +++ b/src/Commands/HelpCommand.php @@ -28,14 +28,30 @@ class HelpCommand extends Command protected $hidden = true; /** - * The response title. + * The help title. */ - protected string $title = 'Command Help'; + protected static string $title = 'Command Help'; /** - * The response message. + * The help message content. */ - protected string $message = 'Here is a list of all available commands.'; + protected static string $message = 'Here is a list of all available commands.'; + + /** + * Set the help title. + */ + public static function setTitle(string $title): void + { + static::$title = $title; + } + + /** + * Set the help message content. + */ + public static function setMessage(string $message): void + { + static::$message = $message; + } /** * Handle the command. @@ -62,8 +78,8 @@ public function handle(Message $message, array $args): void } $this - ->message($this->message) - ->title($this->title) + ->message(static::$message) + ->title(static::$title) ->fields($fields) ->reply($message); } From 839f098d629eed91dc0e8f41354248b07e1e75e8 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 03:03:05 -0600 Subject: [PATCH 083/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Cre?= =?UTF-8?q?ate=20a=20`Hook`=20enum=20for=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHooks.php | 16 ++++++++++++++-- src/Hook.php | 16 ++++++++++++++++ src/Laracord.php | 4 ++-- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/Hook.php diff --git a/src/Bot/Concerns/HasHooks.php b/src/Bot/Concerns/HasHooks.php index 314f2f0..e5409f0 100644 --- a/src/Bot/Concerns/HasHooks.php +++ b/src/Bot/Concerns/HasHooks.php @@ -2,18 +2,26 @@ namespace Laracord\Bot\Concerns; +use Laracord\Hook; + trait HasHooks { /** * The registered hooks. + * + * @var array> */ protected array $hooks = []; /** * Register a hook callback. */ - public function registerHook(string $hook, callable $callback): self + public function registerHook(Hook|string $hook, callable $callback): self { + $hook = $hook instanceof Hook + ? $hook->value + : $hook; + if (! isset($this->hooks[$hook])) { $this->hooks[$hook] = []; } @@ -26,8 +34,12 @@ public function registerHook(string $hook, callable $callback): self /** * Call all registered callbacks for a hook. */ - protected function callHook(string $hook): void + protected function callHook(Hook|string $hook): void { + $hook = $hook instanceof Hook + ? $hook->value + : $hook; + foreach ($this->hooks[$hook] ?? [] as $callback) { $this->app->call($callback); } diff --git a/src/Hook.php b/src/Hook.php new file mode 100644 index 0000000..53047be --- /dev/null +++ b/src/Hook.php @@ -0,0 +1,16 @@ +registerDiscord(); - $this->callHook('beforeBoot'); + $this->callHook(Hook::BEFORE_BOOT); $this->discord->on('init', function () { $this @@ -88,7 +88,7 @@ protected function handleBoot(): void ->bootHttpServer() ->handleInteractions(); - $this->callHook('afterBoot'); + $this->callHook(Hook::AFTER_BOOT); $this->getLoop()->addTimer(1, function () { $status = $this From b0e76a14bd55c912e31bcc2cd90073e9b43b0563 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 03:03:28 -0600 Subject: [PATCH 084/146] =?UTF-8?q?=F0=9F=92=84=20Do=20not=20render=20the?= =?UTF-8?q?=20console=20prompt=20on=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Console.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Console/Console.php b/src/Console/Console.php index 5479478..b64a9b6 100644 --- a/src/Console/Console.php +++ b/src/Console/Console.php @@ -182,6 +182,10 @@ public function getCommands(): array */ public function showPrompt(): void { + if (windows_os()) { + return; + } + $this->output->write($this->prompt); } From ed16620a3d1160c69e09b1e95aac6dc8ce856cd1 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 03:05:07 -0600 Subject: [PATCH 085/146] =?UTF-8?q?=F0=9F=9A=9A=20Move=20the=20`Hook`=20en?= =?UTF-8?q?um=20to=20the=20`Bot`=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHooks.php | 2 +- src/{ => Bot}/Hook.php | 2 +- src/Laracord.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) rename src/{ => Bot}/Hook.php (89%) diff --git a/src/Bot/Concerns/HasHooks.php b/src/Bot/Concerns/HasHooks.php index e5409f0..fa7650c 100644 --- a/src/Bot/Concerns/HasHooks.php +++ b/src/Bot/Concerns/HasHooks.php @@ -2,7 +2,7 @@ namespace Laracord\Bot\Concerns; -use Laracord\Hook; +use Laracord\Bot\Hook; trait HasHooks { diff --git a/src/Hook.php b/src/Bot/Hook.php similarity index 89% rename from src/Hook.php rename to src/Bot/Hook.php index 53047be..1cc2374 100644 --- a/src/Hook.php +++ b/src/Bot/Hook.php @@ -1,6 +1,6 @@ Date: Sat, 8 Feb 2025 03:29:45 -0600 Subject: [PATCH 086/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Spr?= =?UTF-8?q?inkle=20some=20potentially=20useful=20hooks=20throughout=20the?= =?UTF-8?q?=20framework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasApplicationCommands.php | 6 ++- src/Bot/Concerns/HasCommands.php | 3 ++ src/Bot/Concerns/HasEvents.php | 3 ++ src/Bot/Concerns/HasHttpServer.php | 3 ++ src/Bot/Concerns/HasServices.php | 3 ++ src/Bot/Hook.php | 54 ++++++++++++++++++++- src/Http/HttpServer.php | 3 ++ src/Laracord.php | 19 ++++++-- 8 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/Bot/Concerns/HasApplicationCommands.php b/src/Bot/Concerns/HasApplicationCommands.php index 0d806e7..b7e8073 100644 --- a/src/Bot/Concerns/HasApplicationCommands.php +++ b/src/Bot/Concerns/HasApplicationCommands.php @@ -4,6 +4,7 @@ use Discord\Parts\Interactions\Command\Option; use Illuminate\Support\Arr; +use Laracord\Bot\Hook; use Laracord\Commands\ApplicationCommand; use Laracord\Commands\ContextMenu; @@ -211,8 +212,7 @@ protected function bootApplicationCommands(): self $names, fn ($interaction) => rescue(fn () => $command->maybeHandle($interaction)), fn ($interaction) => rescue(fn () => $command->maybeHandleAutocomplete($interaction)) - ) - ); + )); return; } @@ -226,6 +226,8 @@ protected function bootApplicationCommands(): self $this->slashCommands[$command::class] = $command; }); + $this->callHook(Hook::AFTER_APPLICATION_COMMANDS_REGISTERED); + return $this; } diff --git a/src/Bot/Concerns/HasCommands.php b/src/Bot/Concerns/HasCommands.php index fbe7ca0..e6923d8 100644 --- a/src/Bot/Concerns/HasCommands.php +++ b/src/Bot/Concerns/HasCommands.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use InvalidArgumentException; +use Laracord\Bot\Hook; use Laracord\Commands\Command; trait HasCommands @@ -61,6 +62,8 @@ protected function bootCommands(): self { $this->handleCommands(); + $this->callHook(Hook::AFTER_COMMANDS_REGISTERED); + return $this; } diff --git a/src/Bot/Concerns/HasEvents.php b/src/Bot/Concerns/HasEvents.php index 8836033..10ddc4b 100644 --- a/src/Bot/Concerns/HasEvents.php +++ b/src/Bot/Concerns/HasEvents.php @@ -3,6 +3,7 @@ namespace Laracord\Bot\Concerns; use InvalidArgumentException; +use Laracord\Bot\Hook; use Laracord\Events\Event; trait HasEvents @@ -27,6 +28,8 @@ protected function bootEvents(): self $this->logger->info("The {$event->getName()} event has been registered to {$event->getHandler()}."); } + $this->callHook(Hook::AFTER_EVENTS_REGISTERED); + return $this; } diff --git a/src/Bot/Concerns/HasHttpServer.php b/src/Bot/Concerns/HasHttpServer.php index ff6b0f2..403fd5f 100644 --- a/src/Bot/Concerns/HasHttpServer.php +++ b/src/Bot/Concerns/HasHttpServer.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Support\Str; +use Laracord\Bot\Hook; use Laracord\Http\HttpServer; trait HasHttpServer @@ -85,6 +86,8 @@ protected function bootHttpServer(): self if ($this->httpServer->isBooted()) { $this->logger->info("HTTP server started on {$this->httpServer->getAddress()}."); + + $this->callHook(Hook::AFTER_HTTP_SERVER_START); } }); diff --git a/src/Bot/Concerns/HasServices.php b/src/Bot/Concerns/HasServices.php index 31d231f..205ce09 100644 --- a/src/Bot/Concerns/HasServices.php +++ b/src/Bot/Concerns/HasServices.php @@ -3,6 +3,7 @@ namespace Laracord\Bot\Concerns; use InvalidArgumentException; +use Laracord\Bot\Hook; use Laracord\Services\Service; trait HasServices @@ -27,6 +28,8 @@ protected function bootServices(): self $this->logger->info("The {$service->getName()} service has been booted."); } + $this->callHook(Hook::AFTER_SERVICES_REGISTERED); + return $this; } diff --git a/src/Bot/Hook.php b/src/Bot/Hook.php index 1cc2374..764b0ae 100644 --- a/src/Bot/Hook.php +++ b/src/Bot/Hook.php @@ -5,12 +5,62 @@ enum Hook: string { /** - * Hook fired before the bot boots. + * Called before the bot starts its boot process. */ case BEFORE_BOOT = 'beforeBoot'; /** - * Hook fired after the bot boots. + * Called after all bot components are initialized. */ case AFTER_BOOT = 'afterBoot'; + + /** + * Called before the bot begins its shutdown process. + */ + case BEFORE_SHUTDOWN = 'beforeShutdown'; + + /** + * Called before the bot begins its restart process. + */ + case BEFORE_RESTART = 'beforeRestart'; + + /** + * Called after the bot has completed its restart process. + */ + case AFTER_RESTART = 'afterRestart'; + + /** + * Called after all commands (regular chat commands) are registered. + */ + case AFTER_COMMANDS_REGISTERED = 'afterCommandsRegistered'; + + /** + * Called after all application commands (slash commands and context menus) are registered. + */ + case AFTER_APPLICATION_COMMANDS_REGISTERED = 'afterApplicationCommandsRegistered'; + + /** + * Called after all event listeners are registered. + */ + case AFTER_EVENTS_REGISTERED = 'afterEventsRegistered'; + + /** + * Called after all services are booted. + */ + case AFTER_SERVICES_REGISTERED = 'afterServicesRegistered'; + + /** + * Called after the HTTP server has started successfully. + */ + case AFTER_HTTP_SERVER_START = 'afterHttpServerStart'; + + /** + * Called before the HTTP server begins its shutdown process. + */ + case BEFORE_HTTP_SERVER_STOP = 'beforeHttpServerStop'; + + /** + * Called after all interactions are registered. + */ + case AFTER_INTERACTIONS_REGISTERED = 'afterInteractionsRegistered'; } diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index 876a56e..1f089c4 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -7,6 +7,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; +use Laracord\Bot\Hook; use Laracord\Laracord; use Psr\Http\Message\ServerRequestInterface; use React\Http\HttpServer as Server; @@ -80,6 +81,8 @@ public function shutdown(): void return; } + $this->bot->callHook(Hook::BEFORE_HTTP_SERVER_STOP); + $this->getServer()->removeAllListeners(); $this->getSocket()->close(); diff --git a/src/Laracord.php b/src/Laracord.php index ee66941..deef08f 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -77,6 +77,7 @@ public function boot(): void protected function handleBoot(): void { $this->registerDiscord(); + $this->registerSignalHandlers(); $this->callHook(Hook::BEFORE_BOOT); @@ -123,10 +124,17 @@ protected function handleBoot(): void */ public function shutdown(int $code = 0): void { - $this->logger->info("Shutting down {$this->getName()}."); + $this->callHook(Hook::BEFORE_SHUTDOWN); - $this->httpServer()->shutdown(); - $this->discord()->close(); + if ($this->httpServer) { + $this->httpServer()->shutdown(); + } + + $this->discord?->close(closeLoop: false); + + $this->logger->info("{$this->getName()} is shutting down."); + + $this->loop->stop(); exit($code); } @@ -138,11 +146,14 @@ public function restart(): void { $this->logger->info("{$this->getName()} is restarting."); - $this->discord->close(closeLoop: false); + $this->callHook(Hook::BEFORE_RESTART); + $this->discord?->close(closeLoop: false); $this->discord = null; $this->handleBoot(); + + $this->callHook(Hook::AFTER_RESTART); } /** From f54305d248b8946cabac2b9d4f0d25216bffd800 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 03:33:07 -0600 Subject: [PATCH 087/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20signal=20handlers=20for=20bot=20shutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasLoop.php | 26 ++++++++++++++++++++++++++ src/Laracord.php | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Bot/Concerns/HasLoop.php b/src/Bot/Concerns/HasLoop.php index fcfb31e..abe3511 100644 --- a/src/Bot/Concerns/HasLoop.php +++ b/src/Bot/Concerns/HasLoop.php @@ -12,6 +12,32 @@ trait HasLoop */ protected ?LoopInterface $loop = null; + /** + * Register signal handlers for graceful shutdown. + */ + protected function registerSignalHandlers(): void + { + if (! extension_loaded('pcntl')) { + $this->logger->warning('The pcntl extension is not loaded. Signal handling is disabled.'); + + return; + } + + $loop = $this->getLoop(); + + $loop->addSignal(SIGINT, function () { + $this->logger->info('Received shutdown signal (SIGINT).'); + + $this->shutdown(); + }); + + $loop->addSignal(SIGTERM, function () { + $this->logger->info('Received shutdown signal (SIGTERM).'); + + $this->shutdown(); + }); + } + /** * Get the event loop. */ diff --git a/src/Laracord.php b/src/Laracord.php index deef08f..945c543 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -134,7 +134,7 @@ public function shutdown(int $code = 0): void $this->logger->info("{$this->getName()} is shutting down."); - $this->loop->stop(); + $this->getLoop()?->stop(); exit($code); } From 8d5cc2aa301367d6bb892b6815130329d69085d1 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 08:36:33 -0600 Subject: [PATCH 088/146] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unnecessary=20c?= =?UTF-8?q?onfigs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/mail.php | 135 ---------------------------- config/session.php | 217 --------------------------------------------- 2 files changed, 352 deletions(-) delete mode 100644 config/mail.php delete mode 100644 config/session.php diff --git a/config/mail.php b/config/mail.php deleted file mode 100644 index 7ed4812..0000000 --- a/config/mail.php +++ /dev/null @@ -1,135 +0,0 @@ - env('MAIL_MAILER', 'log'), - - /* - |-------------------------------------------------------------------------- - | Mailer Configurations - |-------------------------------------------------------------------------- - | - | Here you may configure all of the mailers used by your application plus - | their respective settings. Several examples have been configured for - | you and you are free to add your own as your application requires. - | - | Laravel supports a variety of mail "transport" drivers that can be used - | when delivering an email. You may specify which one you're using for - | your mailers below. You may also add additional mailers if needed. - | - | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", - | "postmark", "resend", "log", "array", - | "failover", "roundrobin" - | - */ - - 'mailers' => [ - - 'smtp' => [ - 'transport' => 'smtp', - 'scheme' => env('MAIL_SCHEME'), - 'url' => env('MAIL_URL'), - 'host' => env('MAIL_HOST', '127.0.0.1'), - 'port' => env('MAIL_PORT', 2525), - 'username' => env('MAIL_USERNAME'), - 'password' => env('MAIL_PASSWORD'), - 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), - ], - - 'ses' => [ - 'transport' => 'ses', - ], - - 'postmark' => [ - 'transport' => 'postmark', - // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), - // 'client' => [ - // 'timeout' => 5, - // ], - ], - - 'resend' => [ - 'transport' => 'resend', - ], - - 'sendmail' => [ - 'transport' => 'sendmail', - 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), - ], - - 'log' => [ - 'transport' => 'log', - 'channel' => env('MAIL_LOG_CHANNEL'), - ], - - 'array' => [ - 'transport' => 'array', - ], - - 'failover' => [ - 'transport' => 'failover', - 'mailers' => [ - 'smtp', - 'log', - ], - ], - - 'roundrobin' => [ - 'transport' => 'roundrobin', - 'mailers' => [ - 'ses', - 'postmark', - ], - ], - - ], - - /* - |-------------------------------------------------------------------------- - | Global "From" Address - |-------------------------------------------------------------------------- - | - | You may wish for all emails sent by your application to be sent from - | the same address. Here you may specify a name and address that is - | used globally for all emails that are sent by your application. - | - */ - - 'from' => [ - 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), - ], - - /* - |-------------------------------------------------------------------------- - | Markdown Mail Settings - |-------------------------------------------------------------------------- - | - | If you are using Markdown based email rendering, you may configure your - | theme and component paths here, allowing you to customize the design - | of the emails. Or, you may simply stick with the Laravel defaults! - | - */ - - 'markdown' => [ - 'theme' => env('MAIL_MARKDOWN_THEME', 'default'), - - 'paths' => [ - resource_path('views/vendor/mail'), - ], - ], - -]; diff --git a/config/session.php b/config/session.php deleted file mode 100644 index bdcfe12..0000000 --- a/config/session.php +++ /dev/null @@ -1,217 +0,0 @@ - env('SESSION_DRIVER', 'file'), - - /* - |-------------------------------------------------------------------------- - | Session Lifetime - |-------------------------------------------------------------------------- - | - | Here you may specify the number of minutes that you wish the session - | to be allowed to remain idle before it expires. If you want them - | to expire immediately when the browser is closed then you may - | indicate that via the expire_on_close configuration option. - | - */ - - 'lifetime' => env('SESSION_LIFETIME', 120), - - 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), - - /* - |-------------------------------------------------------------------------- - | Session Encryption - |-------------------------------------------------------------------------- - | - | This option allows you to easily specify that all of your session data - | should be encrypted before it's stored. All encryption is performed - | automatically by Laravel and you may use the session like normal. - | - */ - - 'encrypt' => env('SESSION_ENCRYPT', false), - - /* - |-------------------------------------------------------------------------- - | Session File Location - |-------------------------------------------------------------------------- - | - | When utilizing the "file" session driver, the session files are placed - | on disk. The default storage location is defined here; however, you - | are free to provide another location where they should be stored. - | - */ - - 'files' => env('SESSION_PATH', laracord_path('sessions')), - - /* - |-------------------------------------------------------------------------- - | Session Database Connection - |-------------------------------------------------------------------------- - | - | When using the "database" or "redis" session drivers, you may specify a - | connection that should be used to manage these sessions. This should - | correspond to a connection in your database configuration options. - | - */ - - 'connection' => env('SESSION_CONNECTION'), - - /* - |-------------------------------------------------------------------------- - | Session Database Table - |-------------------------------------------------------------------------- - | - | When using the "database" session driver, you may specify the table to - | be used to store sessions. Of course, a sensible default is defined - | for you; however, you're welcome to change this to another table. - | - */ - - 'table' => env('SESSION_TABLE', 'sessions'), - - /* - |-------------------------------------------------------------------------- - | Session Cache Store - |-------------------------------------------------------------------------- - | - | When using one of the framework's cache driven session backends, you may - | define the cache store which should be used to store the session data - | between requests. This must match one of your defined cache stores. - | - | Affects: "apc", "dynamodb", "memcached", "redis" - | - */ - - 'store' => env('SESSION_STORE'), - - /* - |-------------------------------------------------------------------------- - | Session Sweeping Lottery - |-------------------------------------------------------------------------- - | - | Some session drivers must manually sweep their storage location to get - | rid of old sessions from storage. Here are the chances that it will - | happen on a given request. By default, the odds are 2 out of 100. - | - */ - - 'lottery' => [2, 100], - - /* - |-------------------------------------------------------------------------- - | Session Cookie Name - |-------------------------------------------------------------------------- - | - | Here you may change the name of the session cookie that is created by - | the framework. Typically, you should not need to change this value - | since doing so does not grant a meaningful security improvement. - | - */ - - 'cookie' => env( - 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' - ), - - /* - |-------------------------------------------------------------------------- - | Session Cookie Path - |-------------------------------------------------------------------------- - | - | The session cookie path determines the path for which the cookie will - | be regarded as available. Typically, this will be the root path of - | your application, but you're free to change this when necessary. - | - */ - - 'path' => env('SESSION_PATH', '/'), - - /* - |-------------------------------------------------------------------------- - | Session Cookie Domain - |-------------------------------------------------------------------------- - | - | This value determines the domain and subdomains the session cookie is - | available to. By default, the cookie will be available to the root - | domain and all subdomains. Typically, this shouldn't be changed. - | - */ - - 'domain' => env('SESSION_DOMAIN'), - - /* - |-------------------------------------------------------------------------- - | HTTPS Only Cookies - |-------------------------------------------------------------------------- - | - | By setting this option to true, session cookies will only be sent back - | to the server if the browser has a HTTPS connection. This will keep - | the cookie from being sent to you when it can't be done securely. - | - */ - - 'secure' => env('SESSION_SECURE_COOKIE'), - - /* - |-------------------------------------------------------------------------- - | HTTP Access Only - |-------------------------------------------------------------------------- - | - | Setting this value to true will prevent JavaScript from accessing the - | value of the cookie and the cookie will only be accessible through - | the HTTP protocol. It's unlikely you should disable this option. - | - */ - - 'http_only' => env('SESSION_HTTP_ONLY', true), - - /* - |-------------------------------------------------------------------------- - | Same-Site Cookies - |-------------------------------------------------------------------------- - | - | This option determines how your cookies behave when cross-site requests - | take place, and can be used to mitigate CSRF attacks. By default, we - | will set this value to "lax" to permit secure cross-site requests. - | - | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - | - | Supported: "lax", "strict", "none", null - | - */ - - 'same_site' => env('SESSION_SAME_SITE', 'lax'), - - /* - |-------------------------------------------------------------------------- - | Partitioned Cookies - |-------------------------------------------------------------------------- - | - | Setting this value to true will tie the cookie to the top-level site for - | a cross-site context. Partitioned cookies are accepted by the browser - | when flagged "secure" and the Same-Site attribute is set to "none". - | - */ - - 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), - -]; From 1b31e822089f94b7ceb196f79b23a479a6298951 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 08:36:51 -0600 Subject: [PATCH 089/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20service=20booting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Services/Service.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Service.php b/src/Services/Service.php index d06fae6..71e5e22 100644 --- a/src/Services/Service.php +++ b/src/Services/Service.php @@ -52,7 +52,7 @@ public function boot(): self $this->resolveHandler(); } - $this->bot->getLoop()->addPeriodicTimer( + $this->bot()->getLoop()->addPeriodicTimer( $this->getInterval(), fn () => $this->resolveHandler() ); From 09e2ae94c5c7b22faa2f41149b4067912e00c0d4 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 11:01:35 -0600 Subject: [PATCH 090/146] =?UTF-8?q?=E2=8F=AA=20Restore=20the=20sessions=20?= =?UTF-8?q?config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/session.php | 217 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 config/session.php diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..bdcfe12 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => env('SESSION_PATH', laracord_path('sessions')), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; From 71e8ae9edb27ec61d91b0740d17100cb92357db5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 11:15:09 -0600 Subject: [PATCH 091/146] =?UTF-8?q?=F0=9F=8E=A8=20Simplify=20the=20logging?= =?UTF-8?q?=20handler=20for=20all=20environments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Logging/LoggingHandler.php | 61 +++++----------------------------- 1 file changed, 9 insertions(+), 52 deletions(-) diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index f72f023..e14acb9 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -8,28 +8,22 @@ use Monolog\Level; use Monolog\LogRecord; use React\EventLoop\TimerInterface; -use React\Stream\WritableResourceStream; use RuntimeException; class LoggingHandler extends AbstractProcessingHandler { /** - * The stream instance. - */ - protected ?WritableResourceStream $stream = null; - - /** - * The file resource. (Windows only) + * The file handle. */ protected $handle = null; /** - * The buffer of messages to write (Windows only). + * The buffer of messages to write. */ protected array $buffer = []; /** - * The timer for flushing the buffer (Windows only). + * The timer for flushing the buffer. */ protected ?TimerInterface $flushTimer = null; @@ -58,16 +52,6 @@ protected function initializeStream(): void File::ensureDirectoryExists($path); - windows_os() - ? $this->initializeWindowsStream() - : $this->initializeUnixStream(); - } - - /** - * Initialize the stream for Windows systems. - */ - protected function initializeWindowsStream(): void - { $this->handle = fopen($this->path, 'a'); if ($this->handle === false) { @@ -80,25 +64,7 @@ protected function initializeWindowsStream(): void } /** - * Initialize the stream for Unix-like systems. - */ - protected function initializeUnixStream(): void - { - $resource = fopen($this->path, 'a'); - - if ($resource === false) { - throw new RuntimeException("Could not open log file: {$this->path}"); - } - - $this->stream = new WritableResourceStream( - $resource, - Laracord::getLoop(), - ['write_buffer_size' => 64 * 1024] - ); - } - - /** - * Flush the buffer to disk (Windows only). + * Flush the buffer to disk. */ protected function flush(): void { @@ -134,9 +100,7 @@ protected function rotate(): void return; } - windows_os() - ? fclose($this->handle) - : $this->stream->end(); + fclose($this->handle); for ($i = $this->maxFiles - 1; $i >= 0; $i--) { $existing = $i === 0 ? $this->path : "{$this->path}.{$i}"; @@ -160,10 +124,7 @@ protected function rotate(): void protected function write(LogRecord $record): void { $this->rotate(); - - windows_os() - ? $this->buffer[] = $record->formatted - : $this->stream->write($record->formatted); + $this->buffer[] = $record->formatted; } /** @@ -171,18 +132,14 @@ protected function write(LogRecord $record): void */ public function close(): void { - if (! windows_os()) { - $this->stream->end(); - - return; - } - if ($this->flushTimer) { Laracord::getLoop()->cancelTimer($this->flushTimer); } $this->flush(); - fclose($this->handle); + if ($this->handle) { + fclose($this->handle); + } } } From d8bc42475ed31005715094f5cfbdf4eb27459dc0 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 11:22:00 -0600 Subject: [PATCH 092/146] =?UTF-8?q?=F0=9F=94=A7=20Increase=20flush=20inter?= =?UTF-8?q?val?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Logging/LoggingHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index e14acb9..f00d0ba 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -34,7 +34,7 @@ public function __construct( protected string $path, protected int $maxSize = 10485760, protected int $maxFiles = 5, - protected float $flushInterval = 1, + protected float $flushInterval = 60, mixed $level = Level::Debug, bool $bubble = true, ) { From f2fe8b0bd1a7525666d0d94517ee6964569fe8fe Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 11:47:28 -0600 Subject: [PATCH 093/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Imp?= =?UTF-8?q?rove=20service=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasServices.php | 2 -- src/Services/Service.php | 47 +++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/Bot/Concerns/HasServices.php b/src/Bot/Concerns/HasServices.php index 205ce09..2824fda 100644 --- a/src/Bot/Concerns/HasServices.php +++ b/src/Bot/Concerns/HasServices.php @@ -24,8 +24,6 @@ protected function bootServices(): self } $this->services[$service::class] = $service->boot(); - - $this->logger->info("The {$service->getName()} service has been booted."); } $this->callHook(Hook::AFTER_SERVICES_REGISTERED); diff --git a/src/Services/Service.php b/src/Services/Service.php index 71e5e22..088820d 100644 --- a/src/Services/Service.php +++ b/src/Services/Service.php @@ -6,6 +6,7 @@ use Laracord\HasLaracord; use Laracord\Services\Contracts\Service as ServiceContract; use Laracord\Services\Exceptions\InvalidServiceInterval; +use React\EventLoop\TimerInterface; abstract class Service implements ServiceContract { @@ -31,6 +32,16 @@ abstract class Service implements ServiceContract */ protected bool $enabled = true; + /** + * Determine if the service is booted. + */ + protected bool $booted = false; + + /** + * The timer for the service. + */ + protected ?TimerInterface $timer = null; + /** * Make a new service instance. */ @@ -44,6 +55,10 @@ public static function make(): self */ public function boot(): self { + if ($this->booted) { + return $this; + } + if ($this->getInterval() < 1) { throw new InvalidServiceInterval($this->getName()); } @@ -52,11 +67,15 @@ public function boot(): self $this->resolveHandler(); } - $this->bot()->getLoop()->addPeriodicTimer( + $this->timer = $this->bot()->getLoop()->addPeriodicTimer( $this->getInterval(), fn () => $this->resolveHandler() ); + $this->logger->info("The {$this->getName()} service has been booted."); + + $this->booted = true; + return $this; } @@ -105,4 +124,30 @@ public function isEnabled(): bool { return $this->enabled; } + + /** + * Determine if the service is booted. + */ + public function isBooted(): bool + { + return $this->booted; + } + + /** + * Stop the service. + */ + public function stop(): void + { + if (! $this->booted) { + return; + } + + $this->getLoop()->cancelTimer($this->timer); + + $this->logger->info("The {$this->getName()} service has been stopped."); + + $this->timer = null; + + $this->booted = false; + } } From af33a076baf07d4e949341cdde4972cce98fd735 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 11:48:54 -0600 Subject: [PATCH 094/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Sto?= =?UTF-8?q?p=20services=20when=20restarting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Laracord.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Laracord.php b/src/Laracord.php index 945c543..3710e18 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -65,6 +65,7 @@ public function boot(): void $this->registerConsole(); $this->registerLogger(); + $this->registerSignalHandlers(); rescue(fn () => $this->handleBoot()); @@ -77,7 +78,6 @@ public function boot(): void protected function handleBoot(): void { $this->registerDiscord(); - $this->registerSignalHandlers(); $this->callHook(Hook::BEFORE_BOOT); @@ -148,6 +148,10 @@ public function restart(): void $this->callHook(Hook::BEFORE_RESTART); + foreach ($this->services as $service) { + $service->stop(); + } + $this->discord?->close(closeLoop: false); $this->discord = null; From 252f1380dc9eda222bcf92e4e4d7ce5449de8cd8 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 11:51:24 -0600 Subject: [PATCH 095/146] =?UTF-8?q?=F0=9F=8E=A8=20Use=20the=20bot=20instan?= =?UTF-8?q?ce=20for=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Services/Service.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/Service.php b/src/Services/Service.php index 088820d..08d3474 100644 --- a/src/Services/Service.php +++ b/src/Services/Service.php @@ -72,7 +72,7 @@ public function boot(): self fn () => $this->resolveHandler() ); - $this->logger->info("The {$this->getName()} service has been booted."); + $this->bot()->logger->info("The {$this->getName()} service has been booted."); $this->booted = true; @@ -144,7 +144,7 @@ public function stop(): void $this->getLoop()->cancelTimer($this->timer); - $this->logger->info("The {$this->getName()} service has been stopped."); + $this->bot()->logger->info("The {$this->getName()} service has been stopped."); $this->timer = null; From 4096172167ab440233756cb5b4218c515dee62da Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 12:13:50 -0600 Subject: [PATCH 096/146] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20the=20console?= =?UTF-8?q?=20deconstructor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Console.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Console/Console.php b/src/Console/Console.php index b64a9b6..3710222 100644 --- a/src/Console/Console.php +++ b/src/Console/Console.php @@ -67,14 +67,6 @@ public function __construct( $this->stdio->on('end', fn () => $this->handle('shutdown')); } - /** - * Close the console instance. - */ - public function __destruct() - { - $this->stdio->close(); - } - /** * Make a new console instance. */ From 38b4e224184d3925b536b5c609b54296e7a839d4 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 13:29:09 -0600 Subject: [PATCH 097/146] =?UTF-8?q?=F0=9F=9A=9A=20Move=20the=20`HasAsync`?= =?UTF-8?q?=20trait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{ => Bot}/Concerns/HasAsync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ => Bot}/Concerns/HasAsync.php (96%) diff --git a/src/Concerns/HasAsync.php b/src/Bot/Concerns/HasAsync.php similarity index 96% rename from src/Concerns/HasAsync.php rename to src/Bot/Concerns/HasAsync.php index 2cebaaf..ec4fdea 100644 --- a/src/Concerns/HasAsync.php +++ b/src/Bot/Concerns/HasAsync.php @@ -1,6 +1,6 @@ Date: Sat, 8 Feb 2025 13:30:00 -0600 Subject: [PATCH 098/146] =?UTF-8?q?=F0=9F=8E=A8=20Make=20`callHook`=20publ?= =?UTF-8?q?ic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHooks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bot/Concerns/HasHooks.php b/src/Bot/Concerns/HasHooks.php index fa7650c..74a0168 100644 --- a/src/Bot/Concerns/HasHooks.php +++ b/src/Bot/Concerns/HasHooks.php @@ -34,7 +34,7 @@ public function registerHook(Hook|string $hook, callable $callback): self /** * Call all registered callbacks for a hook. */ - protected function callHook(Hook|string $hook): void + public function callHook(Hook|string $hook): void { $hook = $hook instanceof Hook ? $hook->value From c797c56e22652fdbdeeb0e0a54eff3fd98f81437 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 13:30:16 -0600 Subject: [PATCH 099/146] =?UTF-8?q?=F0=9F=94=A7=20Move=20default=20log=20p?= =?UTF-8?q?ath=20to=20`.laracord/logs`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/logging.php b/config/logging.php index 1c94030..2655b1c 100644 --- a/config/logging.php +++ b/config/logging.php @@ -62,7 +62,7 @@ 'driver' => 'monolog', 'handler' => Laracord\Logging\LoggingHandler::class, 'with' => [ - 'path' => storage_path('logs/laracord.log'), + 'path' => laracord_path('logs/laracord.log'), 'level' => env('LOG_LEVEL', 'info'), ], ], From 69e5f1ccc268893f9cdf18317cc0051a4f30eff6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 13:30:37 -0600 Subject: [PATCH 100/146] =?UTF-8?q?=F0=9F=8E=A8=20Add=20the=20`HasAsync`?= =?UTF-8?q?=20trait=20to=20the=20Laracord=20instance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Laracord.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Laracord.php b/src/Laracord.php index 3710e18..4fd6c78 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -16,6 +16,7 @@ class Laracord { use Concerns\HasApplicationCommands, + Concerns\HasAsync, Concerns\HasCommandMiddleware, Concerns\HasCommands, Concerns\HasComponents, From 0ea241d6e86f44cc4df4b445328167c23ea3f64f Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 13:30:51 -0600 Subject: [PATCH 101/146] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20unnecessary=20l?= =?UTF-8?q?og=20rotate=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Logging/LoggingHandler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index f00d0ba..b18ef16 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -123,7 +123,6 @@ protected function rotate(): void */ protected function write(LogRecord $record): void { - $this->rotate(); $this->buffer[] = $record->formatted; } From e044ef000dc7f84b74272d3b996d83abd9faff9f Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 8 Feb 2025 14:50:25 -0600 Subject: [PATCH 102/146] =?UTF-8?q?=F0=9F=92=84=20Use=20box=20table=20styl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasConsole.php | 3 ++- src/Console/Prompts/HelpPrompt.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bot/Concerns/HasConsole.php b/src/Bot/Concerns/HasConsole.php index 7c5dc56..9f60bd9 100644 --- a/src/Bot/Concerns/HasConsole.php +++ b/src/Bot/Concerns/HasConsole.php @@ -98,7 +98,8 @@ public function showCommands(): self collect($commands)->map(fn ($command) => [ $command->getSignature(), $command->getDescription(), - ])->all() + ])->all(), + tableStyle: 'box', ); return $this; diff --git a/src/Console/Prompts/HelpPrompt.php b/src/Console/Prompts/HelpPrompt.php index 386329d..ad91f22 100644 --- a/src/Console/Prompts/HelpPrompt.php +++ b/src/Console/Prompts/HelpPrompt.php @@ -40,6 +40,7 @@ public function handle(Console $console): void $command->getName(), $command->getDescription(), ])->all(), + tableStyle: 'box', ); } } From 9dba2ae1ae06613c74a98195054090c74babcdff Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 9 Feb 2025 16:17:25 -0600 Subject: [PATCH 103/146] =?UTF-8?q?=E2=9E=95=20Add=20`sebastian/environmen?= =?UTF-8?q?t`=20to=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 38aac81..f191314 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "react/async": "^4.2", "react/http": "^1.9", "react/promise": "^3.0", + "sebastian/environment": "^8.0", "symfony/psr-http-message-bridge": "^7.0", "team-reflex/discord-php": "^10.3" }, From 1afb773af1a3a7497224a78cef6a07c41329ed2f Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 9 Feb 2025 16:19:52 -0600 Subject: [PATCH 104/146] =?UTF-8?q?=E2=9E=95=20Add=20`sebastian/environmen?= =?UTF-8?q?t`=20to=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f191314..c948b4e 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "react/async": "^4.2", "react/http": "^1.9", "react/promise": "^3.0", - "sebastian/environment": "^8.0", + "sebastian/environment": "*", "symfony/psr-http-message-bridge": "^7.0", "team-reflex/discord-php": "^10.3" }, From 952c732fb4e46c873669f8e3c7519a9f80f32fb8 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Feb 2025 14:13:07 -0600 Subject: [PATCH 105/146] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20upcom?= =?UTF-8?q?ing=20Discord=20Components=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Components.php | 602 +++++++++++++++++++++++++++++++++++++ src/Discord/Message.php | 112 ++++++- 2 files changed, 709 insertions(+), 5 deletions(-) create mode 100644 src/Discord/Components.php diff --git a/src/Discord/Components.php b/src/Discord/Components.php new file mode 100644 index 0000000..3c45dee --- /dev/null +++ b/src/Discord/Components.php @@ -0,0 +1,602 @@ + 3066993, + 'success' => 3066993, + 'error' => 15158332, + 'warning' => 15105570, + 'info' => 3447003, + ]; + + /** + * The current container being built. + */ + protected ?Container $currentContainer = null; + + /** + * The current section being built. + */ + protected ?Section $currentSection = null; + + /** + * The components being built. + */ + protected array $components = []; + + /** + * The interaction route prefix. + */ + protected ?string $routePrefix = null; + + /** + * The current action row being built. + */ + protected ?ActionRow $currentActionRow = null; + + /** + * The buttons in the current action row. + */ + protected array $rowButtons = []; + + /** + * Create a new components instance. + */ + public function __construct(?Laracord $bot = null) + { + $this->bot = $bot ?: app('bot'); + } + + /** + * Make a new components instance. + */ + public static function make(?Laracord $bot = null): self + { + return new static($bot); + } + + /** + * Start a new container. + */ + public function container(string|int|null $accentColor = null, bool $spoiler = false): self + { + $this->currentContainer = Container::new(); + + if ($accentColor) { + $this->currentContainer->setAccentColor($this->parseColor($accentColor)); + } + + if ($spoiler) { + $this->currentContainer->setSpoiler(); + } + + $this->components[] = $this->currentContainer; + + return $this; + } + + /** + * Parse a color value into its integer representation. + */ + protected function parseColor(int|string $color): int + { + if (is_int($color)) { + return $color; + } + + $color = match ($color) { + 'success' => $this->colors['success'], + 'error' => $this->colors['error'], + 'warning' => $this->colors['warning'], + 'info' => $this->colors['info'], + default => $color, + }; + + if (str_starts_with($color, '#')) { + $color = hexdec( + Str::of($color)->replace('#', '')->limit(6, '')->toString() + ); + } + + return (int) $color; + } + + /** + * Set the current container's color to success. + */ + public function success(): self + { + $this->ensureContainer(); + $this->currentContainer->setAccentColor($this->colors['success']); + + return $this; + } + + /** + * Set the current container's color to error. + */ + public function error(): self + { + $this->ensureContainer(); + $this->currentContainer->setAccentColor($this->colors['error']); + + return $this; + } + + /** + * Set the current container's color to warning. + */ + public function warning(): self + { + $this->ensureContainer(); + + $this->currentContainer->setAccentColor($this->colors['warning']); + + return $this; + } + + /** + * Set the current container's color to info. + */ + public function info(): self + { + $this->ensureContainer(); + + $this->currentContainer->setAccentColor($this->colors['info']); + + return $this; + } + + /** + * Set the current container's color. + */ + public function color(int|string $color): self + { + $this->ensureContainer(); + + $this->currentContainer->setAccentColor($this->parseColor($color)); + + return $this; + } + + /** + * Get or create the current container. + */ + protected function ensureContainer(): void + { + if (! $this->currentContainer) { + $this->container(); + } + } + + /** + * Add a text display to the current container. + */ + public function text(string|array $text): self + { + $this->ensureContainer(); + + $this->currentSection = Section::new(); + + $text = is_string($text) + ? [$text] + : $text; + + $texts = array_slice($text, 0, 3); + + foreach ($texts as $content) { + $this->currentSection->addComponent(TextDisplay::new($content)); + } + + $this->currentContainer->addComponent($this->currentSection); + + return $this; + } + + /** + * Add a thumbnail to the current section. + */ + public function thumbnail(string $url, ?string $description = null, bool $spoiler = false): self + { + if (! $this->currentSection) { + throw new Exception('You must create a section before adding a thumbnail.'); + } + + $thumbnail = Thumbnail::new($url); + + if ($description) { + $thumbnail->setDescription($description); + } + + if ($spoiler) { + $thumbnail->setSpoiler(); + } + + $this->currentSection->setAccessory($thumbnail); + $this->currentSection = null; + + return $this; + } + + /** + * Add a button to the current section or action row. + */ + public function button( + string $label, + mixed $value = null, + mixed $emoji = null, + ?string $style = null, + bool $disabled = false, + bool $hidden = false, + ?string $id = null, + ?string $route = null, + array $options = [] + ): self { + if ($hidden) { + return $this; + } + + $button = $this->createButton($label, $value, $emoji, $style, $disabled, $hidden, $id, $route, $options); + + if (! $button) { + return $this; + } + + if ($this->currentSection) { + $this->currentSection->setAccessory($button); + $this->currentSection = null; + + return $this; + } + + if (! $this->currentActionRow || count($this->rowButtons) >= 5) { + $this->row(); + } + + $this->rowButtons[] = $button; + $this->currentActionRow->addComponent($button); + + return $this; + } + + /** + * Add multiple buttons. + */ + public function buttons(array $buttons): self + { + foreach ($buttons as $key => $value) { + if (is_string($key) && is_string($value)) { + $value = [$key, $value]; + } + + $this->button(...$value); + } + + return $this; + } + + /** + * Add a new section. + */ + public function section(): self + { + $this->ensureContainer(); + + $this->currentSection = Section::new(); + + $this->currentContainer->addComponent($this->currentSection); + + return $this; + } + + /** + * Add a custom accessory to the current section. + */ + public function accessory(mixed $accessory): self + { + if (! $this->currentSection) { + throw new Exception('You must create a section before adding an accessory.'); + } + + $this->currentSection->setAccessory($accessory); + $this->currentSection = null; + + return $this; + } + + /** + * Add a separator to the current container. + */ + public function separator(bool $divider = true, bool $large = false): self + { + $this->ensureContainer(); + + $separator = Separator::new() + ->setDivider($divider) + ->setSpacing($large ? Separator::SPACING_LARGE : Separator::SPACING_SMALL); + + $this->currentContainer->addComponent($separator); + + return $this; + } + + /** + * Add a media gallery to the current container. + */ + public function gallery(): MediaGallery + { + $this->ensureContainer(); + + $gallery = MediaGallery::new(); + + $this->currentContainer->addComponent($gallery); + + return $gallery; + } + + /** + * Add a file to the current container. + */ + public function file(string $filename, bool $spoiler = false): self + { + $this->ensureContainer(); + + $file = File::new($filename); + + if ($spoiler) { + $file->setSpoiler(); + } + + $this->currentContainer->addComponent($file); + + return $this; + } + + /** + * Add a media gallery with items. + */ + public function mediaGallery(array $items): self + { + $gallery = $this->gallery(); + + foreach ($items as $item) { + $gallery->addItem($item); + } + + return $this; + } + + /** + * Get the built components. + */ + public function getComponents(): array + { + return $this->components; + } + + /** + * Get the message flags for v2 components. + */ + public function getFlags(): int + { + return ChannelMessage::FLAG_V2_COMPONENTS; + } + + /** + * Create a button component. + */ + protected function createButton( + string $label, + mixed $value = null, + mixed $emoji = null, + ?string $style = null, + bool $disabled = false, + bool $hidden = false, + ?string $id = null, + ?string $route = null, + array $options = [] + ): ?Button { + if ($hidden) { + return null; + } + + $style = match ($style) { + 'link' => Button::STYLE_LINK, + 'primary' => Button::STYLE_PRIMARY, + 'secondary' => Button::STYLE_SECONDARY, + 'success' => Button::STYLE_SUCCESS, + 'danger' => Button::STYLE_DANGER, + default => $style, + }; + + $style = $style ?? (is_string($value) ? Button::STYLE_LINK : Button::STYLE_PRIMARY); + + $button = Button::new($style) + ->setLabel($label) + ->setEmoji($emoji) + ->setDisabled($disabled); + + if ($id) { + $button = $button->setCustomId($id); + } + + if ($route) { + $button = $this->getRoutePrefix() + ? $button->setCustomId("{$this->getRoutePrefix()}@{$route}") + : $button->setCustomId($route); + } + + if ($options) { + foreach ($options as $key => $option) { + $key = Str::of($key)->camel()->ucfirst()->start('set')->toString(); + + try { + $button = $button->{$key}($option); + } catch (Throwable) { + $this->bot->logger->error("Invalid button option {$key}"); + + continue; + } + } + } + + $button = match ($style) { + Button::STYLE_LINK => $button->setUrl($value), + default => $value ? $button->setListener($value, $this->bot->discord()) : $button, + }; + + if (! $value && ! $route && ! $id) { + throw new Exception('Message buttons must contain a valid `value`, `route`, or `id`.'); + } + + return $button; + } + + /** + * Set the interaction route prefix. + */ + public function routePrefix(?string $routePrefix): self + { + $this->routePrefix = Str::slug($routePrefix); + + return $this; + } + + /** + * Retrieve the interaction route prefix. + */ + public function getRoutePrefix(): ?string + { + return $this->routePrefix; + } + + /** + * Start a new action row. + */ + public function row(): self + { + $this->ensureContainer(); + + $this->currentActionRow = ActionRow::new(); + + $this->rowButtons = []; + + $this->currentContainer->addComponent($this->currentActionRow); + + return $this; + } + + /** + * Add a select menu to the current action row. + */ + public function select( + array $items = [], + ?callable $listener = null, + ?string $placeholder = null, + ?string $id = null, + bool $disabled = false, + bool $hidden = false, + int $minValues = 1, + int $maxValues = 1, + ?string $type = null, + ?string $route = null, + ?array $options = [] + ): self { + if ($hidden) { + return $this; + } + + $select = match ($type) { + 'channel' => ChannelSelect::new(), + 'mentionable' => MentionableSelect::new(), + 'role' => RoleSelect::new(), + 'user' => UserSelect::new(), + default => StringSelect::new(), + }; + + $select = $select + ->setPlaceholder($placeholder) + ->setMinValues($minValues) + ->setMaxValues($maxValues) + ->setDisabled($disabled); + + if ($id) { + $select = $select->setCustomId($id); + } + + if ($route) { + $select = $this->getRoutePrefix() + ? $select->setCustomId("{$this->getRoutePrefix()}@{$route}") + : $select->setCustomId($route); + } + + if ($listener) { + $select = $select->setListener($listener, $this->bot->discord()); + } + + if ($options) { + foreach ($options as $key => $option) { + $key = Str::of($key)->camel()->ucfirst()->start('set')->toString(); + + try { + $select = $select->{$key}($option); + } catch (Throwable) { + $this->bot->logger->error("Invalid select menu option {$key}"); + + continue; + } + } + } + + foreach ($items as $key => $value) { + if (! is_array($value)) { + $select->addOption(Option::new(is_int($key) ? $value : $key, $value)); + + continue; + } + + $option = Option::new($value['label'] ?? $key, $value['value'] ?? $key) + ->setDescription($value['description'] ?? null) + ->setEmoji($value['emoji'] ?? null) + ->setDefault($value['default'] ?? false); + + $select->addOption($option); + } + + $this->currentContainer->addComponent($select); + + return $this; + } +} diff --git a/src/Discord/Message.php b/src/Discord/Message.php index ea6758d..7614f1f 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -169,11 +169,21 @@ class Message */ protected string|bool $webhook = false; + /** + * The webhook cache. + */ + protected static array $webhookCache = []; + /** * The additional message embeds. */ protected array $embeds = []; + /** + * The message sections. + */ + protected array $sections = []; + /** * The default embed colors. */ @@ -190,6 +200,11 @@ class Message */ protected ?string $routePrefix = null; + /** + * The message flags. + */ + protected int $flags = 0; + /** * Create a new Discord message instance. * @@ -226,7 +241,7 @@ public function build(): MessageBuilder ->setTts($this->tts) ->setContent($this->body) ->setStickers($this->stickers) - ->setComponents($this->getComponents()); + ->setFlags($this->flags); if ($this->hasContent() || $this->hasFields()) { $message->addEmbed($this->getEmbed()); @@ -238,6 +253,12 @@ public function build(): MessageBuilder } } + if ($this->hasComponents()) { + foreach ($this->components as $component) { + $message->addComponent($component); + } + } + if ($this->hasSelects()) { foreach ($this->selects as $select) { $message->addComponent($select); @@ -314,9 +335,17 @@ public function sendTo(mixed $user): ?PromiseInterface */ protected function handleWebhook(): ?PromiseInterface { + $channel = $this->getChannel()->id; + try { - /** @var WebhookRepository $webhooks */ - $webhooks = await($this->getChannel()->webhooks->freshen()); + if (! isset(static::$webhookCache[$channel])) { + /** @var WebhookRepository $webhooks */ + $webhooks = await($this->getChannel()->webhooks->freshen()); + + static::$webhookCache[$channel] = $webhooks; + } + + $webhooks = static::$webhookCache[$channel]; } catch (NoPermissionsException) { $this->bot->logger->error("\nMissing permission to fetch channel webhooks."); @@ -336,7 +365,11 @@ protected function handleWebhook(): ?PromiseInterface return $webhooks->save(new Webhook($this->bot->discord(), [ 'name' => $this->bot->discord()->username, ]))->then( - fn (Webhook $webhook) => $webhook->execute($this->build()), + function (Webhook $webhook) use ($channel) { + static::$webhookCache[$channel]->push($webhook); + + return $webhook->execute($this->build()); + }, fn () => $this->bot->logger->error('Failed to create message webhook.') ); } @@ -344,7 +377,7 @@ protected function handleWebhook(): ?PromiseInterface return $webhook->execute($this->build()); } - $webhook = $this->getChannel()->webhooks->get('url', $this->webhook); + $webhook = $webhooks->get('url', $this->webhook); if (! $webhook) { $this->bot->logger->error("Could not find webhook {$this->webhook} on channel to send message."); @@ -1244,6 +1277,75 @@ public function clearEmbeds(): self return $this; } + /** + * Add a component to the message. + */ + public function addComponent(mixed $component): self + { + $this->components[] = $component; + + return $this; + } + + /** + * Add multiple components to the message. + */ + public function addComponents(array $components): self + { + foreach ($components as $component) { + $this->addComponent($component); + } + + return $this; + } + + /** + * Add components using the Components instance. + */ + public function withComponents(Components|callable $components): self + { + if (is_callable($components)) { + $instance = Components::make($this->bot) + ->routePrefix($this->routePrefix); + + $components($instance); + + $components = $instance; + } + + $this->addComponents($components->getComponents()); + + return $this; + } + + /** + * Determine if the message has components. + */ + public function hasComponents(): bool + { + return ! empty($this->components); + } + + /** + * Clear the components from the message. + */ + public function clearComponents(): self + { + $this->components = []; + + return $this; + } + + /** + * Set the message flags. + */ + public function flags(int $flags): self + { + $this->flags = $flags; + + return $this; + } + /** * Set the interaction route prefix. */ From 74111aed24b9ff14cb1d5f6742b60fad94ce4b5b Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Feb 2025 15:46:09 -0600 Subject: [PATCH 106/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20v2=20component?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Components.php | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Discord/Components.php b/src/Discord/Components.php index 3c45dee..42c4f2d 100644 --- a/src/Discord/Components.php +++ b/src/Discord/Components.php @@ -139,6 +139,7 @@ protected function parseColor(int|string $color): int public function success(): self { $this->ensureContainer(); + $this->currentContainer->setAccentColor($this->colors['success']); return $this; @@ -150,6 +151,7 @@ public function success(): self public function error(): self { $this->ensureContainer(); + $this->currentContainer->setAccentColor($this->colors['error']); return $this; @@ -202,14 +204,12 @@ protected function ensureContainer(): void } /** - * Add a text display to the current container. + * Add a text display to the current container or section. */ public function text(string|array $text): self { $this->ensureContainer(); - $this->currentSection = Section::new(); - $text = is_string($text) ? [$text] : $text; @@ -217,10 +217,12 @@ public function text(string|array $text): self $texts = array_slice($text, 0, 3); foreach ($texts as $content) { - $this->currentSection->addComponent(TextDisplay::new($content)); - } + $textDisplay = TextDisplay::new($content); - $this->currentContainer->addComponent($this->currentSection); + $this->currentSection + ? $this->currentSection->addComponent($textDisplay) + : $this->currentContainer->addComponent($textDisplay); + } return $this; } @@ -321,6 +323,16 @@ public function section(): self return $this; } + /** + * Close the current section. + */ + public function endSection(): self + { + $this->currentSection = null; + + return $this; + } + /** * Add a custom accessory to the current section. */ @@ -406,14 +418,6 @@ public function getComponents(): array return $this->components; } - /** - * Get the message flags for v2 components. - */ - public function getFlags(): int - { - return ChannelMessage::FLAG_V2_COMPONENTS; - } - /** * Create a button component. */ From 6d6a2ae07d831577e98906f1f61adf55d1e24b31 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Feb 2025 15:46:33 -0600 Subject: [PATCH 107/146] =?UTF-8?q?=F0=9F=9A=A8=20Run=20Pint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Components.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Discord/Components.php b/src/Discord/Components.php index 42c4f2d..38703c7 100644 --- a/src/Discord/Components.php +++ b/src/Discord/Components.php @@ -17,7 +17,6 @@ use Discord\Builders\Components\TextDisplay; use Discord\Builders\Components\Thumbnail; use Discord\Builders\Components\UserSelect; -use Discord\Parts\Channel\Message as ChannelMessage; use Exception; use Illuminate\Support\Str; use Laracord\Laracord; From 8190cde7b36d799fae972cd5674ef26d68d2c699 Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Feb 2025 15:54:51 -0600 Subject: [PATCH 108/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20middleware=20mergi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasHttpServer.php | 3 ++- src/Http/HttpServer.php | 3 ++- src/LaracordServiceProvider.php | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Bot/Concerns/HasHttpServer.php b/src/Bot/Concerns/HasHttpServer.php index 403fd5f..f7f016e 100644 --- a/src/Bot/Concerns/HasHttpServer.php +++ b/src/Bot/Concerns/HasHttpServer.php @@ -48,7 +48,8 @@ public function withMiddleware(?callable $callback = null): self /** @var \Laracord\Http\Kernel $kernel */ $kernel = $this->app->make(Kernel::class); - $middleware = new Middleware; + /** @var \Illuminate\Foundation\Configuration\Middleware $middleware */ + $middleware = $this->app->make(Middleware::class); if (! is_null($callback)) { $callback($middleware); diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index 1f089c4..038a3b6 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -119,7 +119,8 @@ public function getServer(): Server $this->bot->withMiddleware(function (Middleware $middleware) { $middleware - ->use([\Laracord\Http\Middleware\FlushState::class]) + ->remove([\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class]) + ->append([\Laracord\Http\Middleware\FlushState::class]) ->api([\Laracord\Http\Middleware\AuthorizeToken::class]) ->alias(['auth' => \Laracord\Http\Middleware\AuthorizeToken::class]); }); diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 79d1476..efb3e7c 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Http\Kernel as KernelContract; +use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Date; @@ -76,6 +77,7 @@ public function register() $this->registerLogger(); $this->app->singleton(KernelContract::class, Kernel::class); + $this->app->singleton(Middleware::class, fn () => new Middleware); $this->app->singleton(Laracord::class, fn () => tap(Laracord::make($this->app), function (Laracord $bot) { $this From c29f7e94ca8f9e3498c1b79941b4be074a889eca Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Feb 2025 16:13:09 -0600 Subject: [PATCH 109/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20v2=20component?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Components.php | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Discord/Components.php b/src/Discord/Components.php index 38703c7..8ae59d6 100644 --- a/src/Discord/Components.php +++ b/src/Discord/Components.php @@ -363,20 +363,6 @@ public function separator(bool $divider = true, bool $large = false): self return $this; } - /** - * Add a media gallery to the current container. - */ - public function gallery(): MediaGallery - { - $this->ensureContainer(); - - $gallery = MediaGallery::new(); - - $this->currentContainer->addComponent($gallery); - - return $gallery; - } - /** * Add a file to the current container. */ @@ -396,11 +382,15 @@ public function file(string $filename, bool $spoiler = false): self } /** - * Add a media gallery with items. + * Add a media gallery to the current container. */ - public function mediaGallery(array $items): self + public function gallery(array $items): self { - $gallery = $this->gallery(); + $this->ensureContainer(); + + $gallery = MediaGallery::new(); + + $this->currentContainer->addComponent($gallery); foreach ($items as $item) { $gallery->addItem($item); From 47014c85e063c5d153e66918f18ee9ba55fee17a Mon Sep 17 00:00:00 2001 From: "Vladyslav G." <4073672+mikield@users.noreply.github.com> Date: Sat, 22 Feb 2025 15:19:53 +0100 Subject: [PATCH 110/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20booting=20without?= =?UTF-8?q?=20a=20STDIN=20tty=20available=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Console.php | 3 ++- src/LaracordServiceProvider.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Console/Console.php b/src/Console/Console.php index 3710222..a8125ab 100644 --- a/src/Console/Console.php +++ b/src/Console/Console.php @@ -12,6 +12,7 @@ use InvalidArgumentException; use Laracord\Console\Concerns\WithLog; use React\Stream\DuplexStreamInterface; +use React\Stream\WritableStreamInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -49,7 +50,7 @@ class Console * Initialize the console instance. */ public function __construct( - public readonly DuplexStreamInterface $stdio, + public readonly DuplexStreamInterface|WritableStreamInterface $stdio, public readonly Container $laravel, ConsoleOutputInterface $output, InputInterface $input, diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index efb3e7c..43856c2 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -141,7 +141,7 @@ protected function registerConsole(): void $stdout = $this->createOutputStream($loop); $console = new Console( - stdio: new CompositeStream($stdin, $stdout), + stdio: ! stream_isatty(STDIN) ? $stdout : new CompositeStream($stdin, $stdout), laravel: $this->app, output: new ConsoleOutput, input: new StringInput(''), From 5b3cc7fc3e32dec5f8668ed0ae084272cf51b0ff Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 25 Feb 2025 03:21:55 -0600 Subject: [PATCH 111/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20silencing=20debug?= =?UTF-8?q?=20on=20production?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Logging/Logger.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Logging/Logger.php b/src/Logging/Logger.php index 84e0a6a..2734c3a 100644 --- a/src/Logging/Logger.php +++ b/src/Logging/Logger.php @@ -105,10 +105,6 @@ public function info(string|Stringable $message, array $context = []): void */ public function debug(string|Stringable $message, array $context = []): void { - if (app()->environment('production')) { - return; - } - $this->handle($message, $context, LogLevel::DEBUG); } @@ -136,6 +132,10 @@ public function handle(string|Stringable $message, array $context = [], string $ default => LogLevel::INFO, }; + if (app()->isProduction() && $type === LogLevel::DEBUG) { + return; + } + if (Str::of($message)->lower()->contains($this->except)) { return; } From 76b58989cfa0d751c132f4a36a8e54ad1c89a299 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 25 Feb 2025 22:49:35 -0600 Subject: [PATCH 112/146] =?UTF-8?q?=F0=9F=A9=B9=20Dont=20run=20command=20w?= =?UTF-8?q?hen=20prefix=20is=20followed=20by=20a=20space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasCommands.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Bot/Concerns/HasCommands.php b/src/Bot/Concerns/HasCommands.php index e6923d8..e92312d 100644 --- a/src/Bot/Concerns/HasCommands.php +++ b/src/Bot/Concerns/HasCommands.php @@ -83,9 +83,14 @@ protected function handleCommands(): void return; } + $after = Str::substr($message->content, Str::length($prefix), 1); + + if ($after === ' ') { + return; + } + $parts = Str::of($message->content) ->after($prefix) - ->trim() ->explode(' '); $command = $parts->shift(); From 9d5a629a89f4f6840e7b5f3ca710937cf0d97442 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 25 Feb 2025 22:56:32 -0600 Subject: [PATCH 113/146] =?UTF-8?q?=F0=9F=A9=B9=20Run=20eager=20services?= =?UTF-8?q?=20at=20a=20future=20tick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Services/Service.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Services/Service.php b/src/Services/Service.php index 08d3474..f2f8a0d 100644 --- a/src/Services/Service.php +++ b/src/Services/Service.php @@ -63,15 +63,15 @@ public function boot(): self throw new InvalidServiceInterval($this->getName()); } - if ($this->eager) { - $this->resolveHandler(); - } - $this->timer = $this->bot()->getLoop()->addPeriodicTimer( $this->getInterval(), fn () => $this->resolveHandler() ); + if ($this->eager) { + $this->bot->getLoop()->futureTick(fn () => $this->resolveHandler()); + } + $this->bot()->logger->info("The {$this->getName()} service has been booted."); $this->booted = true; From 9a36fc3482a259b5f0d2fc38df23e5299fb8df2b Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 28 Feb 2025 14:56:55 -0600 Subject: [PATCH 114/146] =?UTF-8?q?=F0=9F=92=84=20Add=20pagination=20to=20?= =?UTF-8?q?the=20default=20`!help`=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/HelpCommand.php | 57 ++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/src/Commands/HelpCommand.php b/src/Commands/HelpCommand.php index 28d0a6b..53c6a75 100644 --- a/src/Commands/HelpCommand.php +++ b/src/Commands/HelpCommand.php @@ -3,6 +3,7 @@ namespace Laracord\Commands; use Discord\Parts\Channel\Message; +use Discord\Parts\Interactions\Interaction; class HelpCommand extends Command { @@ -35,7 +36,12 @@ class HelpCommand extends Command /** * The help message content. */ - protected static string $message = 'Here is a list of all available commands.'; + protected static string $message = 'Showing a list of %s available command(s):'; + + /** + * The maximum commands per page. + */ + protected static int $perPage = 12; /** * Set the help title. @@ -53,20 +59,40 @@ public static function setMessage(string $message): void static::$message = $message; } + /** + * Set the maximum commands per page. + */ + public static function setPerPage(int $perPage): void + { + static::$perPage = max($perPage, 25) ?: static::$perPage; + } + /** * Handle the command. */ public function handle(Message $message, array $args): void + { + $this->show($message, $args[0] ?? 1); + } + + /** + * Show the help command. + */ + public function show(Message|Interaction $context, int $page = 1): void { $commands = collect($this->bot->getCommands()) ->filter(fn ($command) => ! $command->isHidden()) - ->filter(fn ($command) => $command->getGuild() ? $message->guild_id === $command->getGuild() : true) + ->filter(fn ($command) => $command->getGuild() ? $context->guild_id === $command->getGuild() : true) ->sortBy('name'); + $page = max(1, $page); + + $items = $commands->forPage($page, static::$perPage); + $fields = []; - foreach ($commands as $command) { - $fields[$command->getSyntax()] = $command->getDescription(); + foreach ($items as $item) { + $fields[$item->getSyntax()] = $item->getDescription(); } if (count($fields) % 3 !== 0) { @@ -77,10 +103,29 @@ public function handle(Message $message, array $args): void $fields[' '] = ''; } + $pages = ceil($commands->count() / static::$perPage); + $previous = max(1, $page - 1); + $next = min($pages, $page + 1); + + $message = sprintf(static::$message, $commands->count()); + $this - ->message(static::$message) + ->message($message) ->title(static::$title) ->fields($fields) - ->reply($message); + ->button('←', route: "show:{$previous}", style: 'secondary', disabled: $page <= 1, hidden: $pages === 1) + ->button('→', route: "show:{$next}", style: 'secondary', disabled: $page >= $pages, hidden: $pages === 1) + ->footerText("Page {$page} of {$pages}") + ->editOrReply($context); + } + + /** + * The command interaction routes. + */ + public function interactions(): array + { + return [ + 'show:{page}' => fn (Interaction $interaction, string $page) => $this->show($interaction, (int) $page), + ]; } } From a0a16e0bea5eda4208e88e71a242dfe1fe95741e Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 28 Feb 2025 17:29:12 -0600 Subject: [PATCH 115/146] =?UTF-8?q?=F0=9F=9B=82=20Add=20initial=20auth=20s?= =?UTF-8?q?upport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/session.php | 2 +- src/Http/HttpServer.php | 8 ++++++-- src/Http/Middleware/FlushState.php | 18 +++++++++--------- src/LaracordServiceProvider.php | 7 +++++++ 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/config/session.php b/config/session.php index bdcfe12..bc468c9 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ | */ - 'driver' => env('SESSION_DRIVER', 'file'), + 'driver' => env('SESSION_DRIVER', 'cookie'), /* |-------------------------------------------------------------------------- diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index 038a3b6..15fca70 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -121,8 +121,12 @@ public function getServer(): Server $middleware ->remove([\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class]) ->append([\Laracord\Http\Middleware\FlushState::class]) - ->api([\Laracord\Http\Middleware\AuthorizeToken::class]) - ->alias(['auth' => \Laracord\Http\Middleware\AuthorizeToken::class]); + ->api([ + \Laracord\Http\Middleware\AuthorizeToken::class, + ]) + ->alias([ + 'auth.token' => \Laracord\Http\Middleware\AuthorizeToken::class, + ]); }); /** @var \Laracord\Http\Kernel $kernel */ diff --git a/src/Http/Middleware/FlushState.php b/src/Http/Middleware/FlushState.php index 43a838b..86e46ec 100644 --- a/src/Http/Middleware/FlushState.php +++ b/src/Http/Middleware/FlushState.php @@ -3,24 +3,18 @@ namespace Laracord\Http\Middleware; use Closure; +use Illuminate\Contracts\Auth\Factory as Auth; use Illuminate\Contracts\Foundation\Application; use Illuminate\Http\Request; class FlushState { - /** - * The application instance. - */ - protected Application $app; - /** * Create a new middleware instance. - * - * @return void */ - public function __construct(Application $app) + public function __construct(protected Application $app, protected ?Auth $auth = null) { - $this->app = $app; + // } /** @@ -28,6 +22,12 @@ public function __construct(Application $app) */ public function handle(Request $request, Closure $next) { + if ($this->auth) { + foreach (array_keys(config('auth.guards', [])) as $guard) { + $this->auth->guard($guard)->forgetUser(); + } + } + if ($this->app->resolved('cookie')) { $this->app->make('cookie')->flushQueuedCookies(); } diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 43856c2..1bdf3a2 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -49,6 +49,7 @@ abstract class LaracordServiceProvider extends AggregateServiceProvider \Illuminate\Cookie\CookieServiceProvider::class, \Illuminate\Session\SessionServiceProvider::class, \Illuminate\Mail\MailServiceProvider::class, + \Illuminate\Auth\AuthServiceProvider::class, \Laracord\Http\Providers\RouteServiceProvider::class, \Intonate\TinkerZero\TinkerZeroServiceProvider::class, ]; @@ -84,6 +85,12 @@ public function register() ->registerDefaultComponents($bot) ->registerDefaultPrompts($bot); + $user = config('auth.providers.users.model'); + + if (! class_exists($user)) { + config(['auth.providers.users.model' => $bot->getUserModel()]); + } + $this->app->singleton(Message::class, fn () => Message::make($bot)); return $this->bot($bot); From 0c47c63840501bb5e3d3aec1a4f2d7c1d0334a8f Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 28 Feb 2025 18:40:14 -0600 Subject: [PATCH 116/146] =?UTF-8?q?=F0=9F=94=A7=20Disable=20direct=20messa?= =?UTF-8?q?ge=20commands=20by=20default=20=F0=9F=A9=B9=20Fix=20direct=20me?= =?UTF-8?q?ssage=20command=20cooldowns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/AbstractCommand.php | 10 +++++++--- src/Commands/Command.php | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index ca42e01..ed00edf 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -42,7 +42,7 @@ abstract class AbstractCommand /** * Determine whether the command can be used in a direct message. */ - protected bool $directMessage = true; + protected bool $directMessage = false; /** * Determines whether the command requires admin permissions. @@ -212,13 +212,17 @@ public function isAdminCommand(): bool /** * Determine if the user is on cooldown. */ - public function isOnCooldown(User $user, Guild $guild): bool + public function isOnCooldown(User $user, ?Guild $guild = null): bool { if ($this->getCooldown() === 0) { return false; } - $key = "{$user->id}.{$guild->id}"; + $suffix = $guild + ? $guild->id + : 'direct'; + + $key = "{$user->id}.{$suffix}"; if (! isset($this->cooldowns[$key])) { $this->cooldowns[$key] = time(); diff --git a/src/Commands/Command.php b/src/Commands/Command.php index 9837e32..9305ac4 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -37,7 +37,7 @@ public function maybeHandle(Message $message, array $args): void return; } - if ($this->isOnCooldown($message->author, $message->guild)) { + if ($this->isOnCooldown($message->author, $message->guild ?? null)) { return; } From 797cd25453591a6a404563d63c6fbe02d916142e Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 2 Mar 2025 15:42:57 -0600 Subject: [PATCH 117/146] =?UTF-8?q?=E2=9E=95=20Add=20`illuminate/auth`=20t?= =?UTF-8?q?o=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index c948b4e..88b7a58 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "require": { "php": "^8.2", "guzzlehttp/guzzle": "^7.5", + "illuminate/auth": "^11.0", "illuminate/cookie": "^11.0", "illuminate/database": "^11.0", "illuminate/encryption": "^11.0", From 30622b584aab33ab7f9dc7fc400492dd0c961959 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 4 Mar 2025 06:05:04 -0600 Subject: [PATCH 118/146] =?UTF-8?q?=E2=9C=A8=20Add=20package=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 20 +++++++++++++ src/PackageManifest.php | 50 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/PackageManifest.php diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 1bdf3a2..7a33278 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -5,7 +5,11 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Http\Kernel as KernelContract; +use Illuminate\Filesystem\Filesystem; +use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Foundation\Console\PackageDiscoverCommand; +use Illuminate\Foundation\PackageManifest as BasePackageManifest; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Date; @@ -70,8 +74,22 @@ public function register() $this->mergeConfigs(); $this->createDirectories(); + $this->app->singleton(BasePackageManifest::class, fn () => new PackageManifest( + new Filesystem, + laracord_path(basePath: false), + laracord_path('cache/bootstrap/packages.php'), + )); + parent::register(); + foreach ($this->app->make(BasePackageManifest::class)->providers() as $provider) { + $this->app->register($provider); + } + + AliasLoader::getInstance([ + $this->app->make(BasePackageManifest::class)->aliases(), + ]); + $this->registerDatabase(); $this->registerLoop(); $this->registerConsole(); @@ -122,6 +140,7 @@ public function boot() Commands\PromptMakeCommand::class, Commands\ServiceMakeCommand::class, Commands\TokenMakeCommand::class, + PackageDiscoverCommand::class, ]); $this->registerMacros(); @@ -277,6 +296,7 @@ protected function mergeConfigs(): void protected function createDirectories(): void { $paths = [ + 'bootstrap' => laracord_path('cache/bootstrap'), 'cache' => $this->app['config']->get('cache.stores.file.path'), 'sessions' => $this->app['config']->get('session.files'), 'view' => $this->app['config']->get('view.compiled'), diff --git a/src/PackageManifest.php b/src/PackageManifest.php new file mode 100644 index 0000000..81f4f8a --- /dev/null +++ b/src/PackageManifest.php @@ -0,0 +1,50 @@ +files->exists($path = $this->vendorPath.'/composer/installed.json')) { + $installed = json_decode($this->files->get($path), true); + + $packages = $installed['packages'] ?? $installed; + } + + $ignoreAll = in_array('*', $ignore = $this->packagesToIgnore()); + + $this->write(collect($packages)->mapWithKeys(function ($package) { + return [$this->format($package['name']) => $package['extra']['laracord'] ?? []]; + })->each(function ($configuration) use (&$ignore) { + $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []); + })->reject(function ($configuration, $package) use ($ignore, $ignoreAll) { + return $ignoreAll || in_array($package, $ignore); + })->filter()->all()); + } + + /** + * Get all of the package names that should be ignored. + * + * @return array + */ + protected function packagesToIgnore() + { + if (! is_file($this->basePath.'/composer.json')) { + return []; + } + + return json_decode(file_get_contents( + $this->basePath.'/composer.json' + ), true)['extra']['laracord']['dont-discover'] ?? []; + } +} From cd5e3f09451c3c92ba406162b005cce75c566b7b Mon Sep 17 00:00:00 2001 From: Brandon Date: Fri, 21 Mar 2025 08:45:02 -0500 Subject: [PATCH 119/146] =?UTF-8?q?=F0=9F=8E=A8=20Use=20proper=20return=20?= =?UTF-8?q?type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers.php b/src/helpers.php index 0eb6e71..f987a5c 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,7 +4,7 @@ /** * Retrieve the bot instance. */ - function laracord(): Laracord + function laracord(): Laracord\Laracord { return app('bot'); } From 61f54a368a73e5fa7eb54d765ea88fdccb90156a Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 22 Mar 2025 20:29:01 -0500 Subject: [PATCH 120/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20configurable=20`defaults`=20to=20`Message::select`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index 7614f1f..21a22c9 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -989,7 +989,8 @@ public function select( int $maxValues = 1, ?string $type = null, ?string $route = null, - ?array $options = [] + ?array $options = [], + ?array $defaults = null ): self { if ($hidden) { return $this; @@ -1009,6 +1010,10 @@ public function select( ->setMaxValues($maxValues) ->setDisabled($disabled); + if ($default && ! $select instanceof StringSelect) { + $select->setDefaultValues($default); + } + if ($id) { $select = $select->setCustomId($id); } From 5cf31dc5312fda777453e39722185f9f881384ee Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 22 Mar 2025 20:30:11 -0500 Subject: [PATCH 121/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add?= =?UTF-8?q?=20configurable=20`defaults`=20to=20`Message::select`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index 21a22c9..03dbb64 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -1010,8 +1010,8 @@ public function select( ->setMaxValues($maxValues) ->setDisabled($disabled); - if ($default && ! $select instanceof StringSelect) { - $select->setDefaultValues($default); + if ($defaults && ! $select instanceof StringSelect) { + $select->setDefaultValues($defaults); } if ($id) { From a1f670e57141d363f1c0580afd65d020fe530d8b Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 22 Mar 2025 20:30:33 -0500 Subject: [PATCH 122/146] =?UTF-8?q?=F0=9F=8E=A8=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index 03dbb64..1a75809 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -1011,7 +1011,7 @@ public function select( ->setDisabled($disabled); if ($defaults && ! $select instanceof StringSelect) { - $select->setDefaultValues($defaults); + $select = $select->setDefaultValues($defaults); } if ($id) { From f0e4b5b1fa461355f3f4b06ac4a353dea1d77522 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 22 Mar 2025 21:28:35 -0500 Subject: [PATCH 123/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20select=20defau?= =?UTF-8?q?lt=20value=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index 1a75809..a67d70e 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -1011,6 +1011,11 @@ public function select( ->setDisabled($disabled); if ($defaults && ! $select instanceof StringSelect) { + $defaults = collect($defaults)->map(fn ($value) => [ + 'id' => $value, + 'type' => $type, + ])->all(); + $select = $select->setDefaultValues($defaults); } From f3e7a3eeff42ee57211f38cca68d8edc93475b48 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 22 Mar 2025 22:05:09 -0500 Subject: [PATCH 124/146] =?UTF-8?q?=F0=9F=8E=A8=20Allow=20passing=20`defau?= =?UTF-8?q?lts`=20for=20string=20select=20menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index a67d70e..68cab7d 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -1047,21 +1047,28 @@ public function select( } } - foreach ($items as $key => $value) { - if (! is_array($value)) { - $select->addOption( - Option::new(is_int($key) ? $value : $key, $value) - ); + if ($items) { + $defaults = collect($defaults) + ->mapWithKeys(fn ($value) => [$value => true]) + ->all(); + + foreach ($items as $key => $value) { + if (! is_array($value)) { + $select->addOption( + Option::new(is_int($key) ? $value : $key, $value) + ->setDefault($defaults[$value] ?? false) + ); - continue; - } + continue; + } - $option = Option::new($value['label'] ?? $key, $value['value'] ?? $key) - ->setDescription($value['description'] ?? null) - ->setEmoji($value['emoji'] ?? null) - ->setDefault($value['default'] ?? false); + $option = Option::new($value['label'] ?? $key, $value['value'] ?? $key) + ->setDescription($value['description'] ?? null) + ->setEmoji($value['emoji'] ?? null) + ->setDefault($value['default'] ?? $defaults[$value['value'] ?? $key] ?? false); - $select->addOption($option); + $select->addOption($option); + } } $this->selects[] = $select; From 42b670e6c7bc3af976b3a483ab02c4db339db782 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 22 Mar 2025 22:12:16 -0500 Subject: [PATCH 125/146] =?UTF-8?q?=F0=9F=8E=A8=20Tidy=20up=20select=20def?= =?UTF-8?q?ault=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Discord/Message.php | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/Discord/Message.php b/src/Discord/Message.php index 68cab7d..b35c0bb 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -1010,12 +1010,11 @@ public function select( ->setMaxValues($maxValues) ->setDisabled($disabled); - if ($defaults && ! $select instanceof StringSelect) { - $defaults = collect($defaults)->map(fn ($value) => [ - 'id' => $value, - 'type' => $type, - ])->all(); + $defaults = $items + ? collect($defaults)->mapWithKeys(fn ($value) => [$value => true])->all() + : collect($defaults)->map(fn ($value) => ['id' => $value, 'type' => $type])->all(); + if ($defaults && ! $select instanceof StringSelect) { $select = $select->setDefaultValues($defaults); } @@ -1047,28 +1046,22 @@ public function select( } } - if ($items) { - $defaults = collect($defaults) - ->mapWithKeys(fn ($value) => [$value => true]) - ->all(); - - foreach ($items as $key => $value) { - if (! is_array($value)) { - $select->addOption( - Option::new(is_int($key) ? $value : $key, $value) - ->setDefault($defaults[$value] ?? false) - ); + foreach ($items as $key => $value) { + if (! is_array($value)) { + $select->addOption( + Option::new(is_int($key) ? $value : $key, $value) + ->setDefault($defaults[$value] ?? false) + ); - continue; - } + continue; + } - $option = Option::new($value['label'] ?? $key, $value['value'] ?? $key) - ->setDescription($value['description'] ?? null) - ->setEmoji($value['emoji'] ?? null) - ->setDefault($value['default'] ?? $defaults[$value['value'] ?? $key] ?? false); + $option = Option::new($value['label'] ?? $key, $value['value'] ?? $key) + ->setDescription($value['description'] ?? null) + ->setEmoji($value['emoji'] ?? null) + ->setDefault($value['default'] ?? $defaults[$value['value'] ?? $key] ?? false); - $select->addOption($option); - } + $select->addOption($option); } $this->selects[] = $select; From 4522097dca069783ee8d4ad4e8fcb1b34f28314a Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 11:07:14 -0500 Subject: [PATCH 126/146] =?UTF-8?q?=F0=9F=94=A7=20Add=20configuration=20fo?= =?UTF-8?q?r=20the=20logging=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 5 ++++- src/Logging/LoggingHandler.php | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/logging.php b/config/logging.php index 2655b1c..7bb3dde 100644 --- a/config/logging.php +++ b/config/logging.php @@ -62,8 +62,11 @@ 'driver' => 'monolog', 'handler' => Laracord\Logging\LoggingHandler::class, 'with' => [ - 'path' => laracord_path('logs/laracord.log'), + 'path' => env('LOG_PATH', laracord_path('logs/laracord.log')), 'level' => env('LOG_LEVEL', 'info'), + 'maxSize' => env('LOG_MAX_SIZE', 10), + 'maxFiles' => env('LOG_MAX_FILES', 5), + 'flushInterval' => env('LOG_FLUSH_INTERVAL', 60), ], ], diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index b18ef16..94f371e 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -32,7 +32,7 @@ class LoggingHandler extends AbstractProcessingHandler */ public function __construct( protected string $path, - protected int $maxSize = 10485760, + protected int $maxSize = 10, protected int $maxFiles = 5, protected float $flushInterval = 60, mixed $level = Level::Debug, @@ -40,6 +40,8 @@ public function __construct( ) { parent::__construct($level, $bubble); + $this->maxSize *= 1024 * 1024; + $this->initializeStream(); } From 852e4a64f29c641452a1764175b1110234386f33 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 15:20:58 -0500 Subject: [PATCH 127/146] =?UTF-8?q?=E2=9E=95=20Add=20`react/filesystem`=20?= =?UTF-8?q?to=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 88b7a58..e35af84 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "laravel-zero/framework": "^11.0", "laravel/sanctum": "^4.0", "react/async": "^4.2", + "react/filesystem": "0.2.x-dev", "react/http": "^1.9", "react/promise": "^3.0", "sebastian/environment": "*", From 715aad15cee40852d72bdc3d6126cf0fc5d6509e Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 15:22:26 -0500 Subject: [PATCH 128/146] =?UTF-8?q?=E2=9A=A1=20Utilize=20`react/filesystem?= =?UTF-8?q?`=20for=20handling=20file=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 4 +- src/Logging/LoggingHandler.php | 136 +++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 59 deletions(-) diff --git a/config/logging.php b/config/logging.php index 7bb3dde..6adf189 100644 --- a/config/logging.php +++ b/config/logging.php @@ -63,10 +63,10 @@ 'handler' => Laracord\Logging\LoggingHandler::class, 'with' => [ 'path' => env('LOG_PATH', laracord_path('logs/laracord.log')), - 'level' => env('LOG_LEVEL', 'info'), + 'level' => env('LOG_LEVEL', 'debug'), 'maxSize' => env('LOG_MAX_SIZE', 10), 'maxFiles' => env('LOG_MAX_FILES', 5), - 'flushInterval' => env('LOG_FLUSH_INTERVAL', 60), + 'flushInterval' => env('LOG_FLUSH_INTERVAL', 15), ], ], diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index 94f371e..fd4df33 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -8,14 +8,26 @@ use Monolog\Level; use Monolog\LogRecord; use React\EventLoop\TimerInterface; -use RuntimeException; +use React\Filesystem\AdapterInterface; +use React\Filesystem\Factory; +use React\Filesystem\Node\FileInterface; +use React\Filesystem\Node\NodeInterface; +use React\Filesystem\Node\NotExistInterface; +use React\Filesystem\Stat; + +use function React\Promise\all; class LoggingHandler extends AbstractProcessingHandler { /** - * The file handle. + * The file handler. */ - protected $handle = null; + protected ?FileInterface $handle = null; + + /** + * The filesystem adapter. + */ + protected AdapterInterface $filesystem; /** * The buffer of messages to write. @@ -34,7 +46,7 @@ public function __construct( protected string $path, protected int $maxSize = 10, protected int $maxFiles = 5, - protected float $flushInterval = 60, + protected float $flushInterval = 30, mixed $level = Level::Debug, bool $bubble = true, ) { @@ -42,7 +54,17 @@ public function __construct( $this->maxSize *= 1024 * 1024; + $this->filesystem = Factory::create(); + + $this->filesystem->detect(dirname($this->path)) + ->then(fn (NodeInterface $node) => $node instanceof NotExistInterface + ? $node->createDirectory() + : $node + ); + $this->initializeStream(); + + $this->flushTimer = Laracord::getLoop()->addPeriodicTimer($this->flushInterval, fn () => $this->flush()); } /** @@ -50,19 +72,15 @@ public function __construct( */ protected function initializeStream(): void { - $path = dirname($this->path); - - File::ensureDirectoryExists($path); - - $this->handle = fopen($this->path, 'a'); - - if ($this->handle === false) { - throw new RuntimeException("Could not open log file: {$this->path}"); - } - - stream_set_blocking($this->handle, false); - - $this->flushTimer = Laracord::getLoop()->addPeriodicTimer($this->flushInterval, fn () => $this->flush()); + $this->filesystem->detect($this->path) + ->then(function (NodeInterface $node) { + if ($node instanceof NotExistInterface) { + return $node->createFile(); + } + + return $node; + }) + ->then(fn (FileInterface $node) => $this->handle = $node); } /** @@ -70,27 +88,17 @@ protected function initializeStream(): void */ protected function flush(): void { - if (empty($this->buffer)) { + if (! $this->handle || blank($this->buffer)) { return; } - $this->rotate(); - - if (! flock($this->handle, LOCK_EX | LOCK_NB)) { - return; - } - - try { - foreach ($this->buffer as $message) { - fwrite($this->handle, $message); - } - - fflush($this->handle); - } finally { - flock($this->handle, LOCK_UN); - } + $buffer = implode('', $this->buffer); $this->buffer = []; + + $this->handle + ->putContents($buffer, FILE_APPEND) + ->then(fn () => $this->rotate()); } /** @@ -98,26 +106,44 @@ protected function flush(): void */ protected function rotate(): void { - if (! file_exists($this->path) || filesize($this->path) < $this->maxSize) { - return; - } - - fclose($this->handle); - - for ($i = $this->maxFiles - 1; $i >= 0; $i--) { - $existing = $i === 0 ? $this->path : "{$this->path}.{$i}"; - $new = "{$this->path}.".($i + 1); - - if (! file_exists($existing)) { - continue; - } - - $i === $this->maxFiles - 1 - ? unlink($existing) - : rename($existing, $new); - } - - $this->initializeStream(); + $this->filesystem + ->file($this->path) + ->stat() + ->then(function (?Stat $stat) { + if (! $stat || $stat->size() < $this->maxSize) { + return; + } + + $promises = []; + + for ($i = $this->maxFiles - 1; $i >= 0; $i--) { + $existing = $i === 0 ? $this->path : "{$this->path}.{$i}"; + $new = "{$this->path}.".($i + 1); + + $promises[] = $this->filesystem->detect($existing) + ->then(function (NodeInterface $node) use ($new, $i) { + if ($node instanceof NotExistInterface) { + return; + } + + if ($i === $this->maxFiles - 1) { + return $node->unlink(); + } + + return $node->getContents() + ->then(fn (string $contents) => $this->filesystem->detect($new) + ->then(fn (NotExistInterface $file) => $file->createFile()) + ->then(fn (FileInterface $file) => $file + ->putContents($contents) + ->then(fn () => $node->unlink()) + ) + ); + }); + } + + return all($promises); + }) + ->then(fn () => $this->initializeStream()); } /** @@ -138,9 +164,5 @@ public function close(): void } $this->flush(); - - if ($this->handle) { - fclose($this->handle); - } } } From 298202dd574dcc735af5ab9b5d6c311be3c91ffb Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 17:03:51 -0500 Subject: [PATCH 129/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Use?= =?UTF-8?q?=20the=20Message=20builder=20instance=20from=20the=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasDiscord.php | 2 +- src/Commands/AbstractCommand.php | 10 +++++----- src/Discord/Message.php | 2 -- src/HasLaracord.php | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Bot/Concerns/HasDiscord.php b/src/Bot/Concerns/HasDiscord.php index c3f3632..47f40c4 100644 --- a/src/Bot/Concerns/HasDiscord.php +++ b/src/Bot/Concerns/HasDiscord.php @@ -197,7 +197,7 @@ public function discord(): ?Discord */ public function message(string $content = ''): Message { - return Message::make($this) + return app(Message::class) ->content($content); } } diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index ed00edf..32f41a5 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -7,6 +7,7 @@ use Discord\Parts\User\User; use Laracord\Concerns\HasHandler; use Laracord\Discord\Concerns\HasModal; +use Laracord\Discord\Message; use Laracord\HasLaracord; abstract class AbstractCommand @@ -107,14 +108,13 @@ public function interactions(): array } /** - * Build an embed for use in a Discord message. - * - * @param string $content - * @return \Laracord\Discord\Message + * {@inheritdoc} */ public function message($content = '') { - return $this->bot->message($content)->routePrefix($this->getName()); + return app(Message::class) + ->content($content) + ->routePrefix($this->getName()); } /** diff --git a/src/Discord/Message.php b/src/Discord/Message.php index b35c0bb..29f9899 100644 --- a/src/Discord/Message.php +++ b/src/Discord/Message.php @@ -207,8 +207,6 @@ class Message /** * Create a new Discord message instance. - * - * @return void */ public function __construct(?Laracord $bot) { diff --git a/src/HasLaracord.php b/src/HasLaracord.php index 6157501..a7a8df1 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -58,7 +58,7 @@ public function logger(): LogManager */ public function message($content = '') { - return Message::make($this->bot()) + return app(Message::class) ->content($content); } } From e2317e09ad084a4124e4b734b4a62d3f36cfae90 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 18:22:18 -0500 Subject: [PATCH 130/146] =?UTF-8?q?=F0=9F=8E=A8=20Clone=20the=20message=20?= =?UTF-8?q?instances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasDiscord.php | 2 +- src/Commands/AbstractCommand.php | 2 +- src/HasLaracord.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bot/Concerns/HasDiscord.php b/src/Bot/Concerns/HasDiscord.php index 47f40c4..e1e3fe5 100644 --- a/src/Bot/Concerns/HasDiscord.php +++ b/src/Bot/Concerns/HasDiscord.php @@ -197,7 +197,7 @@ public function discord(): ?Discord */ public function message(string $content = ''): Message { - return app(Message::class) + return clone app(Message::class) ->content($content); } } diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index 32f41a5..c510af4 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -112,7 +112,7 @@ public function interactions(): array */ public function message($content = '') { - return app(Message::class) + return clone app(Message::class) ->content($content) ->routePrefix($this->getName()); } diff --git a/src/HasLaracord.php b/src/HasLaracord.php index a7a8df1..d18740b 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -58,7 +58,7 @@ public function logger(): LogManager */ public function message($content = '') { - return app(Message::class) + return clone app(Message::class) ->content($content); } } From a1648ddaab8928d5758bfce1e4e4749de0965efa Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 18:22:30 -0500 Subject: [PATCH 131/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20return=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasAsync.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bot/Concerns/HasAsync.php b/src/Bot/Concerns/HasAsync.php index ec4fdea..839f09c 100644 --- a/src/Bot/Concerns/HasAsync.php +++ b/src/Bot/Concerns/HasAsync.php @@ -5,13 +5,14 @@ use Exception; use React\EventLoop\LoopInterface; use React\Promise\Promise; +use React\Promise\PromiseInterface; trait HasAsync { /** * Perform an asynchronous operation. */ - public static function handleAsync(callable $callback): Promise + public static function handleAsync(callable $callback): PromiseInterface { return new Promise(function ($resolve, $reject) use ($callback) { if (! $loop = app(LoopInterface::class)) { @@ -31,7 +32,7 @@ public static function handleAsync(callable $callback): Promise /** * Perform an asynchronous operation. */ - public function async(callable $callback): Promise + public function async(callable $callback): PromiseInterface { return static::handleAsync($callback); } From ee1bc166b4aaf7e9e58565effd595a3e016bef0d Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 18:23:03 -0500 Subject: [PATCH 132/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20log=20rotation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/logging.php | 2 +- src/Logging/LoggingHandler.php | 45 ++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/config/logging.php b/config/logging.php index 6adf189..1622dd1 100644 --- a/config/logging.php +++ b/config/logging.php @@ -66,7 +66,7 @@ 'level' => env('LOG_LEVEL', 'debug'), 'maxSize' => env('LOG_MAX_SIZE', 10), 'maxFiles' => env('LOG_MAX_FILES', 5), - 'flushInterval' => env('LOG_FLUSH_INTERVAL', 15), + 'flushInterval' => env('LOG_FLUSH_INTERVAL', 30), ], ], diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index fd4df33..b5cdd33 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -116,23 +116,31 @@ protected function rotate(): void $promises = []; - for ($i = $this->maxFiles - 1; $i >= 0; $i--) { - $existing = $i === 0 ? $this->path : "{$this->path}.{$i}"; + $oldest = "{$this->path}.{$this->maxFiles}"; + + $promises[] = $this->filesystem->detect($oldest) + ->then(function (NodeInterface $node) { + if (! ($node instanceof NotExistInterface)) { + return $node->unlink(); + } + }); + + for ($i = $this->maxFiles - 1; $i >= 1; $i--) { + $existing = "{$this->path}.{$i}"; $new = "{$this->path}.".($i + 1); $promises[] = $this->filesystem->detect($existing) - ->then(function (NodeInterface $node) use ($new, $i) { + ->then(function (NodeInterface $node) use ($new) { if ($node instanceof NotExistInterface) { return; } - if ($i === $this->maxFiles - 1) { - return $node->unlink(); - } - return $node->getContents() ->then(fn (string $contents) => $this->filesystem->detect($new) - ->then(fn (NotExistInterface $file) => $file->createFile()) + ->then(fn (NodeInterface $file) => $file instanceof NotExistInterface + ? $file->createFile() + : $file + ) ->then(fn (FileInterface $file) => $file ->putContents($contents) ->then(fn () => $node->unlink()) @@ -141,6 +149,27 @@ protected function rotate(): void }); } + $promises[] = $this->filesystem->detect($this->path) + ->then(function (NodeInterface $node) { + if ($node instanceof NotExistInterface) { + return; + } + + $new = "{$this->path}.1"; + + return $node->getContents() + ->then(fn (string $contents) => $this->filesystem->detect($new) + ->then(fn (NodeInterface $file) => $file instanceof NotExistInterface + ? $file->createFile() + : $file + ) + ->then(fn (FileInterface $file) => $file + ->putContents($contents) + ->then(fn () => $node->unlink()) + ) + ); + }); + return all($promises); }) ->then(fn () => $this->initializeStream()); From d6949658dd7919dacd4485fed8de879e468ab164 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 18:24:23 -0500 Subject: [PATCH 133/146] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20variable=20nam?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Logging/LoggingHandler.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Logging/LoggingHandler.php b/src/Logging/LoggingHandler.php index b5cdd33..805f32a 100644 --- a/src/Logging/LoggingHandler.php +++ b/src/Logging/LoggingHandler.php @@ -127,16 +127,16 @@ protected function rotate(): void for ($i = $this->maxFiles - 1; $i >= 1; $i--) { $existing = "{$this->path}.{$i}"; - $new = "{$this->path}.".($i + 1); + $latest = "{$this->path}.".($i + 1); $promises[] = $this->filesystem->detect($existing) - ->then(function (NodeInterface $node) use ($new) { + ->then(function (NodeInterface $node) use ($latest) { if ($node instanceof NotExistInterface) { return; } return $node->getContents() - ->then(fn (string $contents) => $this->filesystem->detect($new) + ->then(fn (string $contents) => $this->filesystem->detect($latest) ->then(fn (NodeInterface $file) => $file instanceof NotExistInterface ? $file->createFile() : $file @@ -155,10 +155,10 @@ protected function rotate(): void return; } - $new = "{$this->path}.1"; + $latest = "{$this->path}.1"; return $node->getContents() - ->then(fn (string $contents) => $this->filesystem->detect($new) + ->then(fn (string $contents) => $this->filesystem->detect($latest) ->then(fn (NodeInterface $file) => $file instanceof NotExistInterface ? $file->createFile() : $file From 67ee0328931595fe06c1b9aced2439f51eccd212 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 24 Mar 2025 19:57:57 -0500 Subject: [PATCH 134/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Gra?= =?UTF-8?q?cefully=20handle=20stale=20packages=20in=20the=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 7a33278..3f3d7f8 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -83,6 +83,10 @@ public function register() parent::register(); foreach ($this->app->make(BasePackageManifest::class)->providers() as $provider) { + if (! class_exists($provider)) { + continue; + } + $this->app->register($provider); } From 024570babf01968cd5813e76b346e20a76948341 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 17 May 2025 13:44:06 -0500 Subject: [PATCH 135/146] =?UTF-8?q?=F0=9F=8E=A8=20Add=20type=20to=20`Conte?= =?UTF-8?q?xt::getUser`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/Middleware/Context.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Commands/Middleware/Context.php b/src/Commands/Middleware/Context.php index 0b2670f..40a5b8d 100644 --- a/src/Commands/Middleware/Context.php +++ b/src/Commands/Middleware/Context.php @@ -4,6 +4,7 @@ use Discord\Parts\Channel\Message; use Discord\Parts\Interactions\Interaction; +use Discord\Parts\User\User; use Laracord\Commands\Contracts\Command; use Laracord\Commands\Contracts\ContextMenu; use Laracord\Commands\Contracts\SlashCommand; @@ -72,7 +73,7 @@ public function isRawInteraction(): bool /** * Get the user from the context. */ - public function getUser() + public function getUser(): ?User { if ($this->isMessage()) { return $this->source->author; From 308a076dd2fe30615cf92ec0096e26d2d4753370 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 17 May 2025 13:44:25 -0500 Subject: [PATCH 136/146] =?UTF-8?q?=F0=9F=8E=A8=20Disable=20the=20HTTP=20s?= =?UTF-8?q?erver=20when=20not=20on=20shard=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/BootCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/BootCommand.php b/src/Console/Commands/BootCommand.php index 3bf2bdf..8dd6b9a 100644 --- a/src/Console/Commands/BootCommand.php +++ b/src/Console/Commands/BootCommand.php @@ -45,7 +45,9 @@ public function handle(Laracord $bot): void count: $this->option('shard-count') ); - $bot->disableHttpServer(); + if ($bot->getShardId() > 0) { + $bot->disableHttpServer(); + } } $bot->boot(); From a5407c29b2605249e854bbb7a32855e2b254c8d1 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 17 May 2025 13:44:53 -0500 Subject: [PATCH 137/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20the=20help=20comma?= =?UTF-8?q?nd=20when=20it=20is=20not=20paged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/HelpCommand.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Commands/HelpCommand.php b/src/Commands/HelpCommand.php index 53c6a75..edcbec7 100644 --- a/src/Commands/HelpCommand.php +++ b/src/Commands/HelpCommand.php @@ -103,20 +103,25 @@ public function show(Message|Interaction $context, int $page = 1): void $fields[' '] = ''; } - $pages = ceil($commands->count() / static::$perPage); + $pages = min(1, ceil($commands->count() / static::$perPage)); $previous = max(1, $page - 1); $next = min($pages, $page + 1); $message = sprintf(static::$message, $commands->count()); - $this + $embed = $this ->message($message) ->title(static::$title) - ->fields($fields) - ->button('←', route: "show:{$previous}", style: 'secondary', disabled: $page <= 1, hidden: $pages === 1) - ->button('→', route: "show:{$next}", style: 'secondary', disabled: $page >= $pages, hidden: $pages === 1) - ->footerText("Page {$page} of {$pages}") - ->editOrReply($context); + ->fields($fields); + + if ($pages > 1) { + $embed + ->button('←', route: "show:{$previous}", style: 'secondary', disabled: $page <= 1) + ->button('→', route: "show:{$next}", style: 'secondary', disabled: $page >= $pages) + ->footerText("Page {$page} of {$pages}"); + } + + $embed->editOrReply($context); } /** From 0f8b1b14fa33833c82a1600b63de9edf181f43bc Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 17 May 2025 13:48:17 -0500 Subject: [PATCH 138/146] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20the=20help=20comma?= =?UTF-8?q?nd=20when=20it=20is=20not=20paged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/HelpCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/HelpCommand.php b/src/Commands/HelpCommand.php index edcbec7..93176f9 100644 --- a/src/Commands/HelpCommand.php +++ b/src/Commands/HelpCommand.php @@ -103,7 +103,7 @@ public function show(Message|Interaction $context, int $page = 1): void $fields[' '] = ''; } - $pages = min(1, ceil($commands->count() / static::$perPage)); + $pages = max(1, ceil($commands->count() / static::$perPage)); $previous = max(1, $page - 1); $next = min($pages, $page + 1); From c08bab6b604738cf471f3a5b74036cb493e58b50 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 04:48:10 -0500 Subject: [PATCH 139/146] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20to=20Lar?= =?UTF-8?q?avel=20v12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index e35af84..3e25506 100644 --- a/composer.json +++ b/composer.json @@ -15,22 +15,22 @@ "require": { "php": "^8.2", "guzzlehttp/guzzle": "^7.5", - "illuminate/auth": "^11.0", - "illuminate/cookie": "^11.0", - "illuminate/database": "^11.0", - "illuminate/encryption": "^11.0", - "illuminate/hashing": "^11.0", - "illuminate/http": "^11.0", - "illuminate/log": "^11.0", - "illuminate/mail": "^11.0", - "illuminate/queue": "^11.0", - "illuminate/routing": "^11.0", - "illuminate/session": "^11.0", - "illuminate/translation": "^11.0", - "illuminate/validation": "^11.0", - "illuminate/view": "^11.0", + "illuminate/auth": "^12.0", + "illuminate/cookie": "^12.0", + "illuminate/database": "^12.0", + "illuminate/encryption": "^12.0", + "illuminate/hashing": "^12.0", + "illuminate/http": "^12.0", + "illuminate/log": "^12.0", + "illuminate/mail": "^12.0", + "illuminate/queue": "^12.0", + "illuminate/routing": "^12.0", + "illuminate/session": "^12.0", + "illuminate/translation": "^12.0", + "illuminate/validation": "^12.0", + "illuminate/view": "^12.0", "intonate/tinker-zero": "^1.2", - "laravel-zero/framework": "^11.0", + "laravel-zero/framework": "^12.0", "laravel/sanctum": "^4.0", "react/async": "^4.2", "react/filesystem": "0.2.x-dev", From f8241c976c6daa391764751aaee97cb74cdde5ec Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 04:48:32 -0500 Subject: [PATCH 140/146] =?UTF-8?q?=F0=9F=94=A7=20Add=20the=20`ContextServ?= =?UTF-8?q?iceProvider`=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LaracordServiceProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 3f3d7f8..03fc594 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -54,6 +54,7 @@ abstract class LaracordServiceProvider extends AggregateServiceProvider \Illuminate\Session\SessionServiceProvider::class, \Illuminate\Mail\MailServiceProvider::class, \Illuminate\Auth\AuthServiceProvider::class, + \Illuminate\Log\Context\ContextServiceProvider::class, \Laracord\Http\Providers\RouteServiceProvider::class, \Intonate\TinkerZero\TinkerZeroServiceProvider::class, ]; From 9c7788205c8233d8b8a690bd52fc5146b1ec0f1d Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 04:50:25 -0500 Subject: [PATCH 141/146] =?UTF-8?q?=F0=9F=8E=A8=20Do=20not=20needlessly=20?= =?UTF-8?q?cache=20registered=20options=20on=20slash=20command=20instances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/SlashCommand.php | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Commands/SlashCommand.php b/src/Commands/SlashCommand.php index 6d89952..4c90978 100644 --- a/src/Commands/SlashCommand.php +++ b/src/Commands/SlashCommand.php @@ -23,13 +23,6 @@ abstract class SlashCommand extends ApplicationCommand implements SlashCommandCo */ protected $options = []; - /** - * The registered command options. - * - * @var array - */ - protected $registeredOptions = []; - /** * The parsed command options. * @@ -53,8 +46,8 @@ public function create(): DiscordCommand $command = $command->setDefaultMemberPermissions($permissions); } - if ($this->getRegisteredOptions()) { - foreach ($this->getRegisteredOptions() as $option) { + if ($options = $this->getRegisteredOptions()) { + foreach ($options as $option) { $command = $command->addOption($option); } } @@ -246,17 +239,13 @@ public function getSignature(): string */ public function getRegisteredOptions(): ?array { - if ($this->registeredOptions) { - return $this->registeredOptions; - } - $options = collect($this->options())->merge($this->options); if ($options->isEmpty()) { - return $this->registeredOptions = null; + return null; } - return $this->registeredOptions = $options->map(fn ($option) => $option instanceof Option + return $options->map(fn ($option) => $option instanceof Option ? $option : new Option($this->discord(), $option) )->map(fn ($option) => $option->setName(Str::slug($option->name)))->all(); From 912d829041b6e9557c6636ad32689a99da373001 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 06:06:07 -0500 Subject: [PATCH 142/146] =?UTF-8?q?=E2=9C=A8=20Add=20initial=20support=20f?= =?UTF-8?q?or=20static=20files=20to=20the=20HTTP=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/Handlers/StaticFileHandler.php | 161 ++++++++++++++++++++++++ src/Http/HttpServer.php | 22 ++++ 2 files changed, 183 insertions(+) create mode 100644 src/Http/Handlers/StaticFileHandler.php diff --git a/src/Http/Handlers/StaticFileHandler.php b/src/Http/Handlers/StaticFileHandler.php new file mode 100644 index 0000000..ed4b6ab --- /dev/null +++ b/src/Http/Handlers/StaticFileHandler.php @@ -0,0 +1,161 @@ + 'audio/aac', + 'abw' => 'application/x-abiword', + 'apng' => 'image/apng', + 'arc' => 'application/x-freearc', + 'avif' => 'image/avif', + 'avi' => 'video/x-msvideo', + 'azw' => 'application/vnd.amazon.ebook', + 'bin' => 'application/octet-stream', + 'bmp' => 'image/bmp', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'cda' => 'application/x-cdf', + 'csh' => 'application/x-csh', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'eot' => 'application/vnd.ms-fontobject', + 'epub' => 'application/epub+zip', + 'gz' => 'application/gzip', + 'gif' => 'image/gif', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/vnd.microsoft.icon', + 'ics' => 'text/calendar', + 'jar' => 'application/java-archive', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'jsonld' => 'application/ld+json', + 'md' => 'text/markdown', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mjs' => 'text/javascript', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'opus' => 'audio/ogg', + 'otf' => 'font/otf', + 'png' => 'image/png', + 'pdf' => 'application/pdf', + 'php' => 'application/x-httpd-php', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'rar' => 'application/vnd.rar', + 'rtf' => 'application/rtf', + 'sh' => 'application/x-sh', + 'svg' => 'image/svg+xml', + 'tar' => 'application/x-tar', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'ts' => 'video/mp2t', + 'ttf' => 'font/ttf', + 'txt' => 'text/plain', + 'vsd' => 'application/vnd.visio', + 'wav' => 'audio/wav', + 'weba' => 'audio/webm', + 'webm' => 'video/webm', + 'webmanifest' => 'application/manifest+json', + 'webp' => 'image/webp', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + 'xhtml' => 'application/xhtml+xml', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'zip' => 'application/zip', + '3gp' => 'video/3gpp', + '3g2' => 'video/3gpp2', + '7z' => 'application/x-7z-compressed', + ]; + + /** + * The index files to check for. + */ + protected array $indexFiles = [ + 'index.html', + 'index.htm', + ]; + + /** + * Handle the static file request. + */ + public function handle(ServerRequestInterface $request): ?Response + { + $path = $this->resolvePath($request->getUri()->getPath()); + + if (! $path || ! File::exists($path)) { + return null; + } + + if (! File::isReadable($path)) { + return new Response( + 403, + ['Content-Type' => 'text/plain'], + 'Forbidden' + ); + } + + return new Response( + 200, + ['Content-Type' => $this->getContentType($path)], + File::get($path) + ); + } + + /** + * Resolve the file path. + */ + protected function resolvePath(string $requestPath): ?string + { + $path = public_path(ltrim($requestPath, '/')); + + if (! is_dir($path)) { + return $path; + } + + foreach ($this->indexFiles as $index) { + if (File::exists($file = "{$path}/{$index}")) { + return $file; + } + } + + return null; + } + + /** + * Get the content type for a file. + */ + protected function getContentType(string $path): string + { + $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + + return static::$mimeTypes[$extension] ?? 'application/octet-stream'; + } +} diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index 15fca70..e1ba329 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; use Laracord\Bot\Hook; +use Laracord\Http\Handlers\StaticFileHandler; use Laracord\Laracord; use Psr\Http\Message\ServerRequestInterface; use React\Http\HttpServer as Server; @@ -38,6 +39,11 @@ class HttpServer */ protected bool $booted = false; + /** + * The static file handler instance. + */ + protected ?StaticFileHandler $staticFileHandler = null; + /** * Create a new server instance. */ @@ -101,6 +107,10 @@ public function getServer(): Server } return $this->server = new Server($this->bot->getLoop(), function (ServerRequestInterface $request) { + if ($response = $this->handleStaticFile($request)) { + return $response; + } + $headers = $request->getHeaders(); $request = Request::create( @@ -146,6 +156,18 @@ public function getServer(): Server }); } + /** + * Handle a static file request. + */ + protected function handleStaticFile(ServerRequestInterface $request): ?Response + { + if (! $this->staticFileHandler) { + $this->staticFileHandler = new StaticFileHandler; + } + + return $this->staticFileHandler->handle($request); + } + /** * Handle an error response. */ From 9d6fca35b8b9e6d9084c22a68744a77de57b356e Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 06:56:59 -0500 Subject: [PATCH 143/146] =?UTF-8?q?=E2=9A=A1=20Use=20ReactPHP=20Filesystem?= =?UTF-8?q?=20for=20static=20file=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Http/Handlers/StaticFileHandler.php | 166 ++++++++++++++++++++---- src/Http/HttpServer.php | 121 +++++++++++------ 2 files changed, 221 insertions(+), 66 deletions(-) diff --git a/src/Http/Handlers/StaticFileHandler.php b/src/Http/Handlers/StaticFileHandler.php index ed4b6ab..3e2e755 100644 --- a/src/Http/Handlers/StaticFileHandler.php +++ b/src/Http/Handlers/StaticFileHandler.php @@ -2,9 +2,16 @@ namespace Laracord\Http\Handlers; -use Illuminate\Support\Facades\File; use Psr\Http\Message\ServerRequestInterface; +use React\Filesystem\Factory; +use React\Filesystem\Node\FileInterface; +use React\Filesystem\Node\NodeInterface; +use React\Filesystem\Node\NotExistInterface; use React\Http\Message\Response; +use React\Promise\PromiseInterface; +use Throwable; + +use function React\Promise\resolve; class StaticFileHandler { @@ -103,30 +110,110 @@ class StaticFileHandler 'index.htm', ]; + /** + * The filesystem instance. + */ + protected $filesystem; + + /** + * Cache for content types. + */ + protected static array $contentTypeCache = []; + + /** + * Create a new static file handler instance. + */ + public function __construct() + { + $this->filesystem = Factory::create(); + } + /** * Handle the static file request. */ - public function handle(ServerRequestInterface $request): ?Response + public function handle(ServerRequestInterface $request): PromiseInterface { $path = $this->resolvePath($request->getUri()->getPath()); - if (! $path || ! File::exists($path)) { - return null; + if (! $path) { + return resolve(null); } - if (! File::isReadable($path)) { - return new Response( - 403, - ['Content-Type' => 'text/plain'], - 'Forbidden' - ); + return $this->filesystem->detect($path) + ->then(fn (NodeInterface $node) => $this->handleNode($node, $path)) + ->otherwise(function (Throwable $e) { + report($e); + + return $this->createErrorResponse(); + }); + } + + /** + * Handle a filesystem node (file or directory). + */ + protected function handleNode(NodeInterface $node, string $path): ?PromiseInterface + { + if ($node instanceof NotExistInterface) { + return resolve(null); } - return new Response( - 200, - ['Content-Type' => $this->getContentType($path)], - File::get($path) - ); + return $node instanceof FileInterface + ? $this->handleFile($node, $path) + : $this->handleDirectory($node, $path); + } + + /** + * Handle a file request. + */ + protected function handleFile(FileInterface $file, string $path): PromiseInterface + { + return $file + ->getContents() + ->then(function (string $contents) use ($path) { + $headers = [ + 'Content-Type' => $this->getContentType($path), + 'Content-Length' => strlen($contents), + 'Cache-Control' => 'public, max-age=3600', + 'ETag' => '"'.md5($contents).'"', + ]; + + if ($this->isWebAsset($path)) { + $headers['Access-Control-Allow-Origin'] = '*'; + } + + return new Response(200, $headers, $contents); + }) + ->otherwise(fn () => null); + } + + /** + * Handle a directory request by looking for index files. + */ + protected function handleDirectory(NodeInterface $directory, string $path): PromiseInterface + { + return $this->findIndexFile($path, 0); + } + + /** + * Recursively find and serve index files. + */ + protected function findIndexFile(string $path, int $index): PromiseInterface + { + if ($index >= count($this->indexFiles)) { + return resolve(null); + } + + $indexPath = rtrim($path, '/').'/'.$this->indexFiles[$index]; + + return $this->filesystem->detect($indexPath) + ->then(function (NodeInterface $node) use ($indexPath, $path, $index) { + if ($node instanceof FileInterface) { + return $this->handleFile($node, $indexPath); + } + + return $this->findIndexFile($path, $index + 1); + }) + ->otherwise(fn () => $this->findIndexFile($path, $index + 1)); } /** @@ -134,28 +221,57 @@ public function handle(ServerRequestInterface $request): ?Response */ protected function resolvePath(string $requestPath): ?string { - $path = public_path(ltrim($requestPath, '/')); + $requestPath = ltrim($requestPath, '/'); - if (! is_dir($path)) { - return $path; + if ( + str_contains($requestPath, '..') || + str_contains($requestPath, '\\') || + str_starts_with(basename($requestPath), '.') + ) { + return null; } - foreach ($this->indexFiles as $index) { - if (File::exists($file = "{$path}/{$index}")) { - return $file; - } - } + $path = public_path($requestPath); - return null; + return $path ?: null; } /** * Get the content type for a file. */ protected function getContentType(string $path): string + { + if (! isset(static::$contentTypeCache[$path])) { + $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + + static::$contentTypeCache[$path] = static::$mimeTypes[$extension] ?? 'application/octet-stream'; + } + + return static::$contentTypeCache[$path]; + } + + /** + * Check if the file is a common web asset that should have CORS headers. + */ + protected function isWebAsset(string $path): bool { $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - return static::$mimeTypes[$extension] ?? 'application/octet-stream'; + return in_array($extension, [ + 'css', 'js', 'mjs', 'json', 'woff', 'woff2', 'ttf', 'otf', 'eot', + 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', + ]); + } + + /** + * Create a standardized error response. + */ + protected function createErrorResponse(int $status = 500, string $message = 'Internal Server Error'): Response + { + return new Response( + $status, + ['Content-Type' => 'text/plain'], + $message + ); } } diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index e1ba329..cab8e4c 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -13,8 +13,10 @@ use Psr\Http\Message\ServerRequestInterface; use React\Http\HttpServer as Server; use React\Http\Message\Response; +use React\Promise\PromiseInterface; use React\Socket\SocketServer; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Throwable; class HttpServer @@ -107,65 +109,102 @@ public function getServer(): Server } return $this->server = new Server($this->bot->getLoop(), function (ServerRequestInterface $request) { - if ($response = $this->handleStaticFile($request)) { - return $response; - } - - $headers = $request->getHeaders(); - - $request = Request::create( - $request->getUri()->getPath(), - $request->getMethod(), - $request->getQueryParams(), - $request->getCookieParams(), - [], - $request->getServerParams(), - $request->getBody()->getContents() - ); + return $this->handleStaticFile($request) + ->then(function ($response) use ($request) { + if ($response !== null) { + return $response; + } + + return $this->handleLaravelRequest($request); + }) + ->otherwise(fn (Throwable $e) => $this->handleError($e)); + }); + } - $request->headers->replace($headers); + /** + * Handle a Laravel request through the kernel. + */ + protected function handleLaravelRequest(ServerRequestInterface $request): Response + { + $headers = $request->getHeaders(); + + $request = Request::create( + $request->getUri()->getPath(), + $request->getMethod(), + $request->getQueryParams(), + $request->getCookieParams(), + [], + $request->getServerParams(), + $request->getBody()->getContents() + ); + $request->headers->replace($headers); - $this->bot->app->instance('request', $request); + $this->bot->app->instance('request', $request); - $this->bot->withMiddleware(function (Middleware $middleware) { - $middleware - ->remove([\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class]) - ->append([\Laracord\Http\Middleware\FlushState::class]) - ->api([ - \Laracord\Http\Middleware\AuthorizeToken::class, - ]) - ->alias([ - 'auth.token' => \Laracord\Http\Middleware\AuthorizeToken::class, - ]); - }); + $this->configureMiddleware(); - /** @var \Laracord\Http\Kernel $kernel */ - $kernel = $this->bot->app->make(Kernel::class); + /** @var \Laracord\Http\Kernel $kernel */ + $kernel = $this->bot->app->make(Kernel::class); - try { - $kernel->terminate($request, $response = $kernel->handle($request)); - } catch (Throwable $e) { - return $this->handleError($e); - } + try { + $response = $kernel->handle($request); + $kernel->terminate($request, $response); return new Response( $response->getStatusCode(), $response->headers->allPreserveCase(), - $response->getContent() ?: ($response instanceof BinaryFileResponse ? $response->getFile()->getContent() : false) ?: '' + $this->getResponseContent($response) ); + } catch (Throwable $e) { + return $this->handleError($e); + } + } + + /** + * Configure the Laravel middleware for the request. + */ + protected function configureMiddleware(): void + { + $this->bot->withMiddleware(function (Middleware $middleware) { + $middleware + ->remove([\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class]) + ->append([\Laracord\Http\Middleware\FlushState::class]) + ->api([ + \Laracord\Http\Middleware\AuthorizeToken::class, + ]) + ->alias([ + 'auth.token' => \Laracord\Http\Middleware\AuthorizeToken::class, + ]); }); } + /** + * Get the response content from a Laravel response. + */ + protected function getResponseContent(SymfonyResponse $response): string + { + if ($response->getContent()) { + return $response->getContent(); + } + + if ($response instanceof BinaryFileResponse) { + return $response->getFile()->getContent(); + } + + return ''; + } + /** * Handle a static file request. */ - protected function handleStaticFile(ServerRequestInterface $request): ?Response + protected function handleStaticFile(ServerRequestInterface $request): PromiseInterface { if (! $this->staticFileHandler) { $this->staticFileHandler = new StaticFileHandler; } - return $this->staticFileHandler->handle($request); + return $this->staticFileHandler->handle($request) + ->otherwise(fn () => null); } /** @@ -173,10 +212,10 @@ protected function handleStaticFile(ServerRequestInterface $request): ?Response */ protected function handleError(Throwable $e): Response { - $response = 'Internal Server Error'; + $message = 'Internal Server Error'; if (! app()->isProduction()) { - $response = Str::finish($response, ": {$e->getMessage()}"); + $message = Str::finish($message, ": {$e->getMessage()}"); } report($e); @@ -184,7 +223,7 @@ protected function handleError(Throwable $e): Response return new Response( 500, ['Content-Type' => 'application/json'], - json_encode(['code' => 500, 'message' => $response]) + json_encode(['code' => 500, 'message' => $message]) ); } From f61a7ec5be6a0c68489b07a16bf5d6c51dd8787c Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 08:00:11 -0500 Subject: [PATCH 144/146] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Ren?= =?UTF-8?q?ame=20`Services`=20to=20`Tasks`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bot/Concerns/HasServices.php | 93 ----------- src/Bot/Concerns/HasTasks.php | 93 +++++++++++ src/Bot/Hook.php | 4 +- ...iceMakeCommand.php => TaskMakeCommand.php} | 16 +- .../stubs/{service.stub => task.stub} | 10 +- src/Facades/Laracord.php | 10 +- src/Laracord.php | 10 +- src/LaracordServiceProvider.php | 5 +- src/Services/Contracts/Service.php | 8 - src/Services/Service.php | 152 +---------------- src/Tasks/Contracts/Task.php | 8 + .../Exceptions/InvalidTaskInterval.php} | 6 +- src/Tasks/Task.php | 153 ++++++++++++++++++ 13 files changed, 291 insertions(+), 277 deletions(-) delete mode 100644 src/Bot/Concerns/HasServices.php create mode 100644 src/Bot/Concerns/HasTasks.php rename src/Console/Commands/{ServiceMakeCommand.php => TaskMakeCommand.php} (77%) rename src/Console/Commands/stubs/{service.stub => task.stub} (57%) delete mode 100644 src/Services/Contracts/Service.php create mode 100644 src/Tasks/Contracts/Task.php rename src/{Services/Exceptions/InvalidServiceInterval.php => Tasks/Exceptions/InvalidTaskInterval.php} (53%) create mode 100644 src/Tasks/Task.php diff --git a/src/Bot/Concerns/HasServices.php b/src/Bot/Concerns/HasServices.php deleted file mode 100644 index 2824fda..0000000 --- a/src/Bot/Concerns/HasServices.php +++ /dev/null @@ -1,93 +0,0 @@ -services as $service) { - if (! $service->isEnabled()) { - continue; - } - - $this->services[$service::class] = $service->boot(); - } - - $this->callHook(Hook::AFTER_SERVICES_REGISTERED); - - return $this; - } - - /** - * Register a service. - */ - public function registerService(Service|string $service): self - { - if (is_string($service)) { - $service = $service::make(); - } - - if (! is_subclass_of($service, Service::class)) { - $class = $service::class; - - throw new InvalidArgumentException("Class [{$class}] is not a valid service."); - } - - $this->services[$service::class] = $service; - - return $this; - } - - /** - * Register multiple services. - */ - public function registerServices(array $services): self - { - foreach ($services as $service) { - $this->registerService($service); - } - - return $this; - } - - /** - * Discover services in a path. - */ - public function discoverServices(string $in, string $for): self - { - foreach ($this->discover(Service::class, $in, $for) as $service) { - $this->registerService($service); - } - - return $this; - } - - /** - * Get the registered services. - */ - public function getServices(): array - { - return $this->services; - } - - /** - * Get a registered service by name. - */ - public function getService(string $name): ?Service - { - return $this->services[$name] ?? collect($this->services)->first(fn (Service $service): bool => $service->getName() === $name); - } -} diff --git a/src/Bot/Concerns/HasTasks.php b/src/Bot/Concerns/HasTasks.php new file mode 100644 index 0000000..9e7c203 --- /dev/null +++ b/src/Bot/Concerns/HasTasks.php @@ -0,0 +1,93 @@ +tasks as $task) { + if (! $task->isEnabled()) { + continue; + } + + $this->tasks[$task::class] = $task->boot(); + } + + $this->callHook(Hook::AFTER_TASKS_REGISTERED); + + return $this; + } + + /** + * Register a task. + */ + public function registerTask(Task|string $task): self + { + if (is_string($task)) { + $task = $task::make(); + } + + if (! is_subclass_of($task, Task::class)) { + $class = $task::class; + + throw new InvalidArgumentException("Class [{$class}] is not a valid task."); + } + + $this->tasks[$task::class] = $task; + + return $this; + } + + /** + * Register multiple tasks. + */ + public function registerTasks(array $tasks): self + { + foreach ($tasks as $task) { + $this->registerTask($task); + } + + return $this; + } + + /** + * Discover tasks in a path. + */ + public function discoverTasks(string $in, string $for): self + { + foreach ($this->discover(Task::class, $in, $for) as $task) { + $this->registerTask($task); + } + + return $this; + } + + /** + * Get the registered tasks. + */ + public function getTasks(): array + { + return $this->tasks; + } + + /** + * Get a registered task by name. + */ + public function getTask(string $name): ?Task + { + return $this->tasks[$name] ?? collect($this->tasks)->first(fn (Task $task): bool => $task->getName() === $name); + } +} diff --git a/src/Bot/Hook.php b/src/Bot/Hook.php index 764b0ae..5d1b38b 100644 --- a/src/Bot/Hook.php +++ b/src/Bot/Hook.php @@ -45,9 +45,9 @@ enum Hook: string case AFTER_EVENTS_REGISTERED = 'afterEventsRegistered'; /** - * Called after all services are booted. + * Called after all tasks are booted. */ - case AFTER_SERVICES_REGISTERED = 'afterServicesRegistered'; + case AFTER_TASKS_REGISTERED = 'afterTasksRegistered'; /** * Called after the HTTP server has started successfully. diff --git a/src/Console/Commands/ServiceMakeCommand.php b/src/Console/Commands/TaskMakeCommand.php similarity index 77% rename from src/Console/Commands/ServiceMakeCommand.php rename to src/Console/Commands/TaskMakeCommand.php index 443f3d7..ad2d7a7 100644 --- a/src/Console/Commands/ServiceMakeCommand.php +++ b/src/Console/Commands/TaskMakeCommand.php @@ -6,28 +6,28 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -class ServiceMakeCommand extends GeneratorCommand +class TaskMakeCommand extends GeneratorCommand { /** * The command name. * * @var string */ - protected $name = 'make:service'; + protected $name = 'make:task'; /** * The command description. * * @var string */ - protected $description = 'Create a new bot service'; + protected $description = 'Create a new bot task'; /** * The type of class being generated. * * @var string */ - protected $type = 'Service'; + protected $type = 'Task'; /** * Get the stub file for the generator. @@ -36,7 +36,7 @@ class ServiceMakeCommand extends GeneratorCommand */ protected function getStub() { - $relativePath = '/stubs/service.stub'; + $relativePath = '/stubs/task.stub'; return file_exists($customPath = $this->laravel->basePath(trim($relativePath, '/'))) ? $customPath @@ -51,7 +51,7 @@ protected function getStub() */ protected function getDefaultNamespace($rootNamespace) { - return $rootNamespace.'\Services'; + return $rootNamespace.'\Tasks'; } /** @@ -62,7 +62,7 @@ protected function getDefaultNamespace($rootNamespace) protected function getArguments() { return [ - ['name', InputArgument::REQUIRED, 'The name of the service'], + ['name', InputArgument::REQUIRED, 'The name of the task'], ]; } @@ -74,7 +74,7 @@ protected function getArguments() protected function getOptions() { return [ - ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the service already exists'], + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the task already exists'], ]; } } diff --git a/src/Console/Commands/stubs/service.stub b/src/Console/Commands/stubs/task.stub similarity index 57% rename from src/Console/Commands/stubs/service.stub rename to src/Console/Commands/stubs/task.stub index c573510..f757ddb 100644 --- a/src/Console/Commands/stubs/service.stub +++ b/src/Console/Commands/stubs/task.stub @@ -2,22 +2,22 @@ namespace {{ namespace }}; -use Laracord\Services\Service; +use Laracord\Tasks\Task; -class {{ class }} extends Service +class {{ class }} extends Task { /** - * The service interval. + * The task interval. */ protected int $interval = 5; /** - * Determine if the service handler should execute during boot. + * Determine if the task handler should execute during boot. */ protected bool $eager = false; /** - * Handle the service. + * Handle the task. */ public function handle(): void { diff --git a/src/Facades/Laracord.php b/src/Facades/Laracord.php index ba39dd4..6844880 100644 --- a/src/Facades/Laracord.php +++ b/src/Facades/Laracord.php @@ -35,11 +35,11 @@ * @method static self discoverEvents(string $in, string $for) Discover events in a path * @method static array getEvents() Get the registered events * @method static ?\Laracord\Events\Event getEvent(string $name) Get a registered event by name - * @method static self registerService(\Laracord\Services\Service|string $service) Register a service - * @method static self registerServices(array $services) Register multiple services - * @method static self discoverServices(string $in, string $for) Discover services in a path - * @method static array getServices() Get the registered services - * @method static ?\Laracord\Services\Service getService(string $name) Get a registered service by name + * @method static self registerTask(\Laracord\Tasks\Task|string $task) Register a task + * @method static self registerTasks(array $tasks) Register multiple tasks + * @method static self discoverTasks(string $in, string $for) Discover tasks in a path + * @method static array getTasks() Get the registered tasks + * @method static ?\Laracord\Tasks\Task getTask(string $name) Get a registered task by name * @method static self registerPrompt(\Laracord\Console\Prompts\Prompt|string $prompt) Register a console prompt * @method static self registerPrompts(array $prompts) Register multiple console prompts * @method static array getPrompts() Get the registered prompts diff --git a/src/Laracord.php b/src/Laracord.php index 4fd6c78..d930bf9 100644 --- a/src/Laracord.php +++ b/src/Laracord.php @@ -30,8 +30,8 @@ class Laracord Concerns\HasLogger, Concerns\HasLoop, Concerns\HasPlugins, - Concerns\HasServices, Concerns\HasSlashCommands, + Concerns\HasTasks, Concerns\HasUserModel; /** @@ -87,7 +87,7 @@ protected function handleBoot(): void ->bootApplicationCommands() ->bootCommands() ->bootEvents() - ->bootServices() + ->bootTasks() ->bootHttpServer() ->handleInteractions(); @@ -149,8 +149,8 @@ public function restart(): void $this->callHook(Hook::BEFORE_RESTART); - foreach ($this->services as $service) { - $service->stop(); + foreach ($this->tasks as $task) { + $task->stop(); } $this->discord?->close(closeLoop: false); @@ -171,7 +171,7 @@ public function getStatus(): Collection 'slash command' => count($this->slashCommands), 'menu' => count($this->contextMenus), 'event' => count($this->events), - 'service' => count($this->services), + 'task' => count($this->tasks), 'interaction' => count($this->interactions), 'route' => count(Route::getRoutes()->getRoutes()), ])->filter()->mapWithKeys(fn ($count, $type) => [Str::plural($type, $count) => $count]); diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 03fc594..3fe06bb 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -143,7 +143,7 @@ public function boot() Commands\MakeSlashCommand::class, Commands\ModelMakeCommand::class, Commands\PromptMakeCommand::class, - Commands\ServiceMakeCommand::class, + Commands\TaskMakeCommand::class, Commands\TokenMakeCommand::class, PackageDiscoverCommand::class, ]); @@ -246,7 +246,8 @@ protected function registerDefaultComponents(Laracord $bot): self ->discoverSlashCommands(in: app_path('SlashCommands'), for: 'App\\SlashCommands') ->discoverContextMenus(in: app_path('Menus'), for: 'App\\Menus') ->discoverEvents(in: app_path('Events'), for: 'App\\Events') - ->discoverServices(in: app_path('Services'), for: 'App\\Services'); + ->discoverTasks(in: app_path('Tasks'), for: 'App\\Tasks') + ->discoverTasks(in: app_path('Services'), for: 'App\\Services'); return $this; } diff --git a/src/Services/Contracts/Service.php b/src/Services/Contracts/Service.php deleted file mode 100644 index ffb1926..0000000 --- a/src/Services/Contracts/Service.php +++ /dev/null @@ -1,8 +0,0 @@ -booted) { - return $this; - } - - if ($this->getInterval() < 1) { - throw new InvalidServiceInterval($this->getName()); - } - - $this->timer = $this->bot()->getLoop()->addPeriodicTimer( - $this->getInterval(), - fn () => $this->resolveHandler() - ); - - if ($this->eager) { - $this->bot->getLoop()->futureTick(fn () => $this->resolveHandler()); - } - - $this->bot()->logger->info("The {$this->getName()} service has been booted."); - - $this->booted = true; - - return $this; - } - - /** - * Get the loop instance. - */ - public function getLoop() - { - return $this->bot()->getLoop(); - } - - /** - * Get the loop interval. - */ - public function getInterval(): int - { - return $this->interval; - } - - /** - * Set the loop interval. - */ - public function interval(int $interval): self - { - $this->interval = $interval; - - return $this; - } - - /** - * Get the service name. - */ - public function getName(): string - { - if (filled($this->name)) { - return $this->name; - } - - return $this->name = class_basename(static::class); - } - - /** - * Determine if the service is enabled. - */ - public function isEnabled(): bool - { - return $this->enabled; - } - - /** - * Determine if the service is booted. - */ - public function isBooted(): bool - { - return $this->booted; - } - - /** - * Stop the service. - */ - public function stop(): void - { - if (! $this->booted) { - return; - } - - $this->getLoop()->cancelTimer($this->timer); - - $this->bot()->logger->info("The {$this->getName()} service has been stopped."); - - $this->timer = null; - - $this->booted = false; - } + // } diff --git a/src/Tasks/Contracts/Task.php b/src/Tasks/Contracts/Task.php new file mode 100644 index 0000000..8cdfc1f --- /dev/null +++ b/src/Tasks/Contracts/Task.php @@ -0,0 +1,8 @@ +booted) { + return $this; + } + + if ($this->getInterval() < 1) { + throw new InvalidTaskInterval($this->getName()); + } + + $this->timer = $this->bot()->getLoop()->addPeriodicTimer( + $this->getInterval(), + fn () => $this->resolveHandler() + ); + + if ($this->eager) { + $this->bot->getLoop()->futureTick(fn () => $this->resolveHandler()); + } + + $this->bot()->logger->info("The {$this->getName()} task has been booted."); + + $this->booted = true; + + return $this; + } + + /** + * Get the loop instance. + */ + public function getLoop() + { + return $this->bot()->getLoop(); + } + + /** + * Get the loop interval. + */ + public function getInterval(): int + { + return $this->interval; + } + + /** + * Set the loop interval. + */ + public function interval(int $interval): self + { + $this->interval = $interval; + + return $this; + } + + /** + * Get the task name. + */ + public function getName(): string + { + if (filled($this->name)) { + return $this->name; + } + + return $this->name = class_basename(static::class); + } + + /** + * Determine if the task is enabled. + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * Determine if the task is booted. + */ + public function isBooted(): bool + { + return $this->booted; + } + + /** + * Stop the task. + */ + public function stop(): void + { + if (! $this->booted) { + return; + } + + $this->getLoop()->cancelTimer($this->timer); + + $this->bot()->logger->info("The {$this->getName()} task has been stopped."); + + $this->timer = null; + + $this->booted = false; + } +} From e79ea4ee7611e530ac7e74350b2dcfb149abd222 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Jun 2025 08:01:18 -0500 Subject: [PATCH 145/146] =?UTF-8?q?=E2=9C=A8=20Create=20an=20initial=20`la?= =?UTF-8?q?racord:upgrade`=20command=20to=20assist=20in=20upgrading=20betw?= =?UTF-8?q?een=20major=20framework=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/Commands/UpgradeCommand.php | 137 ++++++++++++++++++++++++ src/LaracordServiceProvider.php | 1 + 2 files changed, 138 insertions(+) create mode 100644 src/Console/Commands/UpgradeCommand.php diff --git a/src/Console/Commands/UpgradeCommand.php b/src/Console/Commands/UpgradeCommand.php new file mode 100644 index 0000000..87e5e28 --- /dev/null +++ b/src/Console/Commands/UpgradeCommand.php @@ -0,0 +1,137 @@ +components->info('Starting the Laracord upgrade process...'); + + if (! $this->components->confirm('These actions are irreversible. Do you wish to continue?', true)) { + $this->components->error('The upgrade has been cancelled.'); + + return; + } + + $this->handleServices(); + + $this->components->info('Upgrade process completed successfully!'); + } + + /** + * Handle the Service -> Task upgrade. + */ + protected function handleServices(): void + { + $replacements = [ + 'namespace App\Services' => 'namespace App\Tasks', + 'use App\Services' => 'use App\Tasks', + '@var \App\Services' => '@var \App\Tasks', + 'App\Services\\' => 'App\Tasks\\', + 'use Laracord\Services\Service;' => 'use Laracord\Tasks\Task;', + 'extends Service' => 'extends Task', + 'The service' => 'The task', + 'the service' => 'the task', + ]; + + $this->components->info('Checking for Service classes to migrate...'); + + $services = app_path('Services'); + $tasks = app_path('Tasks'); + + if (! file_exists($services)) { + return; + } + + if (! file_exists($tasks)) { + mkdir($tasks, 0755, true); + } + + $files = glob($services.'/*.php'); + + if (empty($files)) { + return; + } + + $fileNames = collect($files) + ->map(fn ($file) => basename($file, '.php')) + ->map(fn ($class) => "{$class}") + ->all(); + + $this->components->bulletList($fileNames); + + if (! $this->components->confirm('Found '.count($files).' Service classes to migrate. Do you wish to continue?', true)) { + $this->components->error('The upgrade has been cancelled.'); + + exit(0); + } + + foreach ($files as $file) { + $name = basename($file, '.php'); + $newPath = str_replace('/Services/', '/Tasks/', $file); + + $this->components->task( + "Migrating {$name} class", + fn () => $this->handleUpgrade($file, $newPath, $replacements) + ); + } + + if (count(glob($services.'/*')) === 0) { + rmdir($services); + } + + $this->newLine(); + + $this->components->info('Successfully migrated '.count($files).' Service classes to Tasks.'); + } + + /** + * Upgrade the Service class file. + */ + protected function handleUpgrade(string $oldPath, string $newPath, array $replacements): bool + { + $content = file_get_contents($oldPath); + + foreach ($replacements as $pattern => $replacement) { + $content = str_replace($pattern, $replacement, $content); + } + + $result = file_put_contents($newPath, $content) !== false; + + if ($result) { + unlink($oldPath); + } + + return $result; + } +} diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 3fe06bb..03faa43 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -145,6 +145,7 @@ public function boot() Commands\PromptMakeCommand::class, Commands\TaskMakeCommand::class, Commands\TokenMakeCommand::class, + Commands\UpgradeCommand::class, PackageDiscoverCommand::class, ]); From ad6bfa797939b3073a2c9c0085063a4b46a09698 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 21 Apr 2026 16:17:42 -0500 Subject: [PATCH 146/146] =?UTF-8?q?=F0=9F=9A=A8=20Run=20Pint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/commands.php | 31 +++++++++----- config/logging.php | 3 +- src/Bot/Concerns/HasHttpServer.php | 5 ++- src/Console/Commands/AdminCommand.php | 3 +- .../Commands/Concerns/ResolvesUser.php | 3 +- .../Commands/ControllerMakeCommand.php | 2 +- src/Console/Commands/TokenMakeCommand.php | 3 +- src/Console/Components/Log.php | 3 +- src/Console/Concerns/WithLog.php | 5 ++- src/HasLaracord.php | 2 +- src/Http/HttpServer.php | 11 +++-- src/LaracordServiceProvider.php | 42 ++++++++++++------- src/helpers.php | 2 +- 13 files changed, 75 insertions(+), 40 deletions(-) diff --git a/config/commands.php b/config/commands.php index b286e24..046cdde 100644 --- a/config/commands.php +++ b/config/commands.php @@ -1,5 +1,16 @@ Laracord\Console\Commands\BootCommand::class, + 'default' => BootCommand::class, /* |-------------------------------------------------------------------------- @@ -57,14 +68,14 @@ */ 'hidden' => [ - NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, - Symfony\Component\Console\Command\DumpCompletionCommand::class, - Symfony\Component\Console\Command\HelpCommand::class, - Illuminate\Console\Scheduling\ScheduleRunCommand::class, - Illuminate\Console\Scheduling\ScheduleListCommand::class, - Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - Illuminate\Foundation\Console\VendorPublishCommand::class, - LaravelZero\Framework\Commands\StubPublishCommand::class, + SummaryCommand::class, + DumpCompletionCommand::class, + HelpCommand::class, + ScheduleRunCommand::class, + ScheduleListCommand::class, + ScheduleFinishCommand::class, + VendorPublishCommand::class, + StubPublishCommand::class, ], /* @@ -79,7 +90,7 @@ */ 'remove' => [ - LaravelZero\Framework\Commands\MakeCommand::class, + MakeCommand::class, ], ]; diff --git a/config/logging.php b/config/logging.php index 1622dd1..9df61c1 100644 --- a/config/logging.php +++ b/config/logging.php @@ -1,5 +1,6 @@ [ 'driver' => 'monolog', - 'handler' => Laracord\Logging\LoggingHandler::class, + 'handler' => LoggingHandler::class, 'with' => [ 'path' => env('LOG_PATH', laracord_path('logs/laracord.log')), 'level' => env('LOG_LEVEL', 'debug'), diff --git a/src/Bot/Concerns/HasHttpServer.php b/src/Bot/Concerns/HasHttpServer.php index f7f016e..8be34a2 100644 --- a/src/Bot/Concerns/HasHttpServer.php +++ b/src/Bot/Concerns/HasHttpServer.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Routing\Router; use Illuminate\Support\Str; use Laracord\Bot\Hook; use Laracord\Http\HttpServer; @@ -30,7 +31,7 @@ trait HasHttpServer */ public function withRoutes(?callable $callback = null): self { - /** @var \Illuminate\Routing\Router $router */ + /** @var Router $router */ $router = $this->app->make('router'); if (! is_null($callback)) { @@ -48,7 +49,7 @@ public function withMiddleware(?callable $callback = null): self /** @var \Laracord\Http\Kernel $kernel */ $kernel = $this->app->make(Kernel::class); - /** @var \Illuminate\Foundation\Configuration\Middleware $middleware */ + /** @var Middleware $middleware */ $middleware = $this->app->make(Middleware::class); if (! is_null($callback)) { diff --git a/src/Console/Commands/AdminCommand.php b/src/Console/Commands/AdminCommand.php index 85301f9..297eae9 100644 --- a/src/Console/Commands/AdminCommand.php +++ b/src/Console/Commands/AdminCommand.php @@ -2,6 +2,7 @@ namespace Laracord\Console\Commands; +use Illuminate\Database\Eloquent\Model; use Laracord\Console\Commands\Concerns\ResolvesUser; class AdminCommand extends Command @@ -27,7 +28,7 @@ class AdminCommand extends Command /** * The user model. * - * @var \Illuminate\Database\Eloquent\Model|null + * @var Model|null */ protected $user; diff --git a/src/Console/Commands/Concerns/ResolvesUser.php b/src/Console/Commands/Concerns/ResolvesUser.php index ffe2325..36cdd32 100644 --- a/src/Console/Commands/Concerns/ResolvesUser.php +++ b/src/Console/Commands/Concerns/ResolvesUser.php @@ -2,6 +2,7 @@ namespace Laracord\Console\Commands\Concerns; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Http; use Laracord\Facades\Laracord; @@ -10,7 +11,7 @@ trait ResolvesUser /** * Resolve the user. * - * @return \Illuminate\Database\Eloquent\Model|void + * @return Model|void */ protected function resolveUser(?string $user = null) { diff --git a/src/Console/Commands/ControllerMakeCommand.php b/src/Console/Commands/ControllerMakeCommand.php index 6ec7981..2d97586 100644 --- a/src/Console/Commands/ControllerMakeCommand.php +++ b/src/Console/Commands/ControllerMakeCommand.php @@ -112,7 +112,7 @@ protected function buildModelReplacements(array $replace) * @param string $model * @return string * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function parseModel($model) { diff --git a/src/Console/Commands/TokenMakeCommand.php b/src/Console/Commands/TokenMakeCommand.php index ace3ce5..428e701 100644 --- a/src/Console/Commands/TokenMakeCommand.php +++ b/src/Console/Commands/TokenMakeCommand.php @@ -2,6 +2,7 @@ namespace Laracord\Console\Commands; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Laracord\Console\Commands\Concerns\ResolvesUser; @@ -29,7 +30,7 @@ class TokenMakeCommand extends Command /** * The user model. * - * @var \Illuminate\Database\Eloquent\Model|null + * @var Model|null */ protected $user; diff --git a/src/Console/Components/Log.php b/src/Console/Components/Log.php index a7b72fc..029ddb8 100644 --- a/src/Console/Components/Log.php +++ b/src/Console/Components/Log.php @@ -5,6 +5,7 @@ use Illuminate\Console\Contracts\NewLineAware; use Illuminate\Console\View\Components\Component; use Illuminate\Console\View\Components\Mutators; +use Illuminate\Contracts\Support\Arrayable; use Symfony\Component\Console\Output\OutputInterface; use function Termwind\render; @@ -38,7 +39,7 @@ public function render($style, $string, $verbosity = OutputInterface::VERBOSITY_ * Renders the given view. * * @param string $view - * @param \Illuminate\Contracts\Support\Arrayable|array $data + * @param Arrayable|array $data * @param int $verbosity * @return void */ diff --git a/src/Console/Concerns/WithLog.php b/src/Console/Concerns/WithLog.php index a660e17..f8b763a 100644 --- a/src/Console/Concerns/WithLog.php +++ b/src/Console/Concerns/WithLog.php @@ -3,6 +3,7 @@ namespace Laracord\Console\Concerns; use DateTimeInterface; +use Illuminate\Console\OutputStyle; use Laracord\Console\Components\Log; use Psr\Log\LogLevel; use Stringable; @@ -12,7 +13,7 @@ trait WithLog /** * The output style implementation. * - * @var \Illuminate\Console\OutputStyle + * @var OutputStyle */ protected $output; @@ -33,7 +34,7 @@ trait WithLog /** * Render a log message. */ - public function log($level, string|\Stringable $message, array $context = []): void + public function log($level, string|Stringable $message, array $context = []): void { $message = trim($message); diff --git a/src/HasLaracord.php b/src/HasLaracord.php index d18740b..cf582a7 100644 --- a/src/HasLaracord.php +++ b/src/HasLaracord.php @@ -54,7 +54,7 @@ public function logger(): LogManager * Build an embed for use in a Discord message. * * @param string $content - * @return \Laracord\Discord\Message + * @return Message */ public function message($content = '') { diff --git a/src/Http/HttpServer.php b/src/Http/HttpServer.php index cab8e4c..86e6519 100644 --- a/src/Http/HttpServer.php +++ b/src/Http/HttpServer.php @@ -4,11 +4,14 @@ use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; use Laracord\Bot\Hook; use Laracord\Http\Handlers\StaticFileHandler; +use Laracord\Http\Middleware\AuthorizeToken; +use Laracord\Http\Middleware\FlushState; use Laracord\Laracord; use Psr\Http\Message\ServerRequestInterface; use React\Http\HttpServer as Server; @@ -167,13 +170,13 @@ protected function configureMiddleware(): void { $this->bot->withMiddleware(function (Middleware $middleware) { $middleware - ->remove([\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class]) - ->append([\Laracord\Http\Middleware\FlushState::class]) + ->remove([PreventRequestsDuringMaintenance::class]) + ->append([FlushState::class]) ->api([ - \Laracord\Http\Middleware\AuthorizeToken::class, + AuthorizeToken::class, ]) ->alias([ - 'auth.token' => \Laracord\Http\Middleware\AuthorizeToken::class, + 'auth.token' => AuthorizeToken::class, ]); }); } diff --git a/src/LaracordServiceProvider.php b/src/LaracordServiceProvider.php index 03faa43..47ccd6e 100644 --- a/src/LaracordServiceProvider.php +++ b/src/LaracordServiceProvider.php @@ -2,25 +2,39 @@ namespace Laracord; +use Illuminate\Auth\AuthServiceProvider; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Http\Kernel as KernelContract; +use Illuminate\Cookie\CookieServiceProvider; +use Illuminate\Encryption\EncryptionServiceProvider; use Illuminate\Filesystem\Filesystem; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Console\PackageDiscoverCommand; use Illuminate\Foundation\PackageManifest as BasePackageManifest; +use Illuminate\Hashing\HashServiceProvider; +use Illuminate\Log\Context\ContextServiceProvider; +use Illuminate\Mail\MailServiceProvider; +use Illuminate\Queue\QueueServiceProvider; +use Illuminate\Routing\RoutingServiceProvider; +use Illuminate\Session\SessionServiceProvider; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; +use Illuminate\Translation\TranslationServiceProvider; +use Illuminate\Validation\ValidationServiceProvider; +use Illuminate\View\ViewServiceProvider; +use Intonate\TinkerZero\TinkerZeroServiceProvider; use Laracord\Console\Commands; use Laracord\Console\Console; use Laracord\Console\Prompts; use Laracord\Discord\Message; use Laracord\Http\Kernel; +use Laracord\Http\Providers\RouteServiceProvider; use LaravelZero\Framework\Components\Database\Provider as DatabaseProvider; use LaravelZero\Framework\Components\Log\Provider as LogProvider; use React\EventLoop\Loop; @@ -43,20 +57,20 @@ abstract class LaracordServiceProvider extends AggregateServiceProvider * @var array */ protected $providers = [ - \Illuminate\Encryption\EncryptionServiceProvider::class, - \Illuminate\Hashing\HashServiceProvider::class, - \Illuminate\Queue\QueueServiceProvider::class, - \Illuminate\Routing\RoutingServiceProvider::class, - \Illuminate\Translation\TranslationServiceProvider::class, - \Illuminate\Validation\ValidationServiceProvider::class, - \Illuminate\View\ViewServiceProvider::class, - \Illuminate\Cookie\CookieServiceProvider::class, - \Illuminate\Session\SessionServiceProvider::class, - \Illuminate\Mail\MailServiceProvider::class, - \Illuminate\Auth\AuthServiceProvider::class, - \Illuminate\Log\Context\ContextServiceProvider::class, - \Laracord\Http\Providers\RouteServiceProvider::class, - \Intonate\TinkerZero\TinkerZeroServiceProvider::class, + EncryptionServiceProvider::class, + HashServiceProvider::class, + QueueServiceProvider::class, + RoutingServiceProvider::class, + TranslationServiceProvider::class, + ValidationServiceProvider::class, + ViewServiceProvider::class, + CookieServiceProvider::class, + SessionServiceProvider::class, + MailServiceProvider::class, + AuthServiceProvider::class, + ContextServiceProvider::class, + RouteServiceProvider::class, + TinkerZeroServiceProvider::class, ]; abstract public function bot(Laracord $bot): Laracord; diff --git a/src/helpers.php b/src/helpers.php index f987a5c..94a8358 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -16,7 +16,7 @@ function laracord(): Laracord\Laracord */ function laracord_path(string $path = '', bool $basePath = true): string { - $binary = \Phar::running(false); + $binary = Phar::running(false); $basePath = $basePath ? '.laracord' : ''; $appPath = $binary ? pathinfo($binary, PATHINFO_DIRNAME) : null;