Advertisement
mspotilas

class.Bsky.php untested

May 24th, 2024 (edited)
640
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 14.10 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, $languages=['de-DE']) {
  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.            
  60.            
  61.             // find links and mark them as app.bsky.richtext.facet#link
  62.             $start = 0; $end = 0;
  63.             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);
  64.             foreach($matches as $match) {
  65.                 $link = $match[1];
  66.            
  67.                 if($link) {
  68.                     $start = strpos($skeet_text, $link, $end);
  69.                     $end = $start + strlen($link);
  70.                
  71.                     $postfields["record"]["facets"][] = [
  72.                         "index" => [
  73.                             'byteStart' => $start,
  74.                             'byteEnd'   => $end,
  75.                         ],
  76.                         "features" => [[
  77.                             '$type'     => "app.bsky.richtext.facet#link",
  78.                             'uri'       => $link,
  79.                         ]],
  80.                     ];
  81.                    
  82.                     // try to get a card for this link, if we don't already have one
  83.                     if(!isset($postfields["record"]["embed"])) {
  84.                         $card = $this->card($link);
  85.                         if($card) {
  86.                             $postfields["record"]["embed"] = $card;
  87.                         }
  88.                     }
  89.                 }
  90.                
  91.             }
  92.             // *****START find hashtags
  93.             // https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/richtext/facet.json
  94.             $start = 0; $end = 0;
  95.             preg_match_all('/(#[^\s#]+)/', $skeet_text, $matches, PREG_SET_ORDER);
  96.             foreach($matches as $match) {
  97.                 $tag = $match[1];
  98.                 if($tag) {
  99.                     $start = strpos($skeet_text, $tag, $end);
  100.                     $end = $start + strlen($tag);
  101.                     $postfields["record"]["facets"][] = [
  102.                         "index" => [
  103.                             'byteStart' => $start,
  104.                             'byteEnd'   => $end,
  105.                         ],
  106.                         "features" => [[
  107.                             '$type'     => "app.bsky.richtext.facet#tag",
  108.                             'tag'       => substr($tag, 1),
  109.                         ]],
  110.                     ];
  111.                 }
  112.             }
  113.             // *****END find hashtags
  114.            
  115.            
  116.            
  117.             // find mentions and mark them as app.bsky.richtext.facet#mention
  118.             $start = 0; $end = 0;
  119.             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);
  120.             foreach($matches as $match) {
  121.                 $mention = $match[1];
  122.                
  123.                 // check if bsky resolves mention as handle
  124.                 $response = $this->xrpc_get('/com.atproto.identity.resolveHandle', 'handle='.preg_replace('#@#', '', $mention));
  125.                 if($response['_curl']['http_code'] == 200) {
  126.                     $mentioned_did = $response['did'];
  127.  
  128.                     $start = strpos($skeet_text, $mention, $end ?? 0);
  129.                     $end = $start + strlen($mention);
  130.                    
  131.                     $postfields["record"]["facets"][] = [
  132.                         "index" => [
  133.                             'byteStart' => $start,
  134.                             'byteEnd'   => $end,
  135.                         ],
  136.                         "features" => [[
  137.                             '$type'     => "app.bsky.richtext.facet#mention",
  138.                             'did'       => $mentioned_did,
  139.                         ]],
  140.                     ];
  141.                 }
  142.             }
  143.            
  144.             # print "postfields: \n"; print json_encode($postfields); print json_last_error_msg(); print "\n"; exit;
  145.            
  146.             return $this->xrpc_post('/com.atproto.repo.createRecord', $postfields);
  147.         }
  148.        
  149.         function card($url) {
  150.             $doc = new DOMDocument();
  151.             @$doc->loadHTMLFile($url);
  152.             if($doc) {
  153.                 $xpath = new DOMXpath($doc);
  154.                 $xpath->registerNamespace('og', 'http://ogp.me/ns');
  155.                 $xpath->registerNamespace('fb', 'http://ogp.me/ns/fb');
  156.                 $xpath->registerNamespace('twitter', 'http://ogp.me/ns/twitter');
  157.                
  158.                 $title = $this->nodeValue($xpath, ["//meta[@property='og:title']/@content", "//meta[@name='title']/@content", "//title", "//meta[@property='fb:title']/@content", "//meta[@property='twitter:title']/@content"]);
  159.                 $description = $this->nodeValue($xpath, ["//meta[@property='og:description']/@content", "//meta[@name='description']/@content", "//meta[@property='fb:description']/@content", "//meta[@property='twitter:description']/@content"]);
  160.                
  161.                 // search for a possible image in this set of pathes
  162.                 $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"];
  163.                 $imgdata = false;
  164.                 foreach($imgpaths as $path) {
  165.                     foreach($xpath->query($path) as $node) {
  166.                         $imgurl = mb_convert_encoding($node->nodeValue, 'UTF-8', 'UTF-8');
  167.                         if($imgurl) {
  168.                             // load image
  169.                             $imgdata = @file_get_contents($imgurl);
  170.                             if($imgdata) {
  171.                                 // stop after the first loadable image
  172.                                 break;
  173.                             }
  174.                         }
  175.                     }
  176.                     if($imgdata) {
  177.                         break;
  178.                     }
  179.                 }
  180.                
  181.                 if($imgdata) {
  182.                     // max filesize is 976.56 KB
  183.                     if(strlen($imgdata) > 976560) {
  184.                         // resize till it fits
  185.                         $quality = 90;
  186.                         $tmp = sys_get_temp_dir().'/'.basename($img);
  187.                         while(!file_exists($tmp) || filesize($tmp) > 976560 && $quality >= 0) {
  188.                             print "\nDEBUG resizing $img from ".strlen($imgdata)." with quality $quality ..\n";
  189.                             $this->_image_compress($img, $tmp, $quality);
  190.                             print "DEBUG new size: ".filesize($tmp)."\n";
  191.                             $quality -= 10;
  192.                         }
  193.                         $imgdata = file_get_contents($tmp);
  194.                         unlink($tmp);
  195.                     }
  196.                     if(strlen($imgdata) <= 976560) {
  197.                         // get mime type
  198.                         $headers = implode("\n", $http_response_header);
  199.                         if (preg_match_all("/^content-type\s*:\s*(.*)$/mi", $headers, $matches)) {
  200.                             $content_type = end($matches[1]);
  201.                         }
  202.                         // upload image to bsky
  203.                         $response = $this->xrpc_post('/com.atproto.repo.uploadBlob', $imgdata, $content_type);
  204.                         // attach blob data
  205.                         if(isset($response['blob'])) {
  206.                             $thumb = $response['blob'];
  207.                         }
  208.                     }
  209.                 } else {
  210.                     print "\nDEBUG no image found\n";
  211.                 }
  212.                
  213.                 if(isset($thumb)) {
  214.                     return [
  215.                         '$type' => "app.bsky.embed.external",
  216.                         'external' => [
  217.                             "uri" => $url,
  218.                             "title" => html_entity_decode(strip_tags(mb_convert_encoding($title, 'UTF-8', 'UTF-8'))),
  219.                             "description" => html_entity_decode(strip_tags(mb_convert_encoding($description, 'UTF-8', 'UTF-8'))),
  220.                             "thumb" => $thumb
  221.                         ]
  222.                     ];
  223.                 }
  224.             }
  225.             return false;
  226.         }
  227.        
  228.         function nodeValue($xpath, $paths) {
  229.             foreach($paths as $path) {
  230.                 $nodes = $xpath->query($path);
  231.                 if($nodes[0]) {
  232.                     return mb_convert_encoding($nodes[0]->nodeValue, 'UTF-8', 'UTF-8');
  233.                 }
  234.             }
  235.             return false;
  236.         }
  237.        
  238.         function getProfile($actor_id) {
  239.             return $this->xrpc_get('/app.bsky.actor.getProfile', 'actor='.$actor_id);
  240.         }
  241.                
  242.         function xrpc_post($lexicon, $postfields=[], $content_type='application/json') {
  243.             $httpheader = [ 'Content-Type: '.$content_type ];
  244.             // if we have auth, send auth
  245.             if(isset($this->bearer)) {
  246.                 $httpheader[] = 'Authorization: Bearer '.$this->bearer;
  247.             }
  248.            
  249.             $curl = curl_init();
  250.             curl_setopt_array($curl, [
  251.                 CURLOPT_URL             => $this->xrpcbase.$lexicon,
  252.                 CURLOPT_RETURNTRANSFER  => true,
  253.                 CURLOPT_ENCODING        => '',
  254.                 CURLOPT_MAXREDIRS       => 10,
  255.                 CURLOPT_TIMEOUT         => 0,
  256.                 CURLOPT_FOLLOWLOCATION  => true,
  257.                 CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
  258.                 CURLOPT_POST            => true,
  259.                 CURLOPT_CUSTOMREQUEST   => 'POST',
  260.                 CURLOPT_POSTFIELDS      => is_array($postfields) ? json_encode($postfields) : $postfields,
  261.                 CURLOPT_HTTPHEADER      => $httpheader,
  262.                 CURLOPT_HEADER          => true,
  263.             ]);
  264.             $response = curl_exec($curl);
  265.            
  266.             $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  267.             $header = substr($response, 0, $header_size);
  268.             $response = substr($response, $header_size);
  269.            
  270.             $curl_info = curl_getinfo($curl);
  271.             curl_close($curl);
  272.            
  273.             $response = json_decode($response, TRUE);
  274.             $response['_curl'] = $curl_info;
  275.             $response['_header'] = $header;
  276.             return $response;
  277.         }
  278.        
  279.         function xrpc_get($lexicon, $params='') {
  280.             $httpheader = [ 'Content-Type: application/json' ];
  281.             // if we have auth, send auth
  282.             if(isset($this->bearer)) {
  283.                 $httpheader[] = 'Authorization: Bearer '.$this->bearer;
  284.             }
  285.  
  286.             $curl = curl_init();
  287.             curl_setopt_array($curl, [
  288.                 CURLOPT_URL             => $this->xrpcbase.$lexicon.'?'.$params,
  289.                 CURLOPT_RETURNTRANSFER  => true,
  290.                 CURLOPT_ENCODING        => '',
  291.                 CURLOPT_MAXREDIRS       => 10,
  292.                 CURLOPT_TIMEOUT         => 0,
  293.                 CURLOPT_FOLLOWLOCATION  => true,
  294.                 CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
  295.                 CURLOPT_HTTPHEADER      => $httpheader,
  296.             ]);
  297.             $response = curl_exec($curl);
  298.             $curl_info = curl_getinfo($curl);
  299.             curl_close($curl);
  300.            
  301.             $response = json_decode($response, TRUE);
  302.             $response['_curl'] = $curl_info;
  303.             return $response;
  304.         }
  305.        
  306.         function _image_compress($file, $compressed, $quality) {
  307.             $info = getimagesize($file);
  308.             if ($info['mime'] == 'image/jpeg') {
  309.                 $image = imagecreatefromjpeg($file);
  310.             } else if ($info['mime'] == 'image/gif') {
  311.                 $image = imagecreatefromgif($file);
  312.             } else if ($info['mime'] == 'image/png') {
  313.                 $image = imagecreatefrompng($file);
  314.             }
  315.             imagejpeg($image, $compressed, $quality);
  316.            
  317.             return $compressed;
  318.         }
  319.  
  320.     }
  321. ?>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement