mspotilas

class.Bsky.php with hashtags and optional one image upload, tested

May 29th, 2024 (edited)
1,295
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 14.76 KB | None | 0 0
  1. <?php
  2.     // 2023 by Thomas Nesges
  3.     // (c) CC-BY
  4.  
  5.     class Bsky {
  6.         private $username;
  7.         private $password;
  8.         private $xrpcbase;
  9.         private $did;
  10.         private $bearer;
  11.         private $refreshToken;
  12.         public $connected;
  13.         public $handle;
  14.         public $email;
  15.         public $emailConfirmed;
  16.         public $profile_url;
  17.        
  18.         function __construct($username, $password, $xrpcbase='https://bsky.social/xrpc') {
  19.             $this->username = $username;
  20.             $this->password = $password;
  21.             $this->xrpcbase = $xrpcbase;
  22.         }
  23.        
  24.         function connect() {
  25.             $response = $this->xrpc_post('/com.atproto.server.createSession', [
  26.                   "identifier"  => $this->username,
  27.                   "password"    => $this->password,
  28.                 ]);
  29.            
  30.             $this->did =            $response['did'];
  31.             $this->bearer =         $response['accessJwt'];
  32.             $this->handle =         $response['handle'];
  33.             $this->email =          $response['email'];
  34.             $this->emailConfirmed = $response['emailConfirmed'];
  35.             $this->refreshToken =   $response['refreshJwt'];
  36.             // todo: how do I find the profile url?
  37.             $this->profile_url =    'https://bsky.app/profile/'.$this->handle;
  38.            
  39.             $this->connected = false;
  40.             if ($response['_curl']['http_code']==200) {
  41.                 $this->connected = true;
  42.             }
  43.            
  44.             return $response;
  45.         }
  46.        
  47.         function post($skeet_text, $imgurl = '', $alttext = '', $languages=['fi-FI']) {
  48.             $postfields = [
  49.                 "repo"          => $this->did,
  50.                 "collection"    => "app.bsky.feed.post",
  51.                 "record" => [
  52.                     '$type'     => "app.bsky.feed.post",
  53.                     'createdAt' => date("c"),
  54.                     'text'      => $skeet_text,
  55.                     'langs'     => $languages,
  56.                 ]
  57.             ];
  58.            
  59.             if ($imgurl != '') {
  60.                 $embed = $this->embed($imgurl, $alttext);
  61.                 if ($embed) {
  62.                     $postfields["record"]["embed"] = $embed;
  63.                 }
  64.             }
  65.            
  66.             // find links and mark them as app.bsky.richtext.facet#link
  67.             $start = 0; $end = 0;
  68.             preg_match_all('/[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*[-a-zA-Z0-9@%_\+~#\/\/=])?)/', $skeet_text, $matches, PREG_SET_ORDER);
  69.             foreach($matches as $match) {
  70.                 $link = $match[1];
  71.            
  72.                 if ($link) {
  73.                     $start = strpos($skeet_text, $link, $end);
  74.                     $end = $start + strlen($link);
  75.                
  76.                     $postfields["record"]["facets"][] = [
  77.                         "index" => [
  78.                             'byteStart' => $start,
  79.                             'byteEnd'   => $end,
  80.                         ],
  81.                         "features" => [[
  82.                             '$type'     => "app.bsky.richtext.facet#link",
  83.                             'uri'       => $link,
  84.                         ]],
  85.                     ];
  86.                    
  87.                     // try to get a card for this link, if we don't already have one
  88.                     if (!isset($postfields["record"]["embed"])) {
  89.                         $embed = $this->embed($link);
  90.                         if ($embed) {
  91.                             $postfields["record"]["embed"] = $embed;
  92.                         }
  93.                     }
  94.                 }
  95.                
  96.             }
  97.             // *****START find hashtags
  98.             // https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/richtext/facet.json
  99.             $start = 0; $end = 0;
  100.             preg_match_all('/(#[^\s#]+)/', $skeet_text, $matches, PREG_SET_ORDER);
  101.             foreach($matches as $match) {
  102.                 $tag = $match[1];
  103.                 if ($tag) {
  104.                     $start = strpos($skeet_text, $tag, $end);
  105.                     $end = $start + strlen($tag);
  106.                     $postfields["record"]["facets"][] = [
  107.                         "index" => [
  108.                             'byteStart' => $start,
  109.                             'byteEnd'   => $end,
  110.                         ],
  111.                         "features" => [[
  112.                             '$type'     => "app.bsky.richtext.facet#tag",
  113.                             'tag'       => substr($tag, 1),
  114.                         ]],
  115.                     ];
  116.                 }
  117.             }
  118.             // *****END find hashtags
  119.            
  120.            
  121.            
  122.             // find mentions and mark them as app.bsky.richtext.facet#mention
  123.             $start = 0; $end = 0;
  124.             preg_match_all('#[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)#', $skeet_text, $matches, PREG_SET_ORDER);
  125.             foreach($matches as $match) {
  126.                 $mention = $match[1];
  127.                
  128.                 // check if bsky resolves mention as handle
  129.                 $response = $this->xrpc_get('/com.atproto.identity.resolveHandle', 'handle='.preg_replace('#@#', '', $mention));
  130.                 if ($response['_curl']['http_code'] == 200) {
  131.                     $mentioned_did = $response['did'];
  132.  
  133.                     $start = strpos($skeet_text, $mention, $end ?? 0);
  134.                     $end = $start + strlen($mention);
  135.                    
  136.                     $postfields["record"]["facets"][] = [
  137.                         "index" => [
  138.                             'byteStart' => $start,
  139.                             'byteEnd'   => $end,
  140.                         ],
  141.                         "features" => [[
  142.                             '$type'     => "app.bsky.richtext.facet#mention",
  143.                             'did'       => $mentioned_did,
  144.                         ]],
  145.                     ];
  146.                 }
  147.             }
  148.            
  149.             //print "postfields: \n"; print json_encode($postfields); print json_last_error_msg(); print "\n"; exit;
  150.            
  151.             return $this->xrpc_post('/com.atproto.repo.createRecord', $postfields);
  152.         }
  153.        
  154.         function embed($url, $alttext) {
  155.             $doc = new DOMDocument();
  156.             @$doc->loadHTMLFile($url);
  157.             if ($doc) {
  158.                 if ($alttext != '') {
  159.                     $imgdata = @file_get_contents($url);
  160.                 } else {
  161.                     $xpath = new DOMXpath($doc);
  162.                     $xpath->registerNamespace('og', 'http://ogp.me/ns');
  163.                     $xpath->registerNamespace('fb', 'http://ogp.me/ns/fb');
  164.                     $xpath->registerNamespace('twitter', 'http://ogp.me/ns/twitter');
  165.                    
  166.                     $title = $this->nodeValue($xpath, ["//meta[@property='og:title']/@content", "//meta[@name='title']/@content", "//title", "//meta[@property='fb:title']/@content", "//meta[@property='twitter:title']/@content"]);
  167.                     $description = $this->nodeValue($xpath, ["//meta[@property='og:description']/@content", "//meta[@name='description']/@content", "//meta[@property='fb:description']/@content", "//meta[@property='twitter:description']/@content"]);
  168.                    
  169.                     // search for a possible image in this set of pathes
  170.                     $imgpaths = ["//meta[@property='og:image']/@content", "//meta[@property='og:image:url']/@content", "//meta[@property='og:image:secure_url']/@content", "//meta[@property='fb:image']/@content", "//meta[@property='twitter:image']/@content", "//link[@rel='icon']/@href"];
  171.                     $imgdata = false;
  172.                     foreach($imgpaths as $path) {
  173.                         foreach($xpath->query($path) as $node) {
  174.                             $imgurl = mb_convert_encoding($node->nodeValue, 'UTF-8', 'UTF-8');
  175.                             if ($imgurl) {
  176.                                 // load image
  177.                                 $imgdata = @file_get_contents($imgurl);
  178.                                 if ($imgdata) {
  179.                                     // stop after the first loadable image
  180.                                     break;
  181.                                 }
  182.                             }
  183.                         }
  184.                         if ($imgdata) {
  185.                             break;
  186.                         }
  187.                     }
  188.                 }
  189.                 if ($imgdata) {
  190.                     // max filesize is 976.56 KB
  191.                     if (strlen($imgdata) > 976560) {
  192.                         // resize till it fits
  193.                         $quality = 90;
  194.                         $tmp = sys_get_temp_dir().'/'.basename($img);
  195.                         while(!file_exists($tmp) || filesize($tmp) > 976560 && $quality >= 0) {
  196.                             print "\nDEBUG resizing $img from ".strlen($imgdata)." with quality $quality ..\n";
  197.                             $this->_image_compress($img, $tmp, $quality);
  198.                             print "DEBUG new size: ".filesize($tmp)."\n";
  199.                             $quality -= 10;
  200.                         }
  201.                         $imgdata = file_get_contents($tmp);
  202.                         unlink($tmp);
  203.                     }
  204.                     if (strlen($imgdata) <= 976560) {
  205.                         // upload image to bsky
  206.                         $content_type = "image/jpeg";
  207.                         $response = $this->xrpc_post('/com.atproto.repo.uploadBlob', $imgdata, $content_type);
  208.                         // attach blob data
  209.                         if (isset($response['blob'])) {
  210.                             $thumb = $response['blob'];
  211.                         }
  212.                     }
  213.                 } else {
  214.                     print "\nDEBUG no image found\n";
  215.                 }
  216.                
  217.                 if (isset($thumb)) {
  218.                     if ($alttext != '')
  219.                         return [
  220.                             '$type' => "app.bsky.embed.images",
  221.                             'images' => [[
  222.                                 "image" => $thumb,
  223.                                 "alt" => html_entity_decode(strip_tags(mb_convert_encoding($alttext, 'UTF-8', 'UTF-8'))),
  224.                             ]]
  225.                         ];
  226.                     return [
  227.                         '$type' => "app.bsky.embed.external",
  228.                         'external' => [
  229.                             "uri" => $url,
  230.                             "title" => html_entity_decode(strip_tags(mb_convert_encoding($title, 'UTF-8', 'UTF-8'))),
  231.                             "description" => html_entity_decode(strip_tags(mb_convert_encoding($description, 'UTF-8', 'UTF-8'))),
  232.                             "thumb" => $thumb
  233.                         ]
  234.                     ];
  235.                 }
  236.             }
  237.             return false;
  238.         }
  239.        
  240.         function nodeValue($xpath, $paths) {
  241.             foreach($paths as $path) {
  242.                 $nodes = $xpath->query($path);
  243.                 if ($nodes[0]) {
  244.                     return mb_convert_encoding($nodes[0]->nodeValue, 'UTF-8', 'UTF-8');
  245.                 }
  246.             }
  247.             return false;
  248.         }
  249.        
  250.         function getProfile($actor_id) {
  251.             return $this->xrpc_get('/app.bsky.actor.getProfile', 'actor='.$actor_id);
  252.         }
  253.                
  254.         function xrpc_post($lexicon, $postfields=[], $content_type='application/json') {
  255.             $httpheader = [ 'Content-Type: '.$content_type ];
  256.             // if we have auth, send auth
  257.             if (isset($this->bearer)) {
  258.                 $httpheader[] = 'Authorization: Bearer '.$this->bearer;
  259.             }
  260.            
  261.             $curl = curl_init();
  262.             curl_setopt_array($curl, [
  263.                 CURLOPT_URL             => $this->xrpcbase.$lexicon,
  264.                 CURLOPT_RETURNTRANSFER  => true,
  265.                 CURLOPT_ENCODING        => '',
  266.                 CURLOPT_MAXREDIRS       => 10,
  267.                 CURLOPT_TIMEOUT         => 0,
  268.                 CURLOPT_FOLLOWLOCATION  => true,
  269.                 CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
  270.                 CURLOPT_POST            => true,
  271.                 CURLOPT_CUSTOMREQUEST   => 'POST',
  272.                 CURLOPT_POSTFIELDS      => is_array($postfields) ? json_encode($postfields) : $postfields,
  273.                 CURLOPT_HTTPHEADER      => $httpheader,
  274.                 CURLOPT_HEADER          => true,
  275.             ]);
  276.             $response = curl_exec($curl);
  277.            
  278.             $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  279.             $header = substr($response, 0, $header_size);
  280.             $response = substr($response, $header_size);
  281.            
  282.             $curl_info = curl_getinfo($curl);
  283.             curl_close($curl);
  284.            
  285.             $response = json_decode($response, TRUE);
  286.             $response['_curl'] = $curl_info;
  287.             $response['_header'] = $header;
  288.             return $response;
  289.         }
  290.        
  291.         function xrpc_get($lexicon, $params='') {
  292.             $httpheader = [ 'Content-Type: application/json' ];
  293.             // if we have auth, send auth
  294.             if (isset($this->bearer)) {
  295.                 $httpheader[] = 'Authorization: Bearer '.$this->bearer;
  296.             }
  297.  
  298.             $curl = curl_init();
  299.             curl_setopt_array($curl, [
  300.                 CURLOPT_URL             => $this->xrpcbase.$lexicon.'?'.$params,
  301.                 CURLOPT_RETURNTRANSFER  => true,
  302.                 CURLOPT_ENCODING        => '',
  303.                 CURLOPT_MAXREDIRS       => 10,
  304.                 CURLOPT_TIMEOUT         => 0,
  305.                 CURLOPT_FOLLOWLOCATION  => true,
  306.                 CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
  307.                 CURLOPT_HTTPHEADER      => $httpheader,
  308.             ]);
  309.             $response = curl_exec($curl);
  310.             $curl_info = curl_getinfo($curl);
  311.             curl_close($curl);
  312.            
  313.             $response = json_decode($response, TRUE);
  314.             $response['_curl'] = $curl_info;
  315.             return $response;
  316.         }
  317.        
  318.         function _image_compress($file, $compressed, $quality) {
  319.             $info = getimagesize($file);
  320.             if ($info['mime'] == 'image/jpeg') {
  321.                 $image = imagecreatefromjpeg($file);
  322.             } else if ($info['mime'] == 'image/gif') {
  323.                 $image = imagecreatefromgif($file);
  324.             } else if ($info['mime'] == 'image/png') {
  325.                 $image = imagecreatefrompng($file);
  326.             }
  327.             imagejpeg($image, $compressed, $quality);
  328.            
  329.             return $compressed;
  330.         }
  331.  
  332.     }
  333. ?>
  334.  
Advertisement
Add Comment
Please, Sign In to add comment