Skip to content

Add ActivityPub Releases profile and outbox

kik requested to merge gitlab-community/gitlab:activitypub_releases into master

What does this MR do and why?

This MR is the first part in the implementation of ActivityPub in Gitlab, as specified in my design documents, in the context of &260 , and more specifically #21582 (which illustrates the kind of user support there is behind adding this).

Summary of those sources: we want to implement the ActivityPub protocol in Gitlab to allow it to federate instances and join the fediverse, so that:

  • the fediverse (like Mastodon, Lemmy, and apparently soon Threads) can subscribe to content in Gitlab and get Gitlab activities in their timeline
  • various instances of Gitlab can communicate, for example for cross-instance discussions on issues, without the need to create an account on every instances out there
  • ultimately, cross instances merge requests become possible

This is the very humble beginning. As a first step of the first step of… we're implementing a way for the fediverse to read content from Gitlab.

We're starting by exposing project releases, as it will be immediately useful: when this MR and the rest of the first series will be merged, fediverse users will be able to be notified on their fediverse app when their favorite software releases a new version (provided it's on Gitlab, obviously :) ).

This MR alone does not allow that yet, but it allows to display the profile of the resource (called an actor, in ActivityPub terminology).

After that, the following MRs will be about:

  1. allowing to subscribe by implementing POST inbox and the pushing of messages when new events happen (after that, the ActivityPub implementation for releases is complete, but we still need to add a few things to be compatible with Mastodon and most fediverse apps, namely: )
  2. we need to implement the Webfinger protocol for profile discovery (it's actually already sort of implemented in Gitlab through doorkeeper-openid_connect, but we need to extend it)
  3. we need to implement request signing and verifying, as a security measure to limit spam and instance impersonation.

When all of this will be done, we will have joined the fediverse with our first actor, the one related to project releases. Interestingly, we will also have in place the security and (some) discovery features that we will need later for cross instance communication, thanks to the work of Mastodon and the rest of the fediverse that we're following here.

To technical reviewers

The process of following an ActivityPub actor is as follow:

  1. You make a GET request with the Accept header application/ld+json; profile="" on the actor url to get info about it, notably the url of its outbox. An actor is anything that generates activities, a project releases page, in our case. The "outbox" is a page listing those activities in ActivityStreams format (a bit similar to a RSS page). Those activities are the various release creation events, in our case.
  2. You make an other request, with the same Accept header, on the outbox url to see the actor activities.
  3. If you like it, you make a POST request on the inbox of the actor to ask to subscribe to it.
  4. When an activity happen, the actor push to your own inbox.

Only the first two steps are implemented here, the rest will follow in following up MRs.

This first MR implements the base class for serializers and a first actor to use the feature, allowing to design it with a real world use case, and not in pure abstraction.

There is documentation about how to implement an ActivityPub actor in doc/api//activity_pub/ You probably should read it to understand this MR.

That base class is in app/serializers/activitypub/activity_streams_serializer.rb. I wanted to make it a concern rather than a parent class as that's the way that what chosen for others serializers features, but it caused many problems, most notably the fact that I needed to be able to reference the super #represent method via alias_method, and you can't do that in a module : from the module point of view, the original method does not exist until the module has been included, and then it's overwritten by the new method (so you can't even execute the alias_method in a #included block). This kind of method ascendance is work for inheritance, not composition.

There is one limitation of the implemented mime-type : it associates the apub format (for ActivityPub) to all application/ld+json mime-types, and not only to application/ld+json; profile="" as required by the standard. The reason for that is that ActionDispatch is validating the mime-type to forbid : and / characters, and throwing exception if we want to use an url in the mime-type. It's a perfectly valid mime-type, actually recommended by the w3c, so I'll open a PR upstream to fix that, especially since any rails app that tries to implement ActivityPub will hit that problem (unless they do like Mastodon and ignore the mime-type and the w3c specs to make dedicated controllers accepting all mime-types, but I suspect this ActionDispatch bug was part of their reasoning leading to that design).

One thing I'm not satisfied with is how I had to stub most methods from Kaminari to use build_stubbed_list on a paginated resource in spec/serializers/activity_pub/activity_streams_serializer_spec.rb and spec/serializers/activity_pub/releases_outbox_serializer_spec.rb. I fought against that a lot : since the result of build_stubbed_list is not an actual relation, we can't call #page on it (it's a method missing exception) nor call all other Kaminari helpers. What I think would be the best solution would be to use create_list instead to avoid overstubbing, but Rubocop would not let me do that. If you guys agree, I can add a Rubocop exception. I'm all ear if you know a better way of stubbing Kaminari features. :) One thing to consider : all following ActivityPub outboxes will implement similar tests, so this is not a one off decision.

How to set up and validate locally

  1. make a few releases in a project
  2. make sure that project is public
  3. run the following to test the profile page, replacing flightjs/Flight with the fullpath of your project:
curl -H 'Accept: application/ld+json; profile=""'
  1. run the following with similar edit to test the pagination index:
curl -H 'Accept: application/ld+json; profile=""'
  1. run the following (samey) to test a pagination page:
curl -H 'Accept: application/ld+json; profile=""'
  1. you can reproduce the same tests replacing Accept: application/ld+json; profile="" with Accept: application/activity+json

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by kik

Merge request reports