Skip to content

Commit

Permalink
Merge pull request #15 from dc-Ragnarok/feat/message-components
Browse files Browse the repository at this point in the history
  • Loading branch information
Exanlv authored Mar 18, 2023
2 parents 6fa6ec1 + b1965ea commit 8dce8a9
Show file tree
Hide file tree
Showing 33 changed files with 495 additions and 137 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ discord.log
.phpunit.cache
.php-cs-fixer.cache
composer.lock
.vscode
3 changes: 1 addition & 2 deletions fakes/DiscordFake.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Fakes\Exan\Fenrir;

use Exan\Fenrir\Discord;
use Exan\Fenrir\Gateway;
use Mockery;
use Mockery\Mock;

Expand All @@ -17,7 +16,7 @@ public static function get(): Mock|Discord

$discord->rest = RestFake::get();
$discord->gateway = GatewayFake::get();
$discord->command = CommandHandlerFake::get();
$discord->interaction = InteractionHandlerFake::get();

return $discord;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace Fakes\Exan\Fenrir;

use Exan\Fenrir\CommandHandler;
use Exan\Fenrir\InteractionHandler;
use Exan\Fenrir\Rest\Helpers\Command\CommandBuilder;
use PHPUnit\Framework\Assert;

class CommandHandlerFake extends CommandHandler
class InteractionHandlerFake extends InteractionHandler
{
public array $dynamicCommands = [];
public array $guildCommands = [];
Expand All @@ -18,7 +18,7 @@ public function __construct()
{
}

public static function get(): CommandHandlerFake
public static function get(): InteractionHandler
{
return new static();
}
Expand Down
4 changes: 2 additions & 2 deletions src/Component/Button/InteractionButton.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ abstract class InteractionButton extends Component
protected ButtonStyle $style;

public function __construct(
protected string $customId,
public readonly string $customId,
protected ?string $label = null,
protected ?EmojiBuilder $emoji = null,
protected bool $disabled = false
Expand All @@ -24,7 +24,7 @@ public function get(): array
{
$data = [
'type' => 2,
'style' => $this->style,
'style' => $this->style->value,
'custom_id' => $this->customId,
'disabled' => $this->disabled,
];
Expand Down
8 changes: 4 additions & 4 deletions src/Discord.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Discord

public Rest $rest;
public Gateway $gateway;
public CommandHandler $command;
public InteractionHandler $interaction;

public function __construct(
private string $token,
Expand Down Expand Up @@ -73,19 +73,19 @@ public function withRest()

/**
* @param ?string $devGuildId
* When passed, reroute `$this->$command->registerCommand` to be a Guild
* When passed, reroute `$this->interaction->registerCommand` to be a Guild
* command rather than Global. Useful for testing without having to change
* this manually. Explicitly using `registerGlobalCommand` is not affected
*/
public function withCommandHandler(?string $devGuildId = null): self
public function withInteractionHandler(?string $devGuildId = null): self
{
$args = [$this];

if (!is_null($devGuildId) && !empty($devGuildId)) {
$args[] = $devGuildId;
}

$this->command = new CommandHandler(...$args);
$this->interaction = new InteractionHandler(...$args);

return $this;
}
Expand Down
27 changes: 27 additions & 0 deletions src/Interaction/ButtonInteraction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Exan\Fenrir\Interaction;

use Exan\Fenrir\Discord;
use Exan\Fenrir\Interaction\Helpers\InteractionCallbackBuilder;
use Exan\Fenrir\Websocket\Events\InteractionCreate;
use React\Promise\ExtendedPromiseInterface;

class ButtonInteraction
{
public function __construct(public readonly InteractionCreate $interaction, private Discord $discord)
{
}

public function createInteractionResponse(
InteractionCallbackBuilder $interactionCallbackBuilder
): ExtendedPromiseInterface {
return $this->discord->rest->webhook->createInteractionResponse(
$this->interaction->id,
$this->interaction->token,
$interactionCallbackBuilder
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

declare(strict_types=1);

namespace Exan\Fenrir\Command;
namespace Exan\Fenrir\Interaction;

use Exan\Fenrir\Command\Helpers\InteractionCallbackBuilder;
use Exan\Fenrir\Interaction\Helpers\InteractionCallbackBuilder;
use Exan\Fenrir\Discord;
use Exan\Fenrir\Enums\Parts\ApplicationCommandOptionTypes as OptionTypes;
use Exan\Fenrir\Parts\ApplicationCommandInteractionDataOptionStructure as OptionStructure;
use Exan\Fenrir\Rest\Helpers\Webhook\EditWebhookBuilder;
use Exan\Fenrir\Websocket\Events\InteractionCreate;
use React\Promise\ExtendedPromiseInterface;

class FiredCommand
class CommandInteraction
{
/** @var OptionStructure[] */
private array $options = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

declare(strict_types=1);

namespace Exan\Fenrir\Command\Helpers;
namespace Exan\Fenrir\Interaction\Helpers;

use Discord\Http\Multipart\MultipartBody;
use Exan\Fenrir\Enums\Command\InteractionCallbackTypes;
use Exan\Fenrir\Rest\Helpers\Channel\ComponentBuilder;
use Exan\Fenrir\Rest\Helpers\Channel\EmbedBuilder;
use Exan\Fenrir\Rest\Helpers\Channel\Message\AddComponent;
use Exan\Fenrir\Rest\Helpers\Channel\Message\AddEmbed;
Expand Down Expand Up @@ -52,10 +51,7 @@ public function get(): array|MultipartBody
$callbackData = $this->data;

if ($this->hasComponents()) {
$callbackData['components'] = array_map(
fn (ComponentBuilder $component) => $component->get(),
$this->getComponents()
);
$callbackData['components'] = $this->getComponents()->get();
}

if ($this->hasEmbeds()) {
Expand Down
149 changes: 149 additions & 0 deletions src/InteractionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace Exan\Fenrir;

use Exan\Fenrir\Component\Button\InteractionButton;
use Exan\Fenrir\Constants\Events;
use Exan\Fenrir\Enums\Parts\InteractionTypes;
use Exan\Fenrir\Interaction\ButtonInteraction;
use Exan\Fenrir\Interaction\CommandInteraction;
use Exan\Fenrir\Parts\ApplicationCommand;
use Exan\Fenrir\Rest\Helpers\Command\CommandBuilder;
use Exan\Fenrir\Websocket\Events\InteractionCreate;
use Exan\Fenrir\Websocket\Events\Ready;

class InteractionHandler
{
private FilteredEventEmitter $commandListener;
private FilteredEventEmitter $buttonListener;

/** @var array<string, callable> */
private array $handlersCommand = [];

/** @var array<string, callable> */
private array $handlersButton = [];

private bool $devMode = false;

public function __construct(private Discord $discord, private ?string $devGuildId = null)
{
if (!is_null($this->devGuildId)) {
$this->devMode = true;
}
}

public function registerCommand(CommandBuilder $commandBuilder, callable $handler): void
{
if ($this->devMode) {
$this->registerGuildCommand($commandBuilder, $this->devGuildId, $handler);
} else {
$this->registerGlobalCommand($commandBuilder, $handler);
}
}

public function registerGuildCommand(CommandBuilder $commandBuilder, string $guildId, callable $handler): void
{
$this->activateCommandListener();

/** Ready event includes Application ID */
$this->discord->gateway->events->once(
Events::READY,
function (Ready $ready) use ($commandBuilder, $guildId, $handler) {
$this->discord->rest->guildCommand->createApplicationCommand(
$ready->user->id,
$guildId,
$commandBuilder
)->then(function (ApplicationCommand $applicationCommand) use ($handler) {
$this->handlersCommand[$applicationCommand->id] = $handler;
});
}
);
}

public function registerGlobalCommand(CommandBuilder $commandBuilder, callable $handler): void
{
$this->activateCommandListener();

/** Ready event includes Application ID */
$this->discord->gateway->events->once(Events::READY, function (Ready $ready) use ($commandBuilder, $handler) {
$this->discord->rest->globalCommand->createApplicationCommand(
$ready->user->id,
$commandBuilder
)->then(function (ApplicationCommand $applicationCommand) use ($handler) {
$this->handlersCommand[$applicationCommand->id] = $handler;
});
});
}

private function activateCommandListener()
{
if (isset($this->commandListener)) {
return;
}

$this->commandListener = new FilteredEventEmitter(
$this->discord->gateway->events,
Events::INTERACTION_CREATE,
fn (InteractionCreate $interactionCreate) =>
$interactionCreate->type === InteractionTypes::APPLICATION_COMMAND
);

$this->commandListener->on(Events::INTERACTION_CREATE, $this->handleCommandInteraction(...));

$this->commandListener->start();
}

private function handleCommandInteraction(InteractionCreate $interactionCreate)
{
if (!isset($this->handlersCommand[$interactionCreate->data->id])) {
return;
}

$firedCommand = new CommandInteraction($interactionCreate, $this->discord);

$this->handlersCommand[$interactionCreate->data->id]($firedCommand);
}

public function onButtonInteraction(InteractionButton $interactionButton, callable $action)
{
$this->activateButtonListener();

$this->handlersButton[$interactionButton->customId] = $action;
}

private function activateButtonListener()
{
if (isset($this->buttonListener)) {
return;
}

$this->buttonListener = new FilteredEventEmitter(
$this->discord->gateway->events,
Events::INTERACTION_CREATE,
fn (InteractionCreate $interactionCreate) =>
$interactionCreate->type === InteractionTypes::MESSAGE_COMPONENT
&& $interactionCreate->data->component_type === 2 // @todo enum
);

$this->buttonListener->on(Events::INTERACTION_CREATE, $this->handleButtonInteraction(...));

$this->buttonListener->start();
}

private function handleButtonInteraction(InteractionCreate $interactionCreate)
{
if (!isset($this->handlersButton[$interactionCreate->data->custom_id])) {
return;
}

$buttonInteraction = new ButtonInteraction($interactionCreate, $this->discord);

$remove = $this->handlersButton[$interactionCreate->data->custom_id]($buttonInteraction);

if ($remove) {
unset($this->handlersButton[$interactionCreate->data->custom_id]);
}
}
}
57 changes: 57 additions & 0 deletions src/Parts/Component.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Exan\Fenrir\Parts;

use Exan\Fenrir\Enums\Component\ButtonStyle;
use Exan\Fenrir\Enums\Parts\ChannelTypes;
use Exan\Fenrir\Enums\Parts\MessageComponentTypes;
use Exan\Fenrir\Parts\Emoji;

class Component
{
public MessageComponentTypes $type;
/**
* @var \Exan\Fenrir\Parts\Component[]
*/
public ?array $components;
public ?ButtonStyle $style;
public ?string $label;
public ?Emoji $emoji;
public ?string $custom_id;
public ?string $url;
public ?bool $disabled;
/**
* @var \Exan\Fenrir\Parts\ComponentSelectOptions[]
*/
public ?array $options; // @todo
/**
* @var \Exan\Fenrir\Enums\Parts\ChannelTypes[]
*/
public ?array $channel_types;
public ?string $placeholder;
public ?int $min_values;
public ?int $max_values;
public ?bool $required;
public ?string $value;

public function setChannelTypes(array $values): void
{
$this->channel_types = [];

foreach ($values as $entry) {
$this->channel_types[] = ChannelTypes::from($entry);
}
}

public function setType(int $value): void
{
$this->type = MessageComponentTypes::from($value);
}

public function setStyle(int $value): void
{
$this->style = ButtonStyle::from($value);
}
}
14 changes: 14 additions & 0 deletions src/Parts/ComponentSelectOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Exan\Fenrir\Parts;

class ComponentSelectOptions
{
public string $label;
public string $value;
public ?string $description;
public ?Emoji $emoji;
public ?bool $default;
}
Loading

0 comments on commit 8dce8a9

Please sign in to comment.