Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- `Gemfile.lock`:
  Not a real conflict, upstream updated dependencies that were too close to
  glitch-soc-only ones in the file.
- `app/controllers/oauth/authorized_applications_controller.rb`:
  Upstream changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc's theming system.
  Ported upstream changes.
- `app/controllers/settings/base_controller.rb`:
  Upstream refactored and changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc's theming system.
  Ported upstream changes.
- `app/controllers/settings/sessions_controller.rb`:
  Upstream refactored and changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc's theming system.
  Ported upstream changes.
- `app/models/user.rb`:
  Upstream refactored and changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc not preventing moved accounts from logging
  in.
  Ported upstream changes while keeping the ability for moved accounts to log
  in.
- `app/policies/status_policy.rb`:
  Upstream refactored and changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc's local-only toots.
  Ported upstream changes.
- `app/serializers/rest/account_serializer.rb`:
  Upstream refactored and changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc's ability  to hide followers count.
  Ported upstream changes.
- `app/services/process_mentions_service.rb`:
  Upstream refactored and changed the logic surrounding suspended accounts.
  Minor conflict due to glitch-soc's local-only toots.
  Ported upstream changes.
- `package.json`:
  Not a real conflict, upstream updated dependencies that were too close to
  glitch-soc-only ones in the file.
This commit is contained in:
Thibaut Girka
2020-09-28 14:13:30 +02:00
178 changed files with 2307 additions and 964 deletions

View File

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

View File

@ -36,7 +36,7 @@ class BlockDomainService < BaseService
def suspend_accounts!
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
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

View File

@ -0,0 +1,180 @@
# frozen_string_literal: true
class DeleteAccountService < 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)
@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!
fulfill_deletion_request!
end
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?
if @options[:reserve_email]
@account.user.disable!
@account.user.invites.where(uses: 0).destroy_all
else
@account.user.destroy
end
end
def purge_content!
distribute_delete_actor! if @account.local? && !@options[:skip_side_effects]
@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.reorder(nil).find_each do |media_attachment|
next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id)
media_attachment.destroy
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 fulfill_deletion_request!
@account.deletion_request&.destroy
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
end
end
end

View File

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

View File

@ -9,12 +9,13 @@ 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] :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, with_rate_limit: false }.merge(options)
raise ActiveRecord::RecordNotFound if following_not_possible?
raise Mastodon::NotPermittedError if following_not_allowed?
@ -45,18 +46,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])
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 +66,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])
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

View File

@ -25,7 +25,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, follow_limit, reblogs: { header: 'Show boosts', default: true })
end
def import_blocks!
@ -35,7 +35,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 +65,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] }

View File

@ -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?
@ -22,6 +22,10 @@ class NotifyService < BaseService
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
end
def blocked_status?
false
end
def blocked_favourite?
false
end

View File

@ -58,7 +58,7 @@ 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? && !@status.local_only?
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
end

View File

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

View File

@ -1,175 +1,52 @@
# frozen_string_literal: true
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!
unmerge_from_home_timelines!
unmerge_from_list_timelines!
privatize_media_attachments!
end
private
def reject_follows!
return if @account.local? || !@account.activitypub?
def suspend!
@account.suspend! unless @account.suspended?
end
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
def unmerge_from_home_timelines!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.unmerge_from_timeline(@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)
media_attachment.destroy
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
styles.each do |style|
case Paperclip::Attachment.default_options[:storage]
when :s3
attachment.s3_object(style).acl.put(:private)
when :fog
# Not supported
when :filesystem
FileUtils.chmod(0o600 & ~File.umask, attachment.path(style))
end
end
end
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
class UnsuspendAccountService < BaseService
def call(account)
@account = account
unsuspend!
merge_into_home_timelines!
merge_into_list_timelines!
publish_media_attachments!
end
private
def unsuspend!
@account.unsuspend! if @account.suspended?
end
def merge_into_home_timelines!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.merge_into_timeline(@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
styles.each do |style|
case Paperclip::Attachment.default_options[:storage]
when :s3
attachment.s3_object(style).acl.put(Paperclip::Attachment.default_options[:s3_permissions])
when :fog
# Not supported
when :filesystem
FileUtils.chmod(0o666 & ~File.umask, attachment.path(style))
end
end
end
end
end
end