diff --git a/app/Enums/SocialAccount/Platform.php b/app/Enums/SocialAccount/Platform.php index 4ef3380e..ff5d0a2d 100644 --- a/app/Enums/SocialAccount/Platform.php +++ b/app/Enums/SocialAccount/Platform.php @@ -165,9 +165,8 @@ public static function allQueues(): array public function instagramGraphBaseUrl(): string { return match ($this) { - self::InstagramFacebook => 'https://graph.facebook.com/v25.0', - self::Instagram => 'https://graph.instagram.com/v25.0', - default => 'https://graph.instagram.com/v25.0', + self::InstagramFacebook => (string) config('trypost.platforms.instagram-facebook.graph_api'), + default => (string) config('trypost.platforms.instagram.graph_api'), }; } diff --git a/app/Http/Controllers/Auth/FacebookController.php b/app/Http/Controllers/Auth/FacebookController.php index 32b32169..a8315515 100644 --- a/app/Http/Controllers/Auth/FacebookController.php +++ b/app/Http/Controllers/Auth/FacebookController.php @@ -52,7 +52,7 @@ public function connect(Request $request): Response|RedirectResponse return Inertia::location( Socialite::driver($this->driver) - ->usingGraphVersion('v25.0') + ->usingGraphVersion($this->graphVersion()) ->setScopes($this->scopes) ->redirect() ->getTargetUrl() @@ -74,11 +74,11 @@ public function callback(Request $request): View|RedirectResponse } try { - $socialUser = Socialite::driver($this->driver)->usingGraphVersion('v25.0')->user(); + $socialUser = Socialite::driver($this->driver)->usingGraphVersion($this->graphVersion())->user(); // Trigger public_profile and pages_show_list API calls // These calls are needed for Meta app review permission verification - Http::get('https://graph.facebook.com/v25.0/me', [ + Http::get(config('trypost.platforms.facebook.graph_api').'/me', [ 'fields' => 'id,name', 'access_token' => $socialUser->token, ]); @@ -269,7 +269,7 @@ public function select(Request $request): View private function fetchPages(string $userToken): array { try { - $response = Http::get('https://graph.facebook.com/v25.0/me/accounts', [ + $response = Http::get(config('trypost.platforms.facebook.graph_api').'/me/accounts', [ 'access_token' => $userToken, 'fields' => 'id,name,username,picture{url},access_token', ]); @@ -300,4 +300,9 @@ private function fetchPages(string $userToken): array return []; } } + + private function graphVersion(): string + { + return basename((string) parse_url((string) config('trypost.platforms.facebook.graph_api'), PHP_URL_PATH)); + } } diff --git a/app/Http/Controllers/Auth/InstagramFacebookController.php b/app/Http/Controllers/Auth/InstagramFacebookController.php index dd3f1026..a2532dab 100644 --- a/app/Http/Controllers/Auth/InstagramFacebookController.php +++ b/app/Http/Controllers/Auth/InstagramFacebookController.php @@ -53,7 +53,7 @@ public function connect(Request $request): Response|RedirectResponse ]); $url = Socialite::driver($this->driver) - ->usingGraphVersion('v20.0') + ->usingGraphVersion($this->graphVersion()) ->setScopes($this->scopes) ->redirectUrl(route('app.social.instagram-facebook.callback')) ->redirect() @@ -81,12 +81,12 @@ public function callback(Request $request): View|RedirectResponse try { $socialUser = Socialite::driver($this->driver) - ->usingGraphVersion('v20.0') + ->usingGraphVersion($this->graphVersion()) ->redirectUrl(route('app.social.instagram-facebook.callback')) ->user(); // Trigger public_profile API call for Meta app review verification - Http::get('https://graph.facebook.com/v25.0/me', [ + Http::get(config('trypost.platforms.instagram-facebook.graph_api').'/me', [ 'fields' => 'id,name', 'access_token' => $socialUser->token, ]); @@ -238,7 +238,9 @@ private function connectInstagramAccount(Workspace $workspace, array $pageData, private function fetchPagesWithInstagram(string $userToken): array { try { - $response = Http::get('https://graph.facebook.com/v25.0/me/accounts', [ + $graphApi = (string) config('trypost.platforms.instagram-facebook.graph_api'); + + $response = Http::get($graphApi.'/me/accounts', [ 'access_token' => $userToken, 'fields' => 'id,name,username,picture{url},access_token,instagram_business_account', ]); @@ -263,7 +265,7 @@ private function fetchPagesWithInstagram(string $userToken): array } // Fetch IG account details - $igResponse = Http::get("https://graph.facebook.com/v25.0/{$igAccountId}", [ + $igResponse = Http::get("{$graphApi}/{$igAccountId}", [ 'access_token' => data_get($page, 'access_token'), 'fields' => 'username,name,profile_picture_url', ]); @@ -289,4 +291,9 @@ private function fetchPagesWithInstagram(string $userToken): array return []; } } + + private function graphVersion(): string + { + return basename((string) parse_url((string) config('trypost.platforms.instagram-facebook.graph_api'), PHP_URL_PATH)); + } } diff --git a/app/Http/Controllers/Auth/LinkedInController.php b/app/Http/Controllers/Auth/LinkedInController.php index 86239b66..c40b491b 100644 --- a/app/Http/Controllers/Auth/LinkedInController.php +++ b/app/Http/Controllers/Auth/LinkedInController.php @@ -109,7 +109,7 @@ private function fetchVanityName(string $accessToken): ?string try { $response = Http::withToken($accessToken) ->withHeaders(['X-RestLi-Protocol-Version' => '2.0.0']) - ->get('https://api.linkedin.com/v2/me', [ + ->get(config('trypost.platforms.linkedin.api').'/v2/me', [ 'projection' => '(id,vanityName,localizedFirstName,localizedLastName)', ]); diff --git a/app/Http/Controllers/Auth/LinkedInPageController.php b/app/Http/Controllers/Auth/LinkedInPageController.php index 4d9514e2..9baa94cb 100644 --- a/app/Http/Controllers/Auth/LinkedInPageController.php +++ b/app/Http/Controllers/Auth/LinkedInPageController.php @@ -240,7 +240,7 @@ public function select(Request $request): View private function fetchOrganizations(string $accessToken): array { $response = Http::withToken($accessToken) - ->get('https://api.linkedin.com/v2/organizationAcls', [ + ->get(config('trypost.platforms.linkedin-page.api').'/v2/organizationAcls', [ 'q' => 'roleAssignee', 'role' => 'ADMINISTRATOR', 'projection' => '(elements*(organization~(id,localizedName,vanityName,logoV2(original~:playableStreams))))', diff --git a/app/Http/Controllers/Auth/ThreadsController.php b/app/Http/Controllers/Auth/ThreadsController.php index 86e2ff1e..ede4f2e1 100644 --- a/app/Http/Controllers/Auth/ThreadsController.php +++ b/app/Http/Controllers/Auth/ThreadsController.php @@ -84,7 +84,7 @@ public function callback(Request $request): View try { // Exchange code for short-lived token - $tokenResponse = Http::asForm()->post('https://graph.threads.net/oauth/access_token', [ + $tokenResponse = Http::asForm()->post(config('trypost.platforms.threads.auth_api').'/oauth/access_token', [ 'client_id' => config('services.threads.client_id'), 'client_secret' => config('services.threads.client_secret'), 'grant_type' => 'authorization_code', @@ -105,7 +105,7 @@ public function callback(Request $request): View $userId = $tokenData['user_id']; // Exchange for long-lived token - $longLivedResponse = Http::get('https://graph.threads.net/access_token', [ + $longLivedResponse = Http::get(config('trypost.platforms.threads.auth_api').'/access_token', [ 'grant_type' => 'th_exchange_token', 'client_secret' => config('services.threads.client_secret'), 'access_token' => $shortLivedToken, @@ -121,7 +121,7 @@ public function callback(Request $request): View } // Fetch user profile - $profileResponse = Http::get("https://graph.threads.net/v1.0/{$userId}", [ + $profileResponse = Http::get(config('trypost.platforms.threads.graph_api')."/{$userId}", [ 'access_token' => $longLivedToken, 'fields' => 'id,username,name,threads_profile_picture_url', ]); diff --git a/app/Http/Controllers/Auth/YouTubeController.php b/app/Http/Controllers/Auth/YouTubeController.php index b6a27f9a..3d59d70e 100644 --- a/app/Http/Controllers/Auth/YouTubeController.php +++ b/app/Http/Controllers/Auth/YouTubeController.php @@ -279,7 +279,7 @@ private function fetchChannels(string $accessToken): array { try { $response = Http::withToken($accessToken) - ->get('https://www.googleapis.com/youtube/v3/channels', [ + ->get(config('trypost.platforms.youtube.data_api').'/channels', [ 'part' => 'snippet,contentDetails,statistics', 'mine' => 'true', ]); diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index 218edf55..872ef335 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -116,7 +116,7 @@ private function refreshXToken(SocialAccount $account): void $response = Http::asForm() ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) - ->post('https://api.x.com/2/oauth2/token', [ + ->post(config('trypost.platforms.x.api').'/oauth2/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); @@ -225,7 +225,7 @@ private function refreshTikTokToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post('https://open.tiktokapis.com/v2/oauth/token/', [ + $response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, 'client_key' => config('services.tiktok.client_id'), @@ -259,7 +259,7 @@ private function refreshPinterestToken(SocialAccount $account): void $response = Http::withHeaders([ 'Authorization' => "Basic {$credentials}", 'Content-Type' => 'application/x-www-form-urlencoded', - ])->asForm()->post('https://api.pinterest.com/v5/oauth/token', [ + ])->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); @@ -283,7 +283,7 @@ private function refreshPinterestToken(SocialAccount $account): void private function refreshThreadsToken(SocialAccount $account): void { // Threads uses long-lived tokens that can be refreshed - $response = Http::get('https://graph.threads.net/refresh_access_token', [ + $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, ]); @@ -307,7 +307,7 @@ private function refreshThreadsToken(SocialAccount $account): void private function refreshInstagramToken(SocialAccount $account): void { - $response = Http::get('https://graph.instagram.com/refresh_access_token', [ + $response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ 'grant_type' => 'ig_refresh_token', 'access_token' => $account->access_token, ]); @@ -336,7 +336,7 @@ private function verifyLinkedIn(SocialAccount $account): bool 'X-Restli-Protocol-Version' => '2.0.0', 'LinkedIn-Version' => '202601', ]) - ->get('https://api.linkedin.com/rest/userinfo'); + ->get(config('trypost.platforms.linkedin.api').'/rest/userinfo'); if ($response->status() === 401) { throw new TokenExpiredException('LinkedIn access token is invalid or expired'); @@ -352,7 +352,7 @@ private function verifyLinkedInPage(SocialAccount $account): bool 'X-Restli-Protocol-Version' => '2.0.0', 'LinkedIn-Version' => '202601', ]) - ->get('https://api.linkedin.com/rest/organizationAcls', [ + ->get(config('trypost.platforms.linkedin-page.api').'/rest/organizationAcls', [ 'q' => 'roleAssignee', ]); @@ -366,7 +366,7 @@ private function verifyLinkedInPage(SocialAccount $account): bool private function verifyX(SocialAccount $account): bool { $response = Http::withToken($account->access_token) - ->get('https://api.x.com/2/users/me'); + ->get(config('trypost.platforms.x.api').'/users/me'); if ($response->status() === 401) { throw new TokenExpiredException('X access token is invalid or expired'); @@ -377,7 +377,7 @@ private function verifyX(SocialAccount $account): bool private function verifyInstagram(SocialAccount $account): bool { - $response = Http::get('https://graph.instagram.com/v25.0/me', [ + $response = Http::get(config('trypost.platforms.instagram.graph_api').'/me', [ 'fields' => 'id,username', 'access_token' => $account->access_token, ]); @@ -398,7 +398,7 @@ private function verifyInstagram(SocialAccount $account): bool private function verifyFacebook(SocialAccount $account): bool { - $response = Http::get('https://graph.facebook.com/v25.0/me', [ + $response = Http::get(config('trypost.platforms.facebook.graph_api').'/me', [ 'fields' => 'id,name', 'access_token' => $account->access_token, ]); @@ -419,7 +419,7 @@ private function verifyFacebook(SocialAccount $account): bool private function verifyThreads(SocialAccount $account): bool { - $response = Http::get('https://graph.threads.net/v1.0/me', [ + $response = Http::get(config('trypost.platforms.threads.graph_api').'/me', [ 'fields' => 'id,username', 'access_token' => $account->access_token, ]); @@ -444,7 +444,7 @@ private function verifyTikTok(SocialAccount $account): bool ->withHeaders([ 'Content-Type' => 'application/json', ]) - ->get('https://open.tiktokapis.com/v2/user/info/', [ + ->get(config('trypost.platforms.tiktok.api').'/user/info/', [ 'fields' => 'open_id,display_name', ]); @@ -461,7 +461,7 @@ private function verifyTikTok(SocialAccount $account): bool private function verifyYouTube(SocialAccount $account): bool { $response = Http::withToken($account->access_token) - ->get('https://www.googleapis.com/youtube/v3/channels', [ + ->get(config('trypost.platforms.youtube.data_api').'/channels', [ 'part' => 'id', 'mine' => 'true', ]); @@ -476,7 +476,7 @@ private function verifyYouTube(SocialAccount $account): bool private function verifyPinterest(SocialAccount $account): bool { $response = Http::withToken($account->access_token) - ->get('https://api.pinterest.com/v5/user_account'); + ->get(config('trypost.platforms.pinterest.api').'/user_account'); if ($response->status() === 401) { throw new TokenExpiredException('Pinterest access token is invalid or expired'); diff --git a/app/Services/Social/FacebookAnalytics.php b/app/Services/Social/FacebookAnalytics.php index edb07c3f..78aacedb 100644 --- a/app/Services/Social/FacebookAnalytics.php +++ b/app/Services/Social/FacebookAnalytics.php @@ -16,10 +16,15 @@ class FacebookAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://graph.facebook.com/v25.0'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.facebook.graph_api'); + } + public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); diff --git a/app/Services/Social/FacebookPublisher.php b/app/Services/Social/FacebookPublisher.php index ee1673e7..6a612361 100644 --- a/app/Services/Social/FacebookPublisher.php +++ b/app/Services/Social/FacebookPublisher.php @@ -15,7 +15,12 @@ class FacebookPublisher { use HasSocialHttpClient; - private string $baseUrl = 'https://graph.facebook.com/v25.0'; + private string $baseUrl; + + public function __construct() + { + $this->baseUrl = config('trypost.platforms.facebook.graph_api'); + } public function publish(PostPlatform $postPlatform): array { diff --git a/app/Services/Social/InstagramAnalytics.php b/app/Services/Social/InstagramAnalytics.php index e31cfa62..edd4cab3 100644 --- a/app/Services/Social/InstagramAnalytics.php +++ b/app/Services/Social/InstagramAnalytics.php @@ -89,7 +89,7 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { - $this->baseUrl = preg_replace('#/v[\d.]+$#', '', $account->platform->instagramGraphBaseUrl()); + $this->baseUrl = $account->platform->instagramGraphBaseUrl(); if ($account->is_token_expired || $account->is_token_expiring_soon) { $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); @@ -112,7 +112,7 @@ private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $si private function fetchTimeSeriesMetrics(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { $response = $this->getHttpClient() - ->get("{$this->baseUrl}/v21.0/{$account->platform_user_id}/insights", [ + ->get("{$this->baseUrl}/{$account->platform_user_id}/insights", [ 'metric' => 'reach,follower_count', 'period' => 'day', 'since' => $since->startOfDay()->unix(), @@ -156,7 +156,7 @@ private function fetchTimeSeriesMetrics(SocialAccount $account, CarbonInterface private function fetchTotalValueMetrics(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { $response = $this->getHttpClient() - ->get("{$this->baseUrl}/v21.0/{$account->platform_user_id}/insights", [ + ->get("{$this->baseUrl}/{$account->platform_user_id}/insights", [ 'metric' => 'likes,comments,shares,saves,views,total_interactions', 'metric_type' => 'total_value', 'period' => 'day', @@ -206,7 +206,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for Instagram account'); } - $response = Http::get('https://graph.instagram.com/refresh_access_token', [ + $response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ 'grant_type' => 'ig_refresh_token', 'access_token' => $account->access_token, ]); diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index 3dda6aee..85c8b0fb 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -403,7 +403,7 @@ private function refreshToken(SocialAccount $account): void return; } - $response = Http::get('https://graph.instagram.com/refresh_access_token', [ + $response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [ 'grant_type' => 'ig_refresh_token', 'access_token' => $account->access_token, ]); diff --git a/app/Services/Social/LinkedInPageAnalytics.php b/app/Services/Social/LinkedInPageAnalytics.php index 34c251ff..d94fd069 100644 --- a/app/Services/Social/LinkedInPageAnalytics.php +++ b/app/Services/Social/LinkedInPageAnalytics.php @@ -18,10 +18,15 @@ class LinkedInPageAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://api.linkedin.com/v2'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.linkedin-page.api').'/v2'; + } + public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); diff --git a/app/Services/Social/LinkedInPagePublisher.php b/app/Services/Social/LinkedInPagePublisher.php index 37f187c2..52eb44d5 100644 --- a/app/Services/Social/LinkedInPagePublisher.php +++ b/app/Services/Social/LinkedInPagePublisher.php @@ -21,12 +21,17 @@ class LinkedInPagePublisher { use HasSocialHttpClient; - private string $baseUrl = 'https://api.linkedin.com'; + private string $baseUrl; private string $apiVersion = '202601'; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.linkedin-page.api'); + } + private SocialAccount $account; private bool $hasRetried = false; diff --git a/app/Services/Social/LinkedInPublisher.php b/app/Services/Social/LinkedInPublisher.php index 09f28194..6e55aa49 100644 --- a/app/Services/Social/LinkedInPublisher.php +++ b/app/Services/Social/LinkedInPublisher.php @@ -21,12 +21,17 @@ class LinkedInPublisher { use HasSocialHttpClient; - private string $baseUrl = 'https://api.linkedin.com'; + private string $baseUrl; private string $apiVersion = '202601'; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.linkedin.api'); + } + private SocialAccount $account; private bool $hasRetried = false; diff --git a/app/Services/Social/PinterestAnalytics.php b/app/Services/Social/PinterestAnalytics.php index edb3d496..1e98495c 100644 --- a/app/Services/Social/PinterestAnalytics.php +++ b/app/Services/Social/PinterestAnalytics.php @@ -18,7 +18,12 @@ class PinterestAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://api.pinterest.com/v5'; + private string $baseUrl; + + public function __construct() + { + $this->baseUrl = config('trypost.platforms.pinterest.api'); + } private string $accessToken; @@ -165,7 +170,7 @@ private function refreshToken(SocialAccount $account): void $response = Http::withBasicAuth( config('services.pinterest.client_id'), config('services.pinterest.client_secret'), - )->asForm()->post('https://api.pinterest.com/v5/oauth/token', [ + )->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); diff --git a/app/Services/Social/PinterestPublisher.php b/app/Services/Social/PinterestPublisher.php index 8d78db91..f04359b1 100644 --- a/app/Services/Social/PinterestPublisher.php +++ b/app/Services/Social/PinterestPublisher.php @@ -20,7 +20,12 @@ class PinterestPublisher { use HasSocialHttpClient; - private const API_BASE = 'https://api.pinterest.com/v5'; + private string $baseUrl; + + public function __construct() + { + $this->baseUrl = config('trypost.platforms.pinterest.api'); + } public function publish(PostPlatform $postPlatform): array { @@ -119,7 +124,7 @@ private function publishImagePin(PostPlatform $postPlatform, ?string $content): } $response = $this->socialHttp()->withToken($account->access_token) - ->post(self::API_BASE.'/pins', $payload); + ->post($this->baseUrl.'/pins', $payload); if ($response->failed()) { Log::error('Pinterest pin creation failed', [ @@ -160,7 +165,7 @@ private function publishVideoPin(PostPlatform $postPlatform, ?string $content): // Step 1: Register media upload $registerResponse = $this->socialHttp()->withToken($account->access_token) - ->post(self::API_BASE.'/media', [ + ->post($this->baseUrl.'/media', [ 'media_type' => 'video', ]); @@ -271,7 +276,7 @@ private function publishVideoPin(PostPlatform $postPlatform, ?string $content): } $response = $this->socialHttp()->withToken($account->access_token) - ->post(self::API_BASE.'/pins', $payload); + ->post($this->baseUrl.'/pins', $payload); if ($response->failed()) { Log::error('Pinterest video pin creation failed', [ @@ -335,7 +340,7 @@ private function publishCarousel(PostPlatform $postPlatform, ?string $content): } $response = $this->socialHttp()->withToken($account->access_token) - ->post(self::API_BASE.'/pins', $payload); + ->post($this->baseUrl.'/pins', $payload); if ($response->failed()) { Log::error('Pinterest carousel creation failed', [ @@ -357,7 +362,7 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId, { for ($i = 0; $i < $maxAttempts; $i++) { $response = $this->socialHttp()->withToken($account->access_token) - ->get(self::API_BASE."/media/{$mediaId}"); + ->get($this->baseUrl."/media/{$mediaId}"); if ($response->failed()) { Log::warning('Pinterest media status check failed', [ @@ -402,7 +407,7 @@ private function refreshToken(SocialAccount $account): void config('services.pinterest.client_id'), config('services.pinterest.client_secret') ) - ->post(self::API_BASE.'/oauth/token', [ + ->post($this->baseUrl.'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); @@ -432,7 +437,7 @@ public function getBoards(SocialAccount $account): array } $response = $this->socialHttp()->withToken($account->access_token) - ->get(self::API_BASE.'/boards', [ + ->get($this->baseUrl.'/boards', [ 'page_size' => 100, ]); diff --git a/app/Services/Social/ThreadsAnalytics.php b/app/Services/Social/ThreadsAnalytics.php index 3de3375b..47ab8fb1 100644 --- a/app/Services/Social/ThreadsAnalytics.php +++ b/app/Services/Social/ThreadsAnalytics.php @@ -18,10 +18,15 @@ class ThreadsAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://graph.threads.net/v1.0'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.threads.graph_api'); + } + public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); @@ -129,7 +134,7 @@ private function getHttpClient(): PendingRequest private function refreshToken(SocialAccount $account): void { - $response = Http::get('https://graph.threads.net/refresh_access_token', [ + $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, ]); diff --git a/app/Services/Social/ThreadsPublisher.php b/app/Services/Social/ThreadsPublisher.php index 9ea0f35b..d169d135 100644 --- a/app/Services/Social/ThreadsPublisher.php +++ b/app/Services/Social/ThreadsPublisher.php @@ -16,7 +16,12 @@ class ThreadsPublisher { use HasSocialHttpClient; - private string $baseUrl = 'https://graph.threads.net/v1.0'; + private string $baseUrl; + + public function __construct() + { + $this->baseUrl = config('trypost.platforms.threads.graph_api'); + } public function publish(PostPlatform $postPlatform): array { @@ -300,7 +305,7 @@ private function waitForMediaProcessing(string $containerId, string $accessToken private function refreshToken(SocialAccount $account): void { // Threads uses long-lived tokens that can be refreshed - $response = Http::get('https://graph.threads.net/refresh_access_token', [ + $response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [ 'grant_type' => 'th_refresh_token', 'access_token' => $account->access_token, ]); diff --git a/app/Services/Social/TikTokAnalytics.php b/app/Services/Social/TikTokAnalytics.php index bed0e830..19e673d4 100644 --- a/app/Services/Social/TikTokAnalytics.php +++ b/app/Services/Social/TikTokAnalytics.php @@ -16,10 +16,15 @@ class TikTokAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://open.tiktokapis.com/v2'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.tiktok.api'); + } + public function getMetrics(SocialAccount $account): array { $cacheKey = "analytics:tiktok:{$account->id}"; @@ -161,7 +166,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post('https://open.tiktokapis.com/v2/oauth/token/', [ + $response = 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', diff --git a/app/Services/Social/TikTokCreatorInfo.php b/app/Services/Social/TikTokCreatorInfo.php index db59d126..b2237870 100644 --- a/app/Services/Social/TikTokCreatorInfo.php +++ b/app/Services/Social/TikTokCreatorInfo.php @@ -15,10 +15,15 @@ class TikTokCreatorInfo { use HasSocialHttpClient; - private string $baseUrl = 'https://open.tiktokapis.com/v2'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.tiktok.api'); + } + /** * @return array{ * creator_nickname: ?string, @@ -103,7 +108,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post('https://open.tiktokapis.com/v2/oauth/token/', [ + $response = 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', diff --git a/app/Services/Social/TikTokPublisher.php b/app/Services/Social/TikTokPublisher.php index 2f1a81ae..e2b90893 100644 --- a/app/Services/Social/TikTokPublisher.php +++ b/app/Services/Social/TikTokPublisher.php @@ -19,10 +19,15 @@ class TikTokPublisher { use HasSocialHttpClient; - private string $baseUrl = 'https://open.tiktokapis.com/v2'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.tiktok.api'); + } + public function publish(PostPlatform $postPlatform): array { $this->validateContentLength($postPlatform); @@ -292,7 +297,7 @@ private function refreshToken(SocialAccount $account): void throw new TokenExpiredException('No refresh token available for TikTok account'); } - $response = Http::asForm()->post('https://open.tiktokapis.com/v2/oauth/token/', [ + $response = 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', diff --git a/app/Services/Social/XAnalytics.php b/app/Services/Social/XAnalytics.php index dc2cbf05..af894cd0 100644 --- a/app/Services/Social/XAnalytics.php +++ b/app/Services/Social/XAnalytics.php @@ -17,10 +17,15 @@ class XAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://api.x.com/2'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.x.api'); + } + public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); @@ -204,7 +209,7 @@ private function refreshToken(SocialAccount $account): void $response = $this->socialHttp() ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) ->asForm() - ->post('https://api.x.com/2/oauth2/token', [ + ->post(config('trypost.platforms.x.api').'/oauth2/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); diff --git a/app/Services/Social/XPublisher.php b/app/Services/Social/XPublisher.php index c875383b..f2cedd7e 100644 --- a/app/Services/Social/XPublisher.php +++ b/app/Services/Social/XPublisher.php @@ -20,10 +20,15 @@ class XPublisher { use HasSocialHttpClient; - private string $baseUrl = 'https://api.x.com'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.x.api'); + } + public function publish(PostPlatform $postPlatform): array { $this->validateContentLength($postPlatform); @@ -72,7 +77,7 @@ public function publish(PostPlatform $postPlatform): array } $response = $this->getHttpClient() - ->post("{$this->baseUrl}/2/tweets", $data); + ->post("{$this->baseUrl}/tweets", $data); if ($response->failed()) { Log::error('X post creation failed', [ @@ -150,7 +155,7 @@ private function uploadMedia($mediaItem): ?array $formParams['media_category'] = $mediaCategory; } - $response = $response->post("{$this->baseUrl}/2/media/upload", $formParams); + $response = $response->post("{$this->baseUrl}/media/upload", $formParams); if ($response->failed()) { Log::error('X media upload error', [ @@ -179,7 +184,7 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy // INIT $initResponse = $this->socialHttp()->withToken($this->accessToken) ->timeout(60) - ->post("{$this->baseUrl}/2/media/upload/initialize", [ + ->post("{$this->baseUrl}/media/upload/initialize", [ 'media_type' => $mimeType, 'media_category' => $mediaCategory, 'total_bytes' => $totalBytes, @@ -216,7 +221,7 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy $appendResponse = $this->socialHttp()->withToken($this->accessToken) ->timeout(300) ->attach('media', $chunk, 'chunk'.$index) - ->post("{$this->baseUrl}/2/media/upload/{$mediaId}/append", [ + ->post("{$this->baseUrl}/media/upload/{$mediaId}/append", [ 'segment_index' => $index, ]); @@ -238,7 +243,7 @@ private function chunkedUpload(string $tempFile, int $totalBytes, string $mimeTy // FINALIZE - Use the new v2 endpoint $finalizeResponse = $this->socialHttp()->withToken($this->accessToken) ->timeout(60) - ->post("{$this->baseUrl}/2/media/upload/{$mediaId}/finalize"); + ->post("{$this->baseUrl}/media/upload/{$mediaId}/finalize"); if ($finalizeResponse->failed()) { Log::error('X chunked upload FINALIZE error', [ @@ -284,7 +289,7 @@ private function waitForProcessing(string $mediaId, int $maxAttempts = 20): bool { for ($i = 0; $i < $maxAttempts; $i++) { $response = $this->getHttpClient() - ->get("{$this->baseUrl}/2/media/{$mediaId}"); + ->get("{$this->baseUrl}/media/{$mediaId}"); if ($response->failed()) { Log::error('X media status check error', ['body' => $this->redactResponseBody($response->body())]); @@ -329,7 +334,7 @@ private function refreshToken(SocialAccount $account): void $response = Http::asForm() ->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret')) - ->post("{$this->baseUrl}/2/oauth2/token", [ + ->post("{$this->baseUrl}/oauth2/token", [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); diff --git a/app/Services/Social/YouTubeAnalytics.php b/app/Services/Social/YouTubeAnalytics.php index a74309b7..cc4645fe 100644 --- a/app/Services/Social/YouTubeAnalytics.php +++ b/app/Services/Social/YouTubeAnalytics.php @@ -18,10 +18,15 @@ class YouTubeAnalytics { use HasSocialHttpClient; - private string $baseUrl = 'https://youtubeanalytics.googleapis.com/v2'; + private string $baseUrl; private string $accessToken; + public function __construct() + { + $this->baseUrl = config('trypost.platforms.youtube.analytics_api'); + } + public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); diff --git a/app/Socialite/InstagramProvider.php b/app/Socialite/InstagramProvider.php index abdf8dc7..93c401ed 100644 --- a/app/Socialite/InstagramProvider.php +++ b/app/Socialite/InstagramProvider.php @@ -37,7 +37,7 @@ protected function getTokenUrl(): string protected function getUserByToken($token): array { - $response = $this->getHttpClient()->get('https://graph.instagram.com/v25.0/me', [ + $response = $this->getHttpClient()->get(config('trypost.platforms.instagram.graph_api').'/me', [ RequestOptions::QUERY => [ 'access_token' => $token, 'fields' => 'id,username,account_type,name,profile_picture_url', @@ -78,7 +78,7 @@ protected function exchangeForLongLivedToken(array $data): array { // Although Meta's docs don't list `client_id` as a required parameter, // the API in practice rejects the request without it. - $response = $this->getHttpClient()->get('https://graph.instagram.com/access_token', [ + $response = $this->getHttpClient()->get(config('trypost.platforms.instagram.auth_api').'/access_token', [ RequestOptions::QUERY => [ 'grant_type' => 'ig_exchange_token', 'client_id' => $this->clientId, diff --git a/config/trypost.php b/config/trypost.php index 32ce9b60..df4ff9e2 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -42,33 +42,48 @@ 'platforms' => [ 'linkedin' => [ 'enabled' => env('LINKEDIN_ENABLED', true), + 'api' => env('LINKEDIN_API', 'https://api.linkedin.com'), ], 'linkedin-page' => [ 'enabled' => env('LINKEDIN_PAGE_ENABLED', true), + 'api' => env('LINKEDIN_PAGE_API', 'https://api.linkedin.com'), ], 'x' => [ 'enabled' => env('X_ENABLED', true), + 'api' => env('X_API', 'https://api.x.com/2'), ], 'tiktok' => [ 'enabled' => env('TIKTOK_ENABLED', true), + 'api' => env('TIKTOK_API', 'https://open.tiktokapis.com/v2'), ], 'youtube' => [ 'enabled' => env('YOUTUBE_ENABLED', true), + 'data_api' => env('YOUTUBE_DATA_API', 'https://www.googleapis.com/youtube/v3'), + 'analytics_api' => env('YOUTUBE_ANALYTICS_API', 'https://youtubeanalytics.googleapis.com/v2'), ], 'facebook' => [ 'enabled' => env('FACEBOOK_ENABLED', true), + 'graph_api' => env('FACEBOOK_GRAPH_API', 'https://graph.facebook.com/v25.0'), ], 'instagram' => [ 'enabled' => env('INSTAGRAM_ENABLED', true), + 'graph_api' => env('INSTAGRAM_GRAPH_API', 'https://graph.instagram.com/v25.0'), + // graph.instagram.com (no version) is the auth/refresh host. + 'auth_api' => env('INSTAGRAM_AUTH_API', 'https://graph.instagram.com'), ], 'instagram-facebook' => [ 'enabled' => env('INSTAGRAM_FACEBOOK_ENABLED', true), + 'graph_api' => env('INSTAGRAM_FACEBOOK_GRAPH_API', 'https://graph.facebook.com/v25.0'), ], 'threads' => [ 'enabled' => env('THREADS_ENABLED', true), + 'graph_api' => env('THREADS_GRAPH_API', 'https://graph.threads.net/v1.0'), + // graph.threads.net (no version) is the auth/refresh host. + 'auth_api' => env('THREADS_AUTH_API', 'https://graph.threads.net'), ], 'pinterest' => [ 'enabled' => env('PINTEREST_ENABLED', true), + 'api' => env('PINTEREST_API', 'https://api.pinterest.com/v5'), ], 'bluesky' => [ 'enabled' => env('BLUESKY_ENABLED', true),