From cf7d701214b2e2d5c74ef67bb361a3502caadde7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 13 Sep 2025 03:42:47 +0200 Subject: [PATCH] Add support for `Link` objects in `attachment` --- app/lib/activitypub/activity/create.rb | 10 ++++++++- .../activitypub/parser/preview_card_parser.rb | 21 +++++++++++++++++++ .../activitypub/note_serializer.rb | 14 ++++++++++++- .../process_status_update_service.rb | 14 ++++++++++--- app/services/fetch_link_card_service.rb | 4 ++-- app/workers/link_crawl_worker.rb | 4 ++-- 6 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 app/lib/activitypub/parser/preview_card_parser.rb diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 50078c27dd..26b0cfb257 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -44,6 +44,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def process_status @tags = [] @mentions = [] + @links = [] @unresolved_mentions = [] @silenced_account_ids = [] @params = {} @@ -72,7 +73,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def distribute # Spread out crawling randomly to avoid DDoSing the link - LinkCrawlWorker.perform_in(rand(1..59).seconds, @status.id) + # If there are no links in the attachment property, scan text for links + LinkCrawlWorker.perform_in(rand(1..59).seconds, @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? @@ -273,6 +275,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 0000000000..5c679d8fce --- /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 8592b137ea..e7153a2e86 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] end def virtual_tags @@ -253,6 +253,18 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer } end + class PreviewCardSerializer < ActivityPub::Serializer + attributes :type, :href + + def type + 'Link' + end + + def href + object.original_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 2e0e6e8864..3061f1b72d 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -19,6 +19,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @quote_changed = false @request_id = request_id @quote = nil + @next_media_attachments = [] + @next_links = [] # Only native types can be updated at the moment return @status if !expected_type? || already_updated_more_recently? @@ -80,9 +82,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 @@ -334,7 +341,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| @@ -387,7 +395,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService def reset_preview_card! @status.reset_preview_card! - LinkCrawlWorker.perform_in(rand(1..59).seconds, @status.id) + LinkCrawlWorker.perform_in(rand(1..59).seconds, @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 84c4ba06f1..6aa86aa4fb 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 c63af1e43a..d49a1728d9 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