Add support for limited Announce Activity

This commit is contained in:
noellabo 2021-02-12 20:32:55 +09:00
parent c3114f9a5e
commit 5ea43e505b
6 changed files with 155 additions and 102 deletions

View file

@ -6,6 +6,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user! before_action :require_user!
before_action :set_reblog, only: [:create] before_action :set_reblog, only: [:create]
before_action :set_circle, only: [:create]
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
@ -42,7 +43,22 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
not_found not_found
end end
def set_circle
reblog_params[:circle] = begin
if reblog_params[:visibility] == 'mutual'
reblog_params[:visibility] = 'limited'
current_account
elsif reblog_params[:circle_id].blank?
nil
else
current_account.owned_circles.find(reblog_params[:circle_id])
end
end
rescue ActiveRecord::RecordNotFound
render json: { error: I18n.t('statuses.errors.circle_not_found') }, status: 404
end
def reblog_params def reblog_params
params.permit(:visibility) params.permit(:visibility, :circle_id)
end end
end end

View file

@ -153,7 +153,9 @@ class ActivityPub::Activity
return status unless status.nil? return status unless status.nil?
# If the boosted toot is embedded and it is a self-boost, handle it like a Create dereference_object!
# If the boosted toot is embedded and it is a self-boost or dereferenced, handle it like a Create
unless unsupported_object_type? unless unsupported_object_type?
actor_id = value_or_id(first_of_value(@object['attributedTo'])) actor_id = value_or_id(first_of_value(@object['attributedTo']))
@ -165,11 +167,16 @@ class ActivityPub::Activity
fetch_remote_original_status fetch_remote_original_status
end end
def dereferenced?
@dereferenced
end
def dereference_object! def dereference_object!
return unless @object.is_a?(String) return unless @object.is_a?(String)
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account) dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
@dereferenced = !dereferencer.object.nil?
@object = dereferencer.object unless dereferencer.object.nil? @object = dereferencer.object unless dereferencer.object.nil?
end end
@ -204,7 +211,12 @@ class ActivityPub::Activity
def fetch_remote_original_status def fetch_remote_original_status
if object_uri.start_with?('http') if object_uri.start_with?('http')
return if ActivityPub::TagManager.instance.local_uri?(object_uri) return if ActivityPub::TagManager.instance.local_uri?(object_uri)
ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
if dereferenced?
ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, prefetched_body: @object)
else
ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
end
elsif @object['url'].present? elsif @object['url'].present?
::FetchRemoteStatusService.new.call(@object['url']) ::FetchRemoteStatusService.new.call(@object['url'])
end end
@ -242,4 +254,72 @@ class ActivityPub::Activity
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}") Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
nil nil
end end
def audience_to
as_array(@json['to']).map { |x| value_or_id(x) }
end
def audience_cc
as_array(@json['cc']).map { |x| value_or_id(x) }
end
def process_audience
conversation_uri = value_or_id(@object['context'])
(audience_to + audience_cc).uniq.each do |audience|
next if ActivityPub::TagManager.instance.public_collection?(audience) || audience == conversation_uri
# Unlike with tags, there is no point in resolving accounts we don't already
# know here, because silent mentions would only be used for local access
# control anyway
account = account_from_uri(audience)
next if account.nil? || @mentions.any? { |mention| mention.account_id == account.id }
@mentions << Mention.new(account: account, silent: true)
# If there is at least one silent mention, then the status can be considered
# as a limited-audience status, and not strictly a direct message, but only
# if we considered a direct message in the first place
@params[:visibility] = :limited if @params[:visibility] == :direct
end
# If the payload was delivered to a specific inbox, the inbox owner must have
# access to it, unless they already have access to it anyway
return if @options[:delivered_to_account_id].nil? || @mentions.any? { |mention| mention.account_id == @options[:delivered_to_account_id] }
@mentions << Mention.new(account_id: @options[:delivered_to_account_id], silent: true)
@params[:visibility] = :limited if @params[:visibility] == :direct
end
def postprocess_audience_and_deliver
return if @status.mentions.find_by(account_id: @options[:delivered_to_account_id])
delivered_to_account = Account.find(@options[:delivered_to_account_id])
@status.mentions.create(account: delivered_to_account, silent: true)
@status.update(visibility: :limited) if @status.direct_visibility?
return unless delivered_to_account.following?(@account)
FeedInsertWorker.perform_async(@status.id, delivered_to_account.id, :home)
end
def visibility_from_audience
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
:public
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
:unlisted
elsif audience_to.include?(@account.followers_url)
:private
else
:direct
end
end
def audience_includes?(account)
uri = ActivityPub::TagManager.instance.uri_for(account)
audience_to.include?(uri) || audience_cc.include?(uri)
end
end end

View file

@ -5,28 +5,17 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity? return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
lock_or_fail("announce:#{@object['id']}") do lock_or_fail("announce:#{@object['id']}") do
original_status = status_from_object @original_status = status_from_object
return reject_payload! if original_status.nil? || !announceable?(original_status) return reject_payload! if @original_status.nil? || !announceable?(@original_status)
@status = Status.find_by(account: @account, reblog: original_status) @status = Status.find_by(account: @account, reblog: @original_status)
return @status unless @status.nil? if @status.nil?
process_status
@status = Status.create!( elsif @options[:delivered_to_account_id].present?
account: @account, postprocess_audience_and_deliver
reblog: original_status,
uri: @json['id'],
created_at: @json['published'],
override_timestamps: @options[:override_timestamps],
visibility: visibility_from_audience
)
original_status.tags.each do |tag|
tag.use!(@account)
end end
distribute(@status)
end end
@status @status
@ -34,28 +23,47 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
private private
def audience_to def process_status
as_array(@json['to']).map { |x| value_or_id(x) } @mentions = []
@params = {}
process_status_params
process_audience
ApplicationRecord.transaction do
@status = Status.create!(@params)
attach_mentions(@status)
end
@original_status.tags.each do |tag|
tag.use!(@account)
end
distribute(@status)
end end
def audience_cc def process_status_params
as_array(@json['cc']).map { |x| value_or_id(x) } @params = begin
{
account: @account,
reblog: @original_status,
uri: @json['id'],
created_at: @json['published'],
override_timestamps: @options[:override_timestamps],
visibility: visibility_from_audience
}
end
end end
def visibility_from_audience def attach_mentions(status)
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) } @mentions.each do |mention|
:public mention.status = status
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } mention.save
:unlisted
elsif audience_to.include?(@account.followers_url)
:private
else
:direct
end end
end end
def announceable?(status) def announceable?(status)
status.account_id == @account.id || status.distributable? || @account.group? && (status.mentioning?(@account) || status.account.mutual?(@account)) status.account_id == @account.id || (@account.group? && dereferenced?) || status.distributable? || status.account.mutual?(@account)
end end
def related_to_local_activity? def related_to_local_activity?
@ -67,6 +75,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
end end
def reblog_of_local_status? def reblog_of_local_status?
status_from_uri(object_uri)&.account&.local? ActivityPub::TagManager.instance.local_uri?(object_uri) && status_from_uri(object_uri)&.account&.local?
end end
end end

View file

@ -120,53 +120,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
end end
def process_audience
conversation_uri = value_or_id(@object['context'])
(audience_to + audience_cc).uniq.each do |audience|
next if ActivityPub::TagManager.instance.public_collection?(audience) || audience == conversation_uri
# Unlike with tags, there is no point in resolving accounts we don't already
# know here, because silent mentions would only be used for local access
# control anyway
account = account_from_uri(audience)
next if account.nil? || @mentions.any? { |mention| mention.account_id == account.id }
@mentions << Mention.new(account: account, silent: true)
# If there is at least one silent mention, then the status can be considered
# as a limited-audience status, and not strictly a direct message, but only
# if we considered a direct message in the first place
next unless @params[:visibility] == :direct
@params[:visibility] = :limited
end
# If the payload was delivered to a specific inbox, the inbox owner must have
# access to it, unless they already have access to it anyway
return if @options[:delivered_to_account_id].nil? || @mentions.any? { |mention| mention.account_id == @options[:delivered_to_account_id] }
@mentions << Mention.new(account_id: @options[:delivered_to_account_id], silent: true)
return unless @params[:visibility] == :direct
@params[:visibility] = :limited
end
def postprocess_audience_and_deliver
return if @status.mentions.find_by(account_id: @options[:delivered_to_account_id])
delivered_to_account = Account.find(@options[:delivered_to_account_id])
@status.mentions.create(account: delivered_to_account, silent: true)
@status.update(visibility: :limited) if @status.direct_visibility?
return unless delivered_to_account.following?(@account)
FeedInsertWorker.perform_async(@status.id, delivered_to_account.id, :home)
end
def attach_tags(status) def attach_tags(status)
@tags.each do |tag| @tags.each do |tag|
status.tags << tag status.tags << tag
@ -384,23 +337,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
conversation conversation
end end
def visibility_from_audience
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
:public
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
:unlisted
elsif audience_to.include?(@account.followers_url)
:private
else
:direct
end
end
def audience_includes?(account)
uri = ActivityPub::TagManager.instance.uri_for(account)
audience_to.include?(uri) || audience_cc.include?(uri)
end
def replied_to_status def replied_to_status
return @replied_to_status if defined?(@replied_to_status) return @replied_to_status if defined?(@replied_to_status)

View file

@ -89,7 +89,7 @@ class Status < ApplicationRecord
validates_with StatusLengthValidator validates_with StatusLengthValidator
validates_with DisallowedHashtagsValidator validates_with DisallowedHashtagsValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :reblog, uniqueness: { scope: :account }, if: :reblog?
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? validates :visibility, exclusion: { in: %w(direct) }, if: :reblog?
validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote? validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
validates_with ExpiresValidator, on: :create, if: :local? validates_with ExpiresValidator, on: :create, if: :local?

View file

@ -28,8 +28,21 @@ class ReblogService < BaseService
end end
end end
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit]) circle = begin
if visibility == 'mutual'
visibility = 'limited'
account
else
options[:circle]
end
end
ApplicationRecord.transaction do
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit])
reblog.capability_tokens.create! if reblog.limited_visibility?
end
ProcessMentionsService.new.call(reblog, circle)
DistributionWorker.perform_async(reblog.id) DistributionWorker.perform_async(reblog.id)
ActivityPub::DistributionWorker.perform_async(reblog.id) ActivityPub::DistributionWorker.perform_async(reblog.id)