Skip to content

Commit

Permalink
✨ Implement command middleware registration and resolving (Fixes #121)
Browse files Browse the repository at this point in the history
  • Loading branch information
Log1x committed Feb 6, 2025
1 parent 81b6b56 commit e333492
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 0 deletions.
93 changes: 93 additions & 0 deletions src/Bot/Concerns/HasCommandMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Laracord\Bot\Concerns;

use InvalidArgumentException;
use Laracord\Commands\Middleware\Middleware;

trait HasCommandMiddleware
{
/**
* The global command middleware.
*/
protected array $commandMiddleware = [];

/**
* Register a global command middleware.
*/
public function registerCommandMiddleware(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->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)
);
}
}
91 changes: 91 additions & 0 deletions src/Commands/Middleware/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace Laracord\Commands\Middleware;

use Discord\Parts\Channel\Message;
use Discord\Parts\Interactions\Interaction;
use Laracord\Commands\Contracts\Command;
use Laracord\Commands\Contracts\ContextMenu;
use Laracord\Commands\Contracts\SlashCommand;

class Context
{
/**
* Create a new context instance.
*/
public function __construct(
public Message|Interaction $source,
public Command|SlashCommand|ContextMenu|null $command = null,
public array $args = [],
public mixed $target = null,
public array $options = []
) {}

/**
* Determine if the context is from a message command.
*/
public function isMessage(): bool
{
return $this->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;
}
}
15 changes: 15 additions & 0 deletions src/Commands/Middleware/Middleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Laracord\Commands\Middleware;

use Closure;

interface Middleware
{
/**
* Handle the command.
*
* @return mixed
*/
public function handle(Context $context, Closure $next);
}
66 changes: 66 additions & 0 deletions src/Commands/Middleware/ThrottleCommands.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Laracord\Commands\Middleware;

use Closure;
use Illuminate\Support\Facades\Cache;
use Laracord\Discord\Facades\Message;

class ThrottleCommands implements Middleware
{
/**
* Create a new middleware instance.
*/
public function __construct(protected int $maxAttempts = 60, protected int $decayMinutes = 1)
{
//
}

/**
* Handle the command.
*
* @return mixed
*/
public function handle(Context $context, Closure $next)
{
$key = $this->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));
}
}
46 changes: 46 additions & 0 deletions src/Console/Commands/MakeCommandMiddlewareCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Laracord\Console\Commands;

use Illuminate\Foundation\Console\ConsoleMakeCommand as FoundationConsoleMakeCommand;

class MakeCommandMiddlewareCommand extends FoundationConsoleMakeCommand
{
/**
* {@inheritdoc}
*/
protected $name = 'make:command-middleware';

/**
* {@inheritdoc}
*/
protected $description = 'Create a new command middleware';

/**
* {@inheritdoc}
*/
protected function getNameInput(): string
{
return ucfirst(parent::getNameInput());
}

/**
* {@inheritdoc}
*/
protected function getStub(): string
{
$relativePath = '/stubs/command-middleware.stub';

return file_exists($customPath = $this->laravel->basePath(trim($relativePath, '/')))
? $customPath
: __DIR__.$relativePath;
}

/**
* {@inheritdoc}
*/
protected function getDefaultNamespace($rootNamespace): string
{
return $rootNamespace.'\Commands\Middleware';
}
}
22 changes: 22 additions & 0 deletions src/Console/Commands/stubs/command-middleware.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace {{ namespace }};

use Closure;
use Laracord\Commands\Middleware\Context;
use Laracord\Commands\Middleware\Middleware;

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)
{
return $next($context);
}
}

0 comments on commit e333492

Please # to comment.