(2025年8月17日)画像に余計な空白が入らないようにするため一部仕様変更.
(2025年8月14日).pahooEnv導入
目次
サンプル・プログラムの実行例
サンプル・プログラム
| updateProfilesBluesky.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/07/19 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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.actor.profile |
| フィールド名 | 要否 | 内 容 |
|---|---|---|
| repo | 必須 | ハンドル名またはdid |
| collection | 必須 | レコードコレクションのNSID "app.bsky.actor.profile" |
| rkey | 必須 | レコードキー "self" |
応答データ(JSON形式)
{
"uri": プロフィール情報のDID,
"cid": プロフィール情報のCID,
"value": {
"$type": "app.bsky.actor.profile",
"displayName": 表示名,
"description": プロフィール情報(UTF-8),
"avatar": {
"$type": "blob",
"ref": {
"$link": アバター画像のBlob ID(CID)
},
"mimeType": アバター画像のMIME Type,
"size": アバター画像のファイルサイズ
},
"banner": {
"$type": "blob",
"ref": {
"$link": バナー画像のBlob ID(CID)
},
"mimeType": バナー画像のMIME Type,
"size": バナー画像のファイルサイズ
},
"pinnedPost": {
"cid": プロフィール固定メッセージ(CID),
"uri": プロフィール固定メッセージ(DID),
---(中略)---
},
}
}
---(以下略)---
}
pahooBlueskyAPI.php
1489: /**
1490: * 自分のプロフィール情報を取得する.
1491: * @param なし
1492: * @return array メッセージ情報 / FALSE:取得失敗
1493: */
1494: function getMyProfiles() {
1495: $userName = $this->BLUESKY_HANDLE;
1496:
1497: // ユーザーDIDを取得する
1498: $userDID = $this->getDID($userName);
1499: if ($userDID == FALSE) {
1500: $this->seterror($url . 'はユーザーDIDを取得できません');
1501: return FALSE;
1502: }
1503:
1504: // パラメータ配列を作成する
1505: $params = [
1506: 'repo' => $userDID,
1507: 'collection' => 'app.bsky.actor.profile',
1508: 'rkey' => 'self'
1509: ];
1510:
1511: // トークンを取得する.
1512: $this->getValidToken();
1513:
1514: // リクエストURL
1515: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.repo.getRecord?' . http_build_query($params);
1516: $this->webapi = $requestURL;
1517: // cURLを使ったリクエスト
1518: $ch = curl_init($requestURL);
1519: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1520: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1521: 'Content-Type: application/json',
1522: 'Authorization: Bearer ' . $this->accessJwt,
1523: ]);
1524: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1525: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1526: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
1527:
1528: // レスポンス処理
1529: $response = curl_exec($ch);
1530: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1531: if (PHP_VERSION_ID < 80500) {
1532: curl_close($ch);
1533: }
1534: $items = json_decode($response, TRUE);
1535: if ($httpStatusCode != 200) {
1536: $errmsg = '自分のプロフィール情報を取得できません(http code:' . $httpStatusCode . ')';
1537: if (isset($items['message'])) {
1538: $errmsg .= ';' . $items['message'];
1539: }
1540: $this->seterror($errmsg);
1541: return FALSE;
1542: }
1543:
1544: return $items;
1545: }
解説:自分のプロフィール情報を更新
| URL |
|---|
| https://{PDSドメイン}/xrpc/com.atproto.repo.putRecord |
| フィールド名 | 要否 | 内 容 |
|---|---|---|
| repo | 必須 | ハンドル名またはdid |
| collection | 必須 | レコードコレクションのNSID "app.bsky.actor.profile" |
| record | 必須 | レコード情報 |
| フィールド名 | 要否 | 内 容 |
|---|---|---|
| $type | 必須 | "app.bsky.actor.profile" |
| displayName | 必須 | 表示名(UTF-8) |
| description | 必須 | プロフィール情報(UTF-8) |
| avatar | 必須 | アバター画像Blob情報 com.atproto.repo.getRecord で取得したものと同じ構造であること |
| banner | 必須 | バナー画像Blob情報 com.atproto.repo.getRecord で取得したものと同じ構造であること |
| pinnedPost | 必須 | プロフィールに固定するメッセージ情報 com.atproto.repo.getRecord で取得したものと同じ構造であること |
record については、すべてが必須情報となる。空文字にしたものは、削除と同じ意味を持つ。
プロフィール更新メソッドは後述するが、あからじめ com.atproto.repo.getRecord を使って現時点のプロフィール情報を取得しておき、変更しない情報(パラメータにNULLをしていしたもの)については、com.atproto.repo.getRecord で得たデータをそのまま渡すという方針にする。
応答データ(JSON形式)
--- app.bsky.actor.profile と同じ ---
pahooBlueskyAPI.php
1547: /**
1548: * 自分のプロフィール情報を更新する.
1549: * @param string $dispName 表示名(UTF-8)【NULL=更新しない】
1550: * @param string $description プロフィール(UTF-8)【NULL=更新しない】
1551: * @param string $avator アバター画像ファイル名【NULL=更新しない】
1552: * @param string $banner バナー画像ファイル名【NULL=更新しない】
1553: * @param string $pinnedPost プロフィール固定メッセージURL【NULL=更新しない】
1554: * @return array メッセージ情報 / FALSE:更新失敗
1555: */
1556: function updateProfiles($dispName=NULL, $description=NULL, $avatar=NULL, $banner=NULL, $pinnedPost=NULL) {
1557: $height = $width = 0;
1558: $userName = $this->BLUESKY_HANDLE;
1559:
1560: // ユーザーDIDを取得する
1561: $userDID = $this->getDID($userName);
1562: if ($userDID == FALSE) return FALSE;
1563:
1564: // ユーザー・プロファイル情報を取得
1565: // $userProfiles = $this->getProfile($userName);
1566: // if ($userProfiles == FALSE) return FALSE;
1567:
1568: // 自分のプロフィール情報を取得する.
1569: $myProfiles = $this->getMyProfiles($userName);
1570: if ($myProfiles == FALSE) return FALSE;
1571:
1572: // 表示名
1573: if ($dispName == NULL) {
1574: $dispName = $myProfiles['value']['displayName'];
1575: }
1576: // プロフィール
1577: if ($description == NULL) {
1578: $description = $myProfiles['value']['description'];
1579: }
1580: // アバター画像
1581: if ($avatar == NULL) {
1582: $avatar = $myProfiles['value']['avatar'];
1583: } else {
1584: $tempFile = tempnam(sys_get_temp_dir(), 'bsky_avatar_');
1585: @file_put_contents($tempFile, file_get_contents($avatar));
1586: $avatar = $this->uploadBlob($tempFile, $width, $height);
1587: unlink($tempFile);
1588: if ($avatar == FALSE) return FALSE;
1589: }
1590: // バナー画像
1591: if ($banner == NULL) {
1592: $banner = $myProfiles['value']['banner'];
1593: } else {
1594: $tempFile = tempnam(sys_get_temp_dir(), 'bsky_banner_');
1595: @file_put_contents($tempFile, file_get_contents($banner));
1596: $banner = $this->uploadBlob($tempFile, $width, $height);
1597: unlink($tempFile);
1598: if ($banner == FALSE) return FALSE;
1599: }
1600: // プロフィール固定
1601: if ($pinnedPost == NULL) {
1602: $pinnedPost = $myProfiles['value']['pinnedPost'];
1603: } else {
1604: // プロフィールに固定するURL情報を取得する
1605: $items = $this->getPostThread($pinnedPost);
1606: if ($items == FALSE) return FALSE;
1607: $pinnedPost = $items['thread']['post'];
1608: }
1609:
1610: // POSTデータ配列を作成する
1611: $record = [
1612: '$type' => 'app.bsky.actor.profile',
1613: 'displayName' => $dispName,
1614: 'description' => $description,
1615: 'avatar' => $avatar,
1616: 'banner' => $banner,
1617: 'pinnedPost' => $pinnedPost
1618: ];
1619: // トークンを取得する.
1620: $this->getValidToken();
1621:
1622: // リクエストURL
1623: $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.repo.putRecord';
1624: $this->webapi = $requestURL;
1625: // cURLを使ったリクエスト
1626: $ch = curl_init($requestURL);
1627: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1628: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1629: 'Content-Type: application/json',
1630: 'Authorization: Bearer ' . $this->accessJwt,
1631: ]);
1632: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1633: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
1634: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
1635: curl_setopt($ch, CURLOPT_POST, TRUE);
1636: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
1637: 'repo' => $userDID,
1638: 'collection' => 'app.bsky.actor.profile',
1639: 'rkey' => 'self',
1640: 'record' => $record,
1641: ]));
1642:
1643: // レスポンス処理
1644: $response = curl_exec($ch);
1645: $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1646: if (PHP_VERSION_ID < 80500) {
1647: curl_close($ch);
1648: }
1649: $items = json_decode($response, TRUE);
1650: if ($httpStatusCode != 200) {
1651: $errmsg = '自分のプロフィール情報を更新できません(http code:' . $httpStatusCode . ')';
1652: if (isset($items['message'])) {
1653: $errmsg .= ';' . $items['message'];
1654: }
1655: $this->seterror($errmsg);
1656: return FALSE;
1657: }
1658:
1659: return $items;
1660: }
解説:メイン・プログラムの初期値
解説:自分のプロフィール情報を取得する
updateProfilesBluesky.php
307: /**
308: * 自分のプロフィール情報を取得する.
309: * @param string $errmsg エラーメッセージを格納する変数
310: * @return array プロフィール情報
311: * ['displayName'] 表示名
312: * ['description'] プロフィール情報
313: * ['bannerURL'] バナー画像URL
314: * ['avatarURL'] アバター画像URL
315: * FALSE 取得失敗
316: */
317: function getMyProfiles(&$errmsg) {
318: $errmsg = '';
319:
320: // 自分のプロフィール情報を格納する配列
321: $myProfiles = array();
322:
323: // インスタンスを生成する.
324: $pbs = new pahooBlueskyAPI('bsky.social');
325:
326: // 自分のプロフィール情報を取得する.
327: $items = $pbs->getMyProfiles();
328: if ($items === FALSE) {
329: $errmsg = $pbs->geterror();
330: return FALSE;
331: }
332: $myProfiles['webapi'] = $pbs->webapi;
333:
334: // 表示名
335: $myProfiles['displayName'] = $items['value']['displayName'];
336: // プロフィール情報
337: $myProfiles['description'] = $items['value']['description'];
338:
339: // バナー画像URL
340: if (preg_match('/^image\/([a-z0-9]+)$/', $items['value']['banner']['mimeType'], $arr1) === 0) {
341: $errmsg = 'バナー画像フォーマットが不明';
342: return FALSE;
343: }
344: if (preg_match('/at\:\/\/(did\:plc\:[^\/]+\/)/', $items['uri'], $arr2) === 0) {
345: $errmsg = 'バナーATURIが不正';
346: return FALSE;
347: }
348: $path = 'https://cdn.bsky.app/img/banner/plain/' . $arr2[1];
349: $myProfiles['bannerURL'] = $path . $items['value']['banner']['ref']['$link'] . '@' . $arr1[1];
350:
351: // アバター画像URL
352: if (preg_match('/^image\/([a-z0-9]+)$/', $items['value']['avatar']['mimeType'], $arr1) === 0) {
353: $errmsg = 'アバター画像フォーマットが不明';
354: return FALSE;
355: }
356: if (preg_match('/at\:\/\/(did\:plc\:[^\/]+\/)/', $items['uri'], $arr2) === 0) {
357: $errmsg = 'アバターATURIが不正';
358: return FALSE;
359: }
360: $path = 'https://cdn.bsky.app/img/avatar/plain/' . $arr2[1];
361: $myProfiles['avatarURL'] = $path . $items['value']['avatar']['ref']['$link'] . '@' . $arr1[1];
362:
363: // プロフィール固定メッセージURL
364: if (isset($items['value']['pinnedPost'])) {
365: if (preg_match('/\/([^\/]+)$/', $items['value']['pinnedPost']['uri']) === 0) {
366: $errmsg = 'プロフィール固定メッセージURLが不正';
367: return FALSE;
368: }
369: $myProfiles['pinnedPostURL'] = $pbs->atruri2postURL($items['value']['pinnedPost']['uri']);
370: }
371:
372: // プロフィール固定メッセージ(埋め込みHTML)
373: $res = $pbs->getEmbedPosts($myProfiles['pinnedPostURL']);
374: if ($res === FALSE) {
375: $errmsg = $pbs->geterror();
376: return FALSE;
377: }
378: $myProfiles['pinnedPost'] = $res;
379:
380: // インスタンスを解放する.
381: $pbs = NULL;
382:
383: return $myProfiles;
384: }
画像は ATURI形式で返るため、これをURLに置換して配列に代入する。
プロフィール固定メッセージも ATURI形式で返るため、これをURLに置換して配列に代入する。
解説:自分のプロフィール情報を取得する
updateProfilesBluesky.php
117: .img-wrapper {
118: position: relative;
119: display: inline-block;
120: }
121: .img-wrapper img {
122: display: block;
123: width: 300px; /* 必要に応じて調整 */
124: height: auto;
125: }
126: .img-wrapper input[type="file"] {
127: position: absolute;
128: top: 0;
129: left: 0;
130: width: 100%;
131: height: 100%;
132: opacity: 0; /* 見えなくする */
133: cursor: pointer; /* ポインタ変更でクリック可能に */
134: pointer-events: all;
135: }
136: .overlay-label {
137: position: absolute;
138: top: 10px;
139: left: 10px;
140: background: rgba(0,0,0,0.5);
141: color: white;
142: padding: 4px 8px;
143: border-radius: 4px;
144: font-size: 14px;
145: pointer-events: none; /* クリック無効(下のinputが反応) */
146: }
147: </style>
148: <script>
149:
150: // ページロード直後の処理
151: document.addEventListener('DOMContentLoaded', () => {
152: const dropBanner = document.getElementById('banner');
153: dropBanner.addEventListener('dragover', handleDragOver, false);
154: dropBanner.addEventListener('drop', handleFileSelectBanner, false);
155: const bannerFile = document.getElementById('bannerFile');
156: bannerFile.addEventListener('change', handleChangeImageBanner, false);
157:
158: const dropavatar = document.getElementById('avatar');
159: dropavatar.addEventListener('dragover', handleDragOver, false);
160: dropavatar.addEventListener('drop', handleFileSelectAvatar, false);
161: const avatarFile = document.getElementById('avatarFile');
162: avatarFile.addEventListener('change', handleChangeImageAvatar, false);
163: });
164:
165: /**
166: * 対象オブジェクトに画像ファイルをドラッグオーバーしたときの処理
167: * @param object evt 対象オブジェクトのID
168: * @return なし
169: */
170: function handleDragOver(evt) {
171: evt.stopPropagation();
172: evt.preventDefault();
173: evt.dataTransfer.dropEffect = 'copy';
174: }
175:
176: /**
177: * 対象オブジェクトに画像ファイルをドロップしたときの処理(バナー画像)
178: * @param object evt 対象オブジェクトのID
179: * @return なし
180: */
181: function handleFileSelectBanner(evt) {
182: evt.stopPropagation();
183: evt.preventDefault();
184:
185: var files = evt.dataTransfer.files;
186: var output = [];
187:
188: document.getElementById('bannerFile').files = files;
189:
190: // 表示画像を変更する
191: const file = this.files[0];
192: if (file && file.type.startsWith('image/')) {
193: const reader = new FileReader();
194: reader.onload = function(e) {
195: banner.src = e.target.result;
196: };
197: reader.readAsDataURL(file);
198: } else {
199: alert('画像ファイルを選んでください');
200: }
201: }
202:
203: /**
204: * 対象オブジェクトに画像ファイルをドロップしたときの処理(アバター画像)
205: * @param object evt 対象オブジェクトのID
206: * @return なし
207: */
208: function handleFileSelectAvatar(evt) {
209: evt.stopPropagation();
210: evt.preventDefault();
211:
212: var files = evt.dataTransfer.files;
213: var output = [];
214:
215: document.getElementById('avatarFile').files = files;
216:
217: // 表示画像を変更する
218: const file = this.files[0];
219: if (file && file.type.startsWith('image/')) {
220: const reader = new FileReader();
221: reader.onload = function(e) {
222: avatar.src = e.target.result;
223: };
224: reader.readAsDataURL(file);
225: } else {
226: alert('画像ファイルを選んでください');
227: }
228: }
229:
230: /**
231: * バナー画像を変更する
232: * @param なし
233: * @return なし
234: */
235: function handleChangeImageBanner() {
236: const file = this.files[0];
237: if (file && file.type.startsWith('image/')) {
238: const reader = new FileReader();
239: reader.onload = function(e) {
240: banner.src = e.target.result;
241: };
242: reader.readAsDataURL(file);
243: } else {
244: alert('画像ファイルを選んでください');
245: }
246: }
247:
248: /**
249: * アバター画像を変更する
250: * @param なし
251: * @return なし
252: */
253: function handleChangeImageAvatar() {
254: const file = this.files[0];
255: if (file && file.type.startsWith('image/')) {
256: const reader = new FileReader();
257: reader.onload = function(e) {
258: avatar.src = e.target.result;
259: };
260: reader.readAsDataURL(file);
261: } else {
262: alert('画像ファイルを選んでください');
263: }
264: }
265: </script>
266: </head>
いずれの場合も input type="file" オブジェクトに更新後のファイルを格納するようにしてある。
解説:自分のプロフィール情報を更新する
updateProfilesBluesky.php
386: /**
387: * 自分のプロフィール情報を更新する.
388: * @param string $errmsg エラーメッセージを格納する変数
389: * @return array メッセージ情報 / FALSE:更新失敗
390: */
391: function updateMyProfiles(&$errmsg) {
392: $errmsg = '';
393:
394: // 表示名
395: $dispName = trim((string)getParam('displayName', TRUE, NULL));
396:
397: // プロフィール情報
398: $description = trim((string)getParam('description', TRUE, NULL));
399:
400: // アバター画像
401: if (isset($_FILES['avatarFile']) && $_FILES['avatarFile']['tmp_name'] !== '') {
402: $avatar = $_FILES['avatarFile']['tmp_name'];
403: } else {
404: $avatar = NULL;
405: }
406:
407: // バナー画像
408: if (isset($_FILES['bannerFile']) && $_FILES['bannerFile']['tmp_name'] !== '') {
409: $banner = $_FILES['bannerFile']['tmp_name'];
410: } else {
411: $banner = NULL;
412: }
413:
414: // プロフィール固定メッセージURL
415: $pinnedPost = trim((string)getParam('pinnedPostURL', TRUE, NULL));
416:
417: // インスタンスを生成する.
418: $pbs = new pahooBlueskyAPI('bsky.social');
419:
420: // 自分のプロフィール情報を更新する.
421: $res = $pbs->updateProfiles($dispName, $description, $avatar, $banner, $pinnedPost);
422:
423: // インスタンスを解放する.
424: $pbs = NULL;
425:
426: return $res;
427: }
参考サイト
- Bluesky 公式リファレンス
- Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法
- PHPでBlueskyに投稿する:ぱふぅ家のホームページ
- PHPでBlueskyの埋め込み用HTMLを取得:ぱふぅ家のホームページ
- PHPでBlueskyの投稿を検索・一覧表示する:ぱふぅ家のホームページ

- 表示名
- プロフィール情報
- アバター画像
- バナー画像
- プロフィール固定メッセージ
各々の項目は独立して省略することが可能で(パラメータにNULLを代入したときに省略とみなす)、省略時には現在登録されている情報を更新せずに残すことにする。Blusky Webアプリのプロフィール変更UIは、Twitter(現・X)に比べて貧弱だ。ここでは、画像ファイルを画面に表示されている画像にドラッグ&ドロップすることで更新したり、ボタンをクリックしてファイルダイアログを表示して選択する 2つの方法を実装する。ドラッグ&ドロップもしくは選択した画像ファイルは、サーバ通信せずに、JavaScriptだけで画面表示を更新する。