/dev/null | cut -d= -f2-')); return $token ?: null; } function getLinkedinOrgId(){ return trim(@shell_exec('grep "^LINKEDIN_ORG_ID=" /etc/weval/secrets.env 2>/dev/null | cut -d= -f2-')) ?: '69533182'; } switch($action){ case 'overview': $queue=getEntries($queue_file); $published=getEntries($published_file); $scheduled=getEntries($scheduled_file); $stats=@json_decode(@file_get_contents($stats_file),true)?:[]; $pixel=@json_decode(@file_get_contents('http://localhost/api/v85-demo-tracker.php'),true)?:[]; $token=getLinkedinToken(); echo json_encode([ 'v'=>'V97-linkedin-control', 'ts'=>date('c'), 'queue_drafts'=>count(array_filter($queue,fn($e)=>($e['status']??'draft_queued')==='draft_queued')), 'approved'=>count(array_filter($queue,fn($e)=>($e['status']??'')==='approved')), 'scheduled'=>count($scheduled), 'published_count'=>count($published), 'published_today'=>count(array_filter($published,fn($e)=>substr($e['published_at']??'',0,10)===date('Y-m-d'))), 'linkedin_api_ready'=>!empty($token), 'linkedin_org_id'=>getLinkedinOrgId(), 'page_stats'=>$stats, 'pixel_hits_month'=>$pixel['month_hits_total']??0, 'next_scheduled'=>array_filter(array_slice($scheduled,0,3)), 'recent_published'=>array_slice($published,-5), ],JSON_PRETTY_PRINT); break; case 'approve': $id=$_POST['id']??$_GET['id']??''; $queue=getEntries($queue_file); $updated=false; foreach($queue as &$e){ if(($e['id']??'')===$id){$e['status']='approved';$e['approved_at']=date('c');$updated=true;} } writeAll($queue_file,$queue); logit("approved $id"); echo json_encode(['ok'=>$updated,'id'=>$id,'action'=>'approved']); break; case 'schedule': $id=$_POST['id']??$_GET['id']??''; $when=$_POST['when']??$_GET['when']??date('c',time()+3600); $queue=getEntries($queue_file); $post=null; foreach($queue as $e){if(($e['id']??'')===$id){$post=$e;break;}} if(!$post){echo json_encode(['ok'=>false,'err'=>'not_found']);break;} $post['status']='scheduled'; $post['scheduled_at']=$when; $scheduled=getEntries($scheduled_file); $scheduled[]=$post; writeAll($scheduled_file,$scheduled); // Remove from queue $queue=array_filter($queue,fn($e)=>($e['id']??'')!==$id); writeAll($queue_file,$queue); logit("scheduled $id for $when"); echo json_encode(['ok'=>true,'id'=>$id,'scheduled_at'=>$when]); break; case 'publish_now': $id=$_POST['id']??$_GET['id']??''; $queue=getEntries($queue_file); $post=null;$qi=-1; foreach($queue as $i=>$e){if(($e['id']??'')===$id){$post=$e;$qi=$i;break;}} if(!$post){echo json_encode(['ok'=>false,'err'=>'not_found']);break;} $token=getLinkedinToken(); $orgId=getLinkedinOrgId(); $result=['ok'=>false]; if($token){ // Real LinkedIn API call (UGC Posts API) $body=[ 'author'=>"urn:li:organization:$orgId", 'lifecycleState'=>'PUBLISHED', 'specificContent'=>[ 'com.linkedin.ugc.ShareContent'=>[ 'shareCommentary'=>['text'=>$post['post']], 'shareMediaCategory'=>'NONE', ] ], 'visibility'=>['com.linkedin.ugc.MemberNetworkVisibility'=>'PUBLIC'], ]; $ch=curl_init('https://api.linkedin.com/v2/ugcPosts'); curl_setopt_array($ch,[ CURLOPT_POST=>1,CURLOPT_POSTFIELDS=>json_encode($body), CURLOPT_RETURNTRANSFER=>1,CURLOPT_TIMEOUT=>30, CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$token,'Content-Type: application/json','X-Restli-Protocol-Version: 2.0.0'], ]); $resp=curl_exec($ch); $code=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); if($code>=200 && $code<300){ $d=@json_decode($resp,true); $post['linkedin_post_id']=$d['id']??''; $post['published_via']='linkedin_api'; $post['published_at']=date('c'); $post['status']='published'; $result=['ok'=>true,'via'=>'linkedin_api','linkedin_id'=>$post['linkedin_post_id']]; }else{ $result=['ok'=>false,'via'=>'linkedin_api','code'=>$code,'err'=>substr($resp,0,200)]; logit("publish_api fail $id code=$code resp=".substr($resp,0,100)); } }else{ // No token - mark as manually published (assumes Yacine published via copy) $post['published_via']='manual'; $post['published_at']=date('c'); $post['status']='published'; $result=['ok'=>true,'via'=>'manual','note'=>'No LinkedIn API token - marked as manually published']; } if($result['ok']){ $published=getEntries($published_file); $published[]=$post; writeAll($published_file,$published); $queue=array_values(array_filter($queue,fn($e)=>($e['id']??'')!==$id)); writeAll($queue_file,$queue); logit("published $id via ".$result['via']); } echo json_encode($result,JSON_PRETTY_PRINT); break; case 'reject': $id=$_POST['id']??$_GET['id']??''; $queue=getEntries($queue_file); $queue=array_values(array_filter($queue,fn($e)=>($e['id']??'')!==$id)); writeAll($queue_file,$queue); logit("rejected $id"); echo json_encode(['ok'=>true,'id'=>$id,'action'=>'rejected']); break; case 'auto_publish_due': // Cron-driven: publish all scheduled posts whose time has come $scheduled=getEntries($scheduled_file); $now=time(); $published_new=0;$failed=0;$remaining=[]; foreach($scheduled as $p){ $due=strtotime($p['scheduled_at']??'2099-01-01'); if($due<=$now){ $_GET['id']=$p['id']; // Publish via API or mark manual $token=getLinkedinToken(); if($token){ $body=[ 'author'=>"urn:li:organization:".getLinkedinOrgId(), 'lifecycleState'=>'PUBLISHED', 'specificContent'=>[ 'com.linkedin.ugc.ShareContent'=>[ 'shareCommentary'=>['text'=>$p['post']], 'shareMediaCategory'=>'NONE', ] ], 'visibility'=>['com.linkedin.ugc.MemberNetworkVisibility'=>'PUBLIC'], ]; $ch=curl_init('https://api.linkedin.com/v2/ugcPosts'); curl_setopt_array($ch,[CURLOPT_POST=>1,CURLOPT_POSTFIELDS=>json_encode($body),CURLOPT_RETURNTRANSFER=>1,CURLOPT_TIMEOUT=>30,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$token,'Content-Type: application/json','X-Restli-Protocol-Version: 2.0.0']]); $resp=curl_exec($ch);$code=curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch); if($code>=200 && $code<300){ $p['published_at']=date('c');$p['published_via']='linkedin_api_cron';$p['status']='published'; $published=getEntries($published_file);$published[]=$p;writeAll($published_file,$published); $published_new++; }else{$failed++;$remaining[]=$p;logit("cron publish fail ".$p['id']." code=$code");} }else{ // Move to queue with priority flag for Yacine $queue=getEntries($queue_file); $p['status']='due_pending_manual'; $p['due_since']=$p['scheduled_at']; $queue[]=$p;writeAll($queue_file,$queue); logit("due but no token - moved to queue ".$p['id']); } }else{ $remaining[]=$p; } } writeAll($scheduled_file,$remaining); echo json_encode(['ok'=>true,'published'=>$published_new,'failed'=>$failed,'remaining_scheduled'=>count($remaining)]); break; case 'log': $tail=@shell_exec("tail -50 $log_file 2>/dev/null") ?: ''; echo json_encode(['log'=>$tail]); break; case 'all_queues': echo json_encode([ 'queue_drafts'=>getEntries($queue_file), 'scheduled'=>getEntries($scheduled_file), 'published'=>getEntries($published_file), ],JSON_PRETTY_PRINT); break; case 'browser_publish_id': $id = escapeshellarg($_POST['id'] ?? $_GET['id'] ?? ''); $out = shell_exec("cd /tmp && timeout 90 python3 /opt/weval-l99/v98-linkedin-browser-publish.py publish_id $id 2>&1"); $data = @json_decode(trim($out), true); echo json_encode($data ?: ['ok'=>false,'raw'=>substr($out,0,300)], JSON_PRETTY_PRINT); break; case 'browser_publish_due': $out = shell_exec("cd /tmp && timeout 180 python3 /opt/weval-l99/v98-linkedin-browser-publish.py publish_due 2>&1"); $data = @json_decode(trim($out), true); echo json_encode($data ?: ['ok'=>false,'raw'=>substr($out,0,300)], JSON_PRETTY_PRINT); break; case 'browser_session_status': $cookies = '/opt/weval-l99/browser-sessions/linkedin/Default/Cookies'; echo json_encode([ 'session_exists' => file_exists($cookies), 'last_update' => file_exists($cookies) ? date('c', filemtime($cookies)) : null, 'age_hours' => file_exists($cookies) ? round((time()-filemtime($cookies))/3600, 1) : null, ]); break; case 'browser_inject_session': $out = shell_exec("cd /tmp && timeout 60 python3 /opt/weval-l99/v98-linkedin-session-inject.py 2>&1"); $data = @json_decode(trim($out), true); echo json_encode($data ?: ['raw' => substr($out, 0, 300)], JSON_PRETTY_PRINT); break; case 'v99_auto_login': $out = shell_exec("cd /tmp && timeout 60 python3 /opt/weval-l99/v99-linkedin-auto-login.py 2>&1"); $data = @json_decode(trim($out), true); echo json_encode($data ?: ['raw'=>substr($out,0,400)], JSON_PRETTY_PRINT); break; default: echo json_encode(['err'=>'unknown_action','available'=>['overview','approve','schedule','publish_now','reject','auto_publish_due','log','all_queues','browser_publish_id','browser_publish_due','browser_session_status','browser_inject_session']]); }