Drop #305 (2023-07-28): Weekend Project Edition

Post It!

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.

white clouds during daytime

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

persons feet on water

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
  1. https://: This is the protocol used for secure communication over the internet.

  2. bsky.app: This is the domain name for the Bluesky app, which is a social networking application built on the AT Protocol.

  3. /profile: This part of the URL indicates that the following content is related to a user profile.

  4. 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) .

  5. /post: This part of the URL indicates that the following content is related to a specific post made by the user.

  6. 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

brass door knob on brown wooden door

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

white and brown puppy petching wood

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

a person holding a drawing

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. ā˜®

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.