bluesky api php で post

参考
https://mgng.mugbum.info/1699

# blue sky で post したい

ハッシュタグは使えないので投稿時に削除する



1 アプリパスワードを作成
https://bsky.app/settings/app-passwords

2 パスワードを設定

```.env
BLUESKY_APP_PASSWORD = xxx-xxxx-xxx-xxx
```

```app.php
'bluesky_app_password' => env('BLUESKY_APP_PASSWORD')
```


続いてモデル

```bluesky.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;

class Bluesky extends Model
{
    public $jwt;
    public $handle;

    public function __construct($handle, $password)
    {
        $this->handle = $handle;
        $this->jwt = $this->getJwt($handle, $password);
        parent::__construct();
    }

    private function getJwt($handle, $password)
    {
        $ch = curl_init("https://bsky.social/xrpc/com.atproto.server.createSession");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
            CURLOPT_POSTFIELDS => json_encode([
                "identifier" => $handle,
                "password" => $password,
            ]),
        ]);
        $response = curl_exec($ch);
        curl_close($ch);

        $responseJson = json_decode($response, true);
        if (isset($responseJson["accessJwt"])) {
            return $responseJson["accessJwt"];
        } else {
            throw new \Exception("Failed to obtain JWT: " . $response);
        }
    }


    //    ハッシュタグを削除
    public static function removeHashTag($data)
    {
        preg_match_all('/#(w*[一-龠_ぁ-ん_ァ-ヴーａ-ｚＡ-Ｚa-zA-Z0-9]+|[a-zA-Z0-9_]+|[a-zA-Z0-9_]w*)/',  $data, $matches);
        foreach ($matches[0] as $s) {
            $data = str_replace($s,'',$data);
        }

        return $data;
    }


    public function post($text, $imagePath = null, $link = null)
    {
        $imageUri = $imagePath ? $this->uploadImage($imagePath) : null;

        $text = $this->removeHashTag($text);



        $record = [
            "\$type" => "app.bsky.feed.post",
            "text" => $text,
            "createdAt" => Carbon::now()->format('c'),
        ];

        if ($imageUri) {
            $record['embed'] = [
                '$type' => 'app.bsky.embed.images',
                'images' => [
                    [
                        'image' => $imageUri,
                        'alt' => 'Image description'
                    ]
                ]
            ];
        }

        $facets = $this->createFacets($text, $link);

        if (!empty($facets)) {
            $record['facets'] = $facets;
        }

        $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.createRecord");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: application/json",
                "Authorization: Bearer {$this->jwt}",
            ],
            CURLOPT_POSTFIELDS => json_encode([
                "repo" => $this->handle,
                "collection" => "app.bsky.feed.post",
                "record" => $record,
            ]),
        ]);

        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true);
    }

    private function createFacets($text, $link)
    {
        $facets = [];

        // Handle links
        if ($link) {
            $linkStart = strpos($text, $link);
            $linkEnd = $linkStart + strlen($link);

            $facets[] = [
                'index' => [
                    'byteStart' => $linkStart,
                    'byteEnd' => $linkEnd
                ],
                'features' => [
                    [
                        '$type' => 'app.bsky.richtext.facet#link',
                        'uri' => $link
                    ]
                ]
            ];
        }

        // Handle hashtags
        preg_match_all('/#(\w+)/', $text, $matches, PREG_OFFSET_CAPTURE);
        foreach ($matches[0] as $match) {
            $hashtag = $match[0];
            $hashtagStart = $match[1];
            $hashtagEnd = $hashtagStart + strlen($hashtag);

            $facets[] = [
                'index' => [
                    'byteStart' => $hashtagStart,
                    'byteEnd' => $hashtagEnd
                ],
                'features' => [
                    [
                        '$type' => 'app.bsky.richtext.facet#tag',
                        'tag' => substr($hashtag, 1) // remove the hash symbol
                    ]
                ]
            ];
        }

        return $facets;
    }

    private function uploadImage($imagePath)
    {
        $imageData = file_get_contents($imagePath);
        $mime = mime_content_type($imagePath);

        $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.uploadBlob");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: $mime",
                "Authorization: Bearer {$this->jwt}",
            ],
            CURLOPT_POSTFIELDS => $imageData,
        ]);

        $response = curl_exec($ch);
        $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpcode == 200) {
            $responseJson = json_decode($response, true);
            return $responseJson['blob'] ?? null;
        } else {
            throw new \Exception("Failed to upload image: HTTP $httpcode - $response");
        }
    }
}




```


```HogesController.php


$bluesky = new Bluesky("your.sky.social",config('app.bluesky_app_password'));
$res = $bluesky->post("APIからメッセージを\n投稿するよ".time());

```

はい、たったこれだけでpostできます。


# 画像付き、リンク付きでアップロード

そのままだとリンクが有効にならない。
以下のコードでリンクを有効にできる。

```bluesky.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;

class Bluesky extends Model
{
    public $jwt;
    public $handle;

    public function __construct($handle, $password)
    {
        $this->handle = $handle;
        $this->jwt = $this->getJwt($handle, $password);
        parent::__construct();
    }

    private function getJwt($handle, $password)
    {
        $ch = curl_init("https://bsky.social/xrpc/com.atproto.server.createSession");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
            CURLOPT_POSTFIELDS => json_encode([
                "identifier" => $handle,
                "password" => $password,
            ]),
        ]);
        $response = curl_exec($ch);
        curl_close($ch);

        $responseJson = json_decode($response, true);
        if (isset($responseJson["accessJwt"])) {
            return $responseJson["accessJwt"];
        } else {
            throw new \Exception("Failed to obtain JWT: " . $response);
        }
    }

    public function post($text, $imagePath = null, $link = null)
    {
        $imageUri = $imagePath ? $this->uploadImage($imagePath) : null;

        $record = [
            "\$type" => "app.bsky.feed.post",
            "text" => $text,
            "createdAt" => Carbon::now()->format('c'),
        ];

        if ($imageUri) {
            $record['embed'] = [
                '$type' => 'app.bsky.embed.images',
                'images' => [
                    [
                        'image' => $imageUri,
                        'alt' => 'Image description'
                    ]
                ]
            ];
        }

        if ($link) {
            $linkStart = strpos($text, $link);
            $linkEnd = $linkStart + strlen($link);

            $record['facets'] = [
                [
                    'index' => [
                        'byteStart' => $linkStart,
                        'byteEnd' => $linkEnd
                    ],
                    'features' => [
                        [
                            '$type' => 'app.bsky.richtext.facet#link',
                            'uri' => $link
                        ]
                    ]
                ]
            ];
        }

        $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.createRecord");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: application/json",
                "Authorization: Bearer {$this->jwt}",
            ],
            CURLOPT_POSTFIELDS => json_encode([
                "repo" => $this->handle,
                "collection" => "app.bsky.feed.post",
                "record" => $record,
            ]),
        ]);

        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true);
    }

    private function uploadImage($imagePath)
    {
        $imageData = file_get_contents($imagePath);
        $mime = mime_content_type($imagePath);

        $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.uploadBlob");
        curl_setopt_array($ch, [
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: $mime",
                "Authorization: Bearer {$this->jwt}",
            ],
            CURLOPT_POSTFIELDS => $imageData,
        ]);

        $response = curl_exec($ch);
        $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpcode == 200) {
            $responseJson = json_decode($response, true);
            return $responseJson['blob'] ?? null;
        } else {
            throw new \Exception("Failed to upload image: HTTP $httpcode - $response");
        }
    }
}

```

```HogesController.php

$bluesky = new Bluesky("your.bsky.social",config('app.bluesky_app_password'));

$text = 'こんにちは、まじでいいよ！ママリッチです: https://mama-rich.net';
$link = 'https://mama-rich.net';


//画像なし、リンクあり
$response = $bluesky->post($text, null, $link);

//画像、リンクあり
$img = public_path("/img/seiya.jpg");
$response = $bluesky->post($text, $img, $link);

```