Commiting

This commit is contained in:
mgabdev
2020-11-15 12:48:32 -06:00
parent 62515bbaee
commit fb612f60c8
1011 changed files with 3507 additions and 49604 deletions

View File

@@ -68,8 +68,6 @@ class AccountSearchService < BaseService
@_exact_match ||= begin
if domain_is_local?
search_from.without_suspended.find_local(query_username)
else
search_from.without_suspended.find_remote(query_username, query_domain)
end
end
end

View File

@@ -1,54 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FetchFeaturedCollectionService < BaseService
include JsonLdHelper
def call(account)
return if account.featured_collection_url.blank?
@account = account
@json = fetch_resource(@account.featured_collection_url, true)
return unless supported_context?
return if @account.suspended? || @account.local?
case @json['type']
when 'Collection', 'CollectionPage'
process_items @json['items']
when 'OrderedCollection', 'OrderedCollectionPage'
process_items @json['orderedItems']
end
end
private
def process_items(items)
status_ids = items.map { |item| value_or_id(item) }
.reject { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
.map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) }
.compact
.select { |status| status.account_id == @account.id }
.map(&:id)
to_remove = []
to_add = status_ids
StatusPin.where(account: @account).pluck(:status_id).each do |status_id|
if status_ids.include?(status_id)
to_add.delete(status_id)
else
to_remove << status_id
end
end
StatusPin.where(account: @account, status_id: to_remove).delete_all unless to_remove.empty?
to_add.each do |status_id|
StatusPin.create!(account: @account, status_id: status_id)
end
end
def supported_context?
super(@json)
end
end

View File

@@ -1,62 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FetchRemoteAccountService < BaseService
include JsonLdHelper
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false)
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
@json = if prefetched_body.nil?
fetch_resource(uri, id)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?)
@uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
return unless only_key || verified_webfinger?
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key)
rescue Oj::ParseError
nil
end
private
def verified_webfinger?
webfinger = Goldfinger.finger("acct:#{@username}@#{@domain}")
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
return webfinger.link('self')&.href == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
webfinger = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}")
@username, @domain = split_acct(webfinger.subject)
self_reference = webfinger.link('self')
return false unless @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
return false if self_reference&.href != @uri
true
rescue Goldfinger::Error
false
end
def split_acct(acct)
acct.gsub(/\Aacct:/, '').split('@')
end
def supported_context?
super(@json)
end
def expected_type?
equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
end
end

View File

@@ -1,60 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FetchRemoteKeyService < BaseService
include JsonLdHelper
# Returns account that owns the key
def call(uri, id: true, prefetched_body: nil)
if prefetched_body.nil?
if id
@json = fetch_resource_without_id_validation(uri)
if person?
@json = fetch_resource(@json['id'], true)
elsif uri != @json['id']
return
end
else
@json = fetch_resource(uri, id)
end
else
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
return unless supported_context?(@json) && expected_type?
return find_account(@json['id'], @json) if person?
@owner = fetch_resource(owner_uri, true)
return unless supported_context?(@owner) && confirmed_owner?
find_account(owner_uri, @owner)
end
private
def find_account(uri, prefetched_body)
account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body)
account
end
def expected_type?
person? || public_key?
end
def person?
equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
end
def public_key?
@json['publicKeyPem'].present? && @json['owner'].present?
end
def owner_uri
@owner_uri ||= value_or_id(@json['owner'])
end
def confirmed_owner?
equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && value_or_id(@owner['publicKey']) == @json['id']
end
end

View File

@@ -1,11 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FetchRemotePollService < BaseService
include JsonLdHelper
def call(poll, on_behalf_of = nil)
json = fetch_resource(poll.status.uri, true, on_behalf_of)
return unless supported_context?(json)
ActivityPub::ProcessPollService.new.call(poll, json)
end
end

View File

@@ -1,52 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FetchRemoteStatusService < BaseService
include JsonLdHelper
# Should be called when uri has already been checked for locality
def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil)
@json = if prefetched_body.nil?
fetch_resource(uri, id, on_behalf_of)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
return unless supported_context? && expected_type?
return if actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id)
actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account)
actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor)
return if actor.nil? || actor.suspended?
ActivityPub::Activity.factory(activity_json, actor).perform
end
private
def activity_json
{ 'type' => 'Create', 'actor' => actor_id, 'object' => @json }
end
def actor_id
value_or_id(first_of_value(@json['attributedTo']))
end
def trustworthy_attribution?(uri, attributed_to)
return false if uri.nil? || attributed_to.nil?
Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero?
end
def supported_context?
super(@json)
end
def expected_type?
equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
end
def needs_update(actor)
actor.possibly_stale?
end
end

View File

@@ -1,49 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FetchRepliesService < BaseService
include JsonLdHelper
def call(parent_status, collection_or_uri, allow_synchronous_requests = true)
@account = parent_status.account
@allow_synchronous_requests = allow_synchronous_requests
@items = collection_items(collection_or_uri)
return if @items.nil?
FetchReplyWorker.push_bulk(filtered_replies)
@items
end
private
def collection_items(collection_or_uri)
collection = fetch_collection(collection_or_uri)
return unless collection.is_a?(Hash)
collection = fetch_collection(collection['first']) if collection['first'].present?
return unless collection.is_a?(Hash)
case collection['type']
when 'Collection', 'CollectionPage'
collection['items']
when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems']
end
end
def fetch_collection(collection_or_uri)
return collection_or_uri if collection_or_uri.is_a?(Hash)
return unless @allow_synchronous_requests
return if invalid_origin?(collection_or_uri)
fetch_resource_without_id_validation(collection_or_uri, nil, true)
end
def filtered_replies
# Only fetch replies to the same server as the original status to avoid
# amplification attacks.
# Also limit to 5 fetched replies to limit potential for DoS.
@items.map { |item| value_or_id(item) }.reject { |uri| invalid_origin?(uri) }.take(5)
end
end

View File

@@ -1,276 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ProcessAccountService < BaseService
include JsonLdHelper
# Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain
def call(username, domain, json, options = {})
return if json['inbox'].blank? || unsupported_uri_scheme?(json['id'])
@options = options
@json = json
@uri = @json['id']
@username = username
@domain = domain
@collections = {}
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@account = Account.find_remote(@username, @domain)
@old_public_key = @account&.public_key
@old_protocol = @account&.protocol
create_account if @account.nil?
update_account
process_tags
process_attachments
else
raise GabSocial::RaceConditionError
end
end
return if @account.nil?
after_protocol_change! if protocol_changed?
after_key_change! if key_changed? && !@options[:signed_with_known_key]
clear_tombstones! if key_changed?
unless @options[:only_key]
check_featured_collection! if @account.featured_collection_url.present?
check_links! unless @account.fields.empty?
end
@account
rescue Oj::ParseError
nil
end
private
def create_account
@account = Account.new
@account.protocol = :activitypub
@account.username = @username
@account.domain = @domain
@account.private_key = nil
@account.suspended_at = domain_block.created_at if auto_suspend?
@account.silenced_at = domain_block.created_at if auto_silence?
end
def update_account
@account.last_webfingered_at = Time.now.utc unless @options[:only_key]
@account.protocol = :activitypub
set_immediate_attributes!
set_fetchable_attributes! unless @options[:only_keys]
@account.save_with_optional_media!
end
def set_immediate_attributes!
@account.inbox_url = @json['inbox'] || ''
@account.outbox_url = @json['outbox'] || ''
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
@account.followers_url = @json['followers'] || ''
@account.featured_collection_url = @json['featured'] || ''
@account.url = url || @uri
@account.uri = @uri
@account.display_name = @json['name'] || ''
@account.note = @json['summary'] || ''
@account.locked = @json['manuallyApprovesFollowers'] || false
@account.fields = property_values || {}
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.actor_type = actor_type
end
def set_fetchable_attributes!
@account.avatar_remote_url = image_url('icon') unless skip_download?
@account.header_remote_url = image_url('image') unless skip_download?
@account.public_key = public_key || ''
@account.statuses_count = outbox_total_items if outbox_total_items.present?
@account.following_count = following_total_items if following_total_items.present?
@account.followers_count = followers_total_items if followers_total_items.present?
@account.moved_to_account = @json['movedTo'].present? ? moved_account : nil
end
def after_protocol_change!
ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
end
def after_key_change!
RefollowWorker.perform_async(@account.id)
end
def check_featured_collection!
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
end
def check_links!
VerifyAccountLinksWorker.perform_async(@account.id)
end
def actor_type
if @json['type'].is_a?(Array)
@json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) }
else
@json['type']
end
end
def image_url(key)
value = first_of_value(@json[key])
return if value.nil?
return value['url'] if value.is_a?(Hash)
image = fetch_resource_without_id_validation(value)
image['url'] if image
end
def public_key
value = first_of_value(@json['publicKey'])
return if value.nil?
return value['publicKeyPem'] if value.is_a?(Hash)
key = fetch_resource_without_id_validation(value)
key['publicKeyPem'] if key
end
def url
return if @json['url'].blank?
url_candidate = url_to_href(@json['url'], 'text/html')
if unsupported_uri_scheme?(url_candidate) || mismatching_origin?(url_candidate)
nil
else
url_candidate
end
end
def property_values
return unless @json['attachment'].is_a?(Array)
as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
end
def mismatching_origin?(url)
needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@uri).host
!haystack.casecmp(needle).zero?
end
def outbox_total_items
collection_total_items('outbox')
end
def following_total_items
collection_total_items('following')
end
def followers_total_items
collection_total_items('followers')
end
def collection_total_items(type)
return if @json[type].blank?
return @collections[type] if @collections.key?(type)
collection = fetch_resource_without_id_validation(@json[type])
@collections[type] = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
rescue HTTP::Error, OpenSSL::SSL::SSLError
@collections[type] = nil
end
def moved_account
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true)
account
end
def skip_download?
@account.suspended? || domain_block&.reject_media?
end
def auto_suspend?
domain_block&.suspend?
end
def auto_silence?
domain_block&.silence?
end
def domain_block
return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain)
end
def key_changed?
!@old_public_key.nil? && @old_public_key != @account.public_key
end
def clear_tombstones!
Tombstone.where(account_id: @account.id).delete_all
end
def protocol_changed?
!@old_protocol.nil? && @old_protocol != @account.protocol
end
def lock_options
{ redis: Redis.current, key: "process_account:#{@uri}" }
end
def process_tags
return if @json['tag'].blank?
as_array(@json['tag']).each do |tag|
process_emoji tag if equals_or_includes?(tag['type'], 'Emoji')
end
end
def process_attachments
return if @json['attachment'].blank?
previous_proofs = @account.identity_proofs.to_a
current_proofs = []
as_array(@json['attachment']).each do |attachment|
next unless equals_or_includes?(attachment['type'], 'IdentityProof')
current_proofs << process_identity_proof(attachment)
end
previous_proofs.each do |previous_proof|
next if current_proofs.any? { |current_proof| current_proof.id == previous_proof.id }
previous_proof.delete
end
end
def process_emoji(tag)
return if skip_download?
return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank?
shortcode = tag['name'].delete(':')
image_url = tag['icon']['url']
uri = tag['id']
updated = tag['updated']
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
emoji.image_remote_url = image_url
emoji.save
end
def process_identity_proof(attachment)
provider = attachment['signatureAlgorithm']
provider_username = attachment['name']
token = attachment['signatureValue']
@account.identity_proofs.where(provider: provider, provider_username: provider_username).find_or_create_by(provider: provider, provider_username: provider_username, token: token)
end
end

View File

@@ -1,53 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
def call(body, account, **options)
@account = account
@json = Oj.load(body, mode: :strict)
@options = options
return unless supported_context?
return if different_actor? && verify_account!.nil?
return if @account.suspended? || @account.local?
case @json['type']
when 'Collection', 'CollectionPage'
process_items @json['items']
when 'OrderedCollection', 'OrderedCollectionPage'
process_items @json['orderedItems']
else
process_items [@json]
end
rescue Oj::ParseError
nil
end
private
def different_actor?
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri
end
def process_items(items)
items.reverse_each.map { |item| process_item(item) }.compact
end
def supported_context?
super(@json)
end
def process_item(item)
activity = ActivityPub::Activity.factory(item, @account, @options)
activity&.perform
end
def verify_account!
@options[:relayed_through_account] = @account
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
rescue JSON::LD::JsonLdError => e
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
nil
end
end

View File

@@ -1,60 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ProcessPollService < BaseService
include JsonLdHelper
def call(poll, json)
@json = json
return unless expected_type?
previous_expires_at = poll.expires_at
expires_at = begin
if @json['closed'].is_a?(String)
@json['closed']
elsif !@json['closed'].nil? && !@json['closed'].is_a?(FalseClass)
Time.now.utc
else
@json['endTime']
end
end
items = begin
if @json['anyOf'].is_a?(Array)
@json['anyOf']
else
@json['oneOf']
end
end
latest_options = items.map { |item| item['name'].presence || item['content'] }
# If for some reasons the options were changed, it invalidates all previous
# votes, so we need to remove them
poll.votes.delete_all if latest_options != poll.options
begin
poll.update!(
last_fetched_at: Time.now.utc,
expires_at: expires_at,
options: latest_options,
cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
)
rescue ActiveRecord::StaleObjectError
poll.reload
retry
end
# If the poll had no expiration date set but now has, and people have voted,
# schedule a notification.
if previous_expires_at.nil? && poll.expires_at.present? && poll.votes.exists?
PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id)
end
end
private
def expected_type?
equals_or_includes_any?(@json['type'], %w(Question))
end
end

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
class AfterBlockDomainFromAccountService < BaseService
# This service does not create an AccountDomainBlock record,
# it's meant to be called after such a record has been created
# synchronously, to "clean up"
def call(account, domain)
@account = account
@domain = domain
reject_existing_followers!
reject_pending_follow_requests!
end
private
def reject_existing_followers!
@account.passive_relationships.where(account: Account.where(domain: @domain)).includes(:account).reorder(nil).find_each do |follow|
reject_follow!(follow)
end
end
def reject_pending_follow_requests!
FollowRequest.where(target_account: @account).where(account: Account.where(domain: @domain)).includes(:account).reorder(nil).find_each do |follow_request|
reject_follow!(follow_request)
end
end
def reject_follow!(follow)
follow.destroy
end
end

View File

@@ -4,7 +4,6 @@ class AfterBlockService < BaseService
def call(account, target_account)
clear_home_feed(account, target_account)
clear_notifications(account, target_account)
clear_conversations(account, target_account)
end
private
@@ -13,13 +12,6 @@ class AfterBlockService < BaseService
FeedManager.instance.clear_from_timeline(account, target_account)
end
def clear_conversations(account, target_account)
AccountConversation.where(account: account)
.where('? = ANY(participant_account_ids)', target_account.id)
.in_batches
.destroy_all
end
def clear_notifications(account, target_account)
Notification.where(account: account)
.joins(:follow)

View File

@@ -2,8 +2,6 @@
class AppSignUpService < BaseService
def call(app, params)
return unless allowed_registrations?
user_params = params.slice(:email, :password, :agreement, :locale)
account_params = params.slice(:username)
user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params))
@@ -14,10 +12,4 @@ class AppSignUpService < BaseService
expires_in: Doorkeeper.configuration.access_token_expires_in,
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?)
end
private
def allowed_registrations?
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
end
end

View File

@@ -2,6 +2,7 @@
require 'rubygems/package'
# : todo :
class BackupService < BaseService
attr_reader :account, :backup, :collection

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true
class BatchedRemoveStatusService < BaseService
include StreamEntryRenderer
include Redisable
# Delete given statuses and reblogs of them
@@ -13,15 +12,12 @@ class BatchedRemoveStatusService < BaseService
# @param [Hash] options
# @option [Boolean] :skip_side_effects
def call(statuses, **options)
statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a }
statuses = Status.where(id: statuses.map(&:id)).includes(:account).flat_map { |status| [status] + status.reblogs.includes(:account).to_a }
@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,12 @@ 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
@@ -81,36 +61,4 @@ class BatchedRemoveStatusService < BaseService
end
end
def unpush_from_public_timelines(status)
return unless status.public_visibility?
payload = @json_payloads[status.id]
redis.pipelined do
if status.account.is_pro || status.account.is_donor || status.account.is_investor || status.account.is_verified
redis.publish('timeline:pro', payload)
end
@tags[status.id].each do |hashtag|
redis.publish("timeline:hashtag:#{hashtag}", payload)
redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
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

View File

@@ -1,89 +0,0 @@
# frozen_string_literal: true
class BlockDomainService < BaseService
attr_reader :domain_block
def call(domain_block)
@domain_block = domain_block
process_domain_block!
end
private
def process_domain_block!
clear_media! if domain_block.reject_media?
if domain_block.silence?
silence_accounts!
elsif domain_block.suspend?
suspend_accounts!
end
end
def invalidate_association_caches!
# Normally, associated models of a status are immutable (except for accounts)
# so they are aggressively cached. After updating the media attachments to no
# longer point to a local file, we need to clear the cache to make those
# changes appear in the API and UI
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
end
def silence_accounts!
blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
end
def clear_media!
@affected_status_ids = []
clear_account_images!
clear_account_attachments!
clear_emojos!
invalidate_association_caches!
end
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
def clear_account_images!
blocked_domain_accounts.reorder(nil).find_each do |account|
account.avatar.destroy if account.avatar.exists?
account.header.destroy if account.header.exists?
account.save
end
end
def clear_account_attachments!
media_from_blocked_domain.reorder(nil).find_each do |attachment|
@affected_status_ids << attachment.status_id if attachment.status_id.present?
attachment.file.destroy if attachment.file.exists?
attachment.type = :unknown
attachment.save
end
end
def clear_emojos!
emojis_from_blocked_domains.destroy_all
end
def blocked_domain
domain_block.domain
end
def blocked_domain_accounts
Account.where(domain: blocked_domain)
end
def media_from_blocked_domain
MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil)
end
def emojis_from_blocked_domains
CustomEmoji.where(domain: blocked_domain)
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -1,31 +1,9 @@
# frozen_string_literal: true
class FanOutOnWriteService < BaseService
# Push a status into home and mentions feeds
# @param [Status] status
def call(status)
raise GabSocial::RaceConditionError if status.visibility.nil?
render_anonymous_payload(status)
if status.direct_visibility?
deliver_to_own_conversation(status)
elsif status.limited_visibility?
deliver_to_mentioned_followers(status)
else
deliver_to_self(status) if status.account.local?
deliver_to_followers(status)
deliver_to_lists(status)
end
return if status.account.silenced? || !status.public_visibility? || status.reblog?
return if status.reply? && status.in_reply_to_account_id != status.account_id
if status.account.is_pro || status.account.is_donor || status.account.is_investor || status.account.is_verified
deliver_to_pro(status)
end
deliver_to_self(status) if status.account.local?
end
private
@@ -35,46 +13,4 @@ class FanOutOnWriteService < BaseService
FeedManager.instance.push_to_home(status.account, status)
end
def deliver_to_followers(status)
Rails.logger.debug "Delivering status #{status.id} to followers"
status.account.followers_for_local_distribution.select(:id).reorder(nil).find_in_batches do |followers|
FeedInsertWorker.push_bulk(followers) do |follower|
[status.id, follower.id, :home]
end
end
end
def deliver_to_lists(status)
Rails.logger.debug "Delivering status #{status.id} to lists"
status.account.lists_for_local_distribution.select(:id).reorder(nil).find_in_batches do |lists|
FeedInsertWorker.push_bulk(lists) do |list|
[status.id, list.id, :list]
end
end
end
def deliver_to_mentioned_followers(status)
Rails.logger.debug "Delivering status #{status.id} to limited followers"
FeedInsertWorker.push_bulk(status.mentions.includes(:account).map(&:account).select { |mentioned_account| mentioned_account.local? && mentioned_account.following?(status.account) }) do |follower|
[status.id, follower.id, :home]
end
end
def render_anonymous_payload(status)
@payload = InlineRenderer.render(status, nil, :status)
@payload = Oj.dump(event: :update, payload: @payload)
end
def deliver_to_pro(status)
Rails.logger.debug "Delivering status #{status.id} to pro timeline"
Redis.current.publish('timeline:pro', @payload)
end
def deliver_to_own_conversation(status)
AccountConversation.add_status(status.account, status)
end
end

View File

@@ -1,93 +0,0 @@
# frozen_string_literal: true
class FetchAtomService < BaseService
include JsonLdHelper
def call(url)
return if url.blank?
result = process(url)
# retry without ActivityPub
result ||= process(url) if @unsupported_activity
result
rescue OpenSSL::SSL::SSLError => e
Rails.logger.debug "SSL error: #{e}"
nil
rescue HTTP::ConnectionError => e
Rails.logger.debug "HTTP ConnectionError: #{e}"
nil
end
private
def process(url, terminal = false)
@url = url
perform_request { |response| process_response(response, terminal) }
end
def perform_request(&block)
accept = 'text/html'
accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity
Request.new(:get, @url).add_headers('Accept' => accept).perform(&block)
end
def process_response(response, terminal = false)
return nil if response.code != 200
if response.mime_type == 'application/atom+xml'
[@url, { prefetched_body: response.body_with_limit }, :ostatus]
elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
body = response.body_with_limit
json = body_to_json(body)
if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?
[json['id'], { prefetched_body: body, id: true }, :activitypub]
elsif supported_context?(json) && expected_type?(json)
[json['id'], { prefetched_body: body, id: true }, :activitypub]
else
@unsupported_activity = true
nil
end
elsif !terminal
link_header = response['Link'] && parse_link_header(response)
if link_header&.find_link(%w(rel alternate))
process_link_headers(link_header)
elsif response.mime_type == 'text/html'
process_html(response)
end
end
end
def expected_type?(json)
equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
end
def process_html(response)
page = Nokogiri::HTML(response.body_with_limit)
json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) }
atom_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' }
result ||= process(json_link['href'], terminal: true) unless json_link.nil? || @unsupported_activity
result ||= process(atom_link['href'], terminal: true) unless atom_link.nil?
result
end
def process_link_headers(link_header)
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
result ||= process(json_link.href, terminal: true) unless json_link.nil? || @unsupported_activity
result ||= process(atom_link.href, terminal: true) unless atom_link.nil?
result
end
def parse_link_header(response)
LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link'])
end
end

View File

@@ -78,6 +78,7 @@ class FetchLinkCardService < BaseService
end
def parse_urls
# : todo :
if @status.local?
urls = @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
else
@@ -114,8 +115,6 @@ class FetchLinkCardService < BaseService
@card.type = embed[:type]
@card.title = embed[:title] || ''
@card.author_name = embed[:author_name] || ''
@card.author_url = embed[:author_url].present? ? (url + embed[:author_url]).to_s : ''
@card.provider_name = embed[:provider_name] || ''
@card.provider_url = embed[:provider_url].present? ? (url + embed[:provider_url]).to_s : ''
@card.width = 0

View File

@@ -1,45 +0,0 @@
# 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)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :ostatus
process_atom(resource_url, **resource_options)
when :activitypub
ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options)
end
end
private
def process_atom(url, prefetched_body:)
xml = Nokogiri::XML(prefetched_body)
xml.encoding = 'utf-8'
account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: OStatus::TagManager::XMLNS), false)
UpdateRemoteProfileService.new.call(xml, account) if account.present? && trusted_domain?(url, account)
account
rescue TypeError
Rails.logger.debug "Unparseable URL given: #{url}"
nil
rescue Nokogiri::XML::XPath::SyntaxError
Rails.logger.debug 'Invalid XML or missing namespace'
nil
end
def trusted_domain?(url, account)
domain = Addressable::URI.parse(url).normalized_host
domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
end
end

View File

@@ -1,45 +0,0 @@
# 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)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :ostatus
process_atom(resource_url, **resource_options)
when :activitypub
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
end
end
private
def process_atom(url, prefetched_body:)
Rails.logger.debug "Processing Atom for remote status at #{url}"
xml = Nokogiri::XML(prefetched_body)
xml.encoding = 'utf-8'
account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS))
domain = Addressable::URI.parse(url).normalized_host
return nil unless !account.nil? && confirmed_domain?(domain, account)
statuses = ProcessFeedService.new.call(prefetched_body, account)
statuses.first
rescue Nokogiri::XML::XPath::SyntaxError
Rails.logger.debug 'Invalid XML or missing namespace'
nil
end
def confirmed_domain?(domain, account)
account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
end
end

View File

@@ -9,7 +9,6 @@ class FollowService < BaseService
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
def call(source_account, target_account, reblogs: nil)
reblogs = true if reblogs.nil?
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 GabSocial::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved?
@@ -29,9 +28,9 @@ class FollowService < BaseService
ActivityTracker.increment('activity:interactions')
if target_account.locked? || target_account.activitypub?
if target_account.locked?
request_follow(source_account, target_account, reblogs: reblogs)
else
elsif target_account.local?
direct_follow(source_account, target_account, reblogs: reblogs)
end
end

View File

@@ -1,101 +0,0 @@
# frozen_string_literal: true
require 'csv'
class ImportService < BaseService
ROWS_PROCESSING_LIMIT = 20_000
def call(import)
@import = import
@account = @import.account
case @import.type
when 'following'
import_follows!
when 'blocking'
import_blocks!
when 'muting'
import_mutes!
when 'domain_blocking'
import_domain_blocks!
end
end
private
def import_follows!
parse_import_data!(['Account address'])
import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: 'Show reposts')
end
def import_blocks!
parse_import_data!(['Account address'])
import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
end
def import_mutes!
parse_import_data!(['Account address'])
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: 'Hide notifications')
end
def import_domain_blocks!
parse_import_data!(['#domain'])
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#domain'].strip }
if @import.overwrite?
presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
@account.domain_blocks.find_each do |domain_block|
if presence_hash[domain_block.domain]
items.delete(domain_block.domain)
else
@account.unblock_domain!(domain_block.domain)
end
end
end
items.each do |domain|
@account.block_domain!(domain)
end
AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
[@account.id, domain]
end
end
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
items = @data.take(limit).map { |row| [row['Account address']&.strip, Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
if @import.overwrite?
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
overwrite_scope.find_each do |target_account|
if presence_hash[target_account.acct]
items.delete(target_account.acct)
extra = presence_hash[target_account.acct][1]
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, extra)
else
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
end
end
end
Import::RelationshipWorker.push_bulk(items) do |acct, extra|
[@account.id, acct, action, extra]
end
end
def parse_import_data!(default_headers)
data = CSV.parse(import_data, headers: true)
data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(' ')
@data = data.reject(&:blank?)
end
def import_data
Paperclip.io_adapters.for(@import.data).read
end
def follow_limit
FollowLimitValidator.limit_for_account(@account)
end
end

View File

@@ -10,7 +10,6 @@ class NotifyService < BaseService
create_notification!
push_notification! if @notification.browserable?
push_to_conversation! if direct_message?
send_email! if email_enabled?
rescue ActiveRecord::RecordInvalid
return
@@ -59,25 +58,10 @@ class NotifyService < BaseService
@notification.type == :mention
end
def direct_message?
message? && @notification.target_status.direct_visibility?
end
def response_to_recipient?
@notification.target_status.in_reply_to_account_id == @recipient.id && @notification.target_status.thread&.direct_visibility?
end
def from_staff?
@notification.from_account.local? && @notification.from_account.user.present? && @notification.from_account.user.staff?
end
def optional_non_following_and_direct?
direct_message? &&
@recipient.user.settings.interactions['must_be_following_dm'] &&
!following_sender? &&
!response_to_recipient?
end
def hellbanned?
@notification.from_account.silenced? && !following_sender?
end
@@ -86,36 +70,21 @@ class NotifyService < BaseService
@recipient.id == @notification.from_account.id
end
def domain_blocking?
@recipient.domain_blocking?(@notification.from_account.domain) && !following_sender?
end
def blocked?
blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway
blocked ||= from_self? && @notification.type != :poll # Skip for interactions with self
return blocked if message? && from_staff?
blocked ||= domain_blocking? # Skip for domain blocked accounts
blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts
blocked ||= @recipient.muting_notifications?(@notification.from_account)
blocked ||= hellbanned? # Hellban
blocked ||= optional_non_follower? # Options
blocked ||= optional_non_following? # Options
blocked ||= optional_non_following_and_direct? # Options
blocked ||= conversation_muted?
blocked ||= send("blocked_#{@notification.type}?") # Type-dependent filters
blocked
end
def conversation_muted?
if @notification.target_status
@recipient.muting_conversation?(@notification.target_status.conversation)
else
false
end
end
def create_notification!
@notification.save!
end
@@ -127,11 +96,6 @@ class NotifyService < BaseService
send_push_notifications!
end
def push_to_conversation!
return if @notification.activity.nil?
AccountConversation.add_status(@recipient, @notification.target_status)
end
def send_push_notifications!
subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id)
.select { |subscription| subscription.pushable?(@notification) }

View File

@@ -100,8 +100,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)
ExpiringStatusWorker.perform_at(@status.expires_at, @status.id) if @status.expires_at && @account.is_pro
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
end

View File

@@ -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

View File

@@ -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, GabSocial::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

View File

@@ -1,8 +1,6 @@
# frozen_string_literal: true
class ProcessMentionsService < BaseService
include StreamEntryRenderer
# Scan status for mentions and fetch remote mentioned users, create
# local mention pointers, send Salmon notifications to mentioned
# remote users
@@ -15,17 +13,9 @@ class ProcessMentionsService < BaseService
status.text = status.text.gsub(Account::MENTION_RE) do |match|
username, domain = Regexp.last_match(1).split('@')
mentioned_account = Account.find_remote(username, domain)
mentioned_account = Account.find_local(username)
if mention_undeliverable?(mentioned_account)
begin
mentioned_account = resolve_account_service.call(Regexp.last_match(1))
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, GabSocial::UnexpectedResponseError
mentioned_account = nil
end
end
next match if mention_undeliverable?(mentioned_account) || mentioned_account&.suspended?
next match if mentioned_account.nil? || mentioned_account&.suspended?
mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)
@@ -39,10 +29,6 @@ class ProcessMentionsService < BaseService
private
def mention_undeliverable?(mentioned_account)
mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && @status.stream_entry.hidden?)
end
def create_notification(mention)
mentioned_account = mention.account
@@ -51,7 +37,4 @@ class ProcessMentionsService < BaseService
end
end
def resolve_account_service
ResolveAccountService.new
end
end

View File

@@ -1,8 +1,6 @@
# frozen_string_literal: true
class ProcessQuoteService < BaseService
include StreamEntryRenderer
# Create notification for a quote
# @param [Status] status Quoting status
# @return [Status]

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,6 @@
class ReblogService < BaseService
include Authorization
include StreamEntryRenderer
# Reblog a status and notify its remote author
# @param [Account] account Account to reblog from
@@ -24,8 +23,6 @@ class ReblogService < BaseService
reblog = account.statuses.create!(reblog: reblogged_status, text: 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)
bump_potential_friendship(account, reblog)

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true
class RemoveStatusService < BaseService
include StreamEntryRenderer
include Redisable
def call(status, **options)
@@ -11,7 +10,6 @@ class RemoveStatusService < BaseService
@tags = status.tags.pluck(:name).to_a
@mentions = status.active_mentions.includes(:account).to_a
@reblogs = status.reblogs.includes(:account).to_a
@stream_entry = status.stream_entry
@options = options
RedisLock.acquire(lock_options) do |lock|
@@ -36,9 +34,6 @@ class RemoveStatusService < BaseService
# of it. The Delete activity of the original is forwarded
# separately.
return if !@account.local? || @options[:original_removed]
remove_from_remote_followers
remove_from_remote_affected
end
private
@@ -65,66 +60,6 @@ class RemoveStatusService < BaseService
end
end
def remove_from_remote_affected
# People who got mentioned in the status, or who
# reblogged it from someone else might not follow
# the author and wouldn't normally receive the
# delete notification - so here, we explicitly
# send it to them
target_accounts = (@mentions.map(&:account).reject(&:local?) + @reblogs.map(&:account).reject(&:local?))
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]
end
end
def remove_from_remote_followers
# OStatus
Pubsubhubbub::RawDistributionWorker.perform_async(salmon_xml, @account.id)
# ActivityPub
ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url|
[signed_activity_json, @account.id, inbox_url]
end
relay! if relayable?
end
def relayable?
@status.public_visibility?
end
def relay!
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
[signed_activity_json, @account.id, inbox_url]
end
end
def salmon_xml
@salmon_xml ||= stream_entry_to_xml(@stream_entry)
end
def signed_activity_json
@signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account))
end
def activity_json
@activity_json ||= ActiveModelSerializers::SerializableResource.new(
@status,
serializer: @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter
).as_json
end
def remove_reblogs
# We delete reblogs of the status before the original status,
# because once original status is gone, reblogs will disappear

View File

@@ -9,7 +9,6 @@ class ReportService < BaseService
@options = options
create_report!
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
@report
end
@@ -25,24 +24,4 @@ class ReportService < BaseService
)
end
def forward_to_origin!
ActivityPub::DeliveryWorker.perform_async(
payload,
some_local_account.id,
@target_account.inbox_url
)
end
def payload
Oj.dump(ActiveModelSerializers::SerializableResource.new(
@report,
serializer: ActivityPub::FlagSerializer,
adapter: ActivityPub::Adapter,
account: some_local_account
).as_json)
end
def some_local_account
@some_local_account ||= Account.representative
end
end

View File

@@ -1,220 +0,0 @@
# frozen_string_literal: true
class ResolveAccountService < BaseService
include OStatus2::MagicKey
include JsonLdHelper
DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
# Find or create a local account for a remote user.
# When creating, look up the user's webfinger and fetch all
# important information from their feed
# @param [String, Account] uri User URI in the form of username@domain
# @param [Hash] options
# @return [Account]
def call(uri, options = {})
@options = options
if uri.is_a?(Account)
@account = uri
@username = @account.username
@domain = @account.domain
uri = "#{@username}@#{@domain}"
return @account if @account.local? || !webfinger_update_due?
else
@username, @domain = uri.split('@')
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
@account = Account.find_remote(@username, @domain)
return @account unless webfinger_update_due?
end
Rails.logger.debug "Looking up webfinger for #{uri}"
@webfinger = Goldfinger.finger("acct:#{uri}")
confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
@username = confirmed_username
@domain = confirmed_domain
elsif options[:redirected].nil?
return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true))
else
Rails.logger.debug 'Requested and returned acct URIs do not match'
return
end
return if links_missing?
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@account = Account.find_remote(@username, @domain)
if activitypub_ready? || @account&.activitypub?
handle_activitypub
else
handle_ostatus
end
else
raise GabSocial::RaceConditionError
end
end
@account
rescue Goldfinger::Error => e
Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}"
nil
end
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]
end
def handle_activitypub
return if actor_json.nil?
@account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json)
rescue Oj::ParseError
nil
end
def create_account
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
@account = Account.new(username: @username, domain: @domain)
@account.suspended_at = domain_block.created_at if auto_suspend?
@account.silenced_at = domain_block.created_at if auto_silence?
@account.private_key = nil
end
def update_account
@account.last_webfingered_at = Time.now.utc
@account.protocol = :ostatus
@account.remote_url = atom_url
@account.salmon_url = salmon_url
@account.url = url
@account.public_key = public_key
@account.uri = canonical_uri
@account.hub_url = hub_url
@account.save!
end
def auto_suspend?
domain_block&.suspend?
end
def auto_silence?
domain_block&.silence?
end
def domain_block
return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain)
end
def atom_url
@atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href
end
def salmon_url
@salmon_url ||= @webfinger.link('salmon').href
end
def actor_url
@actor_url ||= @webfinger.link('self').href
end
def url
@url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href
end
def public_key
@public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href)
end
def canonical_uri
return @canonical_uri if defined?(@canonical_uri)
author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri')
if author_uri.nil?
owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS)
author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil?
end
@canonical_uri = author_uri.nil? ? nil : author_uri.content
end
def hub_url
return @hub_url if defined?(@hub_url)
hubs = atom.xpath('//xmlns:link[@rel="hub"]')
@hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href']
end
def atom_body
return @atom_body if defined?(@atom_body)
@atom_body = Request.new(:get, atom_url).perform do |response|
raise GabSocial::UnexpectedResponseError, response unless response.code == 200
response.body_with_limit
end
end
def actor_json
return @actor_json if defined?(@actor_json)
json = fetch_resource(actor_url, false)
@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
end

View File

@@ -1,95 +0,0 @@
# frozen_string_literal: true
class ResolveURLService < BaseService
include JsonLdHelper
include Authorization
attr_reader :url
def call(url, on_behalf_of: nil)
@url = url
@on_behalf_of = on_behalf_of
return process_local_url if local_url?
process_url unless fetched_atom_feed.nil?
end
private
def process_url
if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
FetchRemoteAccountService.new.call(atom_url, body, protocol)
elsif equals_or_includes_any?(type, %w(Note Article Image Video Page Question))
FetchRemoteStatusService.new.call(atom_url, body, protocol)
end
end
def fetched_atom_feed
@_fetched_atom_feed ||= FetchAtomService.new.call(url)
end
def atom_url
fetched_atom_feed.first
end
def body
fetched_atom_feed.second[:prefetched_body]
end
def protocol
fetched_atom_feed.third
end
def type
return json_data['type'] if protocol == :activitypub
case xml_root
when 'feed'
'Person'
when 'entry'
'Note'
end
end
def json_data
@_json_data ||= body_to_json(body)
end
def xml_root
xml_data.root.name
end
def xml_data
@_xml_data ||= Nokogiri::XML(body, nil, 'utf-8')
end
def local_url?
TagManager.instance.local_url?(@url)
end
def process_local_url
recognized_params = Rails.application.routes.recognize_path(@url)
return unless recognized_params[:action] == 'show'
if recognized_params[:controller] == 'stream_entries'
status = StreamEntry.find_by(id: recognized_params[:id])&.status
check_local_status(status)
elsif recognized_params[:controller] == 'statuses'
status = Status.find_by(id: recognized_params[:id])
check_local_status(status)
elsif recognized_params[:controller] == 'accounts'
Account.find_local(recognized_params[:username])
end
end
def check_local_status(status)
return if status.nil?
authorize_with @on_behalf_of, status, :show?
status
rescue GabSocial::NotPermittedError
# Do not disclose the existence of status the user is not authorized to see
nil
end
end

View File

@@ -13,12 +13,9 @@ class SearchService < BaseService
default_results.tap do |results|
next if @query.blank? || @limit.zero?
if url_query?
results.merge!(url_resource_results) unless url_resource.nil? || @offset.positive? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
elsif @query.present?
if @query.present?
results[:accounts] = perform_accounts_search! if account_searchable?
results[:statuses] = perform_statuses_search! if full_text_searchable? && !account.nil?
results[:hashtags] = perform_hashtags_search! if hashtag_searchable? && !account.nil?
results[:links] = [] # perform_links_search! if !account.nil?
results[:groups] = perform_groups_search!
end
@@ -77,31 +74,8 @@ class SearchService < BaseService
[]
end
def perform_hashtags_search!
Tag.search_for(
@query.gsub(/\A#/, ''),
@offset
)
end
def default_results
{ accounts: [], hashtags: [], statuses: [], links: [], groups: [] }
end
def url_query?
@resolve && @query =~ /\Ahttps?:\/\//
end
def url_resource_results
{ url_resource_symbol => [url_resource] }
end
def url_resource
@_url_resource ||= ResolveURLService.new.call(@query, on_behalf_of: @account)
end
def url_resource_symbol
url_resource.class.name.downcase.pluralize.to_sym
{ accounts: [], statuses: [], links: [], groups: [] }
end
def full_text_searchable?
@@ -114,18 +88,10 @@ class SearchService < BaseService
account_search? && !(@query.include?('@') && @query.include?(' '))
end
def hashtag_searchable?
hashtag_search? && !@query.include?('@')
end
def account_search?
@options[:type].blank? || @options[:type] == 'accounts'
end
def hashtag_search?
@options[:type].blank? || @options[:type] == 'hashtags'
end
def statuses_search?
@options[:type].blank? || @options[:type] == 'statuses'
end
@@ -136,7 +102,6 @@ class SearchService < BaseService
blocked_by: Account.blocked_by_map(account_ids, account.id),
muting: Account.muting_map(account_ids, account.id),
following: Account.following_map(account_ids, account.id),
domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
}
end
end

View File

@@ -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 GabSocial::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

View File

@@ -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 GabSocial::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

View File

@@ -2,14 +2,11 @@
class SuspendAccountService < BaseService
ASSOCIATIONS_ON_SUSPEND = %w(
account_pins
active_relationships
block_relationships
blocked_by_relationships
conversation_mutes
conversations
custom_filters
domain_blocks
favourites
follow_requests
list_accounts
@@ -23,7 +20,6 @@ class SuspendAccountService < BaseService
scheduled_statuses
status_bookmarks
status_pins
stream_entries
subscriptions
).freeze
@@ -42,7 +38,6 @@ class SuspendAccountService < BaseService
@account = account
@options = options
reject_follows!
purge_user!
purge_profile!
purge_content!
@@ -50,14 +45,6 @@ class SuspendAccountService < BaseService
private
def reject_follows!
return if @account.local? || !@account.activitypub?
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
end
end
def purge_user!
return if !@account.local? || @account.user.nil?
@@ -106,34 +93,6 @@ class SuspendAccountService < BaseService
association.in_batches.destroy_all
end
def delete_actor_json
return @delete_actor_json if defined?(@delete_actor_json)
payload = ActiveModelSerializers::SerializableResource.new(
@account,
serializer: ActivityPub::DeleteActorSerializer,
adapter: ActivityPub::Adapter
).as_json
@delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
end
def build_reject_json(follow)
ActiveModelSerializers::SerializableResource.new(
follow,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end
def delivery_inboxes
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
end
def low_priority_delivery_inboxes
Account.inboxes - delivery_inboxes
end
def associations_for_destruction
if @options[:destroy]
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
class UnblockDomainService < BaseService
attr_accessor :domain_block
def call(domain_block)
@domain_block = domain_block
process_retroactive_updates
domain_block.destroy
end
def process_retroactive_updates
blocked_accounts.in_batches.update_all(update_options) unless domain_block.noop?
end
def blocked_accounts
scope = Account.where(domain: domain_block.domain)
if domain_block.silence?
scope.where(silenced_at: @domain_block.created_at)
else
scope.where(suspended_at: @domain_block.created_at)
end
end
def update_options
{ domain_block_impact => nil }
end
def domain_block_impact
domain_block.silence? ? :silenced_at : :suspended_at
end
end

View File

@@ -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

View File

@@ -9,7 +9,6 @@ class UpdateAccountService < BaseService
next unless ret
authorize_all_follow_requests(account) if was_locked && !account.locked
check_links(account)
process_hashtags(account)
end
rescue GabSocial::DimensionsValidationError => de
@@ -25,10 +24,6 @@ class UpdateAccountService < BaseService
end
end
def check_links(account)
VerifyAccountLinksWorker.perform_async(account.id)
end
def process_hashtags(account)
account.tags_as_strings = Extractor.extract_hashtags(account.note)
end

View File

@@ -1,66 +0,0 @@
# frozen_string_literal: true
class UpdateRemoteProfileService < BaseService
attr_reader :account, :remote_profile
def call(body, account, resubscribe = false)
@account = account
@remote_profile = RemoteProfile.new(body)
return if remote_profile.root.nil?
update_account unless remote_profile.author.nil?
old_hub_url = account.hub_url
account.hub_url = remote_profile.hub_link if remote_profile.hub_link.present? && remote_profile.hub_link != old_hub_url
account.save_with_optional_media!
Pubsubhubbub::SubscribeWorker.perform_async(account.id) if resubscribe && account.hub_url != old_hub_url
end
private
def update_account
account.display_name = remote_profile.display_name || ''
account.note = remote_profile.note || ''
account.locked = remote_profile.locked?
if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
if remote_profile.avatar.present?
account.avatar_remote_url = remote_profile.avatar
else
account.avatar_remote_url = ''
account.avatar.destroy
end
if remote_profile.header.present?
account.header_remote_url = remote_profile.header
else
account.header_remote_url = ''
account.header.destroy
end
save_emojis if remote_profile.emojis.present?
end
end
def save_emojis
do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media?
return if do_not_download
remote_profile.emojis.each do |link|
next unless link['href'] && link['name']
shortcode = link['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain)
next unless emoji.nil?
emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain)
emoji.image_remote_url = link['href']
emoji.save
end
end
end

View File

@@ -1,47 +0,0 @@
# frozen_string_literal: true
class VerifyLinkService < BaseService
def call(field)
@link_back = ActivityPub::TagManager.instance.url_for(field.account)
@url = field.value_for_verification
perform_request!
return unless link_back_present?
field.mark_verified!
rescue OpenSSL::SSL::SSLError, HTTP::Error, Addressable::URI::InvalidURIError, GabSocial::HostValidationError, GabSocial::LengthValidationError => e
Rails.logger.debug "Error fetching link #{@url}: #{e}"
nil
end
private
def perform_request!
@body = Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
res.code != 200 ? nil : res.body_with_limit
end
end
def link_back_present?
return false if @body.blank?
links = Nokogiri::HTML(@body).xpath('//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]|//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]')
if links.any? { |link| link['href'] == @link_back }
true
elsif links.empty?
false
else
link_redirects_back?(links.first['href'])
end
end
def link_redirects_back?(test_url)
redirect_to_url = Request.new(:head, test_url, follow: false).perform do |res|
res.headers['Location']
end
redirect_to_url == @link_back
end
end

View File

@@ -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

View File

@@ -18,42 +18,6 @@ class VoteService < BaseService
end
ActivityTracker.increment('activity:interactions')
if @poll.account.local?
distribute_poll!
else
deliver_votes!
queue_final_poll_check!
end
end
private
def distribute_poll!
return if @poll.hide_totals?
ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, @poll.status.id)
end
def queue_final_poll_check!
return unless @poll.expires?
PollExpirationNotifyWorker.perform_at(@poll.expires_at + 5.minutes, @poll.id)
end
def deliver_votes!
@votes.each do |vote|
ActivityPub::DeliveryWorker.perform_async(
build_json(vote),
@account.id,
@poll.account.inbox_url
)
end
end
def build_json(vote)
ActiveModelSerializers::SerializableResource.new(
vote,
serializer: ActivityPub::VoteSerializer,
adapter: ActivityPub::Adapter
).to_json
end
end