(2025年8月17日)画像に余計な空白が入らないようにするため一部仕様変更.
(2025年8月14日).pahooEnv導入
目次
サンプル・プログラムの実行例
サンプル・プログラム
| embedBlueskyPost.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/01/18 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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: }
解説:メッセージ情報を取得
| URL |
|---|
| https://{PDSドメイン}/xrpc/app.bsky.feed.getPosts?uris={atURI(複数)} |
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
pahooBlueskyAPI.php
1335: /**
1336: * メッセージURLからメッセージ情報を取得する
1337: * @param array $urls メッセージURL(複数)
1338: * @return array メッセージ情報 / FALSE:取得失敗
1339: */
1340: function getPosts($urls) {
1341: $atURIs = [];
1342: foreach ($urls as $url) {
1343: // ユーザー名、投稿IDを取得する
1344: if (preg_match('/\/profile\/([^\/]+)\/post\/([0-9a-zA-Z]+)/ui', $url, $arr) == 0) {
1345: $this->seterror($url . 'は投稿URLではありません');
1346: return FALSE;
1347: }
1348: if (count($arr) < 3) {
1349: $this->seterror($url . '投稿URLではありません');
1350: return FALSE;
1351: }
1352: $userName = $arr[1];
1353: $postID = $arr[2];
1354:
1355: // ユーザーDIDを取得する
1356: $userDID = $this->getDID($userName);
1357: if ($userDID == FALSE) {
1358: $this->seterror($url . 'はユーザーDIDを取得できません');
1359: return FALSE;
1360: }
1361:
1362: // AT-URIを生成する
1363: $atURIs[] = 'at://' . $userDID . '/app.bsky.feed.post/' . $postID;
1364: }
1365:
1366: // トークンを取得する.
1367: $this->getValidToken();
1368:
1369: // リクエストURL (認証必要)
1370: $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getPosts';
1371: $ch = curl_init($requestURL . '?' . http_build_query(['uris' => $atURIs]));
1372: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1373: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1374: 'Content-Type: application/json',
1375: 'Authorization: Bearer ' . $this->accessJwt,
1376: ]);
1377: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1378: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1379: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
1380:
1381: // レスポンス処理
1382: $response = curl_exec($ch);
1383: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1384: if (PHP_VERSION_ID < 80500) {
1385: curl_close($ch);
1386: }
1387: $items = json_decode($response, TRUE);
1388: if ($httpStatusCode != 200) {
1389: $errmsg = 'メッセージ情報を取得できません(http code:' . $httpStatusCode . ')';
1390: if (isset($items['message'])) {
1391: $errmsg .= ';' . $items['message'];
1392: }
1393: $this->seterror($errmsg);
1394: return FALSE;
1395: }
1396:
1397: return $items;
1398: }
解説:埋め込み用HTMLを取得
pahooBlueskyAPI.php
1400: /**
1401: * メッセージURLから埋め込みHTMLを取得する
1402: * @param array $url メッセージURL
1403: * @return string 埋め込みHTML / FALSE:取得失敗
1404: */
1405: function getEmbedPosts($url) {
1406: $items = $this->getPosts([$url]);
1407: if ($items == FALSE) {
1408: return FALSE;
1409: }
1410:
1411: // 投稿日時を日本語に変換する
1412: $createdDateTime = $items['posts'][0]['record']['createdAt'];
1413: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:([0-9]{2})/', $createdDateTime, $arr);
1414: $dt = sprintf('%04d年%d月%d日 %02d:%02d', (int)$arr[1], (int)$arr[2], (int)$arr[3], (int)$arr[4], (int)$arr[5]);
1415: // 埋め込みHTML生成(公式の出力に合わせる)
1416: return <<< EOT
1417: <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>— {$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>
1418:
1419: EOT;
1420: }
参考サイト
- Bluesky 公式リファレンス
- Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法
- PHPでDOMDocumentを使ってスクレイピング:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- PHPでツイートの埋め込み用HTMLを取得:ぱふぅ家のホームページ

そこで今回は、PHPで メッセージ取得用の Bluesky APIを利用し、Bluesky の埋め込み用HTMLを取得するプログラムを作ってみることにする。