Skip to content

Commit

Permalink
Fix: Command groups always emitted the first command in group (#85)
Browse files Browse the repository at this point in the history
* Parse at runtime rather than startup

* Fix up Guild commands & add AllCommandExtension

* Fix class name

* Fix up tests

* Add test for allCOmmandExtension
  • Loading branch information
Exanlv committed Mar 28, 2024
1 parent fd0b84d commit 80b3374
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 199 deletions.
18 changes: 18 additions & 0 deletions src/Command/AllCommandExtension.php
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Ragnarok\Fenrir\Command;

use Ragnarok\Fenrir\Gateway\Events\InteractionCreate;

/**
* Emits an event for Guild Commands and Global Commands
*/
class AllCommandExtension extends CommandExtension
{
protected function emitInteraction(InteractionCreate $interaction): bool
{
return true;
}
}
29 changes: 10 additions & 19 deletions src/Command/CommandExtension.php
Expand Up @@ -13,35 +13,25 @@
use Ragnarok\Fenrir\FilteredEventEmitter;
use Ragnarok\Fenrir\Gateway\Events\InteractionCreate;
use Ragnarok\Fenrir\Interaction\CommandInteraction;
use Ragnarok\Fenrir\Parts\ApplicationCommand;
use Ragnarok\Fenrir\Parts\ApplicationCommandOptionStructure;
use Ragnarok\Fenrir\Parts\ApplicationCommandInteractionDataOptionStructure;

use function Freezemage\ArrayUtils\find;

abstract class CommandExtension extends EventEmitter implements Extension
{
protected array $commandMappings = [];

protected FilteredEventEmitter $commandListener;

abstract protected function loadExistingCommands(Discord $discord): void;
abstract protected function emitInteraction(InteractionCreate $interaction): bool;

public function initialize(Discord $discord): void
{
$this->loadExistingCommands($discord);

$this->registerListener($discord);
}

private function registerListener(Discord $discord): void
{
$this->commandListener = new FilteredEventEmitter(
$discord->gateway->events,
Events::INTERACTION_CREATE,
fn (InteractionCreate $interactionCreate) =>
isset($interactionCreate->type)
&& $interactionCreate->type === InteractionType::APPLICATION_COMMAND
&& isset($this->commandMappings[$interactionCreate->data->id])
&& $this->emitInteraction($interactionCreate)
);

$this->commandListener->on(Events::INTERACTION_CREATE, function (InteractionCreate $interaction) use ($discord) {
Expand All @@ -53,24 +43,25 @@ private function registerListener(Discord $discord): void

private function handleInteraction(InteractionCreate $interaction, Discord $discord)
{
$commandName = $this->getFullNameByInteraction($interaction);
$firedCommand = new CommandInteraction($interaction, $discord);

$this->emit($this->commandMappings[$interaction->data->id], [$firedCommand]);
$this->emit($commandName, [$firedCommand]);
}

protected function getFullNameByCommand(ApplicationCommand $command): string
protected function getFullNameByInteraction(InteractionCreate $command): string
{
$names = [$command->name];
$names = [$command->data->name];

$this->drillName($command->options ?? [], $names);
$this->drillName($command->data->options ?? [], $names);

return implode('.', $names);
}

private function drillName(array $options, array &$names)
{
/** @var ?ApplicationCommandOptionStructure */
$subCommand = find($options ?? [], function (ApplicationCommandOptionStructure $option) {
/** @var ?ApplicationCommandInteractionDataOptionStructure */
$subCommand = find($options ?? [], function (ApplicationCommandInteractionDataOptionStructure $option) {
return in_array(
$option->type,
[
Expand Down
21 changes: 10 additions & 11 deletions src/Command/GlobalCommandExtension.php
Expand Up @@ -4,23 +4,22 @@

namespace Ragnarok\Fenrir\Command;

use Ragnarok\Fenrir\Discord;
use Ragnarok\Fenrir\Parts\ApplicationCommand;
use Ragnarok\Fenrir\Gateway\Events\InteractionCreate;

/**
* Emits an event for each Global Command used anywhere
*/
class GlobalCommandExtension extends CommandExtension
{
public function __construct(protected readonly string $applicationId)
public function __construct(?string $applicationId = null)
{
if (!is_null($applicationId)) {
trigger_error('Providing an application ID is no longer required and will be removed in a later version');
}
}

protected function loadExistingCommands(Discord $discord): void
protected function emitInteraction(InteractionCreate $interaction): bool
{
$discord->rest->globalCommand->getCommands($this->applicationId)
->then(function (array $commands) {
/** @var ApplicationCommand $command */
foreach ($commands as $command) {
$this->commandMappings[$command->id] = $this->getFullNameByCommand($command);
}
});
return !isset($interaction->data->guild_id);
}
}
21 changes: 10 additions & 11 deletions src/Command/GuildCommandExtension.php
Expand Up @@ -4,23 +4,22 @@

namespace Ragnarok\Fenrir\Command;

use Ragnarok\Fenrir\Discord;
use Ragnarok\Fenrir\Parts\ApplicationCommand;
use Ragnarok\Fenrir\Gateway\Events\InteractionCreate;

/**
* Emits an event for each Guild Command used on a specific Guild
*/
class GuildCommandExtension extends CommandExtension
{
public function __construct(protected readonly string $applicationId, protected readonly string $guildId)
public function __construct(?string $applicationId, private readonly string $guildId)
{
if (!is_null($applicationId)) {
trigger_error('Providing an application ID is no longer required and will be removed in a later version');
}
}

protected function loadExistingCommands(Discord $discord): void
protected function emitInteraction(InteractionCreate $interaction): bool
{
$discord->rest->guildCommand->getCommands($this->guildId, $this->applicationId)
->then(function (array $commands) {
/** @var ApplicationCommand $command */
foreach ($commands as $command) {
$this->commandMappings[$command->id] = $this->getFullNameByCommand($command);
}
});
return isset($interaction->data->guild_id) && $interaction->data->guild_id === $this->guildId;
}
}
187 changes: 187 additions & 0 deletions tests/Command/AllCommandExtensionTest.php
@@ -0,0 +1,187 @@
<?php

declare(strict_types=1);

namespace Tests\Ragnarok\Fenrir\Command;

use Fakes\Ragnarok\Fenrir\DiscordFake;
use PHPUnit\Framework\TestCase;
use Ragnarok\Fenrir\Command\AllCommandExtension;
use Ragnarok\Fenrir\Constants\Events;
use Ragnarok\Fenrir\Discord;
use Ragnarok\Fenrir\Enums\ApplicationCommandOptionType;
use Ragnarok\Fenrir\Enums\InteractionType;
use Ragnarok\Fenrir\Gateway\Events\InteractionCreate;
use Ragnarok\Fenrir\Interaction\CommandInteraction;
use Ragnarok\Fenrir\Parts\ApplicationCommandInteractionDataOptionStructure;
use Ragnarok\Fenrir\Parts\InteractionData;

class AllCommandExtensionTest extends TestCase
{
private Discord $discord;

protected function setUp(): void
{
$this->discord = DiscordFake::get();
}

public function testItEmitsEventsForApplicationCommands()
{
$extension = new AllCommandExtension();
$extension->initialize($this->discord);

$hasRun = [false, false, false];

$extension->on('command-1', function (CommandInteraction $firedCommand) use (&$hasRun) {
$hasRun[0] = true;
});

$extension->on('command-2', function (CommandInteraction $firedCommand) use (&$hasRun) {
$hasRun[1] = true;
});

$extension->on('command-3', function (CommandInteraction $firedCommand) use (&$hasRun) {
$hasRun[2] = true;
});

$interaction = new InteractionCreate();
$interaction->type = InteractionType::APPLICATION_COMMAND;
$interaction->data = new InteractionData();
$interaction->data->name = 'command-1';

$this->discord->gateway->events->emit(
Events::INTERACTION_CREATE,
[$interaction]
);

$interaction->data->name = 'command-2';
$interaction->data->guild_id = '::guild id::';

$this->discord->gateway->events->emit(
Events::INTERACTION_CREATE,
[$interaction]
);

$this->assertTrue($hasRun[0], 'Command 1 did not run');
$this->assertTrue($hasRun[1], 'Command 2 did not run');
$this->assertFalse($hasRun[2], 'Command 3 should not have been run');
}

public function testItEmitsEventsForGuildCommands()
{
$extension = new AllCommandExtension();
$extension->initialize($this->discord);

$hasRun = [false, false, false];

$extension->on('command-1', function (CommandInteraction $firedCommand) use (&$hasRun) {
$hasRun[0] = true;
});

$extension->on('command-2', function (CommandInteraction $firedCommand) use (&$hasRun) {
$hasRun[1] = true;
});

$extension->on('command-3', function (CommandInteraction $firedCommand) use (&$hasRun) {
$hasRun[2] = true;
});

$interaction = new InteractionCreate();
$interaction->type = InteractionType::APPLICATION_COMMAND;
$interaction->data = new InteractionData();
$interaction->data->name = 'command-1';

$this->discord->gateway->events->emit(
Events::INTERACTION_CREATE,
[$interaction]
);

$interaction->data->name = 'command-2';

$this->discord->gateway->events->emit(
Events::INTERACTION_CREATE,
[$interaction]
);

$this->assertTrue($hasRun[0], 'Command 1 did not run');
$this->assertTrue($hasRun[1], 'Command 2 did not run');
$this->assertFalse($hasRun[2], 'Command 3 should not have been run');
}

public static function nameMappingProvider(): array
{
return [
'Plain name' => [
'interaction' => (function () {
$command = new InteractionCreate();
$command->type = InteractionType::APPLICATION_COMMAND;
$command->data = new InteractionData();
$command->data->name = 'command-name';

return $command;
})(),
'expectedName' => 'command-name'
],

'Nested 1 layer' => [
'interaction' => (function () {
$command = new InteractionCreate();
$command->type = InteractionType::APPLICATION_COMMAND;
$command->data = new InteractionData();
$command->data->name = 'command-name';

$command->data->options = [new ApplicationCommandInteractionDataOptionStructure()];
$command->data->options[0]->name = 'sub';
$command->data->options[0]->type = ApplicationCommandOptionType::SUB_COMMAND;

return $command;
})(),
'expectedName' => 'command-name.sub'
],

'Nested 2 layer' => [
'interaction' => (function () {
$command = new InteractionCreate();
$command->type = InteractionType::APPLICATION_COMMAND;
$command->data = new InteractionData();
$command->data->name = 'command-name';

$command->data->options = [new ApplicationCommandInteractionDataOptionStructure()];
$command->data->options[0]->name = 'double';
$command->data->options[0]->type = ApplicationCommandOptionType::SUB_COMMAND_GROUP;

$command->data->options[0]->options = [new ApplicationCommandInteractionDataOptionStructure()];
$command->data->options[0]->options[0]->name = 'sub';
$command->data->options[0]->options[0]->type = ApplicationCommandOptionType::SUB_COMMAND_GROUP;

return $command;
})(),
'expectedName' => 'command-name.double.sub'
],

'Nested 3 layer' => [ // NOTE: Not supported by Discord
'interaction' => (function () {
$command = new InteractionCreate();
$command->type = InteractionType::APPLICATION_COMMAND;
$command->data = new InteractionData();
$command->data->name = 'command-name';

$command->data->options = [new ApplicationCommandInteractionDataOptionStructure()];
$command->data->options[0]->name = 'double';
$command->data->options[0]->type = ApplicationCommandOptionType::SUB_COMMAND_GROUP;

$command->data->options[0]->options = [new ApplicationCommandInteractionDataOptionStructure()];
$command->data->options[0]->options[0]->name = 'sub';
$command->data->options[0]->options[0]->type = ApplicationCommandOptionType::SUB_COMMAND_GROUP;

$command->data->options[0]->options[0]->options = [new ApplicationCommandInteractionDataOptionStructure()];
$command->data->options[0]->options[0]->options[0]->name = 'dub';
$command->data->options[0]->options[0]->options[0]->type = ApplicationCommandOptionType::SUB_COMMAND_GROUP;

return $command;
})(),
'expectedName' => 'command-name.double.sub.dub'
],
];
}
}

0 comments on commit 80b3374

Please sign in to comment.