fix: retry LinkedIn publish with token refresh when token is revoked
This commit is contained in:
parent
a976e11263
commit
e1ac9f87f8
4 changed files with 119 additions and 24 deletions
|
|
@ -30,18 +30,23 @@ class LinkedInPagePublisher
|
|||
|
||||
private string $accessToken;
|
||||
|
||||
private SocialAccount $account;
|
||||
|
||||
private bool $hasRetried = false;
|
||||
|
||||
public function publish(PostPlatform $postPlatform): array
|
||||
{
|
||||
$account = $postPlatform->socialAccount;
|
||||
$this->account = $postPlatform->socialAccount;
|
||||
$this->hasRetried = false;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$account->refresh();
|
||||
if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) {
|
||||
$this->refreshToken($this->account);
|
||||
$this->account->refresh();
|
||||
}
|
||||
|
||||
$this->accessToken = $account->access_token;
|
||||
$this->accessToken = $this->account->access_token;
|
||||
|
||||
$organizationId = $account->meta['organization_id'] ?? null;
|
||||
$organizationId = $this->account->meta['organization_id'] ?? null;
|
||||
|
||||
if (! $organizationId) {
|
||||
throw new \Exception('LinkedIn Page organization ID not configured');
|
||||
|
|
@ -50,11 +55,50 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$organizationUrn = "urn:li:organization:{$organizationId}";
|
||||
$contentType = $postPlatform->content_type;
|
||||
|
||||
return match ($contentType) {
|
||||
ContentType::LinkedInPageCarousel => $this->publishCarousel($organizationUrn, $postPlatform->content, $postPlatform->media, $account),
|
||||
ContentType::LinkedInPagePost => $this->publishPost($organizationUrn, $postPlatform->content, $postPlatform->media, $account),
|
||||
default => throw new \Exception("Unsupported LinkedIn Page content type: {$contentType?->value}"),
|
||||
};
|
||||
try {
|
||||
return match ($contentType) {
|
||||
ContentType::LinkedInPageCarousel => $this->publishCarousel($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
|
||||
ContentType::LinkedInPagePost => $this->publishPost($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
|
||||
default => throw new \Exception("Unsupported LinkedIn Page content type: {$contentType?->value}"),
|
||||
};
|
||||
} catch (TokenExpiredException $e) {
|
||||
return $this->retryWithRefresh($postPlatform, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function retryWithRefresh(PostPlatform $postPlatform, TokenExpiredException $originalException): array
|
||||
{
|
||||
if ($this->hasRetried) {
|
||||
throw $originalException;
|
||||
}
|
||||
|
||||
$this->hasRetried = true;
|
||||
|
||||
Log::info('LinkedIn Page token rejected, attempting refresh and retry', [
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->refreshToken($this->account);
|
||||
$this->account->refresh();
|
||||
$this->accessToken = $this->account->access_token;
|
||||
|
||||
$organizationId = $this->account->meta['organization_id'] ?? null;
|
||||
$organizationUrn = "urn:li:organization:{$organizationId}";
|
||||
$contentType = $postPlatform->content_type;
|
||||
|
||||
return match ($contentType) {
|
||||
ContentType::LinkedInPageCarousel => $this->publishCarousel($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
|
||||
ContentType::LinkedInPagePost => $this->publishPost($organizationUrn, $postPlatform->content, $postPlatform->media, $this->account),
|
||||
default => throw new \Exception("Unsupported LinkedIn Page content type: {$contentType?->value}"),
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('LinkedIn Page refresh failed during retry', [
|
||||
'account_id' => $this->account->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $originalException;
|
||||
}
|
||||
}
|
||||
|
||||
private function publishPost(string $organizationUrn, string $content, $media, $account): array
|
||||
|
|
|
|||
|
|
@ -30,25 +30,68 @@ class LinkedInPublisher
|
|||
|
||||
private string $accessToken;
|
||||
|
||||
private SocialAccount $account;
|
||||
|
||||
private bool $hasRetried = false;
|
||||
|
||||
public function publish(PostPlatform $postPlatform): array
|
||||
{
|
||||
$account = $postPlatform->socialAccount;
|
||||
$this->account = $postPlatform->socialAccount;
|
||||
$this->hasRetried = false;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$account->refresh();
|
||||
if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) {
|
||||
$this->refreshToken($this->account);
|
||||
$this->account->refresh();
|
||||
}
|
||||
|
||||
$this->accessToken = $account->access_token;
|
||||
$this->accessToken = $this->account->access_token;
|
||||
|
||||
$personUrn = "urn:li:person:{$account->platform_user_id}";
|
||||
$personUrn = "urn:li:person:{$this->account->platform_user_id}";
|
||||
$contentType = $postPlatform->content_type;
|
||||
|
||||
return match ($contentType) {
|
||||
ContentType::LinkedInCarousel => $this->publishCarousel($personUrn, $postPlatform->content, $postPlatform->media),
|
||||
ContentType::LinkedInPost => $this->publishPost($personUrn, $postPlatform->content, $postPlatform->media),
|
||||
default => throw new \Exception("Unsupported LinkedIn content type: {$contentType?->value}"),
|
||||
};
|
||||
try {
|
||||
return match ($contentType) {
|
||||
ContentType::LinkedInCarousel => $this->publishCarousel($personUrn, $postPlatform->content, $postPlatform->media),
|
||||
ContentType::LinkedInPost => $this->publishPost($personUrn, $postPlatform->content, $postPlatform->media),
|
||||
default => throw new \Exception("Unsupported LinkedIn content type: {$contentType?->value}"),
|
||||
};
|
||||
} catch (TokenExpiredException $e) {
|
||||
return $this->retryWithRefresh($postPlatform, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function retryWithRefresh(PostPlatform $postPlatform, TokenExpiredException $originalException): array
|
||||
{
|
||||
if ($this->hasRetried) {
|
||||
throw $originalException;
|
||||
}
|
||||
|
||||
$this->hasRetried = true;
|
||||
|
||||
Log::info('LinkedIn token rejected, attempting refresh and retry', [
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->refreshToken($this->account);
|
||||
$this->account->refresh();
|
||||
$this->accessToken = $this->account->access_token;
|
||||
|
||||
$personUrn = "urn:li:person:{$this->account->platform_user_id}";
|
||||
$contentType = $postPlatform->content_type;
|
||||
|
||||
return match ($contentType) {
|
||||
ContentType::LinkedInCarousel => $this->publishCarousel($personUrn, $postPlatform->content, $postPlatform->media),
|
||||
ContentType::LinkedInPost => $this->publishPost($personUrn, $postPlatform->content, $postPlatform->media),
|
||||
default => throw new \Exception("Unsupported LinkedIn content type: {$contentType?->value}"),
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('LinkedIn refresh failed during retry', [
|
||||
'account_id' => $this->account->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $originalException;
|
||||
}
|
||||
}
|
||||
|
||||
private function publishPost(string $personUrn, string $content, $media): array
|
||||
|
|
|
|||
|
|
@ -115,12 +115,16 @@
|
|||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('linkedin page publisher throws token expired exception on auth error', function () {
|
||||
test('linkedin page publisher throws token expired exception on auth error after retry', function () {
|
||||
Http::fake([
|
||||
'https://api.linkedin.com/rest/posts' => Http::response([
|
||||
'code' => 'EXPIRED_ACCESS_TOKEN',
|
||||
'message' => 'The token used in the request has expired',
|
||||
], 401),
|
||||
'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
|
||||
'error' => 'invalid_grant',
|
||||
'error_description' => 'The refresh token is invalid',
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
|
|
|
|||
|
|
@ -89,12 +89,16 @@
|
|||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('linkedin publisher throws token expired exception on auth error', function () {
|
||||
test('linkedin publisher throws token expired exception on auth error after retry', function () {
|
||||
Http::fake([
|
||||
'https://api.linkedin.com/rest/posts' => Http::response([
|
||||
'code' => 'EXPIRED_ACCESS_TOKEN',
|
||||
'message' => 'The token used in the request has expired',
|
||||
], 401),
|
||||
'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
|
||||
'error' => 'invalid_grant',
|
||||
'error_description' => 'The refresh token is invalid',
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
|
|
|
|||
Loading…
Reference in a new issue