264 lines
11 KiB
PHP
264 lines
11 KiB
PHP
<?php
|
|
// V97 LinkedIn Control API - approve/schedule/mark-published + LinkedIn API integration
|
|
header('Content-Type: application/json');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
$action = $_GET['action'] ?? $_POST['action'] ?? 'overview';
|
|
$queue_file = '/opt/weval-l99/linkedin-post-queue.jsonl';
|
|
$published_file = '/opt/weval-l99/linkedin-published.jsonl';
|
|
$scheduled_file = '/opt/weval-l99/linkedin-scheduled.jsonl';
|
|
$stats_file = '/opt/weval-l99/linkedin-page-stats.json';
|
|
$log_file = '/var/log/v97-linkedin-control.log';
|
|
|
|
foreach([$queue_file,$published_file,$scheduled_file] as $f){if(!file_exists($f))@file_put_contents($f,'');}
|
|
|
|
function getEntries($f){
|
|
$r=[];
|
|
foreach(@file($f)?:[] as $l){$e=@json_decode(trim($l),true);if($e)$r[]=$e;}
|
|
return $r;
|
|
}
|
|
function writeAll($f,$entries){
|
|
$content='';
|
|
foreach($entries as $e)$content.=json_encode($e)."\n";
|
|
@file_put_contents($f,$content);
|
|
}
|
|
function logit($msg){@file_put_contents('/var/log/v97-linkedin-control.log',date('c')." $msg\n",FILE_APPEND);}
|
|
|
|
// Get LinkedIn token from secrets.env
|
|
function getLinkedinToken(){
|
|
$token=trim(@shell_exec('grep "^LINKEDIN_ACCESS_TOKEN=" /etc/weval/secrets.env 2>/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']]);
|
|
}
|