Remove Salmon and PubSubHubbub (#11205)
* Remove Salmon and PubSubHubbub endpoints * Add error when trying to follow OStatus accounts * Fix new accounts not being created in ResolveAccountService
This commit is contained in:
		@@ -44,7 +44,6 @@ class ActivityPub::InboxesController < Api::BaseController
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Api::V1::FollowsController < Api::BaseController
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :follow, :'write:follows' }
 | 
			
		||||
  before_action :require_user!
 | 
			
		||||
 | 
			
		||||
  respond_to :json
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
    raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
 | 
			
		||||
 | 
			
		||||
    @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
 | 
			
		||||
 | 
			
		||||
    if @account.nil?
 | 
			
		||||
      username, domain = target_uri.split('@')
 | 
			
		||||
      @account         = Account.find_remote!(username, domain)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    render json: @account, serializer: REST::AccountSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def target_uri
 | 
			
		||||
    follow_params[:uri].strip.gsub(/\A@/, '')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def follow_params
 | 
			
		||||
    params.permit(:uri)
 | 
			
		||||
  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(4)
 | 
			
		||||
 | 
			
		||||
    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.reject_media?(@account.domain)
 | 
			
		||||
    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.reject_media?(parent.account.domain)
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@@ -53,8 +53,6 @@ class OStatus::AtomSerializer
 | 
			
		||||
    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)
 | 
			
		||||
 
 | 
			
		||||
@@ -164,8 +164,7 @@ class Account < ApplicationRecord
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def refresh!
 | 
			
		||||
    return if local?
 | 
			
		||||
    ResolveAccountService.new.call(acct)
 | 
			
		||||
    ResolveAccountService.new.call(acct) unless local?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def silenced?
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer
 | 
			
		||||
      { 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}" },
 | 
			
		||||
    ]
 | 
			
		||||
 
 | 
			
		||||
@@ -11,25 +11,17 @@ class AuthorizeFollowService < BaseService
 | 
			
		||||
      follow_request.authorize!
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    create_notification(follow_request) unless source_account.local?
 | 
			
		||||
    create_notification(follow_request) if !source_account.local? && source_account.activitypub?
 | 
			
		||||
    follow_request
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def create_notification(follow_request)
 | 
			
		||||
    if follow_request.account.ostatus?
 | 
			
		||||
      NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id)
 | 
			
		||||
    elsif follow_request.account.activitypub?
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
    ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_json(follow_request)
 | 
			
		||||
    Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_xml(follow_request)
 | 
			
		||||
    OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class BatchedRemoveStatusService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
  include Redisable
 | 
			
		||||
 | 
			
		||||
  # Delete given statuses and reblogs of them
 | 
			
		||||
@@ -18,10 +17,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          = {}
 | 
			
		||||
    @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) }
 | 
			
		||||
 | 
			
		||||
    # Ensure that rendered XML reflects destroyed state
 | 
			
		||||
    statuses.each do |status|
 | 
			
		||||
@@ -39,28 +35,16 @@ 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)
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
@@ -101,20 +85,4 @@ class BatchedRemoveStatusService < BaseService
 | 
			
		||||
      end
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,6 @@ class BlockDomainService < BaseService
 | 
			
		||||
 | 
			
		||||
  def suspend_accounts!
 | 
			
		||||
    blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account|
 | 
			
		||||
      UnsubscribeService.new.call(account) if account.subscribed?
 | 
			
		||||
      SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -13,25 +13,17 @@ class BlockService < BaseService
 | 
			
		||||
    block = account.block!(target_account)
 | 
			
		||||
 | 
			
		||||
    BlockWorker.perform_async(account.id, target_account.id)
 | 
			
		||||
    create_notification(block) unless target_account.local?
 | 
			
		||||
    create_notification(block) if !target_account.local? && target_account.activitypub?
 | 
			
		||||
    block
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def create_notification(block)
 | 
			
		||||
    if block.target_account.ostatus?
 | 
			
		||||
      NotificationWorker.perform_async(build_xml(block), block.account_id, block.target_account_id)
 | 
			
		||||
    elsif block.target_account.activitypub?
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
    ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_json(block)
 | 
			
		||||
    Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer))
 | 
			
		||||
  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,8 +30,6 @@ 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?
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
@@ -46,8 +44,4 @@ class FavouriteService < BaseService
 | 
			
		||||
  def build_json(favourite)
 | 
			
		||||
    Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_xml(favourite)
 | 
			
		||||
    OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class FetchRemoteAccountService < BaseService
 | 
			
		||||
  include AuthorExtractor
 | 
			
		||||
 | 
			
		||||
  def call(url, prefetched_body = nil, protocol = :ostatus)
 | 
			
		||||
    if prefetched_body.nil?
 | 
			
		||||
      resource_url, resource_options, protocol = FetchAtomService.new.call(url)
 | 
			
		||||
@@ -12,34 +10,8 @@ class FetchRemoteAccountService < BaseService
 | 
			
		||||
    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?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class FetchRemoteStatusService < BaseService
 | 
			
		||||
  include AuthorExtractor
 | 
			
		||||
 | 
			
		||||
  def call(url, prefetched_body = nil, protocol = :ostatus)
 | 
			
		||||
    if prefetched_body.nil?
 | 
			
		||||
      resource_url, resource_options, protocol = FetchAtomService.new.call(url)
 | 
			
		||||
@@ -12,34 +10,8 @@ class FetchRemoteStatusService < BaseService
 | 
			
		||||
    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?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class FollowService < BaseService
 | 
			
		||||
    target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
 | 
			
		||||
 | 
			
		||||
    raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
 | 
			
		||||
    raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved?
 | 
			
		||||
    raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?)
 | 
			
		||||
 | 
			
		||||
    if source_account.following?(target_account)
 | 
			
		||||
      # We're already following this account, but we'll call follow! again to
 | 
			
		||||
@@ -32,7 +32,7 @@ class FollowService < BaseService
 | 
			
		||||
 | 
			
		||||
    if target_account.locked? || target_account.activitypub?
 | 
			
		||||
      request_follow(source_account, target_account, reblogs: reblogs)
 | 
			
		||||
    else
 | 
			
		||||
    elsif target_account.local?
 | 
			
		||||
      direct_follow(source_account, target_account, reblogs: reblogs)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
@@ -44,9 +44,6 @@ 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?
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
@@ -57,27 +54,12 @@ class FollowService < BaseService
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
    LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
 | 
			
		||||
    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)
 | 
			
		||||
    Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -88,7 +88,6 @@ class PostStatusService < BaseService
 | 
			
		||||
  def postprocess_status!
 | 
			
		||||
    LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
 | 
			
		||||
    DistributionWorker.perform_async(@status.id)
 | 
			
		||||
    Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
 | 
			
		||||
    ActivityPub::DistributionWorker.perform_async(@status.id)
 | 
			
		||||
    PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
 | 
			
		||||
  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
 | 
			
		||||
@@ -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,7 +1,6 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ProcessMentionsService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
  include Payloadable
 | 
			
		||||
 | 
			
		||||
  # Scan status for mentions and fetch remote mentioned users, create
 | 
			
		||||
@@ -49,17 +48,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?
 | 
			
		||||
      NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
 | 
			
		||||
    elsif mentioned_account.activitypub?
 | 
			
		||||
      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)
 | 
			
		||||
    @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
  include Payloadable
 | 
			
		||||
 | 
			
		||||
  # Reblog a status and notify its remote author
 | 
			
		||||
@@ -24,7 +23,6 @@ class ReblogService < BaseService
 | 
			
		||||
    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
 | 
			
		||||
 | 
			
		||||
    DistributionWorker.perform_async(reblog.id)
 | 
			
		||||
    Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
 | 
			
		||||
    ActivityPub::DistributionWorker.perform_async(reblog.id)
 | 
			
		||||
 | 
			
		||||
    create_notification(reblog)
 | 
			
		||||
@@ -40,8 +38,6 @@ 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)
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
@@ -6,25 +6,17 @@ class RejectFollowService < BaseService
 | 
			
		||||
  def call(source_account, target_account)
 | 
			
		||||
    follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
 | 
			
		||||
    follow_request.reject!
 | 
			
		||||
    create_notification(follow_request) unless source_account.local?
 | 
			
		||||
    create_notification(follow_request) if !source_account.local? && source_account.activitypub?
 | 
			
		||||
    follow_request
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def create_notification(follow_request)
 | 
			
		||||
    if follow_request.account.ostatus?
 | 
			
		||||
      NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id)
 | 
			
		||||
    elsif follow_request.account.activitypub?
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
    ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_json(follow_request)
 | 
			
		||||
    Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer))
 | 
			
		||||
  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
 | 
			
		||||
  include Payloadable
 | 
			
		||||
 | 
			
		||||
@@ -78,11 +77,6 @@ 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|
 | 
			
		||||
      [signed_activity_json, @account.id, target_account.preferred_inbox_url]
 | 
			
		||||
@@ -90,9 +84,6 @@ class RemoveStatusService < BaseService
 | 
			
		||||
  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]
 | 
			
		||||
@@ -111,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(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ResolveAccountService < BaseService
 | 
			
		||||
  include OStatus2::MagicKey
 | 
			
		||||
  include JsonLdHelper
 | 
			
		||||
require_relative '../models/account'
 | 
			
		||||
 | 
			
		||||
  DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
 | 
			
		||||
class ResolveAccountService < BaseService
 | 
			
		||||
  include JsonLdHelper
 | 
			
		||||
 | 
			
		||||
  # Find or create a local account for a remote user.
 | 
			
		||||
  # When creating, look up the user's webfinger and fetch all
 | 
			
		||||
@@ -48,18 +47,16 @@ class ResolveAccountService < BaseService
 | 
			
		||||
      return
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return if links_missing? || auto_suspend?
 | 
			
		||||
    return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
 | 
			
		||||
    return unless activitypub_ready?
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        next unless @account.nil? || @account.activitypub?
 | 
			
		||||
 | 
			
		||||
        handle_activitypub
 | 
			
		||||
      else
 | 
			
		||||
        raise Mastodon::RaceConditionError
 | 
			
		||||
      end
 | 
			
		||||
@@ -73,38 +70,12 @@ 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?)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def activitypub_ready?
 | 
			
		||||
    !@webfinger.link('self').nil? &&
 | 
			
		||||
      ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) &&
 | 
			
		||||
      !actor_json.nil? &&
 | 
			
		||||
      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]
 | 
			
		||||
    !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_activitypub
 | 
			
		||||
@@ -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.rule_for(@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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -7,25 +7,17 @@ class UnblockService < BaseService
 | 
			
		||||
    return unless account.blocking?(target_account)
 | 
			
		||||
 | 
			
		||||
    unblock = account.unblock!(target_account)
 | 
			
		||||
    create_notification(unblock) unless target_account.local?
 | 
			
		||||
    create_notification(unblock) if !target_account.local? && target_account.activitypub?
 | 
			
		||||
    unblock
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def create_notification(unblock)
 | 
			
		||||
    if unblock.target_account.ostatus?
 | 
			
		||||
      NotificationWorker.perform_async(build_xml(unblock), unblock.account_id, unblock.target_account_id)
 | 
			
		||||
    elsif unblock.target_account.activitypub?
 | 
			
		||||
      ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
    ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_json(unblock)
 | 
			
		||||
    Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_xml(block)
 | 
			
		||||
    OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ class UnfavouriteService < BaseService
 | 
			
		||||
  def call(account, status)
 | 
			
		||||
    favourite = Favourite.find_by!(account: account, status: status)
 | 
			
		||||
    favourite.destroy!
 | 
			
		||||
    create_notification(favourite) unless status.local?
 | 
			
		||||
    create_notification(favourite) if !status.account.local? && status.account.activitypub?
 | 
			
		||||
    favourite
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -14,19 +14,10 @@ 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)
 | 
			
		||||
    Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_xml(favourite)
 | 
			
		||||
    OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ class UnfollowService < BaseService
 | 
			
		||||
    return unless follow
 | 
			
		||||
 | 
			
		||||
    follow.destroy!
 | 
			
		||||
    create_notification(follow) unless @target_account.local?
 | 
			
		||||
    create_reject_notification(follow) if @target_account.local? && !@source_account.local?
 | 
			
		||||
    create_notification(follow) if !@target_account.local? && @target_account.activitypub?
 | 
			
		||||
    create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub?
 | 
			
		||||
    UnmergeWorker.perform_async(@target_account.id, @source_account.id)
 | 
			
		||||
    follow
 | 
			
		||||
  end
 | 
			
		||||
@@ -38,16 +38,10 @@ 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
 | 
			
		||||
 | 
			
		||||
@@ -58,8 +52,4 @@ class UnfollowService < BaseService
 | 
			
		||||
  def build_reject_json(follow)
 | 
			
		||||
    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
 | 
			
		||||
  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.reject_media?(account.domain)
 | 
			
		||||
      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.reject_media?(account.domain)
 | 
			
		||||
 | 
			
		||||
    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,7 +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) }/
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
%tr
 | 
			
		||||
  %td
 | 
			
		||||
    %samp= subscription.account.acct
 | 
			
		||||
  %td
 | 
			
		||||
    %samp= subscription.callback_url
 | 
			
		||||
  %td
 | 
			
		||||
    - if subscription.confirmed?
 | 
			
		||||
      %i.fa.fa-check
 | 
			
		||||
  %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" }
 | 
			
		||||
    %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) }
 | 
			
		||||
      = precede subscription.expired? ? '-' : '' do
 | 
			
		||||
        = time_ago_in_words(subscription.expires_at)
 | 
			
		||||
  %td
 | 
			
		||||
    - if subscription.last_successful_delivery_at?
 | 
			
		||||
      %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) }
 | 
			
		||||
        = l subscription.last_successful_delivery_at
 | 
			
		||||
    - else
 | 
			
		||||
      %i.fa.fa-times
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
- content_for :page_title do
 | 
			
		||||
  = t('admin.subscriptions.title')
 | 
			
		||||
 | 
			
		||||
.table-wrapper
 | 
			
		||||
  %table.table
 | 
			
		||||
    %thead
 | 
			
		||||
      %tr
 | 
			
		||||
        %th= t('admin.subscriptions.topic')
 | 
			
		||||
        %th= t('admin.subscriptions.callback_url')
 | 
			
		||||
        %th= t('admin.subscriptions.confirmed')
 | 
			
		||||
        %th= t('admin.subscriptions.expires_in')
 | 
			
		||||
        %th= t('admin.subscriptions.last_delivery')
 | 
			
		||||
    %tbody
 | 
			
		||||
      = render @subscriptions
 | 
			
		||||
 | 
			
		||||
= paginate @subscriptions
 | 
			
		||||
@@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd|
 | 
			
		||||
    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}"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,27 +5,5 @@ class AfterRemoteFollowRequestWorker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options queue: 'pull', retry: 5
 | 
			
		||||
 | 
			
		||||
  attr_reader :follow_request
 | 
			
		||||
 | 
			
		||||
  def perform(follow_request_id)
 | 
			
		||||
    @follow_request = FollowRequest.find(follow_request_id)
 | 
			
		||||
    process_follow_service if processing_required?
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def process_follow_service
 | 
			
		||||
    follow_request.destroy
 | 
			
		||||
    FollowService.new.call(follow_request.account, updated_account.acct)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def processing_required?
 | 
			
		||||
    !updated_account.nil? && !updated_account.locked?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def updated_account
 | 
			
		||||
    @_updated_account ||= FetchRemoteAccountService.new.call(follow_request.target_account.remote_url)
 | 
			
		||||
  end
 | 
			
		||||
  def perform(follow_request_id); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,27 +5,5 @@ class AfterRemoteFollowWorker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options queue: 'pull', retry: 5
 | 
			
		||||
 | 
			
		||||
  attr_reader :follow
 | 
			
		||||
 | 
			
		||||
  def perform(follow_id)
 | 
			
		||||
    @follow = Follow.find(follow_id)
 | 
			
		||||
    process_follow_service if processing_required?
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def process_follow_service
 | 
			
		||||
    follow.destroy
 | 
			
		||||
    FollowService.new.call(follow.account, updated_account.acct)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def updated_account
 | 
			
		||||
    @_updated_account ||= FetchRemoteAccountService.new.call(follow.target_account.remote_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def processing_required?
 | 
			
		||||
    !updated_account.nil? && updated_account.locked?
 | 
			
		||||
  end
 | 
			
		||||
  def perform(follow_id); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,5 @@ class NotificationWorker
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform(xml, source_account_id, target_account_id); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,5 @@ class ProcessingWorker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options backtrace: true
 | 
			
		||||
 | 
			
		||||
  def perform(account_id, body)
 | 
			
		||||
    ProcessFeedService.new.call(body, Account.find(account_id), override_timestamps: true)
 | 
			
		||||
  end
 | 
			
		||||
  def perform(account_id, body); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -2,81 +2,8 @@
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  def perform(subscription_id, mode, secret = nil, lease_seconds = nil); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -2,80 +2,8 @@
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  def perform(subscription_id, payload); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,28 +5,5 @@ class Pubsubhubbub::DistributionWorker
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform(stream_entry_ids); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,18 +5,5 @@ class Pubsubhubbub::RawDistributionWorker
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform(xml, source_account_id); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,30 +5,5 @@ class Pubsubhubbub::SubscribeWorker
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform(account_id); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,5 @@ class Pubsubhubbub::UnsubscribeWorker
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform(account_id); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,5 @@ class RemoteProfileUpdateWorker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options queue: 'pull'
 | 
			
		||||
 | 
			
		||||
  def perform(account_id, body, resubscribe)
 | 
			
		||||
    UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe)
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
  def perform(account_id, body, resubscribe); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,5 @@ class SalmonWorker
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform(account_id, body); end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,13 +5,5 @@ class Scheduler::SubscriptionsScheduler
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  def perform; end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -469,13 +469,6 @@ en:
 | 
			
		||||
      no_status_selected: No statuses were changed as none were selected
 | 
			
		||||
      title: Account statuses
 | 
			
		||||
      with_media: With media
 | 
			
		||||
    subscriptions:
 | 
			
		||||
      callback_url: Callback URL
 | 
			
		||||
      confirmed: Confirmed
 | 
			
		||||
      expires_in: Expires in
 | 
			
		||||
      last_delivery: Last delivery
 | 
			
		||||
      title: WebSub
 | 
			
		||||
      topic: Topic
 | 
			
		||||
    tags:
 | 
			
		||||
      accounts: Accounts
 | 
			
		||||
      hidden: Hidden
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,6 @@ SimpleNavigation::Configuration.run do |navigation|
 | 
			
		||||
      s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings}
 | 
			
		||||
      s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
 | 
			
		||||
      s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays}
 | 
			
		||||
      s.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
 | 
			
		||||
      s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
 | 
			
		||||
      s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
@@ -154,7 +154,6 @@ Rails.application.routes.draw do
 | 
			
		||||
  namespace :admin do
 | 
			
		||||
    get '/dashboard', to: 'dashboard#index'
 | 
			
		||||
 | 
			
		||||
    resources :subscriptions, only: [:index]
 | 
			
		||||
    resources :domain_blocks, only: [:new, :create, :show, :destroy]
 | 
			
		||||
    resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
 | 
			
		||||
    resources :action_logs, only: [:index]
 | 
			
		||||
@@ -191,8 +190,6 @@ Rails.application.routes.draw do
 | 
			
		||||
 | 
			
		||||
    resources :accounts, only: [:index, :show] do
 | 
			
		||||
      member do
 | 
			
		||||
        post :subscribe
 | 
			
		||||
        post :unsubscribe
 | 
			
		||||
        post :enable
 | 
			
		||||
        post :unsilence
 | 
			
		||||
        post :unsuspend
 | 
			
		||||
@@ -257,16 +254,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
 | 
			
		||||
 | 
			
		||||
@@ -318,7 +305,6 @@ Rails.application.routes.draw do
 | 
			
		||||
 | 
			
		||||
      get '/search', to: 'search#index', as: :search
 | 
			
		||||
 | 
			
		||||
      resources :follows,      only: [:create]
 | 
			
		||||
      resources :media,        only: [:create, :update]
 | 
			
		||||
      resources :blocks,       only: [:index]
 | 
			
		||||
      resources :mutes,        only: [:index]
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,6 @@
 | 
			
		||||
  scheduled_statuses_scheduler:
 | 
			
		||||
    every: '5m'
 | 
			
		||||
    class: Scheduler::ScheduledStatusesScheduler
 | 
			
		||||
  subscriptions_scheduler:
 | 
			
		||||
    cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *'
 | 
			
		||||
    class: Scheduler::SubscriptionsScheduler
 | 
			
		||||
  media_cleanup_scheduler:
 | 
			
		||||
    cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
 | 
			
		||||
    class: Scheduler::MediaCleanupScheduler
 | 
			
		||||
 
 | 
			
		||||
@@ -75,44 +75,6 @@ RSpec.describe Admin::AccountsController, type: :controller do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'POST #subscribe' do
 | 
			
		||||
    subject { post :subscribe, params: { id: account.id } }
 | 
			
		||||
 | 
			
		||||
    let(:current_user) { Fabricate(:user, admin: admin) }
 | 
			
		||||
    let(:account) { Fabricate(:account) }
 | 
			
		||||
 | 
			
		||||
    context 'when user is admin' do
 | 
			
		||||
      let(:admin) { true }
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to redirect_to admin_account_path(account.id) }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when user is not admin' do
 | 
			
		||||
      let(:admin) { false }
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to have_http_status :forbidden }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'POST #unsubscribe' do
 | 
			
		||||
    subject { post :unsubscribe, params: { id: account.id } }
 | 
			
		||||
 | 
			
		||||
    let(:current_user) { Fabricate(:user, admin: admin) }
 | 
			
		||||
    let(:account) { Fabricate(:account) }
 | 
			
		||||
 | 
			
		||||
    context 'when user is admin' do
 | 
			
		||||
      let(:admin) { true }
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to redirect_to admin_account_path(account.id) }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when user is not admin' do
 | 
			
		||||
      let(:admin) { false }
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to have_http_status :forbidden }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'POST #memorialize' do
 | 
			
		||||
    subject { post :memorialize, params: { id: account.id } }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Admin::SubscriptionsController, type: :controller do
 | 
			
		||||
  render_views
 | 
			
		||||
 | 
			
		||||
  describe 'GET #index' do
 | 
			
		||||
    around do |example|
 | 
			
		||||
      default_per_page = Subscription.default_per_page
 | 
			
		||||
      Subscription.paginates_per 1
 | 
			
		||||
      example.run
 | 
			
		||||
      Subscription.paginates_per default_per_page
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      sign_in Fabricate(:user, admin: true), scope: :user
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'renders subscriptions' do
 | 
			
		||||
      Fabricate(:subscription)
 | 
			
		||||
      specified = Fabricate(:subscription)
 | 
			
		||||
 | 
			
		||||
      get :index
 | 
			
		||||
 | 
			
		||||
      subscriptions = assigns(:subscriptions)
 | 
			
		||||
      expect(subscriptions.count).to eq 1
 | 
			
		||||
      expect(subscriptions[0]).to eq specified
 | 
			
		||||
 | 
			
		||||
      expect(response).to have_http_status(200)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Api::V1::FollowsController, type: :controller do
 | 
			
		||||
  render_views
 | 
			
		||||
 | 
			
		||||
  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
 | 
			
		||||
  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:follows') }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    allow(controller).to receive(:doorkeeper_token) { token }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'POST #create' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_request(:get,  "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
 | 
			
		||||
      stub_request(:get,  "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
 | 
			
		||||
      stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {})
 | 
			
		||||
      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'))
 | 
			
		||||
      stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
 | 
			
		||||
      stub_request(:post, "https://quitter.no/main/salmon/user/7477").to_return(:status => 200, :body => "", :headers => {})
 | 
			
		||||
 | 
			
		||||
      post :create, params: { uri: 'gargron@quitter.no' }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns http success' do
 | 
			
		||||
      expect(response).to have_http_status(200)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'creates account for remote user' do
 | 
			
		||||
      expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'creates a follow relation between user and remote user' do
 | 
			
		||||
      expect(user.account.following?(Account.find_by(username: 'gargron', domain: 'quitter.no'))).to be true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sends a salmon slap to the remote user' do
 | 
			
		||||
      expect(a_request(:post, "https://quitter.no/main/salmon/user/7477")).to have_been_made
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'subscribes to remote hub' do
 | 
			
		||||
      expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns http success if already following, too' do
 | 
			
		||||
      post :create, params: { uri: 'gargron@quitter.no' }
 | 
			
		||||
      expect(response).to have_http_status(200)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										2
									
								
								spec/fixtures/requests/webfinger.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								spec/fixtures/requests/webfinger.txt
									
									
									
									
										vendored
									
									
								
							@@ -8,4 +8,4 @@ Access-Control-Allow-Origin: *
 | 
			
		||||
Vary: Accept-Encoding,Cookie
 | 
			
		||||
Strict-Transport-Security: max-age=31536000; includeSubdomains;
 | 
			
		||||
 | 
			
		||||
{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]}
 | 
			
		||||
{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]}
 | 
			
		||||
 
 | 
			
		||||
@@ -406,28 +406,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
        scope = entry.nodes.find { |node| node.name == 'mastodon:scope' }
 | 
			
		||||
        expect(scope.text).to eq 'public'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns element whose rendered view triggers creation when processed' do
 | 
			
		||||
        remote_account = Account.create!(username: 'username')
 | 
			
		||||
        remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z')
 | 
			
		||||
 | 
			
		||||
        entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true)
 | 
			
		||||
        entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test
 | 
			
		||||
        xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote.test')
 | 
			
		||||
 | 
			
		||||
        remote_status.destroy!
 | 
			
		||||
        remote_account.destroy!
 | 
			
		||||
 | 
			
		||||
        account = Account.create!(
 | 
			
		||||
          domain: 'remote.test',
 | 
			
		||||
          username: 'username',
 | 
			
		||||
          last_webfingered_at: Time.now.utc
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ProcessFeedService.new.call(xml, account)
 | 
			
		||||
 | 
			
		||||
        expect(Status.find_by(uri: "https://remote.test/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'if status is not present' do
 | 
			
		||||
@@ -683,24 +661,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'appends link element for hub' do
 | 
			
		||||
      account = Fabricate(:account, username: 'username')
 | 
			
		||||
 | 
			
		||||
      feed = OStatus::AtomSerializer.new.feed(account, [])
 | 
			
		||||
 | 
			
		||||
      link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'hub' }
 | 
			
		||||
      expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/api/push'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'appends link element for Salmon' do
 | 
			
		||||
      account = Fabricate(:account, username: 'username')
 | 
			
		||||
 | 
			
		||||
      feed = OStatus::AtomSerializer.new.feed(account, [])
 | 
			
		||||
 | 
			
		||||
      link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'salmon' }
 | 
			
		||||
      expect(link[:href]).to start_with 'https://cb6e6126.ngrok.io/api/salmon/'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'appends stream entries' do
 | 
			
		||||
      account = Fabricate(:account, username: 'username')
 | 
			
		||||
      status = Fabricate(:status, account: account)
 | 
			
		||||
@@ -784,18 +744,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      object = block_salmon.nodes.find { |node| node.name == 'activity:object' }
 | 
			
		||||
      expect(object.id.text).to eq 'https://domain.test/id'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view triggers block when processed' do
 | 
			
		||||
      block = Fabricate(:block)
 | 
			
		||||
      block_salmon = OStatus::AtomSerializer.new.block_salmon(block)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(block_salmon)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair)
 | 
			
		||||
      block.destroy!
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, block.target_account)
 | 
			
		||||
 | 
			
		||||
      expect(block.account.blocking?(block.target_account)).to be true
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#unblock_salmon' do
 | 
			
		||||
@@ -871,17 +819,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' }
 | 
			
		||||
      expect(object.id.text).to eq 'https://domain.test/id'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view triggers block when processed' do
 | 
			
		||||
      block = Fabricate(:block)
 | 
			
		||||
      unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(unblock_salmon)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair)
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, block.target_account)
 | 
			
		||||
 | 
			
		||||
      expect { block.reload }.to raise_error ActiveRecord::RecordNotFound
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#favourite_salmon' do
 | 
			
		||||
@@ -964,17 +901,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote'
 | 
			
		||||
      expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view triggers favourite when processed' do
 | 
			
		||||
      favourite = Fabricate(:favourite)
 | 
			
		||||
      favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(favourite_salmon)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair)
 | 
			
		||||
      favourite.destroy!
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, favourite.status.account)
 | 
			
		||||
      expect(favourite.account.favourited?(favourite.status)).to be true
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#unfavourite_salmon' do
 | 
			
		||||
@@ -1064,16 +990,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote'
 | 
			
		||||
      expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view triggers unfavourite when processed' do
 | 
			
		||||
      favourite = Fabricate(:favourite)
 | 
			
		||||
      unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(unfavourite_salmon)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair)
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, favourite.status.account)
 | 
			
		||||
      expect { favourite.reload }.to raise_error ActiveRecord::RecordNotFound
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#follow_salmon' do
 | 
			
		||||
@@ -1143,18 +1059,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      expect(follow_salmon.title.text).to eq 'account started following target_account@remote'
 | 
			
		||||
      expect(follow_salmon.content.text).to eq 'account started following target_account@remote'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view triggers follow when processed' do
 | 
			
		||||
      follow = Fabricate(:follow)
 | 
			
		||||
      follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(follow_salmon)
 | 
			
		||||
      follow.destroy!
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair)
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, follow.target_account)
 | 
			
		||||
 | 
			
		||||
      expect(follow.account.following?(follow.target_account)).to be true
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#unfollow_salmon' do
 | 
			
		||||
@@ -1251,19 +1155,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' }
 | 
			
		||||
      expect(object.id.text).to eq 'https://domain.test/id'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view triggers unfollow when processed' do
 | 
			
		||||
      follow = Fabricate(:follow)
 | 
			
		||||
      follow.destroy!
 | 
			
		||||
      unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(unfollow_salmon)
 | 
			
		||||
      follow.account.follow!(follow.target_account)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair)
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, follow.target_account)
 | 
			
		||||
 | 
			
		||||
      expect(follow.account.following?(follow.target_account)).to be false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#follow_request_salmon' do
 | 
			
		||||
@@ -1294,18 +1185,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
        follow_request_salmon = serialize(follow_request)
 | 
			
		||||
        expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns element whose rendered view triggers follow request when processed' do
 | 
			
		||||
        follow_request = Fabricate(:follow_request)
 | 
			
		||||
        follow_request_salmon = serialize(follow_request)
 | 
			
		||||
        xml = OStatus::AtomSerializer.render(follow_request_salmon)
 | 
			
		||||
        envelope = OStatus2::Salmon.new.pack(xml, follow_request.account.keypair)
 | 
			
		||||
        follow_request.destroy!
 | 
			
		||||
 | 
			
		||||
        ProcessInteractionService.new.call(envelope, follow_request.target_account)
 | 
			
		||||
 | 
			
		||||
        expect(follow_request.account.requested?(follow_request.target_account)).to eq true
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -1364,18 +1243,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
 | 
			
		||||
      expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view creates follow from follow request when processed' do
 | 
			
		||||
      follow_request = Fabricate(:follow_request)
 | 
			
		||||
      authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(authorize_follow_request_salmon)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair)
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, follow_request.account)
 | 
			
		||||
 | 
			
		||||
      expect(follow_request.account.following?(follow_request.target_account)).to eq true
 | 
			
		||||
      expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#reject_follow_request_salmon' do
 | 
			
		||||
@@ -1427,18 +1294,6 @@ RSpec.describe OStatus::AtomSerializer do
 | 
			
		||||
      verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' }
 | 
			
		||||
      expect(verb.text).to eq OStatus::TagManager::VERBS[:reject]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns element whose rendered view deletes follow request when processed' do
 | 
			
		||||
      follow_request = Fabricate(:follow_request)
 | 
			
		||||
      reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)
 | 
			
		||||
      xml = OStatus::AtomSerializer.render(reject_follow_request_salmon)
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair)
 | 
			
		||||
 | 
			
		||||
      ProcessInteractionService.new.call(envelope, follow_request.account)
 | 
			
		||||
 | 
			
		||||
      expect(follow_request.account.following?(follow_request.target_account)).to eq false
 | 
			
		||||
      expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#object' do
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,6 @@ RSpec.describe AuthorizeFollowService, type: :service do
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -49,19 +49,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
 | 
			
		||||
 
 | 
			
		||||
@@ -28,13 +28,6 @@ RSpec.describe BlockService, type: :service do
 | 
			
		||||
    it 'creates a blocking relation' do
 | 
			
		||||
      expect(sender.blocking?(bob)).to be true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sends a block 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[:block])
 | 
			
		||||
      }).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'remote ActivityPub' do
 | 
			
		||||
 
 | 
			
		||||
@@ -30,13 +30,6 @@ RSpec.describe FavouriteService, type: :service do
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -36,36 +36,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 +45,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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,11 @@ require 'rails_helper'
 | 
			
		||||
RSpec.describe ImportService, type: :service do
 | 
			
		||||
  let!(:account) { Fabricate(:account, locked: false) }
 | 
			
		||||
  let!(:bob)     { Fabricate(:account, username: 'bob', locked: false) }
 | 
			
		||||
  let!(:eve)     { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false) }
 | 
			
		||||
  let!(:eve)     { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    stub_request(:post, "https://example.com/inbox").to_return(status: 200)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'import old-style list of muted users' do
 | 
			
		||||
    subject { ImportService.new }
 | 
			
		||||
@@ -95,7 +99,8 @@ RSpec.describe ImportService, type: :service do
 | 
			
		||||
      let(:import) { Import.create(account: account, type: 'following', data: csv) }
 | 
			
		||||
      it 'follows the listed accounts, including boosts' do
 | 
			
		||||
        subject.call(import)
 | 
			
		||||
        expect(account.following.count).to eq 2
 | 
			
		||||
        expect(account.following.count).to eq 1
 | 
			
		||||
        expect(account.follow_requests.count).to eq 1
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
@@ -106,7 +111,8 @@ RSpec.describe ImportService, type: :service do
 | 
			
		||||
      it 'follows the listed accounts, including notifications' do
 | 
			
		||||
        account.follow!(bob, reblogs: false)
 | 
			
		||||
        subject.call(import)
 | 
			
		||||
        expect(account.following.count).to eq 2
 | 
			
		||||
        expect(account.following.count).to eq 1
 | 
			
		||||
        expect(account.follow_requests.count).to eq 1
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
@@ -117,7 +123,8 @@ RSpec.describe ImportService, type: :service do
 | 
			
		||||
      it 'mutes the listed accounts, including notifications' do
 | 
			
		||||
        account.follow!(bob, reblogs: false)
 | 
			
		||||
        subject.call(import)
 | 
			
		||||
        expect(account.following.count).to eq 2
 | 
			
		||||
        expect(account.following.count).to eq 1
 | 
			
		||||
        expect(account.follow_requests.count).to eq 1
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
@@ -136,9 +143,10 @@ RSpec.describe ImportService, type: :service do
 | 
			
		||||
      let(:import) { Import.create(account: account, type: 'following', data: csv) }
 | 
			
		||||
      it 'follows the listed accounts, respecting boosts' do
 | 
			
		||||
        subject.call(import)
 | 
			
		||||
        expect(account.following.count).to eq 2
 | 
			
		||||
        expect(account.following.count).to eq 1
 | 
			
		||||
        expect(account.follow_requests.count).to eq 1
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false
 | 
			
		||||
        expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -148,9 +156,10 @@ RSpec.describe ImportService, type: :service do
 | 
			
		||||
      it 'mutes the listed accounts, respecting notifications' do
 | 
			
		||||
        account.follow!(bob, reblogs: true)
 | 
			
		||||
        subject.call(import)
 | 
			
		||||
        expect(account.following.count).to eq 2
 | 
			
		||||
        expect(account.following.count).to eq 1
 | 
			
		||||
        expect(account.follow_requests.count).to eq 1
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false
 | 
			
		||||
        expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -160,9 +169,10 @@ RSpec.describe ImportService, type: :service do
 | 
			
		||||
      it 'mutes the listed accounts, respecting notifications' do
 | 
			
		||||
        account.follow!(bob, reblogs: true)
 | 
			
		||||
        subject.call(import)
 | 
			
		||||
        expect(account.following.count).to eq 2
 | 
			
		||||
        expect(account.following.count).to eq 1
 | 
			
		||||
        expect(account.follow_requests.count).to eq 1
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
 | 
			
		||||
        expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false
 | 
			
		||||
        expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -144,7 +144,6 @@ RSpec.describe PostStatusService, type: :service do
 | 
			
		||||
 | 
			
		||||
  it 'gets distributed' do
 | 
			
		||||
    allow(DistributionWorker).to receive(:perform_async)
 | 
			
		||||
    allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async)
 | 
			
		||||
    allow(ActivityPub::DistributionWorker).to receive(:perform_async)
 | 
			
		||||
 | 
			
		||||
    account = Fabricate(:account)
 | 
			
		||||
@@ -152,7 +151,6 @@ RSpec.describe PostStatusService, type: :service do
 | 
			
		||||
    status = subject.call(account, text: "test status update")
 | 
			
		||||
 | 
			
		||||
    expect(DistributionWorker).to have_received(:perform_async).with(status.id)
 | 
			
		||||
    expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id)
 | 
			
		||||
    expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,252 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe ProcessFeedService, type: :service do
 | 
			
		||||
  subject { ProcessFeedService.new }
 | 
			
		||||
 | 
			
		||||
  describe 'processing a feed' do
 | 
			
		||||
    let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) }
 | 
			
		||||
    let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
 | 
			
		||||
      stub_request(:head, "http://kickass.zone/media/2").to_return(:status => 404)
 | 
			
		||||
      stub_request(:head, "http://kickass.zone/media/3").to_return(:status => 404)
 | 
			
		||||
      stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt'))
 | 
			
		||||
      stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt'))
 | 
			
		||||
      stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt'))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when domain does not reject media' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.call(body, account)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'updates remote user\'s account information' do
 | 
			
		||||
        account.reload
 | 
			
		||||
        expect(account.display_name).to eq '::1'
 | 
			
		||||
        expect(account).to have_attached_file(:avatar)
 | 
			
		||||
        expect(account.avatar_file_name).not_to be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates posts' do
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'marks replies as replies' do
 | 
			
		||||
        status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')
 | 
			
		||||
        expect(status.reply?).to be true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'sets account being replied to when possible' do
 | 
			
		||||
        status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')
 | 
			
		||||
        expect(status.in_reply_to_account_id).to eq status.account_id
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'ignores delete statuses unless they existed before' do
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Status')).to be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=12:objectType=Status')).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not create statuses for follows' do
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow')).to be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow')).to be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow')).to be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow')).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not create statuses for favourites' do
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite')).to be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite')).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates posts with media' do
 | 
			
		||||
        status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status')
 | 
			
		||||
 | 
			
		||||
        expect(status).to_not be_nil
 | 
			
		||||
        expect(status.media_attachments.first).to have_attached_file(:file)
 | 
			
		||||
        expect(status.media_attachments.first.image?).to be true
 | 
			
		||||
        expect(status.media_attachments.first.file_file_name).not_to be_nil
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when domain is set to reject media' do
 | 
			
		||||
      let!(:domain_block) { Fabricate(:domain_block, domain: 'kickass.zone', reject_media: true) }
 | 
			
		||||
 | 
			
		||||
      before do
 | 
			
		||||
        subject.call(body, account)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'updates remote user\'s account information' do
 | 
			
		||||
        account.reload
 | 
			
		||||
        expect(account.display_name).to eq '::1'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'rejects remote user\'s avatar' do
 | 
			
		||||
        account.reload
 | 
			
		||||
        expect(account.display_name).to eq '::1'
 | 
			
		||||
        expect(account.avatar_file_name).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates posts' do
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil
 | 
			
		||||
        expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'creates posts with remote-only media' do
 | 
			
		||||
        status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status')
 | 
			
		||||
 | 
			
		||||
        expect(status).to_not be_nil
 | 
			
		||||
        expect(status.media_attachments.first.file_file_name).to be_nil
 | 
			
		||||
        expect(status.media_attachments.first.unknown?).to be true
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'does not accept tampered reblogs' do
 | 
			
		||||
    good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
 | 
			
		||||
 | 
			
		||||
    real_body = <<XML
 | 
			
		||||
<?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:overwatch.com,2017-04-27:objectId=4467137: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://overwatch.com/users/tracer</id>
 | 
			
		||||
    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
    <uri>https://overwatch.com/users/tracer</uri>
 | 
			
		||||
    <name>tracer</name>
 | 
			
		||||
  </author>
 | 
			
		||||
  <content type="html">Overwatch rocks</content>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
    stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' })
 | 
			
		||||
 | 
			
		||||
    bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz')
 | 
			
		||||
 | 
			
		||||
    body = <<XML
 | 
			
		||||
<?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:talon.xyz,2017-04-27:objectId=4467137:objectType=Status</id>
 | 
			
		||||
  <published>2017-04-27T13:49:25Z</published>
 | 
			
		||||
  <updated>2017-04-27T13:49:25Z</updated>
 | 
			
		||||
  <author>
 | 
			
		||||
    <id>https://talon.xyz/users/sombra</id>
 | 
			
		||||
    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
    <uri>https://talon.xyz/users/sombra</uri>
 | 
			
		||||
    <name>sombra</name>
 | 
			
		||||
  </author>
 | 
			
		||||
  <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
 | 
			
		||||
  <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
 | 
			
		||||
  <content type="html">Overwatch SUCKS AHAHA</content>
 | 
			
		||||
  <activity:object>
 | 
			
		||||
    <id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
 | 
			
		||||
    <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://overwatch.com/users/tracer</id>
 | 
			
		||||
      <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
      <uri>https://overwatch.com/users/tracer</uri>
 | 
			
		||||
      <name>tracer</name>
 | 
			
		||||
    </author>
 | 
			
		||||
    <content type="html">Overwatch SUCKS AHAHA</content>
 | 
			
		||||
    <link rel="alternate" type="text/html" href="https://overwatch.com/users/tracer/updates/1" />
 | 
			
		||||
  </activity:object>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
    created_statuses = subject.call(body, bad_actor)
 | 
			
		||||
 | 
			
		||||
    expect(created_statuses.first.reblog?).to be true
 | 
			
		||||
    expect(created_statuses.first.account_id).to eq bad_actor.id
 | 
			
		||||
    expect(created_statuses.first.reblog.account_id).to eq good_actor.id
 | 
			
		||||
    expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'ignores reblogs if it failed to retrieve reblogged statuses' do
 | 
			
		||||
    stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404)
 | 
			
		||||
 | 
			
		||||
    actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
 | 
			
		||||
 | 
			
		||||
    body = <<XML
 | 
			
		||||
<?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:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
 | 
			
		||||
  <published>2017-04-27T13:49:25Z</published>
 | 
			
		||||
  <updated>2017-04-27T13:49:25Z</updated>
 | 
			
		||||
  <author>
 | 
			
		||||
    <id>https://overwatch.com/users/tracer</id>
 | 
			
		||||
    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
    <uri>https://overwatch.com/users/tracer</uri>
 | 
			
		||||
    <name>tracer</name>
 | 
			
		||||
  </author>
 | 
			
		||||
  <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
 | 
			
		||||
  <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
 | 
			
		||||
  <content type="html">Overwatch rocks</content>
 | 
			
		||||
  <activity:object>
 | 
			
		||||
    <id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
 | 
			
		||||
    <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://overwatch.com/users/tracer</id>
 | 
			
		||||
      <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
      <uri>https://overwatch.com/users/tracer</uri>
 | 
			
		||||
      <name>tracer</name>
 | 
			
		||||
    </author>
 | 
			
		||||
    <content type="html">Overwatch rocks</content>
 | 
			
		||||
    <link rel="alternate" type="text/html" href="https://overwatch.com/users/tracer/updates/1" />
 | 
			
		||||
  </activity:object>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
    created_statuses = subject.call(body, actor)
 | 
			
		||||
 | 
			
		||||
    expect(created_statuses).to eq []
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'ignores statuses with an out-of-order delete' do
 | 
			
		||||
    sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
 | 
			
		||||
 | 
			
		||||
    delete_body = <<XML
 | 
			
		||||
<?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:overwatch.com,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/delete</activity:verb>
 | 
			
		||||
  <author>
 | 
			
		||||
    <id>https://overwatch.com/users/tracer</id>
 | 
			
		||||
    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
    <uri>https://overwatch.com/users/tracer</uri>
 | 
			
		||||
    <name>tracer</name>
 | 
			
		||||
  </author>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
    status_body = <<XML
 | 
			
		||||
<?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:overwatch.com,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://overwatch.com/users/tracer</id>
 | 
			
		||||
    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
 | 
			
		||||
    <uri>https://overwatch.com/users/tracer</uri>
 | 
			
		||||
    <name>tracer</name>
 | 
			
		||||
  </author>
 | 
			
		||||
  <content type="html">Overwatch rocks</content>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
    subject.call(delete_body, sender)
 | 
			
		||||
    created_statuses = subject.call(status_body, sender)
 | 
			
		||||
 | 
			
		||||
    expect(created_statuses).to be_empty
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,151 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe ProcessInteractionService, type: :service do
 | 
			
		||||
  let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account }
 | 
			
		||||
  let(:sender)   { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 | 
			
		||||
  let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') }
 | 
			
		||||
 | 
			
		||||
  subject { ProcessInteractionService.new }
 | 
			
		||||
 | 
			
		||||
  describe 'status delete slap' do
 | 
			
		||||
    let(:remote_status) { Fabricate(:status, account: remote_sender) }
 | 
			
		||||
    let(:envelope) { OStatus2::Salmon.new.pack(payload, sender.keypair) }
 | 
			
		||||
    let(:payload) {
 | 
			
		||||
      <<~XML
 | 
			
		||||
        <entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
 | 
			
		||||
          <author>
 | 
			
		||||
            <email>carol@localdomain.com</email>
 | 
			
		||||
            <name>carol</name>
 | 
			
		||||
            <uri>https://webdomain.com/users/carol</uri>
 | 
			
		||||
          </author>
 | 
			
		||||
 | 
			
		||||
          <id>#{remote_status.id}</id>
 | 
			
		||||
          <activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb>
 | 
			
		||||
        </entry>
 | 
			
		||||
      XML
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      receiver.update(locked: true)
 | 
			
		||||
      remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'deletes a record' do
 | 
			
		||||
      expect(RemovalWorker).to receive(:perform_async).with(remote_status.id)
 | 
			
		||||
      subject.call(envelope, receiver)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'follow request slap' do
 | 
			
		||||
    before do
 | 
			
		||||
      receiver.update(locked: true)
 | 
			
		||||
 | 
			
		||||
      payload = <<XML
 | 
			
		||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
 | 
			
		||||
  <author>
 | 
			
		||||
    <name>bob</name>
 | 
			
		||||
    <uri>https://cb6e6126.ngrok.io/users/bob</uri>
 | 
			
		||||
  </author>
 | 
			
		||||
 | 
			
		||||
  <id>someIdHere</id>
 | 
			
		||||
  <activity:verb>http://activitystrea.ms/schema/1.0/request-friend</activity:verb>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(payload, sender.keypair)
 | 
			
		||||
      subject.call(envelope, receiver)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'creates a record' do
 | 
			
		||||
      expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'follow request slap from known remote user identified by email' do
 | 
			
		||||
    before do
 | 
			
		||||
      receiver.update(locked: true)
 | 
			
		||||
      # Copy already-generated key
 | 
			
		||||
      remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key)
 | 
			
		||||
 | 
			
		||||
      payload = <<XML
 | 
			
		||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
 | 
			
		||||
  <author>
 | 
			
		||||
    <email>carol@localdomain.com</email>
 | 
			
		||||
    <name>carol</name>
 | 
			
		||||
    <uri>https://webdomain.com/users/carol</uri>
 | 
			
		||||
  </author>
 | 
			
		||||
 | 
			
		||||
  <id>someIdHere</id>
 | 
			
		||||
  <activity:verb>http://activitystrea.ms/schema/1.0/request-friend</activity:verb>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(payload, remote_sender.keypair)
 | 
			
		||||
      subject.call(envelope, receiver)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'creates a record' do
 | 
			
		||||
      expect(FollowRequest.find_by(account: remote_sender, target_account: receiver)).to_not be_nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'follow request authorization slap' do
 | 
			
		||||
    before do
 | 
			
		||||
      receiver.update(locked: true)
 | 
			
		||||
      FollowRequest.create(account: sender, target_account: receiver)
 | 
			
		||||
 | 
			
		||||
      payload = <<XML
 | 
			
		||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
 | 
			
		||||
  <author>
 | 
			
		||||
    <name>alice</name>
 | 
			
		||||
    <uri>https://cb6e6126.ngrok.io/users/alice</uri>
 | 
			
		||||
  </author>
 | 
			
		||||
 | 
			
		||||
  <id>someIdHere</id>
 | 
			
		||||
  <activity:verb>http://activitystrea.ms/schema/1.0/authorize</activity:verb>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair)
 | 
			
		||||
      subject.call(envelope, sender)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'creates a follow relationship' do
 | 
			
		||||
      expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'removes the follow request' do
 | 
			
		||||
      expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'follow request rejection slap' do
 | 
			
		||||
    before do
 | 
			
		||||
      receiver.update(locked: true)
 | 
			
		||||
      FollowRequest.create(account: sender, target_account: receiver)
 | 
			
		||||
 | 
			
		||||
      payload = <<XML
 | 
			
		||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
 | 
			
		||||
  <author>
 | 
			
		||||
    <name>alice</name>
 | 
			
		||||
    <uri>https://cb6e6126.ngrok.io/users/alice</uri>
 | 
			
		||||
  </author>
 | 
			
		||||
 | 
			
		||||
  <id>someIdHere</id>
 | 
			
		||||
  <activity:verb>http://activitystrea.ms/schema/1.0/reject</activity:verb>
 | 
			
		||||
</entry>
 | 
			
		||||
XML
 | 
			
		||||
 | 
			
		||||
      envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair)
 | 
			
		||||
      subject.call(envelope, sender)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not create a follow relationship' do
 | 
			
		||||
      expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'removes the follow request' do
 | 
			
		||||
      expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -18,10 +18,6 @@ RSpec.describe ProcessMentionsService, type: :service do
 | 
			
		||||
    it 'creates a mention' do
 | 
			
		||||
      expect(remote_user.mentions.where(status: status).count).to eq 1
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'posts to remote user\'s Salmon end point' do
 | 
			
		||||
      expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'OStatus with private toot' do
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe Pubsubhubbub::SubscribeService, type: :service do
 | 
			
		||||
  describe '#call' do
 | 
			
		||||
    subject { described_class.new }
 | 
			
		||||
    let(:user_account) { Fabricate(:account) }
 | 
			
		||||
 | 
			
		||||
    context 'with a nil account' do
 | 
			
		||||
      it 'returns the invalid topic status results' do
 | 
			
		||||
        result = service_call(account: nil)
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq invalid_topic_status
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with an invalid callback url' do
 | 
			
		||||
      it 'returns invalid callback status when callback is blank' do
 | 
			
		||||
        result = service_call(callback: '')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq invalid_callback_status
 | 
			
		||||
      end
 | 
			
		||||
      it 'returns invalid callback status when callback is not a URI' do
 | 
			
		||||
        result = service_call(callback: 'invalid-hostname')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq invalid_callback_status
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with a blocked domain in the callback' do
 | 
			
		||||
      it 'returns callback not allowed' do
 | 
			
		||||
        Fabricate(:domain_block, domain: 'test.host', severity: :suspend)
 | 
			
		||||
        result = service_call(callback: 'https://test.host/api')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq not_allowed_callback_status
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with a valid account and callback' do
 | 
			
		||||
      it 'returns success status and confirms subscription' do
 | 
			
		||||
        allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil)
 | 
			
		||||
        subscription = Fabricate(:subscription, account: user_account)
 | 
			
		||||
 | 
			
		||||
        result = service_call(callback: subscription.callback_url)
 | 
			
		||||
        expect(result).to eq success_status
 | 
			
		||||
        expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'subscribe', 'asdf', 3600)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def service_call(account: user_account, callback: 'https://callback.host', secret: 'asdf', lease_seconds: 3600)
 | 
			
		||||
    subject.call(account, callback, secret, lease_seconds)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def invalid_topic_status
 | 
			
		||||
    ['Invalid topic URL', 422]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def invalid_callback_status
 | 
			
		||||
    ['Invalid callback URL', 422]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def not_allowed_callback_status
 | 
			
		||||
    ['Callback URL not allowed', 403]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def success_status
 | 
			
		||||
    ['', 202]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe Pubsubhubbub::UnsubscribeService, type: :service do
 | 
			
		||||
  describe '#call' do
 | 
			
		||||
    subject { described_class.new }
 | 
			
		||||
 | 
			
		||||
    context 'with a nil account' do
 | 
			
		||||
      it 'returns an invalid topic status' do
 | 
			
		||||
        result = subject.call(nil, 'callback.host')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq invalid_topic_status
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with a valid account' do
 | 
			
		||||
      let(:account) { Fabricate(:account) }
 | 
			
		||||
 | 
			
		||||
      it 'returns a valid topic status and does not run confirm when no subscription' do
 | 
			
		||||
        allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil)
 | 
			
		||||
        result = subject.call(account, 'callback.host')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq valid_topic_status
 | 
			
		||||
        expect(Pubsubhubbub::ConfirmationWorker).not_to have_received(:perform_async)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns a valid topic status and does run confirm when there is a subscription' do
 | 
			
		||||
        subscription = Fabricate(:subscription, account: account, callback_url: 'callback.host')
 | 
			
		||||
        allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil)
 | 
			
		||||
        result = subject.call(account, 'callback.host')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq valid_topic_status
 | 
			
		||||
        expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'unsubscribe')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def invalid_topic_status
 | 
			
		||||
      ['Invalid topic URL', 422]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def valid_topic_status
 | 
			
		||||
      ['', 202]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -46,10 +46,6 @@ RSpec.describe ReblogService, type: :service do
 | 
			
		||||
    it 'creates a reblog' do
 | 
			
		||||
      expect(status.reblogs.count).to eq 1
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sends a Salmon slap for a remote reblog' do
 | 
			
		||||
      expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'ActivityPub' do
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,6 @@ RSpec.describe RejectFollowService, type: :service do
 | 
			
		||||
    it 'does not create follow relation' do
 | 
			
		||||
      expect(bob.following?(sender)).to be false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sends a follow request rejection 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[:reject])
 | 
			
		||||
      }).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'remote ActivityPub' do
 | 
			
		||||
 
 | 
			
		||||
@@ -32,23 +32,10 @@ RSpec.describe RemoveStatusService, type: :service do
 | 
			
		||||
    expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sends PuSH update to PuSH subscribers' do
 | 
			
		||||
    expect(a_request(:post, 'http://example.com/push').with { |req|
 | 
			
		||||
      req.body.match(OStatus::TagManager::VERBS[:delete])
 | 
			
		||||
    }).to have_been_made
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sends delete activity to followers' do
 | 
			
		||||
    expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'sends Salmon slap to previously mentioned users' do
 | 
			
		||||
    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 rebloggers' do
 | 
			
		||||
    expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -6,19 +6,13 @@ RSpec.describe ResolveAccountService, type: :service do
 | 
			
		||||
  before do
 | 
			
		||||
    stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
 | 
			
		||||
    stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404)
 | 
			
		||||
    stub_request(:get, "https://redirected.com/.well-known/host-meta").to_return(request_fixture('redirected.host-meta.txt'))
 | 
			
		||||
    stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
 | 
			
		||||
    stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
 | 
			
		||||
    stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:gargron@redirected.com").to_return(request_fixture('webfinger.txt'))
 | 
			
		||||
    stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker1@redirected.com").to_return(request_fixture('webfinger-hacker1.txt'))
 | 
			
		||||
    stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker2@redirected.com").to_return(request_fixture('webfinger-hacker2.txt'))
 | 
			
		||||
    stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404)
 | 
			
		||||
    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'))
 | 
			
		||||
    stub_request(:get, "https://localdomain.com/.well-known/host-meta").to_return(request_fixture('localdomain-hostmeta.txt'))
 | 
			
		||||
    stub_request(:get, "https://localdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(status: 404)
 | 
			
		||||
    stub_request(:get, "https://webdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(request_fixture('localdomain-webfinger.txt'))
 | 
			
		||||
    stub_request(:get, "https://webdomain.com/users/foo.atom").to_return(request_fixture('localdomain-feed.txt'))
 | 
			
		||||
    stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404)
 | 
			
		||||
    stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt'))
 | 
			
		||||
    stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt'))
 | 
			
		||||
    stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt'))
 | 
			
		||||
    stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'raises error if no such user can be resolved via webfinger' do
 | 
			
		||||
@@ -29,74 +23,7 @@ RSpec.describe ResolveAccountService, type: :service do
 | 
			
		||||
    expect(subject.call('catsrgr8@example.com')).to be_nil
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'prevents hijacking existing accounts' do
 | 
			
		||||
    account = subject.call('hacker1@redirected.com')
 | 
			
		||||
    expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'prevents hijacking inexisting accounts' do
 | 
			
		||||
    expect(subject.call('hacker2@redirected.com')).to be_nil
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'with an OStatus account' do
 | 
			
		||||
    it 'returns an already existing remote account' do
 | 
			
		||||
      old_account      = Fabricate(:account, username: 'gargron', domain: 'quitter.no')
 | 
			
		||||
      returned_account = subject.call('gargron@quitter.no')
 | 
			
		||||
 | 
			
		||||
      expect(old_account.id).to eq returned_account.id
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a new remote account' do
 | 
			
		||||
      account = subject.call('gargron@quitter.no')
 | 
			
		||||
 | 
			
		||||
      expect(account.username).to eq 'gargron'
 | 
			
		||||
      expect(account.domain).to eq 'quitter.no'
 | 
			
		||||
      expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'follows a legitimate account redirection' do
 | 
			
		||||
      account = subject.call('gargron@redirected.com')
 | 
			
		||||
 | 
			
		||||
      expect(account.username).to eq 'gargron'
 | 
			
		||||
      expect(account.domain).to eq 'quitter.no'
 | 
			
		||||
      expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns a new remote account' do
 | 
			
		||||
      account = subject.call('foo@localdomain.com')
 | 
			
		||||
 | 
			
		||||
      expect(account.username).to eq 'foo'
 | 
			
		||||
      expect(account.domain).to eq 'localdomain.com'
 | 
			
		||||
      expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'with an ActivityPub account' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt'))
 | 
			
		||||
      stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt'))
 | 
			
		||||
      stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt'))
 | 
			
		||||
      stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'fallback to OStatus if actor json could not be fetched' do
 | 
			
		||||
      stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404)
 | 
			
		||||
 | 
			
		||||
      account = subject.call('foo@ap.example.com')
 | 
			
		||||
 | 
			
		||||
      expect(account.ostatus?).to eq true
 | 
			
		||||
      expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'fallback to OStatus if actor json did not have inbox_url' do
 | 
			
		||||
      stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt'))
 | 
			
		||||
 | 
			
		||||
      account = subject.call('foo@ap.example.com')
 | 
			
		||||
 | 
			
		||||
      expect(account.ostatus?).to eq true
 | 
			
		||||
      expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns new remote account' do
 | 
			
		||||
      account = subject.call('foo@ap.example.com')
 | 
			
		||||
 | 
			
		||||
@@ -124,13 +51,14 @@ RSpec.describe ResolveAccountService, type: :service do
 | 
			
		||||
  it 'processes one remote account at a time using locks' do
 | 
			
		||||
    wait_for_start = true
 | 
			
		||||
    fail_occurred  = false
 | 
			
		||||
    return_values  = []
 | 
			
		||||
    return_values  = Concurrent::Array.new
 | 
			
		||||
 | 
			
		||||
    threads = Array.new(5) do
 | 
			
		||||
      Thread.new do
 | 
			
		||||
        true while wait_for_start
 | 
			
		||||
 | 
			
		||||
        begin
 | 
			
		||||
          return_values << described_class.new.call('foo@localdomain.com')
 | 
			
		||||
          return_values << described_class.new.call('foo@ap.example.com')
 | 
			
		||||
        rescue ActiveRecord::RecordNotUnique
 | 
			
		||||
          fail_occurred = true
 | 
			
		||||
        end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe SendInteractionService, type: :service do
 | 
			
		||||
  subject { SendInteractionService.new }
 | 
			
		||||
 | 
			
		||||
  it 'sends an XML envelope to the Salmon end point of remote user'
 | 
			
		||||
end
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe SubscribeService, type: :service do
 | 
			
		||||
  let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') }
 | 
			
		||||
  subject { SubscribeService.new }
 | 
			
		||||
 | 
			
		||||
  it 'sends subscription request to PuSH hub' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 202)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
    expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'generates and keeps PuSH secret on successful call' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 202)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
    expect(account.secret).to_not be_blank
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'fails silently if PuSH hub forbids subscription' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 403)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'fails silently if PuSH hub is not found' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 404)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'fails loudly if there is a network error' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error)
 | 
			
		||||
    expect { subject.call(account) }.to raise_error HTTP::Error
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'fails loudly if PuSH hub is unavailable' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 503)
 | 
			
		||||
    expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'fails loudly if rate limited' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 429)
 | 
			
		||||
    expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -30,13 +30,6 @@ RSpec.describe UnblockService, type: :service do
 | 
			
		||||
    it 'destroys the blocking relation' do
 | 
			
		||||
      expect(sender.blocking?(bob)).to be false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sends an unblock 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[:unblock])
 | 
			
		||||
      }).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'remote ActivityPub' do
 | 
			
		||||
 
 | 
			
		||||
@@ -30,13 +30,6 @@ RSpec.describe UnfollowService, type: :service do
 | 
			
		||||
    it 'destroys the following relation' do
 | 
			
		||||
      expect(sender.following?(bob)).to be false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sends an unfollow 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[:unfollow])
 | 
			
		||||
      }).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe 'remote ActivityPub' do
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe UnsubscribeService, type: :service do
 | 
			
		||||
  let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') }
 | 
			
		||||
  subject { UnsubscribeService.new }
 | 
			
		||||
 | 
			
		||||
  it 'removes the secret and resets expiration on account' do
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 204)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
    account.reload
 | 
			
		||||
 | 
			
		||||
    expect(account.secret).to be_blank
 | 
			
		||||
    expect(account.subscription_expires_at).to be_blank
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'logs error on subscription failure' do
 | 
			
		||||
    logger = stub_logger
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_return(status: 404)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
 | 
			
		||||
    expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'logs error on connection failure' do
 | 
			
		||||
    logger = stub_logger
 | 
			
		||||
    stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error)
 | 
			
		||||
    subject.call(account)
 | 
			
		||||
 | 
			
		||||
    expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def stub_logger
 | 
			
		||||
    double(debug: nil).tap do |logger|
 | 
			
		||||
      allow(Rails).to receive(:logger).and_return(logger)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe UpdateRemoteProfileService, type: :service do
 | 
			
		||||
  let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) }
 | 
			
		||||
 | 
			
		||||
  subject { UpdateRemoteProfileService.new }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt'))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'with updated details' do
 | 
			
		||||
    let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      subject.call(xml, remote_account)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'downloads new avatar' do
 | 
			
		||||
      expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets the avatar remote url' do
 | 
			
		||||
      expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets display name' do
 | 
			
		||||
      expect(remote_account.reload.display_name).to eq 'DIGITAL CAT'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets note' do
 | 
			
		||||
      expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'with unchanged details' do
 | 
			
		||||
    let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      subject.call(xml, remote_account)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not re-download avatar' do
 | 
			
		||||
      expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets the avatar remote url' do
 | 
			
		||||
      expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets display name' do
 | 
			
		||||
      expect(remote_account.reload.display_name).to eq 'DIGITAL CAT'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets note' do
 | 
			
		||||
      expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'with updated details from a domain set to reject media' do
 | 
			
		||||
    let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') }
 | 
			
		||||
    let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', reject_media: true) }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      subject.call(xml, remote_account)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not the avatar remote url' do
 | 
			
		||||
      expect(remote_account.reload.avatar_remote_url).to be_nil
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets display name' do
 | 
			
		||||
      expect(remote_account.reload.display_name).to eq 'DIGITAL CAT'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sets note' do
 | 
			
		||||
      expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not set store the avatar' do
 | 
			
		||||
      expect(remote_account.reload.avatar_file_name).to be_nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe AfterRemoteFollowRequestWorker do
 | 
			
		||||
  subject { described_class.new }
 | 
			
		||||
  let(:follow_request) { Fabricate(:follow_request) }
 | 
			
		||||
  describe 'perform' do
 | 
			
		||||
    context 'when the follow_request does not exist' do
 | 
			
		||||
      it 'catches a raise and returns true' do
 | 
			
		||||
        allow(FollowService).to receive(:new)
 | 
			
		||||
        result = subject.perform('aaa')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq(true)
 | 
			
		||||
        expect(FollowService).not_to have_received(:new)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the account cannot be updated' do
 | 
			
		||||
      it 'returns nil and does not call service when account is nil' do
 | 
			
		||||
        allow(FollowService).to receive(:new)
 | 
			
		||||
        service = double(call: nil)
 | 
			
		||||
        allow(FetchRemoteAccountService).to receive(:new).and_return(service)
 | 
			
		||||
 | 
			
		||||
        result = subject.perform(follow_request.id)
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_nil
 | 
			
		||||
        expect(FollowService).not_to have_received(:new)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns nil and does not call service when account is locked' do
 | 
			
		||||
        allow(FollowService).to receive(:new)
 | 
			
		||||
        service = double(call: double(locked?: true))
 | 
			
		||||
        allow(FetchRemoteAccountService).to receive(:new).and_return(service)
 | 
			
		||||
 | 
			
		||||
        result = subject.perform(follow_request.id)
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_nil
 | 
			
		||||
        expect(FollowService).not_to have_received(:new)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the account is updated' do
 | 
			
		||||
      it 'calls the follow service and destroys the follow' do
 | 
			
		||||
        follow_service = double(call: nil)
 | 
			
		||||
        allow(FollowService).to receive(:new).and_return(follow_service)
 | 
			
		||||
        account = Fabricate(:account, locked: false)
 | 
			
		||||
        service = double(call: account)
 | 
			
		||||
        allow(FetchRemoteAccountService).to receive(:new).and_return(service)
 | 
			
		||||
 | 
			
		||||
        result = subject.perform(follow_request.id)
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_nil
 | 
			
		||||
        expect(follow_service).to have_received(:call).with(follow_request.account, account.acct)
 | 
			
		||||
        expect { follow_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe AfterRemoteFollowWorker do
 | 
			
		||||
  subject { described_class.new }
 | 
			
		||||
  let(:follow) { Fabricate(:follow) }
 | 
			
		||||
  describe 'perform' do
 | 
			
		||||
    context 'when the follow does not exist' do
 | 
			
		||||
      it 'catches a raise and returns true' do
 | 
			
		||||
        allow(FollowService).to receive(:new)
 | 
			
		||||
        result = subject.perform('aaa')
 | 
			
		||||
 | 
			
		||||
        expect(result).to eq(true)
 | 
			
		||||
        expect(FollowService).not_to have_received(:new)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the account cannot be updated' do
 | 
			
		||||
      it 'returns nil and does not call service when account is nil' do
 | 
			
		||||
        allow(FollowService).to receive(:new)
 | 
			
		||||
        service = double(call: nil)
 | 
			
		||||
        allow(FetchRemoteAccountService).to receive(:new).and_return(service)
 | 
			
		||||
 | 
			
		||||
        result = subject.perform(follow.id)
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_nil
 | 
			
		||||
        expect(FollowService).not_to have_received(:new)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns nil and does not call service when account is not locked' do
 | 
			
		||||
        allow(FollowService).to receive(:new)
 | 
			
		||||
        service = double(call: double(locked?: false))
 | 
			
		||||
        allow(FetchRemoteAccountService).to receive(:new).and_return(service)
 | 
			
		||||
 | 
			
		||||
        result = subject.perform(follow.id)
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_nil
 | 
			
		||||
        expect(FollowService).not_to have_received(:new)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the account is updated' do
 | 
			
		||||
      it 'calls the follow service and destroys the follow' do
 | 
			
		||||
        follow_service = double(call: nil)
 | 
			
		||||
        allow(FollowService).to receive(:new).and_return(follow_service)
 | 
			
		||||
        account = Fabricate(:account, locked: true)
 | 
			
		||||
        service = double(call: account)
 | 
			
		||||
        allow(FetchRemoteAccountService).to receive(:new).and_return(service)
 | 
			
		||||
 | 
			
		||||
        result = subject.perform(follow.id)
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_nil
 | 
			
		||||
        expect(follow_service).to have_received(:call).with(follow.account, account.acct)
 | 
			
		||||
        expect { follow.reload }.to raise_error(ActiveRecord::RecordNotFound)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,88 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe Pubsubhubbub::ConfirmationWorker do
 | 
			
		||||
  include RoutingHelper
 | 
			
		||||
 | 
			
		||||
  subject { described_class.new }
 | 
			
		||||
 | 
			
		||||
  let!(:alice) { Fabricate(:account, username: 'alice') }
 | 
			
		||||
  let!(:subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example.com/api', confirmed: false, expires_at: 3.days.from_now, secret: nil) }
 | 
			
		||||
 | 
			
		||||
  describe 'perform' do
 | 
			
		||||
    describe 'with subscribe mode' do
 | 
			
		||||
      it 'confirms and updates subscription when challenge matches' do
 | 
			
		||||
        stub_random_value
 | 
			
		||||
        stub_request(:get, url_for_mode('subscribe'))
 | 
			
		||||
          .with(headers: http_headers)
 | 
			
		||||
          .to_return(status: 200, body: challenge_value, headers: {})
 | 
			
		||||
 | 
			
		||||
        seconds = 10.days.seconds.to_i
 | 
			
		||||
        subject.perform(subscription.id, 'subscribe', 'asdf', seconds)
 | 
			
		||||
 | 
			
		||||
        subscription.reload
 | 
			
		||||
        expect(subscription.secret).to eq 'asdf'
 | 
			
		||||
        expect(subscription.confirmed).to eq true
 | 
			
		||||
        expect(subscription.expires_at).to be_within(5).of(10.days.from_now)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not update subscription when challenge does not match' do
 | 
			
		||||
        stub_random_value
 | 
			
		||||
        stub_request(:get, url_for_mode('subscribe'))
 | 
			
		||||
          .with(headers: http_headers)
 | 
			
		||||
          .to_return(status: 200, body: 'wrong value', headers: {})
 | 
			
		||||
 | 
			
		||||
        seconds = 10.days.seconds.to_i
 | 
			
		||||
        subject.perform(subscription.id, 'subscribe', 'asdf', seconds)
 | 
			
		||||
 | 
			
		||||
        subscription.reload
 | 
			
		||||
        expect(subscription.secret).to be_blank
 | 
			
		||||
        expect(subscription.confirmed).to eq false
 | 
			
		||||
        expect(subscription.expires_at).to be_within(5).of(3.days.from_now)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'with unsubscribe mode' do
 | 
			
		||||
      it 'confirms and destroys subscription when challenge matches' do
 | 
			
		||||
        stub_random_value
 | 
			
		||||
        stub_request(:get, url_for_mode('unsubscribe'))
 | 
			
		||||
          .with(headers: http_headers)
 | 
			
		||||
          .to_return(status: 200, body: challenge_value, headers: {})
 | 
			
		||||
 | 
			
		||||
        seconds = 10.days.seconds.to_i
 | 
			
		||||
        subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds)
 | 
			
		||||
 | 
			
		||||
        expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not destroy subscription when challenge does not match' do
 | 
			
		||||
        stub_random_value
 | 
			
		||||
        stub_request(:get, url_for_mode('unsubscribe'))
 | 
			
		||||
          .with(headers: http_headers)
 | 
			
		||||
          .to_return(status: 200, body: 'wrong value', headers: {})
 | 
			
		||||
 | 
			
		||||
        seconds = 10.days.seconds.to_i
 | 
			
		||||
        subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds)
 | 
			
		||||
 | 
			
		||||
        expect { subscription.reload }.not_to raise_error
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def url_for_mode(mode)
 | 
			
		||||
    "http://example.com/api?hub.challenge=#{challenge_value}&hub.lease_seconds=863999&hub.mode=#{mode}&hub.topic=https://#{Rails.configuration.x.local_domain}/users/alice.atom"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def stub_random_value
 | 
			
		||||
    allow(SecureRandom).to receive(:hex).and_return(challenge_value)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def challenge_value
 | 
			
		||||
    '1a2s3d4f'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def http_headers
 | 
			
		||||
    { 'Connection' => 'close', 'Host' => 'example.com' }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe Pubsubhubbub::DeliveryWorker do
 | 
			
		||||
  include RoutingHelper
 | 
			
		||||
  subject { described_class.new }
 | 
			
		||||
 | 
			
		||||
  let(:payload) { 'test' }
 | 
			
		||||
 | 
			
		||||
  describe 'perform' do
 | 
			
		||||
    it 'raises when subscription does not exist' do
 | 
			
		||||
      expect { subject.perform 123, payload }.to raise_error(ActiveRecord::RecordNotFound)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'does not attempt to deliver when domain blocked' do
 | 
			
		||||
      _domain_block = Fabricate(:domain_block, domain: 'example.com', severity: :suspend)
 | 
			
		||||
      subscription = Fabricate(:subscription, callback_url: 'https://example.com/api', last_successful_delivery_at: 2.days.ago)
 | 
			
		||||
 | 
			
		||||
      subject.perform(subscription.id, payload)
 | 
			
		||||
 | 
			
		||||
      expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(2.days.ago)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'raises when request fails' do
 | 
			
		||||
      subscription = Fabricate(:subscription)
 | 
			
		||||
 | 
			
		||||
      stub_request_to_respond_with(subscription, 500)
 | 
			
		||||
      expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'updates subscriptions when delivery succeeds' do
 | 
			
		||||
      subscription = Fabricate(:subscription)
 | 
			
		||||
 | 
			
		||||
      stub_request_to_respond_with(subscription, 200)
 | 
			
		||||
      subject.perform(subscription.id, payload)
 | 
			
		||||
 | 
			
		||||
      expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'updates subscription without a secret when delivery succeeds' do
 | 
			
		||||
      subscription = Fabricate(:subscription, secret: nil)
 | 
			
		||||
 | 
			
		||||
      stub_request_to_respond_with(subscription, 200)
 | 
			
		||||
      subject.perform(subscription.id, payload)
 | 
			
		||||
 | 
			
		||||
      expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def stub_request_to_respond_with(subscription, code)
 | 
			
		||||
      stub_request(:post, 'http://example.com/callback')
 | 
			
		||||
        .with(body: payload, headers: expected_headers(subscription))
 | 
			
		||||
        .to_return(status: code, body: '', headers: {})
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def expected_headers(subscription)
 | 
			
		||||
      {
 | 
			
		||||
        'Connection' => 'close',
 | 
			
		||||
        'Content-Type' => 'application/atom+xml',
 | 
			
		||||
        'Host' => 'example.com',
 | 
			
		||||
        'Link' => "<https://#{Rails.configuration.x.local_domain}/api/push>; rel=\"hub\", <https://#{Rails.configuration.x.local_domain}/users/#{subscription.account.username}.atom>; rel=\"self\"",
 | 
			
		||||
      }.tap do |basic|
 | 
			
		||||
        known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload)
 | 
			
		||||
        basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret?
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user