the red penguin
HOME ABOUT SITEMAP BLOG LOGIN

How to post text and images to Bluesky using PHP

Bluesky has really been taking off this week, and I’ve been posting more and more on there both for myself and for organisations I post for.

This has allowed me to look at posting stuff regularly again using PHP scripts, something I used to do on Twitter a lot before they changed the API. I can still do it – I just don’t know whether I’m going to get an account banned or not for doing it there.

There are people who’ve made some libraries to help post (like this guy) but I wanted to make things fairly simple, especially since my website is on Ionos and using Composer still seems like an enormous faff for me.

Firstly – you need to go to your Bluesky account, go to Settings, scroll down to App Passwords and choose Add App Password. You’ll get a password like this: dylx-y5jk-e1ne-sxcp

So we need to creat some functions first. To authenticate with the Bluesky API, you’ll need to obtain a session token using your Bluesky credentials.

function getSessionToken($username, $password) {
    $url = "https://bsky.social/xrpc/com.atproto.server.createSession";

    $payload = json_encode([
        "identifier" => $username,
        "password" => $password,
    ]);

    $headers = [
        "Content-Type: application/json",
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);

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

    $data = json_decode($response, true);

    return $data['accessJwt'] ?? null;
}

To include an image in your post, you first need to upload it and get a blob reference.

function uploadImage($token, $imagePath) {
    $url = "https://bsky.social/xrpc/com.atproto.repo.uploadBlob";

    $imageData = file_get_contents($imagePath);
    $mimeType = mime_content_type($imagePath);

    $headers = [
        "Authorization: Bearer $token",
        "Content-Type: $mimeType",
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $imageData);

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

    $data = json_decode($response, true);

    return $data['blob'] ?? null;
}

Once you have the blob reference, you can create a post with the image included.

function createPost($token, $text, $blobRef) {
    $url = "https://bsky.social/xrpc/com.atproto.repo.createRecord";

    $payload = json_encode([
        "collection" => "app.bsky.feed.post",
        "repo" => "your-handle.bsky.social", // Replace with your Bluesky handle
        "record" => [
            "\$type" => "app.bsky.feed.post",
            "text" => $text,
            "embed" => [
                "\$type" => "app.bsky.embed.images",
                "images" => [
                    [
                        "image" => $blobRef,
                        "alt" => "An uploaded image",
                    ],
                ],
            ],
            "createdAt" => date(DATE_ATOM),
        ],
    ]);

    $headers = [
        "Authorization: Bearer $token",
        "Content-Type: application/json",
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);

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

    return json_decode($response, true);
}

Note that in the function you will need to replace your-handle.bsky.social with your own handle, or it won’t work.

The actual posting itself is now fairly straightforward:

$username = "your-username";
$password = "your-app-password";
$imagePath = "path/to/your/image.jpg";
$postText = "Hello from PHP with an image!";

$token = getSessionToken($username, $password);
if (!$token) {
    die("Failed to get session token.");
}

$blobRef = uploadImage($token, $imagePath);
if (!$blobRef) {
    die("Failed to upload image.");
}

$response = createPost($token, $postText, $blobRef);
if (isset($response['error'])) {
    echo "Error: " . $response['error'];
} else {
    echo "Post created successfully!";
}

Now you can set your username, password, image path and post text and when you execute the code, it will post the image and text for you.

If you want to post text only, you can create another function:

function createTextPost($token, $text) {
    $url = "https://bsky.social/xrpc/com.atproto.repo.createRecord";

    $payload = json_encode([
        "collection" => "app.bsky.feed.post",
        "repo" => "your-handle.bsky.social", // Replace with your Bluesky handle
        "record" => [
            "\$type" => "app.bsky.feed.post",
            "text" => $text,
            "createdAt" => date(DATE_ATOM),
        ],
    ], JSON_UNESCAPED_SLASHES);

    $headers = [
        "Authorization: Bearer $token",
        "Content-Type: application/json",
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);

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

    return json_decode($response, true);
}

I probably should have started with this first, haha! Once again, replace your-handle with your own username here.

Then the posting code is as follows:

$username = "your-username";
$password = "your-password";
$postText = "Hello, Bluesky! This is a test post from PHP.";

$token = getSessionToken($username, $password);
if (!$token) {
    die("Failed to get session token.");
}

$response = createTextPost($token, $postText);
if (isset($response['error'])) {
    echo "Error: " . $response['error'];
} else {
    echo "Post created successfully!";
}

I tested the image and text code today (14 November 2024) using a CronJob for the first time and it worked like a charm. Good luck with it!

Thursday 14 November 2024, 329 views


Leave a Reply

Your email address will not be published. Required fields are marked *

  1. Dom says:

    Hi – this looks good. It’s great all the documentation on the Blue sky API – it makes it really accessible.

    I’ve coded a similar script in PHP but using stream_context_create. Everything seems to work fine, but but the uploaded image seems to be in invalid format when it’s saved to the Blue sky CDN. So I’ll look at how you’ve implemented with CURL.

    One question: When you get the app password, is that what use as $password rather than your regular password?

    1. rp says:

      Hi yes that’s right. I just realised that in one block of code (for text/timage) I used this:

      $password = “your-app-password”;

      and in another (text only) I used this:

      $password = “your-password”;

      which is confusing but it should be the app password in each time. It’s given to you in the format “aaaa-aaaa-aaaa-aaaa” where a could be anything from a-z or 0-9.

      1. Dom says:

        That’s perfect – thanks. I generated an app password and it works fine. I’ve adapted your script so that I can create a website card, and it’s working fine – many thanks for your help.

        I think the only problem with my original script is that I thought I needed to base64 encode the image, but looks like that gets done automatically when you assign the image to a variable using file_get_contents.

        1. rp says:

          Glad you got it working! Bluesky does look like a developer’s/tinkerer’s dream.