Bluesky API (AT Protocol) で画像投稿を実装する方法【cURL・PHP】

PHP

BlueskyのAPIを利用した際、テキスト投稿まではスムーズに実装できても、画像投稿で躓くケースが非常に多く見られます。その最大の理由は、「投稿APIの中に画像を組み込む」のではなく、「事前に画像をアップロードして識別子(blob)を取得する」という2段階のプロセスが必要だからです。

本記事では、PHPとcURLを用いて、画像アップロードから投稿完了までの全工程を論理的に解説します。

1. 全体フローの理解

Bluesky(AT Protocol)において、画像投稿は以下の3ステップで構成されます。

  1. 認証 (createSession):ハンドル名とアプリパスワードでログインし、accessJwt(トークン)を取得。

  2. Blobアップロード (uploadBlob):画像のバイナリデータをサーバーへ送信。サーバーから返ってくる「受領証(blobオブジェクト)」を保持する。

  3. レコード作成 (createRecord):投稿本文と一緒に、ステップ2で取得した blobembed フィールドに含めて送信する。


2. 実装コード(フルスクラッチ)

以下のコードは、単体で動作するように設計された完全なサンプルです。

<?php /** * Bluesky Image Post Implementation (PHP) * * @author Your Name / Blog Name * @license MIT */ // --- 0. 設定項目 --- $pds_url = 'https://bsky.social/xrpc/'; $handle = 'your-handle.bsky.social'; // 自分のハンドル名 $app_pass = 'xxxx-xxxx-xxxx-xxxx'; // アプリパスワード(設定から生成したもの) $img_path = './target_image.jpg'; // 投稿する画像ファイル // --- 1. セッションの作成(認証) --- $session_url = $pds_url . 'com.atproto.server.createSession'; $auth_payload = json_encode([ 'identifier' => $handle,
    'password'   => $app_pass
]);

$ch = curl_init($session_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $auth_payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$auth_res = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!isset($auth_res['accessJwt'])) {
    die("認証失敗: " . ($auth_res['message'] ?? '不明なエラー'));
}

$access_token = $auth_res['accessJwt'];
$did = $auth_res['did'];

// --- 2. 画像(Blob)のアップロード ---
// Blueskyの画像投稿における最大の難所。
// multipart/form-dataではなく、リクエストボディにバイナリを直接乗せる。
if (!file_exists($img_path)) {
    die("画像ファイルが見つかりません。");
}

$image_binary = file_get_contents($img_path);
$mime_type    = mime_content_type($img_path);

$upload_url = $pds_url . 'com.atproto.repo.uploadBlob';
$ch = curl_init($upload_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $image_binary); // バイナリデータを直接セット
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $access_token",
    "Content-Type: $mime_type" // 画像のMIMEタイプ(image/jpeg等)を明示
]);
$upload_res = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!isset($upload_res['blob'])) {
    die("Blobアップロード失敗: " . ($upload_res['message'] ?? '不明なエラー'));
}

$blob = $upload_res['blob']; // これが画像投稿に必要な「受領証」となる

// --- 3. 投稿(Create Record)の実行 ---
$post_url = $pds_url . 'com.atproto.repo.createRecord';

// 投稿データ(Record)の構築
$post_payload = [
    'repo' => $did,
    'collection' => 'app.bsky.feed.post',
    'record' => [
        'text' => "PHPによる画像投稿テスト\n" . date('Y-m-d H:i:s'),
        'createdAt' => date('c'),
        'embed' => [
            '$type' => 'app.bsky.embed.images',
            'images' => [
                [
                    'alt' => '投稿された画像の説明文', // アクセシビリティ用の代替テキスト
                    'image' => $blob // STEP2で取得したblobをセット
                ]
            ]
        ]
    ]
];

$ch = curl_init($post_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $access_token",
    "Content-Type: application/json"
]);
$final_res = json_decode(curl_exec($ch), true);
curl_close($ch);

// --- 結果確認 ---
if (isset($final_res['uri'])) {
    echo "投稿成功! URI: " . $final_res['uri'];
} else {
    echo "投稿失敗。";
    print_r($final_res);
}

3. 実装上の重要な技術的注意点

① uploadBlob 時のデータ形式

一般的なWebフォームのような $_FILES 経由での送信とは仕組みが異なります。cURLの CURLOPT_POSTFIELDS に、file_get_contents() で読み込んだ生のバイナリデータをそのまま渡してください。この際、Content-Type ヘッダーに image/jpegimage/png を指定しないとサーバー側で正しく処理されません。

② 画像サイズと制限

Bluesky PDS(個人データサーバー)には画像サイズ制限があります。2026年現在、1枚あたり約1MB(1,000,000バイト)を超える画像はエラーとなる可能性が高いため、事前にPHPの GD ライブラリや Imagick を使用してリサイズ・圧縮を行うのが実用的です。

③ embed フィールドの $type

画像投稿を行う際は、embed 内の $type に必ず app.bsky.embed.images を指定する必要があります。ここを間違えると、画像が反映されなかったり、APIがエラーを返したりします。


4. 複数枚投稿への応用

上記のサンプルでは1枚の投稿ですが、images は配列になっているため、ステップ2を繰り返し行い、複数の blob を取得して配列に詰め込むことで、最大4枚までの画像投稿が可能です。

コメント

タイトルとURLをコピーしました