Browse Source

First commit

pezcurrel 5 months ago
commit
6c38b489ff
18 changed files with 1381 additions and 0 deletions
  1. 41 0
      README.md
  2. 400 0
      css/main.css
  3. BIN
      imgs/icon-16.png
  4. BIN
      imgs/icon-180.png
  5. BIN
      imgs/icon-192.png
  6. BIN
      imgs/icon-32.png
  7. BIN
      imgs/icon-512.png
  8. BIN
      imgs/icon_close.png
  9. BIN
      imgs/ogimage.png
  10. 455 0
      index.php
  11. 26 0
      js/main.js
  12. 8 0
      lib/booltostr.php
  13. 36 0
      lib/ckratelimit.php
  14. 20 0
      lib/getfirstbrowserlang.php
  15. 3 0
      lib/gettlds.php
  16. 50 0
      lib/ght.php
  17. 282 0
      lib/mastodon.php
  18. 60 0
      post.php

+ 41 - 0
README.md

@@ -0,0 +1,41 @@
+## What is Verbose?
+
+Verbose is a post splitter for Mastodon in a web page. You can write or paste a long post into it, set some options, push the “Split” button, and you get your long post split into many posts considering [Mastodon rules](https://docs.joinmastodon.org/user/posting/#text): every http(s) link counts as 23 characters and every mention counts only for the length of its username part. Each split post will come with `…` signs where it makes sense and a counter in `[n/t]` form, where `n` is the current post number and `t` is the total posts number.
+
+You can connect Verbose to your account: this way *you’ll be able to post all split posts at once directly from within Verbose*, they will be automatically “chain posted” (the second will be a reply to the first, the third to the second, and so on) and, before posting them, you may set their visibility, language, and a post to reply to with the first split post.
+
+You can also use it without connecting it to your account, but after splitting your long post you’ll have to copy and paste by hand each split post in sequence into Mastodon, and “chain posting” will be up to you.
+
+Verbose doesn’t save anywhere what you write or paste into it, it doesn’t use third parties’ cookies, it sets some cookies of its own only if you choose to connect it to your account, and it can be used even without Javascript.
+
+If you find issues please let me know [here](https://git.lattuga.net/pongrebio/verbose/issues), or using the e-mail address you can find in my profile page, or directly contacting [me on Mastodon](https://puntarella.party/@umpi).
+
+## Setting up Verbose on a webserver
+
+To set up Verbose on a webserver you need it to support PHP, you have to make the directory you put Verbose into and its `/js` subdirectory writeable to the user your webserver runs under, and you have to set a `conf.ini` file into Verbose main directory.
+
+The `conf.ini` file *must* define a `webservertimeout` in seconds, i.e. the maximum time in seconds your webserver allows a request to last (with Apache it’s [this](https://httpd.apache.org/docs/current/mod/core.html#timeout)); for example:
+
+```
+webservertimeout=120
+```
+
+Inside `conf.ini` you can also customize the “link text” (text Verbose can add to last split post if there’s enough space left; by default it’s `[This post was split using https://git.lattuga.net/pongrebio/verbose]`); for example:
+
+```
+link=[This post was split using https://my.server/verbose]
+```
+
+Of course you can put anything as a value for `link`, but the UI mentions it as «link to this page», so it’s expected to contain a link to the URL of your running Verbose instance ;-)
+
+You can also set a `footer` that, if defined, will be added before the link to this repo in the page footer; for example:
+
+```
+footer=<a href="https://my.server/">Home</a>
+```
+
+## Are there running Verbose instances?
+
+You can find a running Verbose instance [here](https://mastodon.help/verbose).
+
+If you set up your own and you want it to be listed here, please let me know using the e-mail address you can find in my profile page, or directly contacting [me on Mastodon](https://puntarella.party/@umpi).

+ 400 - 0
css/main.css

@@ -0,0 +1,400 @@
+* {
+	box-sizing: border-box;
+}
+
+html {
+	scroll-behavior: smooth;
+}
+
+body {
+	font-family: "sans";
+	font-size: 12pt;
+	background-color: #222222;
+	color: white;
+	margin: 0;
+	padding: 0;
+}
+
+a {
+	color: #87decd;
+}
+
+form {
+	padding: 0;
+	margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6 {
+	margin: 3mm 0 5mm 0;
+	padding: 0;
+	text-align: center;
+	color: white;
+}
+
+p {
+	margin: 0;
+	color: white;
+	text-indent: 3mm;
+	/*text-align: justify;
+	-webkit-hyphens: auto;
+	-ms-hyphens: auto;
+	hyphens: auto;*/
+}
+
+.firstp {
+	text-indent: 0;
+}
+
+.ul {
+	padding-left: 5mm;
+	margin-bottom: 0;
+}
+
+.notset {
+	color: #555555;
+}
+
+input:focus, textarea:focus, button:focus {
+	outline: none;
+}
+
+#main {
+	margin-left: auto;
+	margin-right: auto;
+	max-width: 20cm;
+	width: 100%;
+	padding: 3mm;
+}
+
+#notif {
+	width: 6cm;
+	position: fixed;
+	right: 3mm;
+	bottom: 3mm;
+	background-color: #16502d;
+	color: white;
+	border: 1px solid white;
+	padding: 3mm;
+	border-radius: 6px;
+	display: none;
+	font-size: 10pt;
+	cursor: pointer;
+}
+
+#popup {
+	display: none;
+	align-items: center;
+	position: fixed;
+	top: 0;
+	width: 100%;
+	height: 100vh;
+	z-index: 1;
+	background-color: rgba(0, 0, 0, .75);
+}
+
+#popupmsg {
+	margin-left: auto;
+	margin-right: auto;
+	max-width: 98%;
+	width: 15cm;
+}
+
+#puptitle {
+	border-radius: 9px 9px 0 0;
+	padding: 0;
+	background-color: white;
+	color: black;
+	font-weight: bold;
+}
+
+#pupmsg {
+	border-radius: 0 0 9px 9px;
+	border: 1px solid white;
+	border-top: 0;
+	padding: 1.5mm 3mm 1.5mm 3mm;
+	background-color: #555555;
+	color: white;
+}
+
+#pupmsg ul {
+	margin: 0;
+	margin-left: 3mm;
+	padding: 0;
+}
+
+.error, .success, .warning, .normtext {
+	width: 100%;
+	color: red;
+	margin-bottom: 15px;
+	border: 1px solid red;
+	border-radius: 6px;
+	padding: 3mm;
+}
+
+.warning {
+	color: orange;
+	border-color: orange;
+}
+
+.success {
+	color: lightgreen;
+	border-color: lightgreen;
+}
+
+.normtext {
+	background-color: #555555;
+	color: white;
+	border: none;
+}
+
+.hili {
+	color: #ffcc00;
+}
+
+.tittab {
+	border-collapse: collapse;
+	width: 100%;
+	border: none;
+}
+
+.tittab tr {
+	margin: 0;
+	padding: 0;
+}
+
+.tittab td {
+	margin: 0;
+	padding: 1mm;
+	vertical-align: middle;
+}
+
+.closeb {
+	cursor: pointer;
+	top: 3px;
+	vertical-align: middle;
+}
+
+.inputdiv, .lastinputdiv, .outputdiv, .lastoutputdiv {
+	width: 100%;
+}
+
+.inputdiv {
+	margin-bottom: 15px;	
+}
+
+.lastoutputdiv {
+	margin-top: 15px;
+}
+
+.input, .inputx, .textarea, .button, .postbutton, .halfbutton, .copybutton, .output, .outputnobb, .outputli, .posthead, .lastborder, fieldset {
+	width: 100%;
+	border: 1px solid #555555;
+	border-radius: 0 6px 6px 6px;
+	font-size: 12pt;
+	margin: 0;
+	padding: 3px;
+}
+
+.input, .inputx, .textarea {
+	font-family: "sans";
+}
+
+.inputx {
+	border-radius: 0 6px 0 0;
+}
+
+.lastborder {
+	border-top: none;
+	border-radius: 0 0 6px 6px;
+}
+
+fieldset {
+	padding: 5px;
+}
+
+.halfbutton {
+	width: 50%;
+	height: 30px;
+	border-radius: 6px;
+}
+
+.button, .postbutton, .copybutton {
+	height: 40px;
+	border-radius: 6px;
+	font-weight: bold;
+}
+
+.button, .postbutton {
+	color: white;
+	background-color: #916f7c;
+	border-color: #ac939d #6c535d #6c535d #ac939d;
+}
+
+.button:hover, .postbutton:hover {
+	background-color: #6c535d;
+	border-color: #916f7c #48373e #48373e #916f7c;
+}
+
+.copybutton {
+	border-top: none;
+	border-radius: 0 0 6px 6px;
+	display: none;
+}
+
+.output, .outputnobb {
+	border-radius: 0;
+	margin: 0;
+	font-family: "sans";
+}
+
+.outputnobb {
+	background-color: white;
+	color: black;
+	border-bottom: none;
+}
+
+.outputli {
+	border-radius: 0 0 6px 6px;
+}
+
+label {
+	max-width: 96%;
+	font-weight: bold;
+	color: white;
+	background-color: #555555;
+	border-bottom: none;
+	border-radius: 6px 6px 0 0;
+	padding: 2px 6px 3px 6px;
+	display: inline-block;
+	margin: 0;
+}
+
+.trow {
+	display: table-row;
+}
+
+.tcell {
+	display: table-cell;
+}
+
+.cblab {
+	background-color: rgba(0, 0, 0, 0);
+	font-weight: normal;
+	display: table-cell;
+}
+
+.posthead, .errposthead {
+	font-weight: bold;
+	margin-bottom: 0;
+	border-bottom: none;
+	border-radius: 6px 6px 0 0;
+	color: white;
+	background-color: #555555;
+	padding: 3px 6px 3px 6px;
+	margin-top: 15px;
+}
+
+.errposthead {
+	background-color: red;
+}
+
+.separator {
+	width: 100%;
+	height: 25px;
+}
+
+.pseparator {
+	width: 100%;
+	height: 40px;
+}
+
+.postdiv, .postdivnobut {
+	border: 1px solid #555555;
+	background-color: white;
+	color: black;
+	padding: 3px;
+}
+
+.postdivnobut {
+	border-radius: 0 0 6px 6px;
+}
+
+.fullheight {
+	min-height: 90vh;
+}
+
+hr {
+	height: 1px;
+	background-color: #555555;
+	color: #555555;
+	border: none;
+}
+
+.debug {
+	width: 100%;
+	font-size: 10pt;
+	padding: 2mm;
+}
+
+#pmonitor {
+	display: none;
+}
+
+#pstatus {
+	color: white;
+	font-size: 10pt;
+	padding: 3px;
+	border: 1px solid white;
+	border-radius: 4px;
+	margin-bottom: 1mm;
+}
+
+#ppercenv {
+	padding: 3px;
+	border: 1px solid white;
+	border-radius: 4px;
+	margin-bottom: 1mm;
+}
+
+#pperc {
+	height: 3mm;
+	background-color: green;
+	width: 0;
+	border-radius: 2px;
+}
+
+#plog {
+	font-size: 10pt;
+	border-radius: 4px;
+	border: 1px solid white;
+	color: white;
+	height: 120px;
+	padding: 3px;
+	overflow-y: scroll;
+}
+
+#footer, #almfooter {
+	width: 100%;
+	text-align: center;
+	font-size: 9pt;
+	margin: 3mm 0 0 0;
+}
+
+#almfooter {
+	font-size: 10.5pt;
+}
+
+@media only screen and (max-width:10cm) {
+	body, .input, .inputx, .textarea, .button, .postbutton, .halfbutton, .copybutton, .output, .outputnobb, .outputli, .posthead, .lastborder, fieldset {
+		font-size: 11pt;
+	}
+	#pstatus, #plog, .debug {
+		font-size: 9pt;
+	}
+	#footer {
+		font-size: 8.5pt;
+	}
+	#almfooter {
+		font-size: 9.5pt;
+	}
+}

BIN
imgs/icon-16.png


BIN
imgs/icon-180.png


BIN
imgs/icon-192.png


BIN
imgs/icon-32.png


BIN
imgs/icon-512.png


BIN
imgs/icon_close.png


BIN
imgs/ogimage.png


File diff suppressed because it is too large
+ 455 - 0
index.php


+ 26 - 0
js/main.js

@@ -0,0 +1,26 @@
+/*function copytext(elid) {
+	window.getSelection().selectAllChildren(document.getElementById(elid));
+	document.execCommand('copy');
+}*/
+function copytext(index) {
+	navigator.clipboard.writeText(document.getElementById('post_'+index).value);
+	var notif=document.getElementById('notif');
+	notif.textContent='Post '+(index+1)+' text was successfully copied into your clipboard :-)';
+	notif.style.display='block';
+}
+
+function repments(match,p1,p2) {
+	return p1+p2;
+}
+
+function replinks(match,p1) {
+	return p1+'UUUUUUUUUUUUUUUUUUUUUUU';
+}
+
+function mastlength(cont) {
+	const mentre=/(^|\W)(@[a-zA-Z0-9_]+)@(([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\.)+([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\b/g;
+	const linkre=new RegExp(`(^|\\W)https?://(([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\\.)+(${tlds.join('|')})(/\\S*[\\w/=_\-])?`,'g');
+	cont=cont.replace(mentre,repments);
+	cont=cont.replace(linkre,replinks);
+	return cont.length;
+}

+ 8 - 0
lib/booltostr.php

@@ -0,0 +1,8 @@
+<?php
+function booltostr($bool,$true='true',$false='false') {
+	if ($bool)
+		return($true);
+	else
+		return($false);
+}
+?>

+ 36 - 0
lib/ckratelimit.php

@@ -0,0 +1,36 @@
+<?php
+function ckratelimit($headers,$echofun,$onlyret=false,$verbose=false) {//$echofun has to be the name of a defined function to pass messages to
+	$ret=null;
+	if (is_array($headers)) {
+		//echo "ckratelimit: {$headers}: ".print_r($headers,true));
+		$buff=[];
+		array_shift($headers);
+		foreach ($headers as $header)
+			if (preg_match('/^([^:]+):(.*)$/Uu',$header,$matches)===1)
+				$buff[$matches[1]]=trim($matches[2]);
+		$headers=$buff;
+		//print_r($headers);
+		if (isset($headers['Date']) && isset($headers['X-RateLimit-Reset']) && isset($headers['X-RateLimit-Remaining'])) {
+			//Wed, 30 Mar 2022 21:27:22 GMT
+			$srvnow=strtotime($headers['Date']);
+			//2022-03-31T04:05:00.058705Z
+			$srvrlreset=strtotime($headers['X-RateLimit-Reset']);
+			$srvrlremain=$headers['X-RateLimit-Remaining'];
+			$secstoreset=$srvrlreset-$srvnow;
+			$ret=['remaining'=>$srvrlremain,'secstoreset'=>$secstoreset];
+			if ($onlyret)
+				return $ret;
+			if ($verbose) $echofun("ckratelimit: X-RateLimit-Remaining: {$srvrlremain}; server time: {$srvnow}: ".gmdate('c',$srvnow).'; X-RateLimit-Reset: '.gmdate('c',$srvrlreset).'; current seconds before reset: '.$secstoreset.".\n");
+			if ($srvrlremain==0) {
+				$echofun("Reached rate limit, waiting {$secstoreset} seconds for rate limit reset ...\n");
+				sleep($secstoreset);
+			}
+		} else {
+			if ($verbose) $echofun("ckratelimit: no «Date» / «X-RateLimit-Reset» / «X-RateLimit-Remaining» header(s)!\n");
+		}
+	} else {
+		if ($verbose) $echofun("ckratelimit: headers is not an array!\n");
+	}
+	return $ret;
+}
+?>

+ 20 - 0
lib/getfirstbrowserlang.php

@@ -0,0 +1,20 @@
+<?php
+function getfirstbrowserlang($default='en') {
+	if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+		$langs=[];
+		$buff=explode(',',$_SERVER['HTTP_ACCEPT_LANGUAGE']);
+		foreach ($buff as $ent) {
+			$ent=trim($ent);
+			$ent=explode(';',$ent);
+			(count($ent)<2) ? $ent[1]=1 : $ent[1]=preg_replace('/^q=/','',$ent[1])+0;
+			$ent[0]=locale_canonicalize($ent[0]);
+			$langs[$ent[0]]=$ent[1];
+		}
+		arsort($langs);
+		$blang=array_key_first($langs);
+		return $blang;
+	} else {
+		return $default;
+	}
+}
+?>

File diff suppressed because it is too large
+ 3 - 0
lib/gettlds.php


+ 50 - 0
lib/ght.php

@@ -0,0 +1,50 @@
+<?php
+
+function ght($ts,$fa=null,$sd=2) {
+/*
+$ts is seconds (can be float)
+if not null, $fa has to be an array defining the output suffixes, see below
+	its default ;-)
+$sd is how many decimals to put after a dot after seconds (can be 0)
+*/
+	if ($fa==null)
+		$fa=[' year, ',' years, ',' week, ',' weeks, ',' day, ',' days, ',' hour, ',' hours, ',' minute, ',' minutes, ',' second',' seconds'];
+	$out='';
+	$i=0;
+// years
+	$x=floor($ts/31536000);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*31536000;
+	$i+=2;
+// weeks
+	$x=floor($ts/604800);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*604800;
+	$i+=2;
+// days
+	$x=floor($ts/86400);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*86400;
+	$i+=2;
+// hours
+	$x=floor($ts/3600);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*3600;
+	$i+=2;
+// minutes
+	$x=floor($ts/60);
+	if ($x>0)
+		($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	$ts=$ts-$x*60;
+	$i+=2;
+// seconds
+	$x=round($ts,$sd);
+	($x==1) ? $out.=$x.$fa[$i] : $out.=$x.$fa[$i+1];
+	return $out;
+}
+
+?>

+ 282 - 0
lib/mastodon.php

@@ -0,0 +1,282 @@
+<?php
+
+// Warning: postlength function requires $retlds global variable to be defined,
+// it has to be a reverse ordered list of "|" separated valid tlds, you can
+// require gettlds.php in the calling script and use it to set it, like this:
+// $retlds=gettlds(); $retlds=implode('|',$retlds);
+
+function validtoken($token) {
+	if (preg_match('#^[A-Za-z0-9_-]{43}$#',$token)===1)
+		return true;
+	else
+		return false;
+}
+
+function mastreq($context,$host,$endpoint) {
+	$context=stream_context_create($context);
+	$endpoint="https://{$host}{$endpoint}";
+	$res=@file_get_contents($endpoint,false,$context);
+	if ($res===false)
+		return ['ok'=>false,'error'=>"could not connect to «{$host}»",'headers'=>null];
+	$res=@json_decode($res,true);
+	if (is_null($res))
+		return ['ok'=>false,'error'=>"could not decode JSON data from «{$endpoint}» (".json_last_error().': '.json_last_error_msg().")",'headers'=>$http_response_header];
+	if (isset($res['error']))
+		return ['ok'=>false,'error'=>lcfirst($res['error']),'headers'=>$http_response_header];
+	/*print_r($http_response_header);
+	preg_match('#^\S+\s+(\S+)\s+(\S+)#',$http_response_header[0],$matches);
+	print_r($matches);
+	$httpcode=$matches[1]+0;
+	$httpcodetext=$matches[2];
+	if (($httpcode>=400 && $httpcode<=499) || ($httpcode>=500 && $httpcode<=599))
+		return ['ok'=>false,'error'=>"HTTP error: {$httpcodetext}"];*/
+	return ['ok'=>true,'data'=>$res,'headers'=>$http_response_header];
+}
+
+function mastget($host,$token,$endpoint,$timeout) {
+	$context=[
+		'http'=>[
+			'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
+			'method'=>'GET',
+			'ignore_errors'=>true,
+			'timeout'=>$timeout
+		]
+	];
+	if (!is_null($token))
+		$context['http']['header'].="Authorization: Bearer {$token}\r\n";
+	$res=mastreq($context,$host,$endpoint);
+	return $res;
+}
+
+function mastpost($host,$token,$endpoint,$content,$timeout) {
+	$content=http_build_query($content);
+	$context=[
+		'http'=>[
+			'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
+			'method'=>'POST',
+			'ignore_errors'=>true,
+			'content'=>$content,
+			'timeout'=>$timeout
+		]
+	];
+	if (!is_null($token))
+		$context['http']['header'].="Authorization: Bearer {$token}\r\n";
+	$res=mastreq($context,$host,$endpoint);
+	return $res;
+}
+
+function mastpostfile($host,$token,$endpoint,$content,$timeout) {
+	$content=http_build_query($content);
+	$context=[
+		'http'=>[
+			'header'=>"Content-type: multipart/form-data;boundary=\"boundary\"\r\nAccept: application/json\r\n",
+			'method'=>'POST',
+			'ignore_errors'=>true,
+			'content'=>$content,
+			'timeout'=>$timeout
+		]
+	];
+	if (!is_null($token))
+		$context['http']['header'].="Authorization: Bearer {$token}\r\n";
+	$res=mastreq($context,$host,$endpoint);
+	return $res;
+}
+
+function mastdel($host,$token,$endpoint,$timeout) {
+	$context=[
+		'http'=>[
+			'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
+			'method'=>'DELETE',
+			'ignore_errors'=>true,
+			'timeout'=>$timeout
+		]
+	];
+	if (!is_null($token))
+		$context['http']['header'].="Authorization: Bearer {$token}\r\n";
+	$res=mastreq($context,$host,$endpoint);
+	return $res;
+}
+
+/*
+some endpoints
+	get
+		auth required
+			verify app creds and get app info: /api/v1/apps/verify_credentials
+			verify user creds and get account info: /api/v1/accounts/verify_credentials
+			get a post: /api/v1/statuses/[id]
+	post
+		auth required
+			post a status: /api/v1/statuses
+			send follow request to an account: /api/v1/accounts/[id]/follow
+			unfollow an account: /api/v1/accounts/[id]/unfollow
+*/
+
+function splitpost($post,$avchars,$cw,$pre,$cntup) {
+	// decided use $matches[1] instead of $matches[0]
+	// to stay safe, $avchars should be at least 30 (didn't test with less);
+	// $pre can be used to list recipients (in this case it has to end with
+	// a "\n" or " "), or for anything else
+	$post=preg_replace('#[ \t\f\r]+\n#',"\n",$post);
+	$post=rtrim($post);
+	$postrlen=strlen($post);
+	$postlen=postlength($post);
+	$cwlen=mb_strlen($cw,'UTF-8');
+	$prelen=postlength($pre);
+	if ($postlen+$prelen+$cwlen<=$avchars)
+		return [['cw'=>$cw,'post'=>$pre.$post,'mastlen'=>$postlen+$prelen+$cwlen]];
+	// there is no way to know the total of posts before splitting, and its
+	// string length modifies the total, so we roughly estimate it very
+	// cautiosly to the decrease, just to spare cycles
+	$tot='';
+	$gtot=ceil($postlen/($avchars-7-2-$prelen-$cwlen));// "7" is the min length of the counter ("\n\n[x/x]"); 2 counts for start and end "…"
+	for ($i=0; $i<strlen($gtot); $i++)
+		$tot.='x';
+	$c=0;
+	while (true) {
+		$c++;
+		$totlen=strlen($tot);
+		$spost=[];
+		$buf='';
+		$off=0;
+		$i=1;
+		while (true) {
+			//echo "========================\n";
+			if (strlen($i)>$totlen) break;// do another cycle
+			$cnt="__[{$i}/{$tot}]";
+			//$lastcons=substr($post,$off,40);
+			preg_match('#(\S+)(\s+|$)#',$post,$matches,0,$off);
+			//var_dump($matches);
+			if (count($matches)==0) {// done, last post
+				$spost[]=['cw'=>$cw,'post'=>rtrim($buf)];
+				break 2;
+			}
+			$offadd=strlen($matches[0]);
+			($off+$offadd>=$postrlen) ? $dotsaddlen=0 : $dotsaddlen=2;// if we are on the last word, we don't add "…"
+			if ($prelen+$cwlen+postlength($buf.$matches[1].$cnt)+$dotsaddlen>$avchars) {// if current match would make buf+overhead overcome avchars
+				//echo "LONGMATCH: «$matches[0]»\n";
+				$nxcntlen=$totlen+strlen($i+1)+5;// next cnt may be different, so we precalc its length
+				($i==1 || $dotsaddlen==0) ? $nxdotsaddlen=2 : $nxdotsaddlen=4;// if we are on first or last post, we add 1 "…"; otherwise we add 2
+				if ($prelen+$cwlen+postlength($matches[1])+$nxcntlen+$nxdotsaddlen>$avchars) {// if next match+overhead is by itself longer than avchars
+					//echo "BLOCKMATCH: «$matches[0]»\n";
+					//$len=$avchars-$nxcntlen-$prelen-$nxdotsaddlen;
+					$len=$avchars-postlength($buf.$cnt)-$prelen-$cwlen-$dotsaddlen;
+					if ($len>0) {
+						// deactivate possible links because they will be broken
+						$matches[0]=preg_replace('#^http(s)?://#','zttp$1://',$matches[0]);
+						$matches[0]=preg_replace('#^@([a-zA-Z0-9_]+@[a-z0-9-]+)#','+$1',$matches[0]);
+						$matches[0]=mb_substr($matches[0],0,$len,'UTF-8');
+						//echo "SUBSTRING: «$matches[0]»\n";
+						$offadd=strlen($matches[0]);
+						//echo "{$matches[0]}: OFF: {$off}; OFFADD: {$offadd}\n";
+						$buf.=$matches[0];
+						$matches[0]='';
+					}
+				}
+				$spost[]=['cw'=>$cw,'post'=>rtrim($buf).' …'];
+				$buf='… ';
+				$i++;
+			}/* else {
+				echo "NORMATCH: «$matches[0]»\n";
+			}*/
+			$buf.=$matches[0];
+			$off+=$offadd;
+		}
+		$tot.='x';
+	}
+	//echo '<pre>'.print_r($spost,true).'</pre>';
+	if ($cntup)
+		foreach ($spost as $key=>$post) {
+			$spost[$key]['post']="{$pre}[".($key+1)."/{$i}]\n\n{$post['post']}";
+			$spost[$key]['mastlen']=postlength($spost[$key]['post'])+$cwlen;
+		}
+	else
+		foreach ($spost as $key=>$post) {
+			$spost[$key]['post']="{$pre}{$post['post']}\n\n[".($key+1)."/{$i}]";
+			$spost[$key]['mastlen']=postlength($spost[$key]['post'])+$cwlen;
+		}
+	//echo "CYCLES: {$c}\n";
+	//echo "LASTCONS: {$lastcons}\n";
+	return $spost;
+}
+
+function postlength($post) {
+	global $retlds;
+//	echo "-A-> |{$post}|\n";
+	// for some reason, mastodon seems to check tld existence only on http(s) links - see next regexp
+	$res=preg_replace('#(^|\W)(@[a-zA-Z0-9_]+)@(([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\.)+([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\b#u', '$1$2', $post);
+	if (!is_null($res)) $post=$res;
+//	$res=preg_replace('#(^|\W)https?://(([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\.)+([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}(/\S*[\w=?_-])?#u', '$1HTTP://UUUUUUUUUUUUUUUU', $post);
+	// on http(s) links mastodon checks if tld exists...
+	$res=preg_replace('#(^|\W)https?://(([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\.)+('.$retlds.')(/\S*[\w/=_\-])?#u', '$1UUUUUUUUUUUUUUUUUUUUUUU', $post);
+	if (!is_null($res)) $post=$res;
+//	echo "-B-> |{$post}|\n";
+	return mb_strlen($post,'UTF-8');
+}
+
+// this function requires these to be defined:
+// - an "evhandle" function to handle events
+// - an "eecho" function to handle output
+// - a "$doshut" global variable and a "shutdown" function that, since it's placed in secure places, can be used eg to safely shut down the program when "$doshut" is set to true by eg a function bound to a signal, like pcntl_signal(SIGTERM,'sighandler')
+// see ocrbot for an example
+function evlisten($host,$port,$endpoint,$token,$timeout) {
+	global $doshut;
+	while (true) {
+		shutdown($doshut);
+		$dispurl="tls://{$host}:{$port}";
+		eecho(1,"trying to connect to «{$dispurl}».");
+		$sh=@fsockopen("tls://{$host}",$port,$errno,$errstr,$timeout);
+		if ($sh===false) {
+			eecho(3,"could not connect to «{$dispurl}»: {$errstr} ({$errno}); will try again in 1 second.");
+			sleep(1);
+		} else {
+			//stream_set_blocking($sh,false);
+			stream_set_timeout($sh,1,0);
+			eecho(1,"succesfully connected to «{$dispurl}».");
+			$req="GET {$endpoint} HTTP/1.1\r\nHost: {$host}\r\nUser-Agent: a_bot\r\nAuthorization: Bearer {$token}\r\n\r\n";
+			if (fwrite($sh,$req)===false) {
+				eecho(3,"could not subscribe to user notifications on «{$dispurl}»; will try again in 1 second.");
+				fclose($sh);
+				unset($sh);// this is because shutdown can check if $sh is set and if it is, try to fclose it
+				sleep(1);
+			} else {
+				eecho(1,"listening for user notifications on «{$dispurl}».");
+				//$lc=0;
+				while (!feof($sh)) {
+					shutdown($doshut);
+					//$lc++;
+					$line=rtrim(fgets($sh),"\r\n");
+					//echo "{$lc}> {$line}\n";
+					if (preg_match('#^event: #',$line)===1) {
+						$event=['type'=>preg_replace('#^event: #','',$line),'data'=>''];
+						$line=rtrim(fgets($sh),"\r\n");
+						//echo "{$lc} DATA> {$line}\n";
+						if (preg_match('#^data: #',$line)===1) {
+							$event['data'].=preg_replace('#^data: #','',$line);
+							while ($line!='') {
+								$line=rtrim(fgets($sh),"\r\n");
+								if ($line=='') break;
+								//echo "{$lc} LENGTH> {$line}\n";
+								$line=rtrim(fgets($sh),"\r\n");
+								//echo "{$lc} DATA> {$line}\n";
+								$event['data'].=$line;
+							}
+							$event['data']=@json_decode($event['data'],true);
+							if ($event['data']===false) {
+								eecho(2,"could not decode data for event of type «{$event['type']}».");
+							} else {
+								//print_r($event);
+								evhandle($event);
+							}
+						}
+					}
+				}
+				fclose($sh);
+				unset($sh);// this is because shutdown can check if $sh is set and if it is, try to fclose it
+				eecho(3,"lost connection to «{$dispurl}»; will try reconnecting in 1 second.");
+				sleep(1);
+			}
+		}
+	}
+}
+
+?>

+ 60 - 0
post.php

@@ -0,0 +1,60 @@
+<?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/>.
+*/
+
+require 'lib/mastodon.php';
+require 'lib/ckratelimit.php';
+
+$resp=[
+	'ok'=>false,
+	'error'=>null,
+	'remaining'=>null,
+	'secstoreset'=>null,
+	'id'=>null
+];
+
+if (isset($_COOKIE['verbose_host']) && isset($_COOKIE['verbose_token']) && isset($_POST['visibility']) && in_array($_POST['visibility'],['public','unlisted','private','direct']) && isset($_POST['language']) && isset($_POST['status'])) {
+	$timeout=5;
+	$res=mastpost($_COOKIE['verbose_host'],$_COOKIE['verbose_token'],'/api/v1/statuses',$_POST,$timeout);
+	//$res=['ok'=>true, 'data'=>['id'=>999], 'error'=>'server exploded'];// test
+	if ($res['ok']) {
+		$resp['ok']=true;
+		$resp['id']=$res['data']['id'];
+		$rls=ckratelimit($res['headers'],'necho',true,false);
+		//$rls=['remaining'=>20,'secstoreset'=>5];// test
+		if (!is_null($rls)) {
+			$resp['remaining']=$rls['remaining'];
+			$resp['secstoreset']=$rls['secstoreset'];
+		}
+	} else {
+		$resp['error']=htmlentities($res['error']);
+	}
+} else {
+	$resp['error']='malformed POST request';
+}
+
+header('Content-Type: application/json');
+
+$resp=json_encode($resp);
+echo $resp;
+
+//echo '<pre>'.print_r($_POST,true).'</pre>';
+
+function necho($msg) {
+	// do nothing :-)
+}
+
+?>

Some files were not shown because too many files changed in this diff