(2024年11月30日)画像が指定幅より小さい時,画像が透明背景の時の処理を追加
(2024年11月24日)不具合修正
(2024年11月22日)PHP8.4対応
(2024年11月21日)ハッシュタグに対応した
(2024年11月10日)入力メッセージの残文字数とプログレスバーを表示するようにした
(2024年10月31日)文字化け対策
(2024年10月22日)ローカル画像を投稿できるよう修正.画像投稿の優先順位修正
(2024年10月21日)返信,引用ができるよう機能追加
(2024年10月18日)必要に応じて画像の縦方向も縮小するようにした
(2024年10月13日)必要に応じて画像を縮小するようにした
サンプル・プログラムの実行例
目次
サンプル・プログラム
postBluesky.php | サンプル・プログラム本体 |
pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooScraping.php | スクレイピング処理に関わるクラス pahooScraping。 スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。 |
pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
1.4.0 | 2024/12/06 | 画像のドラッグ&ドロップ,コピー&ペーストに対応 |
1.3.0 | 2024/11/10 | 入力メッセージの残文字数とプログレスバー表示 |
1.2.0 | 2024/11/01 | 返信URL代入機能を追加 |
1.1.0 | 2024/10/20 | 返信,引用ができるよう機能追加 |
1.0.0 | 2024/10/01 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
1.8.0 | 2024/12/06 | post()--引用時にも画像やOGP情報を付けられるように |
1.7.0 | 2024/12/06 | reductImage()--OGPかどうかに関わらず画像が指定サイズに収めるよう拡大・縮小する仕様に変更 |
1.6.1 | 2024/12/04 | getRootParentID() -- API仕様変更対応 |
1.6.0 | 2024/11/30 | convertTransparentToWhite() 追加 |
1.5.0 | 2024/11/30 | reductImage() -- 指定幅より小さい場合を追加;reductImage, uploadBlobの引数に$flagCard追加 |
バージョン | 更新日 | 内容 |
---|---|---|
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クラス
基本的に、POSTプロトコルでデータを渡し、JSON形式で応答が戻ってくるAPIであるが、Bluesky は分散型SNSと呼ばれるように、PDS(Personal Data Server)が複数存在し、APIもPDSの中に入っている。これを AT Protocol と呼び、PDSが稼動しているドメインを PDSドメインと呼ぶ。
PDSドメインは、ユーザーによって変わる可能性がある。たとえばハンドル名 hoge.bsky.social であれば、bsky.social が PDSドメインである。
BlueskyAPI は、機能ごとにエンドポイントが用意されており、API呼び出しURLは htttps://{PDSドメイン}/xrpc/{エンドポント} となる。
APIに対する操作は、ユーザー定義クラス pahooBlueskyAPI にカプセル化した。
左図に、今回利用する BlueskyAPI と、それを呼び出すメソッドを整理した。
これ以外にも、APIは呼び出さないが、メッセージ中からURLを取り出すメソッド getURLs や、画像URL(画像など)を取り出すメソッド extractMediaURL などが利用できる。
今回の目的であるメッセージ投稿については、後述 post メソッドに一元化したが、リンクURLが画像URLが含まれていたり、返信や引用をするときには、post から別のメソッドを呼び出して投稿に必要な追加情報を取得する形にした。
解説:セッション開始
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:
21: const INTERNAL_ENCODING = 'UTF-8'; //内部エンコーディング
22: const MAX_MESSAGE_LEN = 300; // 投稿可能なメッセージ文字数
23: const URL_LEN = 23; // メッセージ中のURL文字数(相当)
24: const MAX_IMAGE_WIDTH = 1200; // 投稿可能な最大画像幅(ピクセル)
25: const MAX_IMAGE_HEIGHT = 630; // 投稿可能な最大画像高さ(ピクセル)
26: // これより大きいときは自動縮小する
27:
28: // Bluesky API アプリパスワード
29: // https://bsky.app/
30: var $BLUESKY_HANDLE = '***************'; // ハンドル名
31: var $BLUESKY_PASSWORD = '***************'; // アプリケーション・パスワード
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $BLUESKY_PASSWORD に、あなたのハンドル名を $BLUESKY_HANDLE に代入する。
投稿可能な最大文字数は定数 MAX_MESSAGE_LEN として用意した。現在の仕様では300文字だ。
pahooBlueskyAPI.php
33: /**
34: * コンストラクタ
35: * @param string $pds PDSドメイン
36: * @return なし
37: */
38: function __construct($pds) {
39: $this->pds = $pds;
40: $this->webapi = '';
41: $this->errmsg = '';
42: $this->accessJwt = '';
43: }
pahooBlueskyAPI.php
286: /**
287: * セッション開始する.
288: * @param なし
289: * @return bool TRUE:成功/FALSE:失敗
290: */
291: function createSession() {
292: //エラーメッセージ・クリア
293: $this->clearerror();
294:
295: // リクエストURL
296: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.createSession';
297: $this->webapi = $requestURL;
298: $ch = curl_init($requestURL);
299: // cURLを使ったリクエスト
300: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
301: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
302: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
303: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //サーバ証明書検証をスキップ
304: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
305: curl_setopt($ch, CURLOPT_POST, TRUE);
306: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
307: 'identifier' => $this->BLUESKY_HANDLE,
308: 'password' => $this->BLUESKY_PASSWORD,
309: ]));
310:
311: // レスポンス処理
312: $response = curl_exec($ch);
313: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
314: if ($httpStatusCode != 200) {
315: $this->seterror('セッション開始できません');
316: return FALSE;
317: }
318: curl_close($ch);
319: $items = json_decode($response, TRUE);
320:
321: // エラーチェックとリターン
322: if (isset($items['accessJwt'])) {
323: $this->accessJwt = (string)$items['accessJwt'];
324: return TRUE;
325: } else if (isset($items['error'])) {
326: $this->seterror($items['message']);
327: return FALSE;
328: } else {
329: $this->seterror('セッション開始できません');
330: return FALSE;
331: }
332: }
メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。
解説:セッション終了
URL |
---|
https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession |
アクセストークン accessJwt の盗用を避ける意味で、セッションを開始したら、かならずセッション終了するようにしよう。
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
pahooBlueskyAPI.php
334: /**
335: * セッション終了する.
336: * @param なし
337: * @return bool TRUE:成功/FALSE:失敗
338: */
339: function deleteSession() {
340: // リクエストURL
341: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.deleteSession';
342: $this->webapi = $requestURL;
343: $ch = curl_init($requestURL);
344: // cURLを使ったリクエスト
345: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
346: curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->accessJwt]);
347: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
348: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
349: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
350: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
351:
352: // レスポンス処理
353: $response = curl_exec($ch);
354: if (curl_errno($ch)) {
355: $this->seterror('セッション終了できません' . curl_error($ch));
356: return FALSE;
357: }
358: curl_close($ch);
359: $this->accessJwt = '';
360: return TRUE;
361: }
解説:投稿用URLやハッシュタグ情報を取得
メッセージからURLやハッシュタグの位置情報を取りだすメソッドが getRichTextPositions である。複数のURLやハッシュタグに対応している。
pahooBlueskyAPI.php
153: /**
154: * 指定したテキスト中のURLやハッシュタグの位置情報を取得する.
155: * @param string $text テキスト
156: * @return Array 位置情報
157: */
158: function getRichTextPositions($text) {
159: $urlData = array();
160:
161: // URL
162: $regexURL = '/(https?:\/\/[^\s]+)/ui';
163: preg_match_all($regexURL, $text, $matches, PREG_OFFSET_CAPTURE);
164: foreach ($matches[0] as $match) {
165: $url = $match[0];
166: $start = $match[1];
167: $end = $start + strlen($url);
168:
169: $urlData[] = array(
170: 'type' => 'link',
171: 'start' => $start,
172: 'end' => $end,
173: 'url' => $url,
174: );
175: }
176:
177: // ハッシュタグ
178: $regexHashTag = '/(#[\p{L}\p{N}_\-.]+)/ui';
179: preg_match_all($regexHashTag, $text, $matches, PREG_OFFSET_CAPTURE);
180: foreach ($matches[0] as $match) {
181: $hashtag = $match[0];
182: $start = $match[1];
183: $end = $start + strlen($hashtag);
184:
185: $urlData[] = array(
186: 'type' => 'tag',
187: 'start' => $start,
188: 'end' => $end,
189: 'tag' => $hashtag,
190: );
191: }
192: return $urlData;
193: }
pahooBlueskyAPI.php
195: /**
196: * 投稿用URLやハッシュタグ情報を取得する.
197: * @param string $text テキスト
198: * @return Array 投稿用facets情報
199: */
200: function parseRichText($text) {
201: $positions = $this->getRichTextPositions($text);
202: $results = $facets = array();
203: if (! empty($positions)) {
204: foreach ($positions as $position) {
205: // URL
206: if ($position['type'] == 'link') {
207: $facets[] = [
208: 'index' => [
209: 'byteStart' => $position['start'],
210: 'byteEnd' => $position['end'],
211: ],
212: 'features' => [
213: [
214: '$type' => 'app.bsky.richtext.facet#link',
215: 'uri' => $position['url'],
216: ],
217: ],
218: ];
219: // ハッシュタグ
220: } else if ($position['type'] == 'tag') {
221: $facets[] = [
222: 'index' => [
223: 'byteStart' => $position['start'],
224: 'byteEnd' => $position['end'],
225: ],
226: 'features' => [
227: [
228: '$type' => 'app.bsky.richtext.facet#tag',
229: 'tag' => ltrim($position['tag'], '#'),
230: ],
231: ],
232: ];
233: }
234: }
235: $results = [
236: 'facets' => $facets,
237: ];
238: }
239:
240: return $results;
241: }
このように、クライアント側で用意するデータ構造によって、URLとハッシュタグを同列のハイパーリンクとして扱う Bluesky の設計には感心させられた。ハッシュタグの方はリンク先情報を渡さないが、実際には Bluesky の検索URLにハイパーリンクする。将来的に検索機能も分散方式になった場合でも対応が容易であり、じつに拡張性のある設計だ。
解説:画像データの扱い
- メッセージ中に画像URLを記述する。
- 画像ファイルをコピー&ペーストする。
- 画像ファイルをドラッグ&ドロップする。
1.の手順はサーバ側で、後述するPHPのユーザー定義メソッド extractMediaURL を使って行う。
2.と3.の手順はクライアント側で、JavaScriptを使って行う。2.の手順については「JavaScriptでクリップボードの画像取得+リサイズ」を、3.の手順については「解説:ファイルのドロップ――PHPで撮影場所をマッピング」をご覧いただきたい。
1~3の手順で取得した画像データは、後述するユーザー定義メソッド uploadBlob を使って Bluesky API によりアップロードする。
解説:投稿メッセージから画像URLを抽出
pahooBlueskyAPI.php
243: /**
244: * 指定したテキストから画像URLを抜き出して配列に格納する.
245: * テキストはUTF-8で指定すること.
246: * 画像拡張子$extに複数の拡張子を指定できる.省略時は 'jpg|png|webp|bmp'
247: * @param string $str テキスト
248: * @param array $urls 画像URLを格納する配列
249: * @param string $ext 画像拡張子;省略時 jpg|bng|webp|bmp
250: * @return string 画像URLを除いたテキスト
251: */
252: function extractMediaURL($str, &$urls, $ext='jpg|png|webp|bmp|mp4|mp3') {
253: // http記法
254: $pat1 = '/https?\:\/\/[\-_\.\!\~\*\'\(\)a-zA-Z0-9\;\/\?\:\@\&\=\+\$\,\%\#]+(' . $ext . ')/i';
255: // file記法
256: $pat2 = '/file\:\/\/\/((.*?)(' . $ext . '))/i';
257:
258: // 画像URLを抜き出す.
259: if (preg_match_all($pat1, $str, $arr) > 0) {
260: foreach ($arr[0] as $url) {
261: $urls[] = $url;
262: }
263: // テキストから画像URLを消去する.
264: $str = str_replace($urls, '', $str);
265: }
266:
267: // ローカル画像を抜き出す.
268: if (preg_match_all($pat2, $str, $arr) > 0) {
269: $fnames = array();
270: foreach ($arr[1] as $key=>$fname) {
271: // 画像ファイルかどうかを判定する.
272: if (exif_imagetype($fname) != FALSE) {
273: $urls[] = $fname;
274: $fnames[] = $arr[0][$key];
275: }
276: }
277: // テキストからローカル画像を消去する.
278: $str = str_replace($fnames, '', $str);
279: }
280: // 余分な空白を削除する.
281: $str = trim($str);
282:
283: return $str;
284: }
引数 $str に、画像URLやローカルファイル名を含んだメッセージ(文字列)を、引数 $urls に抽出した画像URLやローカルファイル名を配列として格納する。画像の拡張子は引数 $ext に指定する。区切り文字はパイプ #x7C; である。
インターネット上にある画像URLは "http:// または "https://" ではじまるURLである。
ローカルファイル名は "file:///" ではじまるファイル名である。ローカルファイル名の場合、引数 $urls には "file:///" を除いたローカルファイル名を格納する。PHPで処理しているので、このローカルファイル名はサーバにおけるファイル名であることに留意されたい。
戻り値は、引数 $str から $urls に格納した画像URLやローカルファイル名を除いた残りのテキスト文字列である。
解説:画像をアップロード
URL |
---|
https://{PDSドメイン}/xrpc/com.atproto.repo.uploadBlob |
pahooBlueskyAPI.php
532: /**
533: * 画像をアップロードする.
534: * 画像ファイルなどを投稿するときに事前に呼び出し,blobデータを投稿する.
535: * @param string $message 投稿メッセージ(UTF-8限定)
536: * @param int $maxWidth アップロードする画像の最大幅(ピクセル)
537: * @param int $maxHeight アップロードする画像の最大高(ピクセル)
538: * @param bool $flagFixedSize TRUE:画像を最大幅・高に加工する(背景白色)
539: * @return string Blusky PDSのURL/FALSE:アップロード失敗
540: */
541: function uploadBlob($filename, $maxWidth=self::MAX_IMAGE_WIDTH,
542: $maxHeight=self::MAX_IMAGE_HEIGHT, $flagFixedSize=FALSE) {
543:
544: $mimeType = '';
545: $fileSize = 0;
546:
547: // エラーメッセージ・クリア
548: $this->clearerror();
549:
550: // 画像を読み込む
551: $imageData = file_get_contents($filename);
552: if ($imageData === FALSE) {
553: $this->seterror($filename . ' の読み込みに失敗しました');
554: return FALSE;
555: }
556: // MIMEタイプを判定する
557: $finfo = new finfo(FILEINFO_MIME_TYPE);
558: $mimeType = (string)$finfo->buffer($imageData);
559: $finfo = NULL;
560:
561: // 必要に応じて画像データを縮小する
562: $imageData = $this->reductImage($imageData, $mimeType, $maxWidth, $maxHeight, $flagFixedSize);
563:
564: // 透明背景を白色で塗りつぶす(投稿したときに黒背景になってしまうため)
565: $imageData = $this->convertTransparentToWhite($imageData, $mimeType);
566:
567: // リクエストURL
568: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.repo.uploadBlob';
569: $this->webapi = $requestURL;
570: // cURLを使ったリクエスト
571: $ch = curl_init($requestURL);
572: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
573: curl_setopt($ch, CURLOPT_HTTPHEADER, [
574: 'Authorization: Bearer ' . $this->accessJwt,
575: 'Accept: application/json',
576: 'Content-Type: ' . $mimeType,
577: ]);
578: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
579: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //サーバ証明書検証をスキップ
580: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
581: curl_setopt($ch, CURLOPT_POST, TRUE);
582: // curl_setopt($ch, CURLOPT_BINARYTRANSFER, TRUE);
583: curl_setopt($ch, CURLOPT_POSTFIELDS, $imageData);
584:
585: // レスポンス処理
586: $response = curl_exec($ch);
587: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
588: if ($httpStatusCode != 200) {
589: $this->seterror('画像をアップロードできません');
590: return FALSE;
591: }
592: curl_close($ch);
593: $items = json_decode($response, TRUE);
594:
595: // エラーチェックとリターン
596: if (isset($items['blob'])) {
597: return $items['blob'];
598: } else if (isset($items['error'])) {
599: $this->seterror($items['message']);
600: return FALSE;
601: } else {
602: $this->seterror('画像をアップロードできません');
603: return FALSE;
604: }
605: }
ここで、画像の最大幅を超えた場合は後述する reductImageを呼び出し、自動的に最大幅・最大高に縮小する。なお、引数の画像の最大幅は省略可能で、省略時には冒頭の定数に定義する MAX_IMAGE_WIDTH を代入する。
メソッドの中身は、上述のAPI仕様の通りに作った。
画像ファイルは、組み込み関数 file_get_contents を使って変数 $imageData に格納する。画像のMIMEタイプを判定するのに、finfoクラスを利用した。
解説:画像を指定幅・高さに収まるように拡大・縮小する
また、Bluesky はTwitter(現・X)と異なり、OGP情報の画像は常に横長の画像として表示する。そこで、OGP情報として縦長の画像を登録するときは、全体が収まるよう縮小した上で、余白(背景)を白色にするフラグ $flagFixedSize を用意した。
pahooBlueskyAPI.php
403: /**
404: * 画像データを指定幅・高さに収まるように拡大・縮小する
405: * @param string $imageData 画像データ(画像ファイルから読み込んだバイナリ)
406: * @param string $mimeType 縮小後の画像のMIMEタイプ
407: * @param int $maxWidth 画像データの最大幅(ピクセル)
408: * @param int $maxHeight 画像データの最大高(ピクセル)
409: * @param bool $flagFixedSize TRUE:画像を最大幅・高に加工する(背景白色)
410: * @return string 縮小後の画像データ/FALSE 対応していない画像フォーマット
411: */
412: function reductImage($imageData, $mimeType='image/jpeg',
413: $maxWidth=self::MAX_IMAGE_WIDTH, $maxHeight=self::MAX_IMAGE_HEIGHT,
414: $flagFixedSize=FALSE) {
415:
416: // 拡大・縮小倍率
417: $scale = 1.0;
418:
419: // 画像フォーマットを取得する
420: if (preg_match('/\/([a-z]+)/i', $mimeType, $arr) > 0) {
421: $imageFormat = $arr[1];
422: } else {
423: $imageFormat = 'jpeg';
424: }
425:
426: // GD画像データに変換する
427: $imageSource = imagecreatefromstring($imageData);
428: if (! $imageSource) {
429: $this->seterror('画像データを縮小できません');
430: return FALSE;
431: }
432:
433: // 元の画像の幅・高さを取得
434: $originalWidth = imagesx($imageSource);
435: $originalHeight = imagesy($imageSource);
436:
437: // リサイズ倍率を計算する
438: $scaleW = $maxWidth / $originalWidth;
439: $scaleH = $maxHeight / $originalHeight;
440: $scale = min($scaleW, $scaleH);
441: $newWidth = (int)($originalWidth * $scale);
442: $newHeight = (int)($originalHeight * $scale);
443:
444: // リサイズ後の画像オブジェクトを用意
445: $imageResize = imagecreatetruecolor($newWidth, $newHeight);
446: // 透明色の処理(PNGやGIFの場合)
447: imagealphablending($imageResize, FALSE);
448: imagesavealpha($imageResize, TRUE);
449: $transparent = imagecolorallocatealpha($imageResize, 255, 255, 255, 127);
450: imagefilledrectangle($imageResize, 0, 0, $newWidth, $newHeight, $transparent);
451: // 画像リサイズ実行
452: imagecopyresampled($imageResize, $imageSource, 0, 0, 0, 0, $newWidth, $newHeight, $originalWidth, $originalHeight);
453:
454: // 画像を最大幅・高に加工する(背景白色)
455: if ($flagFixedSize) {
456: // 白色背景を作成する
457: $imageBackground = imagecreatetruecolor($maxWidth, $maxHeight);
458: $white = imagecolorallocate($imageBackground, 255, 255, 255);
459: imagefill($imageBackground, 0, 0, $white);
460: // 画像を背景の中央に配置するための座標計算
461: $dstX = (int)(($maxWidth - $newWidth) / 2);
462: $dstY = (int)(($maxHeight - $newHeight) / 2);
463: // $imageResize を白色背景にコピー
464: imagecopy($imageBackground, $imageResize, $dstX, $dstY, 0, 0, $newWidth, $newHeight);
465: // リサイズした画像をバイナリ形式に変換する
466: $imageData = $this->image2binary($imageBackground, $imageFormat);
467: // メモリ解放
468: imagedestroy($imageBackground);
469:
470: // 画像縮小のみの場合
471: } else {
472: // リサイズした画像をバイナリ形式に変換する
473: $imageData = $this->image2binary($imageResize, $imageFormat);
474: }
475: // メモリ解放
476: imagedestroy($imageResize);
477:
478: return $imageData;
479: }
まず、引数として渡された $mimeType から画像形式を取り出して変数 $imageFormat に代入する。これは拡大・縮小後の画像形式を保つための処理だ。
画像の拡大・縮小には GD関数群を利用する。
その前に、 imagecreatefromstring 関数を使い、引数で渡された画像データ(バイナリデータ)をGD画像データに変換する。次に、 imagesx 関数を使い、画像の幅と高さを取得する。
最大画像幅・高と比較して、縮小率を計算し変数 $scale に代入する。
サイズ後の画像オブジェクト $imageResize を用意し、 imagealphablending 関数、 imagesavealphag 関数、 imagecolorallocatealpha 関数、 imagefilledrectangle 関数を使って透明色の処理を行ったら、 imagecopyresampled 関数を使ってリサイズを実行する。
最後に、GD画像データを画像データ(バイナリデータ)に変換するには、後述する image2binaryメソッドを適用し、メモリを解放する。
$imageFormat が TRUE のときは、 imagecreatetruecolor 関数、 imagecolorallocate 関数、 imagefill 関数を使って新しい白一色の画像を生成する。元画像を白色画像の中央に配置するための座標計算をしたら、 imagecopy 関数を使って2つの画像を合成する。
pahooBlueskyAPI.php
363: /**
364: * GD画像データをバイナリデータに変換する.
365: * @param string $image GD画像データ
366: * @param string $imageFormat 変換する画像フォーマット(jpeg, png, gif)
367: * @return string バイナリデータ/FALSE 対応していない画像フォーマット
368: */
369: function image2binary($image, $imageFormat='jpeg') {
370: // 出力バッファリングを開始
371: ob_start();
372:
373: // 画像フォーマットに応じて変換関数を選択
374: switch ($imageFormat) {
375: case 'jpeg':
376: imagejpeg($image, NULL, 75);
377: break;
378: case 'png':
379: imagepng($image, NULL, 5);
380: break;
381: case 'gif':
382: imagegif($image);
383: break;
384: case 'webp':
385: imagewebp($image, NULL, 75);
386: break;
387: case 'bmp':
388: imagebmp($image);
389: break;
390: case 'avif':
391: imageavif($image, NULL, 50);
392: break;
393: default:
394: return FALSE;
395: }
396:
397: // バッファ内容を取得する
398: $binaryData = ob_get_clean();
399:
400: return $binaryData;
401: }
GD関数群にある imagejpeg 関数などを使い、画面に出力される画像データ(バイナリ)を、 ob_start 関数を使って横取りすることで変数に格納する。
画像フォーマットに応じて変換するGD関数を選択し、最後に ob_get_clean 関数を使って変数に格納する。
解説:透明背景を白色で塗りつぶす
pahooBlueskyAPI.php
481: /**
482: * 透明背景を白色で塗りつぶす
483: * Blueskyに投稿したときに黒背景になってしまうため
484: * @param string $imageData 画像データ(画像ファイルから読み込んだバイナリ)
485: * @return string 変換後の画像データ/FALSE 対応していない画像フォーマット
486: */
487: function convertTransparentToWhite($imageData, $mimeType='image/png') {
488: // 画像フォーマットを取得する
489: if (preg_match('/\/([a-z]+)/i', $mimeType, $arr) > 0) {
490: $imageFormat = $arr[1];
491: } else {
492: $imageFormat = 'png';
493: }
494:
495: // アルファチャネルをサポートしていない画像フォーマットはそのままリターン
496: if (! preg_match('/png|webp|tiff|psd|exr|ico/i', $imageFormat)) {
497: return $imageData;
498: }
499:
500: // GD画像データに変換する
501: $imageSource = imagecreatefromstring($imageData);
502: if (! $imageSource) {
503: $this->seterror('画像データを読み込めません');
504: return FALSE;
505: }
506:
507: // 画像の幅・高さを取得
508: $width = imagesx($imageSource);
509: $height = imagesy($imageSource);
510:
511: // 背景画像を作成する
512: $imageResult = imagecreatetruecolor($width, $height);
513:
514: // 白色を作成して塗りつぶす
515: $white = imagecolorallocate($imageResult, 255, 255, 255);
516: imagefill($imageResult, 0, 0, $white);
517:
518: // 元の画像を新しい画像にコピーする
519: imagecopy($imageResult, $imageSource, 0, 0, 0, 0, $width, $height);
520:
521: // アルファブレンドを無効化して保存する
522: imagesavealpha($imageResult, FALSE);
523:
524: // リサイズした画像をバイナリ形式に変換する
525: $imageData = $this->image2binary($imageResult, $imageFormat);
526: // メモリ解放
527: imagedestroy($imageResult);
528:
529: return $imageData;
530: }
解説:OGP情報を取得
OGP情報 とは、HTMLコンテンツのheadタグの中に含まれている次のタグを指す。
<head prefix="og: https://ogp.me/ns#">
<meta property="og:url" content="{コンテンツURL}">
<meta property="og:type" content="article">
<meta property="og:title" content="{コンテンツ・タイトル}">
<meta property="og:description" content="{コンテンツの概要}">
<meta property="og:site_name" content="{サイト名}">
<meta property="og:image" content="{代表画像URL}">
pahooBlueskyAPI.php
607: /**
608: * OGP情報を取得する.
609: * @param string $url 対象コンテンツ
610: * @return array OGP情報(embed形式)/NULL:OGP情報はない
611: */
612: function getOGPInformation($url) {
613: $contents = '';
614: if (($infp = fopen($url, 'r')) == FALSE) return NULL;
615: while (! feof($infp)) {
616: $contents .= fread($infp, 5000);
617: }
618: fclose($infp);
619:
620: // 文字化け対策:読み込んだコンテンツをUTF-8に変換
621: $contents = mb_convert_encoding($contents, self::INTERNAL_ENCODING, 'auto');
622:
623: // コンテンツからOGP情報を抽出する
624: $pcr = new pahooScraping($contents);
625: $oggImage = $pcr->getValueFistrXPath('//meta[@property="og:image"]', 'content');
626: $oggDescription = $pcr->getValueFistrXPath('//meta[@property="og:description"]', 'content');
627: $oggTitle = $pcr->getValueFistrXPath('//meta[@property="og:title"]', 'content');
628: $pcr = NULL;
629:
630: // OGP情報がない
631: if (($oggImage == '') || ($oggDescription == '') || ($oggTitle == '')) {
632: return NULL;
633: }
634:
635: // embedに成形する
636: $mimeType = '';
637: $fileSize = 0;
638: $image = $this->uploadBlob($oggImage, self::MAX_IMAGE_WIDTH, self::MAX_IMAGE_HEIGHT, TRUE);
639: if ($image == FALSE) return NULL;
640: $embed = [
641: 'embed' => [
642: '$type' => 'app.bsky.embed.external',
643: 'external' => [
644: 'uri' => $url,
645: 'thumb' => $image,
646: 'title' => $oggTitle,
647: 'description' => $oggDescription,
648: ]
649: ]
650: ];
651: return $embed;
652: }
Twitter(現・X) APIは、メッセージにURLを記載するだけで、Twitterボットが OGP情報 を探して非同期にアップロードするが、Bluesky API ではクライアント側でアップロードしてやる必要がある。Bluesky は分散型SNSであるため、ボットに複雑な作業をさせないよう、クライアント側で処理する仕様になっているものと思われる。そのおかげで、後述するように、引用投稿にOGP情報や画像を含めるという、Twitter(現・X) で実装されていない投稿を可能にしている。
解説:ユーザーのDIDを取得する
ユーザーの DID は、ユーザー・プロファイル情報を取得するエンドポイントは app.bsky.actor.getProfile だ。
URL (public) |
---|
https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor={ユーザー名} |
URL (認証必要) |
https://{PDSドメイン}/xrpc/app.bsky.actor.getProfile?actor={ユーザー名} |
目的とするユーザーDIDは、上述のレスポンスの 1項目に過ぎないので、まず、ユーザー名を与えてエンドポイント app.bsky.actor.getProfile を呼び出すメソッド getProfile を作成し、得られたレスポンスからユーザーDIDだけを返すメソッド getDID の2つを用意した。
pahooBlueskyAPI.php
654: /**
655: * ユーザー・プロファイル情報を取得する
656: * @param string $name ユーザーのアカウント名
657: * @return array ユーザー・プロファイル情報 / FALSE:取得失敗
658: */
659: function getProfile($name) {
660: // リクエストURL (public)
661: $requestURL = 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile';
662: // リクエストURL (認証必要)
663: // $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.actor.getProfile';
664: $this->webapi = $requestURL;
665:
666: // cURLを使ったリクエスト
667: $ch = curl_init();
668: curl_setopt($ch, CURLOPT_URL, $requestURL . '?actor=' . $name);
669: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
670: curl_setopt($ch, CURLOPT_HTTPHEADER, [
671: 'Content-Type: application/json',
672: 'Authorization: Bearer ' . $this->accessJwt,
673: ]);
674: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
675: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //サーバ証明書検証をスキッ
676: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
677:
678: // レスポンス処理
679: $response = curl_exec($ch);
680: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
681: if ($httpStatusCode != 200) {
682: $this->seterror('ユーザー・プロファイル情報を取得できません(httpステータス異常)');
683: return FALSE;
684: }
685: curl_close($ch);
686: $items = json_decode($response, TRUE);
687:
688: return $items;
689: }
pahooBlueskyAPI.php
691: /**
692: * ユーザーのDIDを取得する
693: * @param string $name ユーザーのアカウント名
694: * @return string ユーザーのDID / FALSE:取得失敗
695: */
696: function getDID($name) {
697: $userProfiles = $this->getProfile($name);
698:
699: if ($userProfiles == FALSE) {
700: return FALSE;
701: } else if (! isset($userProfiles['did'])) {
702: $this->seterror('ユーザーのDIDを取得できません)');
703: return FALSE;
704: } else {
705: return $userProfiles['did'];
706: }
707: }
解説:ルートIDと親IDを取得する
URL (public) |
---|
https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri={atURI} |
URL (認証必要) |
https://{PDSドメイン}/xrpc/app.bsky.feed.getPostThread?uri={atURI} |
Bluesky は、分散型SNSであるため、1つ1つのメッセージを管理するIDを URI(Uniform Resource Identifier)で行っている。Bluesky の専用URIを atURI と呼び、次のような構造をしている。
at://{ユーザーDID}/app.bsky.feed.post/{ポストID}ポストIDは、投稿メッセージのURLから得ることができる。
https://bsky.app/profile/{ユーザー名}/post/{ポストID}ユーザーDID は、前述のメソッド getDID を使って取得する。
一方、スレッドになっている場合は、下図のようなレスポンスが返る。こちらも抜粋になる。
そこで、返信/引用元メッセージのURLを与え、ここから atURI を生成し、エンドポイント app.bsky.feed.getPostThread を呼び出すメソッド getPostThread を作成し、得られたルートID と親IDの2つを返すメソッド getRootParentID の2つを用意した。
pahooBlueskyAPI.php
710: /**
711: * メッセージURLからスレッド情報を取得する
712: * @param string $url メッセージURL
713: * @return array スレッド情報情報 / FALSE:取得失敗
714: */
715: function getPostThread($url) {
716: // ユーザー名、投稿IDを取得する
717: if (preg_match('/\/profile\/([^\/]+)\/post\/([0-9a-zA-Z]+)/ui', $url, $arr) == 0) {
718: $this->seterror($url . 'は投稿URLではありません');
719: return FALSE;
720: }
721: if (count($arr) < 3) {
722: $this->seterror($url . '投稿URLではありません');
723: return FALSE;
724: }
725: $userName = $arr[1];
726: $postID = $arr[2];
727:
728: // ユーザーDIDを取得する
729: $userDID = $this->getDID($userName);
730: if ($userDID == FALSE) {
731: return FALSE;
732: }
733:
734: // AT-URIを生成する
735: $atURI = 'at://' . $userDID . '/app.bsky.feed.post/' . $postID;
736:
737: // リクエストURL (public)
738: $requestURL = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread';
739: // リクエストURL (認証必要)
740: // $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getPostThread';
741: $ch = curl_init($requestURL . '?uri=' . urlencode($atURI));
742: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
743: curl_setopt($ch, CURLOPT_HTTPHEADER, [
744: 'Content-Type: application/json',
745: 'Authorization: Bearer ' . $this->accessJwt,
746: ]);
747: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
748: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //サーバ証明書検証をスキッ
749: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
750:
751: // レスポンス処理
752: $response = curl_exec($ch);
753: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
754:
755: if ($httpStatusCode != 200) {
756: $this->seterror('ルートID,親IDを取得できません(httpステータス異常)');
757: return FALSE;
758: }
759: curl_close($ch);
760: $items = json_decode($response, TRUE);
761:
762: return $items;
763: }
pahooBlueskyAPI.php
765: /**
766: * メッセージURLからルートIDと親IDを取得する
767: * @param string $url メッセージURL
768: * @return array(ルートID, 親の投稿ID) / FALSE:取得失敗
769: */
770: function getRootParentID($url) {
771: // スレッド情報を取得する
772: $items = $this->getPostThread($url);
773: if ($items == FALSE) return FALSE;
774:
775: // ルートIDを取得する
776: // スレッドがあればrootを取得する
777: if (isset($items['thread']['post']['record']['reply']['root'])) {
778: $rootID = $items['thread']['post']['record']['reply']['root'];
779: // スレッドがなければ投稿IDを取得する
780: } else if (isset($items['thread']['post']['cid'])) {
781: $rootID = array(
782: 'cid' => $items['thread']['post']['cid'],
783: 'uri' => $items['thread']['post']['uri']
784: );
785: } else {
786: $this->seterror('ルートIDを取得できません');
787: return FALSE;
788: }
789: // 親IDを取得する(常に投稿ID)
790: if (isset($items['thread']['post']['cid'])) {
791: $parentID = array(
792: 'cid' => $items['thread']['post']['cid'],
793: 'uri' => $items['thread']['post']['uri']
794: );
795: } else {
796: $this->seterror('親IDを取得できません');
797: return FALSE;
798: }
799:
800: return array($rootID, $parentID);
801: }
解説:メッセージ投稿
URL |
---|
https://{PDSドメイン}/xrpc/com.atproto.repo.createRecord |
pahooBlueskyAPI.php
803: /**
804: * メッセージを投稿する.
805: * リンクが含まれている場合は自動的にハイパーリンクに変換する.
806: * 画像へのリンクが含まれている場合は自動的にアップロードする(4個まで).
807: * @param string $message 投稿メッセージ(UTF-8限定)
808: * @param bool $flagCard FALSE:カード形式で投稿しない(省略時)
809: * TRUE:OOGP情報がある最初のリンクをカード形式で投稿する
810: * @param string $replyURL NULL:返信しない(省略時)/返信する投稿URL
811: * @param string $quoteURL NULL:引用しない(省略時)/引用する投稿URL
812: * @param array $media NULL:使用しない(省略時)/画像データ配列
813: * @return string メッセージURL/FALSE:失敗
814: */
815: function post($message, $flagCard=FALSE, $replyURL=NULL, $quoteURL=NULL, $media=NULL) {
816: // エラーメッセージ・クリア
817: $this->clearerror();
818:
819: // 初期化
820: $embed = NULL;
821: $images = array();
822: $urls = array();
823: $reply = array();
824:
825: // 返信の場合
826: if ($replyURL != NULL) {
827: $res = $this->getRootParentID($replyURL);
828: if (! $res) {
829: return FALSE;
830: }
831: $rootID = $res[0];
832: $parentID = $res[1];
833: $reply = [
834: 'reply' => [
835: 'root' => $rootID,
836: 'parent' => $parentID,
837: ]
838: ];
839: }
840:
841: // メッセージ中から画像へのリンクを抽出する
842: $message = $this->extractMediaURL($message, $urls);
843:
844: // 画像投稿を行う
845: if ($media != NULL) {
846: $cnt = 1;
847: foreach ($media as $data) {
848: $tmpname = $this->saveTempFile($data);
849: $image = $this->uploadBlob($tmpname, self::MAX_IMAGE_WIDTH, self::MAX_IMAGE_HEIGHT, FALSE);
850: unlink($tmpname);
851: $images = array_merge($images, [['alt' => '', 'image' => $image]]);
852: $cnt++;
853: if ($cnt > 4) break;
854: }
855: $embed = [
856: 'embed' => [
857: '$type' => 'app.bsky.embed.images',
858: 'images' => $images,
859: ]
860: ];
861: // メッセージ中に画像URL等が含まれている場合
862: } else if (($embed == NULL) && (count($urls) > 0)) {
863: $cnt = 1;
864: foreach ($urls as $filename) {
865: // 画像アップロード(必要に応じてリサイズ)
866: $image = $this->uploadBlob($filename, self::MAX_IMAGE_WIDTH, self::MAX_IMAGE_HEIGHT, FALSE);
867: $images = array_merge($images, [['alt' => '', 'image' => $image]]);
868: $cnt++;
869: if ($cnt > 4) break;
870: }
871: $embed = [
872: 'embed' => [
873: '$type' => 'app.bsky.embed.images',
874: 'images' => $images,
875: ]
876: ];
877: // OGP情報を取得する
878: } else if ($flagCard) {
879: if (preg_match_all('/https?\:\/\/[^\s]+/', $message, $arr) > 0) {
880: foreach ($arr[0] as $url) {
881: $embed = $this->getOGPInformation($url);
882: if ($embed != NULL) {
883: break;
884: }
885: }
886: }
887: }
888:
889: // 引用処理
890: if ($quoteURL != NULL) {
891: $res = $this->getRootParentID($quoteURL);
892: if (! $res) {
893: return FALSE;
894: }
895: $parentID = $res[1];
896: // 画像やOGP情報がある場合
897: if ($embed != NULL) {
898: $embed = [
899: 'embed' => [
900: '$type' => 'app.bsky.embed.recordWithMedia',
901: 'media' => $embed['embed'],
902: 'record' => [
903: '$type' => 'app.bsky.embed.record',
904: 'record' => $parentID,
905: ],
906: ]
907: ];
908: } else {
909: $embed = [
910: 'embed' => [
911: '$type' => 'app.bsky.embed.record',
912: 'record' => $parentID,
913: ]
914: ];
915: }
916: }
917:
918: // URLやハッシュ情報の取得
919: $facets = $this->parseRichText($message);
920:
921: // POSTデータ配列を作成する
922: $records = [
923: '$type' => 'app.bsky.feed.post',
924: 'text' => $message,
925: 'createdAt' => (new DateTime())->format('c'),
926: ];
927: if ($replyURL == NULL) {
928: if ($embed == NULL) {
929: $records = array_merge($records, $facets);
930: } else {
931: $records = array_merge($records, $facets, $embed);
932: }
933: } else {
934: if ($embed == NULL) {
935: $records = array_merge($records, $facets, $reply);
936: } else {
937: $records = array_merge($records, $facets, $reply, $embed);
938: }
939: }
940: // var_dump($records);
941:
942: // リクエストURL
943: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.repo.createRecord';
944: $this->webapi = $requestURL;
945: // cURLを使ったリクエスト
946: $ch = curl_init($requestURL);
947: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
948: curl_setopt($ch, CURLOPT_HTTPHEADER, [
949: 'Content-Type: application/json',
950: 'Authorization: Bearer ' . $this->accessJwt,
951: ]);
952: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
953: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //サーバ証明書検証をスキップ
954: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
955: curl_setopt($ch, CURLOPT_POST, TRUE);
956: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
957: 'repo' => $this->BLUESKY_HANDLE,
958: 'collection' => 'app.bsky.feed.post',
959: 'record' => $records,
960: ]));
961:
962: // レスポンス処理
963: $response = curl_exec($ch);
964: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
965: if ($httpStatusCode != 200) {
966: $this->seterror('投稿できません(httpステータス異常)');
967: return FALSE;
968: }
969: curl_close($ch);
970: $items = json_decode($response, TRUE);
971: // var_dump($items);
972:
973: // エラーチェックとリターン
974: if (isset($items['uri'])) {
975: if (preg_match('/\/([0-9a-zA-Z]+)$/ui', $items['uri'], $arr) > 0) {
976: $url = 'https://bsky.app/profile/' . $this->BLUESKY_HANDLE . '/post/' . $arr[1];
977: } else {
978: $url = '';
979: }
980: return $url;
981: } else if (isset($items['error'])) {
982: $this->seterror($items['message']);
983: return FALSE;
984: } else {
985: $this->seterror('投稿できません(応答不正)');
986: return FALSE;
987: }
988: }
解説:メイン・プログラム
postBluesky.php
35: // データ入力に関わる関数群:include_pathに配置すること
36: require_once('pahooInputData.php');
37:
38: // PHPバージョン・チェック
39: exitIfLessVersion(MINUMUM_VERSION);
40:
41: // リファラチェック+リリースフラグの設定
42: if (isset($_SERVER['HTTP_HOST']) && ($_SERVER['HTTP_HOST'] == 'localhost')) {
43: define('FLAG_RELEASE', FALSE);
44: define('REFER_ON', '');
45: ini_set('display_errors', 1);
46: ini_set('error_reporting', E_ALL);
47: } else {
48: // リリース・フラグ(公開時にはTRUEにすること)
49: define('FLAG_RELEASE', TRUE);
50: // リファラ・チェック(直リン防止用;空文字ならチェックしない)
51: if (! isCommandLine()) {
52: define('REFER_ON', 'www.pahoo.org');
53: } else {
54: define('REFER_ON', '');
55: }
56: }
57:
58: // 表示幅(ピクセル)
59: define('WIDTH', 600);
60:
61: // 投稿可能な最大画像数
62: define('MAX_IMAGES', 4);
63:
64: // 投稿メッセージ(初期値)
65: define('DEF_MESSAGE', 'PHPでBlueskyにメッセージや画像を投稿するプログラムを作成。カード形式の投稿や返信、引用投稿も可能。API操作はクラスファイルに分離し、他プログラムからも利用可能。他サイト配布以外のプログラムやライブラリは不要。 https://www.pahoo.org/e-soul/webtech/php06/php06-30-01.shtm');
66:
67: // BlueskyAPIクラス:include_pathが通ったディレクトリに配置
68: require_once('pahooBlueskyAPI.php');
postBluesky.php
661: // メイン・プログラム =======================================================
662: // オブジェクトを生成する.
663: $pbs = new pahooBlueskyAPI('bsky.social');
664:
665: // パラメータを取得する.
666: $msg = getParam('msg', TRUE, DEF_MESSAGE);
667: $replyURL = getParam('replyURL', FALSE, NULL);
668: if ($replyURL == '') $replyURL = '';
669: $quoteURL = getParam('quoteURL', FALSE, NULL);
670: if ($quoteURL == '') $quoteURL = '';
671: $reply = isButton('reply') ? TRUE : FALSE;
672: $outmsg = '';
673:
674: // 投稿
675: if (isButton('exec')) {
676: // XSS対策
677: $msg = htmlspecialchars($msg);
678:
679: // 画像データがあればメッセージに追加
680: $saveFileNames = array();
681: saveImage($saveFileNames);
682: foreach ($saveFileNames as $fname) {
683: $imageURI = 'file:///' . preg_replace('/\\\/ui', '/', $fname);
684: $msg .= ' ' . $imageURI;
685: }
686:
687: // セッション開始
688: $res = $pbs->createSession();
689: //投稿
690: if ($res) {
691: $res = $pbs->post($msg, TRUE, $replyURL, $quoteURL);
692: }
693: // エラー処理
694: if ($res == FALSE) {
695: $outmsg = '<p style="color:red;">エラー:' . $pbs->geterror() . '</p>';
696: } else {
697: $outmsg = '<p style="color:blue;">投稿成功:<a href="' . $res . '">' . $res . '</a></p>';
698: // 返信URLに代入する.
699: if ($reply) {
700: $replyURL = $res;
701: }
702: // セッション終了
703: $res = $pbs->deleteSession();
704: // エラー処理
705: if ($res == FALSE) {
706: $outmsg .= '<p style="color:red;">エラー:' . $pbs->geterror() . '</p>';
707: }
708: }
709:
710: // 画像ファイルを削除
711: deleteImage($saveFileNames);
712:
713: // クリア
714: } else if (isButton('clear')) {
715: $msg = $replyURL = $quoteURL = '';
716: $reply = FALSE;
717: }
718:
719: //表示HTMLを作成する.
720: $HtmlBody = makeCommonBody($msg, $replyURL, $quoteURL, $reply, $outmsg, $pbs->webapi);
721:
722: //画面に表示する.
723: echo $HtmlHeader;
724: echo $HtmlBody;
725: echo $HtmlFooter;
726:
727: //オブジェクトを解放する.
728: $pbs = NULL;
textareaに入力されたメッセージを取りだし、 htmlspecialchars 関数でXSS対策を行った後、セッション開始、メッセージ投稿、応答メッセージからエラー処理を行う。このとき正常応答が帰ってきたら、メッセージのURLを表示するようにする。最後にセッション終了する。
また、このプログラムはコマンドライン・パラメータとして、msg(投稿メッセージ)、replyURL(返信元URL)、quoteURL(引用元URL)を指定して呼び出すことができるので、JavaScriptと組み合わせてコンテンツ中に Bluesky への投稿処理を組み込むことができるだろう。
参考サイト
- Bluesky 公式リファレンス
- PHPでDOMDocumentを使ってスクレイピング:ぱふぅ家のホームページ
- PHPでTwitter(現・X)に投稿(ツイート)する:ぱふぅ家のホームページ
API操作はクラスファイルに分離し、他のプログラムから利用しやすいようにした。当サイト以外が配布しているプログラムやライブラリは不要だ。