mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-13 15:58:13 +00:00
Compare commits
9 Commits
6fa52e3517
...
5327c8b10f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5327c8b10f | ||
![]() |
f3b6e3c559 | ||
![]() |
e9ce67cae6 | ||
![]() |
ed86c3c919 | ||
![]() |
10700e703b | ||
![]() |
13a98ac8d6 | ||
![]() |
67d6f4cd9b | ||
![]() |
58dc8d4581 | ||
![]() |
fb33cf8dd5 |
|
@ -9,6 +9,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
before_action :set_statuses, only: [:index]
|
before_action :set_statuses, only: [:index]
|
||||||
before_action :set_status, only: [:show, :context]
|
before_action :set_status, only: [:show, :context]
|
||||||
before_action :set_thread, only: [:create]
|
before_action :set_thread, only: [:create]
|
||||||
|
before_action :set_quoted_status, only: [:create]
|
||||||
before_action :check_statuses_limit, only: [:index]
|
before_action :check_statuses_limit, only: [:index]
|
||||||
|
|
||||||
override_rate_limit_headers :create, family: :statuses
|
override_rate_limit_headers :create, family: :statuses
|
||||||
|
@ -67,6 +68,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
current_user.account,
|
current_user.account,
|
||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
thread: @thread,
|
thread: @thread,
|
||||||
|
quoted_status: @quoted_status,
|
||||||
media_ids: status_params[:media_ids],
|
media_ids: status_params[:media_ids],
|
||||||
sensitive: status_params[:sensitive],
|
sensitive: status_params[:sensitive],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
|
@ -138,6 +140,15 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
||||||
end
|
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?
|
||||||
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
|
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
|
||||||
|
end
|
||||||
|
|
||||||
def check_statuses_limit
|
def check_statuses_limit
|
||||||
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
|
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
|
||||||
end
|
end
|
||||||
|
@ -154,6 +165,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
params.permit(
|
params.permit(
|
||||||
:status,
|
:status,
|
||||||
:in_reply_to_id,
|
:in_reply_to_id,
|
||||||
|
:quoted_status_id,
|
||||||
:sensitive,
|
:sensitive,
|
||||||
:spoiler_text,
|
:spoiler_text,
|
||||||
:visibility,
|
:visibility,
|
||||||
|
|
|
@ -26,6 +26,12 @@ module ContextHelper
|
||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||||
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
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: {
|
interaction_policies: {
|
||||||
'gts' => 'https://gotosocial.org/ns#',
|
'gts' => 'https://gotosocial.org/ns#',
|
||||||
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
||||||
|
|
|
@ -143,6 +143,10 @@ class ActivityPub::Activity
|
||||||
@follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
@follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||||
end
|
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
|
def follow_from_object
|
||||||
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
return accept_follow_for_relay if relay_follow?
|
return accept_follow_for_relay if relay_follow?
|
||||||
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
|
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']
|
case @object['type']
|
||||||
when 'Follow'
|
when 'Follow'
|
||||||
accept_embedded_follow
|
accept_embedded_follow
|
||||||
|
when 'QuoteRequest'
|
||||||
|
accept_embedded_quote_request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,6 +34,32 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||||
RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
|
RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
|
||||||
end
|
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
|
def accept_follow_for_relay
|
||||||
relay.update!(state: :accepted)
|
relay.update!(state: :accepted)
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||||
return reject_follow_for_relay if relay_follow?
|
return reject_follow_for_relay if relay_follow?
|
||||||
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
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 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']
|
case @object['type']
|
||||||
when 'Follow'
|
when 'Follow'
|
||||||
reject_embedded_follow
|
reject_embedded_follow
|
||||||
|
when 'QuoteRequest'
|
||||||
|
reject_embedded_quote
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,6 +32,28 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||||
relay.update!(state: :rejected)
|
relay.update!(state: :rejected)
|
||||||
end
|
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
|
def relay
|
||||||
@relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil?
|
@relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil?
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,9 +12,7 @@ module ActivityPub::CaseTransform
|
||||||
when Hash then value.deep_transform_keys! { |key| camel_lower(key) }
|
when Hash then value.deep_transform_keys! { |key| camel_lower(key) }
|
||||||
when Symbol then camel_lower(value.to_s).to_sym
|
when Symbol then camel_lower(value.to_s).to_sym
|
||||||
when String
|
when String
|
||||||
camel_lower_cache[value] ||= if value.start_with?('_:')
|
camel_lower_cache[value] ||= if value.start_with?('_') || LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
|
||||||
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
|
|
||||||
elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
|
|
||||||
value
|
value
|
||||||
else
|
else
|
||||||
value.underscore.camelize(:lower)
|
value.underscore.camelize(:lower)
|
||||||
|
|
|
@ -19,6 +19,11 @@ class StatusPolicy < ApplicationPolicy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This is about requesting a quote post, not validating it
|
||||||
|
def quote?
|
||||||
|
owned? || active_mention_exists? || quote_approved_by_policy?
|
||||||
|
end
|
||||||
|
|
||||||
def reblog?
|
def reblog?
|
||||||
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
|
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
|
||||||
end
|
end
|
||||||
|
@ -39,6 +44,14 @@ class StatusPolicy < ApplicationPolicy
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def quote_approved_by_policy?
|
||||||
|
flattened_policy = record.quote_approval_policy | (record.quote_approval_policy >> 16)
|
||||||
|
return true if flattened_policy & (Status::QUOTE_APPROVAL_POLICY_FLAGS[:unknown] | Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]) != 0
|
||||||
|
|
||||||
|
# TODO: support `:followed`
|
||||||
|
(flattened_policy & Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] != 0) && following_author?
|
||||||
|
end
|
||||||
|
|
||||||
def requires_mention?
|
def requires_mention?
|
||||||
record.direct_visibility? || record.limited_visibility?
|
record.direct_visibility? || record.limited_visibility?
|
||||||
end
|
end
|
||||||
|
@ -61,6 +74,16 @@ class StatusPolicy < ApplicationPolicy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_mention_exists?
|
||||||
|
return false if current_account.nil?
|
||||||
|
|
||||||
|
if record.active_mentions.loaded?
|
||||||
|
record.active_mentions.any? { |mention| mention.account_id == current_account.id }
|
||||||
|
else
|
||||||
|
record.active_mentions.exists?(account: current_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def author_blocking_domain?
|
def author_blocking_domain?
|
||||||
return false if current_account.nil? || current_account.domain.nil?
|
return false if current_account.nil? || current_account.domain.nil?
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
include FormattingHelper
|
include FormattingHelper
|
||||||
|
|
||||||
context_extensions :atom_uri, :conversation, :sensitive, :voters_count
|
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes
|
||||||
|
|
||||||
attributes :id, :type, :summary,
|
attributes :id, :type, :summary,
|
||||||
:in_reply_to, :published, :url,
|
:in_reply_to, :published, :url,
|
||||||
|
@ -30,6 +30,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
|
|
||||||
attribute :voters_count, if: :poll_and_voters_count?
|
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
|
def id
|
||||||
ActivityPub::TagManager.instance.uri_for(object)
|
ActivityPub::TagManager.instance.uri_for(object)
|
||||||
end
|
end
|
||||||
|
@ -194,6 +199,24 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
object.preloadable_poll&.voters_count
|
object.preloadable_poll&.voters_count
|
||||||
end
|
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
|
class MediaAttachmentSerializer < ActivityPub::Serializer
|
||||||
context_extensions :blurhash, :focal_point
|
context_extensions :blurhash, :focal_point
|
||||||
|
|
||||||
|
|
|
@ -97,12 +97,10 @@ class PostStatusService < BaseService
|
||||||
# we only support incoming quotes so far
|
# we only support incoming quotes so far
|
||||||
|
|
||||||
status.quote = Quote.new(quoted_status: @quoted_status)
|
status.quote = Quote.new(quoted_status: @quoted_status)
|
||||||
status.quote.accept! if @status.account == @quoted_status.account || @quoted_status.active_mentions.exists?(mentions: { account_id: status.account_id })
|
if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote?
|
||||||
|
# TODO: produce a QuoteAuthorization
|
||||||
# TODO: the following has yet to be implemented:
|
status.quote.accept!
|
||||||
# - handle approval of local users (requires the interactionPolicy PR)
|
end
|
||||||
# - produce a QuoteAuthorization for quotes of local users
|
|
||||||
# - send a QuoteRequest for quotes of remote users
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def safeguard_mentions!(status)
|
def safeguard_mentions!(status)
|
||||||
|
@ -146,6 +144,7 @@ class PostStatusService < BaseService
|
||||||
DistributionWorker.perform_async(@status.id)
|
DistributionWorker.perform_async(@status.id)
|
||||||
ActivityPub::DistributionWorker.perform_async(@status.id)
|
ActivityPub::DistributionWorker.perform_async(@status.id)
|
||||||
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
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
|
end
|
||||||
|
|
||||||
def validate_media!
|
def validate_media!
|
||||||
|
|
22
app/workers/activitypub/quote_request_worker.rb
Normal file
22
app/workers/activitypub/quote_request_worker.rb
Normal 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
|
|
@ -3,69 +3,173 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ActivityPub::Activity::Accept do
|
RSpec.describe ActivityPub::Activity::Accept do
|
||||||
let(:sender) { Fabricate(:account) }
|
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||||
let(:recipient) { Fabricate(:account) }
|
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
|
describe '#perform' do
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
before do
|
context 'with a Follow request' do
|
||||||
allow(RemoteAccountRefreshWorker).to receive(:perform_async)
|
let(:json) do
|
||||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
{
|
||||||
subject.perform
|
'@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
|
end
|
||||||
|
|
||||||
it 'creates a follow relationship' do
|
context 'with a QuoteRequest' do
|
||||||
expect(recipient.following?(sender)).to be true
|
let(:status) { Fabricate(:status, account: recipient) }
|
||||||
end
|
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
|
let(:json) do
|
||||||
expect(recipient.requested?(sender)).to be false
|
{
|
||||||
end
|
'@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
|
it 'marks the quote as approved and distribute an update' do
|
||||||
expect(RemoteAccountRefreshWorker).to have_received(:perform_async).with(sender.id)
|
expect { subject.perform }
|
||||||
end
|
.to change { quote.reload.accepted? }.from(false).to(true)
|
||||||
end
|
.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
|
context 'when the quoted status is not from the sender of the Accept' do
|
||||||
subject { described_class.new(json, sender) }
|
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 'when the quoting status is from an unrelated user' do
|
||||||
{
|
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) }
|
||||||
'@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
|
|
||||||
|
|
||||||
it 'marks the relay as accepted' do
|
it 'does not mark the quote as approved and does not distribute an update' do
|
||||||
subject.perform
|
expect { subject.perform }
|
||||||
expect(relay.reload.accepted?).to be true
|
.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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,14 +5,6 @@ require 'rails_helper'
|
||||||
RSpec.describe ActivityPub::Activity::Reject do
|
RSpec.describe ActivityPub::Activity::Reject do
|
||||||
let(:sender) { Fabricate(:account) }
|
let(:sender) { Fabricate(:account) }
|
||||||
let(:recipient) { 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
|
let(:json) do
|
||||||
{
|
{
|
||||||
|
@ -27,124 +19,133 @@ RSpec.describe ActivityPub::Activity::Reject do
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
context 'when rejecting a pending follow request by target' do
|
context 'when rejecting a Follow' do
|
||||||
before do
|
let(:object_json) do
|
||||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
{
|
||||||
subject.perform
|
id: 'bar',
|
||||||
|
type: 'Follow',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create a follow relationship' do
|
context 'when rejecting a pending follow request by target' do
|
||||||
expect(recipient.following?(sender)).to be false
|
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
|
end
|
||||||
|
|
||||||
it 'removes the follow request' do
|
context 'when rejecting a pending follow request by uri' do
|
||||||
expect(recipient.requested?(sender)).to be false
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when rejecting a pending follow request by uri' do
|
context 'when given a relay' do
|
||||||
before do
|
subject { described_class.new(json, sender) }
|
||||||
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
|
|
||||||
subject.perform
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create a follow relationship' do
|
let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') }
|
||||||
expect(recipient.following?(sender)).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes the follow request' do
|
let(:object_json) 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: {
|
|
||||||
id: 'https://abc-123/456',
|
id: 'https://abc-123/456',
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
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
|
end
|
||||||
|
|
||||||
it 'marks the relay as rejected' do
|
context 'with a QuoteRequest' do
|
||||||
subject.perform
|
let(:status) { Fabricate(:status, account: recipient) }
|
||||||
expect(relay.reload.rejected?).to be true
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -86,6 +86,92 @@ RSpec.describe StatusPolicy, type: :model do
|
||||||
end
|
end
|
||||||
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
|
context 'with the permission of reblog?' do
|
||||||
permissions :reblog? do
|
permissions :reblog? do
|
||||||
it 'denies access when private' do
|
it 'denies access when private' do
|
||||||
|
|
|
@ -158,6 +158,27 @@ RSpec.describe '/api/v1/statuses' do
|
||||||
end
|
end
|
||||||
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
|
context 'with a safeguard' do
|
||||||
let!(:alice) { Fabricate(:account, username: 'alice') }
|
let!(:alice) { Fabricate(:account, username: 'alice') }
|
||||||
let!(:bob) { Fabricate(:account, username: 'bob') }
|
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_other_first.uri)) # Replies from others
|
||||||
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
|
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -291,6 +291,14 @@ RSpec.describe PostStatusService do
|
||||||
)
|
)
|
||||||
end
|
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
|
it 'returns existing status when used twice with idempotency key' do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')
|
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
|
Loading…
Reference in New Issue
Block a user