mastodon.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <?php
  2. // Warning: postlength function requires $retlds global variable to be defined,
  3. // it has to be a reverse ordered list of "|" separated valid tlds, you can
  4. // require gettlds.php in the calling script and use it to set it, like this:
  5. // $retlds=gettlds(); $retlds=implode('|',$retlds);
  6. function validtoken($token) {
  7. if (preg_match('#^[A-Za-z0-9_-]{43}$#',$token)===1)
  8. return true;
  9. else
  10. return false;
  11. }
  12. function mastreq($context,$host,$endpoint) {
  13. $context=stream_context_create($context);
  14. $endpoint="https://{$host}{$endpoint}";
  15. $res=@file_get_contents($endpoint,false,$context);
  16. if ($res===false)
  17. return ['ok'=>false,'error'=>"could not connect to «{$host}»",'headers'=>null];
  18. $res=@json_decode($res,true);
  19. if (is_null($res))
  20. return ['ok'=>false,'error'=>"could not decode JSON data from «{$endpoint}» (".json_last_error().': '.json_last_error_msg().")",'headers'=>$http_response_header];
  21. if (isset($res['error']))
  22. return ['ok'=>false,'error'=>lcfirst($res['error']),'headers'=>$http_response_header];
  23. /*print_r($http_response_header);
  24. preg_match('#^\S+\s+(\S+)\s+(\S+)#',$http_response_header[0],$matches);
  25. print_r($matches);
  26. $httpcode=$matches[1]+0;
  27. $httpcodetext=$matches[2];
  28. if (($httpcode>=400 && $httpcode<=499) || ($httpcode>=500 && $httpcode<=599))
  29. return ['ok'=>false,'error'=>"HTTP error: {$httpcodetext}"];*/
  30. return ['ok'=>true,'data'=>$res,'headers'=>$http_response_header];
  31. }
  32. function mastget($host,$token,$endpoint,$timeout) {
  33. $context=[
  34. 'http'=>[
  35. 'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
  36. 'method'=>'GET',
  37. 'ignore_errors'=>true,
  38. 'timeout'=>$timeout
  39. ]
  40. ];
  41. if (!is_null($token))
  42. $context['http']['header'].="Authorization: Bearer {$token}\r\n";
  43. $res=mastreq($context,$host,$endpoint);
  44. return $res;
  45. }
  46. function mastpost($host,$token,$endpoint,$content,$timeout) {
  47. $content=http_build_query($content);
  48. $context=[
  49. 'http'=>[
  50. 'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
  51. 'method'=>'POST',
  52. 'ignore_errors'=>true,
  53. 'content'=>$content,
  54. 'timeout'=>$timeout
  55. ]
  56. ];
  57. if (!is_null($token))
  58. $context['http']['header'].="Authorization: Bearer {$token}\r\n";
  59. $res=mastreq($context,$host,$endpoint);
  60. return $res;
  61. }
  62. function mastpostfile($host,$token,$endpoint,$content,$timeout) {
  63. $content=http_build_query($content);
  64. $context=[
  65. 'http'=>[
  66. 'header'=>"Content-type: multipart/form-data;boundary=\"boundary\"\r\nAccept: application/json\r\n",
  67. 'method'=>'POST',
  68. 'ignore_errors'=>true,
  69. 'content'=>$content,
  70. 'timeout'=>$timeout
  71. ]
  72. ];
  73. if (!is_null($token))
  74. $context['http']['header'].="Authorization: Bearer {$token}\r\n";
  75. $res=mastreq($context,$host,$endpoint);
  76. return $res;
  77. }
  78. function mastdel($host,$token,$endpoint,$timeout) {
  79. $context=[
  80. 'http'=>[
  81. 'header'=>"Content-type: application/x-www-form-urlencoded\r\nAccept: application/json\r\n",
  82. 'method'=>'DELETE',
  83. 'ignore_errors'=>true,
  84. 'timeout'=>$timeout
  85. ]
  86. ];
  87. if (!is_null($token))
  88. $context['http']['header'].="Authorization: Bearer {$token}\r\n";
  89. $res=mastreq($context,$host,$endpoint);
  90. return $res;
  91. }
  92. /*
  93. some endpoints
  94. get
  95. auth required
  96. verify app creds and get app info: /api/v1/apps/verify_credentials
  97. verify user creds and get account info: /api/v1/accounts/verify_credentials
  98. get a post: /api/v1/statuses/[id]
  99. post
  100. auth required
  101. post a status: /api/v1/statuses
  102. send follow request to an account: /api/v1/accounts/[id]/follow
  103. unfollow an account: /api/v1/accounts/[id]/unfollow
  104. */
  105. function splitpost($post,$avchars,$cw,$pre,$cntup) {
  106. // decided use $matches[1] instead of $matches[0]
  107. // to stay safe, $avchars should be at least 30 (didn't test with less);
  108. // $pre can be used to list recipients (in this case it has to end with
  109. // a "\n" or " "), or for anything else
  110. $post=preg_replace('#[ \t\f\r]+\n#',"\n",$post);
  111. $post=rtrim($post);
  112. $postrlen=strlen($post);
  113. $postlen=postlength($post);
  114. $cwlen=mb_strlen($cw,'UTF-8');
  115. $prelen=postlength($pre);
  116. if ($postlen+$prelen+$cwlen<=$avchars)
  117. return [['cw'=>$cw,'post'=>$pre.$post,'mastlen'=>$postlen+$prelen+$cwlen]];
  118. // there is no way to know the total of posts before splitting, and its
  119. // string length modifies the total, so we roughly estimate it very
  120. // cautiosly to the decrease, just to spare cycles
  121. $tot='';
  122. $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 "…"
  123. for ($i=0; $i<strlen($gtot); $i++)
  124. $tot.='x';
  125. $c=0;
  126. while (true) {
  127. $c++;
  128. $totlen=strlen($tot);
  129. $spost=[];
  130. $buf='';
  131. $off=0;
  132. $i=1;
  133. while (true) {
  134. //echo "========================\n";
  135. if (strlen($i)>$totlen) break;// do another cycle
  136. $cnt="__[{$i}/{$tot}]";
  137. //$lastcons=substr($post,$off,40);
  138. preg_match('#(\S+)(\s+|$)#',$post,$matches,0,$off);
  139. //var_dump($matches);
  140. if (count($matches)==0) {// done, last post
  141. $spost[]=['cw'=>$cw,'post'=>rtrim($buf)];
  142. break 2;
  143. }
  144. $offadd=strlen($matches[0]);
  145. ($off+$offadd>=$postrlen) ? $dotsaddlen=0 : $dotsaddlen=2;// if we are on the last word, we don't add "…"
  146. if ($prelen+$cwlen+postlength($buf.$matches[1].$cnt)+$dotsaddlen>$avchars) {// if current match would make buf+overhead overcome avchars
  147. //echo "LONGMATCH: «$matches[0]»\n";
  148. $nxcntlen=$totlen+strlen($i+1)+5;// next cnt may be different, so we precalc its length
  149. ($i==1 || $dotsaddlen==0) ? $nxdotsaddlen=2 : $nxdotsaddlen=4;// if we are on first or last post, we add 1 "…"; otherwise we add 2
  150. if ($prelen+$cwlen+postlength($matches[1])+$nxcntlen+$nxdotsaddlen>$avchars) {// if next match+overhead is by itself longer than avchars
  151. //echo "BLOCKMATCH: «$matches[0]»\n";
  152. //$len=$avchars-$nxcntlen-$prelen-$nxdotsaddlen;
  153. $len=$avchars-postlength($buf.$cnt)-$prelen-$cwlen-$dotsaddlen;
  154. if ($len>0) {
  155. // deactivate possible links because they will be broken
  156. $matches[0]=preg_replace('#^http(s)?://#','zttp$1://',$matches[0]);
  157. $matches[0]=preg_replace('#^@([a-zA-Z0-9_]+@[a-z0-9-]+)#','+$1',$matches[0]);
  158. $matches[0]=mb_substr($matches[0],0,$len,'UTF-8');
  159. //echo "SUBSTRING: «$matches[0]»\n";
  160. $offadd=strlen($matches[0]);
  161. //echo "{$matches[0]}: OFF: {$off}; OFFADD: {$offadd}\n";
  162. $buf.=$matches[0];
  163. $matches[0]='';
  164. }
  165. }
  166. $spost[]=['cw'=>$cw,'post'=>rtrim($buf).' …'];
  167. $buf='… ';
  168. $i++;
  169. }/* else {
  170. echo "NORMATCH: «$matches[0]»\n";
  171. }*/
  172. $buf.=$matches[0];
  173. $off+=$offadd;
  174. }
  175. $tot.='x';
  176. }
  177. //echo '<pre>'.print_r($spost,true).'</pre>';
  178. if ($cntup)
  179. foreach ($spost as $key=>$post) {
  180. $spost[$key]['post']="{$pre}[".($key+1)."/{$i}]\n\n{$post['post']}";
  181. $spost[$key]['mastlen']=postlength($spost[$key]['post'])+$cwlen;
  182. }
  183. else
  184. foreach ($spost as $key=>$post) {
  185. $spost[$key]['post']="{$pre}{$post['post']}\n\n[".($key+1)."/{$i}]";
  186. $spost[$key]['mastlen']=postlength($spost[$key]['post'])+$cwlen;
  187. }
  188. //echo "CYCLES: {$c}\n";
  189. //echo "LASTCONS: {$lastcons}\n";
  190. return $spost;
  191. }
  192. function postlength($post) {
  193. global $retlds;
  194. // echo "-A-> |{$post}|\n";
  195. // for some reason, mastodon seems to check tld existence only on http(s) links - see next regexp
  196. $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);
  197. if (!is_null($res)) $post=$res;
  198. // $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);
  199. // on http(s) links mastodon checks if tld exists...
  200. $res=preg_replace('#(^|\W)https?://(([a-z0-9]([a-z0-9-]+[a-z0-9])?){1,63}\.)+('.$retlds.')(/\S*[\w/=_\-])?#u', '$1UUUUUUUUUUUUUUUUUUUUUUU', $post);
  201. if (!is_null($res)) $post=$res;
  202. // echo "-B-> |{$post}|\n";
  203. return mb_strlen($post,'UTF-8');
  204. }
  205. // this function requires these to be defined:
  206. // - an "evhandle" function to handle events
  207. // - an "eecho" function to handle output
  208. // - 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')
  209. // see ocrbot for an example
  210. function evlisten($host,$port,$endpoint,$token,$timeout) {
  211. global $doshut;
  212. while (true) {
  213. shutdown($doshut);
  214. $dispurl="tls://{$host}:{$port}";
  215. eecho(1,"trying to connect to «{$dispurl}».");
  216. $sh=@fsockopen("tls://{$host}",$port,$errno,$errstr,$timeout);
  217. if ($sh===false) {
  218. eecho(3,"could not connect to «{$dispurl}»: {$errstr} ({$errno}); will try again in 1 second.");
  219. sleep(1);
  220. } else {
  221. //stream_set_blocking($sh,false);
  222. stream_set_timeout($sh,1,0);
  223. eecho(1,"succesfully connected to «{$dispurl}».");
  224. $req="GET {$endpoint} HTTP/1.1\r\nHost: {$host}\r\nUser-Agent: a_bot\r\nAuthorization: Bearer {$token}\r\n\r\n";
  225. if (fwrite($sh,$req)===false) {
  226. eecho(3,"could not subscribe to user notifications on «{$dispurl}»; will try again in 1 second.");
  227. fclose($sh);
  228. unset($sh);// this is because shutdown can check if $sh is set and if it is, try to fclose it
  229. sleep(1);
  230. } else {
  231. eecho(1,"listening for user notifications on «{$dispurl}».");
  232. //$lc=0;
  233. while (!feof($sh)) {
  234. shutdown($doshut);
  235. //$lc++;
  236. $line=rtrim(fgets($sh),"\r\n");
  237. //echo "{$lc}> {$line}\n";
  238. if (preg_match('#^event: #',$line)===1) {
  239. $event=['type'=>preg_replace('#^event: #','',$line),'data'=>''];
  240. $line=rtrim(fgets($sh),"\r\n");
  241. //echo "{$lc} DATA> {$line}\n";
  242. if (preg_match('#^data: #',$line)===1) {
  243. $event['data'].=preg_replace('#^data: #','',$line);
  244. while ($line!='') {
  245. $line=rtrim(fgets($sh),"\r\n");
  246. if ($line=='') break;
  247. //echo "{$lc} LENGTH> {$line}\n";
  248. $line=rtrim(fgets($sh),"\r\n");
  249. //echo "{$lc} DATA> {$line}\n";
  250. $event['data'].=$line;
  251. }
  252. $event['data']=@json_decode($event['data'],true);
  253. if ($event['data']===false) {
  254. eecho(2,"could not decode data for event of type «{$event['type']}».");
  255. } else {
  256. //print_r($event);
  257. evhandle($event);
  258. }
  259. }
  260. }
  261. }
  262. fclose($sh);
  263. unset($sh);// this is because shutdown can check if $sh is set and if it is, try to fclose it
  264. eecho(3,"lost connection to «{$dispurl}»; will try reconnecting in 1 second.");
  265. sleep(1);
  266. }
  267. }
  268. }
  269. }
  270. ?>