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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
$SNAME='GancioFF'; $SNAME='GancioF2F';
$ENAME=strtolower($SNAME); $ENAME=strtolower($SNAME);
$SVERS='0.4.4'; $SVERS='0.5';
require __DIR__.'/lib/ckmkeys.php';
require __DIR__.'/lib/gettlds.php'; require __DIR__.'/lib/gettlds.php';
require __DIR__.'/lib/mastodon-postLength.php'; require __DIR__.'/lib/mastodon-postLength.php';
require __DIR__.'/lib/mb_ucfirst.php';
require __DIR__.'/lib/hashtag.php'; require __DIR__.'/lib/hashtag.php';
require __DIR__.'/lib/html2text.php'; require __DIR__.'/lib/html2text.php';
require __DIR__.'/lib/curl.php'; require __DIR__.'/lib/curl.php';
@ -33,16 +35,17 @@ $help=
[[[ DESCRIPTION ]]] [[[ DESCRIPTION ]]]
This is {$SNAME} v{$SVERS}, a CLI PHP script that can be used to periodically This is {$SNAME} v{$SVERS} («GancioFeed2Fedi»), a CLI PHP script that can
fetch the RSS feed from an instance of Gancio (https://gancio.org) and post be used to periodically fetch the JSON feed from an instance of Gancio
its new entries the events announcements on the fediverse through a (https://gancio.org) and post its new or changed events announcements on the
Mastodon account, recording into a state file a reference to each already Fediverse through a Mastodon account, recording into a state file a reference
posted announcement in order to post only new or changed ones on each run. 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 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: 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, 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 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 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, {$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 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 --- --- Example configuration file ---
# Lines beginnig with a «#» and empty lines will be ignored # 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: # 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 # «fedi_hostname» is required to specify the hostname of the Mastodon instance
# you want to post to. For example: # 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 # 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). # than one year and discard them, to avoid the state file to grow too much).
# For example: # 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 # «timezone» is required to specify the timezone of the Gancio instance, in
# order for {$SNAME} to calculate the correct datetimes. You can list the # order for {$SNAME} to calculate the correct datetimes. You can list the
# supported timezones using option «-T» or «--timezones» (see the related # 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 timezone = Europe/Rome
# «posts_language» is required to specify the ISO 639-1 code for the language # «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; $confFP=null;
$conf=[ $conf=[
'feed_url'=>['required'=>true, 'default'=>null], 'feed_hostname'=>['required'=>true, 'default'=>null],
'fedi_hostname'=>['required'=>true, 'default'=>null], 'fedi_hostname'=>['required'=>true, 'default'=>null],
'fedi_token'=>['required'=>true, 'default'=>null], 'fedi_token'=>['required'=>true, 'default'=>null],
'state_file_absolute_path'=>['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; $i=0;
$buff=file($conf['state_file_absolute_path'],FILE_IGNORE_NEW_LINES); $buff=file($conf['state_file_absolute_path'],FILE_IGNORE_NEW_LINES);
foreach ($buff as $key=>$val) { foreach ($buff as $key=>$val) {
if (preg_match('#^(\d+)\t(\d+)\t(\S+)$#',$val,$matches)===1) { if (preg_match('#^(\S+)\t(\S+)\t(\d+)$#',$val,$matches)===1) {// todo: refine the pattern
if ($matches[1]+0>=$graceLine) if ($matches[3]+0>=$graceLine)
$refs[$matches[3]]=['postdate'=>$matches[1], 'pubdate'=>$matches[2]]; $refs[$matches[1]]=['updatedAt'=>$matches[2], 'postedAt'=>$matches[3]];
else else
$i++; $i++;
} else { } else {
@ -336,232 +339,224 @@ if (file_exists($conf['state_file_absolute_path'])) {
unset($buff); unset($buff);
$fh=fopen($conf['state_file_absolute_path'],'w'); $fh=fopen($conf['state_file_absolute_path'],'w');
foreach ($refs as $key=>$val) 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); 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"); 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']) { } 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 { } else {
vecho($opts['verbose'],"Info: state file «{$conf['state_file_absolute_path']}» was not found.\n"); vecho($opts['verbose'],"Info: state file «{$conf['state_file_absolute_path']}» was not found.\n");
} }
//print_r($refs);die(); //print_r($refs);die();
if (is_null($opts['do-post']) || $opts['test']) $opts['do-post']=true; 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"); $url="https://{$conf['feed_hostname']}/feed/json?show_recurrent=true";
$feed=curl($conf['feed_url'],null,['Accept: application/xml']); vecho($opts['verbose'],"Info: trying to fetch JSON feed from «{$url}».\n");
if ($feed['content']===false) dieYoung("Error: could not connect to «{$conf['feed_url']}» (error: «{$feed['error']}»).\n",1); $feed=curl($url,null,['Accept: application/json']);
if ($feed['httpcode']!='200') dieYoung("Error: «{$conf['feed_url']} returned http code «{$res['httpcode']}».\n",1); if ($feed['content']===false) dieYoung("Error: could not connect to «{$url}» (error: «{$feed['error']}»).\n",1);
$feed=@simplexml_load_string($feed['content'],null,LIBXML_NOCDATA); $feed['content']=@json_decode($feed['content'],true);
if ($feed===false) dieYoung("Error: got no valid XML from «{$conf['feed_url']}».\n",1); (!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); //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); $buff=['id', 'title', 'slug', 'description', 'multidate', 'start_datetime', 'end_datetime', 'media', 'online_locations', 'updatedAt', 'tags', 'place'];
$itemsCount=$feed->channel->item->count(); foreach ($feed as $item)
if ($itemsCount==0) exitYoung("Info: feed from «{$conf['feed_url']}» was empty, bye.\n"); 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)); //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"; $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); if (!$opts['test'] && ($fh=@fopen($tsfp,'w'))===false) dieYoung("Error: could not open «{$tsfp}» in «write» mode.\n",1);
$itemsToPost=0; $itemsToPost=0;
$goodPostsCount=0; $goodPostsCount=0;
$index=0; foreach ($feed as $item) {
//2024-10-27T22:01:28+01:00 //print_r($item);
foreach ($feed->channel->item as $item) { $now=time();
$index++; $postUrl="https://{$conf['feed_hostname']}/event/{$item['slug']}";
if (!isset($item->guid) || !isset($item->title) || !isset($item->link) || !isset($item->description) || !isset($item->pubDate)) { if (!array_key_exists($item['slug'],$refs)) {
fwrite(STDERR,"Warning: announcement #{$index} has unexpected format, skipping.\n"); $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 { } else {
//print_r($item); $state='old';
$now=time(); }
$guid=$item->guid->__toString(); // $state='new';
//$slug=preg_replace('#^.*/(.*)$#','$1',$guid); if ($state=='old' && !$opts['test']) {
$pubdate=strtotime($item->pubDate->__toString()); if ($opts['do-post'])
(preg_match('#^\[(\d{4,}-\d{2}-\d{2})\] #',$item->title,$matches)===1) ? $evdate=$matches[1] : $evdate=false; vecho($opts['verbose'],"Info: wont try to post status for {$state} announcement «{$postUrl}».\n");
$file=null; else
$imgalt=''; vecho($opts['verbose'],"Info: wouldnt try to post status for {$state} announcement «{$postUrl}».\n");
if (isset($item->enclosure[0]['url']) && isset($item->enclosure[0]['type']) && isset($item->enclosure[0]['length'])) { } else {
$file=['url'=>$item->enclosure[0]['url']->__toString(), 'type'=>$item->enclosure[0]['type']->__toString(), 'length'=>$item->enclosure[0]['length']->__toString()]; $itemsToPost++;
if (preg_match('#<img [^>]*alt="([^"]+)"#',$item->description->__toString(),$matches)===1) $imgalt=trim($matches[1]); $postHead="{$item['title']}\n\n";
} if ($item['multidate']) {
if ($imgalt=='') $imgalt='Flyer dellevento'; $dfmt=datefmt_create($conf['posts_language'],0,0,$conf['timezone'],null,"eeee d MMMM '"._('alle')."' HH:mm");
//<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> $postHead.=_('Da').' '.datefmt_format($dfmt,$item['start_datetime']).' '._('a').' '.datefmt_format($dfmt,$item['end_datetime']);
$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";
} else { } else {
$ptext=html2text($item->description); $dfmt=datefmt_create($conf['posts_language'],0,0,$conf['timezone'],null,"eeee d MMMM '"._('dalle')."' HH:mm");
$evdate=false; $postHead.=mb_ucfirst(datefmt_format($dfmt,$item['start_datetime']));
} if (!is_null($item['end_datetime'])) {
//echo "evdate: {$evdate}\n"; $dfmt=datefmt_create($conf['posts_language'],0,0,$conf['timezone'],null,"HH:mm");
$evdate=strtotime($evdate); $postHead.=' '._('alle').' '.datefmt_format($dfmt,$item['end_datetime']);
//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}";
} }
} 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']); $postLen=postLength($post,$tldsregex['tlds']);
if ($postLen<=$conf['max_post_length'] && !$conf['always_link_gancio_post']) { if (!$conf['always_link_gancio_post'] && $postLen<=$conf['max_post_length']) {
$plink=''; $postLink='';
$post="{$ptext}{$pcats}"; $post="{$postHead}{$postBody}{$postTags}";
$postLen=postLength($post,$tldsregex['tlds']); $postLen=postLength($post,$tldsregex['tlds']);
} }
if ($postLen>$conf['max_post_length']) { if ($postLen>$conf['max_post_length']) {
$pcats=''; $postTags='';
$post="{$ptext}{$plink}"; $post="{$postHead}{$postBody}{$postLink}";
$postLen=postLength($post,$tldsregex['tlds']); $postLen=postLength($post,$tldsregex['tlds']);
} }
while ($postLen>$conf['max_post_length'] && $ptext!='') { while ($postLen>$conf['max_post_length'] && $postBody!='') {
$ptext=preg_replace('#\S+\W*$#','',$ptext); $postBody=preg_replace('#\S+\W*$#','',$postBody);
$post="{$ptext}[…]{$plink}{$pcats}"; $post="{$postHead}{$postBody}[…]{$postLink}{$postTags}";
$postLen=postLength($post,$tldsregex['tlds']); $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']) { 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"); fwrite(STDERR,"Warning: could not shrink post for {$state} announcement «{$postUrl}» into {$conf['max_post_length']} characters; wont try to post.\n");
} elseif ($state=='new' || $state=='changed') { } elseif (!$opts['do-post'] && !$opts['test']) {
if ($opts['do-post']) { vecho($opts['verbose'],"Info: would try to post status for {$state} announcement «{$postUrl}».\n");
$doPost=false; if ($state=='new' || $state=='changed') $refs[$item['slug']]=['updatedAt'=>$item['updatedAt'], 'postedAt'=>time()];
$postData=[]; $goodPostsCount++;
//print_r($file); } else {
if (is_null($file)) { vecho($opts['verbose'],"Info: trying to post status for {$state} announcement «{$postUrl}».\n");
vecho($opts['verbose'],"Info: {$state} announcement «{$guid}» has no attachment.\n"); $doPost=false;
$doPost=true; if (isset($item['media']) && count($item['media'])>0) {
} elseif ($file['length']>$conf['max_image_size']) { vecho($opts['verbose'],"Info: {$state} announcement «{$postUrl}» has an attachment; processing.\n");
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"); 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 { } else {
vecho($opts['verbose'],"Info: {$state} announcement «{$guid}» has an attachment; processing.\n"); $url="https://{$conf['feed_hostname']}/media/{$item['media'][0]['url']}";
$res=curl($file['url']); $res=curl($url);
if ($res['content']===false) { 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') { } 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 { } else {
// we don't use CURLStringFile because in php 7.3 it is not available // 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']; //$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) { 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 { } else {
$pd=['file'=>curl_file_create($tfp,$file['type'],'file'), 'description'=>$imgalt]; if (($type=mime_content_type($tfp))===false) {
$url="https://{$conf['fedi_hostname']}/api/v2/media"; fwrite(STDERR,"Warning: could not identify the MIME type of «{$tfp}»; wont try to post.\n");
$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");
} else { } else {
$id=$res['content']['id']; $postData=['file'=>curl_file_create($tfp,$type,'file'), 'description'=>$item['media'][0]['name']];
if ($res['httpcode']=='202') { $url="https://{$conf['fedi_hostname']}/api/v2/media";
$id=null; $res=curl($url,'/api/v2/media',["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json'],$postData);
$i=1; unset($postData);
while ($res['httpcode']!='200' && $i<5) { if ($res['content']===false) {
sleep(2); fwrite(STDERR,"Warning: could not connect to «{$url}»: «{$res['error']}»; wont try to post.\n");
$url="https://{$conf['fedi_hostname']}/api/v1/media/{$res['id']}"; } elseif (is_null($res['content']=@json_decode($res['content'],true))) {
$res=curl($url,'/api/v1/media',["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json']); fwrite(STDERR,"Warning: «{$url}» did not return valid JSON; wont try to post.\n");
if ($res['content']!==false && $res['httpcode']=='200' && !is_null($res['content']=@json_decode($res,true)) && isset($res['content']['id'])) $id=$res['content']['id']; } elseif ($res['httpcode']!='200' && $res['httpcode']!='202') {
$i++; (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 { } else {
vecho($opts['verbose'],"Info: would have tried to post status for {$state} announcement «{$guid}».\n"); vecho($opts['verbose'],"Info: {$state} announcement «{$postUrl}» has no attachment.\n");
if ($state=='new' || $state=='changed') $refs[$guid]=['postdate'=>time(), 'pubdate'=>$pubdate]; $doPost=true;
$goodPostsCount++; }
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']) { if (!$opts['test']) {
fclose($fh); fclose($fh);
rename($tsfp,$conf['state_file_absolute_path']); rename($tsfp,$conf['state_file_absolute_path']);
}
if (!$opts['test']) {
if ($opts['do-post']) 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 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) { } 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 { } 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); exit(0);