Skip to content

ADD controller for ActivityPub subscriptions to releases actor

️ This is the third and last part of the merging of the overarching MR, please read its description if you want to understand what is happening here. :) Feel free to ask as many questions as you want if you want to understand the protocol!

What does this MR do and why?

This is the third and last part of the merging the overarching subscription MR.

This provides the controller, route and service used to create subscriptions.

We add the inbox endpoint as specified by ActivityPub on our releases actor to receive various post requests. The only requests we accept are Follow and unfollow (Undo > Follow) ones. If we receive an other kind of activity (that could be, for example, that someone commented on our release post in the Fediverse), we silently discard it.

The service's role is to create the blank subscription and to queue the background job that will resolve the various URLs and prepare the subscription.

How to set up and validate locally

A Sinatra app has been created to test communication with the application, as this is a server to server feature:

  1. prepare your GitLab local installation by making the flightjs/Fligthjs project public, enabling the activity_pub and activity_pub_project feature flags and allowing requests to the local network from webhooks and integrations. This can be done in a rails console with the following:
Feature.enable(:activity_pub)
Feature.enable(:activity_pub_project)
ApplicationSetting.first.update(allow_local_requests_from_web_hooks_and_services: true)
flight = Project.find_by_name("Flight")
flight.update(visibility: "public")
  1. Install and run the sinatra app:
git clone https://gitlab.com/oelmekki/activitypub-review
cd activitypub-review
bundle
ruby server.rb
  1. Visit the app URL and click the "subscribe" button
  2. You should see the success reply from GitLab (it may takes a while on first request in dev env)
  3. Go check the Sinatra logs, you should see:
  • a request to /subscriber/inbox, this is the GitLab server sending the Accept activity
  • a dump of the request object, you can verify it correctly sets the Accept http header to application/ld+json; profile="https://www.w3.org/ns/activitystreams"
  • a dump of the activity posted, as a JSON object, with type "Accept" and containing the initial Follow activity as object.

️ Note: if you want to run the test several times, you need to delete the subscription between each time: ActivityPub::ReleasesSubscription.destroy_all

MR acceptance checklist

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

Query plans

Requests are all in the service.

ReleasesSubscription.find_by_subscriber_url(subscriber_url)

SELECT "activity_pub_releases_subscriptions".* 
FROM "activity_pub_releases_subscriptions" 
WHERE (LOWER(subscriber_url) = 'https://example.com/new-actor') 
LIMIT 1
Limit  (cost=0.15..3.73 rows=1 width=162)
   ->  Index Scan using index_activity_pub_releases_sub_on_project_id_sub_url on activity_pub_releases_subscriptions  (cost=0.15..7.32 rows=2 width=162)
         Index Cond: (lower(subscriber_url) = 'https://example.com/new-actor'::text)

subscription.save

This generates 3 queries.

SELECT 1 AS one
FROM "activity_pub_releases_subscriptions"
WHERE LOWER("activity_pub_releases_subscriptions"."subscriber_url") = LOWER('https://example.com/new-actor')
AND "activity_pub_releases_subscriptions"."project_id" = 1
LIMIT 1
Limit  (cost=0.15..2.17 rows=1 width=4)
   ->  Index Scan using index_activity_pub_releases_sub_on_project_id_sub_url on activity_pub_releases_subscriptions  (cost=0.15..2.17 rows=1 width=4)
         Index Cond: ((project_id = 1) AND (lower(subscriber_url) = 'https://example.com/new-actor'::text))

There are tons of fields selected here, let me know if you want the details (this is not something I implemented, it comes from Release model callbacks).

SELECT [...]
FROM "application_settings"
ORDER BY "application_settings"."id" DESC
LIMIT 1
Limit  (cost=0.12..2.14 rows=1 width=12592)
   ->  Index Scan Backward using application_settings_pkey on application_settings  (cost=0.12..2.14 rows=1 width=12592)
INSERT INTO "activity_pub_releases_subscriptions" ("project_id", "created_at", "updated_at", "status", "subscriber_url", "payload")
VALUES (1, '2023-10-26 09:23:15.057518', '2023-10-26 09:23:15.057518', 0, 'https://example.com/new-actor', '{"@context":"https://www.w3.org/ns/activitystreams","id":"https://example.com/new-actor#follow-1","type":"Follow","actor":"https://example.com/new-actor","object":"https://localhost/our/project/-/releases"}')
RETURNING "id"
Insert on activity_pub_releases_subscriptions  (cost=0.00..0.01 rows=1 width=162)
   ->  Result  (cost=0.00..0.01 rows=1 width=162)

previous_subscription.destroy

DELETE FROM "activity_pub_releases_subscriptions"
WHERE "activity_pub_releases_subscriptions"."id" = 1
 Delete on activity_pub_releases_subscriptions  (cost=0.15..2.17 rows=0 width=0)
   ->  Index Scan using activity_pub_releases_subscriptions_pkey on activity_pub_releases_subscriptions  (cost=0.15..2.17 rows=1 width=6)
         Index Cond: (id = 1)

Related to epic &11247

Edited by kik

Merge request reports