commit 6c38b489ffe26457674afbc53880c3d9369c849f Author: pezcurrel Date: Mon Nov 20 12:42:57 2023 +0100 First commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..9511c32 --- /dev/null +++ b/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=Home +``` + +## 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). diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..023bdbe --- /dev/null +++ b/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; + } +} diff --git a/imgs/icon-16.png b/imgs/icon-16.png new file mode 100644 index 0000000..dcaf64a Binary files /dev/null and b/imgs/icon-16.png differ diff --git a/imgs/icon-180.png b/imgs/icon-180.png new file mode 100644 index 0000000..2cfcf48 Binary files /dev/null and b/imgs/icon-180.png differ diff --git a/imgs/icon-192.png b/imgs/icon-192.png new file mode 100644 index 0000000..5a29297 Binary files /dev/null and b/imgs/icon-192.png differ diff --git a/imgs/icon-32.png b/imgs/icon-32.png new file mode 100644 index 0000000..e688e2e Binary files /dev/null and b/imgs/icon-32.png differ diff --git a/imgs/icon-512.png b/imgs/icon-512.png new file mode 100644 index 0000000..7495e82 Binary files /dev/null and b/imgs/icon-512.png differ diff --git a/imgs/icon_close.png b/imgs/icon_close.png new file mode 100644 index 0000000..a8f910a Binary files /dev/null and b/imgs/icon_close.png differ diff --git a/imgs/ogimage.png b/imgs/ogimage.png new file mode 100644 index 0000000..1402dfb Binary files /dev/null and b/imgs/ogimage.png differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..3f3cfb5 --- /dev/null +++ b/index.php @@ -0,0 +1,847 @@ +. +*/ + +// Logorrh, Verbose, Charliero, Tashugo? + +require 'lib/gettlds.php'; +$tlds=gettlds(); +$retlds=implode('|',$tlds); +$jstlds="const tlds=['".implode("', '",$tlds)."'];\n"; +if (@file_put_contents('js/tlds.js',$jstlds)===false) + dieyoung('Error: could not save «js/tlds.js»'); +unset($tlds,$jstlds); +require 'lib/mastodon.php'; +require 'lib/ckratelimit.php'; +require 'lib/getfirstbrowserlang.php'; +require 'lib/ght.php'; +require 'lib/booltostr.php'; + +const SNAME='Verbose'; +const SVERS='0.3.5'; +const SREPO='https://git.lattuga.net/pongrebio/verbose'; +const DEFAC=500; +const MINAC=100; +const MAXAC=100000; +const MINCL=40; +const MAXPOSTC=50000; +const MAXREPTOLEN=1500; +const POSTPAUSE=250;// pause between sending split posts (in milliseconds) +const REQSTOPRESV=10;// requests to preserve +const CONFFN='conf.ini'; + +$sname=SNAME; +$svers=SVERS; +$now=time(); +$timeout=5; +$formact=preg_replace('#[^/]+$#','',$_SERVER['REQUEST_URI']); + +$dodebug=false; +$debug=''; + +ini_set('max_execution_time',360);// 6 minutes: default mastodon ratelimits: https://docs.joinmastodon.org/api/rate-limits/ +$conf=['link'=>"\n\n[This post was split using ".SREPO.']']; +if (file_exists(CONFFN)) { + $fconf=@parse_ini_file(CONFFN,false,INI_SCANNER_RAW); + if ($fconf===false) { + dieyoung('Error: configuration file «'.CONFFN.'» was found but could not be opened for reading.'); + } else { + if (isset($fconf['link'])) + $conf['link']="\n\n{$fconf['link']}"; + if (isset($fconf['footer'])) + $conf['footer']=$fconf['footer']; + if (isset($fconf['webservertimeout'])) { + if (preg_match('#^\d+$#',$fconf['webservertimeout'])===1) + $conf['webservertimeout']=$fconf['webservertimeout']+0; + else + dieyoung('Error: configuration file «'.CONFFN.'»: value «'.$fconf['webservertimeout'].'» is not valid for «webservertimeout».'); + } else { + dieyoung('Error: configuration file «'.CONFFN.'» does not define «webservertimeout».'); + } + } + unset($fconf); +} else { + dieyoung('Error: configuration file «'.CONFFN.'» was not found.'); +} +$debug.='CONF: '.preprint($conf)."
\n"; + +$coopts=[ + 'expires'=>$now+365*24*60*60, + 'path'=>preg_replace('#[^/]+$#','',$_SERVER['REQUEST_URI']), + 'domain'=>$_SERVER['SERVER_NAME'], + 'secure'=>false, + 'httponly'=>false, + 'samesite'=>'Lax'// None || Lax || Strict +]; +$cooptsx=$coopts; +$cooptsx['expires']=$now-3600; + +/*setcookie('verbose_client_id','',$cooptsx); +setcookie('verbose_client_secret','',$cooptsx); +setcookie('verbose_host','',$cooptsx); +setcookie('verbose_token','',$cooptsx);*/ + +$debug.='COOKIES: '.preprint($_COOKIE); +$debug.='POST: '.preprint($_POST); + +$blang=getfirstbrowserlang(); +$blang=preg_replace('#^([a-z]+).*#','$1',$blang); + +$host=false; +$token=false; +(isset($_SERVER['HTTPS'])) ? $proto='https' : $proto='http'; +$rediruri=$proto.'://'.$_SERVER['HTTP_HOST'].preg_replace('#^([^?]*).*#','$1',$_SERVER['REQUEST_URI']); +$authmsgs=''; + +$scope='read write:statuses'; +$provided='you provided'; + +setcookie('verbose_client_id','',$cooptsx); +setcookie('verbose_client_secret','',$cooptsx); + +(isset($_POST['host'])) ? $_POST['host']=trim($_POST['host']) : $_POST['host']=''; +(isset($_POST['token'])) ? $_POST['token']=trim($_POST['token']) : $_POST['token']=''; + +if (isset($_GET['code']) && isset($_COOKIE['verbose_host']) && isset($_COOKIE['verbose_client_id']) && isset($_COOKIE['verbose_client_secret'])) { + $host=$_COOKIE['verbose_host']; + $postcont=['grant_type'=>'authorization_code', 'code'=>$_GET['code'], 'client_id'=>$_COOKIE['verbose_client_id'], 'client_secret'=>$_COOKIE['verbose_client_secret'], 'redirect_uri'=>$rediruri, 'scope'=>$scope]; + $res=mastpost($host,null,'/oauth/token',$postcont,$timeout); + if ($res['ok']) { + $res=$res['data']; + $token=$res['access_token']; + setcookie('verbose_token',$token,$coopts); + $authmsgs.="
You successfully authorized {$sname} to use your account on {$host}.
\nYour access token is {$token}
\nPlease save it somewhere safe, like in a password manager (i suggest KeePassXC), because {$sname} does not save it on the server, but only in a cookie in your browser that lasts one year since your last visit, so it may expire or you may loose it and, without your access token, you would have to repeat the authorization process.
\n"; + } else { + $authmsgs.='
Sorry, there was an error trying to authorize you: '.htmlentities($res['error'])."
\n"; + } +} elseif (isset($_POST['act']) && $_POST['act']=='login' && $_POST['host']!='' && $_POST['token']=='') { + $host=$_POST['host']; + $postcont=['client_name'=>'Verbose','redirect_uris'=>$rediruri,'scopes'=>$scope,'website'=>'https://git.lattuga.net/pongrebio/verbose']; + $res=mastpost($host,$token,'/api/v1/apps',$postcont,$timeout); + //$debug.='AUTH RESULTS: '.preprint($res); + if ($res['ok']) { + $res=$res['data']; + setcookie('verbose_host',$host,$coopts); + setcookie('verbose_client_id',$res['client_id'],$coopts); + setcookie('verbose_client_secret',$res['client_secret'],$coopts); + $location='https://'.$host.'/oauth/authorize?client_id=' . $res['client_id'] . '&redirect_uri=' . urlencode($rediruri) . '&response_type=code&scope='. urlencode($postcont['scopes']) . '&force_login=0&lang='.$blang; + if ($debug) + header('Location: '.$location,true,302); + else + echo "".htmlentities($location)."
\n"; + exit(0); + } else { + $authmsgs.='
Sorry, there was an error trying to authorize you: '.htmlentities($res['error'])."
\n"; + } +} elseif (isset($_POST['act']) && $_POST['act']=='login' && $_POST['host']!='' && $_POST['token']!='') { + $host=$_POST['host']; + $token=$_POST['token']; +} elseif (isset($_POST['act']) && $_POST['act']=='logout') { + setcookie('verbose_host','',$cooptsx); + setcookie('verbose_token','',$cooptsx); + $_COOKIE=[]; + //$authmsgs.="
You have been successfully logged out :-)
\n"; +} elseif (isset($_COOKIE['verbose_host']) && isset($_COOKIE['verbose_token'])) { + $host=$_COOKIE['verbose_host']; + $token=$_COOKIE['verbose_token']; + $provided='provided by your cookies'; +} + +(isset($_POST['round'])) ? $_POST['round']++ : $_POST['round']=0; + +$loggedin=false; +if ($host!==false && $token!==false && trim($token)!='') {// trim($token)!='' is to keep it usable without js + $res=mastget($host,$token,'/api/v1/apps/verify_credentials',$timeout); + //$debug.='TOKEN: '.$token."
\nRES:".preprint($res); + if ($res['ok']) { + $myacc=mastget($host,$token,'/api/v1/accounts/verify_credentials',$timeout); + //$debug.=preprint($myacc); + if ($myacc['ok']) { + setcookie('verbose_host',$host,$coopts); + setcookie('verbose_token',$token,$coopts); + $myacc=$myacc['data']; + $loggedin=true; + if ($_POST['round']==0 || $_POST['act']=='login') { + $res=mastget($host,$token,'/api/v1/instance',$timeout); + if ($res['ok'] && isset($res['data']['configuration']['statuses']['max_characters']) && preg_match('#^\d+$#',$res['data']['configuration']['statuses']['max_characters'])===1) { + $avchars=$res['data']['configuration']['statuses']['max_characters']+0; + } else { + $authmsgs.="
Sorry, {$sname} could not detect how many characters per post your instance allows.
\n"; + } + } + } else { + $authmsgs.='
Sorry, verification of your account’s credentials failed (error: '.htmlentities($myacc['error']).")
\n"; + setcookie('verbose_host','',$cooptsx); + setcookie('verbose_token','',$cooptsx); + } + } else { + $authmsgs.="
Sorry, authentication with the credentials {$provided} failed (error: ".htmlentities($res['error']).")
\n"; + setcookie('verbose_host','',$cooptsx); + setcookie('verbose_token','',$cooptsx); + } +} + +$splitmsgs=''; + +if (isset($avchars)) { + $_POST['avchars']=$avchars; +} elseif (isset($_POST['avchars'])) { + //if (!$loggedin) $authmsgs='
Warning: if you authorize now you’ll loose your current split session
'; + (preg_match('#^\d+$#',$_POST['avchars'])===1) ? $_POST['avchars']+=0 : $_POST['avchars']=DEFAC; +} else { + $_POST['avchars']=DEFAC; +} +if ($_POST['avchars']MAXAC) $_POST['avchars']=MAXAC; +$maxcwprec=$_POST['avchars']-MINCL; +if (!isset($_POST['cw']) || trim($_POST['cw'])=='') $_POST['cw']=''; +$cwlen=mb_strlen($_POST['cw'],'UTF-8'); +if (!isset($_POST['pre']) || trim($_POST['pre'])=='') $_POST['pre']=''; +$prelen=mb_strlen($_POST['pre'],'UTF-8')+2; +$cwprelen=$cwlen+$prelen; +if ($cwprelen>$maxcwprec) $splitmsgs.="
“Content Warning” + “Text to prepend” is {$cwprelen} characters long, that is ".($cwprelen-$maxcwprec)." characters longer than its maximum allowed length ({$maxcwprec} characters, that is {$_POST['avchars']} available characters minus ".MINCL." characters)
\n"; +//$_POST['pre']=mb_substr($_POST['pre'],0,$maxprec,'UTF-8'); + +if (!isset($_POST['post']) || trim($_POST['post'])=='') $_POST['post']=''; +$postlen=mb_strlen($_POST['post'],'UTF-8'); +if ($postlen>MAXPOSTC) $splitmsgs.="
“Post to split” is {$postlen} characters long, that is ".($postlen-MAXPOSTC)." characters longer than its maximum allowed length, ".MAXPOSTC." characters
\n"; +//$_POST['post']=mb_substr($_POST['post'],0,MAXPOSTC,'UTF-8'); + +$rbsck=[ + 'cntpos_before'=>'', + 'cntpos_after'=>'', + 'addref_no'=>'', + 'addref_ifav'=>'', +]; + +if (isset($_POST['cntpos']) && $_POST['cntpos']=='before') { + $cntbef=true; + $rbsck['cntpos_before']=' checked'; +} else { + $_POST['cntpos']='after'; + $cntbef=false; + $rbsck['cntpos_after']=' checked'; +} + +if (isset($_POST['addref']) && $_POST['addref']=='no') { + $aliif=false;// "add link if it fits" + $rbsck['addref_no']=' checked'; +} else { + $_POST['addref']='ifav'; + $aliif=true; + $rbsck['addref_ifav']=' checked'; +} + +if (!isset($_POST['replyto']) || trim($_POST['replyto'])=='') $_POST['replyto']=''; +if (strlen($_POST['replyto'])>MAXREPTOLEN) { + $_POST['replyto']=''; + $splitmsgs.='
The “URL of post to reply to” you specified is too long (its maximum allowed length is '.MAXREPTOLEN." characters)
\n"; +} +if (!isset($_POST['lang']) || trim($_POST['lang'])=='') $_POST['lang']=$blang; +if (!isset($_POST['visib']) || trim($_POST['visib'])=='') $_POST['visib']='public'; + +if ($_POST['round']==0) { + $_POST['setcws']='1'; + $setcwsck=' checked'; + $_POST['setments']='1'; + $setmentsck=' checked'; + $_POST['setvisib']='1'; + $setvisibck=' checked'; + $_POST['setlang']='1'; + $setlangck=' checked'; +} else { + (isset($_POST['setcws']) && $_POST['setcws']=='1') ? $setcwsck=' checked' : $setcwsck=''; + (isset($_POST['setments']) && $_POST['setments']=='1') ? $setmentsck=' checked' : $setmentsck=''; + (isset($_POST['setvisib']) && $_POST['setvisib']=='1') ? $setvisibck=' checked' : $setvisibck=''; + (isset($_POST['setlang']) && $_POST['setlang']=='1') ? $setlangck=' checked' : $setlangck=''; +} + +function getpostmentions($post,$exclude,$format) { + $ments=[]; + if (isset($post['account']['acct']) && !in_array($post['account']['acct'],$exclude)) + $ments[]=$post['account']['acct']; + if (isset($post['mentions']) && is_array($post['mentions'])) + foreach ($post['mentions'] as $ment) + if (isset($ment['acct']) && !in_array($ment['acct'],$exclude) && !in_array($ment['acct'],$ments)) + $ments[]=$ment['acct']; + if ($format=='array') + return $ments; + elseif ($format=='list') + return implode(' ',$ments); + elseif ($format=='recipients') + return '@'.implode(' @',$ments); +} + +$reptoid=''; + +if ($loggedin && $_POST['replyto']!='') { + //https://livellosegreto.it/@geranio/110627337076323759 + $res=mastget($host,$token,'/api/v2/search?type=statuses&resolve=1&limit=1&q='.urlencode($_POST['replyto']),$timeout); + $debug.='accsearch res:
'.preprint($res)."
\n"; + if ($res['ok']) { + if (isset($res['data']['statuses'][0]['id'])) + $reptopost=$res['data']['statuses'][0]; + else + $splitmsgs.="
Sorry, {$sname} found no post with the URL you provided as “URL of post to reply to”, please check it and make sure it points to a post you have access to.
\n"; + } else { + $splitmsgs.="
Sorry, {$sname} encountered the following error while trying to retrieve the URL you provided as “URL of post to reply to”: ".htmlentities($res['error'])."; please check the URL and make sure it points to a post you have access to.
\n"; + } + if (isset($reptopost)) { + if (isset($reptopost['id'])) + $reptoid=$reptopost['id']; + if (isset($_POST['setcws']) && $_POST['setcws']=='1' && isset($reptopost['spoiler_text']) && trim($reptopost['spoiler_text'])!='') + $_POST['cw']=$reptopost['spoiler_text']; + if (isset($_POST['setments']) && $_POST['setments']=='1' && isset($reptopost['mentions'])) + $_POST['pre']=getpostmentions($reptopost,[$myacc['acct']],'recipients'); + if (isset($_POST['setvisib']) && $_POST['setvisib']=='1' && isset($reptopost['visibility'])) + $_POST['visib']=$reptopost['visibility']; + if (isset($_POST['setlang']) && $_POST['setlang']=='1' && isset($reptopost['language'])) + $_POST['lang']=$reptopost['language']; + } +} + +$headmsgs=''; + +$intro="

Hello, this is a {$sname} instance, it can split your long post considering Mastodon rules: every http(s) link counts for 23 characters and every mention counts only for its username part. Each split post comes with “…” signs where it makes sense and with a counter in “[n/t]” form, where “n” is current post number and “t” is total posts number.

+

{$sname} 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 report them to me on Mastodon, or here.

\n"; + +$postmsgs=''; +$postsoffset=0; +$postscount=0; +$postingok=true; +$postwait=0; +$postwaituntil=0; +$pbtext='Post all'; + +if (isset($_POST['postsoffset']) && preg_match('#^\d+$#',$_POST['postsoffset'])===1) + $postsoffset=$_POST['postsoffset']+0; + +if ($loggedin && isset($_POST['postwaituntil']) && preg_match('#^\d+$#',$_POST['postwaituntil'])===1 && isset($_POST['postscount']) && preg_match('#^\d+$#',$_POST['postscount'])===1 && isset($_POST['act']) && $_POST['act']=='sendposts' && isset($_POST['cw']) && isset($_POST['post_'.$postsoffset]) && isset($_POST['mastlen_'.$postsoffset])) { + $postscount=$_POST['postscount']+0; + if (isset($_POST['lastsentpostid']) && $_POST['lastsentpostid']!='') + $lspostid=$_POST['lastsentpostid']; + $posts=[]; + $i=$postsoffset; + do { + $_POST['post_'.$i]=str_replace("\r\n","\n",$_POST['post_'.$i]); + $posts[$i]=['cw'=>$_POST['cw'],'post'=>$_POST['post_'.$i],'mastlen'=>$_POST['mastlen_'.$i]]; + $i++; + } while (isset($_POST['post_'.$i])); + if ($now<$_POST['postwaituntil']) { + $debug.="WAIT MORE!!!
\n"; + $postingok=false; + $postwait=$_POST['postwaituntil']-$now+1; + $postwaituntil=$_POST['postwaituntil']+1; + $postmsgs.="
Sorry, you have to wait ".ght($postwait)." more before {$sname} can post the remaining split post(s).
\n"; + $pbtext='Please wait '.ght($postwait).' before posting'; + } else { + for ($i=$postsoffset; $i<$postscount; $i++) { + if (time()-$now+1>=$conf['webservertimeout']) { + $postingok=false; + $postsoffset=$i; + $postmsgs.="
{$sname} could not post all the split posts because «{$_SERVER['SERVER_NAME']}» HTTP server was reaching its timeout (".ght($conf['webservertimeout']).'). You can send the remaining split posts '.($i+1)."-{$postscount} below.
\n"; + break; + } else { + $post=$posts[$i]; + $cont=[ + 'status'=>$post['post'], + 'visibility'=>$_POST['visib'], + 'language'=>$_POST['lang'] + ]; + if ($post['cw']!='') + $cont['spoiler_text']=$post['cw']; + if ($i==0 && $reptoid!='') + $cont['in_reply_to_id']=$reptoid; + elseif (isset($lspostid)) + $cont['in_reply_to_id']=$lspostid; + if ($i>0 && $_POST['visib']=='public') + $cont['visibility']='unlisted'; + //echo ($i+1).preprint($cont)."
\n"; + $res=mastpost($host,$token,'/api/v1/statuses',$cont,$timeout); + //if ($i==2) $res=['ok'=>false,'error'=>'server exploded'];// test err + //$res=['ok'=>true,'data'=>['id'=>'12345']];// test err + if ($res['ok']) { + $lspostid=$res['data']['id']; + $debug.="lspostid: {$lspostid}
\n"; + $rls=ckratelimit($res['headers'],'necho',true,false); + //if ($i==2) $rls=['remaining'=>0,'secstoreset'=>3895];// test err + //$rls=['remaining'=>0,'secstoreset'=>5];// test err + if (!is_null($rls)) { + if ($rls['remaining']<=REQSTOPRESV && $i<$postscount-1) { + $postingok=false; + $postwait=$rls['secstoreset']; + $postwaituntil=time()+$postwait; + $postsoffset=$i+1; + $postmsgs.='
Sending split post '.($i+1)."/{$postscount}, {$sname} reached your {$host}’s account posting rate limit, so it stopped sending; you’ll find the rest of your split posts (those which have not been sent, ".($i+2)."-{$postscount}) below; before posting them, you’ll have to wait ".ght($postwait)." for rate limit reset.
\n"; + $pbtext='Please wait '.ght($postwait).' before posting'; + break; + } + } + if ($i<$postscount-1) + usleep(POSTPAUSE*1000); + } else { + $postingok=false; + $postsoffset=$i; + $postmsgs.='
Trying to send split post '.($i+1)."/{$postscount}, {$sname} encountered the following error: «".htmlentities($res['error']).'»; so it stopped sending. You can (re)try sending split posts '.($i+1)."-{$postscount} below.
\n"; + break; + } + } + } + if ($postingok) { + $postmsgs.="
{$sname} successfully sent {$i}/{$postscount} split posts :-)
\n"; + $posts=[]; + $postscount=0; + } + } +} + +$replyto=''; +$language=''; +$displang=''; +$visibility=''; +$dispvisib=''; + +if (!$loggedin) { + $auth="

Connect?

+{$authmsgs} +
+

If you don’t want to connect {$sname} to your account, you can just skip to the “Split post” section, but after splitting your long post you’ll have to copy and paste by hand each split post in sequence into Mastodon (i recommend “chain posting”, that is replying to the first post with the second, to the second with the third, and so on).

+

If you choose instead to connect {$sname} to your account, you’ll be able to post all split posts at once directly from within {$sname}, they will be automatically “chain posted” and, before posting them, you may set their visibility, language, and a post to reply to with the first split post.

+

 

+

To connect {$sname} to your account

+
    +
  • if you already have an authentication token you just need to specify your server’s hostname and your token (your current split session will be unaffected);
  • +
  • if you have not an authentication token yet, you first have to authorize {$sname} to access your account by specifying only your server’s hostname, leaving the “Your token” field empty; then you’ll be guided through a very quick and easy one-time authorization process, at the end of which {$sname} will tell you your token for future connections, and will be already connected to your account - but please note that this process will make you loose your current split session.
  • +
+
+
+".oldpost2hid()." +
+
+
+
\n"; + $replyto=''; + $visibility=''; + $language=''; +} else { + $auth="

Connected :-)

\n{$authmsgs}\n
\n".oldpost2hid()."
{$sname} is connected to your {$myacc['acct']}@{$host} account.
\n
\n
\n"; + $replyto='
'; + $optsa=['public'=>'Public','unlisted'=>'Unlisted','private'=>'Followers-only','direct'=>'Direct']; + $buff=''; + foreach ($optsa as $key=>$val) { + if ($key==$_POST['visib']) { + $selected=' selected'; + $dispvisib=$val; + } else { + $selected=''; + } + $buff.="\n"; + } + $visibility='
'; + // taken from 'preferences' -> 'other' on mastodon 4.1.2 + $optsa=[['aa','Afar','Afaraf'],['ab','Abkhaz','аҧсуа бызшәа'],['ae','Avestan','avesta'],['af','Afrikaans','Afrikaans'],['ak','Akan','Akan'],['am','Amharic','አማርኛ'],['an','Aragonese','aragonés'],['ar','Arabic','اللغة العربية'],['as','Assamese','অসমীয়া'],['av','Avaric','авар мацӀ'],['ay','Aymara','aymar aru'],['az','Azerbaijani','azərbaycan dili'],['ba','Bashkir','башҡорт теле'],['be','Belarusian','беларуская мова'],['bg','Bulgarian','български език'],['bh','Bihari','भोजपुरी'],['bi','Bislama','Bislama'],['bm','Bambara','bamanankan'],['bn','Bengali','বাংলা'],['bo','Tibetan','བོད་ཡིག'],['br','Breton','brezhoneg'],['bs','Bosnian','bosanski jezik'],['ca','Catalan','Català'],['ce','Chechen','нохчийн мотт'],['ch','Chamorro','Chamoru'],['co','Corsican','corsu'],['cr','Cree','ᓀᐦᐃᔭᐍᐏᐣ'],['cs','Czech','čeština'],['cu','Old Church Slavonic','ѩзыкъ словѣньскъ'],['cv','Chuvash','чӑваш чӗлхи'],['cy','Welsh','Cymraeg'],['da','Danish','dansk'],['de','German','Deutsch'],['dv','Divehi','Dhivehi'],['dz','Dzongkha','རྫོང་ཁ'],['ee','Ewe','Eʋegbe'],['el','Greek','Ελληνικά'],['en','English','English'],['eo','Esperanto','Esperanto'],['es','Spanish','Español'],['et','Estonian','eesti'],['eu','Basque','euskara'],['fa','Persian','فارسی'],['ff','Fula','Fulfulde'],['fi','Finnish','suomi'],['fj','Fijian','Vakaviti'],['fo','Faroese','føroyskt'],['fr','French','Français'],['fy','Western Frisian','Frysk'],['ga','Irish','Gaeilge'],['gd','Scottish Gaelic','Gàidhlig'],['gl','Galician','galego'],['gu','Gujarati','ગુજરાતી'],['gv','Manx','Gaelg'],['ha','Hausa','هَوُسَ'],['he','Hebrew','עברית'],['hi','Hindi','हिन्दी'],['ho','Hiri Motu','Hiri Motu'],['hr','Croatian','Hrvatski'],['ht','Haitian','Kreyòl ayisyen'],['hu','Hungarian','magyar'],['hy','Armenian','Հայերեն'],['hz','Herero','Otjiherero'],['ia','Interlingua','Interlingua'],['id','Indonesian','Bahasa Indonesia'],['ie','Interlingue','Interlingue'],['ig','Igbo','Asụsụ Igbo'],['ii','Nuosu','ꆈꌠ꒿ Nuosuhxop'],['ik','Inupiaq','Iñupiaq'],['io','Ido','Ido'],['is','Icelandic','Íslenska'],['it','Italian','Italiano'],['iu','Inuktitut','ᐃᓄᒃᑎᑐᑦ'],['ja','Japanese','日本語'],['jv','Javanese','basa Jawa'],['ka','Georgian','ქართული'],['kg','Kongo','Kikongo'],['ki','Kikuyu','Gĩkũyũ'],['kj','Kwanyama','Kuanyama'],['kk','Kazakh','қазақ тілі'],['kl','Kalaallisut','kalaallisut'],['km','Khmer','ខេមរភាសា'],['kn','Kannada','ಕನ್ನಡ'],['ko','Korean','한국어'],['kr','Kanuri','Kanuri'],['ks','Kashmiri','कश्मीरी'],['ku','Kurmanji (Kurdish)','Kurmancî'],['kv','Komi','коми кыв'],['kw','Cornish','Kernewek'],['ky','Kyrgyz','Кыргызча'],['la','Latin','latine'],['lb','Luxembourgish','Lëtzebuergesch'],['lg','Ganda','Luganda'],['li','Limburgish','Limburgs'],['ln','Lingala','Lingála'],['lo','Lao','ລາວ'],['lt','Lithuanian','lietuvių kalba'],['lu','Luba-Katanga','Tshiluba'],['lv','Latvian','latviešu valoda'],['mg','Malagasy','fiteny malagasy'],['mh','Marshallese','Kajin M̧ajeļ'],['mi','Māori','te reo Māori'],['mk','Macedonian','македонски јазик'],['ml','Malayalam','മലയാളം'],['mn','Mongolian','Монгол хэл'],['mr','Marathi','मराठी'],['ms','Malay','Bahasa Melayu'],['mt','Maltese','Malti'],['my','Burmese','ဗမာစာ'],['na','Nauru','Ekakairũ Naoero'],['nb','Norwegian Bokmål','Norsk bokmål'],['nd','Northern Ndebele','isiNdebele'],['ne','Nepali','नेपाली'],['ng','Ndonga','Owambo'],['nl','Dutch','Nederlands'],['nn','Norwegian Nynorsk','Norsk Nynorsk'],['no','Norwegian','Norsk'],['nr','Southern Ndebele','isiNdebele'],['nv','Navajo','Diné bizaad'],['ny','Chichewa','chiCheŵa'],['oc','Occitan','occitan'],['oj','Ojibwe','ᐊᓂᔑᓈᐯᒧᐎᓐ'],['om','Oromo','Afaan Oromoo'],['or','Oriya','ଓଡ଼ିଆ'],['os','Ossetian','ирон æвзаг'],['pa','Panjabi','ਪੰਜਾਬੀ'],['pi','Pāli','पाऴि'],['pl','Polish','Polski'],['ps','Pashto','پښتو'],['pt','Portuguese','Português'],['qu','Quechua','Runa Simi'],['rm','Romansh','rumantsch grischun'],['rn','Kirundi','Ikirundi'],['ro','Romanian','Română'],['ru','Russian','Русский'],['rw','Kinyarwanda','Ikinyarwanda'],['sa','Sanskrit','संस्कृतम्'],['sc','Sardinian','sardu'],['sd','Sindhi','सिन्धी'],['se','Northern Sami','Davvisámegiella'],['sg','Sango','yângâ tî sängö'],['si','Sinhala','සිංහල'],['sk','Slovak','slovenčina'],['sl','Slovenian','slovenščina'],['sn','Shona','chiShona'],['so','Somali','Soomaaliga'],['sq','Albanian','Shqip'],['sr','Serbian','српски језик'],['ss','Swati','SiSwati'],['st','Southern Sotho','Sesotho'],['su','Sundanese','Basa Sunda'],['sv','Swedish','Svenska'],['sw','Swahili','Kiswahili'],['ta','Tamil','தமிழ்'],['te','Telugu','తెలుగు'],['tg','Tajik','тоҷикӣ'],['th','Thai','ไทย'],['ti','Tigrinya','ትግርኛ'],['tk','Turkmen','Türkmen'],['tl','Tagalog','Wikang Tagalog'],['tn','Tswana','Setswana'],['to','Tonga','faka Tonga'],['tr','Turkish','Türkçe'],['ts','Tsonga','Xitsonga'],['tt','Tatar','татар теле'],['tw','Twi','Twi'],['ty','Tahitian','Reo Tahiti'],['ug','Uyghur','ئۇيغۇرچە‎'],['uk','Ukrainian','Українська'],['ur','Urdu','اردو'],['uz','Uzbek','Ўзбек'],['ve','Venda','Tshivenḓa'],['vi','Vietnamese','Tiếng Việt'],['vo','Volapük','Volapük'],['wa','Walloon','walon'],['wo','Wolof','Wollof'],['xh','Xhosa','isiXhosa'],['yi','Yiddish','ייִדיש'],['yo','Yoruba','Yorùbá'],['za','Zhuang','Saɯ cueŋƅ'],['zh','Chinese','中文'],['zu','Zulu','isiZulu'],['ast','Asturian','Asturianu'],['ckb','Sorani (Kurdish)','سۆرانی'],['cnr','Montenegrin','crnogorski'],['jbo','Lojban','la .lojban.'],['kab','Kabyle','Taqbaylit'],['kmr','Kurmanji (Kurdish)','Kurmancî'],['ldn','Láadan','Láadan'],['lfn','Lingua Franca Nova','lingua franca nova'],['sco','Scots','Scots'],['sma','Southern Sami','Åarjelsaemien Gïele'],['smj','Lule Sami','Julevsámegiella'],['szl','Silesian','ślůnsko godka'],['tai','Tai','ภาษาไท or ภาษาไต'],['tok','Toki Pona','toki pona'],['zba','Balaibalan','باليبلن'],['zgh','Standard Moroccan Tamazight','ⵜⴰⵎⴰⵣⵉⵖⵜ']]; + $buff=''; + foreach ($optsa as $val) { + if ($val[0]==$_POST['lang']) { + $displang=$val[2].' ('.$val[1].')'; + $selected=' selected'; + } else { + $selected=''; + } + $buff.="\n"; + } + $language="
\n"; + unset($buff,$optsa); +} +$auth.="
\n"; + +if ($dodebug) { + $debug='
'.$debug.'
'; + $svers.='-'.rand(10000,999999); +} else { + $debug=''; +} + +(isset($_POST['act']) && $_POST['act']=='split') ? $splitfh='' : $splitfh=' class="fullheight"'; + +header('Content-Language: en'); + +echo " + + +{$sname}: a post splitter for Mastodon + + + + + + + + + + + + + + + +
Elo!
+
+
+
Uz!
+
Elo!
+
+
+{$debug} +
+{$headmsgs} +

{$sname}: a post splitter for Mastodon

+{$intro} +
+{$auth} + +

Split post

+{$splitmsgs} +
+ +
+{$replyto} +
+
+{$visibility} +{$language} +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
\n
\n
\n"; + +if ($splitmsgs=='') { + if (isset($_POST['act']) && $_POST['act']=='split' && $_POST['post']!='') { + $linklen=postlength($conf['link']); + ($_POST['pre']!='') ? $pre=$_POST['pre']."\n\n" : $pre=''; + $posts=splitpost($_POST['post'],$_POST['avchars'],$_POST['cw'],$pre,$cntbef); + $postscount=count($posts); + $errposts=[]; + for ($i=0; $i<$postscount; $i++) + if ($posts[$i]['mastlen']>$_POST['avchars']) $errposts[]="{$i}"; + $epc=count($errposts); + if ($epc>0) { + if ($epc==1) + echo "

Sorry, the length of one split post (namely {$errposts[0]}) exceeds {$_POST['avchars']} characters."; + else + echo "

Sorry, the length of {$epc} split posts (namely ".implode(', ',$errposts).") exceeds {$_POST['avchars']} characters."; + echo " Please report this bug here, possibly citing the original post text.

\n"; + } + if ($postscount>1 && $aliif && $posts[$postscount-1]['mastlen']+$linklen<=$_POST['avchars']) { + $posts[$postscount-1]['post'].=$conf['link']; + $posts[$postscount-1]['mastlen']+=$linklen; + } + } + if ($postscount>0) { + //if ($loggedin) $postmsgs.="
“Post all” button is at the bottom ;-)
\n"; + echo "
\n

Split results

\n{$postmsgs}
\n"; + $info='Content Warning: '; + ($posts[$postsoffset]['cw']!='') ? $info.=htmlentities($_POST['cw']) : $info.='not set'; + if ($loggedin) $info.='
Visibility: '.$dispvisib.'
Language: '.htmlentities($displang); + ($loggedin) ? $postdivclass='postdivnobut' : $postdivclass='postdiv'; + for ($i=$postsoffset; $i<$postscount; $i++) { + $io=$i+1; + ($posts[$i]['mastlen']>$_POST['avchars']) ? $phclass='errposthead' : $phclass='posthead'; + echo "
Post {$io}/{$postscount} (“Mastodon length”: {$posts[$i]['mastlen']}; real length: ".(mb_strlen($posts[$i]['post'],'UTF-8')+$cwlen).")
\n
{$info}
\n
".nl2br(htmlentities($posts[$i]['post']))."
\n
\n
\n"; + } + if ($loggedin) { + if (!isset($lspostid)) $lspostid=''; + echo "
\n
\n
\n
\n
\n
\n".oldpost2hid()."\n\n\n\n\n"; + } + echo "
\n"; + } elseif ($postmsgs!='') { + echo "
\n{$postmsgs}"; + } +} + +if (isset($conf['footer'])) + echo "
{$conf['footer']}
\n"; + +echo " + + +\n"; + +function preprint($var) { + return '
'.print_r($var,true)."
\n"; +} + +function necho($var) { + // do nothing :-) +} + +function dieyoung($msg) { + echo $msg; + exit(1); +} + +function cb2hid($pkey) { + if (isset($_POST[$pkey]) && $_POST[$pkey]=='1') + return '1'; + else + return '0'; +} + +function oldpost2hid() { + return "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +} + +?> diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..948c501 --- /dev/null +++ b/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; +} diff --git a/lib/booltostr.php b/lib/booltostr.php new file mode 100644 index 0000000..08bc0cd --- /dev/null +++ b/lib/booltostr.php @@ -0,0 +1,8 @@ + diff --git a/lib/ckratelimit.php b/lib/ckratelimit.php new file mode 100644 index 0000000..184bbfd --- /dev/null +++ b/lib/ckratelimit.php @@ -0,0 +1,36 @@ +$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; +} +?> diff --git a/lib/getfirstbrowserlang.php b/lib/getfirstbrowserlang.php new file mode 100644 index 0000000..37318d1 --- /dev/null +++ b/lib/getfirstbrowserlang.php @@ -0,0 +1,20 @@ + diff --git a/lib/gettlds.php b/lib/gettlds.php new file mode 100644 index 0000000..c8106f3 --- /dev/null +++ b/lib/gettlds.php @@ -0,0 +1,44 @@ +86400 || !file_exists($tldsfp)) {// if more than 1 day has passed since last list dl or list file can't be found + $url='https://data.iana.org/TLD/tlds-alpha-by-domain.txt'; + $buf=@file_get_contents($url); + if ($buf===false) + echo "gettlds: could not download «{$url}»\n"; + elseif (@file_put_contents($tldsfp,$buf)===false) + echo "gettlds: could not save «{$tldsfp}»\n"; + elseif (@file_put_contents($ldlfp,$now."\n")===false) + echo "gettlds: could not save «{$ldlfp}»\n"; + } + if (!isset($buf)) + $buf=@file_get_contents($tldsfp); + if ($buf!==false) { + $tlds=[]; + $buf=explode("\n",$buf); + foreach ($buf as $val) + if (trim($val)!=='' && $val[0]!='#') + $tlds[]=$val; + rsort($tlds); + foreach ($tlds as $key=>$val) + $tlds[$key]=strtolower($val); + } + return $tlds; +} + +?> diff --git a/lib/ght.php b/lib/ght.php new file mode 100644 index 0000000..32844ab --- /dev/null +++ b/lib/ght.php @@ -0,0 +1,50 @@ +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; +} + +?> diff --git a/lib/mastodon.php b/lib/mastodon.php new file mode 100644 index 0000000..8516ce0 --- /dev/null +++ b/lib/mastodon.php @@ -0,0 +1,282 @@ +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$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 '
'.print_r($spost,true).'
'; + 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); + } + } + } +} + +?> diff --git a/post.php b/post.php new file mode 100644 index 0000000..546bf09 --- /dev/null +++ b/post.php @@ -0,0 +1,60 @@ +. +*/ + +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 '
'.print_r($_POST,true).'
'; + +function necho($msg) { + // do nothing :-) +} + +?>