-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Implement command middleware registration and resolving (Fixes #121)
- Loading branch information
Showing
6 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |