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?
— Luise Freese (@luisefreese.bsky.social) November 11, 2024 at 8:33 AM
[image or embed]
Have any questions?
ContactYou May Also Like
Introducing the SVG to JSON for SharePoint List formatter
In my latest blog posts, I played a lot of SVGs in SharePoint lists. For everyone who isn’t aware - Unlike other image formats like .png or .jpg, .svg are vectors - which can be expressed as …
ProvisionGenie - an open-source provisioning engine for Microsoft Teams
Once upon a time I teamed up with my friend and partner in crime Carmen Ysewijn. We both work as Power Platform developers and Microsoft 365 consultants, and got both tired of doing the same things …
How to create a wordclock with Python
How to deal with time zones and not go crazy