(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.8.1 | 2026/08/02 | OGPに収まるよう ASPECT_WIDE の値を変更 |
| 2.8.0 | 2026/07/18 | 画像全体を表示するようパディング方法を変更 |
| 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 導入 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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 = 1700; // 投稿可能な最大画像幅(ピクセル)
30: const MAX_IMAGE_HEIGHT = 1700; // 投稿可能な最大画像高(ピクセル)
31: const MAX_IMAGE_NUMBER = 4; // 投稿可能な最大画像数
32: const MAX_IMAGE_WIDTH_OGP = 1200; // OGPの最大画像幅(ピクセル)
33: const MAX_IMAGE_HEIGHT_OGP = 630; // OGPの最大画像高(ピクセル)
34: const ASPECT_WIDE = 1.91; // 画像アスペクト比(横長)
35: const ASPECT_SQUARE = 1.0; // 画像アスペクト比(正方形)
36: // これより大きいときは自動縮小する
37: // トークンを保存するファイル名
38: // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
39: const FILENAME_TOKEN = './.token';
40:
41: // -- 以下のデータは .env ファイルに記述可能
42: // Bluesky API アプリパスワード
43: // https://bsky.app/
44: public $BLUESKY_HANDLE = ''; // ハンドル名
45: public $BLUESKY_PASSWORD = ''; // アプリケーション・パスワード
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $BLUESKY_PASSWORD に、あなたのハンドル名を $BLUESKY_HANDLE に代入する。
投稿可能な最大文字数は定数 MAX_MESSAGE_LEN として用意した。現在の仕様では300文字だ。
pahooBlueskyAPI.php
47: /**
48: * コンストラクタ
49: * もしAPIエラーが出る場合には,新規セッションにしてみる.
50: * @param string $pds PDSドメイン
51: * @param bool $newSession 新規セッションにするかどうか(TRUE:新規,デフォルトはFALSE)
52: * @return なし
53: */
54: function __construct($pds, $newSession=FALSE) {
55: if (isset($_ENV['PAHOO_BLUESKY_HANDLE'])) {
56: $this->BLUESKY_HANDLE = $_ENV['PAHOO_BLUESKY_HANDLE'];
57: }
58: if (isset($_ENV['PAHOO_BLUESKY_PASSWORD'])) {
59: $this->BLUESKY_PASSWORD = $_ENV['PAHOO_BLUESKY_PASSWORD'];
60: }
61:
62: // プロパティを初期化する.
63: $this->pds = $pds;
64: $this->webapi = '';
65: $this->errmsg = '';
66: $this->accessJwt = '';
67: $this->refreshJwt = '';
68:
69: // 新規セッションを開始する.
70: if ($newSession) {
71: $this->createSession();
72: }
73: }
pahooBlueskyAPI.php
393: /**
394: * 新規セッションを開始する.
395: * @param なし
396: * @return bool TRUE:成功/FALSE:失敗
397: */
398: function createSession() {
399: // エラーメッセージ・クリア
400: $this->clearerror();
401:
402: // リクエストURL
403: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.createSession';
404: $this->webapi = $requestURL;
405: $ch = curl_init($requestURL);
406: // cURLを使ったリクエスト
407: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
408: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
409: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
410: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
411: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
412: curl_setopt($ch, CURLOPT_POST, TRUE);
413: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
414: 'identifier' => $this->BLUESKY_HANDLE,
415: 'password' => $this->BLUESKY_PASSWORD,
416: ]));
417:
418: // レスポンス処理
419: $response = curl_exec($ch);
420: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
421: if ($httpStatusCode != 200) {
422: $this->seterror('セッションを開始できません');
423: return FALSE;
424: }
425: if (PHP_VERSION_ID < 80500) {
426: curl_close($ch);
427: }
428: $items = json_decode($response, TRUE);
429:
430: // エラーチェックとリターン
431: if (isset($items['accessJwt']) && isset($items['refreshJwt'])) {
432: $this->accessJwt = (string)$items['accessJwt'];
433: $this->refreshJwt = (string)$items['refreshJwt'];
434: // トークンをファイルに保存する
435: $contents = $this->accessJwt . "\n" . $this->refreshJwt;
436: file_put_contents(self::FILENAME_TOKEN, $contents);
437: return TRUE;
438: } else if (isset($items['error'])) {
439: $this->seterror($items['message']);
440: return FALSE;
441: } else {
442: $this->seterror('セッションを開始できません');
443: return FALSE;
444: }
445: }
メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。
解説:セッション終了
| URL |
|---|
| https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession |
アクセストークン accessJwt の盗用を避ける意味で、セッションを開始したら、かならずセッション終了するようにしよう。
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
pahooBlueskyAPI.php
500: /**
501: * セッション終了する.
502: * @param なし
503: * @return bool TRUE:成功/FALSE:失敗
504: */
505: function deleteSession() {
506: // リクエストURL
507: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.deleteSession';
508: $this->webapi = $requestURL;
509: $ch = curl_init($requestURL);
510: // cURLを使ったリクエスト
511: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
512: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->accessJwt]);
513: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
514: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
515: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
516: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
517:
518: // レスポンス処理
519: $response = curl_exec($ch);
520: if (curl_errno($ch)) {
521: $this->seterror('セッション終了できません' . curl_error($ch));
522: return FALSE;
523: }
524: if (PHP_VERSION_ID < 80500) {
525: curl_close($ch);
526: }
527: $this->accessJwt = '';
528: $this->refreshJwt = '';
529:
530: return TRUE;
531: }
解説:自分の投稿を取得
なお、この 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
1422: /**
1423: * 自分の投稿を取得する
1424: * @param string $name 自分のアカウント名
1425: * @param string $limit 最大取得数(1以上100以下);50【省略時】
1426: * @param int $limit 最大取得数(1以上100以下);50【省略時】
1427: * @param int $embedFlag embed情報があるかどうか;下記のいずれかの値
1428: * 指定できる値 = 0(無視), -1(embedがないもの), +1(embedがあるもの)
1429: * @param string $filter フィルター;下記のいずれかの文字列
1430: * 指定できる値 = posts_with_replies, posts_no_replies,
1431: * posts_with_media, posts_and_author_threads,
1432: * posts_with_video
1433: * @return array メッセージ情報 / FALSE:取得失敗
1434: */
1435: function getUserPosts($name, $limit=50, $embedFlag=0, $filter='') {
1436: // 自分のDIDを取得する
1437: $did = $this->getDID($name);
1438: if ($did == FALSE) {
1439: return FALSE;
1440: }
1441:
1442: // リクエストURL【認証必要】
1443: $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getAuthorFeed?actor=' . urlencode($did) . '&limit=' . $limit . '&filter=' . urlencode($filter);
1444: $ch = curl_init($requestURL);
1445: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1446: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1447: 'Content-Type: application/json',
1448: 'Authorization: Bearer ' . $this->accessJwt,
1449: ]);
1450: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1451: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1452: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
1453:
1454: // レスポンス処理
1455: $response = curl_exec($ch);
1456: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1457: if (PHP_VERSION_ID < 80500) {
1458: curl_close($ch);
1459: }
1460: $items = json_decode($response, TRUE);
1461: if ($httpStatusCode != 200) {
1462: $errmsg = '自分の投稿を取得できません(http code:' . $httpStatusCode . ')';
1463: if (isset($items['message'])) {
1464: $errmsg .= ';' . $items['message'];
1465: }
1466: $this->seterror($errmsg);
1467: return FALSE;
1468: }
1469:
1470: // 情報を整理して返す
1471: $results = array();
1472: $cnt = 0;
1473: foreach ($items['feed'] as $item) {
1474: if (($embedFlag == 0) ||
1475: (($embedFlag == -1) && ! isset($item['post']['record']['embed'])) ||
1476: (($embedFlag == +1) && isset($item['post']['record']['embed']))) {
1477: preg_match('/\/([a-z0-9]+)$/i', $item['post']['uri'], $arr);
1478: if (isset($arr[1])) {
1479: $results[$cnt]['url'] = 'https://bsky.app/profile/' . $name . '/post/' . $arr[1];
1480: $results[$cnt]['text'] = $item['post']['record']['text'];
1481: $results[$cnt]['createdAt'] = $item['post']['record']['createdAt'];
1482: }
1483: $cnt++;
1484: }
1485: }
1486: return $results;
1487: }
解説:メイン・プログラムの初期値
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を取得:ぱふぅ家のホームページ
