Mastodon Discovery

Making notes is helpful when reading and running unfamiliar code for the first time. I usually start with happy paths. Here’s some notes I made while learning about Mastodon account search and discovery. It’s really cool to poke around the code that so many people are using every day to find each other.

When you search on an account identifier on Mastodon, your browser makes a request to your Mastodon instance:

/api/v2/search?q=%40herestomwiththeweather%40mastodon.social&resolve=true&limit=5

The resolve=true parameter tells your Mastodon instance to make a webfinger request to the target Mastodon instance if necessary. The search controller makes a call to the SearchService

  def search_results
    SearchService.new.call(
      params[:q],
      current_account,
      limit_param(RESULTS_LIMIT),
      search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
    )
  end

and since resolve=true, SearchService makes a call to the ResolveAccountService

      if options[:resolve]
        ResolveAccountService.new.call(query)

The purpose of ResolveAccountService is to “Find or create an account record for a remote user” and return an account object to the search controller. It includes WebfingerHelper which is a trivial module with just one one-line method named webfinger!()

module WebfingerHelper
  def webfinger!(uri)
    Webfinger.new(uri).perform
  end
end

This method returns a webfinger object. Rather than call it directly, ResolveAccountService invokes process_webfinger! which invokes it and then asks the returned webfinger object’s subject method for its username and domain and makes them instance variables of the service object.

  def process_webfinger!(uri)
    @webfinger                           = webfinger!("acct:#{uri}")
    confirmed_username, confirmed_domain = split_acct(@webfinger.subject)

    if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
      @username = confirmed_username
      @domain   = confirmed_domain
      return
    end

If the Mastodon instance does not already know about this account, ResolveAccountService invokes fetch_account! which calls the ActivityPub::FetchRemoteAccountService which inherits from ActivityPub::FetchRemoteActorService

      @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])

The actor_url will look something like

https://mastodon.social/users/herestomwiththeweather

The ActivityPub::FetchRemoteActorService passes the actor_url parameter to fetch_resource to receive a json response for the remote account.

    @json = begin
      if prefetched_body.nil?
        fetch_resource(uri, id)
      else

The response includes a lot of information including name, summary, publicKey, images and urls to fetch more information like followers and following.

Finally, the ActivityPub::FetchRemoteActorService calls the ActivityPub::ProcessAccountService, passing it the json response.

    ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)

If the Mastodon instance does not know about the account, ActivityPub::ProcessAccountService invokes create_account and update_account to save the username, domain and all the associated urls from the json response to a new account record in the database.

      create_account if @account.nil?
      update_account

I have several questions about how following others works and will probably look at that soon. I may start out by reading A highly opinionated guide to learning about ActivityPub which I bookmarked a while ago.

IndieAuth login history

In my last post, I mentioned that I planned to add login history to Irwin. As I was testing my code, I logged into indieweb.org and noticed that I needed to update my code to support 5.3.2 Profile URL Response of the IndieAuth spec as this IndieAuth client does not need an access token. Here’s what the history looks like on my IndieAuth server:

IndieAuth login history

If I click on a login timestamp, I have the option to revoke the access token associated with the login if it exists and has not already expired. My next step is to test some other micropub servers than the one I use to see what interoperability updates I may need to make.

Minimum Viable IndieAuth Server

One of the building blocks of the Indieweb is IndieAuth. Like many others, I bootstrapped my experience with indieauth.com but as Marty McGuire explains, there are good reasons to switch and even consider building your own. Because I wanted a server as simple to understand as possible but also wanted to be able to add features that are usually not available, I created a rails project called Irwin and recently configured my blog to use it.

This is not production ready code. While I know that the micropub server I use works with it, I expect others may not. Also, there is no support for refresh tokens and other things in the spec that I didn’t consider high priority. It does support PKCE but not the less useful “plain” method.

All of IndieAuth Spec Updates 2020 was very clear and helpful. In one case, I made the server probably too strict (as an easy way to curtail spam registrations). It requires that the hosts for a blog’s authorization endpoint and token endpoint match the host of the IndieAuth server before a user can register an account on the indieauth server.

I plan to add an option for a user to keep a history of logins to indieauth clients soon. Please let me know if you have any questions or suggestions.