diff --git a/app/Services/Social/Discord/DiscordClient.php b/app/Services/Social/Discord/DiscordClient.php index 70e7b9b0..f7b3bbc9 100644 --- a/app/Services/Social/Discord/DiscordClient.php +++ b/app/Services/Social/Discord/DiscordClient.php @@ -8,6 +8,7 @@ use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response; +use Illuminate\Support\Facades\Cache; /** * Read-side Discord API calls made with the global bot token (channel listing, @@ -22,6 +23,8 @@ class DiscordClient */ private const POSTABLE_CHANNEL_TYPES = [0, 5, 15]; + private const CHANNELS_TTL = 300; + public function baseUrl(): string { return (string) config('trypost.platforms.discord.api'); @@ -38,26 +41,32 @@ public function baseUrl(): string */ public function channels(string $guildId): array { - $response = $this->bot()->get("{$this->baseUrl()}/guilds/{$guildId}/channels"); + // Cached per guild for 5 minutes — channels change rarely and this runs + // both in the composer picker and on every publish (the channel guard), + // all against the shared, rate-limited bot token. A transient failure + // throws and is NOT cached, so the next call retries. + return Cache::remember("discord:channels:{$guildId}", self::CHANNELS_TTL, function () use ($guildId) { + $response = $this->bot()->get("{$this->baseUrl()}/guilds/{$guildId}/channels"); - if ($response->failed()) { - throw new PlatformUnavailableException("Discord channel lookup failed ({$response->status()}).", $response->status()); - } + if ($response->failed()) { + throw new PlatformUnavailableException("Discord channel lookup failed ({$response->status()}).", $response->status()); + } - $channels = $response->json(); + $channels = $response->json(); - if (! is_array($channels)) { - return []; - } + if (! is_array($channels)) { + return []; + } - return collect($channels) - ->filter(fn ($channel) => in_array((int) data_get($channel, 'type'), self::POSTABLE_CHANNEL_TYPES, true)) - ->map(fn ($channel) => [ - 'id' => (string) data_get($channel, 'id'), - 'name' => (string) data_get($channel, 'name'), - ]) - ->values() - ->all(); + return collect($channels) + ->filter(fn ($channel) => in_array((int) data_get($channel, 'type'), self::POSTABLE_CHANNEL_TYPES, true)) + ->map(fn ($channel) => [ + 'id' => (string) data_get($channel, 'id'), + 'name' => (string) data_get($channel, 'name'), + ]) + ->values() + ->all(); + }); } /** diff --git a/lang/en/posts.php b/lang/en/posts.php index 93781fce..6fe57b73 100644 --- a/lang/en/posts.php +++ b/lang/en/posts.php @@ -167,6 +167,8 @@ 'channel' => 'Channel', 'select_channel' => 'Select a channel', 'loading_channels' => 'Loading channels…', + 'search_channel' => 'Search channels…', + 'no_channels' => 'No channels found.', 'channel_required' => 'Select a Discord channel to publish this post.', 'mentions' => 'Mentions', 'search_mention' => 'Mention a role or member…', diff --git a/lang/es/posts.php b/lang/es/posts.php index 1537a9c0..c1da9111 100644 --- a/lang/es/posts.php +++ b/lang/es/posts.php @@ -167,6 +167,8 @@ 'channel' => 'Canal', 'select_channel' => 'Selecciona un canal', 'loading_channels' => 'Cargando canales…', + 'search_channel' => 'Buscar canales…', + 'no_channels' => 'No se encontraron canales.', 'channel_required' => 'Selecciona un canal de Discord para publicar este post.', 'mentions' => 'Menciones', 'search_mention' => 'Menciona un rol o miembro…', diff --git a/lang/pt-BR/posts.php b/lang/pt-BR/posts.php index 01f95f25..831b745e 100644 --- a/lang/pt-BR/posts.php +++ b/lang/pt-BR/posts.php @@ -167,6 +167,8 @@ 'channel' => 'Canal', 'select_channel' => 'Selecione um canal', 'loading_channels' => 'Carregando canais…', + 'search_channel' => 'Buscar canais…', + 'no_channels' => 'Nenhum canal encontrado.', 'channel_required' => 'Selecione um canal do Discord para publicar este post.', 'mentions' => 'Menções', 'search_mention' => 'Mencione um cargo ou membro…', diff --git a/public/images/accounts/discord.png b/public/images/accounts/discord.png index 92b7bde1..90af762a 100644 Binary files a/public/images/accounts/discord.png and b/public/images/accounts/discord.png differ diff --git a/resources/js/components/SearchableSelect.vue b/resources/js/components/SearchableSelect.vue new file mode 100644 index 00000000..fdad0bb2 --- /dev/null +++ b/resources/js/components/SearchableSelect.vue @@ -0,0 +1,83 @@ + + + diff --git a/resources/js/components/posts/editor/DiscordSettings.vue b/resources/js/components/posts/editor/DiscordSettings.vue index 31a8c7e3..c141ea97 100644 --- a/resources/js/components/posts/editor/DiscordSettings.vue +++ b/resources/js/components/posts/editor/DiscordSettings.vue @@ -5,6 +5,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { channels as channelsRoute, mentions as mentionsRoute } from '@/actions/App/Http/Controllers/App/DiscordController'; import InputError from '@/components/InputError.vue'; +import SearchableSelect from '@/components/SearchableSelect.vue'; import { Avatar } from '@/components/ui/avatar'; import { Input } from '@/components/ui/input'; import { usePageErrors } from '@/composables/usePageErrors'; @@ -97,6 +98,8 @@ const channelOptions = computed(() => { return channels.value; }); +const channelSelectOptions = computed(() => channelOptions.value.map((channel) => ({ value: channel.id, label: `#${channel.name}` }))); + const errors = usePageErrors(); const channelError = computed(() => { if (props.meta?.channel_id) { @@ -203,17 +206,15 @@ const updateEmbed = (index: number, patch: Partial) =>

{{ $t('posts.form.discord.channel') }}

- + :invalid="!!channelError" + />
diff --git a/tests/Feature/Discord/DiscordLookupTest.php b/tests/Feature/Discord/DiscordLookupTest.php index 400ac5eb..8792b9b1 100644 --- a/tests/Feature/Discord/DiscordLookupTest.php +++ b/tests/Feature/Discord/DiscordLookupTest.php @@ -6,9 +6,11 @@ use App\Models\SocialAccount; use App\Models\User; use App\Models\Workspace; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; beforeEach(function () { + Cache::flush(); // channel list is cached per guild — isolate between tests config(['trypost.platforms.discord.bot_token' => 'BOTTOKEN']); $this->user = User::factory()->create(); diff --git a/tests/Feature/Services/Social/DiscordPublisherTest.php b/tests/Feature/Services/Social/DiscordPublisherTest.php index 32bf2845..1b09727f 100644 --- a/tests/Feature/Services/Social/DiscordPublisherTest.php +++ b/tests/Feature/Services/Social/DiscordPublisherTest.php @@ -13,9 +13,11 @@ use App\Models\Workspace; use App\Services\Media\MediaOptimizer; use App\Services\Social\Discord\DiscordPublisher; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; beforeEach(function () { + Cache::flush(); // channel list is cached per guild — isolate between tests config(['trypost.platforms.discord.bot_token' => 'BOTTOKEN']); $this->user = User::factory()->create();