PHPでBlueskyのプロフィールを更新する

(1/1)
今回は、PHPで Bluesky APIを利用し、自分のプロフィールを更新するプログラムを作ってみる。更新対象は下記の項目である。
  • 表示名
  • プロフィール情報
  • アバター画像
  • バナー画像
  • プロフィール固定メッセージ
各々の項目は独立して省略することが可能で(パラメータにNULLを代入したときに省略とみなす)、省略時には現在登録されている情報を更新せずに残すことにする。Blusky Webアプリのプロフィール変更UIは、Twitter(現・X)に比べて貧弱だ。ここでは、画像ファイルを画面に表示されている画像にドラッグ&ドロップすることで更新したり、ボタンをクリックしてファイルダイアログを表示して選択する 2つの方法を実装する。ドラッグ&ドロップもしくは選択した画像ファイルは、サーバ通信せずに、JavaScriptだけで画面表示を更新する。
(2025年11月21日)PHP8.5対応:curl_close,imagedestroyを実行しないようにした.
(2025年8月17日)画像に余計な空白が入らないようにするため一部仕様変更.
(2025年8月14日).pahooEnv導入

目次

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

PHPでBlueskyのプロフィールを更新

サンプル・プログラム

圧縮ファイルの内容
updateProfilesBluesky.phpサンプル・プログラム本体
.pahooEnvクラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。
使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
pahooScraping.phpスクレイピング処理に関わるクラス pahooScraping。
スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。
updateProfilesBluesky.php 更新履歴
バージョン 更新日 内容
1.6.0 2025/08/14 .pahooEnv導入
1.0.0 2025/07/19 初版
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
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 導入
pahooScraping.php 更新履歴
バージョン 更新日 内容
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 初版
pahooInputData.php 更新履歴
バージョン 更新日 内容
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対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

解説:pahooBlueskyAPIクラス

Bluesky に投稿したりプログラムで操作するAPIについては、公式リファレンスが詳しい。APIを利用するには、事前に、あなたのアカウントから利用登録を行い、アプリパスワードを取得する必要がある。その手順は「Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
Bluesky APIを利用するメソッドはクラス "pahooBlueskyAPI.php" に分離している。また、このクラスからクラス "pahooScraping.php" を呼び出すので、2つのクラス・ファイルを include_path の通ったディレクトリに配置すること。

解説:セッション開始

BlueskyAPI を利用するには、まずセッションを開き、アクセストークン accessJwt を取得する。使用するエンドポイントは com.atproto.server.createSession だ。
com.atproto.server.createSession
URL
https://{PDSドメイン}/xrpc/com.atproto.server.createSession
リクエスト・データ(http) header Content-Type "application/json" post identifier ハンドル名 password アプリケーション・パスワード
レスポンス・データ(json) accessJwt アクセストークン refreshJwt リフレッシュトークン handle ハンドル名 did did
アクセストークン accessJwt は、後述するメッセージや画像の投稿で使用する。寿命は1~2時間だ。
アクセストークン 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 = '';      // アプリケーション・パスワード

BlueskyAPI を利用するユーザー定義クラス pahooBlueskyAPIをつくる。
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $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: }

コンストラクタの引数は PDSドメインで、変数 $pds に保管し、API呼び出し時に参照できるようにした。

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: }

セッション開始メソッド createSession は、引数はなく、セッション開始に成功したかどうかを戻り値にする。

メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。

解説:セッション終了

アクセストークン accessJwt のセッションを終了するには、エンドポイント com.atproto.server.deleteSession を呼び出す。
com.atproto.server.deleteSession
URL
https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession
リクエスト・データ(json) header Content-Type "application/json" post identifier "Bearer {アクセストークン}" password アプリケーション・パスワード
セッション開始時に取得したアクセストークン accessJwt を使い、このセッションをクローズする。以後、このアクセストークン accessJwt は使用できなくなる。
アクセストークン 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: }

セッション終了メソッド deleteSession は、引数はなく、セッション開始に成功したかどうかを戻り値にする。

解説:自分のプロフィール情報を取得

解説:ユーザーのDIDを取得する - PHPでBlueskyに投稿する」で自分のプロフィール情報を取得する方法を解説したが、後述するように、今回はアバター画像やバナー画像の Blob情報を取得する必要がある。そこで、PDSリポジトリから単一レコードを取り出すエンドポイントは com.atproto.repo.getRecord を利用し、追加で必要になる時分のプロフィール情報を取得する。パラメータを GET で渡し、応答を JSON形式データを受け取る REST API である。
com.atproto.repo.getRecord
URL
https://{PDSドメイン}/xrpc/app.bsky.actor.profile
入力パラメータ
フィールド名 要否 内  容
repo 必須 ハンドル名またはdid
collection 必須 レコードコレクションのNSID
"app.bsky.actor.profile"
rkey 必須 レコードキー
"self"
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。

応答データ(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: }

解説:自分のプロフィール情報を更新

Blueskyでは、自分のプロフィールを更新するAPIは明示的に存在しないが、前述の自分のプロフィール情報を取得するのに使ったエンドポイント com.atproto.repo.getRecord の逆の働きをする com.atproto.repo.putRecord を使って時分のプロフィール情報を更新できる。Bluesky APIの構造は、とても直交性が高いと感じた。このエンドポイントは、パラメータを PUT で渡し、応答を JSON形式データを受け取る REST API である。
com.atproto.repo.putRecord
URL
https://{PDSドメイン}/xrpc/com.atproto.repo.putRecord
入力パラメータ
フィールド名 要否 内  容
repo 必須 ハンドル名またはdid
collection 必須 レコードコレクションのNSID
"app.bsky.actor.profile"
record 必須 レコード情報
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 で取得したものと同じ構造であること
APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
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

  63: // 初期値(START) =============================================================
  64: 
  65: // 表示幅(ピクセル)
  66: define('WIDTH', 600);
  67: 
  68: // アバター画像の表示幅(ピクセル)
  69: define('WIDTH_AVATAR', 64);
  70: 
  71: // 初期値(END) ===============================================================

メイン・プログラムの初期値は、「変更不可」の記載のないものは自由に変更できる。

解説:自分のプロフィール情報を取得する

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: }

ユーザー関数 getMyProfiles は、前述のメソッド getMyProfiles を呼び出し、プロフィール情報(表示名、プロフィール情報、アバター画像、バナー画像、プロフィール固定メッセージ)を配列に格納して返す。
画像は 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>

Blusky Webアプリのプロフィール変更UIは、Twitter(現・X)に比べて貧弱だ。ここでは、画像ファイルを画面に表示されている画像にドラッグ&ドロップすることで更新したり、ボタンをクリックしてファイルダイアログを表示して選択する2つの方法をJavaScriptで実装している。ドラッグ&ドロップもしくは選択した画像ファイルは、サーバ通信せずに、JavaScriptだけで画面表示を更新する。
いずれの場合も 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: }

ユーザー関数 updateMyProfiles は、前述のメソッド getMyProfiles を呼び出し、POST渡しされたプロフィール情報(表示名、プロフィール情報、アバター画像、バナー画像、プロフィール固定メッセージ)を使って更新する。更新のない情報については、NULLを代入することで、更新しない。

参考サイト

(この項おわり)
header