Bluesky API Python experiments

Screenshot by Dropbox Capture.png

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.



0
0
0.000
6 comments
avatar

I'm resisting signing up at the moment, but if that becomes the default short post site then I may consider it. I could be tempted to do automatic posts about my solar panels as @edent does on Mastodon, but then I could do that on Hive too.

For now I have enough social media going on to occupy my time, but it's good to have options.

!BEER

0
0
0.000
avatar

Cool that you share. I actually prefer to use web3 instead of another web2 alternative :)

0
0
0.000
avatar

The niches I follow aren’t on these sites and up until recently I had to follow them on Twitter and facebook groups so I’m happy there’s some competition forming :)

0
0
0.000
avatar

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support. 
 

0
0
0.000