/do-bsky-atproto

Digital Ocean serverless function for posting to Bluesky with the AT Protocol

Primary LanguagePythonMIT LicenseMIT

do-bsky-atproto Goodreads example

(This is meant as an example of how to autopost web embeds to Bluesky with a Digital Ocean function; it's not a publicly accessible function. Make your own with this example code.)

What this does

For years, Goodreads was able to connect to Twitter and autopost about books that you recorded there (but with the destruction of Twitter's APIs, this has stopped working).

I want to create the same thing, but for Bluesky using the AT Protocol, which provides a nice API for automaticaly posting stuff to Bluesky.

As I describe here, I use Make.com to automate a lot of my personal data collection. I have a workflow that watches my Goodreads RSS feed and then does stuff with it (records the book in a central Airtable table, posts to Bluesky, posts to Mastodon, and sends me a notification through ntfy, because why not):

Full Make.com workflow

Posting links to Bluesky is a little trickier than Mastodon or Twitter because the AT Protocol allows you to add links with embedded previews to posts while omitting the URL from the text of the post (see "Website card embeds").

Example website card embed

Doing that, though, requires two API calls—one to upload the thumbnail image to Bluesky and one to post the full post (with thumbnail) to Bluesky. Getting all that to work with Make.com is too tricky, since it requires the ability to download and upload images.

So as a workaround, I decided to create a serverless Function as a Service Python function at Digital Ocean that can handle all the API work on-the-fly (using the atproto Python module) without needing a dedicated server constantly running.

The function (see packages/bsky_atproto/goodreads_rating/__main__.py) does this:

  1. Accepts JSON data with four values:

    {
        "user_rating": 5,
        "book_title": "Book title here",
        "author_name": "Author name here",
        "book_url": "https://www.goodreads.com/WHATEVER"
    }
  2. Parses the URL to extract the title, meta description, and og:image URL

  3. Uploads the thumbnail preview image to Bluesky as a blob

  4. Posts this template to Bluesky with a website card embed linked to the Goodreads page:

    {user_rating} of 5 stars to {book_title} by {author_name}
    

How to deploy

  1. Rename .env.example to .env and change the values there:

    • BSKY_USER: Your Bluesky username (e.g., example.bsky.social)
    • BSKY_PASSWORD: Your Bluesky app password (not your account password!)
    • WHISK: A random string to use in the X-Require-Whisk-Auth header in the HTTP request that you'll make
  2. Install doctl locally

  3. Make sure build.sh is executable:

    $ chmod a+x packages/bsky_atproto/goodreads_rating/build.sh
  4. Deploy the project with --remote-build:

    $ doctl serverless deploy do-bsky-atproto --remote-build

    This will install everything in requirements.txt and make the magical function server thing work.

How to use

Make an HTTP request with these four values, structured as JSON:

{
    "user_rating": 5,
    "book_title": "Book title here",
    "author_name": "Author name here",
    "book_url": "https://www.andrewheiss.com"
}

There are like a billion different ways to do this. Here's the same example with curl, R, Python, and Make.com:

curl

$ curl -X POST "https://example.com/bsky_atproto/goodreads_rating" \
  -H "X-Require-Whisk-Auth: WHISK_SECRET_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "user_rating": 5,
    "book_title": "Book title here",
    "author_name": "Author name here",
    "book_url": "https://www.andrewheiss.com"
  }'

R and {httr2}

library(httr2)

url <- "https://example.com/bsky_atproto/goodreads_rating"

data <- list(
  "user_rating" = 5,
  "book_title" = "Book title here",
  "author_name" = "Author name here",
  "book_url" = "https://www.andrewheiss.com"
)

response <- request(url) |> 
  req_method("POST") |> 
  req_headers(`X-Require-Whisk-Auth` = "WHISK_SECRET_HERE") |> 
  req_body_json(data) |> 
  req_perform()

Python and requests

import requests

url = "https://example.com/bsky_atproto/goodreads_rating"

headers = {
    "X-Require-Whisk-Auth": "WHISK_SECRET_HERE",
    "Content-Type": "application/json"
}

data = {
    "user_rating": 5,
    "book_title": "Book title here",
    "author_name": "Author name here",
    "book_url": "https://www.andrewheiss.com"
}

response = requests.post(url, headers=headers, json=data)

Make.com

Simple RSS to HTTP request in Make.com