diff --git a/app/Services/Social/BlueskyPublisher.php b/app/Services/Social/BlueskyPublisher.php index 4664f33f..5bf037fe 100644 --- a/app/Services/Social/BlueskyPublisher.php +++ b/app/Services/Social/BlueskyPublisher.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Exceptions\Social\BlueskyPublishException; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -327,14 +328,6 @@ public function refreshToken(SocialAccount $account): void private function handleApiError(Response $response): void { - $body = $response->json() ?? []; - $error = $body['error'] ?? 'Unknown error'; - $message = $body['message'] ?? $response->body(); - - if ($error === 'ExpiredToken' || $error === 'InvalidToken') { - throw new TokenExpiredException("Bluesky: {$message}"); - } - - throw new \Exception("Bluesky API error: {$message}"); + throw BlueskyPublishException::fromApiResponse($response); } } diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index 6d389e2e..fc96be37 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -5,6 +5,7 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; +use App\Exceptions\Social\LinkedInPublishException; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -15,17 +16,6 @@ class LinkedInPagePublisher { - /** - * LinkedIn API error codes that indicate token issues. - * - * @see https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/error-handling - */ - private const TOKEN_ERROR_CODES = [ - 'REVOKED_ACCESS_TOKEN', - 'EXPIRED_ACCESS_TOKEN', - 'INVALID_ACCESS_TOKEN', - ]; - private string $baseUrl = 'https://api.linkedin.com'; private string $apiVersion = '202601'; @@ -492,17 +482,6 @@ private function refreshToken(SocialAccount $account): void private function handleApiError(Response $response, string $context): void { - $body = $response->json() ?? []; - $errorCode = $body['code'] ?? null; - $message = $body['message'] ?? $response->body(); - - if ($response->status() === 401 || in_array($errorCode, self::TOKEN_ERROR_CODES)) { - throw new TokenExpiredException( - "{$context}: {$message}", - $errorCode - ); - } - - throw new \Exception("{$context}: {$message}"); + throw LinkedInPublishException::fromApiResponse($response); } } diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index 08a08e09..9e19cd35 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -5,6 +5,7 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; +use App\Exceptions\Social\LinkedInPublishException; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -15,17 +16,6 @@ class LinkedInPublisher { - /** - * LinkedIn API error codes that indicate token issues. - * - * @see https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/error-handling - */ - private const TOKEN_ERROR_CODES = [ - 'REVOKED_ACCESS_TOKEN', - 'EXPIRED_ACCESS_TOKEN', - 'INVALID_ACCESS_TOKEN', - ]; - private string $baseUrl = 'https://api.linkedin.com'; private string $apiVersion = '202601'; @@ -473,17 +463,6 @@ private function refreshToken(SocialAccount $account): void private function handleApiError(Response $response, string $context): void { - $body = $response->json() ?? []; - $errorCode = $body['code'] ?? null; - $message = $body['message'] ?? $response->body(); - - if ($response->status() === 401 || in_array($errorCode, self::TOKEN_ERROR_CODES)) { - throw new TokenExpiredException( - "{$context}: {$message}", - $errorCode - ); - } - - throw new \Exception("{$context}: {$message}"); + throw LinkedInPublishException::fromApiResponse($response); } } diff --git a/app/Services/Social/MastodonPublisher.php b/app/Services/Social/MastodonPublisher.php index 0ab1e232..5937d789 100644 --- a/app/Services/Social/MastodonPublisher.php +++ b/app/Services/Social/MastodonPublisher.php @@ -4,7 +4,7 @@ namespace App\Services\Social; -use App\Exceptions\TokenExpiredException; +use App\Exceptions\Social\MastodonPublishException; use App\Models\PostPlatform; use App\Models\SocialAccount; use Illuminate\Http\Client\Response; @@ -119,13 +119,6 @@ private function uploadMedia(SocialAccount $account, string $instance, string $u private function handleApiError(Response $response): void { - $body = $response->json() ?? []; - $error = $body['error'] ?? $response->body(); - - if ($response->status() === 401 || $response->status() === 403) { - throw new TokenExpiredException("Mastodon: {$error}"); - } - - throw new \Exception("Mastodon API error: {$error}"); + throw MastodonPublishException::fromApiResponse($response); } } diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index 3eab3de0..d23ff2c2 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -5,7 +5,7 @@ namespace App\Services\Social; use App\Enums\PostPlatform\ContentType; -use App\Exceptions\TokenExpiredException; +use App\Exceptions\Social\PinterestPublishException; use App\Models\PostPlatform; use App\Models\SocialAccount; use Illuminate\Http\Client\Response; @@ -16,14 +16,6 @@ class PinterestPublisher { private const API_BASE = 'https://api.pinterest.com/v5'; - /** - * Pinterest API error codes that indicate token issues. - */ - private const TOKEN_ERROR_CODES = [ - 1, // Invalid access token - 2, // Access token has expired - ]; - public function publish(PostPlatform $postPlatform): array { $account = $postPlatform->socialAccount; @@ -407,20 +399,6 @@ public function getBoards(SocialAccount $account): array private function handleApiError(Response $response, string $context): void { - $body = $response->json() ?? []; - $code = $body['code'] ?? $response->status(); - $message = $body['message'] ?? $response->body(); - - $isTokenError = $response->status() === 401 - || in_array($code, self::TOKEN_ERROR_CODES); - - if ($isTokenError) { - throw new TokenExpiredException( - "{$context}: {$message}", - is_int($code) ? (string) $code : null - ); - } - - throw new \Exception("{$context}: {$message}"); + throw PinterestPublishException::fromApiResponse($response); } } diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index 0d72d978..33fad995 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -4,7 +4,7 @@ namespace App\Services\Social; -use App\Exceptions\TokenExpiredException; +use App\Exceptions\Social\ThreadsPublishException; use App\Models\PostPlatform; use App\Models\SocialAccount; use Illuminate\Http\Client\Response; @@ -13,24 +13,6 @@ class ThreadsPublisher { - /** - * Meta Graph API error codes that indicate token issues. - * - * @see https://developers.facebook.com/docs/threads/error-handling - */ - private const TOKEN_ERROR_CODES = [ - 190, // Invalid OAuth access token - ]; - - private const TOKEN_ERROR_SUBCODES = [ - 458, // App not installed - 459, // User checkpointed - 460, // Password changed - 463, // Session expired - 464, // Unconfirmed user - 467, // Invalid access token - ]; - private string $baseUrl = 'https://graph.threads.net/v1.0'; public function publish(PostPlatform $postPlatform): array @@ -328,24 +310,6 @@ private function refreshToken(SocialAccount $account): void private function handleApiError(Response $response, string $context): void { - $body = $response->json() ?? []; - $error = $body['error'] ?? []; - $errorCode = $error['code'] ?? null; - $errorSubcode = $error['error_subcode'] ?? null; - $errorType = $error['type'] ?? null; - $message = $error['message'] ?? $response->body(); - - $isTokenError = $errorType === 'OAuthException' - || in_array($errorCode, self::TOKEN_ERROR_CODES) - || in_array($errorSubcode, self::TOKEN_ERROR_SUBCODES); - - if ($isTokenError) { - throw new TokenExpiredException( - "{$context}: {$message}", - $errorCode ? (string) $errorCode : null - ); - } - - throw new \Exception("{$context}: {$message}"); + throw ThreadsPublishException::fromApiResponse($response); } } diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index 9967e844..f8a487fb 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Exceptions\Social\XPublishException; use App\Exceptions\TokenExpiredException; use App\Models\PostPlatform; use App\Models\SocialAccount; @@ -14,15 +15,6 @@ class XPublisher { - /** - * X/Twitter API error titles that indicate token issues. - * - * @see https://developer.twitter.com/en/support/twitter-api/error-troubleshooting - */ - private const TOKEN_ERROR_TITLES = [ - 'Unauthorized', - ]; - private string $baseUrl = 'https://api.x.com'; private string $accessToken; @@ -359,17 +351,6 @@ private function refreshToken(SocialAccount $account): void private function handleApiError(Response $response, string $context): void { - $body = $response->json() ?? []; - $errorTitle = $body['title'] ?? null; - $message = $body['detail'] ?? $response->body(); - - if ($response->status() === 401 || in_array($errorTitle, self::TOKEN_ERROR_TITLES)) { - throw new TokenExpiredException( - "{$context}: {$message}", - $errorTitle - ); - } - - throw new \Exception("{$context}: {$message}"); + throw XPublishException::fromApiResponse($response); } } diff --git a/tests/Unit/Services/Social/MastodonPublisherTest.php b/tests/Unit/Services/Social/MastodonPublisherTest.php index 588c67f9..c50c18f6 100644 --- a/tests/Unit/Services/Social/MastodonPublisherTest.php +++ b/tests/Unit/Services/Social/MastodonPublisherTest.php @@ -4,6 +4,7 @@ use App\Enums\PostPlatform\ContentType; use App\Enums\SocialAccount\Platform; +use App\Exceptions\Social\MastodonPublishException; use App\Exceptions\TokenExpiredException; use App\Models\Post; use App\Models\PostPlatform; @@ -158,7 +159,7 @@ ->toThrow(TokenExpiredException::class); }); -test('mastodon publisher throws token expired exception on forbidden', function () { +test('mastodon publisher throws permission exception on forbidden', function () { Http::fake([ 'https://mastodon.social/api/v1/statuses' => Http::response([ 'error' => 'This action is not allowed', @@ -166,7 +167,7 @@ ]); expect(fn () => $this->publisher->publish($this->postPlatform)) - ->toThrow(TokenExpiredException::class); + ->toThrow(MastodonPublishException::class); }); test('mastodon publisher limits media to 4', function () { diff --git a/tests/Unit/Services/XPublisherTest.php b/tests/Unit/Services/XPublisherTest.php index 0be0caad..64b224e6 100644 --- a/tests/Unit/Services/XPublisherTest.php +++ b/tests/Unit/Services/XPublisherTest.php @@ -4,6 +4,7 @@ use App\Enums\PostPlatform\ContentType; use App\Enums\SocialAccount\Platform; +use App\Exceptions\Social\XPublishException; use App\Exceptions\TokenExpiredException; use App\Models\Post; use App\Models\PostPlatform; @@ -179,7 +180,7 @@ $publisher = new XPublisher; expect(fn () => $publisher->publish($this->postPlatform)) - ->toThrow(Exception::class, 'X API error'); + ->toThrow(XPublishException::class); }); test('x publisher handles empty error response', function () {