In the previous post, we learned how to add tags to your post when posting to Bluesky.

Today, we'll look at how you can mention another user in the post.

You might think that it's as easy as just adding tags as text prepended with a @ sign, but it is a little more complicated than that unfortunately.

If you add the metions as plain text, you'll see that they are represented that way in Bluesky and that you are unable to click on them.

To add real mentions in your Bluesky post, we again need to use a concept called "facets".

Let's see how mentions work.

Step 1: Get the list of mentioned users from the text

We will start with creating a helper function which can extract the mentioned users from the text and return the proper data structure as expected by Bluesky:

defmodule BlueskyHelpers do
  def get_mentions_as_facets(text) do
    Regex.scan(~r/@\S+/, text, return: :index)
  end
end

This function will parse all mentioned users starting with a @ from the text.

Step 2: Getting the did of the mentioned user

Before we can create the mention facet, we need to know the did of the mentioned user. To do this, we need to use the com.atproto.identity.resolveHandle function. We can also create a simple helper function for this:

defmodule BlueskyHelpers do
  def get_mentions_as_facets(text) do
    Regex.scan(~r/@[a-zA-Z0-9.-]+[a-zA-Z0-9]/, text, return: :index)
  end

  def resolve_handle(handle) do
    response =
      Req.get!(
        "https://bsky.social/xrpc/com.atproto.identity.resolveHandle",
        params: %{"handle" => String.trim_leading(handle, "@")}
      )

    if response.status === 400, do: nil, else: response.body["did"]
  end
end

This function resolve_handle/1 does a HTTP GET call to the endpoint passing the handle as a parameter. The handle should not have the leading @. If the handle exists, it will return the did of that user, in the other cases, we'll return nil.

Step 3: Create the list of mention facets

We can now use these two functions to construct the list of facets for the mentioned users. It is again the same structure as with the tags (by using the byte start and end), but the type will be app.bsky.richtext.facet#mention and we need to include the did as a value.

defmodule BlueskyHelpers do
  def get_mentions_as_facets(text) do
    Regex.scan(~r/@[a-zA-Z0-9.-]+[a-zA-Z0-9]/, text, return: :index)
    |> Enum.map(fn [{start, length}] -> mention_to_facet(text, start, length) end)
    |> Enum.filter(& &1) # Filter out the mentions that didn't resolve
  end

  def mention_to_facet(text, start, length) do
    mention =
      text
      |> String.slice(start, length)
      |> String.trim_leading("@")

    did = resolve_handle(mention)

    if did !== nil do
      %{
        index: %{
          byteStart: start,
          byteEnd: start + length
        },
        features: [
          %{
            "$type": "app.bsky.richtext.facet#mention",
            did: did
          }
        ]
      }
    else
      nil
    end
  end

  def resolve_handle(handle) do
    response =
      Req.get!(
        "https://bsky.social/xrpc/com.atproto.identity.resolveHandle",
        params: %{"handle" => String.trim_leading(handle, "@")}
      )

    if response.status === 400, do: nil, else: response.body["did"]
  end
end

What this code does is:

  • It will use a regular expression to extract the mentions from the text
  • It will then tranform each tag into the proper facet data structure, resolving the handle to a did, omitting the ones that don't exist

The helper function for turning the tag into a facet does:

  • It will use the start index and the length to get the actual tag text
  • It will then create a map with the index containing the byte start and end of the tag (including the # sign)
  • It will use the type app.bsky.richtext.facet#mention to indicate that this is a mention facet
  • It will specify the actual did for the facet (the result from resolving the handle)

Step 4: Creating the post record

This is again the same structure as in the previous posts, but with one change.

We now added the list of facets (combining the mentions and the tags as a single list).

# Create the current timestamp in ISO format with a trailing Z
created_at = DateTime.utc_now() |> DateTime.to_iso8601()

# The text to post
text = "Hello from @yellowduck.be #MyElixir #example"

# Get the list of facets
tag_facets = BlueskyHelpers.get_tags_as_facets(text)
mention_facets = BlueskyHelpers.get_mentions_as_facets(text)

# Create the post record
record =
  %{
    text: text,
    createdAt: created_at,
    "$type": "app.bsky.feed.post",
    langs: ["en-US"],
    facets: List.flatten([tag_facets | mention_facets]) # Add the flattened list of facets
  }

Step 5: Post to Bluesky

We can now use the same code from the previous post to publish the record:

%{"commit" => %{"rev" => rev}} =
  Req.post!(
    "https://bsky.social/xrpc/com.atproto.repo.createRecord",
    auth: {:bearer, token},
    json: %{
      repo: did, # The did value from step 1
      collection: "app.bsky.feed.post", # We want to post to our timeline
      record: record # The record we want to post to our timeline
    }
  ).body

If all goes well, the tags and mentions should show up in the post as clickable items.

Next time, we'll continue with facets with adding website cards.