pezcurrel 2 тижнів тому
коміт
0150d57467
4 змінених файлів з 573 додано та 0 видалено
  1. 41 0
      README.md
  2. 50 0
      lib/ckratelimit.php
  3. 65 0
      lib/httpjson.php
  4. 417 0
      pfaltgall

+ 41 - 0
README.md

@@ -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 (don’t 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 - 0
lib/ckratelimit.php

@@ -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 - 0
lib/httpjson.php

@@ -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 - 0
pfaltgall

@@ -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 (don’t 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";
+	}
+}
+
+?>