

Discover more from hrbrmstr's Daily Drop
Today, we build a small CLI utility to display Bluesky posts, given a Bluesky post URL.
BEFORE YOU CLICK AWAY (since not everyone has or wants access to Bluesky) 👉🏼 you do not need a Bluesky account to work on this week’s WPE. While said account is usually required to read Bluesky posts (unless you spy on the firehose), I’ve set up a REST API — that should work through and past the weekend — which handles one integral component: getting the JSON for the feed, given the correct parameters.
Fundamentally, all that’s truly needed is curl
, trurl
(https://github.com/curl/trurl), and jq
to complete this assignment, but you can use anything you like. Those three tools are available across all operating systems and drop in painlessly. So, we’ll look at what’s required to get everything we need to take in a Bluesky post/thread URL and retrieve the contents of it to do anything you like with it.
After you’re done, you’ll be able to view any post you have a URL (or just the right parameters) for.
Dipping Our Toes Into The Bluesky/AT Protocol Waters
To continue the metaphor used in the title: we’re most certainly not diving into the AT Protocol, today. Previous Drops have introduced it, and the focus on these Summer WPEs is to be able to build something pretty quickly.
Bluesky post URLs look like this:
https://bsky.app/profile/greynoise.bsky.social/post/3k3j5a5pl2x2r
https://: This is the protocol used for secure communication over the internet.
bsky.app: This is the domain name for the Bluesky app, which is a social networking application built on the AT Protocol.
/profile: This part of the URL indicates that the following content is related to a user profile.
greynoise.bsky.social: This is the unique identifier for the user’s profile on the Bluesky network. It consists of a username (greynoise) and the domain name (bsky.social) .
/post: This part of the URL indicates that the following content is related to a specific post made by the user.
3k3j5a5pl2x2r: This is the unique identifier for the post within the user’s profile.
Getting the data we ultimately want is not as simple as just making a request to that URL and retrieving the contents. We’re going to need the user profile identifier (userId
), post identifier (postId
), from a post URL to be able to retrieve the post contents. From the example URL provided, those would be :
userId
:greynoise.bsky.social
postId
:3k3j5a5pl2x2r
We can use the aforementioned trurl
utility:
$ trurl --json "https://bsky.app/profile/greynoise.bsky.social/post/3k3j5a5pl2x2r" | jq -r '.[0].parts.path'
/profile/greynoise.bsky.social/post/3k3j5a5pl2x2r
and some regular expression capture groups to get those values:
postPath="$(trurl --json "https://bsky.app/profile/greynoise.bsky.social/post/3k3j5a5pl2x2r" | jq -r '.[0].parts.path')"
if [[ "${postPath}" =~ /profile/([^/]+)/post/(.+) ]]; then
userId="${BASH_REMATCH[1]}"
postId="${BASH_REMATCH[2]}"
fi
NOTE: In zsh
, BASH_REMATCH
is just match
.
Now we're nearly there!
Resolving Handles
The endpoint we would normally need to hit is app.bsky.feed.getPostThread. This is an authenticated endpoint, which is why I've set up a helper REST API for y'all. So, while you won't be hitting that URL directly, you will still need to provide the components for the uri
that's listed in the parameters
section of the getPostThread
documentation.
This uri
is is an AT URI. AT URIs are always a reference to individual records in a given repository. The AT URI for that the post we're using as an example is:
at://did:plc:3y5lhkm7gbrixowhpoum2zaq/app.bsky.feed.post/3k3j5a5pl2x2r
You may have noticed that the greynoise.bsky.social
is missing! That's because we need to turn it into a decentralized identifier (DID). This is a persistent identifier that won't — or, at least, should not — change even if you handle changes. i.e., When I signed up for Bluesky, my original handle was hrbrmstr.bsky.social
. I eventually set up my own handle — hrbrmstr.dev
— and my DID remained the same.
Thankfully, Bluesky has an unauthenticated endpoint to turn registered handles into DIDs:
$ curl -s "https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${userId}" | jq -r '.did'
did:plc:3y5lhkm7gbrixowhpoum2zaq
That did
is the last component you need to retrieve a Bluesky post from my REST API.
Post Retrieval
Armed with the did
and the postId
, you're ready to retrieve the contents of a Bluesky post.
The API server I have set up for this is at api.hrbrmstr.de
(note the lack of a v
!). The API path prefix is /bsky
and it expects the did
in the second path component, and the postId
in the last. This is what the API call might look like (without using variables):
curl -s "https://api.hrbrmstr.de/bsky/did:plc:3y5lhkm7gbrixowhpoum2zaq/3k3j5a5pl2x2r"
Sample JSON output is in this snippet.
Putting It All Together
This is a contiguous view of the bash code:
postURL="https://bsky.app/profile/greynoise.bsky.social/post/3k3j5a5pl2x2r"
postPath="$(trurl --json "${postURL}" | jq -r '.[0].parts.path')"
if [[ "${postPath}" =~ /profile/([^/]+)/post/(.+) ]]; then
userId="${BASH_REMATCH[1]}"
postId="${BASH_REMATCH[2]}"
did=$(curl -s "https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${userId}" | jq -r '.did')
res=$(curl -s "https://api.hrbrmstr.de/bsky/${did}/${postId}")
echo "${res}" | jq -r '.data.thread.post.record.text' | fold -w 60 -s
fi
You'll want to modify it to take a URL as a parameter and display more fields.
FIN
For those more JS-inclined, there's a Glitch project you can clone to hack and expand on (for free! And, with no need to install anything locally) as well.
I was going to stand up an OG tag/embed service based on this, but these folks already have, and did a bang-up job, so I moved my efforts to this WPE instead. ☮