baseUrl = config('trypost.platforms.threads.graph_api'); } public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); $until ??= now(); $cacheKey = "analytics:threads:{$account->id}:{$since->format('Y-m-d')}:{$until->format('Y-m-d')}"; $cacheTtl = app()->isProduction() ? 3600 : 1; return Cache::remember($cacheKey, $cacheTtl, function () use ($account, $since, $until) { return $this->fetchMetricsFromApi($account, $since, $until); }); } public function fetchPostMetrics(PostPlatform $postPlatform): array { $account = $postPlatform->socialAccount; if (! $account || ! $postPlatform->platform_post_id) { return ['unsupported' => true, 'reason' => 'missing_post_id']; } if ($account->is_token_expired || $account->is_token_expiring_soon) { $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); $account->refresh(); } $response = $this->socialHttp() ->withToken($account->access_token) ->get("{$this->baseUrl}/{$postPlatform->platform_post_id}/insights", [ 'metric' => 'views,likes,replies,reposts,quotes', ]); if ($response->failed()) { Log::warning('Threads post metrics fetch failed', [ 'body' => $this->redactResponseBody($response->body()), ]); return ['unsupported' => true, 'reason' => 'api_error']; } $data = data_get($response->json(), 'data', []); return collect($data) ->map(fn (array $item) => [ 'label' => ucfirst(str_replace('_', ' ', data_get($item, 'name', ''))), 'value' => (int) data_get($item, 'values.0.value', 0), ]) ->values() ->all(); } private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); $account->refresh(); } $this->accessToken = $account->access_token; $response = $this->getHttpClient() ->get("{$this->baseUrl}/{$account->platform_user_id}/threads_insights", [ 'metric' => 'views,likes,replies,reposts,quotes', 'period' => 'day', 'since' => $since->startOfDay()->unix(), 'until' => $until->endOfDay()->unix(), 'access_token' => $this->accessToken, ]); if ($response->failed()) { Log::warning('Threads insights fetch failed', [ 'body' => $this->redactResponseBody($response->body()), ]); return []; } $data = data_get($response->json(), 'data', []); $metrics = []; foreach ($data as $metric) { $name = data_get($metric, 'name'); // Some metrics return total_value, others return values array $totalValue = data_get($metric, 'total_value.value'); if ($totalValue !== null) { $value = $totalValue; } else { $values = data_get($metric, 'values', []); $value = collect($values)->sum('value'); } $label = ucfirst(str_replace('_', ' ', $name)); $metrics[] = ['label' => $label, 'value' => $value]; } return $metrics; } private function getHttpClient(): PendingRequest { return $this->socialHttp(); } private function refreshToken(SocialAccount $account): void { $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, ]); if ($response->failed()) { Log::error('Threads token refresh failed', ['body' => $this->redactResponseBody($response->body())]); throw new TokenExpiredException('Threads token refresh failed'); } $data = $response->json(); $account->update([ 'access_token' => data_get($data, 'access_token'), 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); } }