Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ Generated from the source, so it describes what the framework actually does.

**Plugins** — [Plugin](reference/plugins/plugin.md)

**Middleware** — [Middleware](reference/middleware/middleware.md), [RequiresPermissions](reference/middleware/requires-permissions.md)

65 changes: 65 additions & 0 deletions docs/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@
"default": "null",
"required": false,
"summary": "the catalog key this command's translations live under. Keys for everything beneath it are derived from position, so \"commands.music\" gives commands.music.description for the command, commands.music.playlist.play.description for a subcommand, and commands.music.playlist.play.title.description for its option."
},
{
"name": "middleware",
"type": "array",
"default": "[]",
"required": false,
"summary": "run in the order given, outermost first, before any handler this command has; any of them may answer instead of letting it run"
}
],
"cases": [],
Expand All @@ -131,6 +138,13 @@
"default": null,
"required": true,
"summary": ""
},
{
"name": "middleware",
"type": "array",
"default": "[]",
"required": false,
"summary": "run after whatever the command declares, before the subcommand's own"
}
],
"cases": [],
Expand All @@ -157,6 +171,13 @@
"default": null,
"required": true,
"summary": ""
},
{
"name": "middleware",
"type": "array",
"default": "[]",
"required": false,
"summary": "run after whatever the command and the group around it declare"
}
],
"cases": [],
Expand Down Expand Up @@ -719,6 +740,50 @@
}
]
}
],
"middleware": [
{
"name": "Middleware",
"fqcn": "Tempcord\\Interfaces\\Middleware",
"kind": "interface",
"target": null,
"summary": "Something that runs before a command, and may decide it never runs.",
"slug": "reference/middleware/middleware",
"parameters": [],
"cases": [],
"methods": [
{
"signature": "__invoke(CommandInteraction $interaction, callable $next): void",
"summary": ""
}
]
},
{
"name": "RequiresPermissions",
"fqcn": "Tempcord\\Middleware\\RequiresPermissions",
"kind": "class",
"target": null,
"summary": "Refuses anyone whose permissions in the channel fall short.",
"slug": "reference/middleware/requires-permissions",
"parameters": [
{
"name": "permissions",
"type": "array",
"default": null,
"required": true,
"summary": "every one of which the caller must hold; an administrator holds all of them by definition"
},
{
"name": "refusal",
"type": "string",
"default": "'You are not allowed to use this command.'",
"required": false,
"summary": "what the caller is told, ephemerally"
}
],
"cases": [],
"methods": []
}
]
}
}
1 change: 1 addition & 0 deletions docs/reference/attributes/command.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ use Tempcord\Attributes\Command;
| `type` | `ApplicationCommandTypes` | `ApplicationCommandTypes::CHAT_INPUT` | |
| `handler` | `?EntryPointCommandHandlerType` | `null` | |
| `translationKey` | `?string` | `null` | the catalog key this command's translations live under. Keys for everything beneath it are derived from position, so "commands.music" gives commands.music.description for the command, commands.music.playlist.play.description for a subcommand, and commands.music.playlist.play.title.description for its option. |
| `middleware` | `array` | `[]` | run in the order given, outermost first, before any handler this command has; any of them may answer instead of letting it run |

1 change: 1 addition & 0 deletions docs/reference/attributes/subcommand-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ use Tempcord\Attributes\SubcommandGroup;
| --- | --- | --- | --- |
| `name` | `BackedEnum\|string` | *required* | |
| `description` | `string` | *required* | |
| `middleware` | `array` | `[]` | run after whatever the command declares, before the subcommand's own |

1 change: 1 addition & 0 deletions docs/reference/attributes/subcommand.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ use Tempcord\Attributes\Subcommand;
| --- | --- | --- | --- |
| `name` | `BackedEnum\|string` | *required* | |
| `description` | `string` | *required* | |
| `middleware` | `array` | `[]` | run after whatever the command and the group around it declare |

5 changes: 5 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@

- [Plugin](plugins/plugin.md) — A package that extends a bot with its own behaviour.

## Middleware

- [Middleware](middleware/middleware.md) — Something that runs before a command, and may decide it never runs.
- [RequiresPermissions](middleware/requires-permissions.md) — Refuses anyone whose permissions in the channel fall short.

14 changes: 14 additions & 0 deletions docs/reference/middleware/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- Generated from the source by `composer docs`. Do not edit by hand. -->

# Middleware

Something that runs before a command, and may decide it never runs.

```php
use Tempcord\Interfaces\Middleware;
```

## Methods

### `__invoke(CommandInteraction $interaction, callable $next): void`

17 changes: 17 additions & 0 deletions docs/reference/middleware/requires-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Generated from the source by `composer docs`. Do not edit by hand. -->

# RequiresPermissions

Refuses anyone whose permissions in the channel fall short.

```php
use Tempcord\Middleware\RequiresPermissions;
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `permissions` | `array` | *required* | every one of which the caller must hold; an administrator holds all of them by definition |
| `refusal` | `string` | `'You are not allowed to use this command.'` | what the caller is told, ephemerally |

4 changes: 4 additions & 0 deletions src/Attributes/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
* commands.music.description for the command,
* commands.music.playlist.play.description for a subcommand, and
* commands.music.playlist.play.title.description for its option.
* @param list<\Tempcord\Interfaces\Middleware|class-string<\Tempcord\Interfaces\Middleware>> $middleware
* run in the order given, outermost first, before any handler this
* command has; any of them may answer instead of letting it run
*/
public function __construct(
public string|BackedEnum|null $name = null,
Expand All @@ -45,6 +48,7 @@ public function __construct(
public ApplicationCommandTypes $type = ApplicationCommandTypes::CHAT_INPUT,
public ?EntryPointCommandHandlerType $handler = null,
public ?string $translationKey = null,
public array $middleware = [],
) {
$this->guildId = $guildId === null ? null : (string) $guildId;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Attributes/Subcommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Subcommand
{
/**
* @param list<\Tempcord\Interfaces\Middleware|class-string<\Tempcord\Interfaces\Middleware>> $middleware
* run after whatever the command and the group around it declare
*/
public function __construct(
public string|BackedEnum $name,
public string $description,
public array $middleware = [],
) {}
}
5 changes: 5 additions & 0 deletions src/Attributes/SubcommandGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class SubcommandGroup
{
/**
* @param list<\Tempcord\Interfaces\Middleware|class-string<\Tempcord\Interfaces\Middleware>> $middleware
* run after whatever the command declares, before the subcommand's own
*/
public function __construct(
public string|BackedEnum $name,
public string $description,
public array $middleware = [],
) {}
}
44 changes: 44 additions & 0 deletions src/Compiler/CommandCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Tempcord\Definitions\SubcommandDefinition;
use Tempcord\Definitions\SubcommandGroupDefinition;
use Tempcord\Interfaces\Autocomplete;
use Tempcord\Interfaces\Middleware;
use Tempcord\Localization\LocalizationProvider;
use Tempcord\Localization\NullLocalizations;
use ReflectionEnum;
Expand Down Expand Up @@ -67,6 +68,7 @@ public function compile(ClassReflector $class, Command $command): CommandDefinit
$handlers = [];

$key = $command->translationKey;
$around = $this->middlewareOf($command->middleware, 'Command [' . $name . ']');
$group = $this->groupOf($class, $key);
$subcommands = $this->subcommandsOf($class, $key);

Expand All @@ -81,6 +83,7 @@ public function compile(ClassReflector $class, Command $command): CommandDefinit
method: $subcommand->method,
options: $subcommand->options,
optionPath: $group->name . '.' . $subcommand->name,
middleware: [...$around, ...$group->middleware, ...$subcommand->middleware],
);
}
} elseif ($subcommands !== []) {
Expand All @@ -93,6 +96,7 @@ public function compile(ClassReflector $class, Command $command): CommandDefinit
method: $subcommand->method,
options: $subcommand->options,
optionPath: $subcommand->name,
middleware: [...$around, ...$subcommand->middleware],
);
}
} elseif (!$this->isLaunchedByDiscord($command)) {
Expand All @@ -107,6 +111,7 @@ public function compile(ClassReflector $class, Command $command): CommandDefinit
path: $name,
method: $invoke,
options: $options,
middleware: $around,
);
}

Expand Down Expand Up @@ -211,6 +216,7 @@ private function groupOf(ClassReflector $class, ?string $key): ?SubcommandGroupD
subcommands: $this->subcommandsOf($class, $groupKey),
nameLocalizations: $this->translate($groupKey, 'name'),
descriptionLocalizations: $this->translate($groupKey, 'description'),
middleware: $this->middlewareOf($group->middleware, 'Subcommand group [' . $name . ']'),
);
}

Expand Down Expand Up @@ -239,6 +245,7 @@ private function subcommandsOf(ClassReflector $class, ?string $key): array
method: $method,
nameLocalizations: $this->translate($subcommandKey, 'name'),
descriptionLocalizations: $this->translate($subcommandKey, 'description'),
middleware: $this->middlewareOf($subcommand->middleware, 'Subcommand [' . $name . ']'),
);
}

Expand Down Expand Up @@ -346,6 +353,43 @@ private function autocompleteFor(Option $option, ?MethodReflector $completer): ?
return null;
}

/**
* Middleware as declared, checked before anything is built out of it.
*
* A class name that turns out not to be middleware is a mistake worth
* catching here: discovery runs at start-up, so the bot refuses to boot
* rather than failing the first time somebody uses the command it was meant
* to guard — which, for a guard, is the worst moment to find out.
*
* @param array<mixed> $declared
*
* @return list<Middleware|class-string<Middleware>>
*/
private function middlewareOf(array $declared, string $where): array
{
$middleware = [];

foreach ($declared as $entry) {
if ($entry instanceof Middleware) {
$middleware[] = $entry;
continue;
}

if (is_string($entry) && is_subclass_of($entry, Middleware::class)) {
$middleware[] = $entry;
continue;
}

throw new LogicException(
$where . ' declares middleware ['
. (is_string($entry) ? $entry : get_debug_type($entry))
. '], which does not implement ' . Middleware::class,
);
}

return $middleware;
}

/**
* The command's own methods that complete an option, keyed by the option
* each one answers for.
Expand Down
5 changes: 5 additions & 0 deletions src/Definitions/HandlerDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tempcord\Definitions;

use Tempcord\Interfaces\Middleware;
use Tempest\Reflection\MethodReflector;

/**
Expand All @@ -17,12 +18,16 @@
* @param array<string, OptionDefinition> $options keyed by option name
* @param string $optionPath the prefix getOption() needs to reach this
* handler's options, empty for an invokable command
* @param list<Middleware|class-string<Middleware>> $middleware everything
* declared around this handler, flattened outermost first — the
* command's, then its group's, then the subcommand's own
*/
public function __construct(
public string $path,
public MethodReflector $method,
public array $options,
public string $optionPath = '',
public array $middleware = [],
) {}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Definitions/SubcommandDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tempcord\Definitions;

use Tempcord\Interfaces\Middleware;
use Tempest\Reflection\MethodReflector;

/**
Expand All @@ -13,6 +14,8 @@
* @param array<string, OptionDefinition> $options keyed by option name
* @param array<string, string> $nameLocalizations keyed by Discord locale
* @param array<string, string> $descriptionLocalizations keyed by Discord locale
* @param list<Middleware|class-string<Middleware>> $middleware this
* subcommand's own, before anything declared around it is folded in
*/
public function __construct(
public string $name,
Expand All @@ -21,5 +24,6 @@ public function __construct(
public MethodReflector $method,
public array $nameLocalizations = [],
public array $descriptionLocalizations = [],
public array $middleware = [],
) {}
}
5 changes: 5 additions & 0 deletions src/Definitions/SubcommandGroupDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Tempcord\Definitions;

use Tempcord\Interfaces\Middleware;

/**
* A named grouping of subcommands, which Discord renders as one more level of
* nesting under the command itself.
Expand All @@ -12,12 +14,15 @@
* @param array<string, SubcommandDefinition> $subcommands keyed by subcommand name
* @param array<string, string> $nameLocalizations keyed by Discord locale
* @param array<string, string> $descriptionLocalizations keyed by Discord locale
* @param list<Middleware|class-string<Middleware>> $middleware run around
* every subcommand in the group
*/
public function __construct(
public string $name,
public string $description,
public array $subcommands,
public array $nameLocalizations = [],
public array $descriptionLocalizations = [],
public array $middleware = [],
) {}
}
Loading