ActivityPub delivery (#4566)

* Deliver ActivityPub Like

* Deliver ActivityPub Undo-Like

* Deliver ActivityPub Create/Announce activities

* Deliver ActivityPub creates from mentions

* Deliver ActivityPub Block/Undo-Block

* Deliver ActivityPub Accept/Reject-Follow

* Deliver ActivityPub Undo-Follow

* Deliver ActivityPub Follow

* Deliver ActivityPub Delete activities

Incidentally fix #889

* Adjust BatchedRemoveStatusService for ActivityPub

* Add tests for ActivityPub workers

* Add tests for FollowService

* Add tests for FavouriteService, UnfollowService and PostStatusService

* Add tests for ReblogService, BlockService, UnblockService, ProcessMentionsService

* Add tests for AuthorizeFollowService, RejectFollowService, RemoveStatusService

* Add tests for BatchedRemoveStatusService

* Deliver updates to a local account to ActivityPub followers

* Minor adjustments
This commit is contained in:
Eugen Rochko 2017-08-13 00:44:41 +02:00 committed by GitHub
parent ccdd5a9576
commit b7370ac8ba
41 changed files with 786 additions and 114 deletions

View File

@ -10,8 +10,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
end end
def update def update
current_account.update!(account_params)
@account = current_account @account = current_account
@account.update!(account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer render json: @account, serializer: REST::CredentialAccountSerializer
end end

View File

@ -15,6 +15,7 @@ class Settings::ProfilesController < ApplicationController
def update def update
if @account.update(account_params) if @account.update(account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else else
render :show render :show

View File

@ -93,7 +93,7 @@ class ActivityPub::Activity
end end
def distribute_to_followers(status) def distribute_to_followers(status)
DistributionWorker.perform_async(status.id) ::DistributionWorker.perform_async(status.id)
end end
def delete_arrived_first?(uri) def delete_arrived_first?(uri)

View File

@ -171,6 +171,10 @@ class Account < ApplicationRecord
reorder(nil).pluck('distinct accounts.domain') reorder(nil).pluck('distinct accounts.domain')
end end
def inboxes
reorder(nil).where(protocol: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")
end
def triadic_closures(account, limit: 5, offset: 0) def triadic_closures(account, limit: 5, offset: 0)
sql = <<-SQL.squish sql = <<-SQL.squish
WITH first_degree AS ( WITH first_degree AS (

View File

@ -4,11 +4,28 @@ class AuthorizeFollowService < BaseService
def call(source_account, target_account) def call(source_account, target_account)
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
follow_request.authorize! follow_request.authorize!
NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local? create_notification(follow_request) unless source_account.local?
follow_request
end end
private private
def create_notification(follow_request)
if follow_request.account.ostatus?
NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id)
elsif follow_request.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
end
end
def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new(
follow_request,
serializer: ActivityPub::AcceptFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(follow_request) def build_xml(follow_request)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request))
end end

View File

@ -15,9 +15,11 @@ class BatchedRemoveStatusService < BaseService
@mentions = statuses.map { |s| [s.id, s.mentions.includes(:account).to_a] }.to_h @mentions = statuses.map { |s| [s.id, s.mentions.includes(:account).to_a] }.to_h
@tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h @tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h
@stream_entry_batches = [] @stream_entry_batches = []
@salmon_batches = [] @salmon_batches = []
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h @activity_json_batches = []
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h
@activity_json = {}
# Ensure that rendered XML reflects destroyed state # Ensure that rendered XML reflects destroyed state
Status.where(id: statuses.map(&:id)).in_batches.destroy_all Status.where(id: statuses.map(&:id)).in_batches.destroy_all
@ -27,7 +29,11 @@ class BatchedRemoveStatusService < BaseService
account = account_statuses.first.account account = account_statuses.first.account
unpush_from_home_timelines(account_statuses) unpush_from_home_timelines(account_statuses)
batch_stream_entries(account_statuses) if account.local?
if account.local?
batch_stream_entries(account_statuses)
batch_activity_json(account, account_statuses)
end
end end
# Cannot be batched # Cannot be batched
@ -38,6 +44,7 @@ class BatchedRemoveStatusService < BaseService
Pubsubhubbub::DistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch } Pubsubhubbub::DistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch } NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch }
end end
private private
@ -50,6 +57,22 @@ class BatchedRemoveStatusService < BaseService
end end
end end
def batch_activity_json(account, statuses)
account.followers.inboxes.each do |inbox_url|
statuses.each do |status|
@activity_json_batches << [build_json(status), account.id, inbox_url]
end
end
statuses.each do |status|
other_recipients = (status.mentions + status.reblogs).map(&:account).reject(&:local?).select(&:activitypub?).uniq(&:id)
other_recipients.each do |target_account|
@activity_json_batches << [build_json(status), account.id, target_account.inbox_url]
end
end
end
def unpush_from_home_timelines(statuses) def unpush_from_home_timelines(statuses)
account = statuses.first.account account = statuses.first.account
recipients = account.followers.local.pluck(:id) recipients = account.followers.local.pluck(:id)
@ -79,7 +102,7 @@ class BatchedRemoveStatusService < BaseService
return if @mentions[status.id].empty? return if @mentions[status.id].empty?
payload = stream_entry_to_xml(status.stream_entry.reload) payload = stream_entry_to_xml(status.stream_entry.reload)
recipients = @mentions[status.id].map(&:account).reject(&:local?).uniq(&:domain).map(&:id) recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id)
recipients.each do |recipient_id| recipients.each do |recipient_id|
@salmon_batches << [payload, status.account_id, recipient_id] @salmon_batches << [payload, status.account_id, recipient_id]
@ -111,4 +134,14 @@ class BatchedRemoveStatusService < BaseService
def redis def redis
Redis.current Redis.current
end end
def build_json(status)
return @activity_json[status.id] if @activity_json.key?(status.id)
@activity_json[status.id] = ActiveModelSerializers::SerializableResource.new(
status,
serializer: ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter
).to_json
end
end end

View File

@ -12,11 +12,28 @@ class BlockService < BaseService
block = account.block!(target_account) block = account.block!(target_account)
BlockWorker.perform_async(account.id, target_account.id) BlockWorker.perform_async(account.id, target_account.id)
NotificationWorker.perform_async(build_xml(block), account.id, target_account.id) unless target_account.local? create_notification(block) unless target_account.local?
block
end end
private private
def create_notification(block)
if block.target_account.ostatus?
NotificationWorker.perform_async(build_xml(block), block.account_id, block.target_account_id)
elsif block.target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url)
end
end
def build_json(block)
ActiveModelSerializers::SerializableResource.new(
block,
serializer: ActivityPub::BlockSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(block) def build_xml(block)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block))
end end

View File

@ -15,18 +15,32 @@ class FavouriteService < BaseService
return favourite unless favourite.nil? return favourite unless favourite.nil?
favourite = Favourite.create!(account: account, status: status) favourite = Favourite.create!(account: account, status: status)
create_notification(favourite)
if status.local?
NotifyService.new.call(favourite.status.account, favourite)
else
NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id)
end
favourite favourite
end end
private private
def create_notification(favourite)
status = favourite.status
if status.account.local?
NotifyService.new.call(status.account, favourite)
elsif status.account.ostatus?
NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id)
elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
end
end
def build_json(favourite)
ActiveModelSerializers::SerializableResource.new(
favourite,
serializer: ActivityPub::LikeSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(favourite) def build_xml(favourite)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite))
end end

View File

@ -14,7 +14,7 @@ class FollowService < BaseService
return if source_account.following?(target_account) return if source_account.following?(target_account)
if target_account.locked? if target_account.locked? || target_account.activitypub?
request_follow(source_account, target_account) request_follow(source_account, target_account)
else else
direct_follow(source_account, target_account) direct_follow(source_account, target_account)
@ -28,9 +28,11 @@ class FollowService < BaseService
if target_account.local? if target_account.local?
NotifyService.new.call(target_account, follow_request) NotifyService.new.call(target_account, follow_request)
else elsif target_account.ostatus?
NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id) NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
AfterRemoteFollowRequestWorker.perform_async(follow_request.id) AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
elsif target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url)
end end
follow_request follow_request
@ -63,4 +65,12 @@ class FollowService < BaseService
def build_follow_xml(follow) def build_follow_xml(follow)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow))
end end
def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new(
follow_request,
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end
end end

View File

@ -39,6 +39,7 @@ class PostStatusService < BaseService
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
DistributionWorker.perform_async(status.id) DistributionWorker.perform_async(status.id)
Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
ActivityPub::DistributionWorker.perform_async(status.id)
if options[:idempotency].present? if options[:idempotency].present?
redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id) redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id)

View File

@ -28,18 +28,32 @@ class ProcessMentionsService < BaseService
end end
status.mentions.includes(:account).each do |mention| status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account create_notification(status, mention)
if mentioned_account.local?
NotifyService.new.call(mentioned_account, mention)
else
NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
end
end end
end end
private private
def create_notification(status, mention)
mentioned_account = mention.account
if mentioned_account.local?
NotifyService.new.call(mentioned_account, mention)
elsif mentioned_account.ostatus?
NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
elsif mentioned_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url)
end
end
def build_json(status)
ActiveModelSerializers::SerializableResource.new(
status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).to_json
end
def follow_remote_account_service def follow_remote_account_service
@follow_remote_account_service ||= ResolveRemoteAccountService.new @follow_remote_account_service ||= ResolveRemoteAccountService.new
end end

View File

@ -21,13 +21,31 @@ class ReblogService < BaseService
DistributionWorker.perform_async(reblog.id) DistributionWorker.perform_async(reblog.id)
Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
ActivityPub::DistributionWorker.perform_async(reblog.id)
if reblogged_status.local? create_notification(reblog)
NotifyService.new.call(reblog.reblog.account, reblog)
else
NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), account.id, reblog.reblog.account_id)
end
reblog reblog
end end
private
def create_notification(reblog)
reblogged_status = reblog.reblog
if reblogged_status.account.local?
NotifyService.new.call(reblogged_status.account, reblog)
elsif reblogged_status.account.ostatus?
NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), reblog.account_id, reblogged_status.account_id)
elsif reblogged_status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
end
end
def build_json(reblog)
ActiveModelSerializers::SerializableResource.new(
reblog,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).to_json
end
end end

View File

@ -4,11 +4,28 @@ class RejectFollowService < BaseService
def call(source_account, target_account) def call(source_account, target_account)
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
follow_request.reject! follow_request.reject!
NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local? create_notification(follow_request) unless source_account.local?
follow_request
end end
private private
def create_notification(follow_request)
if follow_request.account.ostatus?
NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id)
elsif follow_request.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
end
end
def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new(
follow_request,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(follow_request) def build_xml(follow_request)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request))
end end

View File

@ -22,8 +22,10 @@ class RemoveStatusService < BaseService
return unless @account.local? return unless @account.local?
remove_from_mentioned(@stream_entry.reload) @stream_entry = @stream_entry.reload
Pubsubhubbub::DistributionWorker.perform_async(@stream_entry.id)
remove_from_remote_followers
remove_from_remote_affected
end end
private private
@ -38,13 +40,46 @@ class RemoveStatusService < BaseService
end end
end end
def remove_from_mentioned(stream_entry) def remove_from_remote_affected
salmon_xml = stream_entry_to_xml(stream_entry) # People who got mentioned in the status, or who
target_accounts = @mentions.map(&:account).reject(&:local?).uniq(&:domain) # reblogged it from someone else might not follow
# the author and wouldn't normally receive the
# delete notification - so here, we explicitly
# send it to them
NotificationWorker.push_bulk(target_accounts) do |target_account| target_accounts = (@mentions.map(&:account).reject(&:local?) + @reblogs.map(&:account).reject(&:local?)).uniq(&:id)
[salmon_xml, stream_entry.account_id, target_account.id]
# Ostatus
NotificationWorker.push_bulk(target_accounts.select(&:ostatus?).uniq(&:domain)) do |target_account|
[salmon_xml, @account.id, target_account.id]
end end
# ActivityPub
ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:inbox_url)) do |inbox_url|
[activity_json, @account.id, inbox_url]
end
end
def remove_from_remote_followers
# OStatus
Pubsubhubbub::DistributionWorker.perform_async(@stream_entry.id)
# ActivityPub
ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url|
[activity_json, @account.id, inbox_url]
end
end
def salmon_xml
@salmon_xml ||= stream_entry_to_xml(@stream_entry)
end
def activity_json
@activity_json ||= ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def remove_reblogs def remove_reblogs

View File

@ -5,11 +5,28 @@ class UnblockService < BaseService
return unless account.blocking?(target_account) return unless account.blocking?(target_account)
unblock = account.unblock!(target_account) unblock = account.unblock!(target_account)
NotificationWorker.perform_async(build_xml(unblock), account.id, target_account.id) unless target_account.local? create_notification(unblock) unless target_account.local?
unblock
end end
private private
def create_notification(unblock)
if unblock.target_account.ostatus?
NotificationWorker.perform_async(build_xml(unblock), unblock.account_id, unblock.target_account_id)
elsif unblock.target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url)
end
end
def build_json(unblock)
ActiveModelSerializers::SerializableResource.new(
unblock,
serializer: ActivityPub::UndoBlockSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(block) def build_xml(block)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block))
end end

View File

@ -4,14 +4,30 @@ class UnfavouriteService < BaseService
def call(account, status) def call(account, status)
favourite = Favourite.find_by!(account: account, status: status) favourite = Favourite.find_by!(account: account, status: status)
favourite.destroy! favourite.destroy!
create_notification(favourite) unless status.local?
NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) unless status.local?
favourite favourite
end end
private private
def create_notification(favourite)
status = favourite.status
if status.account.ostatus?
NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id)
elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
end
end
def build_json(favourite)
ActiveModelSerializers::SerializableResource.new(
favourite,
serializer: ActivityPub::UndoLikeSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(favourite) def build_xml(favourite)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite))
end end

View File

@ -7,12 +7,29 @@ class UnfollowService < BaseService
def call(source_account, target_account) def call(source_account, target_account)
follow = source_account.unfollow!(target_account) follow = source_account.unfollow!(target_account)
return unless follow return unless follow
NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local? create_notification(follow) unless target_account.local?
UnmergeWorker.perform_async(target_account.id, source_account.id) UnmergeWorker.perform_async(target_account.id, source_account.id)
follow
end end
private private
def create_notification(follow)
if follow.target_account.ostatus?
NotificationWorker.perform_async(build_xml(follow), follow.account_id, follow.target_account_id)
elsif follow.target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url)
end
end
def build_json(follow)
ActiveModelSerializers::SerializableResource.new(
follow,
serializer: ActivityPub::UndoFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def build_xml(follow) def build_xml(follow)
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow)) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow))
end end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class ActivityPub::DeliveryWorker
include Sidekiq::Worker
sidekiq_options queue: 'push', retry: 5, dead: false
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url)
@json = json
@source_account = Account.find(source_account_id)
@inbox_url = inbox_url
perform_request
raise Mastodon::UnexpectedResponseError, @response unless response_successful?
rescue => e
raise e.class, "Delivery failed for #{inbox_url}: #{e.message}"
end
private
def build_request
request = Request.new(:post, @inbox_url, body: @json)
request.on_behalf_of(@source_account, :uri)
request.add_headers(HEADERS)
end
def perform_request
@response = build_request.perform
end
def response_successful?
@response.code > 199 && @response.code < 300
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class ActivityPub::DistributionWorker
include Sidekiq::Worker
sidekiq_options queue: 'push'
def perform(status_id)
@status = Status.find(status_id)
@account = @status.account
return if skip_distribution?
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[payload, @account.id, inbox_url]
end
rescue ActiveRecord::RecordNotFound
true
end
private
def skip_distribution?
@status.direct_visibility?
end
def inboxes
@inboxes ||= @account.followers.inboxes
end
def payload
@payload ||= ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).to_json
end
end

View File

@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker
sidekiq_options backtrace: true sidekiq_options backtrace: true
def perform(account_id, body) def perform(account_id, body)
ProcessCollectionService.new.call(body, Account.find(account_id)) ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id))
end end
end end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class ActivityPub::UpdateDistributionWorker
include Sidekiq::Worker
sidekiq_options queue: 'push'
def perform(account_id)
@account = Account.find(account_id)
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[payload, @account.id, inbox_url]
end
rescue ActiveRecord::RecordNotFound
true
end
private
def inboxes
@inboxes ||= @account.followers.inboxes
end
def payload
@payload ||= ActiveModelSerializers::SerializableResource.new(
@account,
serializer: ActivityPub::UpdateSerializer,
adapter: ActivityPub::Adapter
).to_json
end
end

View File

@ -20,6 +20,8 @@ describe Api::V1::Accounts::CredentialsController do
describe 'PATCH #update' do describe 'PATCH #update' do
describe 'with valid data' do describe 'with valid data' do
before do before do
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
patch :update, params: { patch :update, params: {
display_name: "Alice Isn't Dead", display_name: "Alice Isn't Dead",
note: "Hi!\n\nToot toot!", note: "Hi!\n\nToot toot!",
@ -40,6 +42,10 @@ describe Api::V1::Accounts::CredentialsController do
expect(user.account.avatar).to exist expect(user.account.avatar).to exist
expect(user.account.header).to exist expect(user.account.header).to exist
end end
it 'queues up an account update distribution' do
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id)
end
end end
describe 'with invalid data' do describe 'with invalid data' do

View File

@ -17,11 +17,13 @@ RSpec.describe Settings::ProfilesController, type: :controller do
describe 'PUT #update' do describe 'PUT #update' do
it 'updates the user profile' do it 'updates the user profile' do
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
account = Fabricate(:account, user: @user, display_name: 'Old name') account = Fabricate(:account, user: @user, display_name: 'Old name')
put :update, params: { account: { display_name: 'New name' } } put :update, params: { account: { display_name: 'New name' } }
expect(account.reload.display_name).to eq 'New name' expect(account.reload.display_name).to eq 'New name'
expect(response).to redirect_to(settings_profile_path) expect(response).to redirect_to(settings_profile_path)
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
end end
end end
end end

View File

@ -22,7 +22,7 @@ RSpec.describe AuthorizeFollowService do
end end
end end
describe 'remote' do describe 'remote OStatus' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
before do before do
@ -46,4 +46,26 @@ RSpec.describe AuthorizeFollowService do
}).to have_been_made.once }).to have_been_made.once
end end
end end
describe 'remote ActivityPub' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
before do
FollowRequest.create(account: bob, target_account: sender)
stub_request(:post, bob.inbox_url).to_return(status: 200)
subject.call(bob, sender)
end
it 'removes follow request' do
expect(bob.requested?(sender)).to be false
end
it 'creates follow relation' do
expect(bob.following?(sender)).to be true
end
it 'sends an accept activity' do
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
end
end
end end

View File

@ -6,6 +6,7 @@ RSpec.describe BatchedRemoveStatusService do
let!(:alice) { Fabricate(:account) } let!(:alice) { Fabricate(:account) }
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
let!(:jeff) { Fabricate(:account) } let!(:jeff) { Fabricate(:account) }
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
let(:status1) { PostStatusService.new.call(alice, 'Hello @bob@example.com') } let(:status1) { PostStatusService.new.call(alice, 'Hello @bob@example.com') }
let(:status2) { PostStatusService.new.call(alice, 'Another status') } let(:status2) { PostStatusService.new.call(alice, 'Another status') }
@ -15,9 +16,11 @@ RSpec.describe BatchedRemoveStatusService do
stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {})
stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {})
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
jeff.follow!(alice) jeff.follow!(alice)
hank.follow!(alice)
status1 status1
status2 status2
@ -58,4 +61,8 @@ RSpec.describe BatchedRemoveStatusService do
xml.match(TagManager::VERBS[:delete]) xml.match(TagManager::VERBS[:delete])
}).to have_been_made.once }).to have_been_made.once
end end
it 'sends delete activity to followers' do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once
end
end end

View File

@ -17,7 +17,7 @@ RSpec.describe BlockService do
end end
end end
describe 'remote' do describe 'remote OStatus' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
before do before do
@ -36,4 +36,21 @@ RSpec.describe BlockService do
}).to have_been_made.once }).to have_been_made.once
end end
end end
describe 'remote ActivityPub' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
before do
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
subject.call(sender, bob)
end
it 'creates a blocking relation' do
expect(sender.blocking?(bob)).to be true
end
it 'sends a block activity' do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
end
end
end end

View File

@ -18,8 +18,8 @@ RSpec.describe FavouriteService do
end end
end end
describe 'remote' do describe 'remote OStatus' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') } let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') }
before do before do
@ -38,4 +38,22 @@ RSpec.describe FavouriteService do
}).to have_been_made.once }).to have_been_made.once
end end
end end
describe 'remote ActivityPub' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :activitypub, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
let(:status) { Fabricate(:status, account: bob) }
before do
stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
subject.call(sender, status)
end
it 'creates a favourite' do
expect(status.favourites.first).to_not be_nil
end
it 'sends a like activity' do
expect(a_request(:post, "http://example.com/inbox")).to have_been_made.once
end
end
end end

View File

@ -44,9 +44,9 @@ RSpec.describe FollowService do
end end
end end
context 'remote account' do context 'remote OStatus account' do
describe 'locked account' do describe 'locked account' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
before do before do
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
@ -66,7 +66,7 @@ RSpec.describe FollowService do
end end
describe 'unlocked account' do describe 'unlocked account' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
before do before do
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
@ -91,7 +91,7 @@ RSpec.describe FollowService do
end end
describe 'already followed account' do describe 'already followed account' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
before do before do
sender.follow!(bob) sender.follow!(bob)
@ -111,4 +111,21 @@ RSpec.describe FollowService do
end end
end end
end end
context 'remote ActivityPub account' do
let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
before do
stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
subject.call(sender, bob.acct)
end
it 'creates follow request' do
expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
end
it 'sends a follow activity to the inbox' do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
end
end
end end

View File

@ -100,16 +100,18 @@ RSpec.describe PostStatusService do
expect(hashtags_service).to have_received(:call).with(status) expect(hashtags_service).to have_received(:call).with(status)
end end
it 'pings PuSH hubs' do it 'gets distributed' do
allow(DistributionWorker).to receive(:perform_async) allow(DistributionWorker).to receive(:perform_async)
allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async) allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async)
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
account = Fabricate(:account) account = Fabricate(:account)
status = subject.call(account, "test status update") status = subject.call(account, "test status update")
expect(DistributionWorker).to have_received(:perform_async).with(status.id) expect(DistributionWorker).to have_received(:perform_async).with(status.id)
expect(Pubsubhubbub::DistributionWorker). expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id)
to have_received(:perform_async).with(status.stream_entry.id) expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id)
end end
it 'crawls links' do it 'crawls links' do

View File

@ -1,22 +1,44 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ProcessMentionsService do RSpec.describe ProcessMentionsService do
let(:account) { Fabricate(:account, username: 'alice') } let(:account) { Fabricate(:account, username: 'alice') }
let(:remote_user) { Fabricate(:account, username: 'remote_user', domain: 'example.com', salmon_url: 'http://salmon.example.com') } let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
subject { ProcessMentionsService.new } context 'OStatus' do
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
before do subject { ProcessMentionsService.new }
stub_request(:post, remote_user.salmon_url)
subject.(status) before do
stub_request(:post, remote_user.salmon_url)
subject.call(status)
end
it 'creates a mention' do
expect(remote_user.mentions.where(status: status).count).to eq 1
end
it 'posts to remote user\'s Salmon end point' do
expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once
end
end end
it 'creates a mention' do context 'ActivityPub' do
expect(remote_user.mentions.where(status: status).count).to eq 1 let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
end
it 'posts to remote user\'s Salmon end point' do subject { ProcessMentionsService.new }
expect(a_request(:post, remote_user.salmon_url)).to have_been_made
before do
stub_request(:post, remote_user.inbox_url)
subject.call(status)
end
it 'creates a mention' do
expect(remote_user.mentions.where(status: status).count).to eq 1
end
it 'sends activity to the inbox' do
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
end
end end
end end

View File

@ -2,22 +2,49 @@ require 'rails_helper'
RSpec.describe ReblogService do RSpec.describe ReblogService do
let(:alice) { Fabricate(:account, username: 'alice') } let(:alice) { Fabricate(:account, username: 'alice') }
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') }
let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') }
subject { ReblogService.new } context 'OStatus' do
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') }
let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') }
before do subject { ReblogService.new }
stub_request(:post, 'http://salmon.example.com')
subject.(alice, status) before do
stub_request(:post, 'http://salmon.example.com')
subject.call(alice, status)
end
it 'creates a reblog' do
expect(status.reblogs.count).to eq 1
end
it 'sends a Salmon slap for a remote reblog' do
expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
end
end end
it 'creates a reblog' do context 'ActivityPub' do
expect(status.reblogs.count).to eq 1 let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
end let(:status) { Fabricate(:status, account: bob) }
it 'sends a Salmon slap for a remote reblog' do subject { ReblogService.new }
expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
before do
stub_request(:post, bob.inbox_url)
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
subject.call(alice, status)
end
it 'creates a reblog' do
expect(status.reblogs.count).to eq 1
end
it 'distributes to followers' do
expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
end
it 'sends an announce activity to the author' do
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
end
end end
end end

View File

@ -22,7 +22,7 @@ RSpec.describe RejectFollowService do
end end
end end
describe 'remote' do describe 'remote OStatus' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
before do before do
@ -46,4 +46,26 @@ RSpec.describe RejectFollowService do
}).to have_been_made.once }).to have_been_made.once
end end
end end
describe 'remote ActivityPub' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
before do
FollowRequest.create(account: bob, target_account: sender)
stub_request(:post, bob.inbox_url).to_return(status: 200)
subject.call(bob, sender)
end
it 'removes follow request' do
expect(bob.requested?(sender)).to be false
end
it 'does not create follow relation' do
expect(bob.following?(sender)).to be false
end
it 'sends a reject activity' do
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
end
end
end end

View File

@ -6,13 +6,17 @@ RSpec.describe RemoveStatusService do
let!(:alice) { Fabricate(:account) } let!(:alice) { Fabricate(:account) }
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
let!(:jeff) { Fabricate(:account) } let!(:jeff) { Fabricate(:account) }
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
before do before do
stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {})
stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {})
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
jeff.follow!(alice) jeff.follow!(alice)
hank.follow!(alice)
@status = PostStatusService.new.call(alice, 'Hello @bob@example.com') @status = PostStatusService.new.call(alice, 'Hello @bob@example.com')
subject.call(@status) subject.call(@status)
end end
@ -31,6 +35,10 @@ RSpec.describe RemoveStatusService do
}).to have_been_made }).to have_been_made
end end
it 'sends delete activity to followers' do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
end
it 'sends Salmon slap to previously mentioned users' do it 'sends Salmon slap to previously mentioned users' do
expect(a_request(:post, "http://example.com/salmon").with { |req| expect(a_request(:post, "http://example.com/salmon").with { |req|
xml = OStatus2::Salmon.new.unpack(req.body) xml = OStatus2::Salmon.new.unpack(req.body)

View File

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ResolveRemoteAccountService do RSpec.describe ResolveRemoteAccountService do
subject { ResolveRemoteAccountService.new } subject { described_class.new }
before do before do
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
@ -29,29 +29,6 @@ RSpec.describe ResolveRemoteAccountService do
expect(subject.call('catsrgr8@example.com')).to be_nil expect(subject.call('catsrgr8@example.com')).to be_nil
end end
it 'returns an already existing remote account' do
old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no')
returned_account = subject.call('gargron@quitter.no')
expect(old_account.id).to eq returned_account.id
end
it 'returns a new remote account' do
account = subject.call('gargron@quitter.no')
expect(account.username).to eq 'gargron'
expect(account.domain).to eq 'quitter.no'
expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
end
it 'follows a legitimate account redirection' do
account = subject.call('gargron@redirected.com')
expect(account.username).to eq 'gargron'
expect(account.domain).to eq 'quitter.no'
expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
end
it 'prevents hijacking existing accounts' do it 'prevents hijacking existing accounts' do
account = subject.call('hacker1@redirected.com') account = subject.call('hacker1@redirected.com')
expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477' expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477'
@ -61,12 +38,41 @@ RSpec.describe ResolveRemoteAccountService do
expect(subject.call('hacker2@redirected.com')).to be_nil expect(subject.call('hacker2@redirected.com')).to be_nil
end end
it 'returns a new remote account' do context 'with an OStatus account' do
account = subject.call('foo@localdomain.com') it 'returns an already existing remote account' do
old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no')
returned_account = subject.call('gargron@quitter.no')
expect(account.username).to eq 'foo' expect(old_account.id).to eq returned_account.id
expect(account.domain).to eq 'localdomain.com' end
expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
it 'returns a new remote account' do
account = subject.call('gargron@quitter.no')
expect(account.username).to eq 'gargron'
expect(account.domain).to eq 'quitter.no'
expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
end
it 'follows a legitimate account redirection' do
account = subject.call('gargron@redirected.com')
expect(account.username).to eq 'gargron'
expect(account.domain).to eq 'quitter.no'
expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
end
it 'returns a new remote account' do
account = subject.call('foo@localdomain.com')
expect(account.username).to eq 'foo'
expect(account.domain).to eq 'localdomain.com'
expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
end
end
context 'with an ActivityPub account' do
pending
end end
it 'processes one remote account at a time using locks' do it 'processes one remote account at a time using locks' do
@ -78,7 +84,7 @@ RSpec.describe ResolveRemoteAccountService do
Thread.new do Thread.new do
true while wait_for_start true while wait_for_start
begin begin
return_values << ResolveRemoteAccountService.new.call('foo@localdomain.com') return_values << described_class.new.call('foo@localdomain.com')
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
fail_occurred = true fail_occurred = true
end end

View File

@ -18,7 +18,7 @@ RSpec.describe UnblockService do
end end
end end
describe 'remote' do describe 'remote OStatus' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
before do before do
@ -28,7 +28,7 @@ RSpec.describe UnblockService do
end end
it 'destroys the blocking relation' do it 'destroys the blocking relation' do
expect(sender.following?(bob)).to be false expect(sender.blocking?(bob)).to be false
end end
it 'sends an unblock salmon slap' do it 'sends an unblock salmon slap' do
@ -38,4 +38,22 @@ RSpec.describe UnblockService do
}).to have_been_made.once }).to have_been_made.once
end end
end end
describe 'remote ActivityPub' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
before do
sender.block!(bob)
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
subject.call(sender, bob)
end
it 'destroys the blocking relation' do
expect(sender.blocking?(bob)).to be false
end
it 'sends an unblock activity' do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
end
end
end end

View File

@ -18,8 +18,8 @@ RSpec.describe UnfollowService do
end end
end end
describe 'remote' do describe 'remote OStatus' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
before do before do
sender.follow!(bob) sender.follow!(bob)
@ -38,4 +38,22 @@ RSpec.describe UnfollowService do
}).to have_been_made.once }).to have_been_made.once
end end
end end
describe 'remote ActivityPub' do
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
before do
sender.follow!(bob)
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
subject.call(sender, bob)
end
it 'destroys the following relation' do
expect(sender.following?(bob)).to be false
end
it 'sends an unfollow activity' do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
end
end
end end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
describe ActivityPub::DeliveryWorker do
subject { described_class.new }
let(:sender) { Fabricate(:account) }
let(:payload) { 'test' }
describe 'perform' do
it 'performs a request' do
stub_request(:post, 'https://example.com/api').to_return(status: 200)
subject.perform(payload, sender.id, 'https://example.com/api')
expect(a_request(:post, 'https://example.com/api')).to have_been_made.once
end
it 'raises when request fails' do
stub_request(:post, 'https://example.com/api').to_return(status: 500)
expect { subject.perform(payload, sender.id, 'https://example.com/api') }.to raise_error Mastodon::UnexpectedResponseError
end
end
end

View File

@ -0,0 +1,48 @@
require 'rails_helper'
describe ActivityPub::DistributionWorker do
subject { described_class.new }
let(:status) { Fabricate(:status) }
let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') }
describe '#perform' do
before do
allow(ActivityPub::DeliveryWorker).to receive(:push_bulk)
follower.follow!(status.account)
end
context 'with public status' do
before do
status.update(visibility: :public)
end
it 'delivers to followers' do
subject.perform(status.id)
expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com'])
end
end
context 'with private status' do
before do
status.update(visibility: :private)
end
it 'delivers to followers' do
subject.perform(status.id)
expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com'])
end
end
context 'with direct status' do
before do
status.update(visibility: :direct)
end
it 'does nothing' do
subject.perform(status.id)
expect(ActivityPub::DeliveryWorker).to_not have_received(:push_bulk)
end
end
end
end

View File

@ -0,0 +1,15 @@
require 'rails_helper'
describe ActivityPub::ProcessingWorker do
subject { described_class.new }
let(:account) { Fabricate(:account) }
describe '#perform' do
it 'delegates to ActivityPub::ProcessCollectionService' do
allow(ActivityPub::ProcessCollectionService).to receive(:new).and_return(double(:service, call: nil))
subject.perform(account.id, '')
expect(ActivityPub::ProcessCollectionService).to have_received(:new)
end
end
end

View File

@ -0,0 +1,16 @@
require 'rails_helper'
describe ActivityPub::ThreadResolveWorker do
subject { described_class.new }
let(:status) { Fabricate(:status) }
let(:parent) { Fabricate(:status) }
describe '#perform' do
it 'gets parent from ActivityPub::FetchRemoteStatusService and glues them together' do
allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(double(:service, call: parent))
subject.perform(status.id, 'http://example.com/123')
expect(status.reload.in_reply_to_id).to eq parent.id
end
end
end

View File

@ -0,0 +1,20 @@
require 'rails_helper'
describe ActivityPub::UpdateDistributionWorker do
subject { described_class.new }
let(:account) { Fabricate(:account) }
let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') }
describe '#perform' do
before do
allow(ActivityPub::DeliveryWorker).to receive(:push_bulk)
follower.follow!(account)
end
it 'delivers to followers' do
subject.perform(account.id)
expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com'])
end
end
end