Merge branch 'master' into feature-circles
This commit is contained in:
commit
824d1b8893
906 changed files with 37100 additions and 11600 deletions
|
@ -27,7 +27,7 @@ class AccountSearchService < BaseService
|
|||
|
||||
return @exact_match if defined?(@exact_match)
|
||||
|
||||
@exact_match = begin
|
||||
match = begin
|
||||
if options[:resolve]
|
||||
ResolveAccountService.new.call(query)
|
||||
elsif domain_is_local?
|
||||
|
@ -36,6 +36,10 @@ class AccountSearchService < BaseService
|
|||
Account.find_remote(query_username, query_domain)
|
||||
end
|
||||
end
|
||||
|
||||
match = nil if !match.nil? && !account.nil? && options[:following] && !account.following?(match)
|
||||
|
||||
@exact_match = match
|
||||
end
|
||||
|
||||
def search_results
|
||||
|
|
|
@ -28,7 +28,7 @@ class ActivityPub::FetchRemoteAccountService < BaseService
|
|||
|
||||
return unless only_key || verified_webfinger?
|
||||
|
||||
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key)
|
||||
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
|
||||
rescue Oj::ParseError
|
||||
nil
|
||||
end
|
||||
|
@ -39,17 +39,16 @@ class ActivityPub::FetchRemoteAccountService < BaseService
|
|||
webfinger = webfinger!("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?
|
||||
return webfinger.link('self', 'href') == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
||||
|
||||
webfinger = webfinger!("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
|
||||
return false if webfinger.link('self', 'href') != @uri
|
||||
|
||||
true
|
||||
rescue Goldfinger::Error
|
||||
rescue Webfinger::Error
|
||||
false
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::PrepareFollowersSynchronizationService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(account, params)
|
||||
@account = account
|
||||
|
||||
return if params['collectionId'] != @account.followers_url || invalid_origin?(params['url']) || @account.local_followers_hash == params['digest']
|
||||
|
||||
ActivityPub::FollowersSynchronizationWorker.perform_async(@account.id, params['url'])
|
||||
end
|
||||
end
|
|
@ -18,15 +18,18 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
@account = Account.remote.find_by(uri: @uri) if @options[:only_key]
|
||||
@account ||= Account.find_remote(@username, @domain)
|
||||
@old_public_key = @account&.public_key
|
||||
@old_protocol = @account&.protocol
|
||||
@account = Account.remote.find_by(uri: @uri) if @options[:only_key]
|
||||
@account ||= Account.find_remote(@username, @domain)
|
||||
@old_public_key = @account&.public_key
|
||||
@old_protocol = @account&.protocol
|
||||
@suspension_changed = false
|
||||
|
||||
create_account if @account.nil?
|
||||
update_account
|
||||
process_tags
|
||||
process_attachments
|
||||
|
||||
process_duplicate_accounts! if @options[:verified_webfinger]
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
|
@ -37,8 +40,9 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
after_protocol_change! if protocol_changed?
|
||||
after_key_change! if key_changed? && !@options[:signed_with_known_key]
|
||||
clear_tombstones! if key_changed?
|
||||
after_suspension_change! if suspension_changed?
|
||||
|
||||
unless @options[:only_key]
|
||||
unless @options[:only_key] || @account.suspended?
|
||||
check_featured_collection! if @account.featured_collection_url.present?
|
||||
check_links! unless @account.fields.empty?
|
||||
end
|
||||
|
@ -52,46 +56,57 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
|
||||
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?
|
||||
@account.protocol = :activitypub
|
||||
@account.username = @username
|
||||
@account.domain = @domain
|
||||
@account.private_key = nil
|
||||
@account.suspended_at = domain_block.created_at if auto_suspend?
|
||||
@account.suspension_origin = :local if auto_suspend?
|
||||
@account.silenced_at = domain_block.created_at if auto_silence?
|
||||
@account.save
|
||||
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]
|
||||
set_suspension!
|
||||
set_immediate_protocol_attributes!
|
||||
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
|
||||
set_immediate_attributes! unless @account.suspended?
|
||||
set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
|
||||
|
||||
@account.save_with_optional_media!
|
||||
end
|
||||
|
||||
def set_immediate_attributes!
|
||||
def set_immediate_protocol_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.devices_url = @json['devices'] || ''
|
||||
@account.url = url || @uri
|
||||
@account.uri = @uri
|
||||
@account.actor_type = actor_type
|
||||
end
|
||||
|
||||
def set_immediate_attributes!
|
||||
@account.featured_collection_url = @json['featured'] || ''
|
||||
@account.devices_url = @json['devices'] || ''
|
||||
@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
|
||||
@account.discoverable = @json['discoverable'] || false
|
||||
end
|
||||
|
||||
def set_fetchable_key!
|
||||
@account.public_key = public_key || ''
|
||||
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?
|
||||
|
@ -99,6 +114,18 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.moved_to_account = @json['movedTo'].present? ? moved_account : nil
|
||||
end
|
||||
|
||||
def set_suspension!
|
||||
return if @account.suspended? && @account.suspension_origin_local?
|
||||
|
||||
if @account.suspended? && !@json['suspended']
|
||||
@account.unsuspend!
|
||||
@suspension_changed = true
|
||||
elsif !@account.suspended? && @json['suspended']
|
||||
@account.suspend!(origin: :remote)
|
||||
@suspension_changed = true
|
||||
end
|
||||
end
|
||||
|
||||
def after_protocol_change!
|
||||
ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
|
||||
end
|
||||
|
@ -107,6 +134,14 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
RefollowWorker.perform_async(@account.id)
|
||||
end
|
||||
|
||||
def after_suspension_change!
|
||||
if @account.suspended?
|
||||
Admin::SuspensionWorker.perform_async(@account.id)
|
||||
else
|
||||
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||
end
|
||||
end
|
||||
|
||||
def check_featured_collection!
|
||||
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
|
||||
end
|
||||
|
@ -115,6 +150,12 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
VerifyAccountLinksWorker.perform_async(@account.id)
|
||||
end
|
||||
|
||||
def process_duplicate_accounts!
|
||||
return unless Account.where(uri: @account.uri).where.not(id: @account.id).exists?
|
||||
|
||||
AccountMergingWorker.perform_async(@account.id)
|
||||
end
|
||||
|
||||
def actor_type
|
||||
if @json['type'].is_a?(Array)
|
||||
@json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) }
|
||||
|
@ -196,7 +237,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
|
||||
has_first_page = collection.is_a?(Hash) && collection['first'].present?
|
||||
@collections[type] = [total_items, has_first_page]
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::LengthValidationError
|
||||
@collections[type] = [nil, nil]
|
||||
end
|
||||
|
||||
|
@ -227,6 +268,10 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
!@old_public_key.nil? && @old_public_key != @account.public_key
|
||||
end
|
||||
|
||||
def suspension_changed?
|
||||
@suspension_changed
|
||||
end
|
||||
|
||||
def clear_tombstones!
|
||||
Tombstone.where(account_id: @account.id).delete_all
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class ActivityPub::ProcessCollectionService < BaseService
|
|||
@json = Oj.load(body, mode: :strict)
|
||||
@options = options
|
||||
|
||||
return if !supported_context? || (different_actor? && verify_account!.nil?) || @account.suspended? || @account.local?
|
||||
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
|
||||
|
||||
case @json['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
|
@ -28,6 +28,14 @@ class ActivityPub::ProcessCollectionService < BaseService
|
|||
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri
|
||||
end
|
||||
|
||||
def suspended_actor?
|
||||
@account.suspended? && !activity_allowed_while_suspended?
|
||||
end
|
||||
|
||||
def activity_allowed_while_suspended?
|
||||
%w(Delete Reject Undo Update).include?(@json['type'])
|
||||
end
|
||||
|
||||
def process_items(items)
|
||||
items.reverse_each.map { |item| process_item(item) }.compact
|
||||
end
|
||||
|
|
74
app/services/activitypub/synchronize_followers_service.rb
Normal file
74
app/services/activitypub/synchronize_followers_service.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::SynchronizeFollowersService < BaseService
|
||||
include JsonLdHelper
|
||||
include Payloadable
|
||||
|
||||
def call(account, partial_collection_url)
|
||||
@account = account
|
||||
|
||||
items = collection_items(partial_collection_url)
|
||||
return if items.nil?
|
||||
|
||||
# There could be unresolved accounts (hence the call to .compact) but this
|
||||
# should never happen in practice, since in almost all cases we keep an
|
||||
# Account record, and should we not do that, we should have sent a Delete.
|
||||
# In any case there is not much we can do if that occurs.
|
||||
@expected_followers = items.map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }.compact
|
||||
|
||||
remove_unexpected_local_followers!
|
||||
handle_unexpected_outgoing_follows!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_unexpected_local_followers!
|
||||
@account.followers.local.where.not(id: @expected_followers.map(&:id)).each do |unexpected_follower|
|
||||
UnfollowService.new.call(unexpected_follower, @account)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_unexpected_outgoing_follows!
|
||||
@expected_followers.each do |expected_follower|
|
||||
next if expected_follower.following?(@account)
|
||||
|
||||
if expected_follower.requested?(@account)
|
||||
# For some reason the follow request went through but we missed it
|
||||
expected_follower.follow_requests.find_by(target_account: @account)&.authorize!
|
||||
else
|
||||
# Since we were not aware of the follow from our side, we do not have an
|
||||
# ID for it that we can include in the Undo activity. For this reason,
|
||||
# the Undo may not work with software that relies exclusively on
|
||||
# matching activity IDs and not the actor and target
|
||||
follow = Follow.new(account: expected_follower, target_account: @account)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_undo_follow_json(follow), follow.account_id, follow.target_account.inbox_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_undo_follow_json(follow)
|
||||
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
||||
end
|
||||
|
||||
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 if invalid_origin?(collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ class AfterBlockService < BaseService
|
|||
private
|
||||
|
||||
def clear_home_feed!
|
||||
FeedManager.instance.clear_from_timeline(@account, @target_account)
|
||||
FeedManager.instance.clear_from_home(@account, @target_account)
|
||||
end
|
||||
|
||||
def clear_conversations!
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class AfterUnallowDomainService < BaseService
|
||||
def call(domain)
|
||||
Account.where(domain: domain).find_each do |account|
|
||||
SuspendAccountService.new.call(account, reserve_username: false)
|
||||
DeleteAccountService.new.call(account, reserve_username: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AppSignUpService < BaseService
|
||||
def call(app, params)
|
||||
def call(app, remote_ip, params)
|
||||
return unless allowed_registrations?
|
||||
|
||||
user_params = params.slice(:email, :password, :agreement, :locale)
|
||||
account_params = params.slice(:username)
|
||||
invite_request_params = { text: params[:reason] }
|
||||
user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params, invite_request_attributes: invite_request_params))
|
||||
user = User.create!(user_params.merge(created_by_application: app, sign_up_ip: remote_ip, password_confirmation: user_params[:password], account_attributes: account_params, invite_request_attributes: invite_request_params))
|
||||
|
||||
Doorkeeper::AccessToken.create!(application: app,
|
||||
resource_owner_id: user.id,
|
||||
|
|
|
@ -3,32 +3,40 @@
|
|||
class BatchedRemoveStatusService < BaseService
|
||||
include Redisable
|
||||
|
||||
# Delete given statuses and reblogs of them
|
||||
# Dispatch PuSH updates of the deleted statuses, but only local ones
|
||||
# Dispatch Salmon deletes, unique per domain, of the deleted statuses, but only local ones
|
||||
# Remove statuses from home feeds
|
||||
# Push delete events to streaming API for home feeds and public feeds
|
||||
# @param [Enumerable<Status>] statuses A preferably batched array of statuses
|
||||
# Delete multiple statuses and reblogs of them as efficiently as possible
|
||||
# @param [Enumerable<Status>] statuses An array of statuses
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :skip_side_effects
|
||||
# @option [Boolean] :skip_side_effects Do not modify feeds and send updates to streaming API
|
||||
def call(statuses, **options)
|
||||
statuses = Status.where(id: statuses.map(&:id)).includes(:account).flat_map { |status| [status] + status.reblogs.includes(:account).to_a }
|
||||
ActiveRecord::Associations::Preloader.new.preload(statuses, options[:skip_side_effects] ? :reblogs : [:account, :tags, reblogs: :account])
|
||||
|
||||
@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) }
|
||||
statuses_and_reblogs = statuses.flat_map { |status| [status] + status.reblogs }
|
||||
|
||||
@json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) }
|
||||
# The conversations for direct visibility statuses also need
|
||||
# to be manually updated. This part is not efficient but we
|
||||
# rely on direct visibility statuses being relatively rare.
|
||||
statuses_with_account_conversations = statuses.select(&:direct_visibility?)
|
||||
|
||||
# Ensure that rendered XML reflects destroyed state
|
||||
statuses.each do |status|
|
||||
status.mark_for_mass_destruction!
|
||||
status.destroy
|
||||
ActiveRecord::Associations::Preloader.new.preload(statuses_with_account_conversations, [mentions: :account])
|
||||
|
||||
statuses_with_account_conversations.each do |status|
|
||||
status.send(:unlink_from_conversations)
|
||||
end
|
||||
|
||||
# We do not batch all deletes into one to avoid having a long-running
|
||||
# transaction lock the database, but we use the delete method instead
|
||||
# of destroy to avoid all callbacks. We rely on foreign keys to
|
||||
# cascade the delete faster without loading the associations.
|
||||
statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all }
|
||||
|
||||
# Since we skipped all callbacks, we also need to manually
|
||||
# deindex the statuses
|
||||
Chewy.strategy.current.update(StatusesIndex::Status, statuses_and_reblogs) if Chewy.enabled?
|
||||
|
||||
return if options[:skip_side_effects]
|
||||
|
||||
# Batch by source account
|
||||
statuses.group_by(&:account_id).each_value do |account_statuses|
|
||||
statuses_and_reblogs.group_by(&:account_id).each_value do |account_statuses|
|
||||
account = account_statuses.first.account
|
||||
|
||||
next unless account
|
||||
|
@ -38,19 +46,18 @@ class BatchedRemoveStatusService < BaseService
|
|||
end
|
||||
|
||||
# Cannot be batched
|
||||
statuses.each do |status|
|
||||
unpush_from_public_timelines(status)
|
||||
@status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago)
|
||||
redis.pipelined do
|
||||
statuses.each do |status|
|
||||
unpush_from_public_timelines(status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unpush_from_home_timelines(account, statuses)
|
||||
recipients = account.followers_for_local_distribution.to_a
|
||||
|
||||
recipients << account if account.local?
|
||||
|
||||
recipients.each do |follower|
|
||||
account.followers_for_local_distribution.includes(:user).reorder(nil).find_each do |follower|
|
||||
statuses.each do |status|
|
||||
FeedManager.instance.unpush_from_home(follower, status)
|
||||
end
|
||||
|
@ -58,7 +65,7 @@ class BatchedRemoveStatusService < BaseService
|
|||
end
|
||||
|
||||
def unpush_from_list_timelines(account, statuses)
|
||||
account.lists_for_local_distribution.select(:id, :account_id).each do |list|
|
||||
account.lists_for_local_distribution.select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |list|
|
||||
statuses.each do |status|
|
||||
FeedManager.instance.unpush_from_list(list, status)
|
||||
end
|
||||
|
@ -66,30 +73,21 @@ class BatchedRemoveStatusService < BaseService
|
|||
end
|
||||
|
||||
def unpush_from_public_timelines(status)
|
||||
return unless status.public_visibility?
|
||||
return unless status.public_visibility? && status.id > @status_id_cutoff
|
||||
|
||||
payload = @json_payloads[status.id]
|
||||
payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||
|
||||
redis.pipelined do
|
||||
redis.publish('timeline:public', payload)
|
||||
if status.local?
|
||||
redis.publish('timeline:public:local', payload)
|
||||
else
|
||||
redis.publish('timeline:public:remote', payload)
|
||||
end
|
||||
if status.media_attachments.any?
|
||||
redis.publish('timeline:public:media', payload)
|
||||
if status.local?
|
||||
redis.publish('timeline:public:local:media', payload)
|
||||
else
|
||||
redis.publish('timeline:public:remote:media', payload)
|
||||
end
|
||||
end
|
||||
redis.publish('timeline:public', payload)
|
||||
redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
|
||||
|
||||
@tags[status.id].each do |hashtag|
|
||||
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload)
|
||||
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local?
|
||||
end
|
||||
if status.media_attachments.any?
|
||||
redis.publish('timeline:public:media', payload)
|
||||
redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
|
||||
end
|
||||
|
||||
status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag|
|
||||
redis.publish("timeline:hashtag:#{hashtag}", payload)
|
||||
redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ class BlockDomainService < BaseService
|
|||
scope = Account.by_domain_and_subdomains(domain_block.domain)
|
||||
|
||||
scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.silence?
|
||||
scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) unless domain_block.suspend?
|
||||
scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil, suspension_origin: nil) unless domain_block.suspend?
|
||||
end
|
||||
|
||||
def process_domain_block!
|
||||
|
@ -34,9 +34,10 @@ class BlockDomainService < BaseService
|
|||
end
|
||||
|
||||
def suspend_accounts!
|
||||
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
|
||||
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at, suspension_origin: :local)
|
||||
|
||||
blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
|
||||
SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
|
||||
DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
307
app/services/delete_account_service.rb
Normal file
307
app/services/delete_account_service.rb
Normal file
|
@ -0,0 +1,307 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeleteAccountService < BaseService
|
||||
include Payloadable
|
||||
|
||||
ASSOCIATIONS_ON_SUSPEND = %w(
|
||||
account_pins
|
||||
active_relationships
|
||||
aliases
|
||||
block_relationships
|
||||
blocked_by_relationships
|
||||
conversation_mutes
|
||||
conversations
|
||||
custom_filters
|
||||
devices
|
||||
domain_blocks
|
||||
featured_tags
|
||||
follow_requests
|
||||
identity_proofs
|
||||
list_accounts
|
||||
migrations
|
||||
mute_relationships
|
||||
muted_by_relationships
|
||||
notifications
|
||||
owned_lists
|
||||
passive_relationships
|
||||
report_notes
|
||||
scheduled_statuses
|
||||
status_pins
|
||||
).freeze
|
||||
|
||||
# The following associations have no important side-effects
|
||||
# in callbacks and all of their own associations are secured
|
||||
# by foreign keys, making them safe to delete without loading
|
||||
# into memory
|
||||
ASSOCIATIONS_WITHOUT_SIDE_EFFECTS = %w(
|
||||
account_pins
|
||||
aliases
|
||||
conversation_mutes
|
||||
conversations
|
||||
custom_filters
|
||||
devices
|
||||
domain_blocks
|
||||
featured_tags
|
||||
follow_requests
|
||||
identity_proofs
|
||||
list_accounts
|
||||
migrations
|
||||
mute_relationships
|
||||
muted_by_relationships
|
||||
notifications
|
||||
owned_lists
|
||||
scheduled_statuses
|
||||
status_pins
|
||||
)
|
||||
|
||||
ASSOCIATIONS_ON_DESTROY = %w(
|
||||
reports
|
||||
targeted_moderation_notes
|
||||
targeted_reports
|
||||
).freeze
|
||||
|
||||
# Suspend or remove an account and remove as much of its data
|
||||
# as possible. If it's a local account and it has not been confirmed
|
||||
# or never been approved, then side effects are skipped and both
|
||||
# the user and account records are removed fully. Otherwise,
|
||||
# it is controlled by options.
|
||||
# @param [Account]
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
|
||||
# @option [Boolean] :reserve_username Keep account record
|
||||
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
|
||||
# @option [Boolean] :skip_activitypub Skip sending ActivityPub payloads. Implied by :skip_side_effects
|
||||
# @option [Time] :suspended_at Only applicable when :reserve_username is true
|
||||
def call(account, **options)
|
||||
@account = account
|
||||
@options = { reserve_username: true, reserve_email: true }.merge(options)
|
||||
|
||||
if @account.local? && @account.user_unconfirmed_or_pending?
|
||||
@options[:reserve_email] = false
|
||||
@options[:reserve_username] = false
|
||||
@options[:skip_side_effects] = true
|
||||
end
|
||||
|
||||
@options[:skip_activitypub] = true if @options[:skip_side_effects]
|
||||
|
||||
distribute_activities!
|
||||
purge_content!
|
||||
fulfill_deletion_request!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def distribute_activities!
|
||||
return if skip_activitypub?
|
||||
|
||||
if @account.local?
|
||||
delete_actor!
|
||||
elsif @account.activitypub?
|
||||
reject_follows!
|
||||
undo_follows!
|
||||
end
|
||||
end
|
||||
|
||||
def reject_follows!
|
||||
# When deleting a remote account, the account obviously doesn't
|
||||
# actually become deleted on its origin server, i.e. unlike a
|
||||
# locally deleted account it continues to have access to its home
|
||||
# feed and other content. To prevent it from being able to continue
|
||||
# to access toots it would receive because it follows local accounts,
|
||||
# we have to force it to unfollow them.
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
||||
[Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def undo_follows!
|
||||
# When deleting a remote account, the account obviously doesn't
|
||||
# actually become deleted on its origin server, but following relationships
|
||||
# are severed on our end. Therefore, make the remote server aware that the
|
||||
# follow relationships are severed to avoid confusion and potential issues
|
||||
# if the remote account gets un-suspended.
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow|
|
||||
[Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def purge_user!
|
||||
return if !@account.local? || @account.user.nil?
|
||||
|
||||
if keep_user_record?
|
||||
@account.user.disable!
|
||||
@account.user.invites.where(uses: 0).destroy_all
|
||||
else
|
||||
@account.user.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def purge_content!
|
||||
purge_user!
|
||||
purge_profile!
|
||||
purge_statuses!
|
||||
purge_mentions!
|
||||
purge_media_attachments!
|
||||
purge_polls!
|
||||
purge_generated_notifications!
|
||||
purge_favourites!
|
||||
purge_bookmarks!
|
||||
purge_feeds!
|
||||
purge_other_associations!
|
||||
|
||||
@account.destroy unless keep_account_record?
|
||||
end
|
||||
|
||||
def purge_statuses!
|
||||
@account.statuses.reorder(nil).where.not(id: reported_status_ids).in_batches do |statuses|
|
||||
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: skip_side_effects?)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_mentions!
|
||||
@account.mentions.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
|
||||
end
|
||||
|
||||
def purge_media_attachments!
|
||||
@account.media_attachments.reorder(nil).find_each do |media_attachment|
|
||||
next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id)
|
||||
|
||||
media_attachment.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def purge_polls!
|
||||
@account.polls.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
|
||||
end
|
||||
|
||||
def purge_generated_notifications!
|
||||
# By deleting polls and statuses without callbacks, we've left behind
|
||||
# polymorphically associated notifications generated by this account
|
||||
|
||||
Notification.where(from_account: @account).in_batches.delete_all
|
||||
end
|
||||
|
||||
def purge_favourites!
|
||||
@account.favourites.in_batches do |favourites|
|
||||
ids = favourites.pluck(:status_id)
|
||||
StatusStat.where(status_id: ids).update_all('favourites_count = GREATEST(0, favourites_count - 1)')
|
||||
Chewy.strategy.current.update(StatusesIndex::Status, ids) if Chewy.enabled?
|
||||
# Rails.cache.delete_multi would be better, but we don't have it yet
|
||||
ids.each { |id| Rails.cache.delete("statuses/#{id}") }
|
||||
favourites.delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def purge_bookmarks!
|
||||
@account.bookmarks.in_batches do |bookmarks|
|
||||
Chewy.strategy.current.update(StatusesIndex::Status, bookmarks.pluck(:status_id)) if Chewy.enabled?
|
||||
bookmarks.delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def purge_other_associations!
|
||||
associations_for_destruction.each do |association_name|
|
||||
purge_association(association_name)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_feeds!
|
||||
return unless @account.local?
|
||||
|
||||
FeedManager.instance.clean_feeds!(:home, [@account.id])
|
||||
FeedManager.instance.clean_feeds!(:list, @account.owned_lists.pluck(:id))
|
||||
end
|
||||
|
||||
def purge_profile!
|
||||
# If the account is going to be destroyed
|
||||
# there is no point wasting time updating
|
||||
# its values first
|
||||
|
||||
return unless keep_account_record?
|
||||
|
||||
@account.silenced_at = nil
|
||||
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
||||
@account.suspension_origin = :local
|
||||
@account.locked = false
|
||||
@account.memorial = false
|
||||
@account.discoverable = false
|
||||
@account.display_name = ''
|
||||
@account.note = ''
|
||||
@account.fields = []
|
||||
@account.statuses_count = 0
|
||||
@account.followers_count = 0
|
||||
@account.following_count = 0
|
||||
@account.moved_to_account = nil
|
||||
@account.also_known_as = []
|
||||
@account.trust_level = :untrusted
|
||||
@account.avatar.destroy
|
||||
@account.header.destroy
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def fulfill_deletion_request!
|
||||
@account.deletion_request&.destroy
|
||||
end
|
||||
|
||||
def purge_association(association_name)
|
||||
association = @account.public_send(association_name)
|
||||
|
||||
if ASSOCIATIONS_WITHOUT_SIDE_EFFECTS.include?(association_name)
|
||||
association.in_batches.delete_all
|
||||
else
|
||||
association.in_batches.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
def delete_actor!
|
||||
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
|
||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def delete_actor_json
|
||||
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
|
||||
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 reported_status_ids
|
||||
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
|
||||
end
|
||||
|
||||
def associations_for_destruction
|
||||
if keep_account_record?
|
||||
ASSOCIATIONS_ON_SUSPEND
|
||||
else
|
||||
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
|
||||
end
|
||||
end
|
||||
|
||||
def keep_user_record?
|
||||
@options[:reserve_email]
|
||||
end
|
||||
|
||||
def keep_account_record?
|
||||
@options[:reserve_username]
|
||||
end
|
||||
|
||||
def skip_side_effects?
|
||||
@options[:skip_side_effects]
|
||||
end
|
||||
|
||||
def skip_activitypub?
|
||||
@options[:skip_activitypub]
|
||||
end
|
||||
end
|
|
@ -6,20 +6,22 @@ class FanOutOnWriteService < BaseService
|
|||
def call(status)
|
||||
raise Mastodon::RaceConditionError if status.visibility.nil?
|
||||
|
||||
render_anonymous_payload(status)
|
||||
deliver_to_self(status) if status.account.local?
|
||||
|
||||
if status.direct_visibility?
|
||||
deliver_to_mentioned_followers(status)
|
||||
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?
|
||||
|
||||
render_anonymous_payload(status)
|
||||
|
||||
deliver_to_hashtags(status)
|
||||
|
||||
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||
|
@ -58,8 +60,10 @@ class FanOutOnWriteService < BaseService
|
|||
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]
|
||||
status.mentions.joins(:account).merge(status.account.followers_for_local_distribution).select(:id, :account_id).reorder(nil).find_in_batches do |mentions|
|
||||
FeedInsertWorker.push_bulk(mentions) do |mention|
|
||||
[status.id, mention.account_id, :home]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class FavouriteService < BaseService
|
|||
status = favourite.status
|
||||
|
||||
if status.account.local?
|
||||
NotifyService.new.call(status.account, favourite)
|
||||
NotifyService.new.call(status.account, :favourite, favourite)
|
||||
elsif status.account.activitypub?
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
|
||||
end
|
||||
|
|
|
@ -78,6 +78,7 @@ class FetchLinkCardService < BaseService
|
|||
uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme)
|
||||
end
|
||||
|
||||
# rubocop:disable Naming/MethodParameterName
|
||||
def mention_link?(a)
|
||||
@status.mentions.any? do |mention|
|
||||
a['href'] == ActivityPub::TagManager.instance.url_for(mention.account)
|
||||
|
@ -88,6 +89,7 @@ class FetchLinkCardService < BaseService
|
|||
# Avoid links for hashtags and mentions (microformats)
|
||||
a['rel']&.include?('tag') || a['class']&.match?(/u-url|h-card/) || mention_link?(a)
|
||||
end
|
||||
# rubocop:enable Naming/MethodParameterName
|
||||
|
||||
def attempt_oembed
|
||||
service = FetchOEmbedService.new
|
||||
|
|
|
@ -9,12 +9,14 @@ class FollowService < BaseService
|
|||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
|
||||
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
|
||||
# @option [Boolean] :bypass_locked
|
||||
# @option [Boolean] :bypass_limit Allow following past the total follow number
|
||||
# @option [Boolean] :with_rate_limit
|
||||
def call(source_account, target_account, options = {})
|
||||
@source_account = source_account
|
||||
@target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||
@options = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options)
|
||||
@options = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if following_not_possible?
|
||||
raise Mastodon::NotPermittedError if following_not_allowed?
|
||||
|
@ -45,18 +47,18 @@ class FollowService < BaseService
|
|||
end
|
||||
|
||||
def change_follow_options!
|
||||
@source_account.follow!(@target_account, reblogs: @options[:reblogs])
|
||||
@source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
|
||||
end
|
||||
|
||||
def change_follow_request_options!
|
||||
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs])
|
||||
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
|
||||
end
|
||||
|
||||
def request_follow!
|
||||
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
|
||||
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
|
||||
|
||||
if @target_account.local?
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name)
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, :follow_request)
|
||||
elsif @target_account.activitypub?
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
|
||||
end
|
||||
|
@ -65,9 +67,9 @@ class FollowService < BaseService
|
|||
end
|
||||
|
||||
def direct_follow!
|
||||
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
|
||||
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
|
||||
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow)
|
||||
MergeWorker.perform_async(@target_account.id, @source_account.id)
|
||||
|
||||
follow
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HashtagQueryService < BaseService
|
||||
LIMIT_PER_MODE = 4
|
||||
|
||||
def call(tag, params, account = nil, local = false)
|
||||
tags = tags_for(Array(tag.name) | Array(params[:any])).pluck(:id)
|
||||
all = tags_for(params[:all])
|
||||
none = tags_for(params[:none])
|
||||
|
||||
Status.distinct
|
||||
.as_tag_timeline(tags, account, local)
|
||||
.tagged_with_all(all)
|
||||
.tagged_with_none(none)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tags_for(names)
|
||||
Tag.matching_name(Array(names).take(LIMIT_PER_MODE)) if names.present?
|
||||
end
|
||||
end
|
|
@ -18,6 +18,8 @@ class ImportService < BaseService
|
|||
import_mutes!
|
||||
when 'domain_blocking'
|
||||
import_domain_blocks!
|
||||
when 'bookmarks'
|
||||
import_bookmarks!
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -25,7 +27,7 @@ class ImportService < BaseService
|
|||
|
||||
def import_follows!
|
||||
parse_import_data!(['Account address'])
|
||||
import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: 'Show boosts')
|
||||
import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true })
|
||||
end
|
||||
|
||||
def import_blocks!
|
||||
|
@ -35,7 +37,7 @@ class ImportService < BaseService
|
|||
|
||||
def import_mutes!
|
||||
parse_import_data!(['Account address'])
|
||||
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: 'Hide notifications')
|
||||
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
|
||||
end
|
||||
|
||||
def import_domain_blocks!
|
||||
|
@ -65,7 +67,7 @@ class ImportService < BaseService
|
|||
|
||||
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
|
||||
local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
|
||||
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
|
||||
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }]] }.reject { |(id, _)| id.blank? }
|
||||
|
||||
if @import.overwrite?
|
||||
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
|
||||
|
@ -83,11 +85,45 @@ class ImportService < BaseService
|
|||
|
||||
head_items = items.uniq { |acct, _| acct.split('@')[1] }
|
||||
tail_items = items - head_items
|
||||
|
||||
Import::RelationshipWorker.push_bulk(head_items + tail_items) do |acct, extra|
|
||||
[@account.id, acct, action, extra]
|
||||
end
|
||||
end
|
||||
|
||||
def import_bookmarks!
|
||||
parse_import_data!(['#uri'])
|
||||
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#uri'].strip }
|
||||
|
||||
if @import.overwrite?
|
||||
presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
|
||||
|
||||
@account.bookmarks.find_each do |bookmark|
|
||||
if presence_hash[bookmark.status.uri]
|
||||
items.delete(bookmark.status.uri)
|
||||
else
|
||||
bookmark.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
statuses = items.map do |uri|
|
||||
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
||||
next if status.nil? && ActivityPub::TagManager.instance.local_uri?(uri)
|
||||
|
||||
status || ActivityPub::FetchRemoteStatusService.new.call(uri)
|
||||
end.compact
|
||||
|
||||
account_ids = statuses.map(&:account_id)
|
||||
preloaded_relations = relations_map_for_account(@account, account_ids)
|
||||
|
||||
statuses.keep_if { |status| StatusPolicy.new(@account, status, preloaded_relations).show? }
|
||||
|
||||
statuses.each do |status|
|
||||
@account.bookmarks.find_or_create_by!(account: @account, status: status)
|
||||
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?(' ')
|
||||
|
@ -98,7 +134,13 @@ class ImportService < BaseService
|
|||
Paperclip.io_adapters.for(@import.data).read
|
||||
end
|
||||
|
||||
def follow_limit
|
||||
FollowLimitValidator.limit_for_account(@account)
|
||||
def relations_map_for_account(account, account_ids)
|
||||
{
|
||||
blocking: {},
|
||||
blocked_by: Account.blocked_by_map(account_ids, account.id),
|
||||
muting: {},
|
||||
following: Account.following_map(account_ids, account.id),
|
||||
domain_blocking_by_domain: {},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MuteService < BaseService
|
||||
def call(account, target_account, notifications: nil)
|
||||
def call(account, target_account, notifications: nil, duration: 0)
|
||||
return if account.id == target_account.id
|
||||
|
||||
mute = account.mute!(target_account, notifications: notifications)
|
||||
mute = account.mute!(target_account, notifications: notifications, duration: duration)
|
||||
|
||||
if mute.hide_notifications?
|
||||
BlockWorker.perform_async(account.id, target_account.id)
|
||||
|
@ -12,6 +12,8 @@ class MuteService < BaseService
|
|||
MuteWorker.perform_async(account.id, target_account.id)
|
||||
end
|
||||
|
||||
DeleteMuteWorker.perform_at(duration.seconds, mute.id) if duration != 0
|
||||
|
||||
mute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotifyService < BaseService
|
||||
def call(recipient, activity)
|
||||
def call(recipient, type, activity)
|
||||
@recipient = recipient
|
||||
@activity = activity
|
||||
@notification = Notification.new(account: @recipient, activity: @activity)
|
||||
@notification = Notification.new(account: @recipient, type: type, activity: @activity)
|
||||
|
||||
return if recipient.user.nil? || blocked?
|
||||
|
||||
|
@ -13,13 +13,17 @@ class NotifyService < BaseService
|
|||
push_to_conversation! if direct_message?
|
||||
send_email! if email_enabled?
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
return
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blocked_mention?
|
||||
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
|
||||
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
||||
end
|
||||
|
||||
def blocked_status?
|
||||
false
|
||||
end
|
||||
|
||||
def blocked_favourite?
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class PrecomputeFeedService < BaseService
|
||||
def call(account)
|
||||
FeedManager.instance.populate_feed(account)
|
||||
FeedManager.instance.populate_home(account)
|
||||
ensure
|
||||
Redis.current.del("account:#{account.id}:regeneration")
|
||||
end
|
||||
|
|
|
@ -30,14 +30,15 @@ class ProcessMentionsService < BaseService
|
|||
if mention_undeliverable?(mentioned_account)
|
||||
begin
|
||||
mentioned_account = resolve_account_service.call(Regexp.last_match(1))
|
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::UnexpectedResponseError
|
||||
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::UnexpectedResponseError
|
||||
mentioned_account = nil
|
||||
end
|
||||
end
|
||||
|
||||
next match if mention_undeliverable?(mentioned_account) || mentioned_account&.suspended?
|
||||
|
||||
mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)
|
||||
mention = mentioned_account.mentions.new(status: status)
|
||||
mentions << mention if mention.save
|
||||
|
||||
"@#{mentioned_account.acct}"
|
||||
end
|
||||
|
@ -64,9 +65,9 @@ class ProcessMentionsService < BaseService
|
|||
mentioned_account = mention.account
|
||||
|
||||
if mentioned_account.local?
|
||||
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
|
||||
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name, :mention)
|
||||
elsif mentioned_account.activitypub?
|
||||
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
|
||||
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url, { synchronize_followers: !mention.status.distributable? })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class ReblogService < BaseService
|
|||
reblogged_status = reblog.reblog
|
||||
|
||||
if reblogged_status.account.local?
|
||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name)
|
||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, :reblog)
|
||||
elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
||||
end
|
||||
|
|
|
@ -9,44 +9,47 @@ class RemoveStatusService < BaseService
|
|||
# @param [Hash] options
|
||||
# @option [Boolean] :redraft
|
||||
# @option [Boolean] :immediate
|
||||
# @option [Boolean] :original_removed
|
||||
# @option [Boolean] :original_removed
|
||||
def call(status, **options)
|
||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||
@status = status
|
||||
@account = status.account
|
||||
@tags = status.tags.pluck(:name).to_a
|
||||
@mentions = status.active_mentions.includes(:account).to_a
|
||||
@reblogs = status.reblogs.includes(:account).to_a
|
||||
@options = options
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
remove_from_self if status.account.local?
|
||||
remove_from_self if @account.local?
|
||||
remove_from_followers
|
||||
remove_from_lists
|
||||
remove_from_affected
|
||||
remove_reblogs
|
||||
remove_from_hashtags
|
||||
remove_from_public
|
||||
remove_from_media if status.media_attachments.any?
|
||||
remove_from_spam_check
|
||||
remove_media
|
||||
|
||||
# There is no reason to send out Undo activities when the
|
||||
# cause is that the original object has been removed, since
|
||||
# original object being removed implicitly removes reblogs
|
||||
# of it. The Delete activity of the original is forwarded
|
||||
# separately.
|
||||
if @account.local? && !@options[:original_removed]
|
||||
remove_from_remote_followers
|
||||
remove_from_remote_reach
|
||||
end
|
||||
|
||||
# Since reblogs don't mention anyone, don't get reblogged,
|
||||
# favourited and don't contain their own media attachments
|
||||
# or hashtags, this can be skipped
|
||||
unless @status.reblog?
|
||||
remove_from_mentions
|
||||
remove_reblogs
|
||||
remove_from_hashtags
|
||||
remove_from_public
|
||||
remove_from_media if @status.media_attachments.any?
|
||||
remove_from_spam_check
|
||||
remove_media
|
||||
end
|
||||
|
||||
@status.destroy! if @options[:immediate] || !@status.reported?
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
end
|
||||
|
||||
# There is no reason to send out Undo activities when the
|
||||
# cause is that the original object has been removed, since
|
||||
# original object being removed implicitly removes reblogs
|
||||
# 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
|
||||
|
@ -67,31 +70,35 @@ class RemoveStatusService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def remove_from_affected
|
||||
@mentions.map(&:account).select(&:local?).each do |account|
|
||||
redis.publish("timeline:#{account.id}", @payload)
|
||||
def remove_from_mentions
|
||||
# For limited visibility statuses, the mentions that determine
|
||||
# who receives them in their home feed are a subset of followers
|
||||
# and therefore the delete is already handled by sending it to all
|
||||
# followers. Here we send a delete to actively mentioned accounts
|
||||
# that may not follow the account
|
||||
|
||||
@status.active_mentions.find_each do |mention|
|
||||
redis.publish("timeline:#{mention.account_id}", @payload)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_remote_affected
|
||||
def remove_from_remote_reach
|
||||
return if @status.reblog?
|
||||
|
||||
# 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)
|
||||
status_reach_finder = StatusReachFinder.new(@status)
|
||||
|
||||
# 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]
|
||||
ActivityPub::DeliveryWorker.push_bulk(status_reach_finder.inboxes) do |inbox_url|
|
||||
[signed_activity_json, @account.id, inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_remote_followers
|
||||
# ActivityPub
|
||||
ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url|
|
||||
[signed_activity_json, @account.id, inbox_url]
|
||||
end
|
||||
|
@ -118,19 +125,19 @@ class RemoveStatusService < BaseService
|
|||
# because once original status is gone, reblogs will disappear
|
||||
# without us being able to do all the fancy stuff
|
||||
|
||||
@reblogs.each do |reblog|
|
||||
@status.reblogs.includes(:account).find_each do |reblog|
|
||||
RemoveStatusService.new.call(reblog, original_removed: true)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_hashtags
|
||||
@account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag|
|
||||
@account.featured_tags.where(tag_id: @status.tags.map(&:id)).each do |featured_tag|
|
||||
featured_tag.decrement(@status.id)
|
||||
end
|
||||
|
||||
return unless @status.public_visibility?
|
||||
|
||||
@tags.each do |hashtag|
|
||||
@status.tags.map(&:name).each do |hashtag|
|
||||
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
|
||||
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
|
||||
end
|
||||
|
@ -140,22 +147,14 @@ class RemoveStatusService < BaseService
|
|||
return unless @status.public_visibility?
|
||||
|
||||
redis.publish('timeline:public', @payload)
|
||||
if @status.local?
|
||||
redis.publish('timeline:public:local', @payload)
|
||||
else
|
||||
redis.publish('timeline:public:remote', @payload)
|
||||
end
|
||||
redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', @payload)
|
||||
end
|
||||
|
||||
def remove_from_media
|
||||
return unless @status.public_visibility?
|
||||
|
||||
redis.publish('timeline:public:media', @payload)
|
||||
if @status.local?
|
||||
redis.publish('timeline:public:local:media', @payload)
|
||||
else
|
||||
redis.publish('timeline:public:remote:media', @payload)
|
||||
end
|
||||
redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', @payload)
|
||||
end
|
||||
|
||||
def remove_media
|
||||
|
|
|
@ -24,7 +24,8 @@ class ReportService < BaseService
|
|||
target_account: @target_account,
|
||||
status_ids: @status_ids,
|
||||
comment: @comment,
|
||||
uri: @options[:uri]
|
||||
uri: @options[:uri],
|
||||
forwarded: ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ class ResolveAccountService < BaseService
|
|||
include DomainControlHelper
|
||||
include WebfingerHelper
|
||||
|
||||
class WebfingerRedirectError < StandardError; end
|
||||
|
||||
# Find or create an account record for a remote user. When creating,
|
||||
# look up the user's webfinger and fetch ActivityPub data
|
||||
# @param [String, Account] uri URI in the username@domain format or account record
|
||||
|
@ -26,12 +24,12 @@ class ResolveAccountService < BaseService
|
|||
|
||||
@account ||= Account.find_remote(@username, @domain)
|
||||
|
||||
return @account if @account&.local? || !webfinger_update_due?
|
||||
return @account if @account&.local? || @domain.nil? || !webfinger_update_due?
|
||||
|
||||
# At this point we are in need of a Webfinger query, which may
|
||||
# yield us a different username/domain through a redirect
|
||||
|
||||
process_webfinger!(@uri)
|
||||
@domain = nil if TagManager.instance.local_domain?(@domain)
|
||||
|
||||
# Because the username/domain pair may be different than what
|
||||
# we already checked, we need to check if we've already got
|
||||
|
@ -41,13 +39,18 @@ class ResolveAccountService < BaseService
|
|||
|
||||
@account ||= Account.find_remote(@username, @domain)
|
||||
|
||||
return @account if @account&.local? || !webfinger_update_due?
|
||||
if gone_from_origin? && not_yet_deleted?
|
||||
queue_deletion!
|
||||
return
|
||||
end
|
||||
|
||||
return @account if @account&.local? || gone_from_origin? || !webfinger_update_due?
|
||||
|
||||
# Now it is certain, it is definitely a remote account, and it
|
||||
# either needs to be created, or updated from fresh data
|
||||
|
||||
process_account!
|
||||
rescue Goldfinger::Error, WebfingerRedirectError, Oj::ParseError => e
|
||||
fetch_account!
|
||||
rescue Webfinger::Error, Oj::ParseError => e
|
||||
Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
|
||||
nil
|
||||
end
|
||||
|
@ -76,33 +79,37 @@ class ResolveAccountService < BaseService
|
|||
@uri = [@username, @domain].compact.join('@')
|
||||
end
|
||||
|
||||
def process_webfinger!(uri, redirected = false)
|
||||
def process_webfinger!(uri)
|
||||
@webfinger = webfinger!("acct:#{uri}")
|
||||
confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
|
||||
confirmed_username, confirmed_domain = split_acct(@webfinger.subject)
|
||||
|
||||
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
||||
@username = confirmed_username
|
||||
@domain = confirmed_domain
|
||||
@uri = uri
|
||||
elsif !redirected
|
||||
return process_webfinger!("#{confirmed_username}@#{confirmed_domain}", true)
|
||||
else
|
||||
raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
|
||||
return
|
||||
end
|
||||
|
||||
@domain = nil if TagManager.instance.local_domain?(@domain)
|
||||
# Account doesn't match, so it may have been redirected
|
||||
@webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
|
||||
@username, @domain = split_acct(@webfinger.subject)
|
||||
|
||||
unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
||||
raise Webfinger::RedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
|
||||
end
|
||||
rescue Webfinger::GoneError
|
||||
@gone = true
|
||||
end
|
||||
|
||||
def process_account!
|
||||
def split_acct(acct)
|
||||
acct.gsub(/\Aacct:/, '').split('@')
|
||||
end
|
||||
|
||||
def fetch_account!
|
||||
return unless activitypub_ready?
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
@account = Account.find_remote(@username, @domain)
|
||||
|
||||
next if actor_json.nil?
|
||||
|
||||
@account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json)
|
||||
@account = ActivityPub::FetchRemoteAccountService.new.call(actor_url)
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
|
@ -118,18 +125,23 @@ class ResolveAccountService < BaseService
|
|||
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)
|
||||
['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type'))
|
||||
end
|
||||
|
||||
def actor_url
|
||||
@actor_url ||= @webfinger.link('self').href
|
||||
@actor_url ||= @webfinger.link('self', 'href')
|
||||
end
|
||||
|
||||
def actor_json
|
||||
return @actor_json if defined?(@actor_json)
|
||||
def gone_from_origin?
|
||||
@gone
|
||||
end
|
||||
|
||||
json = fetch_resource(actor_url, false)
|
||||
@actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil
|
||||
def not_yet_deleted?
|
||||
@account.present? && !@account.local?
|
||||
end
|
||||
|
||||
def queue_deletion!
|
||||
AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true)
|
||||
end
|
||||
|
||||
def lock_options
|
||||
|
|
|
@ -34,7 +34,17 @@ class ResolveURLService < BaseService
|
|||
|
||||
# It may happen that the resource is a private toot, and thus not fetchable,
|
||||
# but we can return the toot if we already know about it.
|
||||
status = Status.find_by(uri: @url) || Status.find_by(url: @url)
|
||||
scope = Status.where(uri: @url)
|
||||
|
||||
# We don't have an index on `url`, so try guessing the `uri` from `url`
|
||||
parsed_url = Addressable::URI.parse(@url)
|
||||
parsed_url.path.match(%r{/@(?<username>#{Account::USERNAME_RE})/(?<status_id>[0-9]+)\Z}) do |matched|
|
||||
parsed_url.path = "/users/#{matched[:username]}/statuses/#{matched[:status_id]}"
|
||||
scope = scope.or(Status.where(uri: parsed_url.to_s, url: @url))
|
||||
end
|
||||
|
||||
status = scope.first
|
||||
|
||||
authorize_with @on_behalf_of, status, :show? unless status.nil?
|
||||
status
|
||||
rescue Mastodon::NotPermittedError
|
||||
|
|
|
@ -3,173 +3,91 @@
|
|||
class SuspendAccountService < BaseService
|
||||
include Payloadable
|
||||
|
||||
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
|
||||
mute_relationships
|
||||
muted_by_relationships
|
||||
notifications
|
||||
owned_lists
|
||||
passive_relationships
|
||||
report_notes
|
||||
scheduled_statuses
|
||||
status_pins
|
||||
).freeze
|
||||
|
||||
ASSOCIATIONS_ON_DESTROY = %w(
|
||||
reports
|
||||
targeted_moderation_notes
|
||||
targeted_reports
|
||||
).freeze
|
||||
|
||||
# Suspend or remove an account and remove as much of its data
|
||||
# as possible. If it's a local account and it has not been confirmed
|
||||
# or never been approved, then side effects are skipped and both
|
||||
# the user and account records are removed fully. Otherwise,
|
||||
# it is controlled by options.
|
||||
# @param [Account]
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
|
||||
# @option [Boolean] :reserve_username Keep account record
|
||||
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
|
||||
# @option [Time] :suspended_at Only applicable when :reserve_username is true
|
||||
def call(account, **options)
|
||||
def call(account)
|
||||
@account = account
|
||||
@options = { reserve_username: true, reserve_email: true }.merge(options)
|
||||
|
||||
if @account.local? && @account.user_unconfirmed_or_pending?
|
||||
@options[:reserve_email] = false
|
||||
@options[:reserve_username] = false
|
||||
@options[:skip_side_effects] = true
|
||||
end
|
||||
|
||||
reject_follows!
|
||||
purge_user!
|
||||
purge_profile!
|
||||
purge_content!
|
||||
suspend!
|
||||
reject_remote_follows!
|
||||
distribute_update_actor!
|
||||
unmerge_from_home_timelines!
|
||||
unmerge_from_list_timelines!
|
||||
privatize_media_attachments!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_follows!
|
||||
def suspend!
|
||||
@account.suspend! unless @account.suspended?
|
||||
end
|
||||
|
||||
def reject_remote_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]
|
||||
# When suspending a remote account, the account obviously doesn't
|
||||
# actually become suspended on its origin server, i.e. unlike a
|
||||
# locally suspended account it continues to have access to its home
|
||||
# feed and other content. To prevent it from being able to continue
|
||||
# to access toots it would receive because it follows local accounts,
|
||||
# we have to force it to unfollow them. Unfortunately, there is no
|
||||
# counterpart to this operation, i.e. you can't then force a remote
|
||||
# account to re-follow you, so this part is not reversible.
|
||||
|
||||
follows = Follow.where(account: @account).to_a
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(follows) do |follow|
|
||||
[Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
|
||||
end
|
||||
|
||||
follows.each(&:destroy)
|
||||
end
|
||||
|
||||
def distribute_update_actor!
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id) if @account.local?
|
||||
end
|
||||
|
||||
def unmerge_from_home_timelines!
|
||||
@account.followers_for_local_distribution.find_each do |follower|
|
||||
FeedManager.instance.unmerge_from_home(@account, follower)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_user!
|
||||
return if !@account.local? || @account.user.nil?
|
||||
|
||||
if @options[:reserve_email]
|
||||
@account.user.disable!
|
||||
@account.user.invites.where(uses: 0).destroy_all
|
||||
else
|
||||
@account.user.destroy
|
||||
def unmerge_from_list_timelines!
|
||||
@account.lists_for_local_distribution.find_each do |list|
|
||||
FeedManager.instance.unmerge_from_list(@account, list)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_content!
|
||||
distribute_delete_actor! if @account.local? && !@options[:skip_side_effects]
|
||||
def privatize_media_attachments!
|
||||
attachment_names = MediaAttachment.attachment_definitions.keys
|
||||
|
||||
@account.statuses.reorder(nil).find_in_batches do |statuses|
|
||||
statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username]
|
||||
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects])
|
||||
end
|
||||
@account.media_attachments.find_each do |media_attachment|
|
||||
attachment_names.each do |attachment_name|
|
||||
attachment = media_attachment.public_send(attachment_name)
|
||||
styles = [:original] | attachment.styles.keys
|
||||
|
||||
@account.media_attachments.reorder(nil).find_each do |media_attachment|
|
||||
next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id)
|
||||
next if attachment.blank?
|
||||
|
||||
media_attachment.destroy
|
||||
end
|
||||
styles.each do |style|
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
begin
|
||||
attachment.s3_object(style).acl.put(acl: 'private')
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
Rails.logger.warn "Tried to change acl on non-existent key #{attachment.s3_object(style).key}"
|
||||
end
|
||||
when :fog
|
||||
# Not supported
|
||||
when :filesystem
|
||||
begin
|
||||
FileUtils.chmod(0o600 & ~File.umask, attachment.path(style)) unless attachment.path(style).nil?
|
||||
rescue Errno::ENOENT
|
||||
Rails.logger.warn "Tried to change permission on non-existent file #{attachment.path(style)}"
|
||||
end
|
||||
end
|
||||
|
||||
@account.polls.reorder(nil).find_each do |poll|
|
||||
next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id)
|
||||
|
||||
poll.destroy
|
||||
end
|
||||
|
||||
associations_for_destruction.each do |association_name|
|
||||
destroy_all(@account.public_send(association_name))
|
||||
end
|
||||
|
||||
@account.destroy unless @options[:reserve_username]
|
||||
end
|
||||
|
||||
def purge_profile!
|
||||
# If the account is going to be destroyed
|
||||
# there is no point wasting time updating
|
||||
# its values first
|
||||
|
||||
return unless @options[:reserve_username]
|
||||
|
||||
@account.silenced_at = nil
|
||||
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
||||
@account.locked = false
|
||||
@account.memorial = false
|
||||
@account.discoverable = false
|
||||
@account.display_name = ''
|
||||
@account.note = ''
|
||||
@account.fields = []
|
||||
@account.statuses_count = 0
|
||||
@account.followers_count = 0
|
||||
@account.following_count = 0
|
||||
@account.moved_to_account = nil
|
||||
@account.trust_level = :untrusted
|
||||
@account.avatar.destroy
|
||||
@account.header.destroy
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def destroy_all(association)
|
||||
association.in_batches.destroy_all
|
||||
end
|
||||
|
||||
def distribute_delete_actor!
|
||||
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
|
||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def delete_actor_json
|
||||
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
|
||||
end
|
||||
|
||||
def build_reject_json(follow)
|
||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
||||
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 reported_status_ids
|
||||
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
|
||||
end
|
||||
|
||||
def associations_for_destruction
|
||||
if @options[:reserve_username]
|
||||
ASSOCIATIONS_ON_SUSPEND
|
||||
else
|
||||
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
|
||||
CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,6 @@ class UnblockDomainService < BaseService
|
|||
scope = Account.by_domain_and_subdomains(domain_block.domain)
|
||||
|
||||
scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.noop?
|
||||
scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) if domain_block.suspend?
|
||||
scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil, suspension_origin: nil) if domain_block.suspend?
|
||||
end
|
||||
end
|
||||
|
|
84
app/services/unsuspend_account_service.rb
Normal file
84
app/services/unsuspend_account_service.rb
Normal file
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UnsuspendAccountService < BaseService
|
||||
def call(account)
|
||||
@account = account
|
||||
|
||||
unsuspend!
|
||||
refresh_remote_account!
|
||||
|
||||
return if @account.nil?
|
||||
|
||||
merge_into_home_timelines!
|
||||
merge_into_list_timelines!
|
||||
publish_media_attachments!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unsuspend!
|
||||
@account.unsuspend! if @account.suspended?
|
||||
end
|
||||
|
||||
def refresh_remote_account!
|
||||
return if @account.local?
|
||||
|
||||
# While we had the remote account suspended, it could be that
|
||||
# it got suspended on its origin, too. So, we need to refresh
|
||||
# it straight away so it gets marked as remotely suspended in
|
||||
# that case.
|
||||
|
||||
@account.update!(last_webfingered_at: nil)
|
||||
@account = ResolveAccountService.new.call(@account)
|
||||
|
||||
# Worth noting that it is possible that the remote has not only
|
||||
# been suspended, but deleted permanently, in which case
|
||||
# @account would now be nil.
|
||||
end
|
||||
|
||||
def merge_into_home_timelines!
|
||||
@account.followers_for_local_distribution.find_each do |follower|
|
||||
FeedManager.instance.merge_into_home(@account, follower)
|
||||
end
|
||||
end
|
||||
|
||||
def merge_into_list_timelines!
|
||||
@account.lists_for_local_distribution.find_each do |list|
|
||||
FeedManager.instance.merge_into_list(@account, list)
|
||||
end
|
||||
end
|
||||
|
||||
def publish_media_attachments!
|
||||
attachment_names = MediaAttachment.attachment_definitions.keys
|
||||
|
||||
@account.media_attachments.find_each do |media_attachment|
|
||||
attachment_names.each do |attachment_name|
|
||||
attachment = media_attachment.public_send(attachment_name)
|
||||
styles = [:original] | attachment.styles.keys
|
||||
|
||||
next if attachment.blank?
|
||||
|
||||
styles.each do |style|
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
begin
|
||||
attachment.s3_object(style).acl.put(acl: Paperclip::Attachment.default_options[:s3_permissions])
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
Rails.logger.warn "Tried to change acl on non-existent key #{attachment.s3_object(style).key}"
|
||||
end
|
||||
when :fog
|
||||
# Not supported
|
||||
when :filesystem
|
||||
begin
|
||||
FileUtils.chmod(0o666 & ~File.umask, attachment.path(style)) unless attachment.path(style).nil?
|
||||
rescue Errno::ENOENT
|
||||
Rails.logger.warn "Tried to change permission on non-existent file #{attachment.path(style)}"
|
||||
end
|
||||
end
|
||||
|
||||
CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,8 +12,8 @@ class UpdateAccountService < BaseService
|
|||
check_links(account)
|
||||
process_hashtags(account)
|
||||
end
|
||||
rescue Mastodon::DimensionsValidationError => de
|
||||
account.errors.add(:avatar, de.message)
|
||||
rescue Mastodon::DimensionsValidationError, Mastodon::StreamValidationError => e
|
||||
account.errors.add(:avatar, e.message)
|
||||
false
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue