refactor: all remaining publishers use platform-specific exceptions

This commit is contained in:
Paulo Castellano 2026-03-31 21:04:39 -03:00
parent 2320c9ae63
commit 02298f31a6
9 changed files with 19 additions and 150 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 () {

View file

@ -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 () {