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

サンプル・プログラム
searchBlueskyPosts.php | サンプル・プログラム本体 |
pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooScraping.php | スクレイピング処理に関わるクラス pahooScraping。 スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。 |
pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
1.0.0 | 2025/04/05 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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()--リダイレクト対応 |
バージョン | 更新日 | 内容 |
---|---|---|
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 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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対応


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

これで準備は完了だ。
解説:pahooBlueskyAPIクラス
Bluesky APIを利用するメソッドはクラス "pahooBlueskyAPI.php" に分離している。また、このクラスからクラス "pahooScraping.php" を呼び出すので、2つのクラス・ファイルを include_path の通ったディレクトリに配置すること。
解説:セッション開始
URL |
---|
https://{PDSドメイン}/xrpc/com.atproto.server.createSession |
アクセストークン 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 = '***************'; // アプリケーション・パスワード
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $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: }
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: }

メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。
解説:セッション終了
URL |
---|
https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession |
アクセストークン 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: }
解説:自分の投稿を取得
なお、この API は自分の投稿を取り出すだけで、投稿内容を検索(フィルタリング)機能はない。検索機能は後述するメイン・プログラムの関数に実装する。
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 |
応答データ(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: }
解説:メイン・プログラムの初期値
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: }

この関数の中で、前述のメソッド 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: }
解説:正規表現によるパターンマッチング
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: }
検索文字列 $pat が正規表現であるかどうかをユーザー関数 validRegexPattern を使ってチェックした後、組み込み関数 preg_match を使ってマッチングする。
参考サイト
- Bluesky 公式リファレンス
- Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- PHPでBlueskyの埋め込み用HTMLを取得:ぱふぅ家のホームページ