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 ResolveAccountServicemaster
parent
c07cca4727
commit
23aeef52cc
@ -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 |
@ -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 |
@ -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,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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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
Loading…
Reference in new issue