Tag linked FeaturedCollection objects over ActivityPub (#38115)
This commit is contained in:
parent
1935f4db79
commit
39d9da3b82
@ -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!
|
||||
|
||||
17
db/migrate/20260319142348_create_tagged_objects.rb
Normal file
17
db/migrate/20260319142348_create_tagged_objects.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateTaggedObjects < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :tagged_objects do |t|
|
||||
t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: false
|
||||
t.references :object, polymorphic: true, null: true
|
||||
t.string :ap_type, null: false
|
||||
t.string :uri
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :tagged_objects, [:status_id, :object_type, :object_id], unique: true, where: 'object_type IS NOT NULL AND object_id IS NOT NULL'
|
||||
add_index :tagged_objects, [:status_id, :uri], unique: true, where: 'uri IS NOT NULL'
|
||||
end
|
||||
end
|
||||
16
db/schema.rb
16
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_03_18_144837) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_03_19_142348) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
@ -1242,6 +1242,19 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_18_144837) do
|
||||
t.index ["tag_id", "language"], name: "index_tag_trends_on_tag_id_and_language", unique: true
|
||||
end
|
||||
|
||||
create_table "tagged_objects", force: :cascade do |t|
|
||||
t.string "ap_type", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "object_id"
|
||||
t.string "object_type"
|
||||
t.bigint "status_id", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "uri"
|
||||
t.index ["object_type", "object_id"], name: "index_tagged_objects_on_object"
|
||||
t.index ["status_id", "object_type", "object_id"], name: "idx_on_status_id_object_type_object_id_d6ebe374bd", unique: true, where: "((object_type IS NOT NULL) AND (object_id IS NOT NULL))"
|
||||
t.index ["status_id", "uri"], name: "index_tagged_objects_on_status_id_and_uri", unique: true, where: "(uri IS NOT NULL)"
|
||||
end
|
||||
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.string "display_name"
|
||||
@ -1547,6 +1560,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_18_144837) do
|
||||
add_foreign_key "tag_follows", "accounts", on_delete: :cascade
|
||||
add_foreign_key "tag_follows", "tags", on_delete: :cascade
|
||||
add_foreign_key "tag_trends", "tags", on_delete: :cascade
|
||||
add_foreign_key "tagged_objects", "statuses", on_delete: :cascade
|
||||
add_foreign_key "tombstones", "accounts", on_delete: :cascade
|
||||
add_foreign_key "user_invite_requests", "users", on_delete: :cascade
|
||||
add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
|
||||
|
||||
8
spec/fabricators/tagged_object_fabricator.rb
Normal file
8
spec/fabricators/tagged_object_fabricator.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:tagged_object) do
|
||||
status
|
||||
object nil
|
||||
ap_type 'FeaturedCollection'
|
||||
uri { Faker::Internet.device_token }
|
||||
end
|
||||
@ -681,6 +681,30 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tagged Featured Collections' do
|
||||
let(:featured_collection) { Fabricate(:collection) }
|
||||
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
tag: [
|
||||
{
|
||||
type: 'FeaturedCollection',
|
||||
id: ActivityPub::TagManager.instance.uri_for(featured_collection),
|
||||
},
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates the status with appropriate tagged objects' do
|
||||
expect { subject.perform }
|
||||
.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status.tagged_objects.map(&:object)).to contain_exactly(featured_collection)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hashtags' do
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
|
||||
@ -671,5 +671,15 @@ RSpec.describe ActivityPub::TagManager do
|
||||
status = Fabricate(:status, uri: 'https://example.com/123')
|
||||
expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status
|
||||
end
|
||||
|
||||
it 'returns the local featured collection' do
|
||||
collection = Fabricate(:collection)
|
||||
expect(subject.uri_to_resource(subject.uri_for(collection), Collection)).to eq collection
|
||||
end
|
||||
|
||||
it 'returns the remote featured collection' do
|
||||
collection = Fabricate(:remote_collection)
|
||||
expect(subject.uri_to_resource(subject.uri_for(collection), Collection)).to eq collection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -43,6 +43,26 @@ RSpec.describe ActivityPub::NoteSerializer do
|
||||
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
|
||||
end
|
||||
|
||||
context 'with tagged featured collections' do
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
|
||||
before do
|
||||
parent.tagged_objects.create!(object: collection, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(collection))
|
||||
end
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
'type' => 'Note',
|
||||
'tag' => include(
|
||||
a_hash_including({
|
||||
'type' => 'FeaturedCollection',
|
||||
'id' => ActivityPub::TagManager.instance.uri_for(collection),
|
||||
})
|
||||
),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a quote' do
|
||||
let(:quoted_status) { Fabricate(:status) }
|
||||
let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, state: :accepted) }
|
||||
|
||||
@ -92,5 +92,22 @@ RSpec.describe REST::StatusSerializer do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a tagged collection' do
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
|
||||
before do
|
||||
status.tagged_objects.create!(object: collection, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(collection))
|
||||
end
|
||||
|
||||
it 'contains the tagged collection' do
|
||||
expect(subject)
|
||||
.to include(
|
||||
'tagged_collections' => [a_hash_including(
|
||||
'id' => collection.id.to_s
|
||||
)]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -20,6 +20,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
{ type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
|
||||
{ type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
|
||||
{ type: 'Mention', href: bogus_mention },
|
||||
{ type: 'FeaturedCollection', id: ActivityPub::TagManager.instance.uri_for(featured_collection) },
|
||||
],
|
||||
}
|
||||
end
|
||||
@ -27,6 +28,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
|
||||
let(:alice) { Fabricate(:account) }
|
||||
let(:bob) { Fabricate(:account) }
|
||||
let(:featured_collection) { Fabricate(:collection) }
|
||||
|
||||
let(:mentions) { [] }
|
||||
let(:tags) { [] }
|
||||
@ -276,6 +278,16 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when originally without tagged objects' do
|
||||
before do
|
||||
subject.call(status, json, json)
|
||||
end
|
||||
|
||||
it 'updates tags' do
|
||||
expect(status.tagged_objects.reload.map(&:object)).to contain_exactly(featured_collection)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when originally without tags' do
|
||||
before do
|
||||
subject.call(status, json, json)
|
||||
|
||||
@ -209,6 +209,16 @@ RSpec.describe PostStatusService do
|
||||
expect(hashtags_service).to have_received(:call).with(status)
|
||||
end
|
||||
|
||||
it 'processes tagged objects' do
|
||||
account = Fabricate(:account)
|
||||
collection = Fabricate(:collection)
|
||||
|
||||
status = subject.call(account, text: "test #{ActivityPub::TagManager.instance.uri_for(collection)} #{ActivityPub::TagManager.instance.uri_for(collection)}")
|
||||
|
||||
expect(status.tagged_objects.map(&:object))
|
||||
.to contain_exactly(collection)
|
||||
end
|
||||
|
||||
it 'gets distributed' do
|
||||
allow(DistributionWorker).to receive(:perform_async)
|
||||
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
|
||||
|
||||
19
spec/services/process_links_service_spec.rb
Normal file
19
spec/services/process_links_service_spec.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProcessLinksService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:account) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
context 'when status mentions known collections' do
|
||||
let!(:collection) { Fabricate(:collection) }
|
||||
let(:status) { Fabricate(:status, account: account, text: "Hello check out this collection! #{ActivityPub::TagManager.instance.uri_for(collection)}", visibility: :public) }
|
||||
|
||||
it 'creates a tagged object' do
|
||||
expect { subject.call(status) }
|
||||
.to change { status.tagged_objects.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -161,6 +161,19 @@ RSpec.describe UpdateStatusService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tagged objects in text change' do
|
||||
let!(:old_collection) { Fabricate(:collection) }
|
||||
let!(:new_collection) { Fabricate(:collection) }
|
||||
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:status) { PostStatusService.new.call(account, text: "Check out #{ActivityPub::TagManager.instance.uri_for(old_collection)}") }
|
||||
|
||||
it 'changes tagged objects' do
|
||||
expect { subject.call(status, status.account_id, text: "Check out #{ActivityPub::TagManager.instance.uri_for(new_collection)} #{ActivityPub::TagManager.instance.uri_for(new_collection)}") }
|
||||
.to change { status.reload.tagged_objects.map(&:object) }.from([old_collection]).to([new_collection])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hashtags in text change' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user