From cc8f6625a10592f46d1efec3be76e296ce7ea5d2 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 16 Jun 2026 17:05:13 -0300 Subject: [PATCH] feat(channels): Discord post-level engagement metrics Add per-post Discord metrics (server member count, thread replies as comments, and reaction counts) read live with the bot token via GET message + guild with_counts, wired into PostMetricsFetcher. Redesign the shared post metrics row: monochrome line icons for member/comment counts vs clean reaction pills, so audience stats read distinctly from reactions. Applies to Telegram too. --- app/Services/Post/PostMetricsFetcher.php | 2 + .../Social/Discord/DiscordAnalytics.php | 107 ++++++++++++++++++ app/Services/Social/Discord/DiscordClient.php | 7 +- lang/en/analytics.php | 1 + lang/es/analytics.php | 1 + lang/pt-BR/analytics.php | 1 + .../components/posts/PostPlatformMetrics.vue | 46 +++++--- .../Services/Social/DiscordAnalyticsTest.php | 85 ++++++++++++++ 8 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 app/Services/Social/Discord/DiscordAnalytics.php create mode 100644 tests/Feature/Services/Social/DiscordAnalyticsTest.php diff --git a/app/Services/Post/PostMetricsFetcher.php b/app/Services/Post/PostMetricsFetcher.php index de26dff9..b68e3305 100644 --- a/app/Services/Post/PostMetricsFetcher.php +++ b/app/Services/Post/PostMetricsFetcher.php @@ -8,6 +8,7 @@ use App\Models\Post; use App\Models\PostPlatform; use App\Services\Social\BlueskyAnalytics; +use App\Services\Social\Discord\DiscordAnalytics; use App\Services\Social\FacebookAnalytics; use App\Services\Social\InstagramAnalytics; use App\Services\Social\LinkedInPageAnalytics; @@ -66,6 +67,7 @@ public function forPlatform(PostPlatform $postPlatform): array Platform::Bluesky => app(BlueskyAnalytics::class)->fetchPostMetrics($postPlatform), Platform::Mastodon => app(MastodonAnalytics::class)->fetchPostMetrics($postPlatform), Platform::Telegram => app(TelegramAnalytics::class)->fetchPostMetrics($postPlatform), + Platform::Discord => app(DiscordAnalytics::class)->fetchPostMetrics($postPlatform), Platform::Instagram, Platform::InstagramFacebook => app(InstagramAnalytics::class)->fetchPostMetrics($postPlatform), Platform::Facebook => app(FacebookAnalytics::class)->fetchPostMetrics($postPlatform), Platform::Threads => app(ThreadsAnalytics::class)->fetchPostMetrics($postPlatform), diff --git a/app/Services/Social/Discord/DiscordAnalytics.php b/app/Services/Social/Discord/DiscordAnalytics.php new file mode 100644 index 00000000..db0d9f0c --- /dev/null +++ b/app/Services/Social/Discord/DiscordAnalytics.php @@ -0,0 +1,107 @@ + + */ + public function getMetrics(SocialAccount $account): array + { + $guildId = (string) $account->platform_user_id; + + if ($guildId === '') { + return []; + } + + try { + $response = $this->client->getGuild($guildId); + } catch (Throwable) { + return []; + } + + if ($response->failed()) { + return []; + } + + $count = data_get($response->json(), 'approximate_member_count'); + + if (! is_int($count)) { + return []; + } + + return [ + ['label' => __('analytics.metrics.members'), 'value' => $count], + ]; + } + + /** + * Post-level metrics: thread replies plus reaction counts per emoji, tagged + * so the UI renders them as pills alongside the member count. + * + * @return array + */ + public function fetchPostMetrics(PostPlatform $postPlatform): array + { + $account = $postPlatform->socialAccount; + $metrics = $account + ? array_map(fn (array $metric): array => [...$metric, 'kind' => 'subscribers'], $this->getMetrics($account)) + : []; + + $channelId = (string) data_get($postPlatform->meta, 'channel_id'); + $messageId = (string) $postPlatform->platform_post_id; + + if ($channelId === '' || $messageId === '') { + return $metrics; + } + + try { + $response = $this->client->getMessage($channelId, $messageId); + } catch (Throwable) { + return $metrics; + } + + if ($response->failed()) { + return $metrics; + } + + $message = $response->json(); + + foreach ((array) data_get($message, 'reactions', []) as $reaction) { + $name = (string) data_get($reaction, 'emoji.name'); + $label = data_get($reaction, 'emoji.id') ? ":{$name}:" : $name; + + $metrics[] = [ + 'label' => $label !== '' ? $label : __('analytics.metrics.custom_reaction'), + 'value' => (int) data_get($reaction, 'count'), + 'kind' => 'reaction', + ]; + } + + // Thread replies are their own metric (kind "comments") so the UI renders + // them as a 💬 pill next to the member count, with a distinguishing tooltip. + $replies = (int) data_get($message, 'thread.message_count', 0); + + if ($replies > 0) { + $metrics[] = ['label' => __('analytics.metrics.comments'), 'value' => $replies, 'kind' => 'comments']; + } + + return $metrics; + } +} diff --git a/app/Services/Social/Discord/DiscordClient.php b/app/Services/Social/Discord/DiscordClient.php index c1bce942..ac83a243 100644 --- a/app/Services/Social/Discord/DiscordClient.php +++ b/app/Services/Social/Discord/DiscordClient.php @@ -242,7 +242,12 @@ private function getList(string $url, array $query = []): array public function getGuild(string $guildId): Response { - return $this->bot()->get("{$this->baseUrl()}/guilds/{$guildId}"); + return $this->bot()->get("{$this->baseUrl()}/guilds/{$guildId}", ['with_counts' => 'true']); + } + + public function getMessage(string $channelId, string $messageId): Response + { + return $this->bot()->get("{$this->baseUrl()}/channels/{$channelId}/messages/{$messageId}"); } private function bot(): PendingRequest diff --git a/lang/en/analytics.php b/lang/en/analytics.php index ee6fbf22..99c8190c 100644 --- a/lang/en/analytics.php +++ b/lang/en/analytics.php @@ -21,6 +21,7 @@ 'impressions' => 'Impressions', 'interactions' => 'Interactions', 'likes' => 'Likes', + 'members' => 'Members', 'minutes_watched' => 'Minutes Watched', 'organic_followers' => 'Organic Followers', 'outbound_clicks' => 'Outbound Clicks', diff --git a/lang/es/analytics.php b/lang/es/analytics.php index 4f0a4ab0..da4d2a40 100644 --- a/lang/es/analytics.php +++ b/lang/es/analytics.php @@ -21,6 +21,7 @@ 'impressions' => 'Impresiones', 'interactions' => 'Interacciones', 'likes' => 'Me gusta', + 'members' => 'Miembros', 'minutes_watched' => 'Minutos Vistos', 'organic_followers' => 'Seguidores Orgánicos', 'outbound_clicks' => 'Clics Externos', diff --git a/lang/pt-BR/analytics.php b/lang/pt-BR/analytics.php index 95e2139d..bad4c550 100644 --- a/lang/pt-BR/analytics.php +++ b/lang/pt-BR/analytics.php @@ -21,6 +21,7 @@ 'impressions' => 'Impressões', 'interactions' => 'Interações', 'likes' => 'Curtidas', + 'members' => 'Membros', 'minutes_watched' => 'Minutos Assistidos', 'organic_followers' => 'Seguidores Orgânicos', 'outbound_clicks' => 'Cliques Externos', diff --git a/resources/js/components/posts/PostPlatformMetrics.vue b/resources/js/components/posts/PostPlatformMetrics.vue index bf4fb35c..385105d7 100644 --- a/resources/js/components/posts/PostPlatformMetrics.vue +++ b/resources/js/components/posts/PostPlatformMetrics.vue @@ -1,6 +1,6 @@