PHPでBlueskyの投稿を検索・一覧表示する

(1/1)
今回は、PHPで Bluesky APIを利用し、自分が投稿したメッセージを検索し、画面に一覧表示するプログラムを作ってみる。

目次

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

PHPでBlueskyの投稿を検索・表示

サンプル・プログラム

圧縮ファイルの内容
searchBlueskyPosts.phpサンプル・プログラム本体
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
pahooScraping.phpスクレイピング処理に関わるクラス pahooScraping。
スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
searchBlueskyPosts.php 更新履歴
バージョン 更新日 内容
1.0.0 2025/04/05 初版
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
2.1.0 2025/03/20 getUserPosts() 追加
2.0.1 2025/01/24 getPostThread() -- 認証必要のエンドポイントに変更
2.0.0 2025/01/24 トークンを保持するよう改良
1.9.0 2025/01/16 getEmbedPosts() 追加
1.8.1 2024/12/11 getOGPInformation()--リダイレクト対応
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.1 2025/03/15 validRegexPattern() -- debug
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() 追加

準備: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

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

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

pahooBlueskyAPI.php

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

セッション開始メソッド 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

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

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

解説:自分の投稿を取得

自分の投稿を取得するエンドポイントは app.bsky.feed.getAuthorFeed だ。パラメータを GET で渡し、応答を JSON形式データを受け取る REST API である。
なお、この API は自分の投稿を取り出すだけで、投稿内容を検索(フィルタリング)機能はない。検索機能は後述するメイン・プログラムの関数に実装する。
app.bsky.feed.getAuthorFeed
URL
https://{PDSドメイン}/xrpc/app.bsky.feed.getAuthorFeed
入力パラメータ
フィールド名 要否 内  容
actor 必須 did
limit 省略可能 ヒット数の上限(1以上100以下)
省略時は50
filter 省略可能 検索フィルター
指定できる値は posts_with_replies, posts_no_replies, posts_with_media, posts_and_author_threads, posts_with_videoのいずれが1つ
省略時は posts_with_replies
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。

応答データ(JSON形式)

{
    "feed": [
        {
            "post": {
                "uri": "at:\/\/did:plc:{投稿uri:AT形式}",
                "cid": "{CID形式}",
                "author": { (投稿者情報)
                    "did": "did:plc:{AT形式}",
                    "handle": "{ハンドル名}",
                    "displayName": "{ディスプレイ名}",
                    "avatar": "https:{アイコンURL}",
                    "associated": {
                        "chat": {
                            "allowIncoming": "following"
                        }
                    },
                    ---(中略)---
                },
                "record": {
                    "$type": "app.bsky.feed.post",
                    "createdAt": "2025-04-05T12:54:18.848Z", (投稿日時)
                    "embed": { (embed情報)
                    ---(中略)---
                    "facets": [ (facet情報)
                    ---(中略)---
                    "langs": [ (言語情報)
                    ---(中略)---
                    "text": "{投稿文}"
                },
            },
---(以下略)---
}

解説:自分の投稿を取得

pahooBlueskyAPI.php

1218: /**
1219:  * 自分の投稿を取得する
1220:  * @param   string $name  自分のアカウント名
1221:  * @param   string $limit 最大取得数(1以上100以下);50【省略時】
1222:  * @param   int    $limit 最大取得数(1以上100以下);50【省略時】
1223:  * @param   int    $embedFlag embed情報があるかどうか;下記のいずれかの値
1224:  *              指定できる値 = 0(無視), -1(embedがないもの), +1(embedがあるもの)
1225:  * @param   string $filter フィルター;下記のいずれかの文字列
1226:  *              指定できる値 = posts_with_replies, posts_no_replies,
1227:  *                              posts_with_media, posts_and_author_threads,
1228:  *                              posts_with_video
1229:  * @return  array メッセージ情報 / FALSE:取得失敗
1230: */
1231: function getUserPosts($name, $limit=50, $embedFlag=0, $filter='') {
1232:     // 自分のDIDを取得する
1233:     $did = $this->getDID($name);
1234:     if ($did == FALSE) {
1235:         return FALSE;
1236:     }
1237: 
1238:     // リクエストURL【認証必要】
1239:     $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getAuthorFeed?actor=' . urlencode($did. '&limit=' . $limit . '&filter=' . urlencode($filter);
1240:     $ch = curl_init($requestURL);
1241:     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1242:     curl_setopt($ch, CURLOPT_HTTPHEADER, [
1243:         'Content-Type: application/json',
1244:         'Authorization: Bearer ' . $this->accessJwt,
1245:     ]);
1246:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1247:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1248:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //  〃
1249: 
1250:     // レスポンス処理
1251:     $response = curl_exec($ch);
1252:     $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1253: 
1254:     if ($httpStatusCode !200) {
1255:         $this->seterror('メッセージ情報を取得できません(httpステータス異常)');
1256:         return FALSE;
1257:     }
1258:     curl_close($ch);
1259:     $items = json_decode($response, TRUE);
1260: 
1261:     // 情報を整理して返す
1262:     $results = array();
1263:     $cnt = 0;
1264:     foreach ($items['feed'as $item) {
1265:         if (($embedFlag == 0||
1266:             (($embedFlag == -1&& ! isset($item['post']['record']['embed'])) ||
1267:             (($embedFlag == +1&& isset($item['post']['record']['embed']))) {
1268:             preg_match('/\/([a-z0-9]+)$/i', $item['post']['uri'], $arr);
1269:             if (isset($arr[1])) {
1270:                 $results[$cnt]['url'] = 'https://bsky.app/profile/' . $name . '/post/' . $arr[1];
1271:                 $results[$cnt]['text'] = $item['post']['record']['text'];
1272:                 $results[$cnt]['createdAt'] = $item['post']['record']['createdAt'];
1273:             }
1274:             $cnt++;
1275:         }
1276:     }
1277:     return $results;
1278: }

メソッド getUserPosts は、自分のアカウント名(ハンドル名)を渡して、投稿メッセージ情報から必要な項目(URL、投稿文、投稿日時)を配列に格納して返す。

解説:メイン・プログラムの初期値

searchBlueskyPosts.php

  56: // 初期値(START) ===========================================================
  57: // 表示幅(ピクセル)
  58: define('WIDTH', 600);
  59: 
  60: // あなたのハンドル名
  61: define('HANDLENAME', 'pahoo.org');
  62: 
  63: // 検索ワード(初期値)
  64: define('DEF_QUERY', 'Bluesky');
  65: // 検索ワードの最小長
  66: define('QUERY_MINLEN', 3);
  67: // 検索ワードの最大長
  68: define('QUERY_MAXLEN', 50);
  69: 
  70: // 一覧に表示する投稿分の最大長
  71: define('TEXT_MAXLEN', 100);
  72: 
  73: // マッチング関数リスト【変数名の変更不可】
  74: //      label: ラジオボタンに表示するラベル
  75: //      group: ラジオボタン・グループ名
  76: //      fcun:  関数名およびラジオボタンのid
  77: $matchingFunctions = array(
  78: array(
  79:     'label' => '完全一致',
  80:     'group' => 'matchhMethod',
  81:     'func'  => 'matchPerfect',
  82: ), array(
  83:     'label' => '部分一致',
  84:     'group' => 'matchhMethod',
  85:     'func'  => 'matchPartial',
  86: ), array(
  87:     'label' => '正規表現',
  88:     'group' => 'matchhMethod',
  89:     'func'  => 'matchRegularExpression',
  90: ));
  91: 
  92: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
  93: require_once('pahooBlueskyAPI.php');
  94: 
  95: // 初期値(END) =============================================================

メイン・プログラムの初期値は、「変更不可」の記載のないものは自由に変更できる。

配列 $matchingFunctions には、後述する投稿検索の関数やラベルを格納する。要素 func に検索関数名を代入する。もし新たな検索関数を用意したら、ここに追加してほしい。

解説:検索ワードに合致する投稿を検索

searchBlueskyPosts.php

 236: /**
 237:  * 検索ワードに合致する投稿を検索する
 238:  * @param   string $query   検索ワード
 239:  * @param   string $func    マッチング関数
 240:  * @param   Array  $items   検索結果を格納する連想配列
 241:  * @param   string $errmsg  エラーメッセージ(正常時は空文字)
 242:  * @return  string WebAPIのURL
 243: */
 244: function searchBlueskyPosts($query, $func, &$items, &$errmsg) {
 245:     // オブジェクトを生成する.
 246:     $pbs = new pahooBlueskyAPI('bsky.social');
 247: 
 248:     // 自分の投稿を取得する
 249:     $results = $pbs->getUserPosts(HANDLENAME, 100);
 250:     $webapi  = $pbs->webapi;
 251: 
 252:     // エラーの場合
 253:     if ($results == FALSE) {
 254:         $errmsg = $pbs->geterror();
 255: 
 256:     // 検索ワードを探す
 257:     } else {
 258:         $errmsg = '';
 259:         $cnt = 0;
 260:         foreach ($results as $result) {
 261:             if ($func($query, $result['text'], $errmsg) === TRUE) {
 262:                 $items[$cnt]['url']         = $result['url'];
 263:                 $items[$cnt]['text']        = $result['text'];
 264:                 $items[$cnt]['createdAt']   = $result['createdAt'];
 265:                 $cnt++;
 266:             } else if ($errmsg !== '') {
 267:                 break;
 268:             }
 269:         }
 270:     }
 271:     // オブジェクトを解放する.
 272:     $pbs = NULL;
 273: 
 274:     return $webapi;
 275: }

ユーザー関数 searchBlueskyPosts は、検索ワード、マッチング関数等を渡し、自分の投稿を検索した結果を返す。マッチング関数は、前述の配列 $matchingFunctions に代入した関数名を渡す。

この関数の中で、前述のメソッド getUserPosts を呼び出し、自分の投稿を取得して配列 $results に格納する。この配列から投稿を1つずつ取りだし、マッチング関数 $func によって判定し、マッチしたら配列 $items に代入する。

解説:完全一致検索

searchBlueskyPosts.php

 197: /**
 198:  * 完全一致検索を行う.
 199:  * @param   string $pat     検索文字列
 200:  * @param   string $str     検索対象文字列
 201:  * @param   string $errmsg  エラーメッセージ格納用
 202:  * @return  bool TRUE:一致/FALSE:不一致
 203: */
 204: function matchPerfect($pat, $str, &$errmsg) {
 205:     return $pat === $str;
 206: }

ここからは、あらかじめ用意したマッチング関数を紹介する。マッチング関数の引数は共通で、検索文字列、検索対象文字列、エラーメッセージ格納用変数を渡す。

ユーザー関数 matchPerfect は、検索文字列 $pat と検索対象文字列 $str の完全一致検索を行う。等式 === を利用した。

解説:部分一致検索

searchBlueskyPosts.php

 208: /**
 209:  * 部分一致検索を行う.
 210:  * @param   string $pat     検索文字列
 211:  * @param   string $str     検索対象文字列
 212:  * @param   string $errmsg  エラーメッセージ格納用
 213:  * @return  bool TRUE:一致/FALSE:不一致
 214: */
 215: function matchPartial($pat, $str, &$errmsg) {
 216:     return (mb_strstr($str, $pat) === FALSE? FALSE : TRUE;
 217: }

ユーザー関数 matchPartial は、検索文字列 $pat が検索対象文字列 $str に含まれているかどうか、部分一致検索を行う。組み込み関数  mb_strstr  を利用した。

解説:正規表現によるパターンマッチング

searchBlueskyPosts.php

 219: /**
 220:  * 正規表現によるパターンマッチングを行う.
 221:  * @param   string $pat     検索パターン
 222:  * @param   string $str     検索対象文字列
 223:  * @param   string $errmsg  エラーメッセージ格納用
 224:  * @return  bool TRUE:一致/FALSE:不一致
 225: */
 226: function matchRegularExpression($pat, $str, &$errmsg) {
 227:     $reg = '/' . $pat . '/ui';
 228:     if (! validRegexPattern($reg)) {
 229:         $errmsg = '検索ワードが不正です';
 230:         return FALSE;
 231:     } else {
 232:         return (preg_match($reg, $str> 0? TRUE : FALSE;
 233:     }
 234: }

ユーザー関数 matchRegularExpression は、検索文字列 $pat が正規表現で、検索対象文字列 $str とパターンマッチングするかどうかを判定する。
検索文字列 $pat が正規表現であるかどうかをユーザー関数 validRegexPattern を使ってチェックした後、組み込み関数  preg_match  を使ってマッチングする。

参考サイト

(この項おわり)
header