Add initial support for ingesting and verifying remote quote posts (#34370)

This commit is contained in:
Claire 2025-04-17 09:45:23 +02:00 committed by GitHub
parent a324edabdf
commit df2611a10f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1643 additions and 22 deletions

View file

@ -7,7 +7,15 @@ RSpec.describe ActivityPub::Activity::Create do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
'@context': [
'https://www.w3.org/ns/activitystreams',
{
quote: {
'@id': 'https://w3id.org/fep/044f#quote',
'@type': '@id',
},
},
],
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
type: 'Create',
actor: ActivityPub::TagManager.instance.uri_for(sender),
@ -879,6 +887,115 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with an unverifiable quote of a known post', feature: :inbound_quotes do
let(:quoted_status) { Fabricate(:status) }
let(:object_json) do
build_object(
type: 'Note',
content: 'woah what she said is amazing',
quote: ActivityPub::TagManager.instance.uri_for(quoted_status)
)
end
it 'creates a status with an unverified quote' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.quote).to_not be_nil
expect(status.quote).to have_attributes(
state: 'pending',
approval_uri: nil
)
end
end
context 'with an unverifiable unknown post', feature: :inbound_quotes do
let(:unknown_post_uri) { 'https://unavailable.example.com/unavailable-post' }
let(:object_json) do
build_object(
type: 'Note',
content: 'woah what she said is amazing',
quote: unknown_post_uri
)
end
before do
stub_request(:get, unknown_post_uri).to_return(status: 404)
end
it 'creates a status with an unverified quote' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.quote).to_not be_nil
expect(status.quote).to have_attributes(
state: 'pending',
approval_uri: nil
)
end
end
context 'with a verifiable quote of a known post', feature: :inbound_quotes do
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
let(:approval_uri) { 'https://quoted.example.com/quote-approval' }
let(:object_json) do
build_object(
type: 'Note',
content: 'woah what she said is amazing',
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
quoteAuthorization: approval_uri
)
end
before do
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
'@context': [
'https://www.w3.org/ns/activitystreams',
{
toot: 'http://joinmastodon.org/ns#',
QuoteAuthorization: 'toot:QuoteAuthorization',
gts: 'https://gotosocial.org/ns#',
interactionPolicy: {
'@id': 'gts:interactionPolicy',
'@type': '@id',
},
interactingObject: {
'@id': 'gts:interactingObject',
'@type': '@id',
},
interactionTarget: {
'@id': 'gts:interactionTarget',
'@type': '@id',
},
},
],
type: 'QuoteAuthorization',
id: approval_uri,
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
interactingObject: object_json[:id],
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
}))
end
it 'creates a status with a verified quote' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.quote).to_not be_nil
expect(status.quote).to have_attributes(
state: 'accepted',
approval_uri: approval_uri
)
end
end
context 'when a vote to a local poll' do
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
let!(:local_status) { Fabricate(:status, poll: poll) }

View file

@ -77,4 +77,61 @@ RSpec.describe ActivityPub::Activity::Delete do
end
end
end
context 'when the deleted object is an account' do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Delete',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: ActivityPub::TagManager.instance.uri_for(sender),
signature: 'foo',
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
let(:service) { instance_double(DeleteAccountService, call: true) }
before do
allow(DeleteAccountService).to receive(:new).and_return(service)
end
it 'calls the account deletion service' do
subject.perform
expect(service)
.to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true })
end
end
end
context 'when the deleted object is a quote authorization' do
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
let(:status) { Fabricate(:status, account: quoter) }
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Delete',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: quote.approval_uri,
signature: 'foo',
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
it 'revokes the authorization' do
expect { subject.perform }
.to change { quote.reload.state }.to('revoked')
end
end
end
end

View file

@ -40,10 +40,119 @@ RSpec.describe StatusCacheHydrator do
end
end
context 'when handling an unapproved quote' do
let(:quoted_status) { Fabricate(:status) }
before do
Fabricate(:quote, status: status, quoted_status: quoted_status, state: :pending)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:quote]).to_not be_nil
end
end
context 'when handling an approved quote' do
let(:quoted_status) { Fabricate(:status) }
before do
Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:quote]).to_not be_nil
end
context 'when the quoted post has been favourited' do
before do
FavouriteService.new.call(account, quoted_status)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:quote]).to_not be_nil
end
end
context 'when the quoted post has been reblogged' do
before do
ReblogService.new.call(account, quoted_status)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:quote]).to_not be_nil
end
end
context 'when the quoted post matches account filters' do
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
before do
account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }])
end
it 'renders the same attributes as a full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:quote]).to_not be_nil
end
end
end
context 'when handling a reblog' do
let(:reblog) { Fabricate(:status) }
let(:status) { Fabricate(:status, reblog: reblog) }
context 'when the reblog has an approved quote' do
let(:quoted_status) { Fabricate(:status) }
before do
Fabricate(:quote, status: reblog, quoted_status: quoted_status, state: :accepted)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:reblog][:quote]).to_not be_nil
end
context 'when the quoted post has been favourited' do
before do
FavouriteService.new.call(account, quoted_status)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:reblog][:quote]).to_not be_nil
end
end
context 'when the quoted post has been reblogged' do
before do
ReblogService.new.call(account, quoted_status)
end
it 'renders the same attributes as full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:reblog][:quote]).to_not be_nil
end
end
context 'when the quoted post matches account filters' do
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
before do
account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }])
end
it 'renders the same attributes as a full render' do
expect(subject).to eql(compare_to_hash)
expect(subject[:reblog][:quote]).to_not be_nil
end
end
end
context 'when it has been favourited' do
before do
FavouriteService.new.call(account, reblog)