Add support for Link objects in attachment (#36104)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Eugen Rochko
2026-07-22 10:39:29 +01:00
committed by GitHub
parent 99d97477f8
commit 0114b389b0
8 changed files with 127 additions and 19 deletions

View File

@@ -50,6 +50,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@tags = []
@mentions = []
@tagged_objects = []
@links = []
@unresolved_mentions = []
@unresolved_collections = []
@silenced_account_ids = []
@@ -82,7 +83,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def distribute
# Spread out crawling randomly to avoid DDoSing the link
LinkCrawlWorker.perform_in(rand(DISTRIBUTE_DELAY), @status.id)
LinkCrawlWorker.perform_in(rand(DISTRIBUTE_DELAY), @status.id, @links.first)
# Distribute into home and list feeds and notify mentioned accounts
::DistributionWorker.perform_async(@status.id, { 'silenced_account_ids' => @silenced_account_ids }) if @options[:override_timestamps] || @status.within_realtime_window?
@@ -297,6 +298,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
media_attachments = []
as_array(@object['attachment']).each do |attachment|
if attachment['href'].present?
preview_card_parser = ActivityPub::Parser::PreviewCardParser.new(attachment)
@links << preview_card_parser.url if preview_card_parser.url.present?
next
end
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
next if media_attachment_parser.remote_url.blank? || media_attachments.size >= Status::MEDIA_ATTACHMENTS_LIMIT

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
class ActivityPub::Parser::PreviewCardParser
include JsonLdHelper
def initialize(json)
@json = json
end
# @param [PreviewCard] previous_record
def significantly_changes?(previous_record)
url != previous_record.url
end
def url
url = Addressable::URI.parse(@json['href'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError
nil
end
end

View File

@@ -135,7 +135,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
def virtual_attachments
object.ordered_media_attachments
object.ordered_media_attachments + [object.preview_card].compact
end
def virtual_tags
@@ -248,6 +248,18 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
}
end
class PreviewCardSerializer < ActivityPub::Serializer
attributes :type, :href
def type
'Link'
end
def href
object.original_url.presence || object.url
end
end
class MediaAttachmentSerializer < ActivityPub::Serializer
context_extensions :blurhash, :focal_point

View File

@@ -22,6 +22,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
@quote_changed = false
@request_id = request_id
@quote = nil
@next_media_attachments = []
@next_links = []
return @status if !expected_type? || already_updated_more_recently?
@@ -87,9 +89,14 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
def update_media_attachments!
previous_media_attachments = @status.media_attachments.to_a
previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
@next_media_attachments = []
as_array(@json['attachment']).each do |attachment|
if attachment['href'].present?
preview_card_parser = ActivityPub::Parser::PreviewCardParser.new(attachment)
@next_links << preview_card_parser.url if preview_card_parser.url.present?
next
end
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > Status::MEDIA_ATTACHMENTS_LIMIT
@@ -385,7 +392,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
def update_counts!
likes = @status_parser.favourites_count
shares = @status_parser.reblogs_count
shares = @status_parser.reblogs_count
return if likes.nil? && shares.nil?
@status.status_stat.tap do |status_stat|
@@ -438,7 +446,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
def reset_preview_card!
@status.reset_preview_card!
LinkCrawlWorker.perform_in(rand(CRAWL_DELAY), @status.id)
LinkCrawlWorker.perform_in(rand(CRAWL_DELAY), @status.id, @next_links.first)
end
def broadcast_updates!

View File

@@ -15,9 +15,9 @@ class FetchLinkCardService < BaseService
)
}iox
def call(status)
def call(status, original_url = nil)
@status = status
@original_url = parse_urls
@original_url = original_url || parse_urls
return if @original_url.nil? || @status.with_preview_card? || @status.with_media? || @status.quote.present?

View File

@@ -5,8 +5,8 @@ class LinkCrawlWorker
sidekiq_options queue: 'pull', retry: 0
def perform(status_id)
FetchLinkCardService.new.call(Status.find(status_id))
def perform(status_id, url = nil)
FetchLinkCardService.new.call(Status.find(status_id), url)
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique
true
end

View File

@@ -277,24 +277,16 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with a standalone' do
context 'with a standalone post' do
let(:object_json) { build_object }
it 'creates status' do
it 'creates a status with expected text and direct privacy' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
it 'missing to/cc defaults to direct privacy' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'direct'
end
end
@@ -333,6 +325,55 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with a public status mentioning multiple links' do
let(:object_json) do
build_object(
to: 'as:Public',
content: 'This is a test <a href="https://joinmastodon.org">https://joinmastodon.org</a> <a href="https://activitypub.rocks/">https://activitypub.rocks/</a>'
)
end
it 'creates a status and schedules link fetching job for first link' do
expect { subject.perform }
.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(LinkCrawlWorker).to have_enqueued_sidekiq_job(status.id, nil)
expect(status.visibility).to eq 'public'
end
end
context 'with a public status specifying a link attachment (FEP-8967)' do
let(:object_json) do
build_object(
to: 'as:Public',
content: 'This is a test <a href="https://joinmastodon.org">https://joinmastodon.org</a> <a href="https://activitypub.rocks/">https://activitypub.rocks/</a>',
attachment: [
{
href: 'https://activitypub.rocks/',
},
]
)
end
it 'creates a status and schedules link fetching job for specified link' do
expect { subject.perform }
.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(LinkCrawlWorker).to have_enqueued_sidekiq_job(status.id, 'https://activitypub.rocks/')
expect(status.visibility).to eq 'public'
end
end
context 'when public with Public' do
let(:object_json) do
build_object(

View File

@@ -107,4 +107,23 @@ RSpec.describe ActivityPub::NoteSerializer do
})
end
end
context 'with a preview card' do
let(:preview_card) { Fabricate(:preview_card) }
before do
PreviewCardsStatus.create(status: parent, preview_card: preview_card)
end
it 'has the expected shape (using FEP-8967)' do
expect(subject).to include({
'type' => 'Note',
'attachment' => contain_exactly(
a_hash_including(
'href' => preview_card.url
)
),
})
end
end
end