PHPでBlueskyの埋め込み用HTMLを取得

(1/1)
PHPでツイートの埋め込み用HTMLを取得
Bluesky クライアントの機能として、ホームページやブログに投稿メッセージを埋め込むためのHTML取得機能がある。ところが、Twitter(現・X) と異なり、Bluesky API にこの機能が用意されていない。
そこで今回は、PHPで メッセージ取得用の Bluesky APIを利用し、Bluesky の埋め込み用HTMLを取得するプログラムを作ってみることにする。

目次

サンプル・プログラムの実行例

PHPでBlueskyの埋め込み用HTMLを取得

サンプル・プログラム

圧縮ファイルの内容
embedBlueskyPost.phpサンプル・プログラム本体
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
pahooScraping.phpスクレイピング処理に関わるクラス pahooScraping。
スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
embedBlueskyPost.php 更新履歴
バージョン 更新日 内容
1.0.0 2025/01/25 初版
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
2.0.0 2025/01/24 トークンを保持するよう改良
1.9.0 2025/01/16 getEmbedPosts() 追加
1.8.1 2024/12/11 getOGPInformation()--リダイレクト対応
1.8.0 2024/12/06 post()--引用時にも画像やOGP情報を付けられるように
1.7.0 2024/12/06 reductImage()--OGPかどうかに関わらず画像が指定サイズに収めるよう拡大・縮小する仕様に変更
pahooScraping.php 更新履歴
バージョン 更新日 内容
1.2.1 2024/10/31 __construct() 文字化け対策
1.2.0 2024/09/29 getValueFistrXPath() 属性値でない指定に対応
1.1.0 2023/10/15 getValueFistrXPath() 追加
1.0.1 2023/09/29 __construct() bug-fix
1.0.0 2023/09/18 初版
pahooInputData.php 更新履歴
バージョン 更新日 内容
1.8.0 2024/11/12 validRegexPattern() 追加
1.7.0 2024/10/09 validURL() validEmail() 追加
1.6.0 2024/10/07 isButton() -- buttonタグに対応
1.5.0 2024/01/28 exitIfExceedVersion() 追加
1.4.2 2024/01/28 exitIfLessVersion() メッセージ修正

準備:PHP の https対応

Bluesky API の呼び出しはhttps通信で行うため、PHPにOpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOK。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

解説:pahooBlueskyAPIクラス

Bluesky に投稿したりプログラムで操作するAPIについては、公式リファレンスが詳しい。APIを利用するには、事前に、あなたのアカウントから利用登録を行い、アプリパスワードを取得する必要がある。その手順は「Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
Bluesky APIを利用するメソッドはクラス "pahooBlueskyAPI.php" に分離している。また、このクラスからクラス "pahooScraping.php" を呼び出すので、2つのクラス・ファイルを include_path の通ったディレクトリに配置すること。

解説:セッション開始

BlueskyAPI を利用するには、まずセッションを開き、アクセストークン accessJwt を取得する。使用するエンドポイントは com.atproto.server.createSession だ。
com.atproto.server.createSession
URL
https://{PDSドメイン}/xrpc/com.atproto.server.createSession
リクエスト・データ(http) header Content-Type "application/json" post identifier ハンドル名 password アプリケーション・パスワード
レスポンス・データ(json) accessJwt アクセストークン refreshJwt リフレッシュトークン handle ハンドル名 did did
アクセストークン accessJwt は、後述するメッセージや画像の投稿で使用する。寿命は1~2時間だ。
アクセストークン refreshJwt は、アクセストークンの再発行や、セッションの終了・破棄に用いることができ、寿命は数十日と長い。リフレッシュトークン refreshJwt をストレージに保存しておき、次回はアクセストークンを再発行するというのが BlueskyAPI の望ましい運用方法と思われるが、リフレッシュトークン refreshJwt だけでアクセストークン accessJwt を再発行できてしまうので、流出するとたいへん危険である。

今回つくるプログラムは、単発でメッセージや画像を投稿するものなので、リフレッシュトークン refreshJwt は使わず、プログラム起動時にアクセストークン accessJwt を取得するようにする。

pahooBlueskyAPI.php

  11: // スクレイピング処理に関わるクラス:include_pathが通ったディレクトリに配置
  12: require_once('pahooScraping.php');
  13: 
  14: // Bluesky API クラス =======================================================
  15: class pahooBlueskyAPI {
  16:     var $pds;           // PDSドメイン
  17:     var $webapi;        // 直前に呼び出したWebAPI URL
  18:     var $errmsg;        // エラーメッセージ
  19:     var $accessJwt;     // accessJwt
  20:     var $refreshJwt;    // refreshJwt
  21: 
  22:     const INTERNAL_ENCODING = 'UTF-8';  // 内部エンコーディング
  23:     const MAX_MESSAGE_LEN = 300;        // 投稿可能なメッセージ文字数
  24:     const URL_LEN = 23;                 // メッセージ中のURL文字数(相当)
  25:     const MAX_IMAGE_WIDTH  = 1200;      // 投稿可能な最大画像幅(ピクセル)
  26:     const MAX_IMAGE_HEIGHT = 630;       // 投稿可能な最大画像高さ(ピクセル)
  27:                                         // これより大きいときは自動縮小する
  28:     // トークンを保存するファイル名
  29:     // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
  30:     const FILENAME_TOKEN = './.token';
  31: 
  32:     // Bluesky API アプリパスワード
  33:     // https://bsky.app/
  34:     var $BLUESKY_HANDLE   = '***************';      // ハンドル名
  35:     var $BLUESKY_PASSWORD = '***************';      // アプリケーション・パスワード

BlueskyAPI を利用するユーザー定義クラス pahooBlueskyAPIをつくる。
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $BLUESKY_PASSWORD に、あなたのハンドル名を $BLUESKY_HANDLE に代入する。
投稿可能な最大文字数は定数 MAX_MESSAGE_LEN として用意した。現在の仕様では300文字だ。

pahooBlueskyAPI.php

  35: /**
  36:  * コンストラクタ
  37:  * もしAPIエラーが出る場合には,新規セッションにしてみる.
  38:  * @param   string $pds PDSドメイン
  39:  * @param   bool $newSession 新規セッションにするかどうか(TRUE:新規,デフォルトはFALSE)
  40:  * @return  なし
  41: */
  42: function __construct($pds, $newSession=FALSE) {
  43:     $this->pds        = $pds;
  44:     $this->webapi     = '';
  45:     $this->errmsg     = '';
  46:     $this->accessJwt  = '';
  47:     $this->refreshJwt = '';
  48: 
  49:     // 新規セッションを開始する.
  50:     if ($newSession) {
  51:         $this->createSession();
  52:     }
  53: }

コンストラクタの引数は PDSドメインで、変数 $pds に保管し、API呼び出し時に参照できるようにした。

pahooBlueskyAPI.php

 345: /**
 346:  * 新規セッションを開始する.
 347:  * @param   なし
 348:  * @return  bool TRUE:成功/FALSE:失敗
 349: */
 350: function createSession() {
 351:     // エラーメッセージ・クリア
 352:     $this->clearerror();
 353: 
 354:     // リクエストURL
 355:     $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.createSession';
 356:     $this->webapi = $requestURL;
 357:     $ch = curl_init($requestURL);
 358:     // cURLを使ったリクエスト
 359:     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
 360:     curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
 361:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
 362:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
 363:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //  〃
 364:     curl_setopt($ch, CURLOPT_POST, TRUE);
 365:     curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
 366:         'identifier' => $this->BLUESKY_HANDLE,
 367:         'password'   => $this->BLUESKY_PASSWORD,
 368:     ]));
 369: 
 370:     // レスポンス処理
 371:     $response = curl_exec($ch);
 372:     $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
 373:     if ($httpStatusCode !200) {
 374:         $this->seterror('セッションを開始できません');
 375:         return FALSE;
 376:     }
 377:     curl_close($ch);
 378:     $items = json_decode($response, TRUE);
 379: 
 380:     // エラーチェックとリターン
 381:     if (isset($items['accessJwt']) && isset($items['refreshJwt'])) {
 382:         $this->accessJwt = (string)$items['accessJwt'];
 383:         $this->refreshJwt = (string)$items['refreshJwt'];
 384:         // トークンをファイルに保存する
 385:         $contents = $this->accessJwt . "\n" . $this->refreshJwt;
 386:         file_put_contents(self::FILENAME_TOKEN, $contents);
 387:         return TRUE;
 388:     } else if (isset($items['error'])) {
 389:         $this->seterror($items['message']);
 390:         return FALSE;
 391:     } else {
 392:         $this->seterror('セッションを開始できません');
 393:         return FALSE;
 394:     }
 395: }

セッション開始メソッド createSession は、引数はなく、セッション開始に成功したかどうかを戻り値にする。

メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。

解説:セッション終了

アクセストークン accessJwt のセッションを終了するには、エンドポイント com.atproto.server.deleteSession を呼び出す。
com.atproto.server.deleteSession
URL
https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession
リクエスト・データ(json) header Content-Type "application/json" post identifier "Bearer {アクセストークン}" password アプリケーション・パスワード
セッション開始時に取得したアクセストークン accessJwt を使い、このセッションをクローズする。以後、このアクセストークン accessJwt は使用できなくなる。
アクセストークン accessJwt の盗用を避ける意味で、セッションを開始したら、かならずセッション終了するようにしよう。

APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。

pahooBlueskyAPI.php

 448: /**
 449:  * セッション終了する.
 450:  * @param   なし
 451:  * @return  bool TRUE:成功/FALSE:失敗
 452: */
 453: function deleteSession() {
 454:     // リクエストURL
 455:     $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.deleteSession';
 456:     $this->webapi = $requestURL;
 457:     $ch = curl_init($requestURL);
 458:     // cURLを使ったリクエスト
 459:     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
 460:     curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->accessJwt]);
 461:     curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
 462:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
 463:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
 464:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //  〃
 465: 
 466:     // レスポンス処理
 467:     $response = curl_exec($ch);
 468:     if (curl_errno($ch)) {
 469:         $this->seterror('セッション終了できません' . curl_error($ch));
 470:         return FALSE;
 471:     }
 472:     curl_close($ch);
 473:     $this->accessJwt = '';
 474:     $this->refreshJwt = '';
 475:     return TRUE;
 476: }

セッション終了メソッド deleteSession は、引数はなく、セッション開始に成功したかどうかを戻り値にする。

解説:メッセージ情報を取得

メッセージ情報を取得するエンドポイントは app.bsky.feed.getPosts だ。
app.bsky.feed.getPosts
URL
https://{PDSドメイン}/xrpc/app.bsky.feed.getPosts?uris={atURI(複数)}
取得したい複数のメッセージの atURI をGETで渡すことで、一度の呼び出しで複数のメッセージ情報を取得することができる。

APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
レスポンス・データ(json) posts uri メッセージURI(atURI):1つ目 cid メッセージCID author did DID handle ユーザー名 displayName 表示名 avatar アバター画像URL createdAt 登録日時(UTC) record $type "app.bsky.feed.post"(OGP情報) text メッセージ(UTF-8) createdAt 投稿日時(ローカル時刻;ISO 8601形式) embed $type "app.bsky.embed.external" external uri コンテンツURL thumb メディアをアップしたPDSのUR title コンテンツのタイトル description コンテンツの概要 facets index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL replyCount リプライ数 repostCount リポスト数 likeCount いいね数 quoteCount 引用数 uri メッセージURI(atURI):2つ目 cid メッセージCID author did DID handle ユーザー名 displayName 表示名 avatar アバター画像URL createdAt 登録日時(UTC) record $type "app.bsky.feed.post"(OGP情報) text メッセージ(UTF-8) createdAt 投稿日時(ローカル時刻;ISO 8601形式) embed $type "app.bsky.embed.external" external uri コンテンツURL thumb メディアをアップしたPDSのUR title コンテンツのタイトル description コンテンツの概要 facets index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL replyCount リプライ数 repostCount リポスト数 likeCount いいね数 quoteCount 引用数

pahooBlueskyAPI.php

1126: /**
1127:  * メッセージURLからメッセージ情報を取得する
1128:  * @param   array $urls メッセージURL(複数)
1129:  * @return  array メッセージ情報 / FALSE:取得失敗
1130: */
1131: function getPosts($urls) {
1132:     $atURIs = [];
1133:     foreach ($urls as $url) {
1134:         // ユーザー名、投稿IDを取得する
1135:         if (preg_match('/\/profile\/([^\/]+)\/post\/([0-9a-zA-Z]+)/ui', $url, $arr) == 0) {
1136:             $this->seterror($url . 'は投稿URLではありません');
1137:             return FALSE;
1138:         }
1139:         if (count($arr< 3) {
1140:             $this->seterror($url . '投稿URLではありません');
1141:             return FALSE;
1142:         }
1143:         $userName = $arr[1];
1144:         $postID   = $arr[2];
1145: 
1146:         // ユーザーDIDを取得する
1147:         $userDID = $this->getDID($userName);
1148:         if ($userDID == FALSE) {
1149:             $this->seterror($url . 'はユーザーDIDを取得できません');
1150:             return FALSE;
1151:         }
1152: 
1153:         // AT-URIを生成する
1154:         $atURIs[] = 'at://' . $userDID . '/app.bsky.feed.post/' . $postID;
1155:     }
1156: 
1157:     // トークンを取得する.
1158:     $this->getValidToken();
1159: 
1160:     // リクエストURL (認証必要)
1161:     $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getPosts';
1162:     $ch = curl_init($requestURL . '?' . http_build_query(['uris' => $atURIs]));
1163:     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1164:     curl_setopt($ch, CURLOPT_HTTPHEADER, [
1165:         'Content-Type: application/json',
1166:         'Authorization: Bearer ' . $this->accessJwt,
1167:     ]);
1168:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1169:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1170:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //  〃
1171: 
1172:     // レスポンス処理
1173:     $response = curl_exec($ch);
1174:     $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1175: 
1176:     if ($httpStatusCode !200) {
1177:         $this->seterror('メッセージ情報を取得できません(httpステータス異常)');
1178:         return FALSE;
1179:     }
1180:     curl_close($ch);
1181:     $items = json_decode($response, TRUE);
1182: 
1183:     return $items;
1184: }

メッセージ情報を取得メソッド getPosts は、引数に atURI ではなく BlueskyメッセージのURLを配列で渡す。内部でURLを atURI に変換してから BlueskyAPI に渡すようにしている。

解説:埋め込み用HTMLを取得

pahooBlueskyAPI.php

1186: /**
1187:  * メッセージURLから埋め込みHTMLを取得する
1188:  * @param   array $url メッセージURL
1189:  * @return  string 埋め込みHTML / FALSE:取得失敗
1190: */
1191: function getEmbedPosts($url) {
1192:     $items = $this->getPosts([$url]);
1193:     if ($items == FALSE) {
1194:         return FALSE;
1195:     }
1196: 
1197:     // 投稿日時を日本語に変換する
1198:     $createdDateTime = $items['posts'][0]['record']['createdAt'];
1199:     preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:([0-9]{2})/', $createdDateTime, $arr);
1200:     $dt = sprintf('%04d年%d月%d日 %02d:%02d', (int)$arr[1], (int)$arr[2], (int)$arr[3], (int)$arr[4], (int)$arr[5]);
1201:     // 埋め込みHTML生成(公式の出力に合わせる)
1202:     return <<< EOT
1203: <blockquote class="bluesky-embed" data-bluesky-uri="{$items['posts'][0]['uri']}" data-bluesky-cid="{$items['posts'][0]['cid']}"><p lang="">{$items['posts'][0]['record']['text']}<br><br><a href="https://bsky.app/profile/{$items['posts'][0]['uri']}?ref_src=embed">[image or embed]</a></p>&mdash; {$items['posts'][0]['author']['displayName']} (<a href="https://bsky.app/profile/{$items['posts'][0]['author']['did']}?ref_src=embed">@{$items['posts'][0]['author']['handle']}</a>) <a href="https://bsky.app/profile/{$items['posts'][0]['uri']}?ref_src=embed">{$dt}</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
1204: 
1205: EOT;
1206: }

メソッド getEmbedPosts は、メッセージ情報URLを1つ渡すと、前述のメソッド getPosts を利用して、戻ってきたメッセージ情報をHTML形式に成形する。整形テンプレートは、Bluesky 公式が出力するものに合わせた。

参考サイト

(この項おわり)
header