mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-05 17:31:12 +00:00
Add support for local quote stamps (#35626)
Some checks failed
Bundler Audit / security (push) Has been cancelled
Check i18n / check-i18n (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / ImageMagick tests (.ruby-version) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.2) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Some checks failed
Bundler Audit / security (push) Has been cancelled
Check i18n / check-i18n (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
Haml Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / ImageMagick tests (.ruby-version) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.2) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
This commit is contained in:
parent
483da67204
commit
591df1f205
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
||||
include Authorization
|
||||
|
||||
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||
|
||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||
before_action :set_quote_authorization
|
||||
|
||||
def show
|
||||
expires_in 0, public: @quote.status.distributable? && public_fetch_mode?
|
||||
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pundit_user
|
||||
signed_request_account
|
||||
end
|
||||
|
||||
def set_quote_authorization
|
||||
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
|
||||
authorize @quote.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
|
@ -39,6 +39,12 @@ module ContextHelper
|
|||
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
|
||||
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
|
||||
},
|
||||
quote_authorizations: {
|
||||
'gts' => 'https://gotosocial.org/ns#',
|
||||
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
|
||||
'interactingObject' => { '@id' => 'gts:interactingObject' },
|
||||
'interactionTarget' => { '@id' => 'gts:interactionTarget' },
|
||||
},
|
||||
}.freeze
|
||||
|
||||
def full_context
|
||||
|
|
|
@ -230,7 +230,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
return if @quote_uri.blank?
|
||||
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
|
||||
@quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?)
|
||||
end
|
||||
|
||||
|
|
|
@ -51,6 +51,13 @@ class ActivityPub::TagManager
|
|||
end
|
||||
end
|
||||
|
||||
def approval_uri_for(quote, check_approval: true)
|
||||
return quote.approval_uri unless quote.quoted_account&.local?
|
||||
return if check_approval && !quote.accepted?
|
||||
|
||||
account_quote_authorization_url(quote.quoted_account, quote)
|
||||
end
|
||||
|
||||
def key_uri_for(target)
|
||||
[uri_for(target), '#main-key'].join
|
||||
end
|
||||
|
|
|
@ -37,6 +37,7 @@ class Quote < ApplicationRecord
|
|||
before_validation :set_accounts
|
||||
before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? }
|
||||
validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? }
|
||||
validates :approval_uri, absence: true, if: -> { quoted_account&.local? }
|
||||
validate :validate_visibility
|
||||
|
||||
def accept!
|
||||
|
|
|
@ -7,11 +7,11 @@ class ActivityPub::DeleteQuoteAuthorizationSerializer < ActivityPub::Serializer
|
|||
attribute :virtual_object, key: :object
|
||||
|
||||
def id
|
||||
[object.approval_uri, '#delete'].join
|
||||
[ActivityPub::TagManager.instance.approval_uri_for(object, check_approval: false), '#delete'].join
|
||||
end
|
||||
|
||||
def virtual_object
|
||||
object.approval_uri
|
||||
ActivityPub::TagManager.instance.approval_uri_for(object, check_approval: false)
|
||||
end
|
||||
|
||||
def type
|
||||
|
|
|
@ -204,7 +204,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
end
|
||||
|
||||
def quote_authorization?
|
||||
object.quote&.approval_uri.present?
|
||||
object.quote.present? && ActivityPub::TagManager.instance.approval_uri_for(object.quote).present?
|
||||
end
|
||||
|
||||
def quote
|
||||
|
@ -213,8 +213,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
end
|
||||
|
||||
def quote_authorization
|
||||
# TODO: approval of local quotes may work differently, perhaps?
|
||||
object.quote.approval_uri
|
||||
ActivityPub::TagManager.instance.approval_uri_for(object.quote)
|
||||
end
|
||||
|
||||
class MediaAttachmentSerializer < ActivityPub::Serializer
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::QuoteAuthorizationSerializer < ActivityPub::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
context_extensions :quote_authorizations
|
||||
|
||||
attributes :id, :type, :attributed_to, :interacting_object, :interaction_target
|
||||
|
||||
def id
|
||||
ActivityPub::TagManager.instance.approval_uri_for(object)
|
||||
end
|
||||
|
||||
def type
|
||||
'QuoteAuthorization'
|
||||
end
|
||||
|
||||
def attributed_to
|
||||
ActivityPub::TagManager.instance.uri_for(object.quoted_account)
|
||||
end
|
||||
|
||||
def interaction_target
|
||||
ActivityPub::TagManager.instance.uri_for(object.quoted_status)
|
||||
end
|
||||
|
||||
def interacting_object
|
||||
ActivityPub::TagManager.instance.uri_for(object.status)
|
||||
end
|
||||
end
|
|
@ -278,10 +278,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
return unless quote_uri.present? && @status.quote.present?
|
||||
|
||||
quote = @status.quote
|
||||
return if quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(quote.quoted_status) != quote_uri
|
||||
return if quote.quoted_status.present? && (ActivityPub::TagManager.instance.uri_for(quote.quoted_status) != quote_uri || quote.quoted_status.local?)
|
||||
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
|
||||
|
||||
quote.update(approval_uri: approval_uri, state: :pending, legacy: @status_parser.legacy_quote?) if quote.approval_uri != @status_parser.quote_approval_uri
|
||||
|
||||
|
@ -293,7 +293,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
|
||||
if quote_uri.present?
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
|
||||
|
||||
if @status.quote.present?
|
||||
# If the quoted post has changed, discard the old object and create a new one
|
||||
|
|
|
@ -13,6 +13,7 @@ class ActivityPub::VerifyQuoteService < BaseService
|
|||
@fetching_error = nil
|
||||
|
||||
fetch_quoted_post_if_needed!(fetchable_quoted_uri, prefetched_body: prefetched_quoted_object)
|
||||
return handle_local_quote! if quote.quoted_account&.local?
|
||||
return if fast_track_approval! || quote.approval_uri.blank?
|
||||
|
||||
@json = fetch_approval_object(quote.approval_uri, prefetched_body: prefetched_approval)
|
||||
|
@ -34,6 +35,15 @@ class ActivityPub::VerifyQuoteService < BaseService
|
|||
|
||||
private
|
||||
|
||||
def handle_local_quote!
|
||||
@quote.update!(approval_uri: nil)
|
||||
if StatusPolicy.new(@quote.account, @quote.quoted_status).quote?
|
||||
@quote.accept!
|
||||
else
|
||||
@quote.reject!
|
||||
end
|
||||
end
|
||||
|
||||
# FEP-044f defines rules that don't require the approval flow
|
||||
def fast_track_approval!
|
||||
return false if @quote.quoted_status_id.blank?
|
||||
|
|
|
@ -115,6 +115,7 @@ Rails.application.routes.draw do
|
|||
resource :inbox, only: [:create]
|
||||
resources :collections, only: [:show]
|
||||
resource :followers_synchronization, only: [:show]
|
||||
resources :quote_authorizations, only: [:show]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -888,7 +888,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
|
||||
context 'with an unverifiable quote of a known post' do
|
||||
let(:quoted_status) { Fabricate(:status) }
|
||||
let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) }
|
||||
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
|
|
49
spec/requests/activitypub/quote_authorizations_spec.rb
Normal file
49
spec/requests/activitypub/quote_authorizations_spec.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'ActivityPub QuoteAuthorization endpoint' do
|
||||
let(:account) { Fabricate(:account, domain: nil) }
|
||||
let(:status) { Fabricate :status, account: account }
|
||||
let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) }
|
||||
|
||||
before { Fabricate :favourite, status: status }
|
||||
|
||||
describe 'GET /accounts/:account_username/quote_authorizations/:quote_id' do
|
||||
context 'with an accepted quote' do
|
||||
it 'returns http success and activity json' do
|
||||
get account_quote_authorization_url(quote.quoted_account, quote)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.media_type)
|
||||
.to eq 'application/activity+json'
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include(type: 'QuoteAuthorization')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an incorrect quote authorization URL' do
|
||||
it 'returns http not found' do
|
||||
get account_quote_authorization_url(quote.account, quote)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a rejected quote' do
|
||||
before do
|
||||
quote.reject!
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get account_quote_authorization_url(quote.quoted_account, quote)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,13 +7,13 @@ RSpec.describe ActivityPub::DeleteQuoteAuthorizationSerializer do
|
|||
|
||||
describe 'serializing an object' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted, approval_uri: "https://#{Rails.configuration.x.web_domain}/approvals/1234") }
|
||||
let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) }
|
||||
|
||||
it 'returns expected attributes' do
|
||||
expect(subject.deep_symbolize_keys)
|
||||
.to include(
|
||||
actor: eq(ActivityPub::TagManager.instance.uri_for(status.account)),
|
||||
object: quote.approval_uri,
|
||||
object: ActivityPub::TagManager.instance.approval_uri_for(quote, check_approval: false),
|
||||
type: 'Delete'
|
||||
)
|
||||
end
|
||||
|
|
|
@ -44,8 +44,7 @@ RSpec.describe ActivityPub::NoteSerializer do
|
|||
|
||||
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) }
|
||||
let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, state: :accepted) }
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
|
@ -53,7 +52,7 @@ RSpec.describe ActivityPub::NoteSerializer do
|
|||
'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,
|
||||
'quoteAuthorization' => ActivityPub::TagManager.instance.approval_uri_for(quote),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::QuoteAuthorizationSerializer do
|
||||
subject { serialized_record_json(quote, described_class, adapter: ActivityPub::Adapter) }
|
||||
|
||||
describe 'serializing an object' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) }
|
||||
|
||||
it 'returns expected attributes' do
|
||||
expect(subject.deep_symbolize_keys)
|
||||
.to include(
|
||||
attributedTo: eq(ActivityPub::TagManager.instance.uri_for(status.account)),
|
||||
interactionTarget: ActivityPub::TagManager.instance.uri_for(status),
|
||||
interactingObject: ActivityPub::TagManager.instance.uri_for(quote.status),
|
||||
type: 'QuoteAuthorization'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -564,6 +564,80 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when an approved quote of a local post gets updated through an explicit update' do
|
||||
let(:quoted_account) { Fabricate(:account) }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
|
||||
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) }
|
||||
let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quote',
|
||||
'@type': '@id',
|
||||
},
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||
'@type': '@id',
|
||||
},
|
||||
],
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
summary: 'Show more',
|
||||
content: 'Hello universe',
|
||||
updated: '2021-09-08T22:39:25Z',
|
||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
quoteAuthorization: approval_uri,
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the quote post without changing the quote status' do
|
||||
expect { subject.call(status, json, json) }
|
||||
.to not_change(quote, :approval_uri)
|
||||
.and not_change(quote, :state).from('accepted')
|
||||
.and change(status, :text).from('Hello world').to('Hello universe')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an unapproved quote of a local post gets updated through an explicit update and claims approval' do
|
||||
let(:quoted_account) { Fabricate(:account) }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: 0) }
|
||||
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :rejected) }
|
||||
let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quote',
|
||||
'@type': '@id',
|
||||
},
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||
'@type': '@id',
|
||||
},
|
||||
],
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
summary: 'Show more',
|
||||
content: 'Hello universe',
|
||||
updated: '2021-09-08T22:39:25Z',
|
||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
quoteAuthorization: approval_uri,
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the quote post without changing the quote status' do
|
||||
expect { subject.call(status, json, json) }
|
||||
.to not_change(quote, :approval_uri)
|
||||
.and not_change(quote, :state).from('rejected')
|
||||
.and change(status, :text).from('Hello world').to('Hello universe')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has an existing verified quote and removes an approval link through an explicit update' do
|
||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe RevokeQuoteService do
|
|||
|
||||
let(:status) { Fabricate(:status, account: alice) }
|
||||
|
||||
let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted, approval_uri: "https://#{Rails.configuration.x.web_domain}/approvals/1234") }
|
||||
let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) }
|
||||
|
||||
before do
|
||||
hank.follow!(alice)
|
||||
|
|
Loading…
Reference in New Issue
Block a user