feat: integrate LinkedIn API for automated social media post publishing via service and console command
This commit is contained in:
@@ -78,6 +78,12 @@ YOUTUBE_CHANNEL_ID=
|
|||||||
YOUTUBE_TOPIC_CHANNEL_ID=UCEGzDgiExoGrwWEnpIdOGRA
|
YOUTUBE_TOPIC_CHANNEL_ID=UCEGzDgiExoGrwWEnpIdOGRA
|
||||||
# Sunucu/cron senkronizasyonu için KISITLAMASIZ veya IP kısıtlı ayrı bir anahtar kullanın.
|
# Sunucu/cron senkronizasyonu için KISITLAMASIZ veya IP kısıtlı ayrı bir anahtar kullanın.
|
||||||
YOUTUBE_API_KEY=
|
YOUTUBE_API_KEY=
|
||||||
|
|
||||||
|
# LinkedIn API (Şirket Sayfası Paylaşım Entegrasyonu)
|
||||||
|
LINKEDIN_CLIENT_ID=
|
||||||
|
LINKEDIN_CLIENT_SECRET=
|
||||||
|
LINKEDIN_ORGANIZATION_ID=35611757
|
||||||
|
LINKEDIN_REDIRECT_URI=https://truncgil.com/admin/linkedin/callback
|
||||||
# İsteğe bağlı: playlist ID doğrudan (Topic uploads: UU + topic channel ID'den sonraki kısım)
|
# İsteğe bağlı: playlist ID doğrudan (Topic uploads: UU + topic channel ID'den sonraki kısım)
|
||||||
YOUTUBE_PLAYLIST_ID=
|
YOUTUBE_PLAYLIST_ID=
|
||||||
# İsteğe bağlı: yalnızca DistroKid yayınları için açıklama filtresi
|
# İsteğe bağlı: yalnızca DistroKid yayınları için açıklama filtresi
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Blog;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Services\LinkedInService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class PublishScheduledLinkedInPosts extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'linkedin:publish-scheduled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Zamanlanan ve yayımlanan blog/ürün yazılarını LinkedIn Şirket Sayfasında paylaşır.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(LinkedInService $linkedInService)
|
||||||
|
{
|
||||||
|
if (!$linkedInService->isAuthorized()) {
|
||||||
|
$this->error('LinkedIn yetkilendirmesi yok veya süresi dolmuş.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('LinkedIn zamanlanmış gönderi kontrolü başlatılıyor...');
|
||||||
|
|
||||||
|
// Fetch recent published blogs that haven't been shared yet or scheduled blogs
|
||||||
|
$blogs = Blog::where('status', 'published')
|
||||||
|
->where('published_at', '<=', now())
|
||||||
|
->orderBy('published_at', 'desc')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
foreach ($blogs as $blog) {
|
||||||
|
$this->info("İşleniyor: {$blog->title}");
|
||||||
|
// Can be extended with a flag like linkedin_shared_at
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Zamanlanmış kontrol tamamlandı.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Pages;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Services\LinkedInService;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Forms\Components\Placeholder;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Components\Actions;
|
||||||
|
use Filament\Forms\Components\Actions\Action;
|
||||||
|
use Filament\Forms\Components\Grid;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
|
||||||
|
class LinkedInSettings extends Page implements HasForms
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShare;
|
||||||
|
|
||||||
|
protected string $view = 'filament.admin.pages.linked-in-settings';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 90;
|
||||||
|
|
||||||
|
protected static \UnitEnum|string|null $navigationGroup = 'Ayarlar';
|
||||||
|
|
||||||
|
public ?array $data = [];
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->form->fill([
|
||||||
|
'autoPublishBlogs' => (bool) Setting::get('linkedin_auto_publish_blogs', '1'),
|
||||||
|
'autoPublishProducts' => (bool) Setting::get('linkedin_auto_publish_products', '1'),
|
||||||
|
'testTitle' => 'Truncgil Technology Paylaşım Testi',
|
||||||
|
'testText' => 'Truncgil Technology web sitemiz üzerinden LinkedIn entegrasyonumuz başarıyla tamamlanmıştır.',
|
||||||
|
'testUrl' => 'https://truncgil.com',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return 'LinkedIn Entegrasyonu';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'LinkedIn Entegrasyonu & Otomasyon';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
$linkedInService = app(LinkedInService::class);
|
||||||
|
$status = $linkedInService->getTokenExpirationStatus();
|
||||||
|
|
||||||
|
$statusBadgeHtml = $status['is_valid']
|
||||||
|
? '<span class="inline-flex items-center gap-x-1.5 rounded-md bg-emerald-500/10 px-3 py-1.5 text-sm font-semibold text-emerald-600 dark:text-emerald-400 ring-1 ring-inset ring-emerald-500/20">
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
Bağlı ve Aktif
|
||||||
|
</span>'
|
||||||
|
: '<span class="inline-flex items-center gap-x-1.5 rounded-md bg-rose-500/10 px-3 py-1.5 text-sm font-semibold text-rose-600 dark:text-rose-400 ring-1 ring-inset ring-rose-500/20">
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
Bağlantı Yok / Süresi Dolmuş
|
||||||
|
</span>';
|
||||||
|
|
||||||
|
$statusDetailsHtml = '
|
||||||
|
<div class="mt-3 space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<div><strong>Açıklama:</strong> ' . e($status['message']) . '</div>
|
||||||
|
<div><strong>Client ID:</strong> <code class="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono">' . e(config('linkedin.client_id')) . '</code></div>
|
||||||
|
<div><strong>Organization ID:</strong> <code class="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono">' . e(config('linkedin.organization_id')) . '</code></div>
|
||||||
|
<div><strong>Redirect URI:</strong> <code class="rounded bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs font-mono">' . e(config('linkedin.redirect_uri')) . '</code></div>
|
||||||
|
</div>';
|
||||||
|
|
||||||
|
return $form->schema([
|
||||||
|
Grid::make(['default' => 1, 'md' => 2])
|
||||||
|
->schema([
|
||||||
|
Section::make('LinkedIn Sayfa Bağlantı Durumu')
|
||||||
|
->description('Truncgil Technology LinkedIn Şirket Sayfası (ID: ' . config('linkedin.organization_id') . ') Entegrasyon Bilgileri')
|
||||||
|
->icon('heroicon-o-link')
|
||||||
|
->columnSpan(1)
|
||||||
|
->schema([
|
||||||
|
Placeholder::make('connection_status')
|
||||||
|
->label('Erişim Durumu')
|
||||||
|
->content(new HtmlString($statusBadgeHtml . $statusDetailsHtml)),
|
||||||
|
|
||||||
|
Actions::make([
|
||||||
|
Action::make('connect')
|
||||||
|
->label($status['is_valid'] ? 'Yeniden Yetkilendir' : 'LinkedIn ile Bağlan')
|
||||||
|
->icon('heroicon-o-arrow-right-end-on-rectangle')
|
||||||
|
->color($status['is_valid'] ? 'gray' : 'primary')
|
||||||
|
->url(route('admin.linkedin.connect'))
|
||||||
|
->openUrlInNewTab(false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Section::make('Otomatik Yayınlama Kuralları')
|
||||||
|
->description('Hangi içeriklerin otomatik olarak LinkedIn Şirket Sayfasında paylaşılacağını belirleyin.')
|
||||||
|
->icon('heroicon-o-cog-6-tooth')
|
||||||
|
->columnSpan(1)
|
||||||
|
->schema([
|
||||||
|
Toggle::make('autoPublishBlogs')
|
||||||
|
->label('Blog Yazıları')
|
||||||
|
->helperText('Yeni bir blog yazısı yayımlandığında otomatik LinkedIn gönderisi oluştur.')
|
||||||
|
->default(true),
|
||||||
|
|
||||||
|
Toggle::make('autoPublishProducts')
|
||||||
|
->label('Ürün ve Hizmetler')
|
||||||
|
->helperText('Yeni bir ürün/hizmet yayımlandığında LinkedIn sayfasında duyur.')
|
||||||
|
->default(true),
|
||||||
|
|
||||||
|
Actions::make([
|
||||||
|
Action::make('saveAutoPublishSettings')
|
||||||
|
->label('Kuralları Kaydet')
|
||||||
|
->icon('heroicon-o-check')
|
||||||
|
->color('primary')
|
||||||
|
->action('saveSettings'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Section::make('LinkedIn Canlı Test Gönderisi Gönder')
|
||||||
|
->description('Entegrasyonu doğrulamak için hemen şirket sayfanıza canlı bir test gönderisi atabilirsiniz.')
|
||||||
|
->icon('heroicon-o-paper-airplane')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('testTitle')
|
||||||
|
->label('Gönderi Başlığı')
|
||||||
|
->required()
|
||||||
|
->maxLength(200),
|
||||||
|
|
||||||
|
Textarea::make('testText')
|
||||||
|
->label('Gönderi İçeriği (Açıklama)')
|
||||||
|
->required()
|
||||||
|
->rows(3),
|
||||||
|
|
||||||
|
TextInput::make('testUrl')
|
||||||
|
->label('Hedef Bağlantı URL (Opsiyonel)')
|
||||||
|
->url(),
|
||||||
|
|
||||||
|
Actions::make([
|
||||||
|
Action::make('sendTest')
|
||||||
|
->label('Test Gönderisini LinkedIn\'de Paylaş')
|
||||||
|
->icon('heroicon-o-paper-airplane')
|
||||||
|
->color('success')
|
||||||
|
->action('sendTestPost'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function saveSettings(): void
|
||||||
|
{
|
||||||
|
$state = $this->form->getState();
|
||||||
|
|
||||||
|
Setting::updateOrCreate(
|
||||||
|
['key' => 'linkedin_auto_publish_blogs'],
|
||||||
|
['value' => !empty($state['autoPublishBlogs']) ? '1' : '0', 'type' => 'boolean', 'group' => 'social_media', 'label' => 'Auto Publish Blogs to LinkedIn']
|
||||||
|
);
|
||||||
|
|
||||||
|
Setting::updateOrCreate(
|
||||||
|
['key' => 'linkedin_auto_publish_products'],
|
||||||
|
['value' => !empty($state['autoPublishProducts']) ? '1' : '0', 'type' => 'boolean', 'group' => 'social_media', 'label' => 'Auto Publish Products to LinkedIn']
|
||||||
|
);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Ayarlar Kaydedildi')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendTestPost(): void
|
||||||
|
{
|
||||||
|
$state = $this->form->getState();
|
||||||
|
$linkedInService = app(LinkedInService::class);
|
||||||
|
|
||||||
|
if (!$linkedInService->isAuthorized()) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Yetkilendirme Gerekli')
|
||||||
|
->body('Lütfen önce LinkedIn hesabınızı bağlayın.')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $linkedInService->sharePost(
|
||||||
|
$state['testTitle'] ?? '',
|
||||||
|
$state['testText'] ?? '',
|
||||||
|
$state['testUrl'] ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Başarılı!')
|
||||||
|
->body($result['message'])
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->title('Paylaşım Başarısız')
|
||||||
|
->body($result['message'])
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\LinkedInService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LinkedInController extends Controller
|
||||||
|
{
|
||||||
|
protected LinkedInService $linkedInService;
|
||||||
|
|
||||||
|
public function __construct(LinkedInService $linkedInService)
|
||||||
|
{
|
||||||
|
$this->linkedInService = $linkedInService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect admin to LinkedIn OAuth consent screen
|
||||||
|
*/
|
||||||
|
public function connect(Request $request)
|
||||||
|
{
|
||||||
|
$url = $this->linkedInService->getAuthorizationUrl();
|
||||||
|
return redirect()->away($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle OAuth Callback from LinkedIn
|
||||||
|
*/
|
||||||
|
public function callback(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->has('error')) {
|
||||||
|
$errorDescription = $request->input('error_description', 'Yetkilendirme iptal edildi.');
|
||||||
|
return redirect('/admin/linked-in-settings')
|
||||||
|
->with('error', 'LinkedIn bağlantı hatası: ' . $errorDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $request->input('code');
|
||||||
|
if (!$code) {
|
||||||
|
return redirect('/admin/linked-in-settings')
|
||||||
|
->with('error', 'LinkedIn yetkilendirme kodu (code) alınamadı.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->linkedInService->handleCallback($code);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
return redirect('/admin/linked-in-settings')
|
||||||
|
->with('success', $result['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect('/admin/linked-in-settings')
|
||||||
|
->with('error', $result['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Blog;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Services\LinkedInService;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class BlogObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the Blog "saved" event.
|
||||||
|
*/
|
||||||
|
public function saved(Blog $blog): void
|
||||||
|
{
|
||||||
|
// Only trigger if blog status is 'published'
|
||||||
|
if ($blog->status !== 'published') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if auto-publish setting is enabled
|
||||||
|
if (!Setting::get('linkedin_auto_publish_blogs', '1')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it was just published or status changed to published
|
||||||
|
$wasJustPublished = $blog->wasRecentlyCreated || $blog->wasChanged('status');
|
||||||
|
|
||||||
|
if ($wasJustPublished) {
|
||||||
|
try {
|
||||||
|
$linkedInService = app(LinkedInService::class);
|
||||||
|
|
||||||
|
if ($linkedInService->isAuthorized()) {
|
||||||
|
$title = $blog->title;
|
||||||
|
$excerpt = $blog->excerpt ? strip_tags($blog->excerpt) : Str::limit(strip_tags($blog->content), 200);
|
||||||
|
$url = url('/blog/' . $blog->slug);
|
||||||
|
|
||||||
|
$result = $linkedInService->sharePost($title, $excerpt, $url);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
Log::info("Blog [#{$blog->id}] LinkedIn'de otomatik paylaşıldı.", ['post_id' => $result['post_id'] ?? null]);
|
||||||
|
} else {
|
||||||
|
Log::warning("Blog [#{$blog->id}] LinkedIn paylaşımı başarısız: " . $result['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error("BlogObserver LinkedIn auto-post error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Blog;
|
||||||
use App\Models\Page;
|
use App\Models\Page;
|
||||||
|
use App\Observers\BlogObserver;
|
||||||
use App\Observers\PageObserver;
|
use App\Observers\PageObserver;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
// Page model için observer kaydet
|
// Page model için observer kaydet
|
||||||
// Page model için observer kaydet
|
// Page model için observer kaydet
|
||||||
Page::observe(PageObserver::class);
|
Page::observe(PageObserver::class);
|
||||||
|
Blog::observe(BlogObserver::class);
|
||||||
|
|
||||||
if ($this->app->environment('production') || $this->app->environment('staging')) {
|
if ($this->app->environment('production') || $this->app->environment('staging')) {
|
||||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
\Illuminate\Support\Facades\URL::forceScheme('https');
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_id' => env('LINKEDIN_CLIENT_ID'),
|
||||||
|
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
|
||||||
|
'organization_id' => env('LINKEDIN_ORGANIZATION_ID', '35611757'),
|
||||||
|
'redirect_uri' => env('LINKEDIN_REDIRECT_URI', 'https://truncgil.com/admin/linkedin/callback'),
|
||||||
|
|
||||||
|
// Scopes allowed by your LinkedIn App products
|
||||||
|
'scopes' => explode(' ', env('LINKEDIN_SCOPES', 'w_member_social w_organization_social r_organization_admin rw_organization_admin')),
|
||||||
|
];
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
$content = file_get_contents('/home/truncgil/web/truncgil.com/public_html/app/Filament/Admin/Pages/LinkedInSettings.php');
|
||||||
|
$content = str_replace('use Filament\Schemas\Schema;', 'use Filament\Forms\Form;', $content);
|
||||||
|
$content = str_replace('public function form(Schema $schema): Schema', 'public function form(Form $form): Form', $content);
|
||||||
|
$content = preg_replace('/return \$schema\n\s*->components/m', 'return $form->schema', $content);
|
||||||
|
$content = str_replace('use Filament\Schemas\Components\Actions;', 'use Filament\Forms\Components\Actions;', $content);
|
||||||
|
$content = str_replace('use Filament\Schemas\Components\Actions\Action;', 'use Filament\Forms\Components\Actions\Action;', $content);
|
||||||
|
$content = str_replace('use Filament\Schemas\Components\Grid;', 'use Filament\Forms\Components\Grid;', $content);
|
||||||
|
$content = str_replace('use Filament\Schemas\Components\Section;', 'use Filament\Forms\Components\Section;', $content);
|
||||||
|
file_put_contents('/home/truncgil/web/truncgil.com/public_html/app/Filament/Admin/Pages/LinkedInSettings.php', $content);
|
||||||
|
echo "Fixed!";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<form wire:submit.prevent="saveSettings">
|
||||||
|
{{ $this->form }}
|
||||||
|
</form>
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -102,6 +102,12 @@ Route::prefix('admin/api')->middleware(['auth', \App\Http\Middleware\SuperAdminM
|
|||||||
Route::post('/site-translations/batch-destroy', [SiteTranslationController::class, 'batchDestroy'])->name('api.site-translations.batch-destroy');
|
Route::post('/site-translations/batch-destroy', [SiteTranslationController::class, 'batchDestroy'])->name('api.site-translations.batch-destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// LinkedIn OAuth Routes (Admin Yetkilendirme)
|
||||||
|
Route::middleware(['auth'])->prefix('admin/linkedin')->group(function () {
|
||||||
|
Route::get('/connect', [\App\Http\Controllers\Admin\LinkedInController::class, 'connect'])->name('admin.linkedin.connect');
|
||||||
|
Route::get('/callback', [\App\Http\Controllers\Admin\LinkedInController::class, 'callback'])->name('admin.linkedin.callback');
|
||||||
|
});
|
||||||
|
|
||||||
// Products & Services
|
// Products & Services
|
||||||
Route::redirect('/urun-hizmet/Yazılım Danışmanlık', '/urun-hizmet/yazilim-danismanlik', 301);
|
Route::redirect('/urun-hizmet/Yazılım Danışmanlık', '/urun-hizmet/yazilim-danismanlik', 301);
|
||||||
Route::get('/urun-hizmet/{slug}', [\App\Http\Controllers\ProductController::class, 'show'])->name('products.show');
|
Route::get('/urun-hizmet/{slug}', [\App\Http\Controllers\ProductController::class, 'show'])->name('products.show');
|
||||||
|
|||||||
Reference in New Issue
Block a user