Merge branch 'origin/master' into sync/upstream
Conflicts: app/javascript/mastodon/components/status_list.js app/javascript/mastodon/features/notifications/index.js app/javascript/mastodon/features/ui/components/modal_root.js app/javascript/mastodon/features/ui/components/onboarding_modal.js app/javascript/mastodon/features/ui/index.js app/javascript/styles/about.scss app/javascript/styles/accounts.scss app/javascript/styles/components.scss app/presenters/instance_presenter.rb app/services/post_status_service.rb app/services/reblog_service.rb app/views/about/more.html.haml app/views/about/show.html.haml app/views/accounts/_header.html.haml config/webpack/loaders/babel.js spec/controllers/api/v1/accounts/credentials_controller_spec.rb
This commit is contained in:
57
app/services/activitypub/fetch_remote_account_service.rb
Normal file
57
app/services/activitypub/fetch_remote_account_service.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FetchRemoteAccountService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
# Should be called when uri has already been checked for locality
|
||||
# Does a WebFinger roundtrip on each call
|
||||
def call(uri, prefetched_json = nil)
|
||||
@json = body_to_json(prefetched_json) || fetch_resource(uri)
|
||||
|
||||
return unless supported_context? && expected_type?
|
||||
|
||||
@uri = @json['id']
|
||||
@username = @json['preferredUsername']
|
||||
@domain = Addressable::URI.parse(uri).normalized_host
|
||||
|
||||
return unless verified_webfinger?
|
||||
|
||||
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json)
|
||||
rescue Oj::ParseError
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verified_webfinger?
|
||||
webfinger = Goldfinger.finger("acct:#{@username}@#{@domain}")
|
||||
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
||||
|
||||
return true if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
||||
|
||||
webfinger = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}")
|
||||
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
||||
self_reference = webfinger.link('self')
|
||||
|
||||
return false if self_reference&.href != @uri
|
||||
|
||||
@username = confirmed_username
|
||||
@domain = confirmed_domain
|
||||
|
||||
true
|
||||
rescue Goldfinger::Error
|
||||
false
|
||||
end
|
||||
|
||||
def split_acct(acct)
|
||||
acct.gsub(/\Aacct:/, '').split('@')
|
||||
end
|
||||
|
||||
def supported_context?
|
||||
super(@json)
|
||||
end
|
||||
|
||||
def expected_type?
|
||||
@json['type'] == 'Person'
|
||||
end
|
||||
end
|
47
app/services/activitypub/fetch_remote_key_service.rb
Normal file
47
app/services/activitypub/fetch_remote_key_service.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FetchRemoteKeyService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
# Returns account that owns the key
|
||||
def call(uri, prefetched_json = nil)
|
||||
@json = body_to_json(prefetched_json) || fetch_resource(uri)
|
||||
|
||||
return unless supported_context?(@json) && expected_type?
|
||||
return find_account(uri, @json) if person?
|
||||
|
||||
@owner = fetch_resource(owner_uri)
|
||||
|
||||
return unless supported_context?(@owner) && confirmed_owner?
|
||||
|
||||
find_account(owner_uri, @owner)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_account(uri, prefetched_json)
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_json)
|
||||
account
|
||||
end
|
||||
|
||||
def expected_type?
|
||||
person? || public_key?
|
||||
end
|
||||
|
||||
def person?
|
||||
@json['type'] == 'Person'
|
||||
end
|
||||
|
||||
def public_key?
|
||||
@json['publicKeyPem'].present? && @json['owner'].present?
|
||||
end
|
||||
|
||||
def owner_uri
|
||||
@owner_uri ||= value_or_id(@json['owner'])
|
||||
end
|
||||
|
||||
def confirmed_owner?
|
||||
@owner['type'] == 'Person' && value_or_id(@owner['publicKey']) == @json['id']
|
||||
end
|
||||
end
|
48
app/services/activitypub/fetch_remote_status_service.rb
Normal file
48
app/services/activitypub/fetch_remote_status_service.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FetchRemoteStatusService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
# Should be called when uri has already been checked for locality
|
||||
def call(uri, prefetched_json = nil)
|
||||
@json = body_to_json(prefetched_json) || fetch_resource(uri)
|
||||
|
||||
return unless supported_context?
|
||||
|
||||
activity = activity_json
|
||||
actor_id = value_or_id(activity['actor'])
|
||||
|
||||
return unless expected_type?(activity) && trustworthy_attribution?(uri, actor_id)
|
||||
|
||||
actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account)
|
||||
actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id) if actor.nil?
|
||||
|
||||
ActivityPub::Activity.factory(activity, actor).perform
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activity_json
|
||||
if %w(Note Article).include? @json['type']
|
||||
{
|
||||
'type' => 'Create',
|
||||
'actor' => first_of_value(@json['attributedTo']),
|
||||
'object' => @json,
|
||||
}
|
||||
else
|
||||
@json
|
||||
end
|
||||
end
|
||||
|
||||
def trustworthy_attribution?(uri, attributed_to)
|
||||
Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero?
|
||||
end
|
||||
|
||||
def supported_context?
|
||||
super(@json)
|
||||
end
|
||||
|
||||
def expected_type?(json)
|
||||
%w(Create Announce).include? json['type']
|
||||
end
|
||||
end
|
93
app/services/activitypub/process_account_service.rb
Normal file
93
app/services/activitypub/process_account_service.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ProcessAccountService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
# Should be called with confirmed valid JSON
|
||||
# and WebFinger-resolved username and domain
|
||||
def call(username, domain, json)
|
||||
return unless json['inbox'].present?
|
||||
|
||||
@json = json
|
||||
@uri = @json['id']
|
||||
@username = username
|
||||
@domain = domain
|
||||
@account = Account.find_by(uri: @uri)
|
||||
|
||||
create_account if @account.nil?
|
||||
upgrade_account if @account.ostatus?
|
||||
update_account
|
||||
|
||||
@account
|
||||
rescue Oj::ParseError
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_account
|
||||
@account = Account.new
|
||||
@account.protocol = :activitypub
|
||||
@account.username = @username
|
||||
@account.domain = @domain
|
||||
@account.uri = @uri
|
||||
@account.suspended = true if auto_suspend?
|
||||
@account.silenced = true if auto_silence?
|
||||
@account.private_key = nil
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def update_account
|
||||
@account.last_webfingered_at = Time.now.utc
|
||||
@account.protocol = :activitypub
|
||||
@account.inbox_url = @json['inbox'] || ''
|
||||
@account.outbox_url = @json['outbox'] || ''
|
||||
@account.shared_inbox_url = @json['sharedInbox'] || ''
|
||||
@account.followers_url = @json['followers'] || ''
|
||||
@account.url = @json['url'] || @uri
|
||||
@account.display_name = @json['name'] || ''
|
||||
@account.note = @json['summary'] || ''
|
||||
@account.avatar_remote_url = image_url('icon')
|
||||
@account.header_remote_url = image_url('image')
|
||||
@account.public_key = public_key || ''
|
||||
@account.locked = @json['manuallyApprovesFollowers'] || false
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def upgrade_account
|
||||
ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
|
||||
end
|
||||
|
||||
def image_url(key)
|
||||
value = first_of_value(@json[key])
|
||||
|
||||
return if value.nil?
|
||||
return @json[key]['url'] if @json[key].is_a?(Hash)
|
||||
|
||||
image = fetch_resource(value)
|
||||
image['url'] if image
|
||||
end
|
||||
|
||||
def public_key
|
||||
value = first_of_value(@json['publicKey'])
|
||||
|
||||
return if value.nil?
|
||||
return value['publicKeyPem'] if value.is_a?(Hash)
|
||||
|
||||
key = fetch_resource(value)
|
||||
key['publicKeyPem'] if key
|
||||
end
|
||||
|
||||
def auto_suspend?
|
||||
domain_block && domain_block.suspend?
|
||||
end
|
||||
|
||||
def auto_silence?
|
||||
domain_block && domain_block.silence?
|
||||
end
|
||||
|
||||
def domain_block
|
||||
return @domain_block if defined?(@domain_block)
|
||||
@domain_block = DomainBlock.find_by(domain: @domain)
|
||||
end
|
||||
end
|
48
app/services/activitypub/process_collection_service.rb
Normal file
48
app/services/activitypub/process_collection_service.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ProcessCollectionService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(body, account)
|
||||
@account = account
|
||||
@json = Oj.load(body, mode: :strict)
|
||||
|
||||
return if @account.suspended? || !supported_context?
|
||||
|
||||
return if different_actor? && verify_account!.nil?
|
||||
|
||||
case @json['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
process_items @json['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
process_items @json['orderedItems']
|
||||
else
|
||||
process_items [@json]
|
||||
end
|
||||
rescue Oj::ParseError
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def different_actor?
|
||||
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri && @json['signature'].present?
|
||||
end
|
||||
|
||||
def process_items(items)
|
||||
items.reverse_each.map { |item| process_item(item) }.compact
|
||||
end
|
||||
|
||||
def supported_context?
|
||||
super(@json)
|
||||
end
|
||||
|
||||
def process_item(item)
|
||||
activity = ActivityPub::Activity.factory(item, @account)
|
||||
activity&.perform
|
||||
end
|
||||
|
||||
def verify_account!
|
||||
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
|
||||
end
|
||||
end
|
@@ -1,14 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AuthorizeFollowService < BaseService
|
||||
def call(source_account, target_account)
|
||||
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
|
||||
follow_request.authorize!
|
||||
NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local?
|
||||
def call(source_account, target_account, options = {})
|
||||
if options[:skip_follow_request]
|
||||
follow_request = FollowRequest.new(account: source_account, target_account: target_account)
|
||||
else
|
||||
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
|
||||
follow_request.authorize!
|
||||
end
|
||||
|
||||
create_notification(follow_request) unless source_account.local?
|
||||
follow_request
|
||||
end
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
follow_request,
|
||||
serializer: ActivityPub::AcceptFollowSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(follow_request.target_account))
|
||||
end
|
||||
|
||||
def build_xml(follow_request)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request))
|
||||
end
|
||||
|
@@ -15,19 +15,26 @@ class BatchedRemoveStatusService < BaseService
|
||||
@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
|
||||
|
||||
@stream_entry_batches = []
|
||||
@salmon_batches = []
|
||||
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h
|
||||
@stream_entry_batches = []
|
||||
@salmon_batches = []
|
||||
@activity_json_batches = []
|
||||
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h
|
||||
@activity_json = {}
|
||||
@activity_xml = {}
|
||||
|
||||
# Ensure that rendered XML reflects destroyed state
|
||||
Status.where(id: statuses.map(&:id)).in_batches.destroy_all
|
||||
statuses.each(&:destroy)
|
||||
|
||||
# Batch by source account
|
||||
statuses.group_by(&:account_id).each do |_, account_statuses|
|
||||
account = account_statuses.first.account
|
||||
|
||||
unpush_from_home_timelines(account_statuses)
|
||||
batch_stream_entries(account_statuses) if account.local?
|
||||
|
||||
if account.local?
|
||||
batch_stream_entries(account, account_statuses)
|
||||
batch_activity_json(account, account_statuses)
|
||||
end
|
||||
end
|
||||
|
||||
# Cannot be batched
|
||||
@@ -36,17 +43,32 @@ class BatchedRemoveStatusService < BaseService
|
||||
batch_salmon_slaps(status) if status.local?
|
||||
end
|
||||
|
||||
Pubsubhubbub::DistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
|
||||
Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
|
||||
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
|
||||
ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch_stream_entries(statuses)
|
||||
stream_entry_ids = statuses.map { |s| s.stream_entry.id }
|
||||
def batch_stream_entries(account, statuses)
|
||||
statuses.each do |status|
|
||||
@stream_entry_batches << [build_xml(status.stream_entry), account.id]
|
||||
end
|
||||
end
|
||||
|
||||
stream_entry_ids.each_slice(100) do |batch_of_stream_entry_ids|
|
||||
@stream_entry_batches << [batch_of_stream_entry_ids]
|
||||
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
|
||||
|
||||
@@ -78,11 +100,10 @@ class BatchedRemoveStatusService < BaseService
|
||||
def batch_salmon_slaps(status)
|
||||
return if @mentions[status.id].empty?
|
||||
|
||||
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|
|
||||
@salmon_batches << [payload, status.account_id, recipient_id]
|
||||
@salmon_batches << [build_xml(status.stream_entry), status.account_id, recipient_id]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,4 +132,24 @@ class BatchedRemoveStatusService < BaseService
|
||||
def redis
|
||||
Redis.current
|
||||
end
|
||||
|
||||
def build_json(status)
|
||||
return @activity_json[status.id] if @activity_json.key?(status.id)
|
||||
|
||||
@activity_json[status.id] = sign_json(status, ActiveModelSerializers::SerializableResource.new(
|
||||
status,
|
||||
serializer: status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json)
|
||||
end
|
||||
|
||||
def build_xml(stream_entry)
|
||||
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
||||
|
||||
@activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
|
||||
end
|
||||
|
||||
def sign_json(status, json)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account))
|
||||
end
|
||||
end
|
||||
|
@@ -30,7 +30,7 @@ class BlockDomainService < BaseService
|
||||
|
||||
def suspend_accounts!
|
||||
blocked_domain_accounts.where(suspended: false).find_each do |account|
|
||||
account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
|
||||
UnsubscribeService.new.call(account) if account.subscribed?
|
||||
SuspendAccountService.new.call(account)
|
||||
end
|
||||
end
|
||||
|
@@ -12,11 +12,28 @@ class BlockService < BaseService
|
||||
block = account.block!(target_account)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
block,
|
||||
serializer: ActivityPub::BlockSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(block.account))
|
||||
end
|
||||
|
||||
def build_xml(block)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block))
|
||||
end
|
||||
|
@@ -15,18 +15,32 @@ class FavouriteService < BaseService
|
||||
return favourite unless favourite.nil?
|
||||
|
||||
favourite = Favourite.create!(account: account, status: status)
|
||||
|
||||
if status.local?
|
||||
NotifyService.new.call(favourite.status.account, favourite)
|
||||
else
|
||||
NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id)
|
||||
end
|
||||
|
||||
create_notification(favourite)
|
||||
favourite
|
||||
end
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
favourite,
|
||||
serializer: ActivityPub::LikeSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(favourite.account))
|
||||
end
|
||||
|
||||
def build_xml(favourite)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite))
|
||||
end
|
||||
|
@@ -1,21 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FetchAtomService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(url)
|
||||
return if url.blank?
|
||||
|
||||
response = Request.new(:head, url).perform
|
||||
result = process(url)
|
||||
|
||||
Rails.logger.debug "Remote status HEAD request returned code #{response.code}"
|
||||
# retry without ActivityPub
|
||||
result ||= process(url) if @unsupported_activity
|
||||
|
||||
response = Request.new(:get, url).perform if response.code == 405
|
||||
|
||||
Rails.logger.debug "Remote status GET request returned code #{response.code}"
|
||||
|
||||
return nil if response.code != 200
|
||||
return [url, fetch(url)] if response.mime_type == 'application/atom+xml'
|
||||
return process_headers(url, response) if response['Link'].present?
|
||||
process_html(fetch(url))
|
||||
result
|
||||
rescue OpenSSL::SSL::SSLError => e
|
||||
Rails.logger.debug "SSL error: #{e}"
|
||||
nil
|
||||
@@ -26,27 +22,67 @@ class FetchAtomService < BaseService
|
||||
|
||||
private
|
||||
|
||||
def process_html(body)
|
||||
Rails.logger.debug 'Processing HTML'
|
||||
|
||||
page = Nokogiri::HTML(body)
|
||||
alternate_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' }
|
||||
|
||||
return nil if alternate_link.nil?
|
||||
[alternate_link['href'], fetch(alternate_link['href'])]
|
||||
def process(url, terminal = false)
|
||||
@url = url
|
||||
perform_request
|
||||
process_response(terminal)
|
||||
end
|
||||
|
||||
def process_headers(url, response)
|
||||
Rails.logger.debug 'Processing link header'
|
||||
def perform_request
|
||||
accept = 'text/html'
|
||||
accept = 'application/activity+json, application/ld+json, application/atom+xml, ' + accept unless @unsupported_activity
|
||||
|
||||
link_header = LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link'])
|
||||
alternate_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
|
||||
|
||||
return process_html(fetch(url)) if alternate_link.nil?
|
||||
[alternate_link.href, fetch(alternate_link.href)]
|
||||
@response = Request.new(:get, @url)
|
||||
.add_headers('Accept' => accept)
|
||||
.perform
|
||||
end
|
||||
|
||||
def fetch(url)
|
||||
Request.new(:get, url).perform.to_s
|
||||
def process_response(terminal = false)
|
||||
return nil if @response.code != 200
|
||||
|
||||
if @response.mime_type == 'application/atom+xml'
|
||||
[@url, @response.to_s, :ostatus]
|
||||
elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@response.mime_type)
|
||||
if supported_activity?(@response.to_s)
|
||||
[@url, @response.to_s, :activitypub]
|
||||
else
|
||||
@unsupported_activity = true
|
||||
nil
|
||||
end
|
||||
elsif @response['Link'] && !terminal
|
||||
process_headers
|
||||
elsif @response.mime_type == 'text/html' && !terminal
|
||||
process_html
|
||||
end
|
||||
end
|
||||
|
||||
def process_html
|
||||
page = Nokogiri::HTML(@response.to_s)
|
||||
|
||||
json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) }
|
||||
atom_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' }
|
||||
|
||||
result ||= process(json_link['href'], terminal: true) unless json_link.nil? || @unsupported_activity
|
||||
result ||= process(atom_link['href'], terminal: true) unless atom_link.nil?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def process_headers
|
||||
link_header = LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
|
||||
|
||||
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
|
||||
atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
|
||||
|
||||
result ||= process(json_link.href, terminal: true) unless json_link.nil? || @unsupported_activity
|
||||
result ||= process(atom_link.href, terminal: true) unless atom_link.nil?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def supported_activity?(body)
|
||||
json = body_to_json(body)
|
||||
return false unless supported_context?(json)
|
||||
json['type'] == 'Person' ? json['inbox'].present? : true
|
||||
end
|
||||
end
|
||||
|
@@ -4,29 +4,45 @@ class FetchLinkCardService < BaseService
|
||||
URL_PATTERN = %r{https?://\S+}
|
||||
|
||||
def call(status)
|
||||
# Get first http/https URL that isn't local
|
||||
url = parse_urls(status)
|
||||
@status = status
|
||||
@url = parse_urls
|
||||
|
||||
return if url.nil?
|
||||
return if @url.nil? || @status.preview_cards.any?
|
||||
|
||||
url = url.to_s
|
||||
card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url)
|
||||
res = Request.new(:head, url).perform
|
||||
@url = @url.to_s
|
||||
|
||||
return if res.code != 200 || res.mime_type != 'text/html'
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
@card = PreviewCard.find_by(url: @url)
|
||||
process_url if @card.nil?
|
||||
end
|
||||
end
|
||||
|
||||
attempt_opengraph(card, url) unless attempt_oembed(card, url)
|
||||
attach_card unless @card.nil?
|
||||
rescue HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_urls(status)
|
||||
if status.local?
|
||||
urls = status.text.match(URL_PATTERN).to_a.map { |uri| Addressable::URI.parse(uri).normalize }
|
||||
def process_url
|
||||
@card = PreviewCard.new(url: @url)
|
||||
res = Request.new(:head, @url).perform
|
||||
|
||||
return if res.code != 200 || res.mime_type != 'text/html'
|
||||
|
||||
attempt_oembed || attempt_opengraph
|
||||
end
|
||||
|
||||
def attach_card
|
||||
@status.preview_cards << @card
|
||||
end
|
||||
|
||||
def parse_urls
|
||||
if @status.local?
|
||||
urls = @status.text.match(URL_PATTERN).to_a.map { |uri| Addressable::URI.parse(uri).normalize }
|
||||
else
|
||||
html = Nokogiri::HTML(status.text)
|
||||
html = Nokogiri::HTML(@status.text)
|
||||
links = html.css('a')
|
||||
urls = links.map { |a| Addressable::URI.parse(a['href']).normalize unless skip_link?(a) }.compact
|
||||
end
|
||||
@@ -44,41 +60,41 @@ class FetchLinkCardService < BaseService
|
||||
a['rel']&.include?('tag') || a['class']&.include?('u-url')
|
||||
end
|
||||
|
||||
def attempt_oembed(card, url)
|
||||
response = OEmbed::Providers.get(url)
|
||||
def attempt_oembed
|
||||
response = OEmbed::Providers.get(@url)
|
||||
|
||||
card.type = response.type
|
||||
card.title = response.respond_to?(:title) ? response.title : ''
|
||||
card.author_name = response.respond_to?(:author_name) ? response.author_name : ''
|
||||
card.author_url = response.respond_to?(:author_url) ? response.author_url : ''
|
||||
card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
|
||||
card.provider_url = response.respond_to?(:provider_url) ? response.provider_url : ''
|
||||
card.width = 0
|
||||
card.height = 0
|
||||
@card.type = response.type
|
||||
@card.title = response.respond_to?(:title) ? response.title : ''
|
||||
@card.author_name = response.respond_to?(:author_name) ? response.author_name : ''
|
||||
@card.author_url = response.respond_to?(:author_url) ? response.author_url : ''
|
||||
@card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
|
||||
@card.provider_url = response.respond_to?(:provider_url) ? response.provider_url : ''
|
||||
@card.width = 0
|
||||
@card.height = 0
|
||||
|
||||
case card.type
|
||||
case @card.type
|
||||
when 'link'
|
||||
card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
|
||||
@card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
|
||||
when 'photo'
|
||||
card.url = response.url
|
||||
card.width = response.width.presence || 0
|
||||
card.height = response.height.presence || 0
|
||||
@card.url = response.url
|
||||
@card.width = response.width.presence || 0
|
||||
@card.height = response.height.presence || 0
|
||||
when 'video'
|
||||
card.width = response.width.presence || 0
|
||||
card.height = response.height.presence || 0
|
||||
card.html = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
|
||||
@card.width = response.width.presence || 0
|
||||
@card.height = response.height.presence || 0
|
||||
@card.html = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
|
||||
when 'rich'
|
||||
# Most providers rely on <script> tags, which is a no-no
|
||||
return false
|
||||
end
|
||||
|
||||
card.save_with_optional_image!
|
||||
@card.save_with_optional_image!
|
||||
rescue OEmbed::NotFound
|
||||
false
|
||||
end
|
||||
|
||||
def attempt_opengraph(card, url)
|
||||
response = Request.new(:get, url).perform
|
||||
def attempt_opengraph
|
||||
response = Request.new(:get, @url).perform
|
||||
|
||||
return if response.code != 200 || response.mime_type != 'text/html'
|
||||
|
||||
@@ -88,19 +104,23 @@ class FetchLinkCardService < BaseService
|
||||
detector.strip_tags = true
|
||||
|
||||
guess = detector.detect(html, response.charset)
|
||||
page = Nokogiri::HTML(html, nil, guess&.fetch(:encoding))
|
||||
page = Nokogiri::HTML(html, nil, guess&.fetch(:encoding))
|
||||
|
||||
card.type = :link
|
||||
card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content
|
||||
card.description = meta_property(page, 'og:description') || meta_property(page, 'description')
|
||||
card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
|
||||
@card.type = :link
|
||||
@card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content || ''
|
||||
@card.description = meta_property(page, 'og:description') || meta_property(page, 'description') || ''
|
||||
@card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
|
||||
|
||||
return if card.title.blank?
|
||||
return if @card.title.blank?
|
||||
|
||||
card.save_with_optional_image!
|
||||
@card.save_with_optional_image!
|
||||
end
|
||||
|
||||
def meta_property(html, property)
|
||||
html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
|
||||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "fetch:#{@url}" }
|
||||
end
|
||||
end
|
||||
|
@@ -3,16 +3,20 @@
|
||||
class FetchRemoteAccountService < BaseService
|
||||
include AuthorExtractor
|
||||
|
||||
def call(url, prefetched_body = nil)
|
||||
def call(url, prefetched_body = nil, protocol = :ostatus)
|
||||
if prefetched_body.nil?
|
||||
atom_url, body = FetchAtomService.new.call(url)
|
||||
resource_url, body, protocol = FetchAtomService.new.call(url)
|
||||
else
|
||||
atom_url = url
|
||||
body = prefetched_body
|
||||
resource_url = url
|
||||
body = prefetched_body
|
||||
end
|
||||
|
||||
return nil if atom_url.nil?
|
||||
process_atom(atom_url, body)
|
||||
case protocol
|
||||
when :ostatus
|
||||
process_atom(resource_url, body)
|
||||
when :activitypub
|
||||
ActivityPub::FetchRemoteAccountService.new.call(resource_url, body)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FetchRemoteResourceService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
attr_reader :url
|
||||
|
||||
def call(url)
|
||||
@@ -14,11 +16,11 @@ class FetchRemoteResourceService < BaseService
|
||||
private
|
||||
|
||||
def process_url
|
||||
case xml_root
|
||||
when 'feed'
|
||||
FetchRemoteAccountService.new.call(atom_url, body)
|
||||
when 'entry'
|
||||
FetchRemoteStatusService.new.call(atom_url, body)
|
||||
case type
|
||||
when 'Person'
|
||||
FetchRemoteAccountService.new.call(atom_url, body, protocol)
|
||||
when 'Note'
|
||||
FetchRemoteStatusService.new.call(atom_url, body, protocol)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,7 +33,26 @@ class FetchRemoteResourceService < BaseService
|
||||
end
|
||||
|
||||
def body
|
||||
fetched_atom_feed.last
|
||||
fetched_atom_feed.second
|
||||
end
|
||||
|
||||
def protocol
|
||||
fetched_atom_feed.third
|
||||
end
|
||||
|
||||
def type
|
||||
return json_data['type'] if protocol == :activitypub
|
||||
|
||||
case xml_root
|
||||
when 'feed'
|
||||
'Person'
|
||||
when 'entry'
|
||||
'Note'
|
||||
end
|
||||
end
|
||||
|
||||
def json_data
|
||||
@_json_data ||= body_to_json(body)
|
||||
end
|
||||
|
||||
def xml_root
|
||||
|
@@ -3,16 +3,20 @@
|
||||
class FetchRemoteStatusService < BaseService
|
||||
include AuthorExtractor
|
||||
|
||||
def call(url, prefetched_body = nil)
|
||||
def call(url, prefetched_body = nil, protocol = :ostatus)
|
||||
if prefetched_body.nil?
|
||||
atom_url, body = FetchAtomService.new.call(url)
|
||||
resource_url, body, protocol = FetchAtomService.new.call(url)
|
||||
else
|
||||
atom_url = url
|
||||
body = prefetched_body
|
||||
resource_url = url
|
||||
body = prefetched_body
|
||||
end
|
||||
|
||||
return nil if atom_url.nil?
|
||||
process_atom(atom_url, body)
|
||||
case protocol
|
||||
when :ostatus
|
||||
process_atom(resource_url, body)
|
||||
when :activitypub
|
||||
ActivityPub::FetchRemoteStatusService.new.call(resource_url, body)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@@ -14,7 +14,7 @@ class FollowService < BaseService
|
||||
|
||||
return if source_account.following?(target_account)
|
||||
|
||||
if target_account.locked?
|
||||
if target_account.locked? || target_account.activitypub?
|
||||
request_follow(source_account, target_account)
|
||||
else
|
||||
direct_follow(source_account, target_account)
|
||||
@@ -28,9 +28,11 @@ class FollowService < BaseService
|
||||
|
||||
if target_account.local?
|
||||
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)
|
||||
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
|
||||
|
||||
follow_request
|
||||
@@ -63,4 +65,12 @@ class FollowService < BaseService
|
||||
def build_follow_xml(follow)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow))
|
||||
end
|
||||
|
||||
def build_json(follow_request)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
follow_request,
|
||||
serializer: ActivityPub::FollowSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(follow_request.account))
|
||||
end
|
||||
end
|
||||
|
@@ -42,6 +42,8 @@ class PostStatusService < BaseService
|
||||
# match both with and without U+FE0F (the emoji variation selector)
|
||||
unless /👁\ufe0f?\z/.match?(status.content)
|
||||
Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
|
||||
ActivityPub::DistributionWorker.perform_async(status.id)
|
||||
ActivityPub::ReplyDistributionWorker.perform_async(status.id) if status.reply? && status.thread.account.local?
|
||||
end
|
||||
|
||||
if options[:idempotency].present?
|
||||
|
@@ -67,10 +67,13 @@ class ProcessInteractionService < BaseService
|
||||
|
||||
def follow!(account, target_account)
|
||||
follow = account.follow!(target_account)
|
||||
FollowRequest.find_by(account: account, target_account: target_account)&.destroy
|
||||
NotifyService.new.call(target_account, follow)
|
||||
end
|
||||
|
||||
def follow_request!(account, target_account)
|
||||
return if account.requested?(target_account)
|
||||
|
||||
follow_request = FollowRequest.create!(account: account, target_account: target_account)
|
||||
NotifyService.new.call(target_account, follow_request)
|
||||
end
|
||||
@@ -88,6 +91,7 @@ class ProcessInteractionService < BaseService
|
||||
|
||||
def unfollow!(account, target_account)
|
||||
account.unfollow!(target_account)
|
||||
FollowRequest.find_by(account: account, target_account: target_account)&.destroy
|
||||
end
|
||||
|
||||
def reflect_block!(account, target_account)
|
||||
|
@@ -28,18 +28,32 @@ class ProcessMentionsService < BaseService
|
||||
end
|
||||
|
||||
status.mentions.includes(:account).each do |mention|
|
||||
mentioned_account = mention.account
|
||||
|
||||
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
|
||||
create_notification(status, mention)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_notification(status, mention)
|
||||
mentioned_account = mention.account
|
||||
|
||||
if mentioned_account.local?
|
||||
NotifyService.new.call(mentioned_account, mention)
|
||||
elsif mentioned_account.ostatus? && (Rails.configuration.x.use_ostatus_privacy || !status.stream_entry.hidden?)
|
||||
NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
|
||||
elsif mentioned_account.activitypub? && !mentioned_account.following?(status.account)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url)
|
||||
end
|
||||
end
|
||||
|
||||
def build_json(status)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
status,
|
||||
serializer: ActivityPub::ActivitySerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(status.account))
|
||||
end
|
||||
|
||||
def follow_remote_account_service
|
||||
@follow_remote_account_service ||= ResolveRemoteAccountService.new
|
||||
end
|
||||
|
@@ -20,17 +20,35 @@ class ReblogService < BaseService
|
||||
reblog = account.statuses.create!(reblog: reblogged_status, text: '')
|
||||
|
||||
DistributionWorker.perform_async(reblog.id)
|
||||
|
||||
unless /👁$/.match?(reblogged_status.content)
|
||||
Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
|
||||
ActivityPub::DistributionWorker.perform_async(reblog.id)
|
||||
end
|
||||
|
||||
|
||||
if reblogged_status.local?
|
||||
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
|
||||
|
||||
create_notification(reblog)
|
||||
reblog
|
||||
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? && !reblogged_status.account.following?(reblog.account)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
||||
end
|
||||
end
|
||||
|
||||
def build_json(reblog)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
reblog,
|
||||
serializer: ActivityPub::ActivitySerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(reblog.account))
|
||||
end
|
||||
end
|
||||
|
@@ -4,11 +4,28 @@ class RejectFollowService < BaseService
|
||||
def call(source_account, target_account)
|
||||
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
|
||||
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
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
follow_request,
|
||||
serializer: ActivityPub::RejectFollowSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(follow_request.target_account))
|
||||
end
|
||||
|
||||
def build_xml(follow_request)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request))
|
||||
end
|
||||
|
@@ -22,8 +22,8 @@ class RemoveStatusService < BaseService
|
||||
|
||||
return unless @account.local?
|
||||
|
||||
remove_from_mentioned(@stream_entry.reload)
|
||||
Pubsubhubbub::DistributionWorker.perform_async(@stream_entry.id)
|
||||
remove_from_remote_followers
|
||||
remove_from_remote_affected
|
||||
end
|
||||
|
||||
private
|
||||
@@ -38,13 +38,50 @@ class RemoveStatusService < BaseService
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_mentioned(stream_entry)
|
||||
salmon_xml = stream_entry_to_xml(stream_entry)
|
||||
target_accounts = @mentions.map(&:account).reject(&:local?).uniq(&:domain)
|
||||
def remove_from_remote_affected
|
||||
# People who got mentioned in the status, or who
|
||||
# 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|
|
||||
[salmon_xml, stream_entry.account_id, target_account.id]
|
||||
target_accounts = (@mentions.map(&:account).reject(&:local?) + @reblogs.map(&:account).reject(&:local?)).uniq(&:id)
|
||||
|
||||
# Ostatus
|
||||
NotificationWorker.push_bulk(target_accounts.select(&:ostatus?).uniq(&:domain)) do |target_account|
|
||||
[salmon_xml, @account.id, target_account.id]
|
||||
end
|
||||
|
||||
# ActivityPub
|
||||
ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:inbox_url)) do |target_account|
|
||||
[signed_activity_json, @account.id, target_account.inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_remote_followers
|
||||
# OStatus
|
||||
Pubsubhubbub::RawDistributionWorker.perform_async(salmon_xml, @account.id)
|
||||
|
||||
# ActivityPub
|
||||
ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url|
|
||||
[signed_activity_json, @account.id, inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def salmon_xml
|
||||
@salmon_xml ||= stream_entry_to_xml(@stream_entry)
|
||||
end
|
||||
|
||||
def signed_activity_json
|
||||
@signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account))
|
||||
end
|
||||
|
||||
def activity_json
|
||||
@activity_json ||= ActiveModelSerializers::SerializableResource.new(
|
||||
@status,
|
||||
serializer: @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json
|
||||
end
|
||||
|
||||
def remove_reblogs
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
class ResolveRemoteAccountService < BaseService
|
||||
include OStatus2::MagicKey
|
||||
include JsonLdHelper
|
||||
|
||||
DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
|
||||
|
||||
@@ -12,6 +13,7 @@ class ResolveRemoteAccountService < BaseService
|
||||
# @return [Account]
|
||||
def call(uri, update_profile = true, redirected = nil)
|
||||
@username, @domain = uri.split('@')
|
||||
@update_profile = update_profile
|
||||
|
||||
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
||||
|
||||
@@ -42,10 +44,11 @@ class ResolveRemoteAccountService < BaseService
|
||||
if lock.acquired?
|
||||
@account = Account.find_remote(@username, @domain)
|
||||
|
||||
create_account if @account.nil?
|
||||
update_account
|
||||
|
||||
update_account_profile if update_profile
|
||||
if activitypub_ready?
|
||||
handle_activitypub
|
||||
else
|
||||
handle_ostatus
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,18 +61,46 @@ class ResolveRemoteAccountService < BaseService
|
||||
private
|
||||
|
||||
def links_missing?
|
||||
@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? ||
|
||||
!(activitypub_ready? || ostatus_ready?)
|
||||
end
|
||||
|
||||
def ostatus_ready?
|
||||
!(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? ||
|
||||
@webfinger.link('salmon').nil? ||
|
||||
@webfinger.link('http://webfinger.net/rel/profile-page').nil? ||
|
||||
@webfinger.link('magic-public-key').nil? ||
|
||||
canonical_uri.nil? ||
|
||||
hub_url.nil?
|
||||
hub_url.nil?)
|
||||
end
|
||||
|
||||
def webfinger_update_due?
|
||||
@account.nil? || @account.last_webfingered_at.nil? || @account.last_webfingered_at <= 1.day.ago
|
||||
end
|
||||
|
||||
def activitypub_ready?
|
||||
!@webfinger.link('self').nil? &&
|
||||
['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) &&
|
||||
actor_json['inbox'].present?
|
||||
end
|
||||
|
||||
def handle_ostatus
|
||||
create_account if @account.nil?
|
||||
update_account
|
||||
update_account_profile if update_profile?
|
||||
end
|
||||
|
||||
def update_profile?
|
||||
@update_profile
|
||||
end
|
||||
|
||||
def handle_activitypub
|
||||
return if actor_json.nil?
|
||||
|
||||
@account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json)
|
||||
rescue Oj::ParseError
|
||||
nil
|
||||
end
|
||||
|
||||
def create_account
|
||||
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
|
||||
|
||||
@@ -81,6 +112,7 @@ class ResolveRemoteAccountService < BaseService
|
||||
|
||||
def update_account
|
||||
@account.last_webfingered_at = Time.now.utc
|
||||
@account.protocol = :ostatus
|
||||
@account.remote_url = atom_url
|
||||
@account.salmon_url = salmon_url
|
||||
@account.url = url
|
||||
@@ -111,6 +143,10 @@ class ResolveRemoteAccountService < BaseService
|
||||
@salmon_url ||= @webfinger.link('salmon').href
|
||||
end
|
||||
|
||||
def actor_url
|
||||
@actor_url ||= @webfinger.link('self').href
|
||||
end
|
||||
|
||||
def url
|
||||
@url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href
|
||||
end
|
||||
@@ -149,6 +185,13 @@ class ResolveRemoteAccountService < BaseService
|
||||
@atom_body = response.to_s
|
||||
end
|
||||
|
||||
def actor_json
|
||||
return @actor_json if defined?(@actor_json)
|
||||
|
||||
json = fetch_resource(actor_url)
|
||||
@actor_json = supported_context?(json) && json['type'] == 'Person' ? json : nil
|
||||
end
|
||||
|
||||
def atom
|
||||
return @atom if defined?(@atom)
|
||||
@atom = Nokogiri::XML(atom_body)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
class SubscribeService < BaseService
|
||||
def call(account)
|
||||
return unless account.ostatus?
|
||||
return if account.hub_url.blank?
|
||||
|
||||
@account = account
|
||||
@account.secret = SecureRandom.hex
|
||||
@@ -42,7 +42,7 @@ class SubscribeService < BaseService
|
||||
end
|
||||
|
||||
def some_local_account
|
||||
@some_local_account ||= Account.local.first
|
||||
@some_local_account ||= Account.local.where(suspended: false).first
|
||||
end
|
||||
|
||||
# Any response in the 3xx or 4xx range, except for 429 (rate limit)
|
||||
|
@@ -5,11 +5,28 @@ class UnblockService < BaseService
|
||||
return unless account.blocking?(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
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
unblock,
|
||||
serializer: ActivityPub::UndoBlockSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(unblock.account))
|
||||
end
|
||||
|
||||
def build_xml(block)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block))
|
||||
end
|
||||
|
@@ -4,14 +4,30 @@ class UnfavouriteService < BaseService
|
||||
def call(account, status)
|
||||
favourite = Favourite.find_by!(account: account, status: status)
|
||||
favourite.destroy!
|
||||
|
||||
NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) unless status.local?
|
||||
|
||||
create_notification(favourite) unless status.local?
|
||||
favourite
|
||||
end
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
favourite,
|
||||
serializer: ActivityPub::UndoLikeSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(favourite.account))
|
||||
end
|
||||
|
||||
def build_xml(favourite)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite))
|
||||
end
|
||||
|
@@ -5,14 +5,51 @@ class UnfollowService < BaseService
|
||||
# @param [Account] source_account Where to unfollow from
|
||||
# @param [Account] target_account Which to unfollow
|
||||
def call(source_account, target_account)
|
||||
follow = source_account.unfollow!(target_account)
|
||||
return unless follow
|
||||
NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local?
|
||||
UnmergeWorker.perform_async(target_account.id, source_account.id)
|
||||
@source_account = source_account
|
||||
@target_account = target_account
|
||||
|
||||
unfollow! || undo_follow_request!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unfollow!
|
||||
follow = Follow.find_by(account: @source_account, target_account: @target_account)
|
||||
|
||||
return unless follow
|
||||
|
||||
follow.destroy!
|
||||
create_notification(follow) unless @target_account.local?
|
||||
UnmergeWorker.perform_async(@target_account.id, @source_account.id)
|
||||
follow
|
||||
end
|
||||
|
||||
def undo_follow_request!
|
||||
follow_request = FollowRequest.find_by(account: @source_account, target_account: @target_account)
|
||||
|
||||
return unless follow_request
|
||||
|
||||
follow_request.destroy!
|
||||
create_notification(follow_request) unless @target_account.local?
|
||||
follow_request
|
||||
end
|
||||
|
||||
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)
|
||||
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
|
||||
follow,
|
||||
serializer: ActivityPub::UndoFollowSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(follow.account))
|
||||
end
|
||||
|
||||
def build_xml(follow)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow))
|
||||
end
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
class UnsubscribeService < BaseService
|
||||
def call(account)
|
||||
return unless account.ostatus?
|
||||
return if account.hub_url.blank?
|
||||
|
||||
@account = account
|
||||
@response = build_request.perform
|
||||
|
21
app/services/update_account_service.rb
Normal file
21
app/services/update_account_service.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UpdateAccountService < BaseService
|
||||
def call(account, params, raise_error: false)
|
||||
was_locked = account.locked
|
||||
update_method = raise_error ? :update! : :update
|
||||
account.send(update_method, params).tap do |ret|
|
||||
next unless ret
|
||||
authorize_all_follow_requests(account) if was_locked && !account.locked
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_all_follow_requests(account)
|
||||
follow_requests = FollowRequest.where(target_account: account)
|
||||
AuthorizeFollowWorker.push_bulk(follow_requests) do |req|
|
||||
[req.account_id, req.target_account_id]
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user