Bluesky with proto_rune

Print Cheatsheet Copy Markdown

Common Bluesky tasks with the ProtoRune and ProtoRune.Bsky APIs. All authenticated calls take the session returned by ProtoRune.login/3 as their first argument.

Credentials

Environment variables

VariablePurpose
BSKY_HANDLEYour handle, e.g. you.bsky.social
BSKY_APP_PASSWORDAn app password, not your main password
BSKY_SERVICEOptional PDS URL, defaults to https://bsky.social

Create app passwords at https://bsky.app/settings/app-passwords.

Logging in

{:ok, session} =
  ProtoRune.login(
    System.fetch_env!("BSKY_HANDLE"),
    System.fetch_env!("BSKY_APP_PASSWORD")
  )

session.handle # => "you.bsky.social"
session.did    # => "did:plc:..."

Refresh an expired session with ProtoRune.refresh_session/1.

Posting

Simple post

{:ok, post} = ProtoRune.post(session, "Hello Bluesky!")

post.uri # => "at://did:plc:.../app.bsky.feed.post/..."
post.cid # => "bafy..."

Options: :langs (default ["en"]), :reply_to (an AT-URI), :created_at (default DateTime.utc_now()).

Rich text post

alias ProtoRune.RichText

{:ok, rt} =
  RichText.new()
  |> RichText.text("Hello ")
  |> RichText.mention("alice.bsky.social")
  |> RichText.text("! Check out ")
  |> RichText.link("proto_rune", "https://github.com/zoedsoupe/proto_rune")
  |> RichText.text(" ")
  |> RichText.hashtag("elixir")
  |> RichText.build()

{:ok, post} = ProtoRune.Bsky.post(session, rt)

The builder resolves mention handles to DIDs and computes facet byte offsets for you.

Interactions

{:ok, _} = ProtoRune.Bsky.like(session, post.uri, post.cid)
{:ok, _} = ProtoRune.Bsky.repost(session, post.uri, post.cid)
{:ok, _} = ProtoRune.Bsky.follow(session, "alice.bsky.social")
:ok = ProtoRune.Bsky.delete_post(session, post.uri)

Reading

Timeline

{:ok, %{feed: items, cursor: cursor}} =
  ProtoRune.Bsky.get_timeline(session, limit: 25)

for item <- items do
  IO.puts("@#{item.post.author.handle}: #{item.post.record.text}")
end

# Next page
{:ok, %{feed: more}} =
  ProtoRune.Bsky.get_timeline(session, limit: 25, cursor: cursor)

Profiles and threads

{:ok, profile} = ProtoRune.Bsky.get_profile(session, "bob.bsky.social")
{:ok, thread} = ProtoRune.Bsky.get_post_thread(session, post.uri)
{:ok, %{posts: posts}} = ProtoRune.Bsky.search_posts(session, "elixir")

Bots

Defining a bot

defmodule GreeterBot do
  use ProtoRune.Bot, name: __MODULE__, strategy: :polling

  require Logger

  @impl true
  def get_identifier, do: System.fetch_env!("BSKY_HANDLE")

  @impl true
  def get_password, do: System.fetch_env!("BSKY_APP_PASSWORD")

  @impl true
  def handle_event(:mention, %{thread: %{post: post}}) do
    Logger.info("Mentioned by @#{post.author.handle}")
    {:ok, :handled}
  end

  def handle_event(_event, _payload), do: {:ok, :ignored}
end

{:ok, _pid} = GreeterBot.start_link()

Events

EventPayload
:mention, :reply, :quoteget_post_thread response
:like%{uri, user, subject}
:repost%{uri, user, post}
:follow%{uri, user}
:error%{reason}

Callbacks receive no session. To reply from a callback, log in with ProtoRune.login/3 yourself and keep the session around (for example in an Agent), then call ProtoRune.Bsky.post/3 with reply_to: uri.