diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 0ed0a10f377..0e932a695aa 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -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
diff --git a/app/lib/activitypub/parser/preview_card_parser.rb b/app/lib/activitypub/parser/preview_card_parser.rb
new file mode 100644
index 00000000000..5c679d8fce1
--- /dev/null
+++ b/app/lib/activitypub/parser/preview_card_parser.rb
@@ -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
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index d67770933b8..5caab3f95e8 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -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
diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb
index 954e5f0de09..d36eca842fd 100644
--- a/app/services/activitypub/process_status_update_service.rb
+++ b/app/services/activitypub/process_status_update_service.rb
@@ -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!
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 53b6861349b..4c9fc2e230f 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -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?
diff --git a/app/workers/link_crawl_worker.rb b/app/workers/link_crawl_worker.rb
index c63af1e43aa..d49a1728d92 100644
--- a/app/workers/link_crawl_worker.rb
+++ b/app/workers/link_crawl_worker.rb
@@ -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
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index dac1b2a33e4..8724a840149 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -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 https://joinmastodon.org https://activitypub.rocks/'
+ )
+ 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 https://joinmastodon.org https://activitypub.rocks/',
+ 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(
diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb
index dc71a19c6e2..2176d37cdce 100644
--- a/spec/serializers/activitypub/note_serializer_spec.rb
+++ b/spec/serializers/activitypub/note_serializer_spec.rb
@@ -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