Major refactoring: now it usese the JSON feed instead of the RSS one; many minor changes; bumped version to 0.5

This commit is contained in:
pezcurrel 2024-10-29 13:54:18 +01:00
parent 15597fa1cc
commit 2458d03010

367
gancioff
View file

@ -16,12 +16,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
$SNAME='GancioFF';
$SNAME='GancioF2F';
$ENAME=strtolower($SNAME);
$SVERS='0.4.4';
$SVERS='0.5';
require __DIR__.'/lib/ckmkeys.php';
require __DIR__.'/lib/gettlds.php';
require __DIR__.'/lib/mastodon-postLength.php';
require __DIR__.'/lib/mb_ucfirst.php';
require __DIR__.'/lib/hashtag.php';
require __DIR__.'/lib/html2text.php';
require __DIR__.'/lib/curl.php';
@ -33,16 +35,17 @@ $help=
[[[ DESCRIPTION ]]]
This is {$SNAME} v{$SVERS}, a CLI PHP script that can be used to periodically
fetch the RSS feed from an instance of Gancio (https://gancio.org) and post
its new entries the events announcements on the fediverse through a
Mastodon account, recording into a state file a reference to each already
posted announcement in order to post only new or changed ones on each run.
This is {$SNAME} v{$SVERS} («GancioFeed2Fedi»), a CLI PHP script that can
be used to periodically fetch the JSON feed from an instance of Gancio
(https://gancio.org) and post its new or changed events announcements on the
Fediverse through a Mastodon account, recording into a state file a reference
to each already posted announcement in order to post only new or changed ones
on each run.
It can be useful, for example, when the admins of a Gancio instance chose not
to use its federation feature because it would be too heavy on its server:
in fact, {$SNAME} is a light alternative to federating the Gancio instance,
moving from its server to the one running Mastodon the burden of posting each
announcement to each fediverse instance hosting at least one follower, and of
announcement to each Fediverse instance hosting at least one follower, and of
sending them the image a Gancio user can attach to each announcement, because
{$SNAME} will fetch it only once and attach it to the Mastodon post; moreover,
by default, if an announcement on the Gancio instance fits into a Mastodon
@ -62,9 +65,9 @@ as an argument on the command line.
--- Example configuration file ---
# Lines beginnig with a «#» and empty lines will be ignored
# «feed_url» is required to specify the URL to fetch the RSS feed from.
# «feed_hostname» is required to specify the hostname of the Gancio instance.
# For example:
feed_url = https://gancio.some.domain/feed/rss?show_recurrent=true
feed_hostname = gancio.some.domain
# «fedi_hostname» is required to specify the hostname of the Mastodon instance
# you want to post to. For example:
@ -85,12 +88,12 @@ fedi_token = w6oQ_Ot2LSAm_Q31hrvp0asfl22ip3O4ipYq1kV1ceY
# announcements (on every run, {$SNAME} will check this file for entries older
# than one year and discard them, to avoid the state file to grow too much).
# For example:
state_file_absolute_path = /var/local/cache/gancio.some.domain.feed.state
state_file_absolute_path = /var/local/cache/gancioff/gancio.some.domain.state
# «timezone» is required to specify the timezone of the Gancio instance, in
# order for {$SNAME} to calculate the correct datetimes. You can list the
# supported timezones using option «-T» or «--timezones» (see the related
# entry in the «OPTIONS» section. For example:
# entry in the «OPTIONS» section). For example:
timezone = Europe/Rome
# «posts_language» is required to specify the ISO 639-1 code for the language
@ -180,7 +183,7 @@ conditions; see <http://www.gnu.org/licenses/> for details.\n";
$confFP=null;
$conf=[
'feed_url'=>['required'=>true, 'default'=>null],
'feed_hostname'=>['required'=>true, 'default'=>null],
'fedi_hostname'=>['required'=>true, 'default'=>null],
'fedi_token'=>['required'=>true, 'default'=>null],
'state_file_absolute_path'=>['required'=>true, 'default'=>null],
@ -324,9 +327,9 @@ if (file_exists($conf['state_file_absolute_path'])) {
$i=0;
$buff=file($conf['state_file_absolute_path'],FILE_IGNORE_NEW_LINES);
foreach ($buff as $key=>$val) {
if (preg_match('#^(\d+)\t(\d+)\t(\S+)$#',$val,$matches)===1) {
if ($matches[1]+0>=$graceLine)
$refs[$matches[3]]=['postdate'=>$matches[1], 'pubdate'=>$matches[2]];
if (preg_match('#^(\S+)\t(\S+)\t(\d+)$#',$val,$matches)===1) {// todo: refine the pattern
if ($matches[3]+0>=$graceLine)
$refs[$matches[1]]=['updatedAt'=>$matches[2], 'postedAt'=>$matches[3]];
else
$i++;
} else {
@ -336,232 +339,224 @@ if (file_exists($conf['state_file_absolute_path'])) {
unset($buff);
$fh=fopen($conf['state_file_absolute_path'],'w');
foreach ($refs as $key=>$val)
fwrite($fh,"{$val['postdate']}\t{$val['pubdate']}\t{$key}\n");
fwrite($fh,"{$key}\t{$val['updatedAt']}\t{$val['postedAt']}\n");
fclose($fh);
vecho($opts['verbose'],'Info: got '.count($refs)." reference(s) to already posted announcement(s) from state file «{$conf['state_file_absolute_path']}»; removed {$i} reference(s) older than one year.\n");
} elseif (is_null($opts['do-post']) && !$opts['test']) {
dieyoung("State file «{$conf['state_file_absolute_path']}» doesnt exist yet, so this is probably a first run on feed «{$conf['feed_url']}»; thus, all the announcements {$SNAME} may find in the feed will be considered new and, as a precaution against unintentionally flooding your Mastodon instances «Local» timeline, and possibly your followers «Home» timelines, you have to explicitly declare whether you want it to post them all, or not, by explicitly setting option «-p» or «--do-post» to «y» («yes») or «n» («no»); mind that in both cases the references to the announcements will be recorded in the state file, so the announcements wont be posted again on subsequent runs (unless they were changed in the meantime).\n",1);
dieyoung("Warning: state file «{$conf['state_file_absolute_path']}» doesnt exist yet, so this is probably a first run on Gancio instance «{$conf['feed_hostname']}»; thus, all the announcements {$SNAME} may find in the feed will be considered new and, as a precaution against unintentionally flooding your Mastodon instances «Local» timeline, and possibly your followers «Home» timelines, you have to explicitly declare whether you want it to post them all, or not, by explicitly setting option «-p» or «--do-post» to «y» («yes») or «n» («no»); mind that in both cases the references to the announcements will be recorded in the state file, so the announcements wont be posted again on subsequent runs (unless they were changed in the meantime).\n",1);
} else {
vecho($opts['verbose'],"Info: state file «{$conf['state_file_absolute_path']}» was not found.\n");
}
//print_r($refs);die();
if (is_null($opts['do-post']) || $opts['test']) $opts['do-post']=true;
vecho($opts['verbose'],"Info: trying to fetch feed from «{$conf['feed_url']}».\n");
$feed=curl($conf['feed_url'],null,['Accept: application/xml']);
if ($feed['content']===false) dieYoung("Error: could not connect to «{$conf['feed_url']}» (error: «{$feed['error']}»).\n",1);
if ($feed['httpcode']!='200') dieYoung("Error: «{$conf['feed_url']} returned http code «{$res['httpcode']}».\n",1);
$feed=@simplexml_load_string($feed['content'],null,LIBXML_NOCDATA);
if ($feed===false) dieYoung("Error: got no valid XML from «{$conf['feed_url']}».\n",1);
$url="https://{$conf['feed_hostname']}/feed/json?show_recurrent=true";
vecho($opts['verbose'],"Info: trying to fetch JSON feed from «{$url}».\n");
$feed=curl($url,null,['Accept: application/json']);
if ($feed['content']===false) dieYoung("Error: could not connect to «{$url}» (error: «{$feed['error']}»).\n",1);
$feed['content']=@json_decode($feed['content'],true);
(!is_null($feed['content']) && isset($feed['content']['error'])) ? $buff=" ({$feed['content']['error']})" : $buff='';
if ($feed['httpcode']!='200') dieYoung("Error: «{$url} returned http code «{$res['httpcode']}»{$buff}.\n",1);
$feed=$feed['content'];
if (is_null($feed)) dieYoung("Error: got no valid JSON from «{$url}».\n",1);
//print_r($feed);
if (!isset($feed->channel->item) || !is_iterable($feed->channel->item) || !is_countable($feed->channel->item)) dieYoung("Error: feed from «{$conf['feed_url']}» had unexpected format.\n",1);
$itemsCount=$feed->channel->item->count();
if ($itemsCount==0) exitYoung("Info: feed from «{$conf['feed_url']}» was empty, bye.\n");
$buff=['id', 'title', 'slug', 'description', 'multidate', 'start_datetime', 'end_datetime', 'media', 'online_locations', 'updatedAt', 'tags', 'place'];
foreach ($feed as $item)
if (!ckmkeys($buff,$item)) dieYoung("Error: feed from «{$url}» had unexpected format.\n",1);
$itemsCount=count($feed);
if ($itemsCount==0) exitYoung("Info: feed from «{$url}» was empty, bye.\n");
vecho($opts['verbose'],"Info: got feed with {$itemsCount} announcement(s) from «{$url}».\n");
//file_put_contents(__DIR__.'/storage/dump-'.time(),print_r($feed,true));
date_default_timezone_set($conf['timezone']);
//date_default_timezone_set($conf['timezone']);
//$dfmt=datefmt_create('it',IntlDateFormatter::FULL,IntlDateFormatter::SHORT,$conf['timezone'],IntlDateFormatter::GREGORIAN,"eeee d MMMM '"._('alle')."' HH:mm");
$tsfp="{$conf['state_file_absolute_path']}.tmp";
if (!$opts['test'] && ($fh=@fopen($tsfp,'w'))===false) dieYoung("Error: could not open «{$tsfp}» in «write» mode.\n",1);
$itemsToPost=0;
$goodPostsCount=0;
$index=0;
//2024-10-27T22:01:28+01:00
foreach ($feed->channel->item as $item) {
$index++;
if (!isset($item->guid) || !isset($item->title) || !isset($item->link) || !isset($item->description) || !isset($item->pubDate)) {
fwrite(STDERR,"Warning: announcement #{$index} has unexpected format, skipping.\n");
foreach ($feed as $item) {
//print_r($item);
$now=time();
$postUrl="https://{$conf['feed_hostname']}/event/{$item['slug']}";
if (!array_key_exists($item['slug'],$refs)) {
$state='new';
} elseif ($item['updatedAt']!=$refs[$item['slug']]['updatedAt']) {
if ($item['start_datetime']>$now || (!is_null($item['end_datetime']) && $item['end_datetime']>$now))
$state='changed';
else
$state='old';
} else {
//print_r($item);
$now=time();
$guid=$item->guid->__toString();
//$slug=preg_replace('#^.*/(.*)$#','$1',$guid);
$pubdate=strtotime($item->pubDate->__toString());
(preg_match('#^\[(\d{4,}-\d{2}-\d{2})\] #',$item->title,$matches)===1) ? $evdate=$matches[1] : $evdate=false;
$file=null;
$imgalt='';
if (isset($item->enclosure[0]['url']) && isset($item->enclosure[0]['type']) && isset($item->enclosure[0]['length'])) {
$file=['url'=>$item->enclosure[0]['url']->__toString(), 'type'=>$item->enclosure[0]['type']->__toString(), 'length'=>$item->enclosure[0]['length']->__toString()];
if (preg_match('#<img [^>]*alt="([^"]+)"#',$item->description->__toString(),$matches)===1) $imgalt=trim($matches[1]);
}
if ($imgalt=='') $imgalt='Flyer dellevento';
//<h3>Raawwr Beats</h3><strong>Kassel - Werner-Hilpert-Straße 22</strong><br/><small>(samedi, 26 octobre 22:00)</small><br/><img alt="This is the alt-text" src="https://demo.gancio.org/media/fcb4ac7e55cb5a53a4008e7c49200dbd.jpg"/><p></p>
$buff=$item->description->__toString();
if ($buff=='') {
$ptext='';
} elseif (preg_match('#^\n?<h3>(.+)</h3><strong>(.+)</strong><br/><small>\((\w+)\W+(\d+)\W+(\w+)\W+(\d+:\d+)\)</small><br/>(.*)$#iuU',$buff,$matches)===1) {
//print_r($matches);
$matches[1]=hent($matches[1]);
$matches[2]=hent($matches[2]);
$ptext="{$matches[1]}\n\n".ucfirst($matches[3])." {$matches[4]} {$matches[5]} dalle {$matches[6]} presso {$matches[2]}\n\n".html2text($matches[7]);
if ($evdate!==false) $evdate.="T{$matches[6]}:00";
$state='old';
}
// $state='new';
if ($state=='old' && !$opts['test']) {
if ($opts['do-post'])
vecho($opts['verbose'],"Info: wont try to post status for {$state} announcement «{$postUrl}».\n");
else
vecho($opts['verbose'],"Info: wouldnt try to post status for {$state} announcement «{$postUrl}».\n");
} else {
$itemsToPost++;
$postHead="{$item['title']}\n\n";
if ($item['multidate']) {
$dfmt=datefmt_create($conf['posts_language'],0,0,$conf['timezone'],null,"eeee d MMMM '"._('alle')."' HH:mm");
$postHead.=_('Da').' '.datefmt_format($dfmt,$item['start_datetime']).' '._('a').' '.datefmt_format($dfmt,$item['end_datetime']);
} else {
$ptext=html2text($item->description);
$evdate=false;
}
//echo "evdate: {$evdate}\n";
$evdate=strtotime($evdate);
//echo "{$now}: ".date('c',$now)." (now)\n{$pubdate}: ".date('c',$pubdate)." (pubdate: {$item->pubDate})\n{$evdate}: ".date('c',$evdate)." (evdate)\n";
//exitYoung("Ciao\n");
$plink="\n\n".$item->link->__toString();
if (isset($item->category) && is_countable($item->category) && is_iterable($item->category) && $item->category->count()>0) {
$pcats=[];
foreach ($item->category as $val)
$pcats[]=hashtag($val->__toString());
$pcats="\n\n".implode(' ',$pcats);
} else {
$pcats='';
}
if ($opts['test']) {
vecho($opts['verbose'],"Info: considering announcement «{$guid}» as new because we are in «test mode»; processing.\n");
$state='new';
} elseif ($evdate===false) {
fwrite(STDERR,"Warning: could not identify the event start datetime in announcement «{$guid}»; skipping.\n");
$state='error';
} elseif ($evdate<$now) {
vecho($opts['verbose'],"Info: announcement «{$guid}» has a start datetime of ".date('c',$evdate).", which is before now, ".date('c',$now)."; skipping.\n");
$state='error';
} elseif (array_key_exists($guid,$refs)) {
if ($pubdate==$refs[$guid]['pubdate']) {
vecho($opts['verbose'],"Info: announcement «{$guid}» is not new and has not changed; skipping.\n");
$state='old';
} else {
vecho($opts['verbose'],"Info: announcement «{$guid}» is not new, but it has changed; processing.\n");
$state='changed';
$itemsToPost++;
$ptext="[MODIFICATO]\n\n{$ptext}";
$dfmt=datefmt_create($conf['posts_language'],0,0,$conf['timezone'],null,"eeee d MMMM '"._('dalle')."' HH:mm");
$postHead.=mb_ucfirst(datefmt_format($dfmt,$item['start_datetime']));
if (!is_null($item['end_datetime'])) {
$dfmt=datefmt_create($conf['posts_language'],0,0,$conf['timezone'],null,"HH:mm");
$postHead.=' '._('alle').' '.datefmt_format($dfmt,$item['end_datetime']);
}
} else {
vecho($opts['verbose'],"Info: announcement «{$guid}» is new; processing.\n");
$state='new';
$itemsToPost++;
}
$post="{$ptext}{$plink}{$pcats}";
$postHead.=', '._('presso')." {$item['place']['name']}, {$item['place']['address']}";
if (is_array($item['online_locations']) && count($item['online_locations'])>0) $postHead.='; '._('e anche online su ').implode(' - ',$item['online_locations']);
// if (isset($item['parentId']))// this probably means it's a recurring event, but i see no way to check *when* it is recurring
$postBody='';
if (!is_null($item['description']) && $item['description']!='' && $item['description']!='<p></p>') $postBody.=html2text($item['description']);
if ($postBody!='') $postBody="\n\n{$postBody}";
$postLink="\n\n{$postUrl}";
$postTags='';
if (isset($item['tags']) && is_array($item['tags']) && count($item['tags'])>0) {
$buff=[];
foreach ($item['tags'] as $val)
$buff[]=hashtag($val['tag']);
$postTags.=implode(' ',$buff);
if ($postTags!='') $postTags="\n\n{$postTags}";
}
$post="{$postHead}{$postBody}{$postLink}{$postTags}";
$postLen=postLength($post,$tldsregex['tlds']);
if ($postLen<=$conf['max_post_length'] && !$conf['always_link_gancio_post']) {
$plink='';
$post="{$ptext}{$pcats}";
if (!$conf['always_link_gancio_post'] && $postLen<=$conf['max_post_length']) {
$postLink='';
$post="{$postHead}{$postBody}{$postTags}";
$postLen=postLength($post,$tldsregex['tlds']);
}
if ($postLen>$conf['max_post_length']) {
$pcats='';
$post="{$ptext}{$plink}";
$postTags='';
$post="{$postHead}{$postBody}{$postLink}";
$postLen=postLength($post,$tldsregex['tlds']);
}
while ($postLen>$conf['max_post_length'] && $ptext!='') {
$ptext=preg_replace('#\S+\W*$#','',$ptext);
$post="{$ptext}[…]{$plink}{$pcats}";
while ($postLen>$conf['max_post_length'] && $postBody!='') {
$postBody=preg_replace('#\S+\W*$#','',$postBody);
$post="{$postHead}{$postBody}[…]{$postLink}{$postTags}";
$postLen=postLength($post,$tldsregex['tlds']);
}
//echo "--- #{$index}: {$guid} ---\n{$post}\n--- (length: ".postLength($post,$tldsregex['tlds']).") ---\n\n";
// echo "@@@ {$postUrl}: {$postLen} @@@\n{$post}\n---\n";
if ($postLen>$conf['max_post_length']) {
fwrite(STDERR,"Warning: could not shorten post for announcement «{$guid}» to make it fit into {$conf['max_post_length']} characters; wont post.\n");
} elseif ($state=='new' || $state=='changed') {
if ($opts['do-post']) {
$doPost=false;
$postData=[];
//print_r($file);
if (is_null($file)) {
vecho($opts['verbose'],"Info: {$state} announcement «{$guid}» has no attachment.\n");
$doPost=true;
} elseif ($file['length']>$conf['max_image_size']) {
fwrite(STDERR,"Warning: the size of attachment «{$file['ulr']}» is greater than image upload max size on «{$conf['fedi_hostname']}»; wont post status for {$state} announcement «{$guid}».\n");
fwrite(STDERR,"Warning: could not shrink post for {$state} announcement «{$postUrl}» into {$conf['max_post_length']} characters; wont try to post.\n");
} elseif (!$opts['do-post'] && !$opts['test']) {
vecho($opts['verbose'],"Info: would try to post status for {$state} announcement «{$postUrl}».\n");
if ($state=='new' || $state=='changed') $refs[$item['slug']]=['updatedAt'=>$item['updatedAt'], 'postedAt'=>time()];
$goodPostsCount++;
} else {
vecho($opts['verbose'],"Info: trying to post status for {$state} announcement «{$postUrl}».\n");
$doPost=false;
if (isset($item['media']) && count($item['media'])>0) {
vecho($opts['verbose'],"Info: {$state} announcement «{$postUrl}» has an attachment; processing.\n");
if ($item['media'][0]['size']>$conf['max_image_size']) {
fwrite(STDERR,"Warning: attachment size is greater than «{$conf['fedi_hostname']}» maximum image size; wont try to post.\n");
} else {
vecho($opts['verbose'],"Info: {$state} announcement «{$guid}» has an attachment; processing.\n");
$res=curl($file['url']);
$url="https://{$conf['feed_hostname']}/media/{$item['media'][0]['url']}";
$res=curl($url);
if ($res['content']===false) {
fwrite(STDERR,"Warning: could not connect to «{$file['url']}» (error: «{$res['error']}»); wont post status for {$state} announcement «{$guid}».\n");
fwrite(STDERR,"Warning: could not connect to «{$url}» to fetch attachment: «{$res['error']}»; wont try to post.\n");
} elseif ($res['httpcode']!='200') {
fwrite(STDERR,"Warning: «{$file['url']}» returned http code «{$res['httpcode']}»; wont post status for {$state} announcement «{$guid}».\n");
fwrite(STDERR,"Warning: could not fetch attachment «{$url}»: the server returned «{$res['httpcode']}»; wont try to post.\n");
} else {
// we don't use CURLStringFile because in php 7.3 it is not available
//$pd=['file'=>new CURLStringFile($res['content'],'file',$file['type']), 'description'=>'Flyer dellevento'];
$tfp=__DIR__.'/storage/'.basename($file['url']);
$tfp=__DIR__."/storage/{$item['media'][0]['url']}";
if (@file_put_contents($tfp,$res['content'])===false) {
fwrite(STDERR,"Warning: could not save «{$tfp}»; wont post status for {$state} announcement «{$guid}».\n");
fwrite(STDERR,"Warning: could not save attachment into «{$tfp}»; wont try to post.\n");
} else {
$pd=['file'=>curl_file_create($tfp,$file['type'],'file'), 'description'=>$imgalt];
$url="https://{$conf['fedi_hostname']}/api/v2/media";
$res=curl($url,'/api/v2/media',["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json'],$pd);
if ($res['content']===false) {
fwrite(STDERR,"Warning: could not connect to «{$url}» (error: «{$res['error']}»); wont post status for {$state} announcement «{$guid}».\n");
} elseif (is_null($res['content']=@json_decode($res['content'],true))) {
fwrite(STDERR,"Warning: «{$url}» did not return valid JSON; wont post status for {$state} announcement «{$guid}».\n");
} elseif ($res['httpcode']!='200' && $res['httpcode']!='202') {
(isset($res['content']['error'])) ? $buff=" (error: «{$res['content']['error']}»)" : $buff='';
fwrite(STDERR,"Warning: «{$url}» returned http code «{$res['httpcode']}»{$buff}; wont post status for {$state} announcement «{$guid}».\n");
} elseif (!isset($res['content']['id'])) {
fwrite(STDERR,"Warning: no «id» in JSON from «{$url}»; file has not been uploaded successfully; wont post status for {$state} announcement «{$guid}».\n");
if (($type=mime_content_type($tfp))===false) {
fwrite(STDERR,"Warning: could not identify the MIME type of «{$tfp}»; wont try to post.\n");
} else {
$id=$res['content']['id'];
if ($res['httpcode']=='202') {
$id=null;
$i=1;
while ($res['httpcode']!='200' && $i<5) {
sleep(2);
$url="https://{$conf['fedi_hostname']}/api/v1/media/{$res['id']}";
$res=curl($url,'/api/v1/media',["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json']);
if ($res['content']!==false && $res['httpcode']=='200' && !is_null($res['content']=@json_decode($res,true)) && isset($res['content']['id'])) $id=$res['content']['id'];
$i++;
$postData=['file'=>curl_file_create($tfp,$type,'file'), 'description'=>$item['media'][0]['name']];
$url="https://{$conf['fedi_hostname']}/api/v2/media";
$res=curl($url,'/api/v2/media',["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json'],$postData);
unset($postData);
if ($res['content']===false) {
fwrite(STDERR,"Warning: could not connect to «{$url}»: «{$res['error']}»; wont try to post.\n");
} elseif (is_null($res['content']=@json_decode($res['content'],true))) {
fwrite(STDERR,"Warning: «{$url}» did not return valid JSON; wont try to post.\n");
} elseif ($res['httpcode']!='200' && $res['httpcode']!='202') {
(isset($res['content']['error'])) ? $buff=" (error: «{$res['content']['error']}»)" : $buff='';
fwrite(STDERR,"Warning: «{$url}» returned http code «{$res['httpcode']}»{$buff}; wont try to post.\n");
} elseif (!isset($res['content']['id'])) {
fwrite(STDERR,"Warning: no «id» in JSON from «{$url}»; file has not been uploaded successfully; wont try to post.\n");
} else {
$id=$res['content']['id'];
if ($res['httpcode']=='202') {
$id=null;
$i=1;
while ($res['httpcode']!='200' && $i<5) {
sleep(2);
$url="https://{$conf['fedi_hostname']}/api/v1/media/{$res['id']}";
$res=curl($url,'/api/v1/media',["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json']);
if ($res['content']!==false && $res['httpcode']=='200' && !is_null($res['content']=@json_decode($res,true)) && isset($res['content']['id'])) $id=$res['content']['id'];
$i++;
}
}
if (!is_null($id)) {
vecho($opts['verbose'],"Info: successfully posted attachment for {$state} announcement «{$postUrl}».\n");
$postData['media_ids[]']=$id;
$doPost=true;
} else {
fwrite(STDERR,"Warning: server took too long to process file, or could not; wont try to post.\n");
}
}
if (!is_null($id)) {
vecho($opts['verbose'],"Info: successfully posted attachment for {$state} announcement «{$guid}».\n");
$postData['media_ids[]']=$id;
$doPost=true;
} else {
fwrite(STDERR,"Warning: server took too long to process file, or could not; wont post status for {$state} announcement «{$guid}».\n");
}
}
if (@unlink($tfp)===false) fwrite(STDERR,"Warning: could not delete «{$tfp}».\n");
if (@unlink($tfp)===false) fwrite(STDERR,"Warning: could not delete temporary attachment file «{$tfp}».\n");
}
}
}
if ($doPost) {
$postData['status']=$post;
$postData['visibility']=$conf['posts_visibility'];
$postData['language']=$conf['posts_language'];
$url="https://{$conf['fedi_hostname']}/api/v1/statuses";
$headers=["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json', 'Idempotency-Key: '.md5(implode('-',$postData).time())];
$res=curl($url,'/api/v1/statuses',$headers,$postData);
if ($res['content']===false) {
fwrite(STDERR,"Warning: could not connect to «{$url}» (error: «{$res['error']}»); could not post status for {$state} announcement «{$guid}».\n");
} elseif (is_null($res['content']=@json_decode($res['content'],true))) {
fwrite(STDERR,"Warning: «{$url}» did not return good JSON; could not post status for {$state} announcement «{$guid}».\n");
} elseif ($res['httpcode']!='200') {
(isset($res['content']['error'])) ? $buff=" (error: «{$res['content']['error']}»)" : $buff='';
fwrite(STDERR,"Warning: «{$url}» returned http code «{$res['httpcode']}»{$buff}; could not post status for {$state} announcement «{$guid}».\n");
} elseif (!isset($res['content']['url'])) {
fwrite(STDERR,"Warning: JSON from «{$url}» had unexpected format; could not post status for {$state} announcement «{$guid}».\n");
} else {
vecho($opts['verbose'],"Info: successfully posted status for {$state} announcement «{$guid}» (URL: «{$res['content']['url']}»).\n");
//print_r($res['content']);
$refs[$guid]=['postdate'=>time(), 'pubdate'=>$pubdate];
$goodPostsCount++;
}
}
} else {
vecho($opts['verbose'],"Info: would have tried to post status for {$state} announcement «{$guid}».\n");
if ($state=='new' || $state=='changed') $refs[$guid]=['postdate'=>time(), 'pubdate'=>$pubdate];
$goodPostsCount++;
vecho($opts['verbose'],"Info: {$state} announcement «{$postUrl}» has no attachment.\n");
$doPost=true;
}
if ($doPost) {
$postData['status']=$post;
$postData['visibility']=$conf['posts_visibility'];
$postData['language']=$conf['posts_language'];
$url="https://{$conf['fedi_hostname']}/api/v1/statuses";
$headers=["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json', 'Idempotency-Key: '.md5(implode('-',$postData).time())];
$res=curl($url,'/api/v1/statuses',$headers,$postData);
if ($res['content']===false) {
fwrite(STDERR,"Warning: could not connect to «{$url}»: «{$res['error']}»; could not post status for {$state} announcement «{$postUrl}».\n");
} elseif (is_null($res['content']=@json_decode($res['content'],true))) {
fwrite(STDERR,"Warning: «{$url}» did not return good JSON; could not post status for {$state} announcement «{$postUrl}».\n");
} elseif ($res['httpcode']!='200') {
(isset($res['content']['error'])) ? $buff=" (error: «{$res['content']['error']}»)" : $buff='';
fwrite(STDERR,"Warning: «{$url}» returned http code «{$res['httpcode']}»{$buff}; could not post status for {$state} announcement «{$postUrl}».\n");
} elseif (!isset($res['content']['url'])) {
fwrite(STDERR,"Warning: JSON from «{$url}» had unexpected format; could not post status for {$state} announcement «{$postUrl}».\n");
} else {
vecho($opts['verbose'],"Info: successfully posted status for {$state} announcement «{$postUrl}» (post URL: «{$res['content']['url']}»).\n");
//print_r($res['content']);
$refs[$item['slug']]=['updatedAt'=>$item['updatedAt'], 'postedAt'=>time()];
$goodPostsCount++;
}
}
}
if (!$opts['test'] && array_key_exists($guid,$refs)) fwrite($fh,"{$refs[$guid]['postdate']}\t{$refs[$guid]['pubdate']}\t{$guid}\n");
}
if ($opts['test']) break;// to test a single post
if (!$opts['test']) {
if (array_key_exists($item['slug'],$refs)) fwrite($fh,"{$item['slug']}\t{$refs[$item['slug']]['updatedAt']}\t{$refs[$item['slug']]['postedAt']}\n");
} else {
break;
}
}
if (!$opts['test']) {
fclose($fh);
rename($tsfp,$conf['state_file_absolute_path']);
}
if (!$opts['test']) {
if ($opts['do-post'])
vecho($opts['verbose'],"Info: succesfully posted {$goodPostsCount} of {$itemsToPost} new or edited announcement(s) (of {$itemsCount} total announcements in the feed).\n");
vecho($opts['verbose'],"Info: succesfully posted {$goodPostsCount} of {$itemsToPost} statuses for new or changed announcement(s) (of {$itemsCount} total announcement(s) in the feed).\n");
else
vecho($opts['verbose'],"Info: would have tried to post {$itemsToPost} new or changed announcement(s) of {$itemsCount} total announcements in the feed.\n");
vecho($opts['verbose'],"Info: would have tried to post {$itemsToPost} statuses for new or changed announcement(s) of {$itemsCount} total announcement(s) in the feed.\n");
} elseif ($goodPostsCount==1) {
vecho($opts['verbose'],"Info: successfully posted the first of {$itemsCount} total announcements in the feed ({$itemsToPost} are new or changed).\n");
vecho($opts['verbose'],"Info: successfully posted status for the first of {$itemsCount} total announcements in the feed.\n");
} else {
vecho($opts['verbose'],"Info: failed to post the first of {$itemsCount} total announcements in the feed ({$itemsToPost} are new or changed).\n");
vecho($opts['verbose'],"Info: failed to post status for the first of {$itemsCount} total announcements in the feed.\n");
}
exit(0);