Advertisement
Mayumi_H

BSKY JWT post w/card

Jun 12th, 2025 (edited)
827
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 8.19 KB | None | 0 0
  1. <?php
  2. //Text to Bluesky (with link card) sample 2025-06
  3. //MH+ presents, visit my blog => https://sl-memo.blogspot.com/
  4.  
  5. date_default_timezone_set('Asia/Tokyo');
  6. $UAstring = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0";
  7.  
  8. //入力データ
  9. $readfile = "text_data.txt";
  10. //$readdata = explode("\n", file_get_contents($readfile));
  11. //テスト用なので固定の配列で記述
  12. $readdata = array(
  13.  '001<->IMG - 3<->https://www.flickr.com/photos/153589246@N07/37182374834/<->https://live.staticflickr.com/4459/37182374834_a89ca42e10_c.jpg'
  14. ,'002<->Baby Anythings are Cute<->https://www.flickr.com/photos/dennissylvesterhurd/52852294515/<->https://live.staticflickr.com/65535/52852294515_b00fb1e17d_c.jpg'
  15. ,'003<->Labradoodle<->https://www.flickr.com/photos/195591092@N07/52064576115/<->https://live.staticflickr.com/65535/52064576115_636e0487bf_c.jpg'
  16. ,'004<->Day with Monty 1<->https://www.flickr.com/photos/ianlivesey/49847245082/<->https://live.staticflickr.com/65535/49847245082_41ca62b972_c.jpg'
  17. );
  18.  
  19.  
  20. $counter  = 0;
  21.  
  22. //Bluesky接続 (Json Web Token (JWT) 取得)
  23. $handle   = "**********************";       //あなたのBluesky ID (username.bsky.social など)
  24. $password = "**********************";       //あなたのBluesky パスワード
  25.  
  26. $jwt = NULL;
  27. $connect_FLG = false;
  28. $logfile = "text_test.log";
  29. $log_FLG = false;
  30.  
  31. //Main処理
  32. //各行毎で処理する
  33. foreach($readdata as $lines) {
  34.     $items = explode("<->", $lines);// 0=番号, 1=タイトル, 2=URL, 3=imageURL
  35.     //画像URLの無いものは破棄
  36.     if(!(isset($items[3]))) continue;
  37.  
  38.     if(!$connect_FLG){
  39.         $connect_FLG = true;
  40.         // Json Web Token (JWT) 取得
  41.         $jwt = login($handle, $password);
  42.     }
  43.  
  44.     //-----------------------------------------------------------------------------------------------
  45.     //Blusky 投稿
  46.     $title= (string)$items[1];
  47.     $link = (string)$items[2];
  48.     $image= (string)$items[3];
  49.     $text = $title . " #Flickr #PublicDomain \n" . $link;
  50.  
  51.     $facets = [];
  52.     //この辺の facets でのタグ付けやLink付けを丸投げでやってくれるライブラリーもあるらしい
  53.     //Linkを付ける
  54.     $linkStart = strpos($text, $link);
  55.     $linkEnd   = $linkStart + strlen($link);
  56.     $facets[] = [
  57.             'index' => [
  58.                 'byteStart' => $linkStart,
  59.                 'byteEnd' => $linkEnd
  60.             ],
  61.             'features' => [
  62.                 [
  63.                     '$type' => 'app.bsky.richtext.facet#link',
  64.                     'uri' => $link
  65.                 ]
  66.             ]
  67.     ];
  68.     //HashTagを付ける
  69.     $tagstring = "#Flickr";// # を含む
  70.     $linkStart = strpos($text, $tagstring);
  71.     $linkEnd   = $linkStart + strlen($tagstring);
  72.     $facets[] = [
  73.             'index' => [
  74.                 'byteStart' => $linkStart,
  75.                 'byteEnd' => $linkEnd
  76.             ],
  77.             'features' => [
  78.                 [
  79.                     '$type' => 'app.bsky.richtext.facet#tag',
  80.                     'tag' => "Flickr"   // # を含まない
  81.                 ]
  82.             ]
  83.     ];
  84.     $tagstring = "#PublicDomain";// # を含む
  85.     $linkStart = strpos($text, $tagstring);
  86.     $linkEnd   = $linkStart + strlen($tagstring);
  87.     $facets[] = [
  88.             'index' => [
  89.                 'byteStart' => $linkStart,
  90.                 'byteEnd' => $linkEnd
  91.             ],
  92.             'features' => [
  93.                 [
  94.                     '$type' => 'app.bsky.richtext.facet#tag',
  95.                     'tag' => "PublicDomain" // # を含まない
  96.                 ]
  97.             ]
  98.     ];
  99.     $record = [
  100.         '$type' => "app.bsky.feed.post",
  101.         'text' => $text,
  102.         'createdAt' => (new DateTime())->format("c"),
  103.         'facets' => $facets,
  104.     ];
  105.  
  106.     //画像データ取得とUpload
  107.     $imageUri = uploadImage($jwt, $image);
  108.  
  109.     $record['embed'] = [
  110.         '$type' => 'app.bsky.embed.external',
  111.         'external'=> [
  112.             'uri' => $link,  //リンクカードのURL
  113.             'title' => $title,
  114.             'description' => 'Blog記事用のネタ投稿です。descriptionは省略可。titleは省略するとurlになるよ',
  115.             'thumb' => $imageUri,
  116.         ],
  117.     ];
  118.  
  119.     $response = post_w_link($handle, $jwt, $record);
  120.  
  121.     file_put_contents($logfile,print_r($response,true),$log_FLG);
  122.     if(!$log_FLG) $log_FLG = FILE_APPEND;
  123.  
  124.     if(isset($response['validationStatus'])){
  125.         if($response['validationStatus'] == 'valid'){
  126.             //投稿成功
  127.             $counter++;
  128.         }
  129.     }
  130.     //-----------------------------------------------------------------------------------------------
  131.  
  132.     usleep(100000 * 3);//0.1秒 x N停止 (sleep 1秒 = usleep 1000000)
  133. }
  134.  
  135.  
  136. echo "total : " . (string)$counter ." items post\n";
  137.  
  138.  
  139.  
  140. /////////////////////////////////////////////////////////////////
  141.  
  142. function login($handle, $password)
  143. {
  144.     $ch = curl_init("https://bsky.social/xrpc/com.atproto.server.createSession");
  145.     curl_setopt_array($ch, [
  146.         CURLOPT_CONNECTTIMEOUT => 10,
  147.         CURLOPT_SSL_VERIFYPEER => false,
  148.         CURLOPT_RETURNTRANSFER => true,
  149.         CURLOPT_FOLLOWLOCATION => true,
  150.         CURLOPT_MAXREDIRS => 1000,
  151.         CURLOPT_COOKIEJAR  => dirname(__FILE__) . '/cookie_bsky.txt',
  152.         CURLOPT_COOKIEFILE => dirname(__FILE__) . '/cookie_bsky.txt',
  153.         CURLOPT_POST => true,
  154.         CURLOPT_USERAGENT  => $GLOBALS['UAstring'],
  155.         CURLOPT_HTTPHEADER => [
  156.             "Content-Type: application/json",
  157.         ],
  158.         CURLOPT_POSTFIELDS => json_encode([
  159.             "identifier" => $handle,
  160.             "password" => $password,
  161.         ]),
  162.     ]);
  163.     $response = curl_exec($ch);
  164.     if(curl_error($ch)){
  165.         echo "login error : " . curl_error($ch) . "\n";
  166.         die();
  167.     }
  168.     curl_close($ch);
  169.     $responseJson = json_decode($response, true);
  170.     if(isset($responseJson["accessJwt"])){
  171.         return $responseJson["accessJwt"];
  172.     }else{
  173.         print_r($responseJson);
  174.     }
  175. }
  176.  
  177. function post_w_link($handle, $jwt, $record)
  178. {
  179.     $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.createRecord");
  180.     curl_setopt_array($ch, [
  181.         CURLOPT_CONNECTTIMEOUT => 10,
  182.         CURLOPT_SSL_VERIFYPEER => false,
  183.         CURLOPT_RETURNTRANSFER => true,
  184.         CURLOPT_FOLLOWLOCATION => true,
  185.         CURLOPT_MAXREDIRS => 1000,
  186.         CURLOPT_COOKIEJAR  => dirname(__FILE__) . '/cookie_bsky.txt',
  187.         CURLOPT_COOKIEFILE => dirname(__FILE__) . '/cookie_bsky.txt',
  188.         CURLOPT_POST => true,
  189.         CURLOPT_USERAGENT  => $GLOBALS['UAstring'],
  190.         CURLOPT_HTTPHEADER => [
  191.             "Content-Type: application/json",
  192.             "Authorization: Bearer {$jwt}",
  193.         ],
  194.         CURLOPT_POSTFIELDS => json_encode([
  195.             "repo" => $handle,
  196.             "collection" => "app.bsky.feed.post",
  197.             "record" => $record,
  198.         ]),
  199.     ]);
  200.     $response = curl_exec($ch);
  201.     if(curl_error($ch)){
  202.         echo "post error : " . curl_error($ch) . "\n";
  203.         die();
  204.     }
  205.     curl_close($ch);
  206.     $responseJson = json_decode($response, true);
  207.     return $responseJson;
  208. }
  209.  
  210. function uploadImage($jwt, $imagePath)
  211. {
  212.     $imageData = file_get_contents($imagePath);
  213. //  $mime = mime_content_type($imagePath);
  214.     //Flickr は形式固定なのでローカル保存しない
  215.     //他サイト等て混在する場合は一時的にローカル環境に出力してmime_content_typeで判別
  216.     $mime = 'image/jpeg';
  217.  
  218.     $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.uploadBlob");
  219.     curl_setopt_array($ch, [
  220.         CURLOPT_CONNECTTIMEOUT => 10,
  221.         CURLOPT_SSL_VERIFYPEER => false,
  222.         CURLOPT_RETURNTRANSFER => true,
  223.         CURLOPT_MAXREDIRS => 1000,
  224.         CURLOPT_COOKIEJAR  => dirname(__FILE__) . '/cookie_bsky.txt',
  225.         CURLOPT_COOKIEFILE => dirname(__FILE__) . '/cookie_bsky.txt',
  226.         CURLOPT_POST => true,
  227.         CURLOPT_USERAGENT  => $GLOBALS['UAstring'],
  228.         CURLOPT_HTTPHEADER => [
  229.             "Content-Type: $mime",
  230.             "Authorization: Bearer {$jwt}",
  231.         ],
  232.         CURLOPT_POSTFIELDS => $imageData,
  233.     ]);
  234.  
  235.     $response = curl_exec($ch);
  236.     if(curl_error($ch)){
  237.         echo "upload error : " . curl_error($ch) . "\n";
  238.         die();
  239.     }
  240.     curl_close($ch);
  241.     $responseJson = json_decode($response, true);
  242.     return $responseJson['blob'] ?? null;   //PHP 7.x 以降要
  243.     /*
  244.         3項演算の亜種
  245.         $x ?? $y; は
  246.         isset($x) ? $x : $y; と同じ効果です(PHP 7.x以上)
  247.     */
  248. }
  249. ?>
  250.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement