refactor: centralize social API base URLs into the configuration file to improve maintainability and environment flexibility
This commit is contained in:
parent
8ba5877bdf
commit
f4fd8a2aab
27 changed files with 176 additions and 75 deletions
|
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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))))',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue