(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.)
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):
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").
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:
-
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" }
-
Parses the URL to extract the title, meta description, and og:image URL
-
Uploads the thumbnail preview image to Bluesky as a blob
-
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}
-
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 theX-Require-Whisk-Auth
header in the HTTP request that you'll make
-
Install
doctl
locally -
Make sure
build.sh
is executable:$ chmod a+x packages/bsky_atproto/goodreads_rating/build.sh
-
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.
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 -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"
}'
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()
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)