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

サンプル・プログラム
embedBlueskyPost.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/01/25 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
2.0.0 | 2025/01/24 | トークンを保持するよう改良 |
1.9.0 | 2025/01/16 | getEmbedPosts() 追加 |
1.8.1 | 2024/12/11 | getOGPInformation()--リダイレクト対応 |
1.8.0 | 2024/12/06 | post()--引用時にも画像やOGP情報を付けられるように |
1.7.0 | 2024/12/06 | reductImage()--OGPかどうかに関わらず画像が指定サイズに収めるよう拡大・縮小する仕様に変更 |
バージョン | 更新日 | 内容 |
---|---|---|
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.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() 追加 |
1.4.2 | 2024/01/28 | exitIfLessVersion() メッセージ修正 |
準備: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
35: /**
36: * コンストラクタ
37: * もしAPIエラーが出る場合には,新規セッションにしてみる.
38: * @param string $pds PDSドメイン
39: * @param bool $newSession 新規セッションにするかどうか(TRUE:新規,デフォルトはFALSE)
40: * @return なし
41: */
42: function __construct($pds, $newSession=FALSE) {
43: $this->pds = $pds;
44: $this->webapi = '';
45: $this->errmsg = '';
46: $this->accessJwt = '';
47: $this->refreshJwt = '';
48:
49: // 新規セッションを開始する.
50: if ($newSession) {
51: $this->createSession();
52: }
53: }
pahooBlueskyAPI.php
345: /**
346: * 新規セッションを開始する.
347: * @param なし
348: * @return bool TRUE:成功/FALSE:失敗
349: */
350: function createSession() {
351: // エラーメッセージ・クリア
352: $this->clearerror();
353:
354: // リクエストURL
355: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.createSession';
356: $this->webapi = $requestURL;
357: $ch = curl_init($requestURL);
358: // cURLを使ったリクエスト
359: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
360: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
361: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
362: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
363: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
364: curl_setopt($ch, CURLOPT_POST, TRUE);
365: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
366: 'identifier' => $this->BLUESKY_HANDLE,
367: 'password' => $this->BLUESKY_PASSWORD,
368: ]));
369:
370: // レスポンス処理
371: $response = curl_exec($ch);
372: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
373: if ($httpStatusCode != 200) {
374: $this->seterror('セッションを開始できません');
375: return FALSE;
376: }
377: curl_close($ch);
378: $items = json_decode($response, TRUE);
379:
380: // エラーチェックとリターン
381: if (isset($items['accessJwt']) && isset($items['refreshJwt'])) {
382: $this->accessJwt = (string)$items['accessJwt'];
383: $this->refreshJwt = (string)$items['refreshJwt'];
384: // トークンをファイルに保存する
385: $contents = $this->accessJwt . "\n" . $this->refreshJwt;
386: file_put_contents(self::FILENAME_TOKEN, $contents);
387: return TRUE;
388: } else if (isset($items['error'])) {
389: $this->seterror($items['message']);
390: return FALSE;
391: } else {
392: $this->seterror('セッションを開始できません');
393: return FALSE;
394: }
395: }

メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。
解説:セッション終了
URL |
---|
https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession |
アクセストークン accessJwt の盗用を避ける意味で、セッションを開始したら、かならずセッション終了するようにしよう。

APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
pahooBlueskyAPI.php
448: /**
449: * セッション終了する.
450: * @param なし
451: * @return bool TRUE:成功/FALSE:失敗
452: */
453: function deleteSession() {
454: // リクエストURL
455: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.deleteSession';
456: $this->webapi = $requestURL;
457: $ch = curl_init($requestURL);
458: // cURLを使ったリクエスト
459: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
460: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->accessJwt]);
461: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
462: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
463: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
464: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
465:
466: // レスポンス処理
467: $response = curl_exec($ch);
468: if (curl_errno($ch)) {
469: $this->seterror('セッション終了できません' . curl_error($ch));
470: return FALSE;
471: }
472: curl_close($ch);
473: $this->accessJwt = '';
474: $this->refreshJwt = '';
475: return TRUE;
476: }
解説:メッセージ情報を取得
URL |
---|
https://{PDSドメイン}/xrpc/app.bsky.feed.getPosts?uris={atURI(複数)} |

APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
pahooBlueskyAPI.php
1126: /**
1127: * メッセージURLからメッセージ情報を取得する
1128: * @param array $urls メッセージURL(複数)
1129: * @return array メッセージ情報 / FALSE:取得失敗
1130: */
1131: function getPosts($urls) {
1132: $atURIs = [];
1133: foreach ($urls as $url) {
1134: // ユーザー名、投稿IDを取得する
1135: if (preg_match('/\/profile\/([^\/]+)\/post\/([0-9a-zA-Z]+)/ui', $url, $arr) == 0) {
1136: $this->seterror($url . 'は投稿URLではありません');
1137: return FALSE;
1138: }
1139: if (count($arr) < 3) {
1140: $this->seterror($url . '投稿URLではありません');
1141: return FALSE;
1142: }
1143: $userName = $arr[1];
1144: $postID = $arr[2];
1145:
1146: // ユーザーDIDを取得する
1147: $userDID = $this->getDID($userName);
1148: if ($userDID == FALSE) {
1149: $this->seterror($url . 'はユーザーDIDを取得できません');
1150: return FALSE;
1151: }
1152:
1153: // AT-URIを生成する
1154: $atURIs[] = 'at://' . $userDID . '/app.bsky.feed.post/' . $postID;
1155: }
1156:
1157: // トークンを取得する.
1158: $this->getValidToken();
1159:
1160: // リクエストURL (認証必要)
1161: $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getPosts';
1162: $ch = curl_init($requestURL . '?' . http_build_query(['uris' => $atURIs]));
1163: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1164: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1165: 'Content-Type: application/json',
1166: 'Authorization: Bearer ' . $this->accessJwt,
1167: ]);
1168: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1169: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1170: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
1171:
1172: // レスポンス処理
1173: $response = curl_exec($ch);
1174: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1175:
1176: if ($httpStatusCode != 200) {
1177: $this->seterror('メッセージ情報を取得できません(httpステータス異常)');
1178: return FALSE;
1179: }
1180: curl_close($ch);
1181: $items = json_decode($response, TRUE);
1182:
1183: return $items;
1184: }
解説:埋め込み用HTMLを取得
pahooBlueskyAPI.php
1186: /**
1187: * メッセージURLから埋め込みHTMLを取得する
1188: * @param array $url メッセージURL
1189: * @return string 埋め込みHTML / FALSE:取得失敗
1190: */
1191: function getEmbedPosts($url) {
1192: $items = $this->getPosts([$url]);
1193: if ($items == FALSE) {
1194: return FALSE;
1195: }
1196:
1197: // 投稿日時を日本語に変換する
1198: $createdDateTime = $items['posts'][0]['record']['createdAt'];
1199: preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:([0-9]{2})/', $createdDateTime, $arr);
1200: $dt = sprintf('%04d年%d月%d日 %02d:%02d', (int)$arr[1], (int)$arr[2], (int)$arr[3], (int)$arr[4], (int)$arr[5]);
1201: // 埋め込みHTML生成(公式の出力に合わせる)
1202: return <<< EOT
1203: <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>
1204:
1205: EOT;
1206: }
参考サイト
- Bluesky 公式リファレンス
- Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法
- PHPでDOMDocumentを使ってスクレイピング:ぱふぅ家のホームページ
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- PHPでツイートの埋め込み用HTMLを取得:ぱふぅ家のホームページ
そこで今回は、PHPで メッセージ取得用の Bluesky APIを利用し、Bluesky の埋め込み用HTMLを取得するプログラムを作ってみることにする。