Add experimental basic quote post authoring (#35355)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Accept do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
describe '#perform' do
|
||||
@@ -48,5 +48,128 @@ RSpec.describe ActivityPub::Activity::Accept do
|
||||
end
|
||||
end
|
||||
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) }
|
||||
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
|
||||
|
||||
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: quote.activity_uri,
|
||||
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 '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, { 'updated_at' => be_a(String) })
|
||||
end
|
||||
|
||||
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')) }
|
||||
|
||||
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, anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the quoting status is from an unrelated user' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) }
|
||||
|
||||
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, anything)
|
||||
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, anything)
|
||||
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: quote.activity_uri,
|
||||
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, { 'updated_at' => be_a(String) })
|
||||
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, anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -125,5 +125,27 @@ RSpec.describe ActivityPub::Activity::Reject do
|
||||
expect(relay.reload.rejected?).to be true
|
||||
end
|
||||
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" }
|
||||
|
||||
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
|
||||
|
||||
@@ -28,6 +28,18 @@ RSpec.describe StatusCacheHydrator do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling a status with a quote policy', feature: :outgoing_quotes do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||
|
||||
before do
|
||||
account.follow!(status.account)
|
||||
end
|
||||
|
||||
it 'renders the same attributes as a full render' do
|
||||
expect(subject).to eql(compare_to_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling a filtered status' do
|
||||
let(:status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Status::InteractionPolicyConcern do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: (0b0101 << 16) | 0b0010) }
|
||||
|
||||
describe '#quote_policy_as_keys' do
|
||||
it 'returns the expected values' do
|
||||
expect(status.quote_policy_as_keys(:automatic)).to eq ['unknown', 'followers']
|
||||
expect(status.quote_policy_as_keys(:manual)).to eq ['public']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#quote_policy_for_account' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
context 'when the account is not following the user' do
|
||||
it 'returns :manual because of the public entry in the manual policy' do
|
||||
expect(status.quote_policy_for_account(account)).to eq :manual
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account is following the user' do
|
||||
before do
|
||||
account.follow!(status.account)
|
||||
end
|
||||
|
||||
it 'returns :automatic because of the followers entry in the automatic policy' do
|
||||
expect(status.quote_policy_for_account(account)).to eq :automatic
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account falls into the unknown bucket' do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: (0b0001 << 16) | 0b0100) }
|
||||
|
||||
it 'returns :automatic because of the followers entry in the automatic policy' do
|
||||
expect(status.quote_policy_for_account(account)).to eq :unknown
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -86,6 +86,92 @@ RSpec.describe StatusPolicy, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the permission of quote?' do
|
||||
permissions :quote? do
|
||||
it 'grants access when direct and account is viewer' do
|
||||
status.visibility = :direct
|
||||
|
||||
expect(subject).to permit(status.account, status)
|
||||
end
|
||||
|
||||
it 'grants access when direct and viewer is mentioned' do
|
||||
status.visibility = :direct
|
||||
status.mentions = [Fabricate(:mention, account: alice)]
|
||||
|
||||
expect(subject).to permit(alice, status)
|
||||
end
|
||||
|
||||
it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
|
||||
status.visibility = :direct
|
||||
status.mentions = [Fabricate(:mention, account: bob)]
|
||||
status.active_mentions.load
|
||||
|
||||
expect(subject).to permit(bob, status)
|
||||
end
|
||||
|
||||
it 'denies access when direct and viewer is not mentioned' do
|
||||
viewer = Fabricate(:account)
|
||||
status.visibility = :direct
|
||||
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'denies access when private and viewer is not mentioned' do
|
||||
viewer = Fabricate(:account)
|
||||
status.visibility = :private
|
||||
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'grants access when private and viewer is mentioned' do
|
||||
status.visibility = :private
|
||||
status.mentions = [Fabricate(:mention, account: bob)]
|
||||
|
||||
expect(subject).to permit(bob, status)
|
||||
end
|
||||
|
||||
it 'denies access when private and non-viewer is mentioned' do
|
||||
viewer = Fabricate(:account)
|
||||
status.visibility = :private
|
||||
status.mentions = [Fabricate(:mention, account: bob)]
|
||||
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'denies access when private and account is following viewer' do
|
||||
follow = Fabricate(:follow)
|
||||
status.visibility = :private
|
||||
status.account = follow.target_account
|
||||
|
||||
expect(subject).to_not permit(follow.account, status)
|
||||
end
|
||||
|
||||
it 'denies access when public but policy does not allow anyone' do
|
||||
viewer = Fabricate(:account)
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'grants access when public and policy allows everyone' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]
|
||||
viewer = Fabricate(:account)
|
||||
expect(subject).to permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'denies access when public and policy allows followers but viewer is not one' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
|
||||
viewer = Fabricate(:account)
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'grants access when public and policy allows followers and viewer is one' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
|
||||
viewer = Fabricate(:account)
|
||||
viewer.follow!(status.account)
|
||||
expect(subject).to permit(viewer, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the permission of reblog?' do
|
||||
permissions :reblog? do
|
||||
it 'denies access when private' do
|
||||
|
||||
@@ -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') }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
30
spec/workers/activitypub/quote_request_worker_spec.rb
Normal file
30
spec/workers/activitypub/quote_request_worker_spec.rb
Normal 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
|
||||
@@ -9,36 +9,64 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
|
||||
let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
follower.follow!(status.account)
|
||||
|
||||
status.snapshot!
|
||||
status.text = 'bar'
|
||||
status.edited_at = Time.now.utc
|
||||
status.snapshot!
|
||||
status.save!
|
||||
end
|
||||
|
||||
context 'with public status' do
|
||||
context 'with an explicitly edited status' do
|
||||
before do
|
||||
status.update(visibility: :public)
|
||||
follower.follow!(status.account)
|
||||
|
||||
status.snapshot!
|
||||
status.text = 'bar'
|
||||
status.edited_at = Time.now.utc
|
||||
status.snapshot!
|
||||
status.save!
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
|
||||
subject.perform(status.id)
|
||||
context 'with public status' do
|
||||
before do
|
||||
status.update(visibility: :public)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect { subject.perform(status.id) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private status' do
|
||||
before do
|
||||
status.update(visibility: :private)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect { subject.perform(status.id) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private status' do
|
||||
context 'with an implicitly edited status' do
|
||||
before do
|
||||
status.update(visibility: :private)
|
||||
follower.follow!(status.account)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
|
||||
subject.perform(status.id)
|
||||
context 'with public status' do
|
||||
before do
|
||||
status.update(visibility: :public)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private status' do
|
||||
before do
|
||||
status.update(visibility: :private)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user