feat: integrate LinkedIn API for automated social media post publishing via service and console command
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LinkedInService
|
||||
{
|
||||
protected string $clientId;
|
||||
protected string $clientSecret;
|
||||
protected string $organizationId;
|
||||
protected string $redirectUri;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->clientId = config('linkedin.client_id', '');
|
||||
$this->clientSecret = config('linkedin.client_secret', '');
|
||||
$this->organizationId = config('linkedin.organization_id', '35611757');
|
||||
$this->redirectUri = config('linkedin.redirect_uri', url('/admin/linkedin/callback'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OAuth 2.0 Authorization URL for admin redirect
|
||||
*/
|
||||
public function getAuthorizationUrl(): string
|
||||
{
|
||||
$scopes = implode(' ', config('linkedin.scopes', [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'w_member_social',
|
||||
'w_organization_social',
|
||||
'r_organization_admin',
|
||||
'rw_organization_admin',
|
||||
]));
|
||||
|
||||
$state = Str::random(32);
|
||||
session(['linkedin_oauth_state' => $state]);
|
||||
|
||||
$queryParams = http_build_query([
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->clientId,
|
||||
'redirect_uri' => $this->redirectUri,
|
||||
'state' => $state,
|
||||
'scope' => $scopes,
|
||||
]);
|
||||
|
||||
return "https://www.linkedin.com/oauth/v2/authorization?" . $queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth Callback and exchange authorization code for access token
|
||||
*/
|
||||
public function handleCallback(string $code): array
|
||||
{
|
||||
try {
|
||||
$response = Http::asForm()->post('https://www.linkedin.com/oauth/v2/accessToken', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $this->redirectUri,
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('LinkedIn OAuth Token Exchange Failed', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'LinkedIn Access Token alınamadı: ' . ($response->json('error_description') ?? $response->body()),
|
||||
];
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$accessToken = $data['access_token'] ?? null;
|
||||
$expiresIn = $data['expires_in'] ?? 5184000; // Default 60 days in seconds
|
||||
|
||||
if (!$accessToken) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'LinkedIn Access Token yanıt içinde bulunamadı.',
|
||||
];
|
||||
}
|
||||
|
||||
$expiresAt = now()->addSeconds($expiresIn)->toDateTimeString();
|
||||
|
||||
// Save in Setting model
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'linkedin_access_token'],
|
||||
['value' => $accessToken, 'type' => 'text', 'group' => 'social_media', 'label' => 'LinkedIn Access Token']
|
||||
);
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'linkedin_token_expires_at'],
|
||||
['value' => $expiresAt, 'type' => 'datetime', 'group' => 'social_media', 'label' => 'LinkedIn Token Expiration']
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'LinkedIn hesabınız başarıyla bağlandı! Access Token kaydedildi.',
|
||||
'expires_at' => $expiresAt,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('LinkedIn Callback Error: ' . $e->getMessage());
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Hata oluştu: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if valid LinkedIn Access Token exists
|
||||
*/
|
||||
public function isAuthorized(): bool
|
||||
{
|
||||
$token = Setting::get('linkedin_access_token');
|
||||
$expiresAt = Setting::get('linkedin_token_expires_at');
|
||||
|
||||
if (!$token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($expiresAt && now()->greaterThanOrEqualTo($expiresAt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining days of Access Token
|
||||
*/
|
||||
public function getTokenExpirationStatus(): array
|
||||
{
|
||||
$expiresAt = Setting::get('linkedin_token_expires_at');
|
||||
|
||||
if (!$expiresAt) {
|
||||
return [
|
||||
'is_valid' => false,
|
||||
'message' => 'Yetkilendirme yapılmadı.',
|
||||
];
|
||||
}
|
||||
|
||||
$date = \Carbon\Carbon::parse($expiresAt);
|
||||
$diff = now()->diffInDays($date, false);
|
||||
|
||||
if ($diff <= 0) {
|
||||
return [
|
||||
'is_valid' => false,
|
||||
'message' => 'Erişim anahtarının süresi doldu (' . $date->format('d.m.Y H:i') . '). Yeniden yetkilendirin.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'is_valid' => true,
|
||||
'days_left' => $diff,
|
||||
'expires_at' => $date->format('d.m.Y H:i'),
|
||||
'message' => "Erişim anahtarı aktif. Kalan süre: {$diff} gün ({$date->format('d.m.Y H:i')}).",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a post to LinkedIn Company Page
|
||||
*/
|
||||
public function sharePost(string $title, string $text, ?string $url = null, ?string $imageUrl = null): array
|
||||
{
|
||||
if (!$this->isAuthorized()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'LinkedIn yetkilendirmesi bulunamadı veya süresi dolmuş.',
|
||||
];
|
||||
}
|
||||
|
||||
$token = Setting::get('linkedin_access_token');
|
||||
$authorUrn = "urn:li:organization:{$this->organizationId}";
|
||||
|
||||
// Prepare UGC Post payload
|
||||
$shareCommentary = trim($title . "\n\n" . $text);
|
||||
if ($url) {
|
||||
$shareCommentary .= "\n\n" . $url;
|
||||
}
|
||||
|
||||
$mediaContent = [];
|
||||
if ($url) {
|
||||
$mediaItem = [
|
||||
'status' => 'READY',
|
||||
'originalUrl' => $url,
|
||||
'title' => [
|
||||
'text' => Str::limit($title, 200),
|
||||
],
|
||||
'description' => [
|
||||
'text' => Str::limit(strip_tags($text), 250),
|
||||
],
|
||||
];
|
||||
$mediaContent[] = $mediaItem;
|
||||
}
|
||||
|
||||
$shareContent = [
|
||||
'shareCommentary' => [
|
||||
'text' => $shareCommentary,
|
||||
],
|
||||
'shareMediaCategory' => !empty($mediaContent) ? 'ARTICLE' : 'NONE',
|
||||
];
|
||||
|
||||
if (!empty($mediaContent)) {
|
||||
$shareContent['media'] = $mediaContent;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'author' => $authorUrn,
|
||||
'lifecycleState' => 'PUBLISHED',
|
||||
'specificContent' => [
|
||||
'com.linkedin.ugc.ShareContent' => $shareContent,
|
||||
],
|
||||
'visibility' => [
|
||||
'com.linkedin.ugc.ShareProductVisibility' => 'PUBLIC',
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'X-Restli-Protocol-Version' => '2.0.0',
|
||||
'Content-Type' => 'application/json',
|
||||
])->post('https://api.linkedin.com/v2/ugcPosts', $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
$postId = $response->header('x-restli-id') ?? $response->json('id');
|
||||
Log::info('LinkedIn post published successfully', [
|
||||
'organization_id' => $this->organizationId,
|
||||
'post_id' => $postId,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'LinkedIn gönderisi başarıyla paylaşıldı!',
|
||||
'post_id' => $postId,
|
||||
];
|
||||
}
|
||||
|
||||
Log::error('LinkedIn Share API Failed', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'LinkedIn gönderisi paylaşılamadı: ' . ($response->json('message') ?? $response->body()),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('LinkedIn Share Post Exception: ' . $e->getMessage());
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Gönderi paylaşılırken bir hata oluştu: ' . $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user