mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Tag linked FeaturedCollection objects over ActivityPub (#38115)
This commit is contained in:
@@ -42,6 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
def process_status
|
||||
@tags = []
|
||||
@mentions = []
|
||||
@tagged_objects = []
|
||||
@unresolved_mentions = []
|
||||
@silenced_account_ids = []
|
||||
@params = {}
|
||||
@@ -56,6 +57,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
ApplicationRecord.transaction do
|
||||
@status = Status.create!(@params.merge(quote: @quote))
|
||||
attach_tags(@status)
|
||||
attach_tagged_objects(@status)
|
||||
attach_mentions(@status)
|
||||
attach_counts(@status)
|
||||
end
|
||||
@@ -181,6 +183,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
end
|
||||
|
||||
def attach_tagged_objects(status)
|
||||
@tagged_objects.each do |tagged_object|
|
||||
tagged_object.status = status
|
||||
tagged_object.save
|
||||
end
|
||||
end
|
||||
|
||||
def attach_mentions(status)
|
||||
@mentions.each do |mention|
|
||||
mention.status = status
|
||||
@@ -210,6 +219,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
process_mention tag
|
||||
elsif equals_or_includes?(tag['type'], 'Emoji')
|
||||
process_emoji tag
|
||||
elsif equals_or_includes?(tag['type'], 'FeaturedCollection')
|
||||
process_tagged_collection tag
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -266,6 +277,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
end
|
||||
|
||||
def process_tagged_collection(tag)
|
||||
return if tag['id'].blank?
|
||||
|
||||
# TODO: We probably want to resolve unknown objects and push them to an `@unresolved_tagged_objects` on failure
|
||||
collection = ActivityPub::TagManager.instance.uri_to_resource(tag['id'], Collection)
|
||||
|
||||
@tagged_objects << TaggedObject.new(uri: ActivityPub::TagManager.instance.uri_for(collection), object: collection, ap_type: 'FeaturedCollection') if collection.present?
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return [] if @object['attachment'].nil?
|
||||
|
||||
|
||||
@@ -262,6 +262,14 @@ class ActivityPub::TagManager
|
||||
uri_to_resource(uri, Account)
|
||||
end
|
||||
|
||||
def uri_to_local_collection(uri)
|
||||
path_params = Rails.application.routes.recognize_path(uri)
|
||||
return unless path_params[:controller] == 'collections'
|
||||
|
||||
# TODO: check account, but this requires handling potentially two different schemes
|
||||
Collection.find_by(id: path_params[:id])
|
||||
end
|
||||
|
||||
def uri_to_local_conversation(uri)
|
||||
path_params = Rails.application.routes.recognize_path(uri)
|
||||
return unless path_params[:controller] == 'activitypub/contexts'
|
||||
@@ -279,6 +287,8 @@ class ActivityPub::TagManager
|
||||
uris_to_local_accounts([uri]).first
|
||||
when 'Conversation'
|
||||
uri_to_local_conversation(uri)
|
||||
when 'Collection'
|
||||
uri_to_local_collection(uri)
|
||||
else
|
||||
StatusFinder.new(uri).status
|
||||
end
|
||||
|
||||
@@ -80,6 +80,7 @@ class Status < ApplicationRecord
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
has_many :media_attachments, dependent: :nullify
|
||||
has_many :tagged_objects, dependent: :destroy
|
||||
has_many :quotes, foreign_key: 'quoted_status_id', inverse_of: :quoted_status, dependent: :nullify
|
||||
|
||||
# The `dependent` option is enabled by the initial `mentions` association declaration
|
||||
@@ -172,6 +173,7 @@ class Status < ApplicationRecord
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: :account,
|
||||
tagged_objects: :object,
|
||||
reblog: [
|
||||
:application,
|
||||
:media_attachments,
|
||||
|
||||
19
app/models/tagged_object.rb
Normal file
19
app/models/tagged_object.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: tagged_objects
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# ap_type :string not null
|
||||
# object_type :string
|
||||
# uri :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# object_id :bigint(8)
|
||||
# status_id :bigint(8) not null
|
||||
#
|
||||
class TaggedObject < ApplicationRecord
|
||||
belongs_to :status, inverse_of: :tagged_objects
|
||||
belongs_to :object, polymorphic: true, optional: true
|
||||
end
|
||||
@@ -139,7 +139,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
|
||||
def virtual_tags
|
||||
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis
|
||||
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis + object.tagged_objects.map(&:object)
|
||||
end
|
||||
|
||||
def atom_uri
|
||||
@@ -345,8 +345,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
|
||||
end
|
||||
class CustomEmojiSerializer < ActivityPub::EmojiSerializer; end
|
||||
|
||||
class CollectionSerializer < ActivityPub::FeaturedCollectionSerializer; end
|
||||
|
||||
class OptionSerializer < ActivityPub::Serializer
|
||||
class RepliesSerializer < ActivityPub::Serializer
|
||||
|
||||
@@ -5,7 +5,7 @@ class REST::CollectionSerializer < ActiveModel::Serializer
|
||||
:local, :sensitive, :discoverable, :item_count,
|
||||
:created_at, :updated_at
|
||||
|
||||
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
|
||||
belongs_to :tag, serializer: REST::ShallowTagSerializer
|
||||
|
||||
has_many :items, serializer: REST::CollectionItemSerializer
|
||||
|
||||
|
||||
11
app/serializers/rest/shallow_tag_serializer.rb
Normal file
11
app/serializers/rest/shallow_tag_serializer.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::ShallowTagSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :name, :url
|
||||
|
||||
def url
|
||||
tag_url(object)
|
||||
end
|
||||
end
|
||||
@@ -28,6 +28,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
has_many :ordered_mentions, key: :mentions
|
||||
has_many :tags
|
||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||
has_many :tagged_collections, serializer: REST::CollectionSerializer
|
||||
|
||||
# Due to a ActiveModel::Serializer quirk, if you change any of the following, have a look at
|
||||
# updating `app/serializers/rest/shallow_status_serializer.rb` as well
|
||||
@@ -166,6 +167,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
object.active_mentions.to_a.sort_by(&:id)
|
||||
end
|
||||
|
||||
def tagged_collections
|
||||
object.tagged_objects.filter_map { |tagged_object| tagged_object.object if tagged_object.ap_type == 'FeaturedCollection' }
|
||||
end
|
||||
|
||||
def quote_approval
|
||||
{
|
||||
automatic: object.proper.quote_policy_as_keys(:automatic),
|
||||
@@ -208,13 +213,5 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
class TagSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :name, :url
|
||||
|
||||
def url
|
||||
tag_url(object)
|
||||
end
|
||||
end
|
||||
class TagSerializer < REST::ShallowTagSerializer; end
|
||||
end
|
||||
|
||||
@@ -182,9 +182,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
end
|
||||
|
||||
def update_metadata!
|
||||
@raw_tags = []
|
||||
@raw_tags = []
|
||||
@raw_mentions = []
|
||||
@raw_emojis = []
|
||||
@raw_tagged_objects = []
|
||||
@raw_emojis = []
|
||||
|
||||
as_array(@json['tag']).each do |tag|
|
||||
if equals_or_includes?(tag['type'], 'Hashtag')
|
||||
@@ -193,10 +194,13 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
@raw_mentions << tag['href'] if tag['href'].present?
|
||||
elsif equals_or_includes?(tag['type'], 'Emoji')
|
||||
@raw_emojis << tag
|
||||
elsif equals_or_includes?(tag['type'], 'FeaturedCollection')
|
||||
@raw_tagged_objects << tag if tag['id']
|
||||
end
|
||||
end
|
||||
|
||||
update_tags!
|
||||
update_tagged_objects!
|
||||
update_mentions!
|
||||
update_emojis!
|
||||
update_quote!
|
||||
@@ -229,6 +233,24 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
end
|
||||
end
|
||||
|
||||
def update_tagged_objects!
|
||||
current_tagged_objects = @raw_tagged_objects.filter_map do |tagged_object|
|
||||
url = tagged_object['id']
|
||||
|
||||
# TODO: We probably want to resolve unknown objects at authoring time
|
||||
ActivityPub::TagManager.instance.uri_to_resource(url, Collection)
|
||||
end
|
||||
|
||||
# Any previously-unresolved URI would be resolved here
|
||||
@status.tagged_objects.upsert_all(
|
||||
current_tagged_objects.uniq.map { |object| { object_type: object.class.name, object_id: object.id, uri: ActivityPub::TagManager.instance.uri_for(object), ap_type: 'FeaturedCollection' } },
|
||||
unique_by: %w(status_id uri)
|
||||
)
|
||||
|
||||
# Remove unused links
|
||||
@status.tagged_objects.where.not(uri: current_tagged_objects.map { |object| ActivityPub::TagManager.instance.uri_for(object) }).delete_all
|
||||
end
|
||||
|
||||
def update_mentions!
|
||||
unresolved_mentions = []
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class PostStatusService < BaseService
|
||||
process_mentions_service.call(@status)
|
||||
safeguard_mentions!(@status)
|
||||
safeguard_private_mention_quote!(@status)
|
||||
attach_tagged_objects!(@status)
|
||||
attach_quote!(@status)
|
||||
|
||||
antispam = Antispam.new(@status)
|
||||
@@ -111,6 +112,10 @@ class PostStatusService < BaseService
|
||||
status.quote.accept! if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote?
|
||||
end
|
||||
|
||||
def attach_tagged_objects!(status)
|
||||
ProcessLinksService.new.call(status)
|
||||
end
|
||||
|
||||
def safeguard_mentions!(status)
|
||||
return if @options[:allowed_mentions].nil?
|
||||
|
||||
|
||||
52
app/services/process_links_service.rb
Normal file
52
app/services/process_links_service.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProcessLinksService < BaseService
|
||||
include Payloadable
|
||||
|
||||
# Scan status for links to ActivityPub objects and attach them to statuses
|
||||
# @param [Status] status
|
||||
def call(status)
|
||||
return unless status.local?
|
||||
|
||||
@status = status
|
||||
@previous_objects = @status.tagged_objects.includes(:object).to_a
|
||||
@current_objects = []
|
||||
|
||||
Status.transaction do
|
||||
scan_text!
|
||||
assign_tagged_objects!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scan_text!
|
||||
urls = @status.text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||
|
||||
urls.each do |url|
|
||||
# We only support `FeaturedCollection` at this time
|
||||
|
||||
# TODO: We probably want to resolve unknown objects at authoring time
|
||||
object = ActivityPub::TagManager.instance.uri_to_resource(url.to_s, Collection)
|
||||
|
||||
tagged_object = @previous_objects.find { |x| x.object == object || x.uri == url }
|
||||
tagged_object ||= @current_objects.find { |x| x.object == object || x.uri == url }
|
||||
tagged_object ||= @status.tagged_objects.new(object: object, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(object))
|
||||
|
||||
@current_objects << tagged_object
|
||||
end
|
||||
end
|
||||
|
||||
def assign_tagged_objects!
|
||||
return unless @status.persisted?
|
||||
|
||||
@current_objects.each do |object|
|
||||
object.save if object.new_record?
|
||||
end
|
||||
|
||||
# If previous objects are no longer contained in the text, remove them to lighten the database
|
||||
removed_objects = @previous_objects - @current_objects
|
||||
|
||||
TaggedObject.where(id: removed_objects.map(&:id)).delete_all unless removed_objects.empty?
|
||||
end
|
||||
end
|
||||
@@ -134,6 +134,7 @@ class UpdateStatusService < BaseService
|
||||
def update_metadata!
|
||||
ProcessHashtagsService.new.call(@status)
|
||||
ProcessMentionsService.new.call(@status)
|
||||
ProcessLinksService.new.call(@status)
|
||||
end
|
||||
|
||||
def broadcast_updates!
|
||||
|
||||
Reference in New Issue
Block a user