From fee38e57f04b543789b842b179b3db4ec1a60151 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 16 Apr 2026 09:26:34 +0200 Subject: [PATCH] Federate and store a collection `url` (#38697) --- app/models/collection.rb | 1 + .../activitypub/featured_collection_serializer.rb | 15 +++++++++++++-- .../process_featured_collection_service.rb | 8 ++++++++ .../20260415133505_add_url_to_collections.rb | 7 +++++++ db/schema.rb | 3 ++- .../featured_collection_serializer_spec.rb | 1 + .../process_featured_collection_service_spec.rb | 14 ++++++++++++++ 7 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20260415133505_add_url_to_collections.rb diff --git a/app/models/collection.rb b/app/models/collection.rb index c8d4fb99bc..5956b7c087 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -15,6 +15,7 @@ # original_number_of_items :integer # sensitive :boolean not null # uri :string +# url :string # created_at :datetime not null # updated_at :datetime not null # account_id :bigint(8) not null diff --git a/app/serializers/activitypub/featured_collection_serializer.rb b/app/serializers/activitypub/featured_collection_serializer.rb index 00fdb368e6..60b4d8459c 100644 --- a/app/serializers/activitypub/featured_collection_serializer.rb +++ b/app/serializers/activitypub/featured_collection_serializer.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer - attributes :id, :type, :total_items, :name, :attributed_to, + # include Rails.application.routes.url_helpers + include RoutingHelper + + attributes :id, :type, :total_items, :name, :attributed_to, :url, :sensitive, :discoverable, :published, :updated attribute :summary, unless: :language_present? attribute :summary_map, if: :language_present? - has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer + has_one :topic, serializer: ActivityPub::NoteSerializer::TagSerializer has_many :collection_items, key: :ordered_items, serializer: ActivityPub::FeaturedItemSerializer @@ -31,6 +34,10 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer ActivityPub::TagManager.instance.uri_for(object.account) end + def url + account_collection_url(object.account, object) + end + def total_items object.accepted_collection_items.size end @@ -50,4 +57,8 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer def collection_items object.accepted_collection_items end + + def topic + object.tag + end end diff --git a/app/services/activitypub/process_featured_collection_service.rb b/app/services/activitypub/process_featured_collection_service.rb index f7ae08dd26..18e0da32d7 100644 --- a/app/services/activitypub/process_featured_collection_service.rb +++ b/app/services/activitypub/process_featured_collection_service.rb @@ -46,6 +46,13 @@ class ActivityPub::ProcessFeaturedCollectionService @json['summaryMap']&.keys&.first end + def url + url = url_to_href(@json['url'], 'text/html') + return @json['id'] if url.blank? || unsupported_uri_scheme?(url) + + url + end + def collection_attributes { local: false, @@ -56,6 +63,7 @@ class ActivityPub::ProcessFeaturedCollectionService discoverable: @json['discoverable'], original_number_of_items: @json['totalItems'] || 0, tag_name: @json.dig('topic', 'name'), + url:, } end diff --git a/db/migrate/20260415133505_add_url_to_collections.rb b/db/migrate/20260415133505_add_url_to_collections.rb new file mode 100644 index 0000000000..b40a792fc4 --- /dev/null +++ b/db/migrate/20260415133505_add_url_to_collections.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddURLToCollections < ActiveRecord::Migration[8.1] + def change + add_column :collections, :url, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 6dddbc0efa..677ed7caa9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_04_10_083500) do +ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -400,6 +400,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_10_083500) do t.bigint "tag_id" t.datetime "updated_at", null: false t.string "uri" + t.string "url" t.index ["account_id"], name: "index_collections_on_account_id" t.index ["tag_id"], name: "index_collections_on_tag_id" t.index ["uri"], name: "index_collections_on_uri", unique: true, where: "(uri IS NOT NULL)" diff --git a/spec/serializers/activitypub/featured_collection_serializer_spec.rb b/spec/serializers/activitypub/featured_collection_serializer_spec.rb index 78b9daf613..64b2fdebbe 100644 --- a/spec/serializers/activitypub/featured_collection_serializer_spec.rb +++ b/spec/serializers/activitypub/featured_collection_serializer_spec.rb @@ -25,6 +25,7 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do 'attributedTo' => ActivityPub::TagManager.instance.uri_for(collection.account), 'sensitive' => false, 'discoverable' => false, + 'url' => account_collection_url(collection.account, collection), 'topic' => { 'href' => match(%r{/tags/people$}), 'type' => 'Hashtag', diff --git a/spec/services/activitypub/process_featured_collection_service_spec.rb b/spec/services/activitypub/process_featured_collection_service_spec.rb index 343d3ac3b2..6a74eaa398 100644 --- a/spec/services/activitypub/process_featured_collection_service_spec.rb +++ b/spec/services/activitypub/process_featured_collection_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do 'name' => 'Good people from other servers', 'sensitive' => false, 'discoverable' => true, + 'url' => 'https://example.com/c/1', 'topic' => { 'type' => 'Hashtag', 'name' => '#people', @@ -50,6 +51,7 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do expect(new_collection.description_html).to eq '

A list of remote actors you should follow.

' expect(new_collection.sensitive).to be false expect(new_collection.discoverable).to be true + expect(new_collection.url).to eq 'https://example.com/c/1' expect(new_collection.tag.formatted_name).to eq '#people' expect(ActivityPub::ProcessFeaturedItemWorker).to have_enqueued_sidekiq_job.with(new_collection.id, 'https://example.com/featured_items/1', 1, nil) @@ -75,6 +77,18 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do end end + context 'when the json does not include a `url`' do + let(:featured_collection_json) do + base_json.except('url') + end + + it 'uses the `id` instead' do + subject.call(account, featured_collection_json) + + expect(Collection.last.url).to eq 'https://example.com/featured_collections/1' + end + end + context 'when the collection already exists' do let(:collection) { Fabricate(:remote_collection, account:, uri: base_json['id'], name: 'placeholder') }