Luise Freese

Introducing Bluesky Terminal Poster

Hello World, Bluesky

Took me a while to fully adopt Bluesky, but here we are. One of the first things I try to do when adopting a service as a user, is also to have a peak the services API to understand what going on under the hood. So while I had a nice cuppa tea, I read in the Bluesky API documentation and had this nice geeky idea. Why not use that API to send posts directly from my terminal? When I just want to get my thoughts out, I do not necessarily need a UI (which will then only distract me again for the near forseeable future by making me read all the interesting posts others put out…I need to timebox the latter 🙈)

So how did I build this?

Setting Up the Project

  • In a new folder, create a new Python file: I named mine bluesky_post_image.py.
  • Install necessary packages: The requests library was essential for making HTTP requests to the Bluesky API. I also used Python’s json module to handle API responses. To install requests, run:

pip install requests

Understanding Authentication and Access Tokens

There is an ongoing joke in my filter bubble that’s like Once auth is done, everything else is easy. So let’s do this part thouroughly.

Bluesky’s API requires authentication to ensure that only authorized users can post on their platform. The first thing we need is an access token. Instead of directly using our username and password each time, we generated an access token, which acts as a key for logging in.

To get our access token:

  • We use an app password from Bluesky to authenticate with our handle.
  • This returns an access token, which we can use for any future requests within the session

import requests

def get_access_token(handle, app_password):
    url = 'https://bsky.social/xrpc/com.atproto.server.createSession'
    data = {
        'identifier': handle,
        'password': app_password
    }
    response = requests.post(url, json=data)
    response.raise_for_status()  
    return response.json().get('accessJwt')

This function sends a POST request to the Bluesky authentication endpoint with our handle and app password, then returns an access token for further use

Posting Text Content

With the access token in hand, we can now focus on the main task: posting content to Bluesky. I decided to make the app post a simple text message first.

In the API call, I used the access token in the request headers:


def create_text_post(content, access_token):
    url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord'
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'application/json'
    }
    data = {
        'repo': 'YOUR_DID',  # Replace with your DID
        'collection': 'app.bsky.feed.post',
        'record': {
            '$type': 'app.bsky.feed.post',
            'text': content,
            'createdAt': datetime.utcnow().isoformat() + 'Z'
        }
    }
    response = requests.post(url, headers=headers, json=data)
    response.raise_for_status()  # Check for errors
    print("Post created:", response.json())

This code sends the post content and metadata to the Bluesky API, where it’s added to our feed.

Attaching Images to Posts

An image tells more than a 1000 words, right? So once I had text posts working, I moved on to adding images. Posting images is a bit trickier because we need to:

  • Upload the image file to create a “blob” in the API
  • Use the blob’s metadata in our post request to attach the image

def upload_image(file_path, access_token):
    url = 'https://bsky.social/xrpc/com.atproto.repo.uploadBlob'
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'image/jpeg'
    }
    with open(file_path, 'rb') as img_file:
        response = requests.post(url, headers=headers, data=img_file)
    response.raise_for_status()
    return response.json().get('blob')

This reads the image file and sends it to Bluesky’s blob upload endpoint. The server responds with a blob object, which includes the image’s reference ID.

Creating a Post with Image

With the blob reference from our image upload, we can now attach it to a new post. I updated my create_text_post function to add the image as an embedded object


def create_post_with_image(content, image_blob, access_token):
    url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord'
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'application/json'
    }
    data = {
        'repo': 'YOUR_DID',  # Replace with your DID
        'collection': 'app.bsky.feed.post',
        'record': {
            '$type': 'app.bsky.feed.post',
            'text': content,
            'createdAt': datetime.utcnow().isoformat() + 'Z',
            'embed': {
                '$type': 'app.bsky.embed.images',
                'images': [{
                    'image': image_blob,
                    'alt': 'An image attached to the post'
                }]
            }
        }
    }
    response = requests.post(url, headers=headers, json=data)
    response.raise_for_status()
    print("Post with image created:", response.json())

In this function, I added an embed field with my image_blob, linking the image in my post.

Finally, I combined all my functions into a single script and added input prompts for the user to:

  • Enter the post content.
  • Optionally specify an image path to attach.

If you want to check it out, I put it on GitHub and would love for you to try it out!

And this is my first post from my terminal which attaches an image:

Can I post images straight from my terminal as well?

[image or embed]

— Luise Freese (@luisefreese.bsky.social) November 11, 2024 at 8:33 AM

You May Also Like

×

Want to see me speak live?

Speaking Event

I have some speaking gigs coming up and would love to connect in person!

View Speaking Gigs