Add conversation-based forwarding for limited visibility statuses through bearcaps

This commit is contained in:
Eugen Rochko 2020-08-26 03:16:47 +02:00
parent 52157fdcba
commit 7cd4ed7d42
26 changed files with 430 additions and 78 deletions

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class ActivityPub::ContextsController < ActivityPub::BaseController
before_action :set_conversation
def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: @conversation, serializer: ActivityPub::ContextSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def set_conversation
@conversation = Conversation.local.find(params[:id])
end
end

View file

@ -25,7 +25,7 @@ module CacheConcern
end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
response.headers['Vary'] = public_fetch_mode? ? 'Accept, Authorization' : 'Accept, Signature, Authorization'
end
def cache_collection(raw, klass)

View file

@ -66,7 +66,12 @@ class StatusesController < ApplicationController
def set_status
@status = @account.statuses.find(params[:id])
if request.authorization.present? && request.authorization.match(/^Bearer /i)
raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, ''))
else
authorize @status, :show?
end
rescue Mastodon::NotPermittedError
not_found
end

View file

@ -49,13 +49,12 @@ module JsonLdHelper
!uri.start_with?('http://', 'https://')
end
def same_origin?(url_a, url_b)
Addressable::URI.parse(url_a).host.casecmp(Addressable::URI.parse(url_b).host).zero?
end
def invalid_origin?(url)
return true if unsupported_uri_scheme?(url)
needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@account.uri).host
!haystack.casecmp(needle).zero?
unsupported_uri_scheme?(url) || !same_origin?(url, @account.uri)
end
def canonicalize(json)

View file

@ -90,6 +90,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
fetch_replies(@status)
check_for_spam
distribute(@status)
forward_for_conversation
forward_for_reply
end
@ -114,7 +115,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
sensitive: @object['sensitive'] || false,
visibility: visibility_from_audience,
thread: replied_to_status,
conversation: conversation_from_uri(@object['conversation']),
conversation: conversation_from_context,
media_attachment_ids: process_attachments.take(4).map(&:id),
poll: process_poll,
}
@ -122,8 +123,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_audience
conversation_uri = value_or_id(@object['context'])
(audience_to + audience_cc).uniq.each do |audience|
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
next if audience == ActivityPub::TagManager::COLLECTIONS[:public] || 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
@ -340,16 +343,46 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
end
def conversation_from_uri(uri)
return nil if uri.nil?
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
def conversation_from_context
atom_uri = @object['conversation']
conversation = begin
if atom_uri.present? && OStatus::TagManager.instance.local_id?(atom_uri)
Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(atom_uri, 'Conversation'))
elsif atom_uri.present? && @object['context'].present?
Conversation.find_by(uri: atom_uri)
elsif atom_uri.present?
begin
Conversation.find_or_create_by!(uri: uri)
Conversation.find_or_create_by!(uri: atom_uri)
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
retry
end
end
end
return conversation if @object['context'].nil?
uri = value_or_id(@object['context'])
conversation ||= ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation)
return conversation if (conversation.present? && conversation.uri == uri) || !uri.start_with?('https://')
conversation_json = begin
if @object['context'].is_a?(Hash) && !invalid_origin?(uri)
@object['context']
else
fetch_resource(uri, true)
end
end
return conversation if conversation_json.blank?
conversation ||= Conversation.new
conversation.uri = uri
conversation.inbox_url = conversation_json['inbox']
conversation.save! if conversation.changed?
conversation
end
def visibility_from_audience
if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public])
@ -492,6 +525,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
SpamCheck.perform(@status)
end
def forward_for_conversation
return unless audience_to.include?(value_or_id(@object['context'])) && @json['signature'].present? && @status.conversation.local?
ActivityPub::ForwardDistributionWorker.perform_async(@status.conversation_id, Oj.dump(@json))
end
def forward_for_reply
return unless @status.distributable? && @json['signature'].present? && reply_to_local?

View file

@ -21,10 +21,13 @@ class ActivityPub::TagManager
when :person
target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
if target.reblog?
activity_account_status_url(target.account, target)
else
short_account_status_url(target.account, target)
end
end
end
def uri_for(target)
return target.uri if target.respond_to?(:local?) && !target.local?
@ -33,10 +36,15 @@ class ActivityPub::TagManager
when :person
target.instance_actor? ? instance_actor_url : account_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
if target.reblog?
activity_account_status_url(target.account, target)
else
account_status_url(target.account, target)
end
when :emoji
emoji_url(target)
when :conversation
context_url(target)
end
end
@ -66,7 +74,9 @@ class ActivityPub::TagManager
[COLLECTIONS[:public]]
when 'unlisted', 'private'
[account_followers_url(status.account)]
when 'direct', 'limited'
when 'limited'
status.conversation_id.present? ? [uri_for(status.conversation)] : []
when 'direct'
if status.account.silenced?
# Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id)
@ -104,7 +114,7 @@ class ActivityPub::TagManager
cc << COLLECTIONS[:public]
end
unless status.direct_visibility? || status.limited_visibility?
unless status.direct_visibility?
if status.account.silenced?
# Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id)

View file

@ -7,14 +7,40 @@
# uri :string
# created_at :datetime not null
# updated_at :datetime not null
# parent_status_id :bigint(8)
# parent_account_id :bigint(8)
# inbox_url :string
#
class Conversation < ApplicationRecord
validates :uri, uniqueness: true, if: :uri?
has_many :statuses
belongs_to :parent_status, class_name: 'Status', optional: true, inverse_of: :conversation
belongs_to :parent_account, class_name: 'Account', optional: true
has_many :statuses, inverse_of: :conversation
scope :local, -> { where(uri: nil) }
before_validation :set_parent_account, on: :create
after_create :set_conversation_on_parent_status
def local?
uri.nil?
end
def object_type
:conversation
end
private
def set_parent_account
self.parent_account = parent_status.account if parent_status.present?
end
def set_conversation_on_parent_status
parent_status.update_column(:conversation_id, id) if parent_status.present?
end
end

View file

@ -50,9 +50,11 @@ class Status < ApplicationRecord
belongs_to :account, inverse_of: :statuses
belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
belongs_to :conversation, optional: true
belongs_to :conversation, optional: true, inverse_of: :statuses
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true
has_one :owned_conversation, class_name: 'Conversation', foreign_key: 'parent_status_id', inverse_of: :parent_status
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
@ -63,6 +65,7 @@ class Status < ApplicationRecord
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify
has_many :capability_tokens, class_name: 'StatusCapabilityToken', inverse_of: :status, dependent: :destroy
has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards
@ -205,7 +208,9 @@ class Status < ApplicationRecord
public_visibility? || unlisted_visibility?
end
alias sign? distributable?
def sign?
distributable? || limited_visibility?
end
def with_media?
media_attachments.any?
@ -264,11 +269,11 @@ class Status < ApplicationRecord
around_create Mastodon::Snowflake::Callbacks
before_validation :prepare_contents, if: :local?
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
before_validation :set_local
before_validation :prepare_contents, on: :create, if: :local?
before_validation :set_reblog, on: :create
before_validation :set_visibility, on: :create
before_validation :set_conversation, on: :create
before_validation :set_local, on: :create
after_create :set_poll_id
@ -464,7 +469,7 @@ class Status < ApplicationRecord
self.in_reply_to_account_id = carried_over_reply_to_account_id
self.conversation_id = thread.conversation_id if conversation_id.nil?
elsif conversation_id.nil?
self.conversation = Conversation.new
build_owned_conversation
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: status_capability_tokens
#
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# token :string
# created_at :datetime not null
# updated_at :datetime not null
#
class StatusCapabilityToken < ApplicationRecord
belongs_to :status
validates :token, presence: true
before_validation :generate_token, on: :create
private
def generate_token
self.token = Doorkeeper::OAuth::Helpers::UniqueToken.generate
end
end

View file

@ -20,6 +20,8 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
else
ActivityPub::TagManager.instance.uri_for(status.proper)
end
elsif status.limited_visibility?
"bear:?#{{ u: ActivityPub::TagManager.instance.uri_for(status.proper), t: status.capability_tokens.first.token }.to_query}"
else
status.proper
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class ActivityPub::ContextSerializer < ActivityPub::Serializer
include RoutingHelper
attributes :id, :type, :inbox
def id
ActivityPub::TagManager.instance.uri_for(object)
end
def type
'Group'
end
def inbox
account_inbox_url(object.parent_account)
end
end

View file

@ -7,7 +7,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:in_reply_to, :published, :url,
:attributed_to, :to, :cc, :sensitive,
:atom_uri, :in_reply_to_atom_uri,
:conversation
:conversation, :context
attribute :content
attribute :content_map, if: :language?
@ -121,6 +121,12 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
end
def context
return if object.conversation.nil?
ActivityPub::TagManager.instance.uri_for(object.conversation)
end
def local?
object.account.local?
end

View file

@ -52,6 +52,7 @@ class PostStatusService < BaseService
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@visibility = :limited if @visibility&.to_sym != :direct && @in_reply_to&.limited_visibility?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError
@ -64,10 +65,11 @@ class PostStatusService < BaseService
ApplicationRecord.transaction do
@status = @account.statuses.create!(status_attributes)
@status.capability_tokens.create! if @status.limited_visibility?
end
process_hashtags_service.call(@status)
process_mentions_service.call(@status)
ProcessHashtagsService.new.call(@status)
ProcessMentionsService.new.call(@status)
end
def schedule_status!
@ -109,14 +111,6 @@ class PostStatusService < BaseService
ISO_639.find(str)&.alpha2
end
def process_mentions_service
ProcessMentionsService.new
end
def process_hashtags_service
ProcessHashtagsService.new
end
def scheduled?
@scheduled_at.present?
end

View file

@ -42,9 +42,21 @@ class ProcessMentionsService < BaseService
"@#{mentioned_account.acct}"
end
if status.limited_visibility? && status.thread&.limited_visibility?
# If we are replying to a local status, then we'll have the complete
# audience copied here, both local and remote. If we are replying
# to a remote status, only local audience will be copied. Then we
# need to send our reply to the remote author's inbox for distribution
status.thread.mentions.includes(:account).find_each do |mention|
status.mentions.create(silent: true, account: mention.account)
end
end
status.save!
check_for_spam(status)
# Silent mentions need to be delivered separately
mentions.each { |mention| create_notification(mention) }
end

View file

@ -12,8 +12,10 @@ class ActivityPub::DistributionWorker
return if skip_distribution?
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[payload, @account.id, inbox_url]
if delegate_distribution?
deliver_to_parent!
else
deliver_to_inboxes!
end
relay! if relayable?
@ -24,23 +26,45 @@ class ActivityPub::DistributionWorker
private
def skip_distribution?
@status.direct_visibility? || @status.limited_visibility?
@status.direct_visibility?
end
def delegate_distribution?
@status.limited_visibility? && @status.reply? && !@status.conversation.local?
end
def relayable?
@status.public_visibility?
end
def deliver_to_parent!
return if @status.conversation.inbox_url.blank?
ActivityPub::DeliveryWorker.perform_async(payload, @account.id, @status.conversation.inbox_url)
end
def deliver_to_inboxes!
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[payload, @account.id, inbox_url]
end
end
def inboxes
# Deliver the status to all followers.
# If the status is a reply to another local status, also forward it to that
# status' authors' followers.
@inboxes ||= if @status.reply? && @status.thread.account.local? && @status.distributable?
# Deliver the status to all followers. If the status is a reply
# to another local status, also forward it to that status'
# authors' followers. If the status has limited visibility,
# deliver it to inboxes of people mentioned (no shared ones)
@inboxes ||= begin
if @status.limited_visibility?
DeliveryFailureTracker.without_unavailable(Account.remote.joins(:mentions).merge(@status.mentions).pluck(:inbox_url))
elsif @status.reply? && @status.thread.account.local? && @status.distributable?
@account.followers.or(@status.thread.account.followers).inboxes
else
@account.followers.inboxes
end
end
end
def payload
@payload ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status), ActivityPub::ActivitySerializer, signer: @account))

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
class ActivityPub::ForwardDistributionWorker < ActivityPub::DistributionWorker
include Sidekiq::Worker
sidekiq_options queue: 'push'
def perform(conversation_id, json)
conversation = Conversation.find(conversation_id)
@status = conversation.parent_status
@account = conversation.parent_account
@json = json
return if @status.nil? || @account.nil?
deliver_to_inboxes!
rescue ActiveRecord::RecordNotFound
true
end
private
def payload
@json
end
end

View file

@ -85,6 +85,7 @@ Rails.application.routes.draw do
end
resource :inbox, only: [:create], module: :activitypub
resources :contexts, only: [:show], module: :activitypub
get '/@:username', to: 'accounts#show', as: :short_account
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies

View file

@ -0,0 +1,10 @@
class CreateStatusCapabilityTokens < ActiveRecord::Migration[5.2]
def change
create_table :status_capability_tokens do |t|
t.belongs_to :status, foreign_key: true
t.string :token
t.timestamps
end
end
end

View file

@ -0,0 +1,7 @@
class AddInboxUrlToConversations < ActiveRecord::Migration[5.2]
def change
add_column :conversations, :parent_status_id, :bigint, null: true, default: nil
add_column :conversations, :parent_account_id, :bigint, null: true, default: nil
add_column :conversations, :inbox_url, :string, null: true, default: nil
end
end

View file

@ -0,0 +1,15 @@
class ConversationIdsToTimestampIds < ActiveRecord::Migration[5.2]
def up
safety_assured do
execute("ALTER TABLE conversations ALTER COLUMN id SET DEFAULT timestamp_id('conversations')")
end
Mastodon::Snowflake.ensure_id_sequences_exist
end
def down
execute("LOCK conversations")
execute("SELECT setval('conversations_id_seq', (SELECT MAX(id) FROM conversations))")
execute("ALTER TABLE conversations ALTER COLUMN id SET DEFAULT nextval('conversations_id_seq')")
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_06_30_190544) do
ActiveRecord::Schema.define(version: 2020_08_27_205543) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -278,10 +278,13 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
t.index ["account_id", "conversation_id"], name: "index_conversation_mutes_on_account_id_and_conversation_id", unique: true
end
create_table "conversations", force: :cascade do |t|
create_table "conversations", id: :bigint, default: -> { "timestamp_id('conversations'::text)" }, force: :cascade do |t|
t.string "uri"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "parent_status_id"
t.bigint "parent_account_id"
t.string "inbox_url"
t.index ["uri"], name: "index_conversations_on_uri", unique: true
end
@ -743,6 +746,14 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
t.index ["var"], name: "index_site_uploads_on_var", unique: true
end
create_table "status_capability_tokens", force: :cascade do |t|
t.bigint "status_id"
t.string "token"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["status_id"], name: "index_status_capability_tokens_on_status_id"
end
create_table "status_pins", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "status_id", null: false
@ -1003,6 +1014,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
add_foreign_key "status_capability_tokens", "statuses"
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade
add_foreign_key "status_pins", "statuses", on_delete: :cascade
add_foreign_key "status_stats", "statuses", on_delete: :cascade

View file

@ -67,7 +67,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns public Cache-Control header' do
@ -92,7 +92,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it_behaves_like 'cachable response'
@ -191,7 +191,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns no Cache-Control header' do
@ -216,7 +216,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns public Cache-Control header' do
@ -255,7 +255,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns no Cache-Control header' do
@ -280,7 +280,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns private Cache-Control header' do
@ -342,7 +342,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns no Cache-Control header' do
@ -367,7 +367,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns private Cache-Control header' do
@ -455,7 +455,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns no Cache-Control header' do
@ -480,7 +480,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it_behaves_like 'cachable response'
@ -517,7 +517,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns no Cache-Control header' do
@ -542,7 +542,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns private Cache-Control header' do
@ -604,7 +604,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns no Cache-Control header' do
@ -629,7 +629,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns private Cache-Control header' do
@ -797,7 +797,7 @@ describe StatusesController do
end
it 'returns Vary header' do
expect(response.headers['Vary']).to eq 'Accept'
expect(response.headers['Vary']).to eq 'Accept, Authorization'
end
it 'returns public Cache-Control header' do

View file

@ -0,0 +1,2 @@
Fabricator(:status_capability_token) do
end

View file

@ -13,17 +13,22 @@ RSpec.describe ActivityPub::Activity::Create do
}.with_indifferent_access
end
let(:delivered_to_account_id) { nil }
let(:dereferenced_object_json) { nil }
before do
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
stub_request(:get, 'http://example.com/object/123').to_return(body: Oj.dump(dereferenced_object_json), headers: { 'Content-Type' => 'application/activitypub+json' })
end
describe '#perform' do
context 'when fetching' do
subject { described_class.new(json, sender) }
subject { described_class.new(json, sender, delivered_to_account_id: delivered_to_account_id) }
before do
subject.perform
@ -43,6 +48,54 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'when object is a URI' do
let(:object_json) { 'http://example.com/object/123' }
let(:dereferenced_object_json) do
{
id: 'http://example.com/object/123',
type: 'Note',
content: 'Lorem ipsum',
to: 'https://www.w3.org/ns/activitystreams#Public',
}
end
it 'dereferences object from URI' do
expect(a_request(:get, 'http://example.com/object/123')).to have_been_made.once
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'public'
end
end
context 'when object is a bearcap' do
let(:object_json) { 'bear:?u=http://example.com/object/123&t=hoge' }
let(:dereferenced_object_json) do
{
id: 'http://example.com/object/123',
type: 'Note',
content: 'Lorem ipsum',
}
end
it 'dereferences object from URI' do
expect(a_request(:get, 'http://example.com/object/123').with(headers: { 'Authorization' => 'Bearer hoge' })).to have_been_made.once
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.uri).to eq 'http://example.com/object/123'
expect(status.visibility).to eq 'direct'
end
end
context 'standalone' do
let(:object_json) do
{
@ -146,12 +199,15 @@ RSpec.describe ActivityPub::Activity::Create do
context 'limited' do
let(:recipient) { Fabricate(:account) }
let(:delivered_to_account_id) { recipient.id }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient),
to: [],
cc: [],
}
end
@ -164,7 +220,7 @@ RSpec.describe ActivityPub::Activity::Create do
it 'creates silent mention' do
status = sender.statuses.first
expect(status.mentions.first).to be_silent
expect(status.mentions.find_by(account: recipient)).to be_silent
end
end

View file

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe StatusCapabilityToken, type: :model do
end

View file

@ -9,6 +9,8 @@ describe ActivityPub::DistributionWorker do
describe '#perform' do
before do
allow(ActivityPub::DeliveryWorker).to receive(:push_bulk)
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
follower.follow!(status.account)
end
@ -34,6 +36,40 @@ describe ActivityPub::DistributionWorker do
end
end
context 'with limited status' do
before do
status.update(visibility: :limited)
status.capability_tokens.create!
end
context 'standalone' do
before do
2.times do |i|
status.mentions.create!(silent: true, account: Fabricate(:account, username: "bob#{i}", domain: "example#{i}.com", inbox_url: "https://example#{i}.com/inbox"))
end
end
it 'delivers to personal inboxes' do
subject.perform(status.id)
expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['https://example0.com/inbox', 'https://example1.com/inbox'])
end
end
context 'when it\'s a reply' do
let(:conversation) { Fabricate(:conversation, uri: 'https://example.com/123', inbox_url: 'https://example.com/123/inbox') }
let(:parent) { Fabricate(:status, visibility: :limited, account: Fabricate(:account, username: 'alice', domain: 'example.com', inbox_url: 'https://example.com/inbox'), conversation: conversation) }
before do
status.update(thread: parent, conversation: conversation)
end
it 'delivers to inbox of conversation only' do
subject.perform(status.id)
expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).once
end
end
end
context 'with direct status' do
before do
status.update(visibility: :direct)