From fcb70b3599f2623fe65042194a00b2e78badeada Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:18:58 -0300 Subject: [PATCH] refactor(social): single source of truth for token refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every per-platform publisher and analytics class had its own private refreshToken() implementation, all doing essentially the same thing as ConnectionVerifier's per-platform refresh*Token methods. ~17 copies of near-identical OAuth refresh logic across the codebase, which is how the 5xx→TokenExpired bug got planted in the publish path even after we fixed it on the hourly/daily jobs. Consolidation: - All publish/analytics paths call app(ConnectionVerifier::class) ->refreshToken($account) instead of their own implementation. - 17 private refreshToken() methods deleted. - refreshTokenWithLock helper removed from HasSocialHttpClient trait (its lock semantics are duplicated by ConnectionVerifier's per- account lock). - ConnectionVerifier::refreshLinkedInToken passes $account->platform to TokenRefreshClient so the "LinkedIn" vs "LinkedIn Page" label in error messages is preserved. - TokenRefreshClient now treats HTTP 429 the same as 5xx (raises PlatformUnavailableException) — replaces the retry-on-429 behavior that socialHttp() provided to the deleted refresh methods. Net: -460 LOC. Single per-platform refresh implementation. Every fix or new platform now lands in exactly one place. --- app/Services/Social/BlueskyPublisher.php | 47 +------------------ .../Social/Concerns/HasSocialHttpClient.php | 21 --------- app/Services/Social/ConnectionVerifier.php | 4 +- app/Services/Social/InstagramAnalytics.php | 30 +----------- app/Services/Social/InstagramPublisher.php | 27 +---------- app/Services/Social/LinkedInPageAnalytics.php | 30 +----------- app/Services/Social/LinkedInPagePublisher.php | 30 +----------- app/Services/Social/LinkedInPublisher.php | 30 +----------- app/Services/Social/PinterestAnalytics.php | 30 +----------- app/Services/Social/PinterestPublisher.php | 25 +--------- app/Services/Social/ThreadsAnalytics.php | 21 +-------- app/Services/Social/ThreadsPublisher.php | 24 +--------- app/Services/Social/TikTokAnalytics.php | 28 +---------- app/Services/Social/TikTokCreatorInfo.php | 28 +---------- app/Services/Social/TikTokPublisher.php | 28 +---------- app/Services/Social/TokenRefreshClient.php | 2 +- app/Services/Social/XAnalytics.php | 29 +----------- app/Services/Social/XPublisher.php | 26 +--------- app/Services/Social/YouTubeAnalytics.php | 30 +----------- app/Services/Social/YouTubePublisher.php | 28 +---------- 20 files changed, 29 insertions(+), 489 deletions(-) diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index 68c5fc15..bc7a5b65 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -6,7 +6,6 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\BlueskyPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; @@ -30,7 +29,7 @@ public function publish(PostPlatform $postPlatform): array // Refresh token if needed if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -268,50 +267,6 @@ private function buildPostUrl(string $handle, string $postId): string return "https://bsky.app/profile/{$handle}/post/{$postId}"; } - public function refreshToken(SocialAccount $account): void - { - $service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service'); - $client = TokenRefreshClient::for(Platform::Bluesky); - - try { - $response = $client->send(fn () => $this->socialHttp()->withToken($account->refresh_token) - ->post("{$service}/xrpc/com.atproto.server.refreshSession")); - - $data = $response->json(); - $account->update([ - 'access_token' => data_get($data, 'accessJwt'), - 'refresh_token' => data_get($data, 'refreshJwt'), - 'token_expires_at' => now()->addHours(2), - ]); - - return; - } catch (TokenExpiredException) { - // refresh token rejected (4xx) — fall back to re-auth below - } - - if (isset($account->meta['password'])) { - try { - $reauth = $client->send(fn () => Http::post("{$service}/xrpc/com.atproto.server.createSession", [ - 'identifier' => $account->meta['identifier'], - 'password' => decrypt($account->meta['password']), - ])); - - $data = $reauth->json(); - $account->update([ - 'access_token' => data_get($data, 'accessJwt'), - 'refresh_token' => data_get($data, 'refreshJwt'), - 'token_expires_at' => now()->addHours(2), - ]); - - return; - } catch (TokenExpiredException) { - // re-auth rejected with stored credentials — fall through - } - } - - throw new TokenExpiredException('Bluesky session expired'); - } - private function handleApiError(Response $response): never { throw BlueskyPublishException::fromApiResponse($response); diff --git a/app/Services/Social/Concerns/HasSocialHttpClient.php b/app/Services/Social/Concerns/HasSocialHttpClient.php index 301032a2..1aa41c7c 100644 --- a/app/Services/Social/Concerns/HasSocialHttpClient.php +++ b/app/Services/Social/Concerns/HasSocialHttpClient.php @@ -5,9 +5,7 @@ namespace App\Services\Social\Concerns; use App\Models\PostPlatform; -use App\Models\SocialAccount; use Illuminate\Http\Client\PendingRequest; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; trait HasSocialHttpClient @@ -28,25 +26,6 @@ protected function validateContentLength(PostPlatform $postPlatform): void ); } - protected function refreshTokenWithLock(SocialAccount $account, callable $refreshFn): void - { - $lock = Cache::lock("token_refresh:{$account->id}", 30); - - if (! $lock->get()) { - // Another process is refreshing, wait and reload - sleep(2); - $account->refresh(); - - return; - } - - try { - $refreshFn(); - } finally { - $lock->release(); - } - } - protected function socialHttp(): PendingRequest { return Http::retry( diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index d13bb620..a0b25dfb 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -112,10 +112,10 @@ public function refreshToken(SocialAccount $account): void private function refreshLinkedInToken(SocialAccount $account): void { if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn account'); + throw new TokenExpiredException("No refresh token available for {$account->platform->label()} account"); } - $response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm() + $response = TokenRefreshClient::for($account->platform)->send(fn () => Http::asForm() ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, diff --git a/app/Services/Social/InstagramAnalytics.php b/app/Services/Social/InstagramAnalytics.php index 126a4626..de677310 100644 --- a/app/Services/Social/InstagramAnalytics.php +++ b/app/Services/Social/InstagramAnalytics.php @@ -5,15 +5,12 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class InstagramAnalytics @@ -48,7 +45,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -92,7 +89,7 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -200,27 +197,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp(); } - - private function refreshToken(SocialAccount $account): void - { - if ($account->platform === Platform::InstagramFacebook) { - return; - } - - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for Instagram account'); - } - - $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ - 'grant_type' => 'ig_refresh_token', - 'access_token' => $account->access_token, - ])); - - $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, - ]); - } } diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index 9ad27dd5..d4da8274 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -5,11 +5,9 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; -use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\InstagramPublishException; use App\Models\PostPlatform; -use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\Response; @@ -32,7 +30,7 @@ public function publish(PostPlatform $postPlatform): array $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -395,29 +393,6 @@ private function waitForMediaProcessing(string $containerId, string $accessToken Log::warning('Instagram media processing timeout, proceeding anyway'); } - private function refreshToken(SocialAccount $account): void - { - // Instagram via Facebook uses page tokens that don't expire - if ($account->platform === Platform::InstagramFacebook) { - return; - } - - $response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ - 'grant_type' => 'ig_refresh_token', - 'access_token' => $account->access_token, - ])); - - $data = $response->json(); - $newToken = data_get($data, 'access_token'); - - $account->update([ - 'access_token' => $newToken, - 'refresh_token' => $newToken, - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - } - private function handleApiError(Response $response): never { throw InstagramPublishException::fromApiResponse($response); diff --git a/app/Services/Social/LinkedInPageAnalytics.php b/app/Services/Social/LinkedInPageAnalytics.php index 98c1d519..3fdba549 100644 --- a/app/Services/Social/LinkedInPageAnalytics.php +++ b/app/Services/Social/LinkedInPageAnalytics.php @@ -4,15 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class LinkedInPageAnalytics @@ -54,7 +51,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -84,7 +81,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array 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)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -234,27 +231,4 @@ private function getHttpClient(): PendingRequest 'X-Restli-Protocol-Version' => '2.0.0', ]); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); - } - - $response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin-openid.client_id'), - 'client_secret' => config('services.linkedin-openid.client_secret'), - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index 7486f5d3..8269a21b 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -46,7 +46,7 @@ public function publish(PostPlatform $postPlatform): array $this->hasRetried = false; if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) { - $this->refreshTokenWithLock($this->account, fn () => $this->refreshToken($this->account)); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); } @@ -81,7 +81,7 @@ private function retryWithRefresh(PostPlatform $postPlatform, ?string $content, $this->hasRetried = true; try { - $this->refreshToken($this->account); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); $this->accessToken = $this->account->access_token; @@ -452,32 +452,6 @@ private function waitForVideoProcessing(string $videoUrn, int $maxAttempts = 30) Log::warning('LinkedIn Page video processing timeout, proceeding anyway'); } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn Page account'); - } - - $response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin-openid.client_id'), - 'client_secret' => config('services.linkedin-openid.client_secret'), - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - // Sync tokens to LinkedIn personal if it exists - app(LinkedInTokenSynchronizer::class)->syncTokens($account); - } - private function handleApiError(Response $response): never { throw LinkedInPublishException::fromApiResponse($response); diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index ed6d230c..567bf1e0 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -46,7 +46,7 @@ public function publish(PostPlatform $postPlatform): array $this->hasRetried = false; if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) { - $this->refreshTokenWithLock($this->account, fn () => $this->refreshToken($this->account)); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); } @@ -75,7 +75,7 @@ private function retryWithRefresh(PostPlatform $postPlatform, ?string $content, $this->hasRetried = true; try { - $this->refreshToken($this->account); + app(ConnectionVerifier::class)->refreshToken($this->account); $this->account->refresh(); $this->accessToken = $this->account->access_token; @@ -433,32 +433,6 @@ private function waitForVideoProcessing(string $videoUrn, int $maxAttempts = 30) Log::warning('LinkedIn video processing timeout, proceeding anyway'); } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for LinkedIn account'); - } - - $response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - 'client_id' => config('services.linkedin.client_id'), - 'client_secret' => config('services.linkedin.client_secret'), - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - // Sync tokens to LinkedIn Page if it exists - app(LinkedInTokenSynchronizer::class)->syncTokens($account); - } - private function handleApiError(Response $response): never { throw LinkedInPublishException::fromApiResponse($response); diff --git a/app/Services/Social/PinterestAnalytics.php b/app/Services/Social/PinterestAnalytics.php index 2a343b9e..d18391a4 100644 --- a/app/Services/Social/PinterestAnalytics.php +++ b/app/Services/Social/PinterestAnalytics.php @@ -4,15 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class PinterestAnalytics @@ -50,7 +47,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -95,7 +92,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array 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)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -161,27 +158,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for Pinterest account'); - } - - $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::withBasicAuth( - config('services.pinterest.client_id'), - config('services.pinterest.client_secret'), - )->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index 116b93f5..5fe1d7b3 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -34,7 +34,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -400,34 +400,13 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId, ); } - private function refreshToken(SocialAccount $account): void - { - $response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::asForm() - ->withBasicAuth( - config('services.pinterest.client_id'), - config('services.pinterest.client_secret') - ) - ->post($this->baseUrl.'/oauth/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : now()->addDays(30), - ]); - } - /** * Get user's boards for board selection. */ public function getBoards(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } diff --git a/app/Services/Social/ThreadsAnalytics.php b/app/Services/Social/ThreadsAnalytics.php index dc245f63..26258979 100644 --- a/app/Services/Social/ThreadsAnalytics.php +++ b/app/Services/Social/ThreadsAnalytics.php @@ -4,14 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ThreadsAnalytics @@ -49,7 +47,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -81,7 +79,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array 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)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -138,19 +136,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp(); } - - private function refreshToken(SocialAccount $account): void - { - $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ - 'grant_type' => 'th_refresh_token', - 'access_token' => $account->access_token, - ])); - - $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, - ]); - } } diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index 135f9dd4..28d3d19a 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -4,13 +4,10 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ThreadsPublishException; use App\Models\PostPlatform; -use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\Response; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ThreadsPublisher @@ -33,7 +30,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -303,25 +300,6 @@ private function waitForMediaProcessing(string $containerId, string $accessToken throw new \Exception('Threads media processing timeout after '.$maxAttempts.' attempts'); } - private function refreshToken(SocialAccount $account): void - { - // Threads uses long-lived tokens that can be refreshed - $response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ - 'grant_type' => 'th_refresh_token', - 'access_token' => $account->access_token, - ])); - - $data = $response->json(); - - $newToken = data_get($data, 'access_token'); - - $account->update([ - 'access_token' => $newToken, - 'refresh_token' => $newToken, - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } - private function handleApiError(Response $response): never { throw ThreadsPublishException::fromApiResponse($response); diff --git a/app/Services/Social/TikTokAnalytics.php b/app/Services/Social/TikTokAnalytics.php index 7532ae3c..a1a80d63 100644 --- a/app/Services/Social/TikTokAnalytics.php +++ b/app/Services/Social/TikTokAnalytics.php @@ -4,13 +4,10 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TikTokAnalytics @@ -39,7 +36,7 @@ public function getMetrics(SocialAccount $account): array private function fetchMetricsFromApi(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -160,27 +157,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->asJson()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for TikTok account'); - } - - $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/TikTokCreatorInfo.php b/app/Services/Social/TikTokCreatorInfo.php index d3fe8ab5..418316bd 100644 --- a/app/Services/Social/TikTokCreatorInfo.php +++ b/app/Services/Social/TikTokCreatorInfo.php @@ -4,13 +4,10 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TikTokCreatorInfo @@ -62,7 +59,7 @@ public function fetch(SocialAccount $account): array private function fetchFresh(SocialAccount $account): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -124,27 +121,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->asJson()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for TikTok account'); - } - - $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/TikTokPublisher.php b/app/Services/Social/TikTokPublisher.php index c0b875d0..554e3402 100644 --- a/app/Services/Social/TikTokPublisher.php +++ b/app/Services/Social/TikTokPublisher.php @@ -7,13 +7,11 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\TikTokPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TikTokPublisher @@ -38,7 +36,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -317,30 +315,6 @@ private function buildTikTokUrl(SocialAccount $account, ?string $postId = null): return null; } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for TikTok account'); - } - - $response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ - 'client_key' => config('services.tiktok.client_id'), - 'client_secret' => config('services.tiktok.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - } - private function handleApiError(Response $response): never { throw TikTokPublishException::fromApiResponse($response); diff --git a/app/Services/Social/TokenRefreshClient.php b/app/Services/Social/TokenRefreshClient.php index 05b1c774..861389f7 100644 --- a/app/Services/Social/TokenRefreshClient.php +++ b/app/Services/Social/TokenRefreshClient.php @@ -49,7 +49,7 @@ public function send(Closure $request): Response throw new PlatformUnavailableException("{$name} API unreachable: {$e->getMessage()}"); } - if ($response->serverError()) { + if ($response->serverError() || $response->status() === 429) { throw new PlatformUnavailableException( "{$name} API returned {$response->status()} during token refresh", $response->status(), diff --git a/app/Services/Social/XAnalytics.php b/app/Services/Social/XAnalytics.php index 4d620260..1b22c7e9 100644 --- a/app/Services/Social/XAnalytics.php +++ b/app/Services/Social/XAnalytics.php @@ -4,8 +4,6 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -49,7 +47,7 @@ public function getMetrics(SocialAccount $account, ?CarbonInterface $since = nul 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)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -165,7 +163,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -200,27 +198,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for X account'); - } - - $response = TokenRefreshClient::for(Platform::X)->send(fn () => $this->socialHttp() - ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) - ->asForm() - ->post(config('trypost.platforms.x.api').'/oauth2/token', [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index 56f1a10a..fefdda53 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -6,9 +6,7 @@ use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\XPublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; -use App\Models\SocialAccount; use App\Services\Media\MediaOptimizer; use App\Services\Social\Concerns\HasSocialHttpClient; use Illuminate\Http\Client\PendingRequest; @@ -39,7 +37,7 @@ public function publish(PostPlatform $postPlatform): array // Refresh token if expired or expiring soon if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -329,28 +327,6 @@ private function waitForProcessing(string $mediaId, int $maxAttempts = 20): bool return false; } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for X account'); - } - - $response = TokenRefreshClient::for(Platform::X)->send(fn () => Http::asForm() - ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) - ->post("{$this->baseUrl}/oauth2/token", [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', 7200)), - ]); - } - private function handleApiError(Response $response): never { throw XPublishException::fromApiResponse($response); diff --git a/app/Services/Social/YouTubeAnalytics.php b/app/Services/Social/YouTubeAnalytics.php index f082667b..43d01248 100644 --- a/app/Services/Social/YouTubeAnalytics.php +++ b/app/Services/Social/YouTubeAnalytics.php @@ -4,15 +4,12 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; use Carbon\CarbonInterface; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class YouTubeAnalytics @@ -50,7 +47,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array } if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -102,7 +99,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array 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)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -162,27 +159,4 @@ private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } - - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for YouTube account'); - } - - $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - } } diff --git a/app/Services/Social/YouTubePublisher.php b/app/Services/Social/YouTubePublisher.php index 3840766d..b3a68fd6 100644 --- a/app/Services/Social/YouTubePublisher.php +++ b/app/Services/Social/YouTubePublisher.php @@ -4,10 +4,8 @@ namespace App\Services\Social; -use App\Enums\SocialAccount\Platform; use App\Exceptions\Social\ErrorCategory; use App\Exceptions\Social\YouTubePublishException; -use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -36,7 +34,7 @@ public function publish(PostPlatform $postPlatform): array $account = $postPlatform->socialAccount; if ($account->is_token_expired || $account->is_token_expiring_soon) { - $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); + app(ConnectionVerifier::class)->refreshToken($account); $account->refresh(); } @@ -224,30 +222,6 @@ private function buildTitle(string $content): string return $title.$shortsTag; } - private function refreshToken(SocialAccount $account): void - { - if (! $account->refresh_token) { - throw new TokenExpiredException('No refresh token available for YouTube account'); - } - - $response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm() - ->post(config('trypost.platforms.youtube.oauth_api').'/token', [ - 'client_id' => config('services.google.client_id'), - 'client_secret' => config('services.google.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $account->refresh_token, - ])); - - $data = $response->json(); - - $account->update([ - 'access_token' => data_get($data, 'access_token'), - 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, - ]); - - } - private function handleGoogleError(Exception $e): never { throw YouTubePublishException::fromGoogleException($e);