Understanding ActivityPub Part 2: Lemmy
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.
Part 2: Lemmy (this article)
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.
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/
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/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/
{
"@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.
-
In the Mastodon world, we saw the
Announce
activity when a user boosted a post by another user, so theAnnounce
was triggered manually. In Lemmy, theAnnounce
activities are triggered automatically by every post. They are authored by the community actor and sent to every follower of the community. This means that activities sent between lemmy.world and lemmy.activitypub.academy trigger activities between lemmy.activitypub.academy and activitypub.academy. -
In Mastodon, the
object
of anAnnounce
activity is just a URL which points at the post that was boosted. The Mastodon instance is responsible for fetching the actual post data. In Lemmy, theobject
of theAnnounce
contains the announced object in its entirety. -
Finally, in Mastodon, posts were created by a
Create
activity with aNote
as an object. In Lemmy, we see aCreate
activity with aPage
as an object.
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 Like
s and Dislike
s, 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.
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!
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/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.
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/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.
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.