Compare commits

..

9 Commits

15 changed files with 459 additions and 173 deletions

View File

@ -141,10 +141,10 @@ class Api::V1::StatusesController < Api::BaseController
end
def set_quoted_status
return unless Mastodon::Feature.outgoing_quotes_enabled?
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
# TODO: handle non-local quote posts when we implement sending `QuoteRequest`
@quoted_status = nil unless @quoted_status.local?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
end

View File

@ -26,6 +26,12 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
quotes: {
'quote' => 'https://w3id.org/fep/044f#quote',
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
'_misskey_quote' => 'https://misskey-hub.net/ns/#_misskey_quote',
'quoteAuthorization' => 'https://w3id.org/fep/044f#quoteAuthorization',
},
interaction_policies: {
'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },

View File

@ -143,6 +143,10 @@ class ActivityPub::Activity
@follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end
def quote_request_from_object
@quote_request_from_object ||= Quote.find_by(quoted_account: @account, activity_uri: object_uri) unless object_uri.nil?
end
def follow_from_object
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end

View File

@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform
return accept_follow_for_relay if relay_follow?
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil?
case @object['type']
when 'Follow'
accept_embedded_follow
when 'QuoteRequest'
accept_embedded_quote_request
end
end
@ -31,6 +34,32 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
end
def accept_embedded_quote_request
quoted_status_uri = value_or_id(@object['object'])
quoting_status_uri = value_or_id(@object['instrument'])
approval_uri = value_or_id(@json['result'])
return if quoted_status_uri.nil? || quoting_status_uri.nil? || approval_uri.nil?
quoting_status = status_from_uri(quoting_status_uri)
return unless quoting_status.local?
quoted_status = status_from_uri(quoted_status_uri)
return unless quoted_status.account == @account && quoting_status.quote.quoted_status == quoted_status
accept_quote!(quoting_status.quote)
end
def accept_quote!(quote)
approval_uri = value_or_id(@json['result'])
return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local?
# TODO: should this go through `ActivityPub::VerifyQuoteService`?
quote.update!(state: :accepted, approval_uri: approval_uri)
DistributionWorker.perform_async(quote.status_id, { 'update' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(quote.status_id)
end
def accept_follow_for_relay
relay.update!(state: :accepted)
end

View File

@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
return reject_follow_for_relay if relay_follow?
return follow_request_from_object.reject! unless follow_request_from_object.nil?
return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil?
return reject_quote!(quote_request_from_object) unless quote_request_from_object.nil?
case @object['type']
when 'Follow'
reject_embedded_follow
when 'QuoteRequest'
reject_embedded_quote
end
end
@ -29,6 +32,28 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
relay.update!(state: :rejected)
end
def reject_embedded_quote
quoted_status_uri = value_or_id(@object['object'])
quoting_status_uri = value_or_id(@object['instrument'])
approval_uri = value_or_id(@json['instrument'])
return if quoted_status_uri.nil? || quoted_uri.nil? || approval_uri.nil?
quoting_status = status_from_uri(quoting_status_uri)
return unless quoting_status.local?
quoted_status = status_from_uri(quoted_status_uri)
return unless quoted_status.account == @account && quoting_status.quote.quoted_status == quoted_status
reject_quote!(quoting_status.quote)
end
def reject_quote!(quote)
return unless quote.quoted_account == @account && quote.status.local?
# TODO: broadcast an update?
quote.reject!
end
def relay
@relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil?
end

View File

@ -12,9 +12,7 @@ module ActivityPub::CaseTransform
when Hash then value.deep_transform_keys! { |key| camel_lower(key) }
when Symbol then camel_lower(value.to_s).to_sym
when String
camel_lower_cache[value] ||= if value.start_with?('_:')
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
camel_lower_cache[value] ||= if value.start_with?('_') || LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
value
else
value.underscore.camelize(:lower)

View File

@ -3,7 +3,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer
include FormattingHelper
context_extensions :atom_uri, :conversation, :sensitive, :voters_count
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes
attributes :id, :type, :summary,
:in_reply_to, :published, :url,
@ -30,6 +30,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
attribute :voters_count, if: :poll_and_voters_count?
attribute :quote, if: :quote?
attribute :quote, key: :_misskey_quote, if: :quote?
attribute :quote, key: :quote_uri, if: :quote?
attribute :quote_authorization, if: :quote_authorization?
def id
ActivityPub::TagManager.instance.uri_for(object)
end
@ -194,6 +199,24 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
object.preloadable_poll&.voters_count
end
def quote?
object.quote&.present?
end
def quote_authorization?
object.quote&.approval_uri.present?
end
def quote
# TODO: handle inlining self-quotes
ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status)
end
def quote_authorization
# TODO: approval of local quotes may work differently, perhaps?
object.quote.approval_uri
end
class MediaAttachmentSerializer < ActivityPub::Serializer
context_extensions :blurhash, :focal_point

View File

@ -97,12 +97,10 @@ class PostStatusService < BaseService
# we only support incoming quotes so far
status.quote = Quote.new(quoted_status: @quoted_status)
status.quote.accept! if StatusPolicy.new(@status.account, @quoted_status).quote? && @quoted_status.local?
# TODO: the following has yet to be implemented:
# - handle approval of local users (requires the interactionPolicy PR)
# - produce a QuoteAuthorization for quotes of local users
# - send a QuoteRequest for quotes of remote users
if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote?
# TODO: produce a QuoteAuthorization
status.quote.accept!
end
end
def safeguard_mentions!(status)
@ -146,6 +144,7 @@ class PostStatusService < BaseService
DistributionWorker.perform_async(@status.id)
ActivityPub::DistributionWorker.perform_async(@status.id)
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
ActivityPub::QuoteRequestWorker.perform_async(@status.quote.id) if @status.quote&.quoted_status.present? && !@status.quote&.quoted_status&.local?
end
def validate_media!

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class ActivityPub::QuoteRequestWorker < ActivityPub::RawDistributionWorker
def perform(quote_id)
@quote = Quote.find(quote_id)
@account = @quote.account
distribute!
rescue ActiveRecord::RecordNotFound
true
end
protected
def inboxes
@inboxes ||= [@quote.quoted_account&.preferred_inbox_url].compact
end
def payload
@payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account))
end
end

View File

@ -3,69 +3,173 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Accept do
let(:sender) { Fabricate(:account) }
let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:recipient) { Fabricate(:account) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
id: 'bar',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
},
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
allow(RemoteAccountRefreshWorker).to receive(:perform_async)
Fabricate(:follow_request, account: recipient, target_account: sender)
subject.perform
context 'with a Follow request' do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
id: 'https://abc-123/456',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
},
}.with_indifferent_access
end
context 'with a regular Follow' do
before do
Fabricate(:follow_request, account: recipient, target_account: sender)
end
it 'creates a follow relationship, removes the follow request, and queues a refresh' do
expect { subject.perform }
.to change { recipient.following?(sender) }.from(false).to(true)
.and change { recipient.requested?(sender) }.from(true).to(false)
expect(RemoteAccountRefreshWorker).to have_enqueued_sidekiq_job(sender.id)
end
end
context 'when given a relay' do
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
it 'marks the relay as accepted' do
expect { subject.perform }
.to change { relay.reload.accepted? }.from(false).to(true)
end
end
end
it 'creates a follow relationship' do
expect(recipient.following?(sender)).to be true
end
context 'with a QuoteRequest' do
let(:status) { Fabricate(:status, account: recipient) }
let(:quoted_status) { Fabricate(:status, account: sender) }
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') }
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
it 'removes the follow request' do
expect(recipient.requested?(sender)).to be false
end
let(:json) do
{
'@context': [
'https://www.w3.org/ns/activitystreams',
{
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
},
],
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
id: 'https://abc-123/456',
type: 'QuoteRequest',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
instrument: ActivityPub::TagManager.instance.uri_for(status),
},
result: approval_uri,
}.with_indifferent_access
end
it 'queues a refresh' do
expect(RemoteAccountRefreshWorker).to have_received(:perform_async).with(sender.id)
end
end
it 'marks the quote as approved and distribute an update' do
expect { subject.perform }
.to change { quote.reload.accepted? }.from(false).to(true)
.and change { quote.reload.approval_uri }.to(approval_uri)
expect(DistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to have_enqueued_sidekiq_job(status.id)
end
context 'when given a relay' do
subject { described_class.new(json, sender) }
context 'when the quoted status is not from the sender of the Accept' do
let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) }
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id)
end
end
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
id: 'https://abc-123/456',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
},
}.with_indifferent_access
end
context 'when the quoting status is from an unrelated user' do
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) }
it 'marks the relay as accepted' do
subject.perform
expect(relay.reload.accepted?).to be true
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id)
end
end
context 'when approval_uri is missing' do
let(:approval_uri) { nil }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id)
end
end
context 'when the QuoteRequest is referenced by its identifier' do
let(:json) do
{
'@context': [
'https://www.w3.org/ns/activitystreams',
{
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
},
],
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: 'https://abc-123/456',
result: approval_uri,
}.with_indifferent_access
end
it 'marks the quote as approved and distribute an update' do
expect { subject.perform }
.to change { quote.reload.accepted? }.from(false).to(true)
.and change { quote.reload.approval_uri }.to(approval_uri)
expect(DistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to have_enqueued_sidekiq_job(status.id)
end
context 'when approval_uri is missing' do
let(:approval_uri) { nil }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id)
end
end
end
end
end
end

View File

@ -5,14 +5,6 @@ require 'rails_helper'
RSpec.describe ActivityPub::Activity::Reject do
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: 'bar',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
}
end
let(:json) do
{
@ -27,124 +19,133 @@ RSpec.describe ActivityPub::Activity::Reject do
describe '#perform' do
subject { described_class.new(json, sender) }
context 'when rejecting a pending follow request by target' do
before do
Fabricate(:follow_request, account: recipient, target_account: sender)
subject.perform
context 'when rejecting a Follow' do
let(:object_json) do
{
id: 'bar',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
}
end
it 'does not create a follow relationship' do
expect(recipient.following?(sender)).to be false
context 'when rejecting a pending follow request by target' do
before do
Fabricate(:follow_request, account: recipient, target_account: sender)
end
it 'removes the follow request without creating a follow relationship' do
expect { subject.perform }
.to change { recipient.requested?(sender) }.from(true).to(false)
.and not_change { recipient.following?(sender) }.from(false)
end
end
it 'removes the follow request' do
expect(recipient.requested?(sender)).to be false
context 'when rejecting a pending follow request by uri' do
before do
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
end
it 'removes the follow request without creating a follow relationship' do
expect { subject.perform }
.to change { recipient.requested?(sender) }.from(true).to(false)
.and not_change { recipient.following?(sender) }.from(false)
end
end
context 'when rejecting a pending follow request by uri only' do
let(:object_json) { 'bar' }
before do
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
end
it 'removes the follow request without creating a follow relationship' do
expect { subject.perform }
.to change { recipient.requested?(sender) }.from(true).to(false)
.and not_change { recipient.following?(sender) }.from(false)
end
end
context 'when rejecting an existing follow relationship by target' do
before do
Fabricate(:follow, account: recipient, target_account: sender)
end
it 'removes the follow relationship without creating a request' do
expect { subject.perform }
.to change { recipient.following?(sender) }.from(true).to(false)
.and not_change { recipient.requested?(sender) }.from(false)
end
end
context 'when rejecting an existing follow relationship by uri' do
before do
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
end
it 'removes the follow relationship without creating a request' do
expect { subject.perform }
.to change { recipient.following?(sender) }.from(true).to(false)
.and not_change { recipient.requested?(sender) }.from(false)
end
end
context 'when rejecting an existing follow relationship by uri only' do
let(:object_json) { 'bar' }
before do
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
end
it 'removes the follow relationship without creating a request' do
expect { subject.perform }
.to change { recipient.following?(sender) }.from(true).to(false)
.and not_change { recipient.requested?(sender) }.from(false)
end
end
end
context 'when rejecting a pending follow request by uri' do
before do
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
subject.perform
end
context 'when given a relay' do
subject { described_class.new(json, sender) }
it 'does not create a follow relationship' do
expect(recipient.following?(sender)).to be false
end
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
it 'removes the follow request' do
expect(recipient.requested?(sender)).to be false
end
end
context 'when rejecting a pending follow request by uri only' do
let(:object_json) { 'bar' }
before do
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
subject.perform
end
it 'does not create a follow relationship' do
expect(recipient.following?(sender)).to be false
end
it 'removes the follow request' do
expect(recipient.requested?(sender)).to be false
end
end
context 'when rejecting an existing follow relationship by target' do
before do
Fabricate(:follow, account: recipient, target_account: sender)
subject.perform
end
it 'removes the follow relationship' do
expect(recipient.following?(sender)).to be false
end
it 'does not create a follow request' do
expect(recipient.requested?(sender)).to be false
end
end
context 'when rejecting an existing follow relationship by uri' do
before do
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
subject.perform
end
it 'removes the follow relationship' do
expect(recipient.following?(sender)).to be false
end
it 'does not create a follow request' do
expect(recipient.requested?(sender)).to be false
end
end
context 'when rejecting an existing follow relationship by uri only' do
let(:object_json) { 'bar' }
before do
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
subject.perform
end
it 'removes the follow relationship' do
expect(recipient.following?(sender)).to be false
end
it 'does not create a follow request' do
expect(recipient.requested?(sender)).to be false
end
end
end
context 'when given a relay' do
subject { described_class.new(json, sender) }
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Reject',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
let(:object_json) do
{
id: 'https://abc-123/456',
type: 'Follow',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(sender),
},
}.with_indifferent_access
}.with_indifferent_access
end
it 'marks the relay as rejected' do
subject.perform
expect(relay.reload.rejected?).to be true
end
end
it 'marks the relay as rejected' do
subject.perform
expect(relay.reload.rejected?).to be true
context 'with a QuoteRequest' do
let(:status) { Fabricate(:status, account: recipient) }
let(:quoted_status) { Fabricate(:status, account: sender) }
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') }
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
let(:object_json) do
{
id: 'https://abc-123/456',
type: 'QuoteRequest',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
instrument: ActivityPub::TagManager.instance.uri_for(status),
}.with_indifferent_access
end
it 'marks the quote as rejected' do
expect { subject.perform }
.to change { quote.reload.rejected? }.from(false).to(true)
end
end
end
end

View File

@ -158,6 +158,27 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'with a self-quote post', feature: :outgoing_quotes do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
{
status: 'Hello world, this is a self-quote',
quoted_status_id: quoted_status.id,
}
end
it 'returns a quote post, as well as rate limit headers', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:quote]).to be_present
expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s
end
end
context 'with a safeguard' do
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob') }

View File

@ -41,4 +41,20 @@ RSpec.describe ActivityPub::NoteSerializer do
.and(not_include(reply_by_other_first.uri)) # Replies from others
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
end
context 'with a quote' do
let(:quoted_status) { Fabricate(:status) }
let(:approval_uri) { 'https://example.com/foo/bar' }
let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, approval_uri: approval_uri) }
it 'has the expected shape' do
expect(subject).to include({
'type' => 'Note',
'quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
'quoteUri' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
'_misskey_quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
'quoteAuthorization' => approval_uri,
})
end
end
end

View File

@ -291,6 +291,14 @@ RSpec.describe PostStatusService do
)
end
it 'correctly requests a quote for remote posts' do
account = Fabricate(:account)
quoted_status = Fabricate(:status, account: Fabricate(:account, domain: 'example.com'))
expect { subject.call(account, text: 'test', quoted_status: quoted_status) }
.to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker)
end
it 'returns existing status when used twice with idempotency key' do
account = Fabricate(:account)
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::QuoteRequestWorker do
subject { described_class.new }
let(:quoted_account) { Fabricate(:account, inbox_url: 'http://example.com', domain: 'example.com') }
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
let(:status) { Fabricate(:status, text: 'foo') }
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'TODO') } # TODO: activity URI
describe '#perform' do
it 'sends the expected QuoteRequest activity' do
subject.perform(quote.id)
expect(ActivityPub::DeliveryWorker)
.to have_enqueued_sidekiq_job(match_object_shape, quote.account_id, 'http://example.com', {})
end
def match_object_shape
match_json_values(
type: 'QuoteRequest',
actor: ActivityPub::TagManager.instance.uri_for(quote.account),
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
instrument: anything # TODO: inline post in request?
)
end
end
end