July 18, 2023

Understanding ActivityPub Part 2: Lemmy

The Lemmy mascot wearing a graduation cap

This article is part of the series Understanding ActivityPub, which takes a look at the ActivityPub protocol through the lens of real-world examples. The protocol exchanges are taken from ActivityPub.Academy, a modified Mastodon instance that shows ActivityPub messages in real time (see the announcement post).

The following articles are available.

Introduction

While the first post used examples exclusively from Mastodon, this time we will focus on Lemmy, the Reddit alternative for the Fediverse.

At first sight, Mastodon and Lemmy look fairly different, so it might be surprising that they are based on the same underlying protocol. Not only that, they can talk to each other, show each other’s messages and more. In this post, we will see how this works.

The setup we’ll use is as follows.

A diagram showing a Mastodon instance and two Lemmy instances

This means that @alice is a user on the Mastodon instance activitypub.academy, @landon is a user on the Lemmy instance lemmy.world, and @crepels is a user on the Lemmy instance lemmy.activitypub.academy. They all will interact with the community !activitypub on lemmy.activitypub.academy.

The ActivityPub.Academy Activity Log will show activities sent between activitypub.academy and lemmy.activitypub.academy (it is not important that the two are under the same domain; if we follow a community on any other Lemmy instance, it will show the exchange of activities as well). We cannot see activities sent between lemmy.world and lemmy.activitypub.academy though (since ActivityPub.Academy has no access to them). But we’ll see that the communication between activitypub.academy and lemmy.activitypub.academy is enough to deduce the communication between lemmy.world and lemmy.activitypub.academy.

As in the first post, I encourage you to do your own experiments on ActivityPub.Academy to deepen your understanding of the protocol. To avoid spamming other Lemmy instances, you can use lemmy.activitypub.academy for any test posts or other things you want to try out (you cannot create new users there, but you can post with a user from any other Lemmy instance).

A new type of actor

In part one, I wrote “I think of actors as user accounts”. This mental model worked well in the world of Mastodon, but it no longer holds when looking at Lemmy. Like in Mastodon, users in Lemmy are represented by actors. But communities are represented by actors as well! This is possible because ActivityPub defines several types of actors. While user accounts are represented by actors of type Person, communities are represented by actors of type Group.

Since communities are actors, they have inboxes, can perform activities, and can be followed by other actors (if you’re not familiar with these concepts, check out part one). So on a protocol level, apart from having a different type, there is not much difference between Person actors and Group actors. The difference rather comes from the way the protocol is used by Lemmy. Let’s find out how this looks like.

Interacting with a community

Resolving community names to actor IDs

Before we can follow !activitypub, we have to find its actor ID. In part one, we saw how Mastodon uses the WebFinger protocol to resolve full mentions like @alice@activitypub.academy to actor IDs. Lemmy supports WebFinger as well, both for user names and community names. To find the actor ID for !activitypub@lemmy.activitypub.academy we query https://lemmy.world/.well-known/webfinger?resource=acct:activitypub@lemmy.activitypub.academy

{
  "subject":"acct:activitypub@lemmy.activitypub.academy",
    "links":[
      {
        "rel":"http://webfinger.net/rel/profile-page",
        "type":"text/html",
        "href":"https://lemmy.activitypub.academy/c/activitypub"
      },
      {
        "rel":"self",
        "type":"application/activity+json",
        "href":"https://lemmy.activitypub.academy/c/activitypub",
        "properties":{
          "https://www.w3.org/ns/activitystreams#type":"Group"
        }
      }
    ]
}

So the actor ID we are looking for is https://lemmy.activitypub.academy/c/activitypub.

Hint: On ActivityPub.Academy, you can type the full community name into the ActivityPub Explorer and it will be automatically translated into the corresponding WebFinger URL.

Following the community

Now we can follow !activitypub by both @alice and @landon.

On ActivityPub.Academy, we search for https://lemmy.activitypub.academy/c/activitypub (we cannot search for the community name !activitypub@lemmy.activitypub.academy since this notation is not supported by Mastodon). It returns a single result, shown as @activitypub@lemmy.activitypub.academy; since Mastodon only deals with Person actors, it pretends that every actor is a person. After clicking the follow button, we get the following Activity Log.

So our Follow activity is immediately answered by an Accept, the same pattern that we saw in part one.

On lemmy.world, we can search for the community by its name !activitypub@lemmy.activitypub.academy and then subscribe to it. We can’t see the exchange of activities here, but we can see the effect by looking at the ActivityPub data. Showing the data for https://lemmy.activitypub.academy/c/activitypub/followers in the ActivityPub Explorer shows the number of followers of !activitypub.

{
  "@context":[
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "lemmy":"https://join-lemmy.org/ns#",
      "litepub":"http://litepub.social/ns#",
      "pt":"https://joinpeertube.org/ns#",
      "sc":"http://schema.org/",
      "ChatMessage":"litepub:ChatMessage",
      "commentsEnabled":"pt:commentsEnabled",
      "sensitive":"as:sensitive",
      "matrixUserId":"lemmy:matrixUserId",
      "postingRestrictedToMods":"lemmy:postingRestrictedToMods",
      "removeData":"lemmy:removeData",
      "stickied":"lemmy:stickied",
      "moderators":{
        "@type":"@id",
        "@id":"lemmy:moderators"
      },
      "expires":"as:endTime",
      "distinguished":"lemmy:distinguished",
      "language":"sc:inLanguage",
      "identifier":"sc:identifier"
    }
  ],
  "id":"https://lemmy.activitypub.academy/c/activitypub/followers",
  "type":"Collection",
  "totalItems":3,
  "items":[
  ]
}

If we look at the data before and after @landon subscribes, we see that the follower count increases by one.

Posting and commenting

So far, everything worked similar to how it did on Mastodon. That changes when we look at post creation. We let @landon create a post in !activitypub, and while doing this, we keep a browser tab open with @alice’s Activity Log. After @landon posts his message, @alice receives the following activity.

Several interesting things happened that we haven’t seen in the last post.

Why does the community actor announce the Create activity of the new Page? Because this is a way to notify all instances that have followers of !activitypub of the new post. So no matter from which instance you have subscribed, you will see the new post immediately and will be able to interact with it.

It is not clear to me though why the Page object is announced separately as well; this seems redundant, since the Page is already contained in the Create.

Next, let’s see what happens when @landon adds a comment to his own post.

This time, there is only one Announce activity, announcing a Create activity with a Note as an object. We see that Lemmy uses different objects to distinguish between top-level posts (Page) and comments (Note). This is different from Mastodon, where everything is just a post.

For later use, we let @crepels (who is on the same instance as !activitypub) create a post as well.

Remember that what is shown here are the activities sent from lemmy.activitypub.academy to activitypub.academy. But we can also deduce the activities that were sent by lemmy.world to lemmy.activitypub.academy, namely the announced Create activities.

Voting

A central feature of Lemmy is the voting mechanism, which determines which posts and comments rise to the top. Let’s see how this is reflected on the protocol level. We let @landon upvote @crepels’s post, again keeping an eye on @alice’s Activity Log.

We see a Like from @landon, announced to all followers of !activitypub.

Downvoting works similarly, but using a Dislike activity instead.

So we see that every post and comment, and every upvote and downvote is distributed to all instances. This way Lemmy ensures that every instance has the full picture of all communities that its users follow. Users can see all posts with complete comment threads and accurate vote count, no matter on which instance they are. Compare this with Mastodon, where the boost and like counter varies on a post depending from which instance you view it (digging into the reasons deserves a blog post on its own). On Mastodon, these inaccuracies are acceptable since they are mostly vanity metrics, but on Lemmy, the voting count is a key element, so accuracy is important.

Voting privacy

It might be interesting to note that the Like and Dislike activity contain @landon as author. This means that in the Lemmy world every instance knows who voted on what, even the instances that don’t contain the post that’s voted on! It’s enough that a single user on an instance follows a community on a different instance to receive all the votes with their authors in that community. This is different from Reddit, where voting is a private matter.

To understand the extent of this, go to ActivityPub.Academy and subscribe to a high traffic community, like !memes@lemmy.ml. Within seconds, the Activity Log will be filled by Likes and Dislikes, each showing the author and the thing they voted on.

Fediverse Interoperability

One of the promises of the Fediverse is the interoperability between different instances, even if they belong to different platforms. This is made possible by ActivityPub. We have made use of this in this post to visualize Lemmy’s activities, but so far we have used the interoperability mostly passively. Now let’s get a little more active and see how far the interoperability goes. We’ll see that it is far from perfect, but analyzing the problems will give us a better understanding of how the platforms use the protocol.

Checking @alice’s home feed, we see that @landon’s and @crepels’s posts show up, as well as @landon’s comment.

Alice's home feed showing Landon's post and comment

Remember that Lemmy sent a Create activity with a Page object, but all Mastodon posts are usually Note objects. Here Mastodon was clever enough to still support the Page object and show it as a regular post, even though it only shows the title and then adds a link to the original post.

Replying to comments

We can now interact with the post from Mastodon; for example, let @alice reply to the “Goodbye” comment from @landon.

We see the Create activity sent out by activitypub.academy, but it is not followed by an Announce activity. This seems weird. When we check the post on lemmy.activitypub.academy, we see the comment from @landon, but there is no reply from @alice. So it seems like the activity wasn’t processed. But when we view the same post on lemmy.world, @alice’s comment shows up!

The same post on two Lemmy instances, but one is missing a comment

The same post on lemmy.activitypub.academy and on lemmy.world

What’s going on? Let’s examine the activity a little closer. We see that the activity is sent to the shared inbox of lemmy.world. Looking at the JSON source, we see that it is a public post (the to field contains https://www.w3.org/ns/activitystreams#Public), with copies sent to @landon and to @alice’s followers (check the cc field). There is no mention of !activitypub or lemmy.activitypub.academy at all. From Mastodon’s side, this works as expected, it’s exactly what happens when a Mastodon user replies to a Mastodon post. But Lemmy expects something different. For the distribution to all instances to work correctly, the community always has to be in the loop. We can verify this by having @landon reply to @alice’s comment.

This resulted in two activities delivered to activitypub.academy. First, a Create activity sent by @landon@lemmy.world. And second, an Announce activity with that same create, but sent by !activitypub@lemmy.activitypub.academy. So we can deduce that lemmy.world sent two Create activities, one directly to activitypub.academy, intended to be delivered to @alice, and one to lemmy.activitypub.academy, which then can distribute the post to all instances that have followers of !activitypub.

When we now check the post on the lemmy.activitypub.academy UI, we see that not only the reply by @landon shows up, but the comment by @alice appeared as well! Lemmy was clever enough to fill in the gaps. When the activity with @landon’s reply showed up, the instance realized that the comment that @landon was replying to wasn’t on the server, so it fetched it from the ActivityPub data.

Note that if @alice replies to a comment from @crepels, then everything works as expected. Since @crepels is on the same instance as !activitypub, it can distribute the reply to all other instances. (I won’t show the Activity Log here, but you can try it out yourself on ActivityPub.Academy.)

We’re not off to a good start. Replying to other people seems like a basic concept in a social network, and here it fails when used across platforms. To make matters worse, for each platform on its own, this works as expected, there is no bug on either side. They just use the protocol differently, but still according to the spec, as far as I can tell. So there doesn’t seem to be an easy fix for this.

Voting

Next, let’s try to vote on posts from Mastodon. There is no dislike option in Mastodon, so we cannot downvote, but Mastodon allows to favorite posts, and favorites are turned into Like activities. We could try to upvote @landon’s post, but we would likely see the same problem as with the replies: the Like activity would be delivered to lemmy.world, but not distributed to other instances (you can try this out yourself). Instead, we let her vote on @crepels’ post.

The Like is sent to lemmy.activitypub.academy. When we check the post on the lemmy.activitypub.academy UI, we see two upvotes, which means that the instance received and processed this Like. But there is no Announce activity, so it is not distributed to other instances! On lemmy.world, the post shows only one upvote.

Crepels post showing inconsistent votes on two different instances

The same post on lemmy.activitypub.academy and on lemmy.world

Let’s investigate again. First, we let @landon upvote the post as well.

The vote from @landon was properly announced. The post now has three upvotes on lemmy.activitypub.academy and two upvotes on lemmy.world. So what makes @landon’s post different from @alice’s? Looking at the JSON source, we see that the Like from Lemmy has an audience field with value https://lemmy.activitypub.academy/c/activitypub, but the Like from Mastodon does not. At first, it seems like this could be the culprit. But the PR that added the audience field mentions that the field is optional to be backwards compatible; otherwise, instances with older Lemmy versions could no longer vote. So what else could be the reason?

Looking at the recipients of the activities, we see that Lemmy always sends them to the shared inbox of an instance. But Mastodon sent the Like to !activitypub’s personal inbox. It looks like Lemmy processes those activities, meaning that it adds it to its internal vote count, but doesn’t consider them for distribution. Unlike the problem with replies that we saw above, this seems like a bug that can be fixed.

Creating top-level posts

Finally, we can try to create top-level posts. Of the features we looked at, this one seems the least likely to work, since top-level posts in Lemmy are Page objects and Mastodon can only create Note objects. But this time it’s Lemmy that’s clever enough to support this scenario.

Sending a direct message to !activitypub is a little quirky, since Mastodon requires that we @-mention the actors that we want to message. But since Mastdon thinks of all actors as Person actors anyway, we can simply message @activitypub@lemmy.activitypub.academy instead of !activitypub@lemmy.activitypub.academy.

Lemmy accepts the post and sends out Announce activities (again, one with the Create, and a separate one with the nested Note). Note that Mastodon always sends message activities to the shared inbox, so we are not suffering from the problem that we saw with the Like activity above. The post also shows up in the Lemmy UI.

Alice's post in the Lemmy UI

The title is duplicated in the body, and we have to deal with the @-mention, but other than that it looks like a regular Lemmy post.

Conclusion

We saw how Lemmy makes use of the ActivityPub protocol, and the way it differs from how Mastodon uses it. The key differences are a separate type of actor for communities, and systematic announcing of activities to keep all instances in sync, but the basic concepts are the same. We also saw the interoperability between Lemmy and Mastodon in action and investigated its shortcomings.

There are still more things to try out, for example Lemmy’s moderation tools. Moderators can block users, lock posts, mark posts as featured, appoint moderators for communities, and more. All of those are translated to activities because they have to be synced to all instances. But I’ll leave that for you to explore.

—Written by Sebastian Jambor. Follow me on Mastodon @crepels@mastodon.social for updates on new blog posts.