(2025年8月17日)画像に余計な空白が入らないようにするため一部仕様変更.
(2025年8月14日).pahooEnv導入
目次
サンプル・プログラムの実行例
サンプル・プログラム
| searchBlueskyPosts.php | サンプル・プログラム本体 |
| .pahooEnv | クラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。 使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooScraping.php | スクレイピング処理に関わるクラス pahooScraping。 スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.6.0 | 2025/08/14 | .pahooEnv導入 |
| 1.0.0 | 2025/04/05 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.7.1 | 2025/11/22 | PHP8.5対応:curl_close,imagedestroyを実行しないようにした |
| 2.7.0 | 2025/08/17 | reductImage,uploadBlob仕様変更←画像に余計な空白が入らないようにするため |
| 2.6.0 | 2025/08/14 | .pahooEnv 導入 |
| 2.5.1 | 2025/08/10 | uploadBlob() -- bug-fix |
| 2.5.0 | 2025/08/02 | getOGPInformation() -- og:imageがないページに対応 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.1 | 2025/08/11 | getParam() bug-fix |
| 2.0.0 | 2025/08/11 | pahooLoadEnv() 追加 |
| 1.9.0 | 2025/07/26 | getParam() 引数に$trim追加 |
| 1.8.1 | 2025/03/15 | validRegexPattern() debug |
| 1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
準備:PHP の https対応
Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dllLinuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順
これで準備は完了だ。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
解説: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
15: // スクレイピング処理に関わるクラス:include_pathが通ったディレクトリに配置
16: require_once('pahooScraping.php');
17:
18: // Bluesky API クラス =======================================================
19: class pahooBlueskyAPI {
20: public $pds; // PDSドメイン
21: public $webapi ; // 直前に呼び出したWebAPI URL
22: public $errmsg; // エラーメッセージ
23: public $accessJwt; // accessJwt
24: public $refreshJwt; // refreshJwt
25:
26: const INTERNAL_ENCODING = 'UTF-8'; // 内部エンコーディング
27: const MAX_MESSAGE_LEN = 300; // 投稿可能なメッセージ文字数
28: const URL_LEN = 23; // メッセージ中のURL文字数(相当)
29: const MAX_IMAGE_WIDTH = 1200; // 投稿可能な最大画像幅(ピクセル)
30: const MAX_IMAGE_HEIGHT = 675; // 投稿可能な最大画像高さ(ピクセル)
31: // これより大きいときは自動縮小する
32: // トークンを保存するファイル名
33: // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
34: const FILENAME_TOKEN = './.token';
35:
36: // -- 以下のデータは .env ファイルに記述可能
37: // Bluesky API アプリパスワード
38: // https://bsky.app/
39: public $BLUESKY_HANDLE = ''; // ハンドル名
40: public $BLUESKY_PASSWORD = ''; // アプリケーション・パスワード
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $BLUESKY_PASSWORD に、あなたのハンドル名を $BLUESKY_HANDLE に代入する。
投稿可能な最大文字数は定数 MAX_MESSAGE_LEN として用意した。現在の仕様では300文字だ。
pahooBlueskyAPI.php
42: /**
43: * コンストラクタ
44: * もしAPIエラーが出る場合には,新規セッションにしてみる.
45: * @param string $pds PDSドメイン
46: * @param bool $newSession 新規セッションにするかどうか(TRUE:新規,デフォルトはFALSE)
47: * @return なし
48: */
49: function __construct($pds, $newSession=FALSE) {
50: if (isset($_ENV['PAHOO_BLUESKY_HANDLE'])) {
51: $this->BLUESKY_HANDLE = $_ENV['PAHOO_BLUESKY_HANDLE'];
52: }
53: if (isset($_ENV['PAHOO_BLUESKY_PASSWORD'])) {
54: $this->BLUESKY_PASSWORD = $_ENV['PAHOO_BLUESKY_PASSWORD'];
55: }
56:
57: // プロパティを初期化する.
58: $this->pds = $pds;
59: $this->webapi = '';
60: $this->errmsg = '';
61: $this->accessJwt = '';
62: $this->refreshJwt = '';
63:
64: // 新規セッションを開始する.
65: if ($newSession) {
66: $this->createSession();
67: }
68: }
pahooBlueskyAPI.php
360: /**
361: * 新規セッションを開始する.
362: * @param なし
363: * @return bool TRUE:成功/FALSE:失敗
364: */
365: function createSession() {
366: // エラーメッセージ・クリア
367: $this->clearerror();
368:
369: // リクエストURL
370: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.createSession';
371: $this->webapi = $requestURL;
372: $ch = curl_init($requestURL);
373: // cURLを使ったリクエスト
374: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
375: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
376: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
377: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
378: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
379: curl_setopt($ch, CURLOPT_POST, TRUE);
380: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
381: 'identifier' => $this->BLUESKY_HANDLE,
382: 'password' => $this->BLUESKY_PASSWORD,
383: ]));
384:
385: // レスポンス処理
386: $response = curl_exec($ch);
387: // var_dump('*createSession');
388: // var_dump($response);
389: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
390: if ($httpStatusCode != 200) {
391: $this->seterror('セッションを開始できません');
392: return FALSE;
393: }
394: if (PHP_VERSION_ID < 80500) {
395: curl_close($ch);
396: }
397: $items = json_decode($response, TRUE);
398:
399: // エラーチェックとリターン
400: if (isset($items['accessJwt']) && isset($items['refreshJwt'])) {
401: $this->accessJwt = (string)$items['accessJwt'];
402: $this->refreshJwt = (string)$items['refreshJwt'];
403: // トークンをファイルに保存する
404: $contents = $this->accessJwt . "\n" . $this->refreshJwt;
405: file_put_contents(self::FILENAME_TOKEN, $contents);
406: return TRUE;
407: } else if (isset($items['error'])) {
408: $this->seterror($items['message']);
409: return FALSE;
410: } else {
411: $this->seterror('セッションを開始できません');
412: return FALSE;
413: }
414: }
メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。
解説:セッション終了
| URL |
|---|
| https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession |
アクセストークン accessJwt の盗用を避ける意味で、セッションを開始したら、かならずセッション終了するようにしよう。
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
pahooBlueskyAPI.php
472: /**
473: * セッション終了する.
474: * @param なし
475: * @return bool TRUE:成功/FALSE:失敗
476: */
477: function deleteSession() {
478: // リクエストURL
479: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.deleteSession';
480: $this->webapi = $requestURL;
481: $ch = curl_init($requestURL);
482: // cURLを使ったリクエスト
483: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
484: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->accessJwt]);
485: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
486: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
487: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
488: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
489:
490: // レスポンス処理
491: $response = curl_exec($ch);
492: if (curl_errno($ch)) {
493: $this->seterror('セッション終了できません' . curl_error($ch));
494: return FALSE;
495: }
496: if (PHP_VERSION_ID < 80500) {
497: curl_close($ch);
498: }
499: $this->accessJwt = '';
500: $this->refreshJwt = '';
501:
502: return TRUE;
503: }
解説:自分の投稿を取得
なお、この 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
1371: /**
1372: * 自分の投稿を取得する
1373: * @param string $name 自分のアカウント名
1374: * @param string $limit 最大取得数(1以上100以下);50【省略時】
1375: * @param int $limit 最大取得数(1以上100以下);50【省略時】
1376: * @param int $embedFlag embed情報があるかどうか;下記のいずれかの値
1377: * 指定できる値 = 0(無視), -1(embedがないもの), +1(embedがあるもの)
1378: * @param string $filter フィルター;下記のいずれかの文字列
1379: * 指定できる値 = posts_with_replies, posts_no_replies,
1380: * posts_with_media, posts_and_author_threads,
1381: * posts_with_video
1382: * @return array メッセージ情報 / FALSE:取得失敗
1383: */
1384: function getUserPosts($name, $limit=50, $embedFlag=0, $filter='') {
1385: // 自分のDIDを取得する
1386: $did = $this->getDID($name);
1387: if ($did == FALSE) {
1388: return FALSE;
1389: }
1390:
1391: // リクエストURL【認証必要】
1392: $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getAuthorFeed?actor=' . urlencode($did) . '&limit=' . $limit . '&filter=' . urlencode($filter);
1393: $ch = curl_init($requestURL);
1394: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1395: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1396: 'Content-Type: application/json',
1397: 'Authorization: Bearer ' . $this->accessJwt,
1398: ]);
1399: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1400: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1401: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
1402:
1403: // レスポンス処理
1404: $response = curl_exec($ch);
1405: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1406: if (PHP_VERSION_ID < 80500) {
1407: curl_close($ch);
1408: }
1409: $items = json_decode($response, TRUE);
1410: if ($httpStatusCode != 200) {
1411: $errmsg = '自分の投稿を取得できません(http code:' . $httpStatusCode . ')';
1412: if (isset($items['message'])) {
1413: $errmsg .= ';' . $items['message'];
1414: }
1415: $this->seterror($errmsg);
1416: return FALSE;
1417: }
1418:
1419: // 情報を整理して返す
1420: $results = array();
1421: $cnt = 0;
1422: foreach ($items['feed'] as $item) {
1423: if (($embedFlag == 0) ||
1424: (($embedFlag == -1) && ! isset($item['post']['record']['embed'])) ||
1425: (($embedFlag == +1) && isset($item['post']['record']['embed']))) {
1426: preg_match('/\/([a-z0-9]+)$/i', $item['post']['uri'], $arr);
1427: if (isset($arr[1])) {
1428: $results[$cnt]['url'] = 'https://bsky.app/profile/' . $name . '/post/' . $arr[1];
1429: $results[$cnt]['text'] = $item['post']['record']['text'];
1430: $results[$cnt]['createdAt'] = $item['post']['record']['createdAt'];
1431: }
1432: $cnt++;
1433: }
1434: }
1435: return $results;
1436: }
解説:メイン・プログラムの初期値
searchBlueskyPosts.php
60: // 初期値(START) ===========================================================
61:
62: // 表示幅(ピクセル)
63: define('WIDTH', 600);
64:
65: // あなたのハンドル名
66: define('HANDLENAME', 'pahoo.org');
67:
68: // 検索ワード(初期値)
69: define('DEF_QUERY', 'Bluesky');
70: // 検索ワードの最小長
71: define('QUERY_MINLEN', 3);
72: // 検索ワードの最大長
73: define('QUERY_MAXLEN', 50);
74:
75: // 一覧に表示する投稿分の最大長
76: define('TEXT_MAXLEN', 100);
77:
78: // マッチング関数リスト【変数名の変更不可】
79: // label: ラジオボタンに表示するラベル
80: // group: ラジオボタン・グループ名
81: // fcun: 関数名およびラジオボタンのid
82: $matchingFunctions = array(
83: array(
84: 'label' => '完全一致',
85: 'group' => 'matchhMethod',
86: 'func' => 'matchPerfect',
87: ), array(
88: 'label' => '部分一致',
89: 'group' => 'matchhMethod',
90: 'func' => 'matchPartial',
91: ), array(
92: 'label' => '正規表現',
93: 'group' => 'matchhMethod',
94: 'func' => 'matchRegularExpression',
95: ));
96:
97: // 初期値(END) =============================================================
配列 $matchingFunctions には、後述する投稿検索の関数やラベルを格納する。要素 func に検索関数名を代入する。もし新たな検索関数を用意したら、ここに追加してほしい。
解説:検索ワードに合致する投稿を検索
searchBlueskyPosts.php
238: /**
239: * 検索ワードに合致する投稿を検索する
240: * @param string $query 検索ワード
241: * @param string $func マッチング関数
242: * @param Array $items 検索結果を格納する連想配列
243: * @param string $errmsg エラーメッセージ(正常時は空文字)
244: * @return string WebAPIのURL
245: */
246: function searchBlueskyPosts($query, $func, &$items, &$errmsg) {
247: // オブジェクトを生成する.
248: $pbs = new pahooBlueskyAPI('bsky.social');
249:
250: // 自分の投稿を取得する
251: $results = $pbs->getUserPosts(HANDLENAME, 100);
252: $webapi = $pbs->webapi;
253:
254: // エラーの場合
255: if ($results == FALSE) {
256: $errmsg = $pbs->geterror();
257:
258: // 検索ワードを探す
259: } else {
260: $errmsg = '';
261: $cnt = 0;
262: foreach ($results as $result) {
263: if ($func($query, $result['text'], $errmsg) === TRUE) {
264: $items[$cnt]['url'] = $result['url'];
265: $items[$cnt]['text'] = $result['text'];
266: $items[$cnt]['createdAt'] = $result['createdAt'];
267: $cnt++;
268: } else if ($errmsg !== '') {
269: break;
270: }
271: }
272: }
273: // オブジェクトを解放する.
274: $pbs = NULL;
275:
276: return $webapi;
277: }
この関数の中で、前述のメソッド getUserPosts を呼び出し、自分の投稿を取得して配列 $results に格納する。この配列から投稿を1つずつ取りだし、マッチング関数 $func によって判定し、マッチしたら配列 $items に代入する。
解説:完全一致検索
searchBlueskyPosts.php
199: /**
200: * 完全一致検索を行う.
201: * @param string $pat 検索文字列
202: * @param string $str 検索対象文字列
203: * @param string $errmsg エラーメッセージ格納用
204: * @return bool TRUE:一致/FALSE:不一致
205: */
206: function matchPerfect($pat, $str, &$errmsg) {
207: return $pat === $str;
208: }
ユーザー関数 matchPerfect は、検索文字列 $pat と検索対象文字列 $str の完全一致検索を行う。等式 === を利用した。
解説:部分一致検索
searchBlueskyPosts.php
210: /**
211: * 部分一致検索を行う.
212: * @param string $pat 検索文字列
213: * @param string $str 検索対象文字列
214: * @param string $errmsg エラーメッセージ格納用
215: * @return bool TRUE:一致/FALSE:不一致
216: */
217: function matchPartial($pat, $str, &$errmsg) {
218: return (mb_strstr($str, $pat) === FALSE) ? FALSE : TRUE;
219: }
解説:正規表現によるパターンマッチング
searchBlueskyPosts.php
221: /**
222: * 正規表現によるパターンマッチングを行う.
223: * @param string $pat 検索パターン
224: * @param string $str 検索対象文字列
225: * @param string $errmsg エラーメッセージ格納用
226: * @return bool TRUE:一致/FALSE:不一致
227: */
228: function matchRegularExpression($pat, $str, &$errmsg) {
229: $reg = '/' . $pat . '/ui';
230: if (! validRegexPattern($reg)) {
231: $errmsg = '検索ワードが不正です';
232: return FALSE;
233: } else {
234: return (preg_match($reg, $str) > 0) ? TRUE : FALSE;
235: }
236: }
検索文字列 $pat が正規表現であるかどうかをユーザー関数 validRegexPattern を使ってチェックした後、組み込み関数 preg_match を使ってマッチングする。
参考サイト
- Bluesky 公式リファレンス
- Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- PHPでBlueskyの埋め込み用HTMLを取得:ぱふぅ家のホームページ
