Drop OStatus support. Fix some of the Rspec tests.
parent
6e8ec7f0a5
commit
6c374b5153
1
Gemfile
1
Gemfile
|
@ -62,7 +62,6 @@ gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
|
|||
gem 'nokogiri', '~> 1.10'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.7'
|
||||
gem 'ostatus2', '~> 2.0'
|
||||
gem 'ox', '~> 2.10'
|
||||
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
||||
gem 'pundit', '~> 2.0'
|
||||
|
|
|
@ -32,13 +32,6 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
format.atom do
|
||||
mark_cacheable!
|
||||
|
||||
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
|
||||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? || entry.status.local_only? }))
|
||||
end
|
||||
|
||||
format.rss do
|
||||
mark_cacheable!
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||
if unknown_deleted_account?
|
||||
head 202
|
||||
elsif signed_request_account
|
||||
upgrade_account
|
||||
process_payload
|
||||
head 202
|
||||
else
|
||||
|
@ -38,16 +37,6 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||
@body
|
||||
end
|
||||
|
||||
def upgrade_account
|
||||
if signed_request_account.ostatus?
|
||||
signed_request_account.update(last_webfingered_at: nil)
|
||||
ResolveAccountWorker.perform_async(signed_request_account.acct)
|
||||
end
|
||||
|
||||
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
|
||||
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
|
||||
end
|
||||
|
||||
def process_payload
|
||||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
|
||||
end
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
module Admin
|
||||
class AccountsController < BaseController
|
||||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
||||
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
||||
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
||||
before_action :require_remote_account!, only: [:redownload]
|
||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||
|
||||
def index
|
||||
|
@ -19,18 +19,6 @@ module Admin
|
|||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
end
|
||||
|
||||
def subscribe
|
||||
authorize @account, :subscribe?
|
||||
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
authorize @account, :unsubscribe?
|
||||
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def memorialize
|
||||
authorize @account, :memorialize?
|
||||
@account.memorialize!
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::PushController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
def update
|
||||
response, status = process_push_request
|
||||
render plain: response, status: status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_push_request
|
||||
case hub_mode
|
||||
when 'subscribe'
|
||||
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
|
||||
when 'unsubscribe'
|
||||
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
|
||||
else
|
||||
["Unknown mode: #{hub_mode}", 422]
|
||||
end
|
||||
end
|
||||
|
||||
def hub_mode
|
||||
params['hub.mode']
|
||||
end
|
||||
|
||||
def hub_topic
|
||||
params['hub.topic']
|
||||
end
|
||||
|
||||
def hub_callback
|
||||
params['hub.callback']
|
||||
end
|
||||
|
||||
def hub_lease_seconds
|
||||
params['hub.lease_seconds']
|
||||
end
|
||||
|
||||
def hub_secret
|
||||
params['hub.secret']
|
||||
end
|
||||
|
||||
def account_from_topic
|
||||
if hub_topic.present? && local_domain? && account_feed_path?
|
||||
Account.find_local(hub_topic_params[:username])
|
||||
end
|
||||
end
|
||||
|
||||
def hub_topic_params
|
||||
@_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
|
||||
end
|
||||
|
||||
def hub_topic_uri
|
||||
@_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
|
||||
end
|
||||
|
||||
def local_domain?
|
||||
TagManager.instance.web_domain?(hub_topic_domain)
|
||||
end
|
||||
|
||||
def verified_domain
|
||||
return signed_request_account.domain if signed_request_account
|
||||
end
|
||||
|
||||
def hub_topic_domain
|
||||
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
|
||||
end
|
||||
|
||||
def account_feed_path?
|
||||
hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::SalmonController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
before_action :set_account
|
||||
respond_to :txt
|
||||
|
||||
def update
|
||||
if verify_payload?
|
||||
process_salmon
|
||||
head 202
|
||||
elsif payload.present?
|
||||
render plain: signature_verification_failure_reason, status: 401
|
||||
else
|
||||
head 400
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def payload
|
||||
@_payload ||= request.body.read
|
||||
end
|
||||
|
||||
def verify_payload?
|
||||
payload.present? && VerifySalmonService.new.call(payload)
|
||||
end
|
||||
|
||||
def process_salmon
|
||||
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
|
||||
end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::SubscriptionsController < Api::BaseController
|
||||
before_action :set_account
|
||||
respond_to :txt
|
||||
|
||||
def show
|
||||
if subscription.valid?(params['hub.topic'])
|
||||
@account.update(subscription_expires_at: future_expires)
|
||||
render plain: encoded_challenge, status: 200
|
||||
else
|
||||
head 404
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
|
||||
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
|
||||
end
|
||||
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subscription
|
||||
@_subscription ||= @account.subscription(
|
||||
api_subscription_url(@account.id)
|
||||
)
|
||||
end
|
||||
|
||||
def body
|
||||
@_body ||= request.body.read
|
||||
end
|
||||
|
||||
def encoded_challenge
|
||||
HTMLEntities.new.encode(params['hub.challenge'])
|
||||
end
|
||||
|
||||
def future_expires
|
||||
Time.now.utc + lease_seconds_or_default
|
||||
end
|
||||
|
||||
def lease_seconds_or_default
|
||||
(params['hub.lease_seconds'] || 1.day).to_i.seconds
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -30,7 +30,6 @@ module AccountControllerConcern
|
|||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
webfinger_account_link,
|
||||
atom_account_url_link,
|
||||
actor_url_link,
|
||||
]
|
||||
)
|
||||
|
@ -47,13 +46,6 @@ module AccountControllerConcern
|
|||
]
|
||||
end
|
||||
|
||||
def atom_account_url_link
|
||||
[
|
||||
account_url(@account, format: 'atom'),
|
||||
[%w(rel alternate), %w(type application/atom+xml)],
|
||||
]
|
||||
end
|
||||
|
||||
def actor_url_link
|
||||
[
|
||||
ActivityPub::TagManager.instance.uri_for(@account),
|
||||
|
|
|
@ -145,7 +145,7 @@ module SignatureVerification
|
|||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
return if account.local?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -181,7 +181,6 @@ class StatusesController < ApplicationController
|
|||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
|
||||
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
|
||||
]
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ class StreamEntriesController < ApplicationController
|
|||
before_action :check_account_suspension
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
@ -24,15 +25,6 @@ class StreamEntriesController < ApplicationController
|
|||
|
||||
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
|
||||
end
|
||||
|
||||
format.atom do
|
||||
unless @stream_entry.hidden?
|
||||
skip_session!
|
||||
expires_in 3.minutes, public: true
|
||||
end
|
||||
|
||||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -49,7 +41,6 @@ class StreamEntriesController < ApplicationController
|
|||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
|
||||
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
|
||||
]
|
||||
)
|
||||
|
|
|
@ -119,8 +119,7 @@ class ActivityPub::Activity
|
|||
def crawl_links(status)
|
||||
return if status.spoiler_text?
|
||||
|
||||
# Spread out crawling randomly to avoid DDoSing the link
|
||||
LinkCrawlWorker.perform_in(rand(1..59).seconds, status.id)
|
||||
LinkCrawlWorker.perform_async(status.id)
|
||||
end
|
||||
|
||||
def distribute_to_followers(status)
|
||||
|
|
|
@ -47,8 +47,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
def find_existing_status
|
||||
status = status_from_uri(object_uri)
|
||||
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
|
||||
status
|
||||
end
|
||||
|
||||
def process_status_params
|
||||
|
|
|
@ -27,7 +27,6 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
end
|
||||
|
||||
@status = Status.find_by(uri: object_uri, account: @account)
|
||||
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
return if @status.nil?
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
|
|||
return if object_uri.nil?
|
||||
|
||||
status = Status.find_by(uri: object_uri, account: @account)
|
||||
status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
if status.nil?
|
||||
delete_later!(object_uri)
|
||||
|
|
|
@ -15,8 +15,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
|
||||
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' } },
|
||||
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
|
||||
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
|
||||
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
|
||||
conversation: { 'ostatus' => 'http://ostatus.org#', 'conversation' => 'ostatus:conversation' },
|
||||
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
|
||||
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
|
||||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
|
|
|
@ -498,7 +498,6 @@ class Bangtags
|
|||
case post_cmd[0]
|
||||
when 'mention'
|
||||
mention = @account.mentions.where(status: status).first_or_create(status: status)
|
||||
LocalNotificationWorker.perform_async(@account.id, mention.id, mention.class.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::Base
|
||||
include Redisable
|
||||
|
||||
def initialize(xml, account = nil, **options)
|
||||
@xml = xml
|
||||
@account = account
|
||||
@options = options
|
||||
end
|
||||
|
||||
def status?
|
||||
[:activity, :note, :comment].include?(type)
|
||||
end
|
||||
|
||||
def verb
|
||||
raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content
|
||||
OStatus::TagManager::VERBS.key(raw)
|
||||
rescue
|
||||
:post
|
||||
end
|
||||
|
||||
def type
|
||||
raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content
|
||||
OStatus::TagManager::TYPES.key(raw)
|
||||
rescue
|
||||
:activity
|
||||
end
|
||||
|
||||
def id
|
||||
@xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content
|
||||
end
|
||||
|
||||
def url
|
||||
link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' }
|
||||
link.nil? ? nil : link['href']
|
||||
end
|
||||
|
||||
def activitypub_uri
|
||||
link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) }
|
||||
link.nil? ? nil : link['href']
|
||||
end
|
||||
|
||||
def activitypub_uri?
|
||||
activitypub_uri.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_status(uri)
|
||||
if OStatus::TagManager.instance.local_id?(uri)
|
||||
local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')
|
||||
return Status.find_by(id: local_id)
|
||||
elsif ActivityPub::TagManager.instance.local_uri?(uri)
|
||||
local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri)
|
||||
return Status.find_by(id: local_id)
|
||||
end
|
||||
|
||||
Status.find_by(uri: uri)
|
||||
end
|
||||
|
||||
def find_activitypub_status(uri, href)
|
||||
tag_matches = /tag:([^,:]+)[^:]*:objectId=([\d]+)/.match(uri)
|
||||
href_matches = %r{/users/([^/]+)}.match(href)
|
||||
|
||||
unless tag_matches.nil? || href_matches.nil?
|
||||
uri = "https://#{tag_matches[1]}/users/#{href_matches[1]}/statuses/#{tag_matches[2]}"
|
||||
Status.find_by(uri: uri)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,219 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::Creation < OStatus::Activity::Base
|
||||
def perform
|
||||
if redis.exists("delete_upon_arrival:#{@account.id}:#{id}")
|
||||
Rails.logger.debug "Delete for status #{id} was queued, ignoring"
|
||||
return [nil, false]
|
||||
end
|
||||
|
||||
return [nil, false] if @account.suspended? || invalid_origin?
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
# Return early if status already exists in db
|
||||
@status = find_status(id)
|
||||
return [@status, false] unless @status.nil?
|
||||
@status = process_status
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
end
|
||||
|
||||
[@status, true]
|
||||
end
|
||||
|
||||
def process_status
|
||||
Rails.logger.debug "Creating remote status #{id}"
|
||||
cached_reblog = reblog
|
||||
status = nil
|
||||
|
||||
# Skip if the reblogged status is not public
|
||||
return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?)
|
||||
|
||||
media_attachments = save_media.take(6)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
status = Status.create!(
|
||||
uri: id,
|
||||
url: url,
|
||||
account: @account,
|
||||
reblog: cached_reblog,
|
||||
text: content,
|
||||
spoiler_text: content_warning,
|
||||
created_at: published,
|
||||
override_timestamps: @options[:override_timestamps],
|
||||
reply: thread?,
|
||||
language: content_language,
|
||||
visibility: visibility_scope,
|
||||
conversation: find_or_create_conversation,
|
||||
thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil,
|
||||
media_attachment_ids: media_attachments.map(&:id),
|
||||
sensitive: sensitive?
|
||||
)
|
||||
|
||||
save_mentions(status)
|
||||
save_hashtags(status)
|
||||
save_emojis(status)
|
||||
end
|
||||
|
||||
if thread? && status.thread.nil? && Request.valid_url?(thread.second)
|
||||
Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}"
|
||||
ThreadResolveWorker.perform_async(status.id, thread.second)
|
||||
end
|
||||
|
||||
Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
|
||||
|
||||
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
|
||||
|
||||
# Only continue if the status is supposed to have arrived in real-time.
|
||||
# Note that if @options[:override_timestamps] isn't set, the status
|
||||
# may have a lower snowflake id than other existing statuses, potentially
|
||||
# "hiding" it from paginated API calls
|
||||
return status unless @options[:override_timestamps] || status.within_realtime_window?
|
||||
|
||||
DistributionWorker.perform_async(status.id)
|
||||
|
||||
status
|
||||
end
|
||||
|
||||
def content
|
||||
@xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content
|
||||
end
|
||||
|
||||
def content_language
|
||||
@xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en'
|
||||
end
|
||||
|
||||
def content_warning
|
||||
@xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || ''
|
||||
end
|
||||
|
||||
def visibility_scope
|
||||
@xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public
|
||||
end
|
||||
|
||||
def published
|
||||
@xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content
|
||||
end
|
||||
|
||||
def thread?
|
||||
!@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil?
|
||||
end
|
||||
|
||||
def thread
|
||||
thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS)
|
||||
[thr['ref'], thr['href']]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sensitive?
|
||||
# OStatus-specific convention (not standard)
|
||||
@xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' }
|
||||
end
|
||||
|
||||
def find_or_create_conversation
|
||||
uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content
|
||||
return if uri.nil?
|
||||
|
||||
if OStatus::TagManager.instance.local_id?(uri)
|
||||
local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')
|
||||
return Conversation.find_by(id: local_id)
|
||||
end
|
||||
|
||||
Conversation.find_by(uri: uri) || Conversation.create!(uri: uri)
|
||||
end
|
||||
|
||||
def save_mentions(parent)
|
||||
processed_account_ids = []
|
||||
|
||||
@xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
|
||||
next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type']
|
||||
|
||||
mentioned_account = account_from_href(link['href'])
|
||||
|
||||
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
|
||||
|
||||
mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
|
||||
|
||||
# So we can skip duplicate mentions
|
||||
processed_account_ids << mentioned_account.id
|
||||
end
|
||||
end
|
||||
|
||||
def save_hashtags(parent)
|
||||
tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
|
||||
ProcessHashtagsService.new.call(parent, tags)
|
||||
end
|
||||
|
||||
def save_media
|
||||
do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
||||
media_attachments = []
|
||||
|
||||
@xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
|
||||
next unless link['href']
|
||||
|
||||
media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href'])
|
||||
parsed_url = Addressable::URI.parse(link['href']).normalize
|
||||
|
||||
next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
|
||||
|
||||
media.save
|
||||
media_attachments << media
|
||||
|
||||
next if do_not_download
|
||||
|
||||
begin
|
||||
media.file_remote_url = link['href']
|
||||
media.save!
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
media_attachments
|
||||
end
|
||||
|
||||
def save_emojis(parent)
|
||||
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
|
||||
|
||||
return if do_not_download
|
||||
|
||||
@xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
|
||||
next unless link['href'] && link['name']
|
||||
|
||||
shortcode = link['name'].delete(':')
|
||||
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain)
|
||||
|
||||
next unless emoji.nil?
|
||||
|
||||
emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain)
|
||||
emoji.image_remote_url = link['href']
|
||||
emoji.save
|
||||
end
|
||||
end
|
||||
|
||||
def account_from_href(href)
|
||||
url = Addressable::URI.parse(href).normalize
|
||||
|
||||
if TagManager.instance.web_domain?(url.host)
|
||||
Account.find_local(url.path.gsub('/users/', ''))
|
||||
else
|
||||
Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href)
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_origin?
|
||||
return false unless id.start_with?('http') # Legacy IDs cannot be checked
|
||||
|
||||
needle = Addressable::URI.parse(id).normalized_host
|
||||
|
||||
!(needle.casecmp(@account.domain).zero? ||
|
||||
needle.casecmp(Addressable::URI.parse(@account.remote_url.presence || @account.uri).normalized_host).zero?)
|
||||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "create:#{id}" }
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::Deletion < OStatus::Activity::Base
|
||||
def perform
|
||||
Rails.logger.debug "Deleting remote status #{id}"
|
||||
|
||||
status = Status.find_by(uri: id, account: @account)
|
||||
status ||= Status.find_by(uri: activitypub_uri, account: @account) if activitypub_uri?
|
||||
|
||||
if status.nil?
|
||||
redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id)
|
||||
else
|
||||
RemoveStatusService.new.call(status)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::General < OStatus::Activity::Base
|
||||
def specialize
|
||||
special_class&.new(@xml, @account, @options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def special_class
|
||||
case verb
|
||||
when :post
|
||||
OStatus::Activity::Post
|
||||
when :share
|
||||
OStatus::Activity::Share
|
||||
when :delete
|
||||
OStatus::Activity::Deletion
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::Post < OStatus::Activity::Creation
|
||||
def perform
|
||||
status, just_created = super
|
||||
|
||||
if just_created
|
||||
status.mentions.includes(:account).each do |mention|
|
||||
mentioned_account = mention.account
|
||||
next unless mentioned_account.local?
|
||||
NotifyService.new.call(mentioned_account, mention)
|
||||
end
|
||||
end
|
||||
|
||||
status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reblog
|
||||
nil
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::Remote < OStatus::Activity::Base
|
||||
def perform
|
||||
if activitypub_uri?
|
||||
find_status(activitypub_uri) || FetchRemoteStatusService.new.call(url)
|
||||
else
|
||||
find_status(id) || FetchRemoteStatusService.new.call(url)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::Activity::Share < OStatus::Activity::Creation
|
||||
def perform
|
||||
return if reblog.nil?
|
||||
|
||||
status, just_created = super
|
||||
NotifyService.new.call(reblog.account, status) if reblog.account.local? && just_created
|
||||
status
|
||||
end
|
||||
|
||||
def object
|
||||
@xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reblog
|
||||
return @reblog if defined? @reblog
|
||||
|
||||
original_status = OStatus::Activity::Remote.new(object).perform
|
||||
return if original_status.nil?
|
||||
|
||||
@reblog = original_status.reblog? ? original_status.reblog : original_status
|
||||
end
|
||||
end
|
|
@ -1,378 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OStatus::AtomSerializer
|
||||
include RoutingHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
class << self
|
||||
def render(element)
|
||||
document = Ox::Document.new(version: '1.0')
|
||||
document << element
|
||||
('<?xml version="1.0"?>' + Ox.dump(element, effort: :tolerant)).force_encoding('UTF-8')
|
||||
end
|
||||
end
|
||||
|
||||
def author(account)
|
||||
author = Ox::Element.new('author')
|
||||
|
||||
uri = OStatus::TagManager.instance.uri_for(account)
|
||||
|
||||
append_element(author, 'id', uri)
|
||||
append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person])
|
||||
append_element(author, 'uri', uri)
|
||||
append_element(author, 'name', account.username)
|
||||
append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
|
||||
append_element(author, 'summary', Formatter.instance.simplified_format(account).to_str, type: :html) if account.note?
|
||||
append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account))
|
||||
append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) if account.avatar?
|
||||
append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) if account.header?
|
||||
account.emojis.each do |emoji|
|
||||
append_element(author, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
|
||||
end
|
||||
append_element(author, 'poco:preferredUsername', account.username)
|
||||
append_element(author, 'poco:displayName', account.display_name) if account.display_name?
|
||||
append_element(author, 'poco:note', account.local? ? account.note : strip_tags(account.note)) if account.note?
|
||||
append_element(author, 'mastodon:scope', account.locked? ? :private : :public)
|
||||
|
||||
author
|
||||
end
|
||||
|
||||
def feed(account, stream_entries)
|
||||
feed = Ox::Element.new('feed')
|
||||
|
||||
add_namespaces(feed)
|
||||
|
||||
append_element(feed, 'id', account_url(account, format: 'atom'))
|
||||
append_element(feed, 'title', account.display_name.presence || account.username)
|
||||
append_element(feed, 'subtitle', account.note)
|
||||
append_element(feed, 'updated', account.updated_at.iso8601)
|
||||
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
|
||||
|
||||
feed << author(account)
|
||||
|
||||
append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account))
|
||||
append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
|
||||
append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20
|
||||
append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
|
||||
append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id))
|
||||
|
||||
stream_entries.each do |stream_entry|
|
||||
feed << entry(stream_entry)
|
||||
end
|
||||
|
||||
feed
|
||||
end
|
||||
|
||||
def entry(stream_entry, root = false)
|
||||
entry = Ox::Element.new('entry')
|
||||
|
||||
add_namespaces(entry) if root
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status))
|
||||
append_element(entry, 'published', stream_entry.created_at.iso8601)
|
||||
append_element(entry, 'updated', stream_entry.updated_at.iso8601)
|
||||
append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status")
|
||||
|
||||
entry << author(stream_entry.account) if root
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb])
|
||||
|
||||
entry << object(stream_entry.target) if stream_entry.targeted?
|
||||
|
||||
if stream_entry.status.nil?
|
||||
append_element(entry, 'content', 'Deleted status')
|
||||
elsif stream_entry.status.destroyed?
|
||||
append_element(entry, 'content', 'Deleted status')
|
||||
append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(stream_entry.status)) if stream_entry.account.local?
|
||||
else
|
||||
serialize_status_attributes(entry, stream_entry.status)
|
||||
end
|
||||
|
||||
append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(stream_entry.status))
|
||||
append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
|
||||
append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: ::TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
|
||||
append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?
|
||||
|
||||
entry
|
||||
end
|
||||
|
||||
def object(status)
|
||||
object = Ox::Element.new('activity:object')
|
||||
|
||||
append_element(object, 'id', OStatus::TagManager.instance.uri_for(status))
|
||||
append_element(object, 'published', status.created_at.iso8601)
|
||||
append_element(object, 'updated', status.updated_at.iso8601)
|
||||
append_element(object, 'title', status.title)
|
||||
|
||||
object << author(status.account)
|
||||
|
||||
append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type])
|
||||
append_element(object, 'activity:verb', OStatus::TagManager::VERBS[status.verb])
|
||||
|
||||
serialize_status_attributes(object, status)
|
||||
|
||||
append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(status))
|
||||
append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: ::TagManager.instance.url_for(status.thread)) unless status.thread.nil?
|
||||
append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil?
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
def follow_salmon(follow)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{follow.account.acct} started following #{follow.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(follow.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow])
|
||||
|
||||
object = author(follow.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def follow_request_salmon(follow_request)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest'))
|
||||
append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}")
|
||||
|
||||
entry << author(follow_request.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
|
||||
|
||||
object = author(follow_request.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def authorize_follow_request_salmon(follow_request)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
|
||||
append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}")
|
||||
|
||||
entry << author(follow_request.target_account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:authorize])
|
||||
|
||||
object = Ox::Element.new('activity:object')
|
||||
object << author(follow_request.account)
|
||||
|
||||
append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
|
||||
|
||||
inner_object = author(follow_request.target_account)
|
||||
inner_object.value = 'activity:object'
|
||||
|
||||
object << inner_object
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def reject_follow_request_salmon(follow_request)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
|
||||
append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}")
|
||||
|
||||
entry << author(follow_request.target_account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:reject])
|
||||
|
||||
object = Ox::Element.new('activity:object')
|
||||
object << author(follow_request.account)
|
||||
|
||||
append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend])
|
||||
|
||||
inner_object = author(follow_request.target_account)
|
||||
inner_object.value = 'activity:object'
|
||||
|
||||
object << inner_object
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def unfollow_salmon(follow)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(follow.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow])
|
||||
|
||||
object = author(follow.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def block_salmon(block)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
|
||||
append_element(entry, 'title', description)
|
||||
|
||||
entry << author(block.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block])
|
||||
|
||||
object = author(block.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def unblock_salmon(block)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{block.account.acct} no longer blocks #{block.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
|
||||
append_element(entry, 'title', description)
|
||||
|
||||
entry << author(block.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock])
|
||||
|
||||
object = author(block.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def favourite_salmon(favourite)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(favourite.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:favorite])
|
||||
|
||||
entry << object(favourite.status)
|
||||
|
||||
append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status))
|
||||
|
||||
entry
|
||||
end
|
||||
|
||||
def unfavourite_salmon(favourite)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
|
||||
|
||||
append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(favourite.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfavorite])
|
||||
|
||||
entry << object(favourite.status)
|
||||
|
||||
append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status))
|
||||
|
||||
entry
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def append_element(parent, name, content = nil, **attributes)
|
||||
element = Ox::Element.new(name)
|
||||
attributes.each { |k, v| element[k] = sanitize_str(v) }
|
||||
element << sanitize_str(content) unless content.nil?
|
||||
parent << element
|
||||
end
|
||||
|
||||
def sanitize_str(raw_str)
|
||||
raw_str.to_s
|
||||
end
|
||||
|
||||
def conversation_uri(conversation)
|
||||
return conversation.uri if conversation.uri?
|
||||
OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation')
|
||||
end
|
||||
|
||||
def add_namespaces(parent)
|
||||
parent['xmlns'] = OStatus::TagManager::XMLNS
|
||||
parent['xmlns:thr'] = OStatus::TagManager::THR_XMLNS
|
||||
parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS
|
||||
parent['xmlns:poco'] = OStatus::TagManager::POCO_XMLNS
|
||||
parent['xmlns:media'] = OStatus::TagManager::MEDIA_XMLNS
|
||||
parent['xmlns:ostatus'] = OStatus::TagManager::OS_XMLNS
|
||||
parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS
|
||||
end
|
||||
|
||||
def serialize_status_attributes(entry, status)
|
||||
append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local?
|
||||
|
||||
append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
|
||||
append_element(entry, 'content', Formatter.instance.format(status, inline_poll_options: true).to_str || '.', type: 'html', 'xml:lang': status.language)
|
||||
|
||||
status.active_mentions.sort_by(&:id).each do |mentioned|
|
||||
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account))
|
||||
end
|
||||
|
||||
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility?
|
||||
|
||||
status.tags.each do |tag|
|
||||
append_element(entry, 'category', nil, term: tag.name)
|
||||
end
|
||||
|
||||
status.media_attachments.each do |media|
|
||||
append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false)))
|
||||
end
|
||||
|
||||
append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any?
|
||||
append_element(entry, 'mastodon:scope', status.visibility)
|
||||
|
||||
status.emojis.each do |emoji|
|
||||
append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -321,10 +321,6 @@ class Account < ApplicationRecord
|
|||
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
|
||||
end
|
||||
|
||||
def subscription(webhook_url)
|
||||
@subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
|
||||
end
|
||||
|
||||
def save_with_optional_media!
|
||||
save!
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
|
|
|
@ -52,8 +52,6 @@ class Form::AccountBatch
|
|||
def reject_follow!(follow)
|
||||
follow.destroy
|
||||
|
||||
return unless follow.account.activitypub?
|
||||
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
follow,
|
||||
serializer: ActivityPub::RejectFollowSerializer,
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoteProfile
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_reader :document
|
||||
|
||||
def initialize(body)
|
||||
@document = Nokogiri::XML.parse(body, nil, 'utf-8')
|
||||
end
|
||||
|
||||
def root
|
||||
@root ||= document.at_xpath('/atom:feed|/atom:entry', atom: OStatus::TagManager::XMLNS)
|
||||
end
|
||||
|
||||
def author
|
||||
@author ||= root.at_xpath('./atom:author|./dfrn:owner', atom: OStatus::TagManager::XMLNS, dfrn: OStatus::TagManager::DFRN_XMLNS)
|
||||
end
|
||||
|
||||
def hub_link
|
||||
@hub_link ||= link_href_from_xml(root, 'hub')
|
||||
end
|
||||
|
||||
def display_name
|
||||
@display_name ||= author.at_xpath('./poco:displayName', poco: OStatus::TagManager::POCO_XMLNS)&.content
|
||||
end
|
||||
|
||||
def note
|
||||
@note ||= author.at_xpath('./atom:summary|./poco:note', atom: OStatus::TagManager::XMLNS, poco: OStatus::TagManager::POCO_XMLNS)&.content
|
||||
end
|
||||
|
||||
def scope
|
||||
@scope ||= author.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content
|
||||
end
|
||||
|
||||
def avatar
|
||||
@avatar ||= link_href_from_xml(author, 'avatar')
|
||||
end
|
||||
|
||||
def header
|
||||
@header ||= link_href_from_xml(author, 'header')
|
||||
end
|
||||
|
||||
def emojis
|
||||
@emojis ||= author.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS)
|
||||
end
|
||||
|
||||
def locked?
|
||||
scope == 'private'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_href_from_xml(xml, type)
|
||||
xml.at_xpath(%(./atom:link[@rel="#{type}"]/@href), atom: OStatus::TagManager::XMLNS)&.content
|
||||
end
|
||||
end
|
|
@ -5,7 +5,6 @@ class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
|||
|
||||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object?
|
||||
attribute :proper_uri, key: :object, unless: :serialize_object?
|
||||
attribute :atom_uri, if: :announce?
|
||||
|
||||
def id
|
||||
ActivityPub::TagManager.instance.activity_uri_for(object)
|
||||
|
@ -35,10 +34,6 @@ class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
|||
ActivityPub::TagManager.instance.uri_for(object.proper)
|
||||
end
|
||||
|
||||
def atom_uri
|
||||
OStatus::TagManager.instance.uri_for(object)
|
||||
end
|
||||
|
||||
def announce?
|
||||
object.reblog?
|
||||
end
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
class ActivityPub::DeleteSerializer < ActivityPub::Serializer
|
||||
class TombstoneSerializer < ActivityPub::Serializer
|
||||
context_extensions :atom_uri
|
||||
|
||||
attributes :id, :type, :atom_uri
|
||||
attributes :id, :type
|
||||
|
||||
def id
|
||||
ActivityPub::TagManager.instance.uri_for(object)
|
||||
|
@ -13,10 +11,6 @@ class ActivityPub::DeleteSerializer < ActivityPub::Serializer
|
|||
def type
|
||||
'Tombstone'
|
||||
end
|
||||
|
||||
def atom_uri
|
||||
OStatus::TagManager.instance.uri_for(object)
|
||||
end
|
||||
end
|
||||
|
||||
attributes :id, :type, :actor, :to
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
context_extensions :atom_uri, :conversation, :sensitive,
|
||||
context_extensions :conversation, :sensitive,
|
||||
:hashtag, :emoji, :focal_point, :blurhash
|
||||
|
||||
attributes :id, :type, :summary,
|
||||
:in_reply_to, :published, :url,
|
||||
:attributed_to, :to, :cc, :sensitive,
|
||||
:atom_uri, :in_reply_to_atom_uri,
|
||||
:conversation, :source
|
||||
|
||||
attribute :content
|
||||
|
@ -102,18 +101,6 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
object.active_mentions.to_a.sort_by(&:id) + object.tags.reject { |t| t.local || t.private } + object.emojis
|
||||
end
|
||||
|
||||
def atom_uri
|
||||
return unless object.local?
|
||||
|
||||
OStatus::TagManager.instance.uri_for(object)
|
||||
end
|
||||
|
||||
def in_reply_to_atom_uri
|
||||
return unless object.reply? && !object.thread.nil?
|
||||
|
||||
OStatus::TagManager.instance.uri_for(object.thread)
|
||||
end
|
||||
|
||||
def conversation
|
||||
return if object.conversation.nil?
|
||||
|
||||
|
|
|
@ -16,11 +16,8 @@ class WebfingerSerializer < ActiveModel::Serializer
|
|||
def links
|
||||
[
|
||||
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
|
||||
{ rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
|
||||
{ rel: 'self', type: 'application/activity+json', href: account_url(object) },
|
||||
{ rel: 'salmon', href: api_salmon_url(object.id) },
|
||||
{ rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" },
|
||||
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
if lock.acquired?
|
||||
@account = Account.find_remote(@username, @domain)
|
||||
@old_public_key = @account&.public_key
|
||||
@old_protocol = @account&.protocol
|
||||
|
||||
create_account if @account.nil?
|
||||
update_account
|
||||
|
@ -32,7 +31,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
|
||||
return if @account.nil?
|
||||
|
||||
after_protocol_change! if protocol_changed?
|
||||
after_key_change! if key_changed? && !@options[:signed_with_known_key]
|
||||
clear_tombstones! if key_changed?
|
||||
|
||||
|
@ -50,7 +48,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
|
||||
def create_account
|
||||
@account = Account.new
|
||||
@account.protocol = :activitypub
|
||||
@account.username = @username
|
||||
@account.domain = @domain
|
||||
@account.private_key = nil
|
||||
|
@ -60,7 +57,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
|
||||
def update_account
|
||||
@account.last_webfingered_at = Time.now.utc unless @options[:only_key]
|
||||
@account.protocol = :activitypub
|
||||
|
||||
set_immediate_attributes!
|
||||
set_fetchable_attributes! unless @options[:only_keys]
|
||||
|
@ -94,10 +90,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.moved_to_account = @json['movedTo'].present? ? moved_account : nil
|
||||
end
|
||||
|
||||
def after_protocol_change!
|
||||
ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
|
||||
end
|
||||
|
||||
def after_key_change!
|
||||
RefollowWorker.perform_async(@account.id)
|
||||
end
|
||||
|
@ -216,10 +208,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
Tombstone.where(account_id: @account.id).delete_all
|
||||
end
|
||||
|
||||
def protocol_changed?
|
||||
!@old_protocol.nil? && @old_protocol != @account.protocol
|
||||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "process_account:#{@uri}" }
|
||||
end
|
||||
|
|
|
@ -29,8 +29,6 @@ class AfterBlockDomainFromAccountService < BaseService
|
|||
def reject_follow!(follow)
|
||||
follow.destroy
|
||||
|
||||
return unless follow.account.activitypub?
|
||||
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
follow,
|
||||
serializer: ActivityPub::RejectFollowSerializer,
|
||||
|
|
|
@ -16,11 +16,7 @@ class AuthorizeFollowService < BaseService
|
|||
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
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(follow_request)
|
||||
|
@ -30,8 +26,4 @@ class AuthorizeFollowService < BaseService
|
|||
adapter: ActivityPub::Adapter
|
||||
).to_json
|
||||
end
|
||||
|
||||
def build_xml(follow_request)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BatchedRemoveStatusService < BaseService
|
||||
include StreamEntryRenderer
|
||||
include Redisable
|
||||
|
||||
# Delete given statuses and reblogs of them
|
||||
# Dispatch PuSH updates of the deleted statuses, but only local ones
|
||||
# Dispatch Salmon deletes, unique per domain, of the deleted statuses, but only local ones
|
||||
# Remove statuses from home feeds
|
||||
# Push delete events to streaming API for home feeds and public feeds
|
||||
# @param [Status] statuses A preferably batched array of statuses
|
||||
|
@ -18,10 +15,7 @@ class BatchedRemoveStatusService < BaseService
|
|||
@mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a }
|
||||
@tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) }
|
||||
|
||||
@stream_entry_batches = []
|
||||
@salmon_batches = []
|
||||
@json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) }
|
||||
@activity_xml = {}
|
||||
|
||||
# Ensure that rendered XML reflects destroyed state
|
||||
statuses.each do |status|
|
||||
|
@ -39,29 +33,17 @@ class BatchedRemoveStatusService < BaseService
|
|||
|
||||
unpush_from_home_timelines(account, account_statuses)
|
||||
unpush_from_list_timelines(account, account_statuses)
|
||||
|
||||
batch_stream_entries(account, account_statuses) if account.local?
|
||||
end
|
||||
|
||||
# Cannot be batched
|
||||
statuses.each do |status|
|
||||
unpush_from_public_timelines(status)
|
||||
unpush_from_direct_timelines(status) if status.direct_visibility?
|
||||
batch_salmon_slaps(status) if status.local?
|
||||
end
|
||||
|
||||
Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
|
||||
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch_stream_entries(account, statuses)
|
||||
statuses.each do |status|
|
||||
@stream_entry_batches << [build_xml(status.stream_entry), account.id]
|
||||
end
|
||||
end
|
||||
|
||||
def unpush_from_home_timelines(account, statuses)
|
||||
recipients = account.followers_for_local_distribution.to_a
|
||||
|
||||
|
@ -112,20 +94,4 @@ class BatchedRemoveStatusService < BaseService
|
|||
redis.publish("timeline:direct:#{status.account.id}", payload) if status.account.local?
|
||||
end
|
||||
end
|
||||
|
||||
def batch_salmon_slaps(status)
|
||||
return if @mentions[status.id].empty?
|
||||
|
||||
recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id)
|
||||
|
||||
recipients.each do |recipient_id|
|
||||
@salmon_batches << [build_xml(status.stream_entry), status.account_id, recipient_id]
|
||||
end
|
||||
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
|
||||
end
|
||||
|
|
|
@ -18,11 +18,7 @@ class BlockService < BaseService
|
|||
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
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(block)
|
||||
|
@ -32,8 +28,4 @@ class BlockService < BaseService
|
|||
adapter: ActivityPub::Adapter
|
||||
).to_json
|
||||
end
|
||||
|
||||
def build_xml(block)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AuthorExtractor
|
||||
def author_from_xml(xml, update_profile = true)
|
||||
return nil if xml.nil?
|
||||
|
||||
# Try <email> for acct
|
||||
acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: OStatus::TagManager::XMLNS)&.content
|
||||
|
||||
# Try <name> + <uri>
|
||||
if acct.blank?
|
||||
username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: OStatus::TagManager::XMLNS)&.content
|
||||
uri = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: OStatus::TagManager::XMLNS)&.content
|
||||
|
||||
return nil if username.blank? || uri.blank?
|
||||
|
||||
domain = Addressable::URI.parse(uri).normalized_host
|
||||
acct = "#{username}@#{domain}"
|
||||
end
|
||||
|
||||
ResolveAccountService.new.call(acct, update_profile: update_profile)
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StreamEntryRenderer
|
||||
def stream_entry_to_xml(stream_entry)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(stream_entry, true))
|
||||
end
|
||||
end
|
|
@ -30,9 +30,7 @@ class FavouriteService < BaseService
|
|||
|
||||
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?
|
||||
else
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
|
||||
end
|
||||
end
|
||||
|
@ -51,10 +49,6 @@ class FavouriteService < BaseService
|
|||
).as_json).sign!(favourite.account))
|
||||
end
|
||||
|
||||
def build_xml(favourite)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite))
|
||||
end
|
||||
|
||||
def curate_status(status)
|
||||
return if status.curated || !status.distributable? || (status.reply? && status.in_reply_to_account_id != status.account_id)
|
||||
status.curated = true
|
||||
|
|
|
@ -7,11 +7,6 @@ class FetchAtomService < BaseService
|
|||
return if url.blank?
|
||||
|
||||
result = process(url)
|
||||
|
||||
# retry without ActivityPub
|
||||
result ||= process(url) if @unsupported_activity
|
||||
|
||||
result
|
||||
rescue OpenSSL::SSL::SSLError => e
|
||||
Rails.logger.debug "SSL error: #{e}"
|
||||
nil
|
||||
|
@ -28,8 +23,7 @@ class FetchAtomService < BaseService
|
|||
end
|
||||
|
||||
def perform_request(&block)
|
||||
accept = 'text/html'
|
||||
accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity
|
||||
accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html'
|
||||
|
||||
Request.new(:get, @url).add_headers('Accept' => accept).perform(&block)
|
||||
end
|
||||
|
@ -37,17 +31,14 @@ class FetchAtomService < BaseService
|
|||
def process_response(response, terminal = false)
|
||||
return nil if response.code != 200
|
||||
|
||||
if response.mime_type == 'application/atom+xml'
|
||||
[@url, { prefetched_body: response.body_with_limit }, :ostatus]
|
||||
elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
|
||||
if ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
|
||||
body = response.body_with_limit
|
||||
json = body_to_json(body)
|
||||
if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?
|
||||
[json['id'], { prefetched_body: body, id: true }, :activitypub]
|
||||
[json['id'], { prefetched_body: body, id: true }]
|
||||
elsif supported_context?(json) && expected_type?(json)
|
||||
[json['id'], { prefetched_body: body, id: true }, :activitypub]
|
||||
[json['id'], { prefetched_body: body, id: true }]
|
||||
else
|
||||
@unsupported_activity = true
|
||||
nil
|
||||
end
|
||||
elsif !terminal
|
||||
|
@ -69,21 +60,17 @@ class FetchAtomService < BaseService
|
|||
page = Nokogiri::HTML(response.body_with_limit)
|
||||
|
||||
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 = process(json_link['href'], terminal: true) unless json_link.nil?
|
||||
result ||= nil
|
||||
result
|
||||
end
|
||||
|
||||
def process_link_headers(link_header)
|
||||
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 = process(json_link.href, terminal: true) unless json_link.nil?
|
||||
result ||= nil
|
||||
result
|
||||
end
|
||||
|
||||
|
|
|
@ -1,45 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FetchRemoteAccountService < BaseService
|
||||
include AuthorExtractor
|
||||
|
||||
def call(url, prefetched_body = nil, protocol = :ostatus)
|
||||
def call(url, prefetched_body = nil)
|
||||
if prefetched_body.nil?
|
||||
resource_url, resource_options, protocol = FetchAtomService.new.call(url)
|
||||
resource_url, resource_options = FetchAtomService.new.call(url)
|
||||
else
|
||||
resource_url = url
|
||||
resource_options = { prefetched_body: prefetched_body }
|
||||
end
|
||||
|
||||
case protocol
|
||||
when :ostatus
|
||||
process_atom(resource_url, **resource_options)
|
||||
when :activitypub
|
||||
ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_atom(url, prefetched_body:)
|
||||
xml = Nokogiri::XML(prefetched_body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: OStatus::TagManager::XMLNS), false)
|
||||
|
||||
UpdateRemoteProfileService.new.call(xml, account) if account.present? && trusted_domain?(url, account)
|
||||
|
||||
account
|
||||
rescue TypeError
|
||||
Rails.logger.debug "Unparseable URL given: #{url}"
|
||||
nil
|
||||
rescue Nokogiri::XML::XPath::SyntaxError
|
||||
Rails.logger.debug 'Invalid XML or missing namespace'
|
||||
nil
|
||||
end
|
||||
|
||||
def trusted_domain?(url, account)
|
||||
domain = Addressable::URI.parse(url).normalized_host
|
||||
domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
|
||||
return if resource_url.blank?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,45 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FetchRemoteStatusService < BaseService
|
||||
include AuthorExtractor
|
||||
|
||||
def call(url, prefetched_body = nil, protocol = :ostatus)
|
||||
def call(url, prefetched_body = nil)
|
||||
if prefetched_body.nil?
|
||||
resource_url, resource_options, protocol = FetchAtomService.new.call(url)
|
||||
resource_url, resource_options = FetchAtomService.new.call(url)
|
||||
else
|
||||
resource_url = url
|
||||
resource_options = { prefetched_body: prefetched_body }
|
||||
end
|
||||
|
||||
case protocol
|
||||
when :ostatus
|
||||
process_atom(resource_url, **resource_options)
|
||||
when :activitypub
|
||||
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_atom(url, prefetched_body:)
|
||||
Rails.logger.debug "Processing Atom for remote status at #{url}"
|
||||
|
||||
xml = Nokogiri::XML(prefetched_body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS))
|
||||
domain = Addressable::URI.parse(url).normalized_host
|
||||
|
||||
return nil unless !account.nil? && confirmed_domain?(domain, account)
|
||||
|
||||
statuses = ProcessFeedService.new.call(prefetched_body, account)
|
||||
statuses.first
|
||||
rescue Nokogiri::XML::XPath::SyntaxError
|
||||
Rails.logger.debug 'Invalid XML or missing namespace'
|
||||
nil
|
||||
end
|
||||
|
||||
def confirmed_domain?(domain, account)
|
||||
account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
|
||||
return if resource_url.blank?
|
||||
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,10 +29,12 @@ class FollowService < BaseService
|
|||
|
||||
ActivityTracker.increment('activity:interactions')
|
||||
|
||||
if target_account.locked? || target_account.activitypub?
|
||||
request_follow(source_account, target_account, reblogs: reblogs)
|
||||
if target_account.local? && !target_account.locked?
|
||||
follow = source_account.follow!(target_account, reblogs: reblogs)
|
||||
LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
|
||||
follow
|
||||
else
|
||||
direct_follow(source_account, target_account, reblogs: reblogs)
|
||||
request_follow(source_account, target_account, reblogs: reblogs)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -43,40 +45,13 @@ class FollowService < BaseService
|
|||
|
||||
if target_account.local?
|
||||
LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
|
||||
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?
|
||||
else
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url)
|
||||
end
|
||||
|
||||
follow_request
|
||||
end
|
||||
|
||||
def direct_follow(source_account, target_account, reblogs: true)
|
||||
follow = source_account.follow!(target_account, reblogs: reblogs)
|
||||
|
||||
if target_account.local?
|
||||
LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
|
||||
else
|
||||
Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
|
||||
NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
|
||||
AfterRemoteFollowWorker.perform_async(follow.id)
|
||||
end
|
||||
|
||||
MergeWorker.perform_async(target_account.id, source_account.id)
|
||||
|
||||
follow
|
||||
end
|
||||
|
||||
def build_follow_request_xml(follow_request)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
|
||||
end
|
||||
|
||||
def build_follow_xml(follow)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow))
|
||||
end
|
||||
|
||||
def build_json(follow_request)
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
follow_request,
|
||||
|
|
|
@ -103,7 +103,6 @@ class PostStatusService < BaseService
|
|||
DistributionWorker.perform_async(@status.id)
|
||||
|
||||
unless @status.local_only?
|
||||
Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
|
||||
ActivityPub::DistributionWorker.perform_async(@status.id)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessFeedService < BaseService
|
||||
def call(body, account, **options)
|
||||
@options = options
|
||||
|
||||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
update_author(body, account)
|
||||
process_entries(xml, account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_author(body, account)
|
||||
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true)
|
||||
end
|
||||
|
||||
def process_entries(xml, account)
|
||||
xml.xpath('//xmlns:entry', xmlns: OStatus::TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact
|
||||
end
|
||||
|
||||
def process_entry(xml, account)
|
||||
activity = OStatus::Activity::General.new(xml, account, @options)
|
||||
activity.specialize&.perform if activity.status?
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.debug "Nothing was saved for #{activity.id} because: #{e}"
|
||||
nil
|
||||
end
|
||||
end
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
class ProcessHashtagsService < BaseService
|
||||
def call(status, tags = [])
|
||||
tags = Extractor.extract_hashtags(status.text) if tags.blank? && status.local?
|
||||
if status.local?
|
||||
tags = Extractor.extract_hashtags(status.text) | (tags.nil? ? [] : tags)
|
||||
end
|
||||
records = []
|
||||
|
||||
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
|
||||
name = name.gsub(/::+/, ':')
|
||||
next if name.blank?
|
||||
component_indices = name.size.times.select {|i| name[i] == ':'}
|
||||
component_indices << name.size - 1
|
||||
component_indices.take(6).each_with_index do |i, nest|
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessInteractionService < BaseService
|
||||
include AuthorExtractor
|
||||
include Authorization
|
||||
|
||||
# Record locally the remote interaction with our user
|
||||
# @param [String] envelope Salmon envelope
|
||||
# @param [Account] target_account Account the Salmon was addressed to
|
||||
def call(envelope, target_account)
|
||||
body = salmon.unpack(envelope)
|
||||
|
||||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS))
|
||||
|
||||
return if account.nil? || account.suspended?
|
||||
|
||||
if salmon.verify(envelope, account.keypair)
|
||||
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true)
|
||||
|
||||
case verb(xml)
|
||||
when :follow
|
||||
follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain)
|
||||
when :request_friend
|
||||
follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain)
|
||||
when :authorize
|
||||
authorize_follow_request!(account, target_account)
|
||||
when :reject
|
||||
reject_follow_request!(account, target_account)
|
||||
when :unfollow
|
||||
unfollow!(account, target_account)
|
||||
when :favorite
|
||||
favourite!(xml, account)
|
||||
when :unfavorite
|
||||
unfavourite!(xml, account)
|
||||
when :post
|
||||
add_post!(body, account) if mentions_account?(xml, target_account)
|
||||
when :share
|
||||
add_post!(body, account) unless status(xml).nil?
|
||||
when :delete
|
||||
delete_post!(xml, account)
|
||||
when :block
|
||||
reflect_block!(account, target_account)
|
||||
when :unblock
|
||||
reflect_unblock!(account, target_account)
|
||||
end
|
||||
end
|
||||
rescue HTTP::Error, OStatus2::BadSalmonError, Mastodon::NotPermittedError
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mentions_account?(xml, account)
|
||||
xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each { |mention_link| return true if [OStatus::TagManager.instance.uri_for(account), OStatus::TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) }
|
||||
false
|
||||
end
|
||||
|
||||
def verb(xml)
|
||||
raw = xml.at_xpath('//activity:verb', activity: OStatus::TagManager::AS_XMLNS).content
|
||||
OStatus::TagManager::VERBS.key(raw)
|
||||
rescue
|
||||
:post
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def authorize_follow_request!(account, target_account)
|
||||
follow_request = FollowRequest.find_by(account: target_account, target_account: account)
|
||||
follow_request&.authorize!
|
||||
Pubsubhubbub::SubscribeWorker.perform_async(account.id) unless account.subscribed?
|
||||
end
|
||||
|
||||
def reject_follow_request!(account, target_account)
|
||||
follow_request = FollowRequest.find_by(account: target_account, target_account: account)
|
||||
follow_request&.reject!
|
||||
end
|
||||
|
||||
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)
|
||||
UnfollowService.new.call(target_account, account) if target_account.following?(account)
|
||||
account.block!(target_account)
|
||||
end
|
||||
|
||||
def reflect_unblock!(account, target_account)
|
||||
UnblockService.new.call(account, target_account)
|
||||
end
|
||||
|
||||
def delete_post!(xml, account)
|
||||
status = Status.find(xml.at_xpath('//xmlns:id', xmlns: OStatus::TagManager::XMLNS).content)
|
||||
|
||||
return if status.nil?
|
||||
|
||||
authorize_with account, status, :destroy?
|
||||
|
||||
RemovalWorker.perform_async(status.id)
|
||||
end
|
||||
|
||||
def favourite!(xml, from_account)
|
||||
current_status = status(xml)
|
||||
|
||||
return if current_status.nil?
|
||||
|
||||
favourite = current_status.favourites.where(account: from_account).first_or_create!(account: from_account)
|
||||
NotifyService.new.call(current_status.account, favourite)
|
||||
end
|
||||
|
||||
def unfavourite!(xml, from_account)
|
||||
current_status = status(xml)
|
||||
|
||||
return if current_status.nil?
|
||||
|
||||
favourite = current_status.favourites.where(account: from_account).first
|
||||
favourite&.destroy
|
||||
end
|
||||
|
||||
def add_post!(body, account)
|
||||
ProcessingWorker.perform_async(account.id, body.force_encoding('UTF-8'))
|
||||
end
|
||||
|
||||
def status(xml)
|
||||
uri = activity_id(xml)
|
||||
return nil unless OStatus::TagManager.instance.local_id?(uri)
|
||||
Status.find(OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status'))
|
||||
end
|
||||
|
||||
def activity_id(xml)
|
||||
xml.at_xpath('//activity:object', activity: OStatus::TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content
|
||||
end
|
||||
|
||||
def salmon
|
||||
@salmon ||= OStatus2::Salmon.new
|
||||
end
|
||||
end
|
|
@ -1,11 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessMentionsService < BaseService
|
||||
include StreamEntryRenderer
|
||||
|
||||
# Scan status for mentions and fetch remote mentioned users, create
|
||||
# local mention pointers, send Salmon notifications to mentioned
|
||||
# remote users
|
||||
# local mention pointers
|
||||
# @param [Status] status
|
||||
def call(status)
|
||||
return unless status.local? && !status.draft?
|
||||
|
@ -40,7 +37,7 @@ class ProcessMentionsService < BaseService
|
|||
private
|
||||
|
||||
def mention_undeliverable?(mentioned_account)
|
||||
mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && @status.stream_entry.hidden?)
|
||||
mentioned_account.nil?
|
||||
end
|
||||
|
||||
def create_notification(mention)
|
||||
|
@ -48,17 +45,11 @@ class ProcessMentionsService < BaseService
|
|||
|
||||
if mentioned_account.local?
|
||||
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
|
||||
elsif mentioned_account.ostatus? && !@status.stream_entry.hidden? && !@status.local_only?
|
||||
NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
|
||||
elsif mentioned_account.activitypub? && !@status.local_only?
|
||||
elsif !@status.local_only?
|
||||
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
|
||||
end
|
||||
end
|
||||
|
||||
def ostatus_xml
|
||||
@ostatus_xml ||= stream_entry_to_xml(@status.stream_entry)
|
||||
end
|
||||
|
||||
def activitypub_json
|
||||
return @activitypub_json if defined?(@activitypub_json)
|
||||
payload = ActiveModelSerializers::SerializableResource.new(
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::SubscribeService < BaseService
|
||||
URL_PATTERN = /\A#{URI.regexp(%w(http https))}\z/
|
||||
|
||||
attr_reader :account, :callback, :secret,
|
||||
:lease_seconds, :domain
|
||||
|
||||
def call(account, callback, secret, lease_seconds, verified_domain = nil)
|
||||
@account = account
|
||||
@callback = Addressable::URI.parse(callback).normalize.to_s
|
||||
@secret = secret
|
||||
@lease_seconds = lease_seconds
|
||||
@domain = verified_domain
|
||||
|
||||
process_subscribe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_subscribe
|
||||
if account.nil?
|
||||
['Invalid topic URL', 422]
|
||||
elsif !valid_callback?
|
||||
['Invalid callback URL', 422]
|
||||
elsif blocked_domain?
|
||||
['Callback URL not allowed', 403]
|
||||
else
|
||||
confirm_subscription
|
||||
['', 202]
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_subscription
|
||||
subscription = locate_subscription
|
||||
Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds)
|
||||
end
|
||||
|
||||
def valid_callback?
|
||||
callback.present? && callback =~ URL_PATTERN
|
||||
end
|
||||
|
||||
def blocked_domain?
|
||||
DomainBlock.blocked? Addressable::URI.parse(callback).host
|
||||
end
|
||||
|
||||
def locate_subscription
|
||||
subscription = Subscription.find_or_initialize_by(account: account, callback_url: callback)
|
||||
subscription.domain = domain
|
||||
subscription.save!
|
||||
subscription
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::UnsubscribeService < BaseService
|
||||
attr_reader :account, :callback
|
||||
|
||||
def call(account, callback)
|
||||
@account = account
|
||||
@callback = Addressable::URI.parse(callback).normalize.to_s
|
||||
|
||||
process_unsubscribe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_unsubscribe
|
||||
if account.nil?
|
||||
['Invalid topic URL', 422]
|
||||
else
|
||||
confirm_unsubscribe unless subscription.nil?
|
||||
['', 202]
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_unsubscribe
|
||||
Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe')
|
||||
end
|
||||
|
||||
def subscription
|
||||
@_subscription ||= Subscription.find_by(account: account, callback_url: callback)
|
||||
end
|
||||
end
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
class ReblogService < BaseService
|
||||
include Authorization
|
||||
include StreamEntryRenderer
|
||||
|
||||
# Reblog a status and notify its remote author
|
||||
# @param [Account] account Account to reblog from
|
||||
|
@ -25,7 +24,6 @@ class ReblogService < BaseService
|
|||
DistributionWorker.perform_async(reblog.id)
|
||||
|
||||
unless reblogged_status.local_only?
|
||||
Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
|
||||
ActivityPub::DistributionWorker.perform_async(reblog.id)
|
||||
end
|
||||
|
||||
|
@ -43,9 +41,7 @@ class ReblogService < BaseService
|
|||
|
||||
if reblogged_status.account.local?
|
||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name)
|
||||
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)
|
||||
elsif !reblogged_status.account.following?(reblog.account)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,11 +11,7 @@ class RejectFollowService < BaseService
|
|||
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
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(follow_request)
|
||||
|
@ -25,8 +21,4 @@ class RejectFollowService < BaseService
|
|||
adapter: ActivityPub::Adapter
|
||||
).to_json
|
||||
end
|
||||
|
||||
def build_xml(follow_request)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveStatusService < BaseService
|
||||
include StreamEntryRenderer
|
||||
include Redisable
|
||||
|
||||
MIN_SCHEDULE_OFFSET = 60.seconds.freeze
|
||||
|
@ -80,22 +79,12 @@ class RemoveStatusService < BaseService
|
|||
target_accounts << @status.reblog.account if @status.reblog? && !@status.reblog.account.local?
|
||||
target_accounts.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(&:preferred_inbox_url)) do |target_account|
|
||||
ActivityPub::DeliveryWorker.push_bulk(target_accounts.uniq(&:preferred_inbox_url)) do |target_account|
|
||||
[signed_activity_json, @account.id, target_account.preferred_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
|
||||
|
@ -113,10 +102,6 @@ class RemoveStatusService < BaseService
|
|||
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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ResolveAccountService < BaseService
|
||||
include OStatus2::MagicKey
|
||||
include JsonLdHelper
|
||||
|
||||
DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
|
||||
|
@ -48,18 +47,13 @@ class ResolveAccountService < BaseService
|
|||
return
|
||||
end
|
||||
|
||||
return if links_missing?
|
||||
return unless activitypub_ready?
|
||||
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
@account = Account.find_remote(@username, @domain)
|
||||
|
||||
if activitypub_ready? || @account&.activitypub?
|
||||
handle_activitypub
|
||||
else
|
||||
handle_ostatus
|
||||
end
|
||||
handle_activitypub if activitypub_ready?
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
|
@ -73,21 +67,8 @@ class ResolveAccountService < BaseService
|
|||
|
||||
private
|
||||
|
||||
def links_missing?
|
||||
!(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?)
|
||||
end
|
||||
|
||||
def webfinger_update_due?
|
||||
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
||||
@account.nil? || @account.inbox_url.blank? || (!@options[:skip_webfinger] && @account.possibly_stale?)
|
||||
end
|
||||
|
||||
def activitypub_ready?
|
||||
|
@ -97,16 +78,6 @@ class ResolveAccountService < BaseService
|
|||
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?
|
||||
@options[:update_profile]
|
||||
end
|
||||
|
||||
def handle_activitypub
|
||||
return if actor_json.nil?
|
||||
|
||||
|
@ -115,89 +86,10 @@ class ResolveAccountService < BaseService
|
|||
nil
|
||||
end
|
||||
|
||||
def create_account
|
||||
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
|
||||
|
||||
@account = Account.new(username: @username, domain: @domain)
|
||||
@account.suspended_at = domain_block.created_at if auto_suspend?
|
||||
@account.silenced_at = domain_block.created_at if auto_silence?
|
||||
@account.private_key = nil
|
||||
end
|
||||
|
||||
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
|
||||
@account.public_key = public_key
|
||||
@account.uri = canonical_uri
|
||||
@account.hub_url = hub_url
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def auto_suspend?
|
||||
domain_block&.suspend?
|
||||
end
|
||||
|
||||
def auto_silence?
|
||||
domain_block&.silence?
|
||||
end
|
||||
|
||||
def domain_block
|
||||
return @domain_block if defined?(@domain_block)
|
||||
@domain_block = DomainBlock.find_by(domain: @domain)
|
||||
end
|
||||
|
||||
def atom_url
|
||||
@atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href
|
||||
end
|
||||
|
||||
def salmon_url
|
||||
@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
|
||||
|
||||
def public_key
|
||||
@public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href)
|
||||
end
|
||||
|
||||
def canonical_uri
|
||||
return @canonical_uri if defined?(@canonical_uri)
|
||||
|
||||
author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri')
|
||||
|
||||
if author_uri.nil?
|
||||
owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS)
|
||||
author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil?
|
||||
end
|
||||
|
||||
@canonical_uri = author_uri.nil? ? nil : author_uri.content
|
||||
end
|
||||
|
||||
def hub_url
|
||||
return @hub_url if defined?(@hub_url)
|
||||
|
||||
hubs = atom.xpath('//xmlns:link[@rel="hub"]')
|
||||
@hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href']
|
||||
end
|
||||
|
||||
def atom_body
|
||||
return @atom_body if defined?(@atom_body)
|
||||
|
||||
@atom_body = Request.new(:get, atom_url).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless response.code == 200
|
||||
response.body_with_limit
|
||||
end
|
||||
end
|
||||
|
||||
def actor_json
|
||||
return @actor_json if defined?(@actor_json)
|
||||
|
||||
|
@ -205,15 +97,6 @@ class ResolveAccountService < BaseService
|
|||
@actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil
|
||||
end
|
||||
|
||||
def atom
|
||||
return @atom if defined?(@atom)
|
||||
@atom = Nokogiri::XML(atom_body)
|
||||
end
|
||||
|
||||
def update_account_profile
|
||||
RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false)
|
||||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
|
||||
end
|
||||
|
|
|
@ -19,9 +19,9 @@ class ResolveURLService < BaseService
|
|||
|
||||
def process_url
|
||||
if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
|
||||
FetchRemoteAccountService.new.call(atom_url, body, protocol)
|
||||
FetchRemoteAccountService.new.call(atom_url, body)
|
||||
elsif equals_or_includes_any?(type, %w(Note Article Image Video Page Question))
|
||||
FetchRemoteStatusService.new.call(atom_url, body, protocol)
|
||||
FetchRemoteStatusService.new.call(atom_url, body)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,33 +37,14 @@ class ResolveURLService < BaseService
|
|||
fetched_atom_feed.second[:prefetched_body]
|
||||
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
|
||||
return json_data['type'] unless json_data.nil?
|
||||
end
|
||||
|
||||
def json_data
|
||||
@_json_data ||= body_to_json(body)
|
||||
end
|
||||
|
||||
def xml_root
|
||||
xml_data.root.name
|
||||
end
|
||||
|
||||
def xml_data
|
||||
@_xml_data ||= Nokogiri::XML(body, nil, 'utf-8')
|
||||
end
|
||||
|
||||
def local_url?
|
||||
TagManager.instance.local_url?(@url)
|
||||
end
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SendInteractionService < BaseService
|
||||
# Send an Atom representation of an interaction to a remote Salmon endpoint
|
||||
# @param [String] Entry XML
|
||||
# @param [Account] source_account
|
||||
# @param [Account] target_account
|
||||
def call(xml, source_account, target_account)
|
||||
@xml = xml
|
||||
@source_account = source_account
|
||||
@target_account = target_account
|
||||
|
||||
return if !target_account.ostatus? || block_notification?
|
||||
|
||||
build_request.perform do |delivery|
|
||||
raise Mastodon::UnexpectedResponseError, delivery unless delivery.code > 199 && delivery.code < 300
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_request
|
||||
request = Request.new(:post, @target_account.salmon_url, body: envelope)
|
||||
request.add_headers('Content-Type' => 'application/magic-envelope+xml')
|
||||
request
|
||||
end
|
||||
|
||||
def envelope
|
||||
salmon.pack(@xml, @source_account.keypair)
|
||||
end
|
||||
|
||||
def block_notification?
|
||||
DomainBlock.blocked?(@target_account.domain)
|
||||
end
|
||||
|
||||
def salmon
|
||||
@salmon ||= OStatus2::Salmon.new
|
||||
end
|
||||
end
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SubscribeService < BaseService
|
||||
def call(account)
|
||||
return if account.hub_url.blank?
|
||||
|
||||
@account = account
|
||||
@account.secret = SecureRandom.hex
|
||||
|
||||
build_request.perform do |response|
|
||||
if response_failed_permanently? response
|
||||
# We're not allowed to subscribe. Fail and move on.
|
||||
@account.secret = ''
|
||||
@account.save!
|
||||
elsif response_successful? response
|
||||
# The subscription will be confirmed asynchronously.
|
||||
@account.save!
|
||||
else
|
||||
# The response was either a 429 rate limit, or a 5xx error.
|
||||
# We need to retry at a later time. Fail loudly!
|
||||
raise Mastodon::UnexpectedResponseError, response
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_request
|
||||
request = Request.new(:post, @account.hub_url, form: subscription_params)
|
||||
request.on_behalf_of(some_local_account) if some_local_account
|
||||
request
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
{
|
||||
'hub.topic': @account.remote_url,
|
||||
'hub.mode': 'subscribe',
|
||||
'hub.callback': api_subscription_url(@account.id),
|
||||
'hub.verify': 'async',
|
||||
'hub.secret': @account.secret,
|
||||
'hub.lease_seconds': 7.days.seconds,
|
||||
}
|
||||
end
|
||||
|
||||
def some_local_account
|
||||
@some_local_account ||= Account.local.without_suspended.first
|
||||
end
|
||||
|
||||
# Any response in the 3xx or 4xx range, except for 429 (rate limit)
|
||||
def response_failed_permanently?(response)
|
||||
(response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests?
|
||||
end
|
||||
|
||||
# Any response in the 2xx range
|
||||
def response_successful?(response)
|
||||
response.status.success?
|
||||
end
|
||||
end
|
|
@ -50,7 +50,7 @@ class SuspendAccountService < BaseService
|
|||
private
|
||||
|
||||
def reject_follows!
|
||||
return if @account.local? || !@account.activitypub?
|
||||
return if @account.local?
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
||||
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
|
||||
|
|
|
@ -12,11 +12,7 @@ class UnblockService < BaseService
|
|||
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
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(unblock)
|
||||
|
@ -26,8 +22,4 @@ class UnblockService < BaseService
|
|||
adapter: ActivityPub::Adapter
|
||||
).to_json
|
||||
end
|
||||
|
||||
def build_xml(block)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,12 +12,7 @@ class UnfavouriteService < BaseService
|
|||
|
||||
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
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(favourite)
|
||||
|
@ -27,8 +22,4 @@ class UnfavouriteService < BaseService
|
|||
adapter: ActivityPub::Adapter
|
||||
).as_json).sign!(favourite.account))
|
||||
end
|
||||
|
||||
def build_xml(favourite)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,16 +36,11 @@ class UnfollowService < BaseService
|
|||
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
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url)
|
||||
end
|
||||
|
||||
def create_reject_notification(follow)
|
||||
# Rejecting an already-existing follow request
|
||||
return unless follow.account.activitypub?
|
||||
ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url)
|
||||
end
|
||||
|
||||
|
@ -64,8 +59,4 @@ class UnfollowService < BaseService
|
|||
adapter: ActivityPub::Adapter
|
||||
).to_json
|
||||
end
|
||||
|
||||
def build_xml(follow)
|
||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UnsubscribeService < BaseService
|
||||
def call(account)
|
||||
return if account.hub_url.blank?
|
||||
|
||||
@account = account
|
||||
|
||||
begin
|
||||
build_request.perform do |response|
|
||||
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{response.status}" unless response.status.success?
|
||||
end
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
|
||||
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}"
|
||||
end
|
||||
|
||||
@account.secret = ''
|
||||
@account.subscription_expires_at = nil
|
||||
@account.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_request
|
||||
Request.new(:post, @account.hub_url, form: subscription_params)
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
{
|
||||
'hub.topic': @account.remote_url,
|
||||
'hub.mode': 'unsubscribe',
|
||||
'hub.callback': api_subscription_url(@account.id),
|
||||
'hub.verify': 'async',
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,66 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateRemoteProfileService < BaseService
|
||||
attr_reader :account, :remote_profile
|
||||
|
||||
def call(body, account, resubscribe = false)
|
||||
@account = account
|
||||
@remote_profile = RemoteProfile.new(body)
|
||||
|
||||
return if remote_profile.root.nil?
|
||||
|
||||
update_account unless remote_profile.author.nil?
|
||||
|
||||
old_hub_url = account.hub_url
|
||||
account.hub_url = remote_profile.hub_link if remote_profile.hub_link.present? && remote_profile.hub_link != old_hub_url
|
||||
|
||||
account.save_with_optional_media!
|
||||
|
||||
Pubsubhubbub::SubscribeWorker.perform_async(account.id) if resubscribe && account.hub_url != old_hub_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_account
|
||||
account.display_name = remote_profile.display_name || ''
|
||||
account.note = remote_profile.note || ''
|
||||
account.locked = remote_profile.locked?
|
||||
|
||||
if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
|
||||
if remote_profile.avatar.present?
|
||||
account.avatar_remote_url = remote_profile.avatar
|
||||
else
|
||||
account.avatar_remote_url = ''
|
||||
account.avatar.destroy
|
||||
end
|
||||
|
||||
if remote_profile.header.present?
|
||||
account.header_remote_url = remote_profile.header
|
||||
else
|
||||
account.header_remote_url = ''
|
||||
account.header.destroy
|
||||
end
|
||||
|
||||
save_emojis if remote_profile.emojis.present?
|
||||
end
|
||||
end
|
||||
|
||||
def save_emojis
|
||||
do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media?
|
||||
|
||||
return if do_not_download
|
||||
|
||||
remote_profile.emojis.each do |link|
|
||||
next unless link['href'] && link['name']
|
||||
|
||||
shortcode = link['name'].delete(':')
|
||||
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain)
|
||||
|
||||
next unless emoji.nil?
|
||||
|
||||
emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain)
|
||||
emoji.image_remote_url = link['href']
|
||||
emoji.save
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class VerifySalmonService < BaseService
|
||||
include AuthorExtractor
|
||||
|
||||
def call(payload)
|
||||
body = salmon.unpack(payload)
|
||||
|
||||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS))
|
||||
|
||||
if account.nil?
|
||||
false
|
||||
else
|
||||
salmon.verify(payload, account.keypair)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def salmon
|
||||
@salmon ||= OStatus2::Salmon.new
|
||||
end
|
||||
end
|
|
@ -7,9 +7,6 @@
|
|||
- if @account.user&.setting_noindex
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
%link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
|
||||
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
|
||||
%link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
|
||||
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
|
||||
|
||||
- if @older_url
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
- if @account.user&.setting_noindex
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/
|
||||
%link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/
|
||||
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@stream_entry.activity) }/
|
||||
|
||||
|
|
|
@ -13,32 +13,16 @@ doc << Ox::Element.new('XRD').tap do |xrd|
|
|||
link['href'] = short_account_url(@account)
|
||||
end
|
||||
|
||||
xrd << Ox::Element.new('Link').tap do |link|
|
||||
link['rel'] = 'http://schemas.google.com/g/2010#updates-from'
|
||||
link['type'] = 'application/atom+xml'
|
||||
link['href'] = account_url(@account, format: 'atom')
|
||||
end
|
||||
|
||||
xrd << Ox::Element.new('Link').tap do |link|
|
||||
link['rel'] = 'self'
|
||||
link['type'] = 'application/activity+json'
|
||||
link['href'] = account_url(@account)
|
||||
end
|
||||
|
||||
xrd << Ox::Element.new('Link').tap do |link|
|
||||
link['rel'] = 'salmon'
|
||||
link['href'] = api_salmon_url(@account.id)
|
||||
end
|
||||
|
||||
xrd << Ox::Element.new('Link').tap do |link|
|
||||
link['rel'] = 'magic-public-key'
|
||||
link['href'] = "data:application/magic-public-key,#{@account.magic_key}"
|
||||
end
|
||||
|
||||
xrd << Ox::Element.new('Link').tap do |link|
|
||||
link['rel'] = 'http://ostatus.org/schema/1.0/subscribe'
|
||||
link['template'] = "#{authorize_interaction_url}?acct={uri}"
|
||||
end
|
||||
end
|
||||
|
||||
('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8')
|
||||
|
|
|
@ -31,7 +31,7 @@ class ActivityPub::DistributePollUpdateWorker
|
|||
|
||||
@inboxes = [@status.mentions, @status.reblogs, @status.preloadable_poll.votes].flat_map do |relation|
|
||||
relation.includes(:account).map do |record|
|
||||
record.account.preferred_inbox_url if !record.account.local? && record.account.activitypub?
|
||||
record.account.preferred_inbox_url if !record.account.local?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::PostUpgradeWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(domain)
|
||||
Account.where(domain: domain)
|
||||
.where(protocol: :ostatus)
|
||||
.where.not(last_webfingered_at: nil)
|
||||
.in_batches
|
||||
.update_all(last_webfingered_at: nil)
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotificationWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'push', retry: 5
|
||||
|
||||
def perform(xml, source_account_id, target_account_id)
|
||||
SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessingWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options backtrace: true
|
||||
|
||||
def perform(account_id, body)
|
||||
ProcessFeedService.new.call(body, Account.find(account_id), override_timestamps: true)
|
||||
end
|
||||
end
|
|
@ -1,82 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::ConfirmationWorker
|
||||
include Sidekiq::Worker
|
||||
include RoutingHelper
|
||||
|
||||
sidekiq_options queue: 'push', retry: false
|
||||
|
||||
attr_reader :subscription, :mode, :secret, :lease_seconds
|
||||
|
||||
def perform(subscription_id, mode, secret = nil, lease_seconds = nil)
|
||||
@subscription = Subscription.find(subscription_id)
|
||||
@mode = mode
|
||||
@secret = secret
|
||||
@lease_seconds = lease_seconds
|
||||
process_confirmation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_confirmation
|
||||
prepare_subscription
|
||||
|
||||
callback_get_with_params
|
||||
logger.debug "Confirming PuSH subscription for #{subscription.callback_url} with challenge #{challenge}: #{@callback_response_body}"
|
||||
|
||||
update_subscription
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
if successful_subscribe?
|
||||
subscription.save!
|
||||
elsif successful_unsubscribe?
|
||||
subscription.destroy!
|
||||
end
|
||||
end
|
||||
|
||||
def successful_subscribe?
|
||||
subscribing? && response_matches_challenge?
|
||||
end
|
||||
|
||||
def successful_unsubscribe?
|
||||
(unsubscribing? && response_matches_challenge?) || !subscription.confirmed?
|
||||
end
|
||||
|
||||
def response_matches_challenge?
|
||||
@callback_response_body == challenge
|
||||
end
|
||||
|
||||
def subscribing?
|
||||
mode == 'subscribe'
|
||||
end
|
||||
|
||||
def unsubscribing?
|
||||
mode == 'unsubscribe'
|
||||
end
|
||||
|
||||
def callback_get_with_params
|
||||
Request.new(:get, subscription.callback_url, params: callback_params).perform do |response|
|
||||
@callback_response_body = response.body_with_limit
|
||||
end
|
||||
end
|
||||
|
||||
def callback_params
|
||||
{
|
||||
'hub.topic': account_url(subscription.account, format: :atom),
|
||||
'hub.mode': mode,
|
||||
'hub.challenge': challenge,
|
||||
'hub.lease_seconds': subscription.lease_seconds,
|
||||
}
|
||||
end
|
||||
|
||||
def prepare_subscription
|
||||
subscription.secret = secret
|
||||
subscription.lease_seconds = lease_seconds
|
||||
subscription.confirmed = true
|
||||
end
|
||||
|
||||
def challenge
|
||||
@_challenge ||= SecureRandom.hex
|
||||
end
|
||||
end
|
|
@ -1,81 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::DeliveryWorker
|
||||
include Sidekiq::Worker
|
||||
include RoutingHelper
|
||||
|
||||
sidekiq_options queue: 'push', retry: 3, dead: false
|
||||
|
||||
sidekiq_retry_in do |count|
|
||||
5 * (count + 1)
|
||||
end
|
||||
|
||||
attr_reader :subscription, :payload
|
||||
|
||||
def perform(subscription_id, payload)
|
||||
@subscription = Subscription.find(subscription_id)
|
||||
@payload = payload
|
||||
process_delivery unless blocked_domain?
|
||||
rescue => e
|
||||
raise e.class, "Delivery failed for #{subscription&.callback_url}: #{e.message}", e.backtrace[0]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_delivery
|
||||
callback_post_payload do |payload_delivery|
|
||||
raise Mastodon::UnexpectedResponseError, payload_delivery unless response_successful? payload_delivery
|
||||
end
|
||||
|
||||
subscription.touch(:last_successful_delivery_at)
|
||||
end
|
||||
|
||||
def callback_post_payload(&block)
|
||||
request = Request.new(:post, subscription.callback_url, body: payload)
|
||||
request.add_headers(headers)
|
||||
request.perform(&block)
|
||||
end
|
||||
|
||||
def blocked_domain?
|
||||
DomainBlock.blocked?(host)
|
||||
end
|
||||
|
||||
def host
|
||||
Addressable::URI.parse(subscription.callback_url).normalized_host
|
||||
end
|
||||
|
||||
def headers
|
||||
{
|
||||
'Content-Type' => 'application/atom+xml',
|
||||
'Link' => link_header,
|
||||
}.merge(signature_headers.to_h)
|
||||
end
|
||||
|
||||
def link_header
|
||||
LinkHeader.new([hub_link_header, self_link_header]).to_s
|
||||
end
|
||||
|
||||
def hub_link_header
|
||||
[api_push_url, [%w(rel hub)]]
|
||||
end
|
||||
|
||||
def self_link_header
|
||||
[account_url(subscription.account, format: :atom), [%w(rel self)]]
|
||||
end
|
||||
|
||||
def signature_headers
|
||||
{ 'X-Hub-Signature' => payload_signature } if subscription.secret?
|
||||
end
|
||||
|
||||
def payload_signature
|
||||
"sha1=#{hmac_payload_digest}"
|
||||
end
|
||||
|
||||
def hmac_payload_digest
|
||||
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret, payload)
|
||||
end
|
||||
|
||||
def response_successful?(payload_delivery)
|
||||
payload_delivery.code > 199 && payload_delivery.code < 300
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::DistributionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'push'
|
||||
|
||||
def perform(stream_entry_ids)
|
||||
stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.hidden? }
|
||||
|
||||
return if stream_entries.empty?
|
||||
|
||||
@account = stream_entries.first.account
|
||||
@subscriptions = active_subscriptions.to_a
|
||||
|
||||
distribute_public!(stream_entries)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def distribute_public!(stream_entries)
|
||||
@payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries))
|
||||
|
||||
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription_id|
|
||||
[subscription_id, @payload]
|
||||
end
|
||||
end
|
||||
|
||||
def active_subscriptions
|
||||
Subscription.where(account: @account).active.pluck(:id)
|
||||
end
|
||||
end
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::RawDistributionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'push'
|
||||
|
||||
def perform(xml, source_account_id)
|
||||
@account = Account.find(source_account_id)
|
||||
@subscriptions = active_subscriptions.to_a
|
||||
|
||||
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription|
|
||||
[subscription.id, xml]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def active_subscriptions
|
||||
Subscription.where(account: @account).active.select('id, callback_url, domain')
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::SubscribeWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false
|
||||
|
||||
sidekiq_retry_in do |count|
|
||||
case count
|
||||
when 0
|
||||
30.minutes.seconds
|
||||
when 1
|
||||
2.hours.seconds
|
||||
when 2
|
||||
12.hours.seconds
|
||||
else
|
||||
24.hours.seconds * (count - 2)
|
||||
end
|
||||
end
|
||||
|
||||
sidekiq_retries_exhausted do |msg, _e|
|
||||
account = Account.find(msg['args'].first)
|
||||
Sidekiq.logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing"
|
||||
::UnsubscribeService.new.call(account)
|
||||
end
|
||||
|
||||
def perform(account_id)
|
||||
account = Account.find(account_id)
|
||||
logger.debug "PuSH re-subscribing to #{account.acct}"
|
||||
::SubscribeService.new.call(account)
|
||||
rescue => e
|
||||
raise e.class, "Subscribe failed for #{account&.acct}: #{e.message}", e.backtrace[0]
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Pubsubhubbub::UnsubscribeWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'push', retry: false, unique: :until_executed, dead: false
|
||||
|
||||
def perform(account_id)
|
||||
account = Account.find(account_id)
|
||||
logger.debug "PuSH unsubscribing from #{account.acct}"
|
||||
::UnsubscribeService.new.call(account)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -7,7 +7,6 @@ class RefollowWorker
|
|||
|
||||
def perform(target_account_id)
|
||||
target_account = Account.find(target_account_id)
|
||||
return unless target_account.protocol == :activitypub
|
||||
|
||||
target_account.followers.where(domain: nil).reorder(nil).find_each do |follower|
|
||||
# Locally unfollow remote account
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoteProfileUpdateWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id, body, resubscribe)
|
||||
UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SalmonWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options backtrace: true
|
||||
|
||||
def perform(account_id, body)
|
||||
ProcessInteractionService.new.call(body, Account.find(account_id))
|
||||
rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Scheduler::SubscriptionsScheduler
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options unique: :until_executed, retry: 0
|
||||
|
||||
def perform
|
||||
Pubsubhubbub::SubscribeWorker.push_bulk(expiring_accounts.pluck(:id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expiring_accounts
|
||||
Account.expiring(1.day.from_now).partitioned
|
||||
end
|
||||
end
|
|
@ -251,16 +251,6 @@ Rails.application.routes.draw do
|
|||
get '/admin', to: redirect('/admin/dashboard', status: 302)
|
||||
|
||||
namespace :api do
|
||||
# PubSubHubbub outgoing subscriptions
|
||||
resources :subscriptions, only: [:show]
|
||||
post '/subscriptions/:id', to: 'subscriptions#update'
|
||||
|
||||
# PubSubHubbub incoming subscriptions
|
||||
post '/push', to: 'push#update', as: :push
|
||||
|
||||
# Salmon
|
||||
post '/salmon/:id', to: 'salmon#update', as: :salmon
|
||||
|
||||
# OEmbed
|
||||
get '/oembed', to: 'oembed#show', as: :oembed
|
||||
|
||||
|
|
|
@ -138,7 +138,6 @@ ActiveRecord::Schema.define(version: 2019_05_19_130537) do
|
|||
t.string "outbox_url", default: "", null: false
|
||||
t.string "shared_inbox_url", default: "", null: false
|
||||
t.string "followers_url", default: "", null: false
|
||||
t.integer "protocol", default: 0, null: false
|
||||
t.boolean "memorial", default: false, null: false
|
||||
t.bigint "moved_to_account_id"
|
||||
t.string "featured_collection_url"
|
||||
|
|
|
@ -48,37 +48,6 @@ RSpec.describe AccountsController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
context 'atom' do
|
||||
let(:format) { 'atom' }
|
||||
let(:content_type) { 'application/atom+xml' }
|
||||
|
||||
shared_examples 'responsed streams' do
|
||||
it 'assigns @entries' do
|
||||
entries = assigns(:entries).to_a
|
||||
expect(entries.size).to eq expected_statuses.size
|
||||
entries.each.zip(expected_statuses.each) do |entry, expected_status|
|
||||
expect(entry.status).to eq expected_status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'responses'
|
||||
|
||||
context 'without max_id nor since_id' do
|
||||
let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] }
|
||||
|
||||
include_examples 'responsed streams'
|
||||
end
|
||||
|
||||
context 'with max_id and since_id' do
|
||||
let(:max_id) { status4.stream_entry.id }
|
||||
let(:since_id) { status1.stream_entry.id }
|
||||
let(:expected_statuses) { [status3, status2] }
|
||||
|
||||
include_examples 'responsed streams'
|
||||
end
|
||||
end
|
||||
|
||||
context 'activitystreams2' do
|
||||
let(:format) { 'json' }
|
||||
let(:content_type) { 'application/activity+json' }
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Api::PushController, type: :controller do
|
||||
describe 'POST #update' do
|
||||
context 'with hub.mode=subscribe' do
|
||||
it 'creates a subscription' do
|
||||
service = double(call: ['', 202])
|
||||
allow(Pubsubhubbub::SubscribeService).to receive(:new).and_return(service)
|
||||
account = Fabricate(:account)
|
||||
account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom"
|
||||
post :update, params: {
|
||||
'hub.mode' => 'subscribe',
|
||||
'hub.topic' => account_topic_url,
|
||||
'hub.callback' => 'https://callback.host/api',
|
||||
'hub.lease_seconds' => '3600',
|
||||
'hub.secret' => 'as1234df',
|
||||
}
|
||||
|
||||
expect(service).to have_received(:call).with(
|
||||
account,
|
||||
'https://callback.host/api',
|
||||
'as1234df',
|
||||
'3600',
|
||||
nil
|
||||
)
|
||||
expect(response).to have_http_status(202)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hub.mode=unsubscribe' do
|
||||
it 'unsubscribes the account' do
|
||||
service = double(call: ['', 202])
|
||||
allow(Pubsubhubbub::UnsubscribeService).to receive(:new).and_return(service)
|
||||
account = Fabricate(:account)
|
||||
account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom"
|
||||
post :update, params: {
|
||||
'hub.mode' => 'unsubscribe',
|
||||
'hub.topic' => account_topic_url,
|
||||
'hub.callback' => 'https://callback.host/api',
|
||||
}
|
||||
|
||||
expect(service).to have_received(:call).with(
|
||||
account,
|
||||
'https://callback.host/api',
|
||||
)
|
||||
expect(response).to have_http_status(202)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown mode' do
|
||||
it 'returns an unknown mode error' do
|
||||
post :update, params: { 'hub.mode' => 'fake' }
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/Unknown mode/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,65 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Api::SalmonController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account }
|
||||
|
||||
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/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
|
||||
stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
|
||||
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
|
||||
end
|
||||
|
||||
describe 'POST #update' do
|
||||
context 'with valid post data' do
|
||||
before do
|
||||
post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml'))
|
||||
end
|
||||
|
||||
it 'contains XML in the request body' do
|
||||
expect(request.body.read).to be_a String
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(202)
|
||||
end
|
||||
|
||||
it 'creates remote account' do
|
||||
expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil
|
||||
end
|
||||
|
||||
it 'creates mention for target account' do
|
||||
expect(account.mentions.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'with empty post data' do
|
||||
before do
|
||||
post :update, params: { id: account.id }, body: ''
|
||||
end
|
||||
|
||||
it 'returns http client error' do
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid post data' do
|
||||
before do
|
||||
service = double(call: false)
|
||||
allow(VerifySalmonService).to receive(:new).and_return(service)
|
||||
|
||||
post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml'))
|
||||
end
|
||||
|
||||
it 'returns http client error' do
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,68 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Api::SubscriptionsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:account) { Fabricate(:account, username: 'gargron', domain: 'quitter.no', remote_url: 'topic_url', secret: 'abc') }
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'with valid subscription' do
|
||||
before do
|
||||
get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" }
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'echoes back the challenge' do
|
||||
expect(response.body).to match '456'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid subscription' do
|
||||
before do
|
||||
expect_any_instance_of(Account).to receive_message_chain(:subscription, :valid?).and_return(false)
|
||||
get :show, params: { :id => account.id }
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #update' do
|
||||
let(:feed) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) }
|
||||
|
||||
before do
|
||||
stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
|
||||
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
|
||||
stub_request(:get, "https://quitter.no/notice/1269244").to_return(status: 404)
|
||||
stub_request(:get, "https://quitter.no/notice/1265331").to_return(status: 404)
|
||||
stub_request(:get, "https://community.highlandarrow.com/notice/54411").to_return(status: 404)
|
||||
stub_request(:get, "https://community.highlandarrow.com/notice/53857").to_return(status: 404)
|
||||
stub_request(:get, "https://community.highlandarrow.com/notice/51852").to_return(status: 404)
|
||||
stub_request(:get, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404)
|
||||
stub_request(:get, "https://community.highlandarrow.com/notice/50467").to_return(status: 404)
|
||||
stub_request(:get, "https://quitter.no/notice/1243309").to_return(status: 404)
|
||||
stub_request(:get, "https://quitter.no/user/7477").to_return(status: 404)
|
||||
stub_request(:any, "https://community.highlandarrow.com/user/1").to_return(status: 404)
|
||||
stub_request(:any, "https://social.umeahackerspace.se/user/2").to_return(status: 404)
|
||||
stub_request(:any, "https://gs.kawa-kun.com/user/2").to_return(status: 404)
|
||||
stub_request(:any, "https://mastodon.social/users/Gargron").to_return(status: 404)
|
||||
|
||||
request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}"
|
||||
|
||||
post :update, params: { id: account.id }, body: feed
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'creates statuses for feed' do
|
||||
expect(account.statuses.count).to_not eq 0
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,7 +21,7 @@ RSpec.describe StreamEntriesController, type: :controller do
|
|||
|
||||
get route, params: { account_username: alice.username, id: status.stream_entry.id }
|
||||
|
||||
expect(response.headers['Link'].to_s).to eq "<http://test.host/users/alice/updates/#{status.stream_entry.id}.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://cb6e6126.ngrok.io/users/alice/statuses/#{status.id}>; rel=\"alternate\"; type=\"application/activity+json\""
|
||||
expect(response.headers['Link'].to_s).to eq "<https://cb6e6126.ngrok.io/users/alice/statuses/#{status.id}>; rel=\"alternate\"; type=\"application/activity+json\""
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -73,12 +73,6 @@ RSpec.describe StreamEntriesController, type: :controller do
|
|||
|
||||
expect(response).to redirect_to(short_account_status_url(status.account, status))
|
||||
end
|
||||
|
||||
it 'returns http success with Atom' do
|
||||
status = Fabricate(:status)
|
||||
get :show, params: { account_username: status.account.username, id: status.stream_entry.id }, format: 'atom'
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #embed' do
|
||||
|
|
|
@ -25,7 +25,6 @@ RSpec.describe ActivityPub::Activity::Undo do
|
|||
type: 'Announce',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
atomUri: 'barbar',
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -39,17 +38,6 @@ RSpec.describe ActivityPub::Activity::Undo do
|
|||
expect(sender.reblogged?(status)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with atomUri' do
|
||||
before do
|
||||
Fabricate(:status, reblog: status, account: sender, uri: 'barbar')
|
||||
end
|
||||
|
||||
it 'deletes the reblog by atomUri' do
|
||||
subject.perform
|
||||
expect(sender.reblogged?(status)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Accept' do
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -215,13 +215,6 @@ RSpec.describe Account, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#subscription' do
|
||||
it 'returns an OStatus subscription' do
|
||||
account = Fabricate(:account)
|
||||
expect(account.subscription('')).to be_instance_of OStatus2::Subscription
|
||||
end
|
||||
end
|
||||
|
||||
describe '#object_type' do
|
||||
it 'is always a person' do
|
||||
account = Fabricate(:account)
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe RemoteProfile do
|
||||
let(:remote_profile) { RemoteProfile.new(body) }
|
||||
let(:body) do
|
||||
<<-XML
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<author>John</author>
|
||||
XML
|
||||
end
|
||||
|
||||
describe '.initialize' do
|
||||
it 'calls Nokogiri::XML.parse' do
|
||||
expect(Nokogiri::XML).to receive(:parse).with(body, nil, 'utf-8')
|
||||
RemoteProfile.new(body)
|
||||
end
|
||||
|
||||
it 'sets document' do
|
||||
remote_profile = RemoteProfile.new(body)
|
||||
expect(remote_profile).not_to be nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#root' do
|
||||
let(:document) { remote_profile.document }
|
||||
|
||||
it 'callse document.at_xpath' do
|
||||
expect(document).to receive(:at_xpath).with(
|
||||
'/atom:feed|/atom:entry',
|
||||
atom: OStatus::TagManager::XMLNS
|
||||
)
|
||||
|
||||
remote_profile.root
|
||||
end
|
||||
end
|
||||
|
||||
describe '#author' do
|
||||
let(:root) { remote_profile.root }
|
||||
|
||||
it 'calls root.at_xpath' do
|
||||
expect(root).to receive(:at_xpath).with(
|
||||
'./atom:author|./dfrn:owner',
|
||||
atom: OStatus::TagManager::XMLNS,
|
||||
dfrn: OStatus::TagManager::DFRN_XMLNS
|
||||
)
|
||||
|
||||
remote_profile.author
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hub_link' do
|
||||
let(:root) { remote_profile.root }
|
||||
|
||||
it 'calls #link_href_from_xml' do
|
||||
expect(remote_profile).to receive(:link_href_from_xml).with(root, 'hub')
|
||||
remote_profile.hub_link
|
||||
end
|
||||
end
|
||||
|
||||
describe '#display_name' do
|
||||
let(:author) { remote_profile.author }
|
||||
|
||||
it 'calls author.at_xpath.content' do
|
||||
expect(author).to receive_message_chain(:at_xpath, :content).with(
|
||||
'./poco:displayName',
|
||||
poco: OStatus::TagManager::POCO_XMLNS
|
||||
).with(no_args)
|
||||
|
||||
remote_profile.display_name
|
||||
end
|
||||
end
|
||||
|
||||
describe '#note' do
|
||||
let(:author) { remote_profile.author }
|
||||
|
||||
it 'calls author.at_xpath.content' do
|
||||
expect(author).to receive_message_chain(:at_xpath, :content).with(
|
||||
'./atom:summary|./poco:note',
|
||||
atom: OStatus::TagManager::XMLNS,
|
||||
poco: OStatus::TagManager::POCO_XMLNS
|
||||
).with(no_args)
|
||||
|
||||
remote_profile.note
|
||||
end
|
||||
end
|
||||
|
||||
describe '#scope' do
|
||||
let(:author) { remote_profile.author }
|
||||
|
||||
it 'calls author.at_xpath.content' do
|
||||
expect(author).to receive_message_chain(:at_xpath, :content).with(
|
||||
'./mastodon:scope',
|
||||
mastodon: OStatus::TagManager::MTDN_XMLNS
|
||||
).with(no_args)
|
||||
|
||||
remote_profile.scope
|
||||
end
|
||||
end
|
||||
|
||||
describe '#avatar' do
|
||||
let(:author) { remote_profile.author }
|
||||
|
||||
it 'calls #link_href_from_xml' do
|
||||
expect(remote_profile).to receive(:link_href_from_xml).with(author, 'avatar')
|
||||
remote_profile.avatar
|
||||
end
|
||||
end
|
||||
|
||||
describe '#header' do
|
||||
let(:author) { remote_profile.author }
|
||||
|
||||
it 'calls #link_href_from_xml' do
|
||||
expect(remote_profile).to receive(:link_href_from_xml).with(author, 'header')
|
||||
remote_profile.header
|
||||
end
|
||||
end
|
||||
|
||||
describe '#locked?' do
|
||||
before do
|
||||
allow(remote_profile).to receive(:scope).and_return(scope)
|
||||
end
|
||||
|
||||
subject { remote_profile.locked? }
|
||||
|
||||
context 'scope is private' do
|
||||
let(:scope) { 'private' }
|
||||
|
||||
it 'returns true' do
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'scope is not private' do
|
||||
let(:scope) { 'public' }
|
||||
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,31 +22,6 @@ RSpec.describe AuthorizeFollowService, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
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 }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
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 a follow request authorization salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:authorize])
|
||||
}).to have_been_made.once
|
||||
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 }
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
|||
subject { BatchedRemoveStatusService.new }
|
||||
|
||||
let!(:alice) { Fabricate(:account) }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
|
||||
let!(:jeff) { Fabricate(:user).account }
|
||||
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
|
@ -14,11 +13,8 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
|||
before do
|
||||
allow(Redis.current).to receive_messages(publish: nil)
|
||||
|
||||
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/inbox').to_return(status: 200)
|
||||
|
||||
Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
|
||||
jeff.user.update(current_sign_in_at: Time.zone.now)
|
||||
jeff.follow!(alice)
|
||||
hank.follow!(alice)
|
||||
|
@ -49,19 +45,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
|||
expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once)
|
||||
end
|
||||
|
||||
it 'sends PuSH update to PuSH subscribers' do
|
||||
expect(a_request(:post, 'http://example.com/push').with { |req|
|
||||
matches = req.body.match(OStatus::TagManager::VERBS[:delete])
|
||||
}).to have_been_made.at_least_once
|
||||
end
|
||||
|
||||
it 'sends Salmon slap to previously mentioned users' do
|
||||
expect(a_request(:post, "http://example.com/salmon").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:delete])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sends delete activity to followers' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once
|
||||
end
|
||||
|
|
|
@ -18,27 +18,6 @@ RSpec.describe FavouriteService, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
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') }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://salmon.example.com/").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 salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:favorite])
|
||||
}).to have_been_made.once
|
||||
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) }
|
||||
|
|
|
@ -54,24 +54,18 @@ RSpec.describe FetchAtomService, type: :service do
|
|||
WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers)
|
||||
end
|
||||
|
||||
context 'content type is application/atom+xml' do
|
||||
let(:content_type) { 'application/atom+xml' }
|
||||
|
||||
it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] }
|
||||
end
|
||||
|
||||
context 'content_type is activity+json' do
|
||||
let(:content_type) { 'application/activity+json; charset=utf-8' }
|
||||
let(:body) { json }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
|
||||
end
|
||||
|
||||
context 'content_type is ld+json with profile' do
|
||||
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
|
||||
let(:body) { json }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -82,14 +76,14 @@ RSpec.describe FetchAtomService, type: :service do
|
|||
context 'has link header' do
|
||||
let(:headers) { { 'Link' => '<http://example.com/foo>; rel="alternate"; type="application/activity+json"', } }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] }
|
||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
|
||||
end
|
||||
|
||||
context 'content type is text/html' do
|
||||
let(:content_type) { 'text/html' }
|
||||
let(:body) { '<html><head><link rel="alternate" href="http://example.com/foo" type="application/activity+json"/></head></html>' }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] }
|
||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,7 @@ require 'rails_helper'
|
|||
RSpec.describe FetchRemoteAccountService, type: :service do
|
||||
let(:url) { 'https://example.com/alice' }
|
||||
let(:prefetched_body) { nil }
|
||||
let(:protocol) { :ostatus }
|
||||
subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) }
|
||||
subject { FetchRemoteAccountService.new.call(url, prefetched_body) }
|
||||
|
||||
let(:actor) do
|
||||
{
|
||||
|
@ -19,7 +18,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do
|
|||
end
|
||||
|
||||
let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) }
|
||||
|
||||
shared_examples 'return Account' do
|
||||
it { is_expected.to be_an Account }
|
||||
|
@ -27,7 +25,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do
|
|||
|
||||
context 'protocol is :activitypub' do
|
||||
let(:prefetched_body) { Oj.dump(actor) }
|
||||
let(:protocol) { :activitypub }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
|
@ -36,36 +33,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do
|
|||
include_examples 'return Account'
|
||||
end
|
||||
|
||||
context 'protocol is :ostatus' do
|
||||
let(:prefetched_body) { xml }
|
||||
let(:protocol) { :ostatus }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt'))
|
||||
stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
|
||||
end
|
||||
|
||||
include_examples 'return Account'
|
||||
|
||||
it 'does not update account information if XML comes from an unverified domain' do
|
||||
feed_xml = <<-XML.squish
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
|
||||
<author>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>http://kickass.zone/users/localhost</uri>
|
||||
<name>localhost</name>
|
||||
<poco:preferredUsername>localhost</poco:preferredUsername>
|
||||
<poco:displayName>Villain!!!</poco:displayName>
|
||||
</author>
|
||||
</feed>
|
||||
XML
|
||||
|
||||
returned_account = described_class.new.call('https://real-fake-domains.com/alice', feed_xml, :ostatus)
|
||||
expect(returned_account.display_name).to_not eq 'Villain!!!'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when prefetched_body is nil' do
|
||||
context 'protocol is :activitypub' do
|
||||
before do
|
||||
|
@ -75,15 +42,5 @@ RSpec.describe FetchRemoteAccountService, type: :service do
|
|||
|
||||
include_examples 'return Account'
|
||||
end
|
||||
|
||||
context 'protocol is :ostatus' do
|
||||
before do
|
||||
stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' })
|
||||
stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt'))
|
||||
stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
|
||||
end
|
||||
|
||||
include_examples 'return Account'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,9 +16,8 @@ RSpec.describe FetchRemoteStatusService, type: :service do
|
|||
end
|
||||
|
||||
context 'protocol is :activitypub' do
|
||||
subject { described_class.new.call(note[:id], prefetched_body, protocol) }
|
||||
subject { described_class.new.call(note[:id], prefetched_body) }
|
||||
let(:prefetched_body) { Oj.dump(note) }
|
||||
let(:protocol) { :activitypub }
|
||||
|
||||
before do
|
||||
account.update(uri: ActivityPub::TagManager.instance.uri_for(account))
|
||||
|
@ -32,56 +31,4 @@ RSpec.describe FetchRemoteStatusService, type: :service do
|
|||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'protocol is :ostatus' do
|
||||
subject { described_class.new }
|
||||
|
||||
before do
|
||||
Fabricate(:account, username: 'tracer', domain: 'real.domain', remote_url: 'https://real.domain/users/tracer')
|
||||
end
|
||||
|
||||
it 'does not create status with author at different domain' do
|
||||
status_body = <<-XML.squish
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
|
||||
<id>tag:real.domain,2017-04-27:objectId=4487555:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://real.domain/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://real.domain/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
expect(subject.call('https://fake.domain/foo', status_body, :ostatus)).to be_nil
|
||||
end
|
||||
|
||||
it 'does not create status with wrong id when id uses http format' do
|
||||
status_body = <<-XML.squish
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
|
||||
<id>https://other-real.domain/statuses/123</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://real.domain/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://real.domain/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
expect(subject.call('https://real.domain/statuses/456', status_body, :ostatus)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -96,74 +96,6 @@ RSpec.describe FollowService, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
context 'remote OStatus account' do
|
||||
describe 'locked account' do
|
||||
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
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates a follow request' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends a follow request salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:request_friend])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unlocked account' do
|
||||
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
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
stub_request(:post, "http://hub.example.com/").to_return(status: 202)
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates a following relation' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
end
|
||||
|
||||
it 'sends a follow salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:follow])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'subscribes to PuSH' do
|
||||
expect(a_request(:post, "http://hub.example.com/")).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'already followed account' do
|
||||
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
|
||||
sender.follow!(bob)
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'keeps a following relation' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
end
|
||||
|
||||
it 'does not send a follow salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/")).not_to have_been_made
|
||||
end
|
||||
|
||||
it 'does not subscribe to PuSH' do
|
||||
expect(a_request(:post, "http://hub.example.com/")).not_to have_been_made
|
||||
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 }
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue