refactor(social): single source of truth for token refresh
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.
This commit is contained in:
parent
32e1f89adb
commit
fcb70b3599
20 changed files with 29 additions and 489 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue