First commit

This commit is contained in:
pezcurrel 2024-09-27 21:20:18 +02:00
commit 0150d57467
4 changed files with 573 additions and 0 deletions

41
README.md Normal file
View file

@ -0,0 +1,41 @@
```text
[[[ SYNOPSIS ]]]
pfaltgall [options] <configuration file path>
[[[ DESCRIPTION ]]]
This is pfaltgall v0.1, a CLI PHP script that can generate an html file with
a gallery from your Pixelfed profile.
In order to create it, you just need to login to your Pixelfed account and
get an app token (Settings -> Applications -> Create new token), then create
a configuration file for pfaltgall like this (dont write the «---» lines):
---
host=your_instance_host
token=your_token
---
For example:
---
host=pixelfed.social
token=as7f8a7s0d89f7as97df09a8s7d90f81jkl2h34lkj12h3jkl4
---
Then run pfaltgall with the path of the configuration file you have
created. This will create an «index.html» file that will be ready to be put
where you want (you can also see it locally, obviously).
[[[ OPTIONS ]]]
-h, --help
Show this help text and exit.
[[[ 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.
```

50
lib/ckratelimit.php Normal file
View file

@ -0,0 +1,50 @@
<?php
function ckratelimit($headers) {
$aaheaders=[];
array_shift($headers);
foreach ($headers as $header)
if (preg_match('#^([^:]+): (.*)$#',$header,$matches)===1)
$aaheaders[strtolower($matches[1])]=$matches[2];
//$aaheaders['x-ratelimit-remaining']=0;
//print_r($aaheaders);
if (!isset($aaheaders['date'])) return ['ok'=>false,'error'=>'no «date» header'];
if (!isset($aaheaders['x-ratelimit-reset'])) return ['ok'=>false,'error'=>'no «x-ratelimit-reset» header'];
if (!isset($aaheaders['x-ratelimit-remaining'])) return ['ok'=>false,'error'=>'no «x-ratelimit-remaining» header'];
if (preg_match('#^\d+$#',$aaheaders['x-ratelimit-remaining'])!==1) return ['ok'=>false,'error'=>'«x-ratelimit-remaining» header is not an integer'];
$remaining=$aaheaders['x-ratelimit-remaining']+0;
$date=@strtotime($aaheaders['date']);
if (!is_int($date)) return ['ok'=>false,'error'=>'«date» header could not be converted to a unix timestamp'];
$reset=@strtotime($aaheaders['x-ratelimit-reset']);
if (!is_int($reset)) return ['ok'=>false,'error'=>'«x-ratelimit-reset» header could not be converted to a unix timestamp'];
// don't do the one on the line below, since it happens lots of times
//if ($reset<$date) return ['ok'=>false,'error'=>'the unix timestamp parsed from «x-ratelimit-reset» header is less than the unix timestamp parsed from «date» header'];
if ($remaining==0)
return ['ok'=>true,'sleep'=>$reset-$date+1,'remaining'=>$remaining];
else
return ['ok'=>true,'sleep'=>0,'remaining'=>$remaining];
}
/*
// test
$context=[
'http'=>[
'header'=>"Accept: application/json\r\n";
]
];
$context=stream_context_create($context);
while (true) {
$res=@file_get_contents('https://livellosegreto.it/api/v2/instance',false,$context);
echo "{$res}\n";
print_r($http_response_header);
$rl=ckratelimit($http_response_header);
print_r($rl);
if ($rl['sleep']>0) {
echo 'Reached rate limit, sleeping for '.ght($rl['sleep']).' (until '.date('c',time()+$rl['sleep']).') ...';
sleep($rl['sleep']);
echo "\n";
}
}
exit(0);*/
?>

65
lib/httpjson.php Normal file
View file

@ -0,0 +1,65 @@
<?php
function httpjson($endpoint,$timeout=null,$method=null,$postdata=null,$accept=null,$token=null,$okcodes=null) {
if (is_null($timeout)) $timeout=5;
if (is_null($method)) $method='GET';
if (is_null($accept)) $accept='application/json';
if (is_null($okcodes)) $okcodes=[200];
$context=[
'http'=>[
'timeout'=>$timeout,
'method'=>$method,
'user_agent'=>'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0',
'header'=>"Accept: {$accept}\r\n",
'ignore_errors'=>true
]
];
if (!is_null($token)) $context['http']['header'].="Authorization: Bearer {$token}\r\n";
if (!is_null($postdata)) {
$context['http']['header'].="Content-type: application/x-www-form-urlencoded\r\n";
$context['http']['content']=http_build_query($postdata);
}
$context=stream_context_create($context);
$headers=[];
$errors=[];
$ret=['ok'=>false,'headers'=>[],'content'=>[],'errors'=>null];
$http_response_header=null;
$res=@file_get_contents($endpoint,false,$context);
if ($res===false) {
$errors[]="could not connect";
} else {
if (is_array($http_response_header)) {
$httprc=null;
$li=count($http_response_header)-1;
for ($i=$li; $i>=0; $i--) {
array_unshift($headers,$http_response_header[$i]);
if (preg_match('#HTTP/\S+\s+(\d+)#',$http_response_header[$i],$matches)===1) {
$httprc=$matches[1]+0;
break;
}
}
if (is_null($httprc))
$errors[]="got no HTTP response status code";
elseif (!in_array($httprc,$okcodes))
$errors[]="got «{$httprc}» HTTP response status code";
} else {
$errors[]="«got no headers";
}
$res=@json_decode($res,true);
if ($res===false) {
$errors[]="got no valid JSON";
} else {
if (count($errors)>0 && isset($res['error']))
$errors[]=$res['error'];
$ret['content']=$res;
}
}
if (count($errors)==0)
$ret['ok']=true;
$errors=implode('; ',$errors);
$ret['headers']=$headers;
$ret['errors']=$errors;
return $ret;
}
?>

417
pfaltgall Executable file
View file

@ -0,0 +1,417 @@
#!/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/>.
*/
$SCRIPTNAME='pfaltgall';
$SCRIPTVERSION='0.1';
require 'lib/ckratelimit.php';
require 'lib/httpjson.php';
$configfp=null;
$conf=[
'host'=>null,
'token'=>null,
'accid'=>null
];
$help=
"[[[ SYNOPSIS ]]]
{$SCRIPTNAME} [options] <configuration file path>
[[[ DESCRIPTION ]]]
This is {$SCRIPTNAME} v{$SCRIPTVERSION}, a CLI PHP script that can generate an html file with
a gallery from your Pixelfed profile.
In order to create it, you just need to login to your Pixelfed account and
get an app token (Settings -> Applications -> Create new token), then create
a configuration file for {$SCRIPTNAME} like this (dont write the «---» lines):
---
host=your_instance_host
token=your_token
---
For example:
---
host=pixelfed.social
token=as7f8a7s0d89f7as97df09a8s7d90f81jkl2h34lkj12h3jkl4
---
Then run {$SCRIPTNAME} with the path of the configuration file you have
created. This will create an «index.html» file that will be ready to be put
where you want (you can also see it locally, obviously).
[[[ OPTIONS ]]]
-h, --help
Show this help text and exit.
[[[ 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";
for ($i=1; $i<$argc; $i++) {
if ($argv[$i]=='-h' || $argv[$i]=='--help') {
echo $help;
exit(0);
} elseif ($argv[$i]=='--make-readme') {
file_put_contents(__DIR__.'/README.md',"```text\n{$help}\n```\n");
exit(0);
} elseif (is_null($configfp)) {
$configfp=$argv[$i];
} else {
eecho("Error: «{$argv[$i]}» is not a valid option and the configuration file has already been set to «{$configfp}» (use «-h» or «--help» to read help).\n");
exit(1);
}
}
if (is_null($configfp)) {
eecho("Error: you have not specified a config file (use «-h» or «--help» to read help).\n");
exit(1);
}
$fconf=@parse_ini_file($configfp);
if ($fconf===false) {
eecho("Error: {$SCRIPTNAME} could not open configuration file «{$configfp}».\n");
exit(1);
}
$errors=[];
if (!array_key_exists('host',$fconf))
$errors[]="no «host» defined";
if (!array_key_exists('token',$fconf))
$errors[]="no «token» defined";
if (count($errors)>0) {
eecho("Error: {$SCRIPTNAME} has found errors in «{$configfp}» configuration file:\n");
foreach ($errors as $val)
eecho(" - {$val}\n");
eecho("Use «-h» or «--help» to read help.\n");
exit(1);
}
foreach ($conf as $key=>$val)
if (array_key_exists($key,$fconf))
$conf[$key]=$fconf[$key];
//print_r($conf);
$acc=httpjson("https://{$conf['host']}/api/v1/accounts/verify_credentials",null,null,null,null,$conf['token']);
//print_r($res);
if (!$acc['ok']) {
eecho("Error: {$SCRIPTNAME} could not retrieve the account id associated with the given token ({$acc['errors']}).\n");
exit(2);
}
ckrl($acc['headers']);
$acc=$acc['content'];
$urls=[];
$imgs='';
$i=0;
$ic=0;
do {
$i++;
echo "\rRetrieving chunk {$i}";
$endpoint="https://{$conf['host']}/api/v1/accounts/{$acc['id']}/statuses?limit=40&only_media=1&exclude_replies=1&exclude_reblogs=1";
if (isset($max_id)) $endpoint.="&max_id={$max_id}";
$res=httpjson($endpoint,null,null,null,null,$conf['token']);
//print_r($res);
if (!$res['ok']) {
eecho("\rError: {$SCRIPTNAME} could not retrieve chunk {$i} of statuses ({$res['errors']}).\n");
exit(2);
}
$count=count($res['content']);
if ($count>0) {
foreach ($res['content'] as $status) {
if (isset($status['created_at']) && preg_match('#^\s+$#',$status['created_at'])!==1) {
$date=strtotime($status['created_at']);
$date=' ['.date('Y/m/d',$date).']';
} else {
$date='';
}
if (isset($status['content']) && preg_match('#^\s+$#',$status['content'])!==1) {
$desc=strip_tags($status['content']);
$desc=preg_replace('/#\w+/','',$desc);
$desc=trim($desc);
} else {
$desc='';
}
if (isset($status['media_attachments']) && is_array($status['media_attachments'])) {
$ca=count($status['media_attachments']);
$ia=0;
foreach ($status['media_attachments'] as $attachment) {
if (isset($attachment['url'])) {
$url=$attachment['url'];
$ia++;
if (isset($attachment['description']) && preg_match('#^\s+$#',$attachment['description'])!==1)
$altdesc=' alt="'.htmlspecialchars(trim($attachment['description']),ENT_QUOTES|ENT_HTML5).'"';
else
$altdesc='';
if ($ca>1)
$icnt=" ({$ia}/{$ca})";
else
$icnt='';
$imgs.="<div class=\"page\"><table class=\"imgtab\"><tr><td class=\"imgcel\"><a href=\"{$url}\" name=\"img{$ic}\"><img class=\"img\" src=\"{$url}\"{$altdesc}></a></td></tr><caption class=\"imgcaptcel\">{$desc}{$icnt}{$date}</caption></table></div>\n";
$ic++;
}
}
}
}
$max_id=$res['content'][$count-1]['id'];
//echo "count: {$count}; max_id: {$max_id}\n";
if ($count<40)
break;
}
ckrl($res['headers']);
} while ($count>0);
echo "\n";
$title="{$acc['username']}@{$conf['host']}";
if ($acc['display_name']!='') $title=htmlspecialchars($acc['display_name'],ENT_QUOTES|ENT_HTML5)." ({$title})";
$html='<!DOCTYPE HTML>
<html lang="en">
<head>
<title>'.$title.'</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="description" content="Album">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes">
<meta property="og:image" content="'.$acc['avatar_static'].'">
<link rel="icon" type="image/png" href="'.$acc['avatar_static'].'">
<!--
<meta property="og:image" content="imgs/ogimage.jpg">
<link rel="icon" type="image/png" href="imgs/icon-16.png" sizes="16x16">
<link rel="icon" type="image/png" href="imgs/icon-24.png" sizes="24x24">
<link rel="icon" type="image/png" href="imgs/icon-32.png" sizes="32x32">
<link rel="icon" type="image/png" href="imgs/icon-64.png" sizes="64x64">
<link rel="icon" type="image/png" href="imgs/icon-128.png" sizes="128x128">
<link rel="apple-touch-icon-precomposed" href="imgs/icon-180.png">
<link rel="icon" type="image/png" href="imgs/icon-192.png" sizes="192x192">
<link rel="icon" type="image/png" href="imgs/icon-256.png" sizes="256x256">
<link rel="icon" type="image/png" href="imgs/icon-512.png" sizes="512x512">
-->
<!-- <link rel="stylesheet" type="text/css" href="gallery.css"> -->
<style>
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
height: 100vh;
}
body {
font-family: "sans";
font-size: 12pt;
background-color: black;
color: white;
margin: 0;
height: 100vh;
}
a {
color: #87decd;
}
a:focus {
outline: none;
}
p {
margin: 0;
padding: 0;
text-indent: 3mm;
line-height: 1.3em;
}
p.firstp, p.center {
text-indent: 0;
}
p.center {
text-align: center;
text-wrap: balance;
padding: 1em 0 1em 0;
}
.profile {
width: 640px;
}
.avatar {
border-radius: 12px;
}
hr {
display: block;
border: none;
height: 1px;
background-color: #666666;
margin: 3mm 0 3mm 0;
}
#maindiv {
width: 100%;
height: 100%;
overflow: auto;
scroll-snap-type: y mandatory;
scroll-padding: 0;
}
.page {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
scroll-snap-stop: always;
scroll-snap-align: start;
scroll-margin: 0;
/*border: 1px solid yellow;*/
}
.imgtab, .imgtab tr, .imgtab td {
margin: 0;
padding: 0;
border: none;
border-collapse: collapse;
table-layout: fixed;
scroll-snap-align: none;
}
.img {
display: block;
position: relative;
max-width: 92vw;
max-height: 92vh;
border: 8px solid white;
border-bottom: none;
scroll-snap-align: none;
}
.imgcaptcel {
background-color: white;
color: black;
padding: 4px 8px 4px 8px;
font-size: 10pt;
caption-side: bottom;
text-align: left;
line-height: 1.2;
scroll-snap-align: none;
}
.textdiv {
width: 20cm;
max-width: 92%;
background-color: #333333;
border-radius: 8px;
padding: 3mm;
scroll-snap-align: none;
}
#notif {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgb(51 51 51 / .85);
padding: 4px 6px 4px 6px;
color: white;
border-radius: 6px;
font-size: 10pt;
z-index: 1;
cursor: pointer;
}
.link {
margin-top: 2px;
float: right;
color: #333333;
cursor: pointer;
}
#tools {
/*display: none;*/
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: rgb(51 51 51 / .85);;
padding: 4px 6px 4px 6px;
color: white;
font-size: 10pt;
z-index: 1;
}
@media only screen and (max-width:15cm) {
.img {
max-width: 100vw;
max-height: 98vh;
border: 2px solid white;
}
.imgcaptcel {
padding: 1px 2px 1px 2px;
font-size: 8pt;
}
.textdiv {
font-size: 9pt;
}
.link {
margin-top: 0;
}
}
</style>
</head>
<body>
<div id="maindiv">
<div class="page">
<div class="profile">
<p class="center"><img src="'.$acc['avatar_static'].'" class="avatar"></p>
<p class="center"><a href="'.$acc['url'].'">'.$title.'</a></p>
<p class="center">'.nl2br($acc['note']).'</p>
</div>
</div>
'.$imgs.'
</div>
</body>
</html>
';
file_put_contents('index.html',$html);
exit(0);
function eecho($text) {
fwrite(STDERR,$text);
}
function ckrl($headers) {
$rl=ckratelimit($headers);
if ($rl['ok'] && $rl['sleep']>0) {
echo "\rInfo: reached rate limit, sleeping for {$rl['sleep']} second(s)... ";
sleep($rl['sleep']);
echo "\n";
}
}
?>