From fb8fb6ac19307b778f53057492870cdddec4d072 Mon Sep 17 00:00:00 2001 From: 1pride Date: Tue, 5 May 2026 19:55:29 -0300 Subject: [PATCH] fix(voice-channel): improve channel deletion and state handling logic --- .env.example | 1 + .../VoiceChannel/DeleteVoiceChannelAction.php | 94 ++++++++++++++++--- .../VoiceChannel/HandleStateChannelAction.php | 44 ++++++++- .../bot-discord/src/DTO/VoiceChannelDTO.php | 12 ++- .../src/SlashCommands/DynamicVoiceCommand.php | 14 ++- .../HandleStateChannelActionTest.php | 41 +++++--- 6 files changed, 177 insertions(+), 29 deletions(-) diff --git a/.env.example b/.env.example index 62dad216..6e1a71fc 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,7 @@ LARAVEL_CLOUDFLARE_ENABLED=false BROADCAST_DRIVER=log CACHE_DRIVER=file +CACHE_STORE=redis FILESYSTEM_DISK=local QUEUE_CONNECTION=sync SESSION_DRIVER=file diff --git a/app-modules/bot-discord/src/Actions/VoiceChannel/DeleteVoiceChannelAction.php b/app-modules/bot-discord/src/Actions/VoiceChannel/DeleteVoiceChannelAction.php index 397e8163..f86b8c20 100644 --- a/app-modules/bot-discord/src/Actions/VoiceChannel/DeleteVoiceChannelAction.php +++ b/app-modules/bot-discord/src/Actions/VoiceChannel/DeleteVoiceChannelAction.php @@ -6,21 +6,76 @@ use Discord\Discord; use He4rt\BotDiscord\DTO\VoiceChannelDTO; +use stdClass; final class DeleteVoiceChannelAction { public function execute(Discord $discord): void { $channels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys', []); - foreach ($channels as $index => $channel) { - /** @var VoiceChannelDTO $channel */ - if ($channel->isEmpty() && $channel->isLongTermEmpty()) { - $this->delete($channel->guildId, $channel->channelId, $index, $discord); + + $validChannels = []; + $hasInvalid = false; + + foreach ($channels as $channel) { + $dto = $this->normalizeChannel($channel); + + if (!$dto instanceof VoiceChannelDTO) { + $hasInvalid = true; + + continue; + } + + $validChannels[] = $dto; + + if ($dto->isEmpty() && $dto->isLongTermEmpty()) { + $this->delete($dto->guildId, $dto->channelId, $discord); } } + + if ($hasInvalid) { + cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $this->dtosToArrays($validChannels)); + } + } + + /** + * @param array $dtos + * @return array> + */ + private function dtosToArrays(array $dtos): array + { + return array_map(fn (VoiceChannelDTO $dto) => [ + 'guildId' => $dto->guildId, + 'channelId' => $dto->channelId, + 'ownerId' => $dto->ownerId, + 'usersCount' => $dto->usersCount, + 'users' => $dto->users, + 'lastJoinedAt' => $dto->lastJoinedAt?->toIso8601String(), + ], $dtos); + } + + private function normalizeChannel(mixed $channel): ?VoiceChannelDTO + { + if ($channel instanceof VoiceChannelDTO) { + return $channel; + } + + $data = null; + + if (is_array($channel)) { + $data = $channel; + } elseif ($channel instanceof stdClass) { + $data = (array) $channel; + } + + if ($data !== null && isset($data['guildId'], $data['channelId'], $data['ownerId'])) { + return VoiceChannelDTO::make($data); + } + + return null; } - private function delete(string $guildId, string $channelId, int $arrayIndex, Discord $discord): void + private function delete(string $guildId, string $channelId, Discord $discord): void { $guild = $discord->guilds->get('id', $guildId); @@ -30,14 +85,31 @@ private function delete(string $guildId, string $channelId, int $arrayIndex, Dis $channels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys', []); - if (isset($channels[$arrayIndex])) { + $filtered = array_values(array_filter($channels, function ($channel) use ($channelId): bool { + $dto = $this->normalizeChannel($channel); - unset($channels[$arrayIndex]); + return !$dto instanceof VoiceChannelDTO || $dto->channelId !== $channelId; + })); - $channels = array_filter($channels, fn (VoiceChannelDTO $channel) => $channel->channelId !== $channelId); - $channels = array_values($channels); + $filteredArrays = array_map(function ($channel): ?array { + $dto = $this->normalizeChannel($channel); - cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $channels); - } + if (!$dto instanceof VoiceChannelDTO) { + return null; + } + + return [ + 'guildId' => $dto->guildId, + 'channelId' => $dto->channelId, + 'ownerId' => $dto->ownerId, + 'usersCount' => $dto->usersCount, + 'users' => $dto->users, + 'lastJoinedAt' => $dto->lastJoinedAt?->toIso8601String(), + ]; + }, $filtered); + $filteredArrays = array_filter($filteredArrays); + $filteredArrays = array_values($filteredArrays); + + cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $filteredArrays); } } diff --git a/app-modules/bot-discord/src/Actions/VoiceChannel/HandleStateChannelAction.php b/app-modules/bot-discord/src/Actions/VoiceChannel/HandleStateChannelAction.php index 202f09a5..69a2a142 100644 --- a/app-modules/bot-discord/src/Actions/VoiceChannel/HandleStateChannelAction.php +++ b/app-modules/bot-discord/src/Actions/VoiceChannel/HandleStateChannelAction.php @@ -4,11 +4,13 @@ namespace He4rt\BotDiscord\Actions\VoiceChannel; +use He4rt\BotDiscord\DTO\VoiceChannelDTO; + final class HandleStateChannelAction { public function execute(int|string $userId, ?string $channelId): void { - $activeChannels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys', []); + $activeChannels = $this->loadChannels(); $oldChannelId = $this->getUserLastChannel($userId); if ($this->isLeavingVoice($channelId, $oldChannelId)) { @@ -16,6 +18,7 @@ public function execute(int|string $userId, ?string $channelId): void activeChannels: $activeChannels, user: $userId ); + $this->saveChannels($activeChannels); $this->clearUserLastChannel($userId); @@ -33,6 +36,7 @@ public function execute(int|string $userId, ?string $channelId): void activeChannels: $activeChannels, user: $userId ); + $this->saveChannels($activeChannels); $this->setUserLastChannel($userId, $channelId); @@ -45,6 +49,7 @@ public function execute(int|string $userId, ?string $channelId): void activeChannels: $activeChannels, user: $userId ); + $this->saveChannels($activeChannels); $this->setUserLastChannel($userId, $channelId); return; @@ -94,4 +99,41 @@ private function isUpdatingInSameChannel(?string $newChannelId, ?string $oldChan { return !is_null($newChannelId) && $oldChannelId === $newChannelId; } + + /** + * @return array + */ + private function loadChannels(): array + { + $channels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys', []); + + return array_map(function ($channel): ?VoiceChannelDTO { + if ($channel instanceof VoiceChannelDTO) { + return $channel; + } + + if (is_array($channel) && isset($channel['guildId'], $channel['channelId'], $channel['ownerId'])) { + return VoiceChannelDTO::make($channel); + } + + return null; + }, $channels); + } + + /** + * @param array $channels + */ + private function saveChannels(array $channels): void + { + $arrays = array_map(fn (VoiceChannelDTO $dto) => [ + 'guildId' => $dto->guildId, + 'channelId' => $dto->channelId, + 'ownerId' => $dto->ownerId, + 'usersCount' => $dto->usersCount, + 'users' => $dto->users, + 'lastJoinedAt' => $dto->lastJoinedAt?->toIso8601String(), + ], $channels); + + cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $arrays); + } } diff --git a/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php b/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php index dfca5af9..5d93f95d 100644 --- a/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php +++ b/app-modules/bot-discord/src/DTO/VoiceChannelDTO.php @@ -5,6 +5,7 @@ namespace He4rt\BotDiscord\DTO; use Carbon\CarbonInterface; +use Illuminate\Support\Facades\Date; final class VoiceChannelDTO { @@ -23,13 +24,22 @@ public function __construct( */ public static function make(array $data): self { + $lastJoinedAt = null; + if (filled($data['lastJoinedAt'])) { + if ($data['lastJoinedAt'] instanceof CarbonInterface) { + $lastJoinedAt = $data['lastJoinedAt']; + } elseif (is_string($data['lastJoinedAt'])) { + $lastJoinedAt = Date::parse($data['lastJoinedAt']); + } + } + return new self( guildId: $data['guildId'], channelId: $data['channelId'], ownerId: $data['ownerId'], usersCount: $data['usersCount'], users: $data['users'], - lastJoinedAt: $data['lastJoinedAt'] ?? null, + lastJoinedAt: $lastJoinedAt, ); } diff --git a/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php b/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php index 6c677dcb..2c3a29b4 100644 --- a/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/DynamicVoiceCommand.php @@ -81,7 +81,7 @@ public function handle(Interaction $interaction): void 'users' => [], 'lastJoinedAt' => now(), ]); - $channels[] = $channelDto; + $channels[] = $this->dtoToArray($channelDto); cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $channels); @@ -149,4 +149,16 @@ private function interactionWithUser(Interaction $interaction, Channel $channel) ->content(sprintf('Sala Criada com sucesso !! <#%s>', $channel->id)) ->reply($interaction, true); } + + private function dtoToArray(VoiceChannelDTO $dto): array + { + return [ + 'guildId' => $dto->guildId, + 'channelId' => $dto->channelId, + 'ownerId' => $dto->ownerId, + 'usersCount' => $dto->usersCount, + 'users' => $dto->users, + 'lastJoinedAt' => $dto->lastJoinedAt?->toIso8601String(), + ]; + } } diff --git a/app-modules/bot-discord/tests/Feature/Actions/VoiceChannel/HandleStateChannelActionTest.php b/app-modules/bot-discord/tests/Feature/Actions/VoiceChannel/HandleStateChannelActionTest.php index 5f777f63..39eb9db6 100644 --- a/app-modules/bot-discord/tests/Feature/Actions/VoiceChannel/HandleStateChannelActionTest.php +++ b/app-modules/bot-discord/tests/Feature/Actions/VoiceChannel/HandleStateChannelActionTest.php @@ -26,8 +26,24 @@ 'users' => [], ]); - $activeChannels[] = $this->firstChannel; - $activeChannels[] = $this->secondChannel; + $activeChannels = [ + [ + 'guildId' => 'guild-123-id', + 'channelId' => $this->firstChannelId, + 'ownerId' => $this->userId, + 'usersCount' => 0, + 'users' => [], + 'lastJoinedAt' => null, + ], + [ + 'guildId' => 'guild-123-id', + 'channelId' => $this->secondChannelId, + 'ownerId' => $this->userId, + 'usersCount' => 0, + 'users' => [], + 'lastJoinedAt' => null, + ], + ]; cache()->tags(['voice_channels'])->put('active_voice_channels_keys', $activeChannels); }); @@ -37,10 +53,8 @@ $cachedChannels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys'); - /** @var VoiceChannelDTO $activeChannelFromCache */ - $activeChannelFromCache = $cachedChannels[0]; + $activeChannelFromCache = VoiceChannelDTO::make($cachedChannels[0]); expect($cachedChannels)->not->toBeEmpty() - ->and($activeChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($activeChannelFromCache->guildId)->toBe($this->firstChannel->guildId) ->and($activeChannelFromCache->channelId)->toBe($this->firstChannel->channelId) ->and($activeChannelFromCache->usersCount)->toBe(1) @@ -57,9 +71,8 @@ $cachedChannels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys'); - $firstChannelFromCache = $cachedChannels[0]; + $firstChannelFromCache = VoiceChannelDTO::make($cachedChannels[0]); expect($cachedChannels)->not->toBeEmpty() - ->and($firstChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($firstChannelFromCache->guildId)->toBe($this->firstChannel->guildId) ->and($firstChannelFromCache->channelId)->toBe($this->firstChannel->channelId) ->and($firstChannelFromCache->usersCount)->toBe(1) @@ -71,9 +84,8 @@ expect($userLastChannel)->not->toBeEmpty() ->and($userLastChannel)->toBe($this->firstChannel->channelId); - $secondChannelFromCache = $cachedChannels[1]; + $secondChannelFromCache = VoiceChannelDTO::make($cachedChannels[1]); expect($cachedChannels)->not->toBeEmpty() - ->and($secondChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($secondChannelFromCache->channelId)->toBe($this->secondChannel->channelId) ->and($secondChannelFromCache->usersCount)->toBe(0) ->and($secondChannelFromCache->lastJoinedAt)->toBeNull() @@ -83,8 +95,9 @@ $action->execute($this->userId, $this->secondChannelId); + $cachedChannels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys'); + $firstChannelFromCache = VoiceChannelDTO::make($cachedChannels[0]); expect($cachedChannels)->not->toBeEmpty() - ->and($firstChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($firstChannelFromCache->guildId)->toBe($this->firstChannel->guildId) ->and($firstChannelFromCache->channelId)->toBe($this->firstChannel->channelId) ->and($firstChannelFromCache->usersCount)->toBe(0) @@ -94,9 +107,9 @@ // user is now at second channel $userLastChannel = cache()->tags(['voice_tracking'])->get('user_last_channel_'.$this->userId); + $secondChannelFromCache = VoiceChannelDTO::make($cachedChannels[1]); expect($userLastChannel)->toBe($this->secondChannel->channelId) ->and($cachedChannels)->not->toBeEmpty() - ->and($secondChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($secondChannelFromCache->channelId)->toBe($this->secondChannel->channelId) ->and($secondChannelFromCache->usersCount)->toBe(1) ->and($secondChannelFromCache->lastJoinedAt)->not->toBeNull() @@ -111,9 +124,8 @@ $cachedChannels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys'); - $firstChannelFromCache = $cachedChannels[0]; + $firstChannelFromCache = VoiceChannelDTO::make($cachedChannels[0]); expect($cachedChannels)->not->toBeEmpty() - ->and($firstChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($firstChannelFromCache->guildId)->toBe($this->firstChannel->guildId) ->and($firstChannelFromCache->channelId)->toBe($this->firstChannel->channelId) ->and($firstChannelFromCache->usersCount)->toBe(1) @@ -140,10 +152,9 @@ expect($userLastChannel)->toBeNull(); $cachedChannels = cache()->tags(['voice_channels'])->get('active_voice_channels_keys'); - $firstChannelFromCache = $cachedChannels[0]; + $firstChannelFromCache = VoiceChannelDTO::make($cachedChannels[0]); expect($cachedChannels)->not->toBeEmpty() - ->and($firstChannelFromCache)->toBeInstanceOf(VoiceChannelDTO::class) ->and($firstChannelFromCache->guildId)->toBe($this->firstChannel->guildId) ->and($firstChannelFromCache->channelId)->toBe($this->firstChannel->channelId) ->and($firstChannelFromCache->usersCount)->toBe(0)