499 lines
24 KiB
Text
499 lines
24 KiB
Text
|
#!/usr/bin/php
|
|||
|
<?php
|
|||
|
|
|||
|
/*
|
|||
|
This program is free software: you can redistribute it and/or modify
|
|||
|
it under the terms of the GNU General Public License as published by
|
|||
|
the Free Software Foundation, either version 3 of the License, or
|
|||
|
(at your option) any later version.
|
|||
|
|
|||
|
This program is distributed in the hope that it will be useful,
|
|||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
GNU General Public License for more details.
|
|||
|
|
|||
|
You should have received a copy of the GNU General Public License
|
|||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
*/
|
|||
|
|
|||
|
$SNAME='GancioFF';
|
|||
|
$ENAME=strtolower($SNAME);
|
|||
|
$SVERS='0.1';
|
|||
|
|
|||
|
require __DIR__.'/lib/gettlds.php';
|
|||
|
$tldsregex=gettlds(__DIR__.'/storage/tlds.txt',true);
|
|||
|
require __DIR__.'/lib/mastodon-postLength.php';
|
|||
|
require __DIR__.'/lib/hashtag.php';
|
|||
|
require __DIR__.'/lib/html2text.php';
|
|||
|
require __DIR__.'/lib/curl.php';
|
|||
|
|
|||
|
$help=
|
|||
|
"[[[ SYNOPSIS ]]]
|
|||
|
|
|||
|
{$ENAME} [options] <configuration file>
|
|||
|
|
|||
|
[[[ 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 – on the fediverse through a Mastodon account,
|
|||
|
keeping track of already posted events’ GUIDs (Global Unique IDentifiers) in
|
|||
|
order to post only the new 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
|
|||
|
this case {$SNAME} is a quite light alternative, moving from the server
|
|||
|
running Gancio to the one running Mastodon the burden of posting each event to
|
|||
|
all the instances that host at least one follower, and of sending them the
|
|||
|
image a Gancio user can and almost always do attach to each event, because
|
|||
|
{$SNAME} will fetch it only once and attach it to the post for the event.
|
|||
|
{$SNAME} is meant to be periodically run, every half an hour or so, by a cron
|
|||
|
job, or systemd timer, or the likes (you can find a sample «{$ENAME}.timer»
|
|||
|
and a commented sample «{$ENAME}.service» in the «systemd» directory).
|
|||
|
In order to work, {$SNAME} needs a configuration file to be passed to it as
|
|||
|
an argument on the command line.
|
|||
|
|
|||
|
[[[ CONFIGURATION FILE ]]]
|
|||
|
|
|||
|
The configuration file needs to be like this:
|
|||
|
|
|||
|
--- 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;
|
|||
|
# for example:
|
|||
|
feed_url = https://gancio.some.domain/feed/rss?show_recurrent=true
|
|||
|
|
|||
|
# «fedi_hostname» is required to specify the hostname of the Mastodon instance
|
|||
|
# you want to post to; for example:
|
|||
|
fedi_hostname = mastodon.another.domain
|
|||
|
|
|||
|
# «fedi_token» is required to specify an «app token» to access the account
|
|||
|
# that you want to use on the instance defined by «fedi_hostname». On Mastodon
|
|||
|
# default web frontend you can get such a token under «Preferences» ->
|
|||
|
# «Development», by clicking on the «New application» button; the new
|
|||
|
# application should have at least the «write:statuses» privilege; when you’ll
|
|||
|
# be done setting it up, it will be listed under «Your applications», and by
|
|||
|
# clicking on its name you’ll be able to copy «Your access token» and paste it
|
|||
|
# here. For example:
|
|||
|
fedi_token = w6oQ_Ot2LSAm_Q31hrvp0asfl22ip3O4ipYq1kV1ceY
|
|||
|
|
|||
|
# «state_file_absolute_path» is required to specify the absolute path of the
|
|||
|
# state file where {$SNAME} will store the GUIDs of already posted events and
|
|||
|
# the timestamps of the moments when it posted them (on each run, {$SNAME}
|
|||
|
# will check for entries older than one year and discard them, in order to
|
|||
|
# avoid the state file to grow too much). For example:
|
|||
|
state_file_absolute_path = /var/local/cache/gancio.some.domain.feed.state
|
|||
|
|
|||
|
# «posts_language» is required to specify the ISO 639-1 code for the language
|
|||
|
# of posts (see https://www.loc.gov/standards/iso639-2/php/code_list.php for
|
|||
|
# a complete list); for example:
|
|||
|
posts_language = it
|
|||
|
|
|||
|
# «posts_visibility» is optional and lets you override the default “public”
|
|||
|
# visibility of posts; it can be set to «public» (posts will be visible in the
|
|||
|
# «Local» and «Federated» timelines, and any user will be able to boost them),
|
|||
|
# «unlisted» (posts will be visible only in the «Home» timeline of followers
|
|||
|
# and on the profile of the Mastodon account in use, not in the «Local» or
|
|||
|
# «Federated» timelines, but any user will still be able to boost them),
|
|||
|
# «private» (AKA «followers only»: posts will be visible only by followers and
|
|||
|
# won’t be boostable by anyone), and «direct» (since {$SNAME} posts won’t ever
|
|||
|
# explicitly mention any account, posts with this visibility will be visible
|
|||
|
# only from the Mastodon account in use»). For example:
|
|||
|
post_visibility = unlisted
|
|||
|
|
|||
|
# «max_post_length» is optional and lets you override the automatically
|
|||
|
# detected maximum length that a post can have on the instance specified with
|
|||
|
# «fedi_hostname»; it can be used for testing purposes or just to keep the
|
|||
|
# posts shorter than they would be otherwise; for example:
|
|||
|
max_post_length = 840
|
|||
|
--- End of example configuration file ---
|
|||
|
|
|||
|
[[[ OPTIONS ]]]
|
|||
|
|
|||
|
-h / --help
|
|||
|
Show this help text and exit.
|
|||
|
-p / --do-post <y|n>
|
|||
|
Setting this option to «n» («no») will make {$SNAME} skip posting. Note that
|
|||
|
even in this case it will save into the state file the GUIDs of new events
|
|||
|
it may find in the feed, so it won’t post them even on the subsequent runs.
|
|||
|
You may want to set this option to «n» on the first run on a given feed, i.e.
|
|||
|
when the state file doesn’t exist yet and all events in the feed will be
|
|||
|
considered new, to avoid flooding the timeline of the given Mastodon
|
|||
|
instance. That’s why when the state file doesn’t exist yet, {$SNAME} refuses
|
|||
|
to run unless you explicitly set this option to «n» («no») or «y» («yes»).
|
|||
|
When the state file exists, this option defaults to «y» («yes»).
|
|||
|
-v / --verbose
|
|||
|
Show some more messages about what the script is doing.
|
|||
|
--
|
|||
|
Treat every possible subsequent argument as non-options. Useful only in the
|
|||
|
very improbable case your config file is named «--help» or as another option.
|
|||
|
|
|||
|
[[[ EXIT VALUES ]]]
|
|||
|
|
|||
|
0: regular run
|
|||
|
1: some error occurred
|
|||
|
|
|||
|
[[[ DISCLAIMER AND LICENSE ]]]
|
|||
|
|
|||
|
This program comes with ABSOLUTELY NO WARRANTY; for details see the source.
|
|||
|
This is free software, and you are welcome to redistribute it under certain
|
|||
|
conditions; see <http://www.gnu.org/licenses/> for details.\n";
|
|||
|
|
|||
|
$confFP=null;
|
|||
|
|
|||
|
$conf=[
|
|||
|
'feed_url'=>['required'=>true, 'default'=>null],
|
|||
|
'fedi_hostname'=>['required'=>true, 'default'=>null],
|
|||
|
'fedi_token'=>['required'=>true, 'default'=>null],
|
|||
|
'state_file_absolute_path'=>['required'=>true, 'default'=>null],
|
|||
|
'posts_language'=>['required'=>true, 'default'=>null],
|
|||
|
'posts_visibility'=>['required'=>false, 'default'=>'public'],
|
|||
|
'max_post_length'=>['required'=>false, 'default'=>null]
|
|||
|
];
|
|||
|
|
|||
|
$opts=[
|
|||
|
'do-post'=>null,
|
|||
|
'verbose'=>false,
|
|||
|
'update-language-codes'=>false
|
|||
|
];
|
|||
|
|
|||
|
$canBeOpt=true;
|
|||
|
for ($i=1; $i<$argc; $i++) {
|
|||
|
if ($canBeOpt && $argv[$i][0]=='-') {
|
|||
|
if ($argv[$i]=='--') {
|
|||
|
$canBeOpt=false;
|
|||
|
} elseif ($argv[$i]=='-h' || $argv[$i]=='--help') {
|
|||
|
echo $help;
|
|||
|
exit(0);
|
|||
|
} elseif ($argv[$i]=='-p' || $argv[$i]=='--do-post') {
|
|||
|
if ($i+1>=$argc) dieYoung("Error: option «{$argv[$i]}» requires an argument; use «-h» or «--help» to display help.\n",1);
|
|||
|
if ($argv[$i+1]=='y')
|
|||
|
$opts['do-post']=true;
|
|||
|
elseif ($argv[$i+1]=='n')
|
|||
|
$opts['do-post']=false;
|
|||
|
else
|
|||
|
dieYoung("Error: option «{$argv[$i]}» requires an argument of «y» for «yes» or «n» for «no»; use «-h» or «--help» to display help.\n",1);
|
|||
|
$i++;
|
|||
|
} elseif ($argv[$i]=='-v' || $argv[$i]=='--verbose') {
|
|||
|
$opts['verbose']=true;
|
|||
|
} elseif ($argv[$i]=='-u' || $argv[$i]=='--update-language-codes') {
|
|||
|
$opts['update-language-codes']=true;
|
|||
|
} elseif ($argv[$i]=='--make-readme') {
|
|||
|
file_put_contents(__DIR__.'/README.md',"```text\n{$help}```\n");
|
|||
|
exit(0);
|
|||
|
} else {
|
|||
|
dieYoung("Error: «{$argv[$i]}» is not a known option; use «-h» or «--help» to display help.\n",1);
|
|||
|
}
|
|||
|
} elseif (is_null($confFP)) {
|
|||
|
$confFP=$argv[$i];
|
|||
|
} else {
|
|||
|
dieYoung("Error: could not interpret «{$argv[$i]}» (configuration file has already been set to «{$confFP}»); use «-h» or «--help» to display help.\n",1);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$langsFP=__DIR__.'/lib/iso-639-1-langcodes.txt';
|
|||
|
if ($opts['update-language-codes']) {
|
|||
|
$langCodes=[];
|
|||
|
$url='https://www.loc.gov/standards/iso639-2/ISO-639-2_8859-1.txt';
|
|||
|
$res=curl($url);
|
|||
|
if ($res['content']===false) dieYoung("Error: could not connect to «{$url}».\n",1);
|
|||
|
if ($res['httpcode']!='200') dieYoung("Error: got http code «{$res['httpcode']}» from «{$url}».\n",1);
|
|||
|
$res=explode("\r\n",$res['content']);
|
|||
|
// alb|sqi|sq|Albanian|albanais
|
|||
|
// tup|||Tupi languages|tupi, langues
|
|||
|
foreach ($res as $val)
|
|||
|
if (preg_match('#^[a-z]{3}\|([a-z]{3})?\|([a-z]{2})\|.+\|.+$#',$val,$matches)===1)
|
|||
|
$langCodes[]=$matches[2];
|
|||
|
$count=count($langCodes);
|
|||
|
if (@file_put_contents($langsFP,implode("\n",$langCodes)."\n")===false) dieYoung("Error: could not save the {$count} ISO 639-1 language code(s) i got from «{$url}» into «{$langsFP}».\n",1);
|
|||
|
echo "Info: successfully saved the {$count} ISO 639-1 language code(s) i got from «{$url}» into «{$langsFP}».\n";
|
|||
|
exit(0);
|
|||
|
}
|
|||
|
if (($langs=@file($langsFP,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES))===false) dieYoung("Error: could not load ISO 639-1 language codes from «{$langsFP}».\n",1);
|
|||
|
|
|||
|
if (is_null($confFP)) dieYoung("Error: you have not specified a configuration file; use «-h» or «--help» to display help.\n",1);
|
|||
|
echo "Info: trying to load configuration file «{$confFP}» from directory «".getcwd()."».\n";
|
|||
|
if (!file_exists($confFP)) dieYoung("Error: «{$confFP}» does not exist.\n",1);
|
|||
|
if (!is_file($confFP)) dieYoung("Error: «{$confFP}» is not a file.\n",1);
|
|||
|
if (!is_readable($confFP)) dieYoung("Error: «{$confFP}» is not readable.\n",1);
|
|||
|
getConf($conf,$confFP);
|
|||
|
if (preg_match('#^/.*$#',$conf['state_file_absolute_path'])!==1) dieYoung("Error: in configuration file: «state_file_absolute_path» must be an absolute path.\n",1);
|
|||
|
if (!in_array($conf['posts_language'],$langs)) dieYoung("Error: in configuration file: «posts_language»: «{$conf['posts_language']}» is not a known language code.\n",1);
|
|||
|
if (!in_array($conf['posts_visibility'],['public', 'unlisted', 'private', 'direct'])) dieYoung("Error: in configuration file: «posts_visibility» must be one of «public», «unlisted», «private» or «direct».\n",1);
|
|||
|
if (!is_null($conf['max_post_length'])) {
|
|||
|
if (preg_match('#^\d+$#',$conf['max_post_length'])!==1 || $conf['max_post_length']+0<10) dieYoung("Error: configuration file: «max_post_length» must be an integer greater than or equal to 10.\n",1);
|
|||
|
$conf['max_post_length']+=0;
|
|||
|
echo "Info: got «{$conf['max_post_length']}» as «max_post_length» from configuration file.\n";
|
|||
|
}
|
|||
|
|
|||
|
$url="https://{$conf['fedi_hostname']}/api/v2/instance";
|
|||
|
echo "Info: trying to fetch instance info from «{$url}».\n";
|
|||
|
$res=curl($url,null,["Authorization: Bearer {$conf['fedi_token']}", 'Accept: application/json']);
|
|||
|
if ($res['content']===false) dieYoung("Error: could not connect to «{$url}» (error: «{$res['error']}»).\n",1);
|
|||
|
$res['content']=@json_decode($res['content'],true);
|
|||
|
if (is_null($res['content'])) dieYoung("Error: content from «{$url}» was not good JSON.\n",1);
|
|||
|
(isset($res['content']['error'])) ? $buff=" («{$res['content']['error']}»)" : $buff='';
|
|||
|
if ($res['httpcode']!='200') dieYoung("Error: got http code «{$res['httpcode']}»{$buff} from «{$url}».\n",1);
|
|||
|
if (!isset($res['content']['configuration']['media_attachments']['image_size_limit'])) dieYoung("Error: JSON from «{$url}» doesn’t declare «image_size_limit».\n",1);
|
|||
|
if (!is_int($res['content']['configuration']['media_attachments']['image_size_limit'])) dieYoung("Error: JSON from «{$url}» declares «image_size_limit» with an unexpected format.\n",1);
|
|||
|
$conf['max_image_size']=$res['content']['configuration']['media_attachments']['image_size_limit']+0;
|
|||
|
echo "Info: got «{$conf['max_image_size']}» as «max_image_size» from «{$url}».\n";
|
|||
|
if (!isset($res['content']['configuration']['statuses']['max_characters'])) dieYoung("Error: JSON from «{$url}» doesn’t declare «max_characters».\n",1);
|
|||
|
if (!is_int($res['content']['configuration']['statuses']['max_characters'])) dieYoung("Error: JSON from «{$url}» declares «max_characters» with an unexpected format.\n",1);
|
|||
|
if (is_null($conf['max_post_length'])) {
|
|||
|
$conf['max_post_length']=$res['content']['configuration']['statuses']['max_characters']+0;
|
|||
|
echo "Info: got «{$conf['max_post_length']}» as «max_post_length» from «{$url}».\n";
|
|||
|
}
|
|||
|
//print_r($conf);
|
|||
|
|
|||
|
echo "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);
|
|||
|
//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");
|
|||
|
echo "Info: got good feed from «{$conf['feed_url']}».\n";
|
|||
|
|
|||
|
$guids=[];
|
|||
|
echo "Info: trying to load GUIDs of already posted events from state file «{$conf['state_file_absolute_path']}».\n";
|
|||
|
if (!file_exists($conf['state_file_absolute_path']) && is_null($opts['do-post'])) dieYoung("Error: state file «{$conf['state_file_absolute_path']}» does not exist: this seems to be a first run on feed «{$conf['feed_url']}», so you should explicitly declare whether you want {$SNAME} to post all of its events or not, setting option «-p» or «--do-post» to «y» («yes») or «n» («no»); use «-h» or «--help» to display help.\n",1);
|
|||
|
if (is_null($opts['do-post'])) $opts['do-post']=true;
|
|||
|
if (file_exists($conf['state_file_absolute_path'])) {
|
|||
|
if (!is_file($conf['state_file_absolute_path'])) dieYoung("Error: «{$conf['state_file_absolute_path']}» exists but it’s not a file.\n",1);
|
|||
|
if (!is_readable($conf['state_file_absolute_path'])) dieYoung("Error: «{$conf['state_file_absolute_path']}» exists but it’s not readable.\n",1);
|
|||
|
if (!is_readable($conf['state_file_absolute_path'])) dieYoung("Error: «{$conf['state_file_absolute_path']}» exists but it’s not writable.\n",1);
|
|||
|
$guids=[];
|
|||
|
$buff=file($conf['state_file_absolute_path'],FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
|
|||
|
$graceTime=365*24*60*60;
|
|||
|
$graceLine=time()-$graceTime;
|
|||
|
$i=0;
|
|||
|
$fh=fopen($conf['state_file_absolute_path'],'w');
|
|||
|
foreach ($buff as $key=>$val) {
|
|||
|
if (preg_match('#^(\d+)\t(\S+)$#',$val,$matches)===1) {
|
|||
|
if ($matches[1]+0>=$graceLine) {
|
|||
|
fwrite($fh,"{$matches[1]}\t{$matches[2]}\n");
|
|||
|
$guids[$matches[2]]=$matches[1];
|
|||
|
} else {
|
|||
|
$i++;
|
|||
|
}
|
|||
|
} else {
|
|||
|
fwrite(STDERR,"Warning: in state file «{$conf['state_file_absolute_path']}», line ".($key+1)." had unexpected format.\n");
|
|||
|
}
|
|||
|
}
|
|||
|
fclose($fh);
|
|||
|
echo 'Info: got '.count($guids)." GUID(s) for already posted event(s) from state file «{$conf['state_file_absolute_path']}»; removed {$i} line(s) older than one year.\n";
|
|||
|
}
|
|||
|
|
|||
|
if (($fh=@fopen($conf['state_file_absolute_path'],'a'))===false) dieYoung("Error: could not open «{$conf['state_file_absolute_path']}» in «append» mode.\n",1);
|
|||
|
$newItemsCount=0;
|
|||
|
$goodPostsCount=0;
|
|||
|
$index=0;
|
|||
|
foreach ($feed->channel->item as $item) {
|
|||
|
$index++;
|
|||
|
// print_r($item);
|
|||
|
if (!isset($item->guid) || ($guid=$item->guid->__toString())=='') {
|
|||
|
fwrite(STDERR,"Warning: event #{$index} has no GUID, skipping.\n");
|
|||
|
} elseif (!array_key_exists($guid,$guids)) {
|
|||
|
$newItemsCount++;
|
|||
|
$file=null;
|
|||
|
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 (isset($item->description)) {
|
|||
|
$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]} alle {$matches[6]} presso {$matches[2]}\n\n".html2text($matches[7]);
|
|||
|
} else {
|
|||
|
$ptext=html2text($item->description);
|
|||
|
}
|
|||
|
}
|
|||
|
if (isset($item->link) && $item->link->__toString()!='') {
|
|||
|
$plink="\n\n".$item->link->__toString();
|
|||
|
} else {
|
|||
|
$plink='';
|
|||
|
}
|
|||
|
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='';
|
|||
|
}
|
|||
|
$post="{$ptext}{$plink}{$pcats}";
|
|||
|
while (postLength($post,$tldsregex['tlds'])>$conf['max_post_length'] && $ptext!='') {
|
|||
|
$ptext=preg_replace('#\S+\W*$#','',$ptext);
|
|||
|
// echo "[[[{$ptext}]]]\n";
|
|||
|
$post="{$ptext}[…]{$plink}{$pcats}";
|
|||
|
}
|
|||
|
if (postLength($post,$tldsregex['tlds'])>$conf['max_post_length']) $post=$plink;
|
|||
|
if (postLength($post,$tldsregex['tlds'])>$conf['max_post_length']) {
|
|||
|
fwrite(STDERR,"Warning: could not shorten post for event «{$guid}» to make it fit into {$conf['max_post_length']} characters; will not post.\n");
|
|||
|
} else {
|
|||
|
if ($opts['verbose']) echo "--- #{$index}: {$guid} ---\n{$post}\n--- (length: ".postLength($post,$tldsregex['tlds']).") ---\n\n";
|
|||
|
if ($opts['do-post']) {
|
|||
|
$postData=[];
|
|||
|
// print_r($file);
|
|||
|
if (is_null($file)) {
|
|||
|
if ($opts['verbose']) echo "Info: event «{$guid}» has no attachment.\n";
|
|||
|
} elseif ($file['length']>$conf['max_image_size']) {
|
|||
|
fwrite(STDERR,"Warning: ignoring attachment of event «{$guid}» because its size is greater than image upload max size on «{$conf['fedi_hostname']}».\n");
|
|||
|
} else {
|
|||
|
$res=curl($file['url']);
|
|||
|
if ($res['content']===false) {
|
|||
|
fwrite(STDERR,"Warning: could not connect to «{$file['url']}» (error: «{$res['error']}»); won’t post file as attachment.\n");
|
|||
|
} elseif ($res['httpcode']!='200') {
|
|||
|
fwrite(STDERR,"Warning: «{$file['url']}» returned http code «{$res['httpcode']}»; won’t post file as attachment.\n");
|
|||
|
} else {
|
|||
|
$pd=['file'=>new CURLStringFile($res['content'],'file',$file['type']), 'description'=>'Flyer dell’evento'];
|
|||
|
$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']}»); won’t post file as attachment.\n");
|
|||
|
} elseif (is_null($res['content']=@json_decode($res['content'],true))) {
|
|||
|
fwrite(STDERR,"Warning: «{$url}» did not return valid JSON; won’t post file as attachment.\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}; won’t post file as attachment.\n");
|
|||
|
} elseif (!isset($res['content']['id'])) {
|
|||
|
fwrite(STDERR,"Warning: no «id» in JSON from «{$url}»; won’t post file as attachment.\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))
|
|||
|
$postData['media_ids[]']=$id;
|
|||
|
else
|
|||
|
fwrite(STDERR,"Warning: server took too long to process file, or could not; won’t post file as attachment.\n");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
$postData['status']=$post;
|
|||
|
$postData['visibility']=$conf['posts_visibility'];
|
|||
|
$postData['language']='it';
|
|||
|
$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 event «{$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 event «{$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 event «{$guid}».\n");
|
|||
|
} elseif (!isset($res['content']['url'])) {
|
|||
|
fwrite(STDERR,"Warning: JSON from «{$url}» had unexpected format; could not post status for event «{$guid}».\n");
|
|||
|
} else {
|
|||
|
echo "Info: successfully posted status for event «{$guid}» (URL: «{$res['content']['url']}»).\n";
|
|||
|
// print_r($res['content']);
|
|||
|
$now=time();
|
|||
|
$guids[$guid]=$now;
|
|||
|
fwrite($fh,"{$now}\t{$guid}\n");
|
|||
|
$goodPostsCount++;
|
|||
|
}
|
|||
|
} else {
|
|||
|
echo "Info: would have posted status for event «{$guid}».\n";
|
|||
|
$now=time();
|
|||
|
$guids[$guid]=$now;
|
|||
|
fwrite($fh,"{$now}\t{$guid}\n");
|
|||
|
$goodPostsCount++;
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
if ($opts['verbose']) echo "Info: event «{$guid}» has already been posted on ".date('c',$guids[$guid]).", skipping.\n";
|
|||
|
}
|
|||
|
// fclose($fh); exit(0);// to test a single post
|
|||
|
}
|
|||
|
fclose($fh);
|
|||
|
if ($opts['do-post'])
|
|||
|
echo "Info: feed got {$itemsCount} events; succesfully posted {$goodPostsCount} of {$newItemsCount} new event(s).\n";
|
|||
|
else
|
|||
|
echo "Info: feed got {$itemsCount} event(s), {$newItemsCount} new.\n";
|
|||
|
|
|||
|
exit(0);
|
|||
|
|
|||
|
|
|||
|
function getConf(&$conf,&$confFP) {
|
|||
|
$errors=[];
|
|||
|
$nconf=[];
|
|||
|
$buff=@file($confFP,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
|
|||
|
if ($buff===false) dieYoung("Error: could not read configuration file «$confFP}».\n",1);
|
|||
|
$i=0;
|
|||
|
foreach ($buff as $line) {
|
|||
|
$i++;
|
|||
|
if ($line[0]!=='#') {
|
|||
|
if (preg_match('#^([^=]+)=(.+)$#',$line,$matches)===1) {
|
|||
|
$matches[1]=trim($matches[1]);
|
|||
|
$matches[2]=trim($matches[2]);
|
|||
|
if (array_key_exists($matches[1],$conf))
|
|||
|
$nconf[$matches[1]]=$matches[2];
|
|||
|
else
|
|||
|
$errors[]="line {$i}: «{$matches[1]}» is an unknown key.\n";
|
|||
|
} else {
|
|||
|
$errors[]="could not interpret line {$i} («{$line}»)";
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
//print_r($nconf);
|
|||
|
foreach ($conf as $key=>$val) {
|
|||
|
if ($conf[$key]['required'] && !array_key_exists($key,$nconf))
|
|||
|
$errors[]="«{$key}» is not defined";
|
|||
|
if (array_key_exists($key,$nconf))
|
|||
|
$conf[$key]=$nconf[$key];
|
|||
|
else
|
|||
|
$conf[$key]=$conf[$key]['default'];
|
|||
|
}
|
|||
|
$errorsCount=count($errors);
|
|||
|
if ($errorsCount>0) {
|
|||
|
fwrite(STDERR,"Sorry, there are errors in configuration file «{$confFP}»:\n");
|
|||
|
for ($i=1; $i<=$errorsCount; $i++)
|
|||
|
fwrite(STDERR," {$i}. {$errors[$i-1]}\n");
|
|||
|
fwrite(STDERR,"Use «-h» or «--help» to display help.\n");
|
|||
|
exit(1);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function cknap($napid) {
|
|||
|
global $naps;
|
|||
|
$now=time();
|
|||
|
if (isset($naps[$napid]) && $naps[$napid]>$now) {
|
|||
|
$sleepsecs=$naps[$napid]-$now;
|
|||
|
echo "Info: reached rate limit on «{$napid}»; sleeping until ".date('c',$naps[$napid]).' ...';
|
|||
|
sleep($sleepsecs);
|
|||
|
echo "\n";
|
|||
|
$naps[$napid]=0;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function hent($str) {
|
|||
|
return html_entity_decode($str,ENT_QUOTES,'UTF-8');
|
|||
|
}
|
|||
|
|
|||
|
function dieYoung($msg,$ec) {
|
|||
|
fwrite(STDERR,$msg);
|
|||
|
die($ec);
|
|||
|
}
|
|||
|
|
|||
|
function exitYoung($msg) {
|
|||
|
echo $msg;
|
|||
|
exit(0);
|
|||
|
}
|
|||
|
|
|||
|
?>
|