Bluesky has been a big surprise recently.
I joined last year when it was fresh and new, and understandably it was quite quiet.
For a while I thought that Threads would beat it, as that platform had a couple of advantages, not least the influx of Instagram users.
But the winner, for me, and for now, has been Bluesky.
One of the lovely things about the Bluesky system is you can use your own domain as your handle. So instead of being in a rush to pick a handle before someone else grabs it, you can be more "you". Helps with imposters too! (You can follow me here)
Another nice thing is the feeds and lists - you are not forced to put up with whatever billionare owner or tech team wants to prioritize.
You can see what your friends are saying, what your friends are finding interesting, you can even make a custom feed based on whatever criteria you choose.
While there are third party tools to make that approachable for non-coders, the API is where the granular magic happens.
This lead me to look into what their API is like, and I started with the basics of automating posting.
Import the ATProto API
As usual, you can install with pip install atproto and then we need the Client class, the utils, and the models.
My other imports here are sys because we are using command line parameters, and PIL because we want to post images of the correct aspect ratio as attachments.
import sys
from PIL import Image
from atproto import Client, client_utils, models
Authentication
Right now the API is using basic login authentication, which is easy for us, but has obvious security disadvantages.
def login(handle, password):
client = Client()
profile = client.login(handle, password)
print('Logged in as ', profile.display_name)
return client
Posting a Message
(I refuse to call Bluesky posts skeets)
def post(client, message, anchortext='', link=''):
if link !='':
text = client_utils.TextBuilder().text(message).link(anchortext, link)
else:
text = client_utils.TextBuilder().text(message)
post = client.send_post(text)
return post
Here you can see we optionally have a link attached, those will be formatted with blue anchor text rather than the text of the URL like monsters.
Posting Images
As mentioned above, we either need to crop our images to 1:1 ratio, or we need to provide the height and the width to prevent the image being distorted:
def get_size(imagefile):
# get image dimensions
img = Image.open(imagefile)
return img.width, img.height
def post_image(client, text, imagefile, alt_text=''):
# the path to our image file
with open(imagefile, 'rb') as f:
img_data = f.read()
# get width and height
width, height = get_size(imagefile)
# Add image aspect ratio to prevent default 1:1 aspect ratio
# Replace with your desired aspect ratio
aspect_ratio = models.AppBskyEmbedDefs.AspectRatio(height=height, width=width)
client.send_image(
text=text,
image=img_data,
image_alt=alt_text,
image_aspect_ratio=aspect_ratio,
)
With the image size retreived we simply need to supply the image data along with the descriptive alt-text of the image.
There's probably a way to use PIL to provide the image data too rather than load it twice but this was an experiment not production code!
Getting a user's feed
One of my use-cases is to syndicate my posts to other platforms where I spend less time. How do we get our most recent posts?
Turns out really easy to get the feed of any Bluesky handle:
def get_user_feed(client, handle):
print(f'\nProfile Posts of {handle}:\n\n')
# Get profile's posts. Use pagination (cursor + limit) to fetch all
profile_feed = client.get_author_feed(actor=handle)
return profile_feed.feed
Pulling it together
if __name__ == '__main__':
if len(sys.argv) < 3:
print("Please provide handle and password")
sys.exit(1)
handle = sys.argv[1]
password = sys.argv[2]
client = login(handle, password)
In my test I use the command line to get the handle and password, this avoids storing them in plain text on Github!
# New post with link
link = "https://github.com/omiq/bluesky"
result = post(client, "I think I will work on a #WordPress plugin that posts newly published articles (after a short delay). Will add to my repo here any experiments I make: ", "Github", link)
print(result)
# Post picture of kitteh
post_image(client, "Kara", "./kara.jpg")
# Get most recent post if it is not a reply and contains an image
feed = get_user_feed(client, "chrisg.com")
post = feed[0]
if(post.post.record.reply==None & post.post.embed != None):
print('\n\n', post.post.record.text, post.post.embed.images[0].fullsize, post.post.embed.images[0].alt)
If you found this interesting please follow me and say hi.
