Skip to content

Commit

Permalink
Add support for reversible suspensions through ActivityPub
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron committed Oct 18, 2020
1 parent 4130aef commit db01b5d
Show file tree
Hide file tree
Showing 24 changed files with 209 additions and 53 deletions.
6 changes: 3 additions & 3 deletions app/controllers/activitypub/collections_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ def show
def set_items
case params[:id]
when 'featured'
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
@items = @account.suspended? ? [] : for_signed_account { cache_collection(@account.pinned_statuses, Status) }
when 'tags'
@items = for_signed_account { @account.featured_tags }
@items = @account.suspended? ? [] : for_signed_account { @account.featured_tags }
when 'devices'
@items = @account.devices
@items = @account.suspended? ? [] : @account.devices
else
not_found
end
Expand Down
18 changes: 12 additions & 6 deletions app/controllers/activitypub/outboxes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ def prev_page
def set_statuses
return unless page_requested?

@statuses = cache_collection_paginated_by_id(
@account.statuses.permitted_for(@account, signed_request_account),
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)
)
@statuses = begin
if @account.suspended?
[]
else
cache_collection_paginated_by_id(
@account.statuses.permitted_for(@account, signed_request_account),
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)
)
end
end
end

def page_requested?
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/activitypub/replies_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def set_status
end

def set_replies
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/concerns/account_owned_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ def check_account_approval
end

def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended?
expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? || (@account.suspended? && request.format != :json)
end
end
2 changes: 1 addition & 1 deletion app/controllers/follower_accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def follows

scope = Follow.where(target_account: @account)
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
@follows = @account.suspended? ? [] : scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end

def page_requested?
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/following_accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def follows

scope = Follow.where(account: @account)
scope = scope.where.not(target_account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
@follows = @account.suspended? ? [] : scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end

def page_requested?
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/deletes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def challenge_passed?
end

def destroy_account!
current_account.suspend!
current_account.suspend!(origin: :local)
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out
end
Expand Down
1 change: 1 addition & 0 deletions app/lib/activitypub/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze

def self.default_key_transform
Expand Down
4 changes: 4 additions & 0 deletions app/lib/webfinger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

class Webfinger
class Error < StandardError; end
class GoneError < Error; end
class RedirectError < StandardError; end

class Response
def initialize(body)
Expand Down Expand Up @@ -47,6 +49,8 @@ def body_from_webfinger(url = standard_url, use_fallback = true)
res.body_with_limit
elsif res.code == 404 && use_fallback
body_from_host_meta
elsif res.code == 410
raise Webfinger::GoneError, "#{@uri} is gone from the server"
else
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
end
Expand Down
16 changes: 13 additions & 3 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
# suspension_origin :integer
#

class Account < ApplicationRecord
Expand All @@ -72,6 +73,7 @@ class Account < ApplicationRecord
}.freeze

enum protocol: [:ostatus, :activitypub]
enum suspension_origin: [:local, :remote], _prefix: true

validates :username, presence: true
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
Expand Down Expand Up @@ -220,17 +222,25 @@ def suspended?
suspended_at.present?
end

def suspend!(date = Time.now.utc)
def suspended_permanently?
suspended? && deletion_request.nil?
end

def suspended_temporarily?
suspended? && deletion_request.present?
end

def suspend!(date: Time.now.utc, origin: :local)
transaction do
create_deletion_request!
update!(suspended_at: date)
update!(suspended_at: date, suspension_origin: origin)
end
end

def unsuspend!
transaction do
deletion_request&.destroy!
update!(suspended_at: nil)
update!(suspended_at: nil, suspension_origin: nil)
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/models/admin/account_action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def handle_silence!
def handle_suspend!
authorize(target_account, :suspend?)
log_action(:suspend, target_account)
target_account.suspend!
target_account.suspend!(origin: :local)
end

def text_for_warning
Expand Down
4 changes: 2 additions & 2 deletions app/policies/account_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ def suspend?
end

def destroy?
record.suspended? && record.deletion_request.present? && admin?
record.suspended_temporarily? && admin?
end

def unsuspend?
staff?
staff? && record.suspension_origin_local?
end

def silence?
Expand Down
33 changes: 23 additions & 10 deletions app/serializers/activitypub/actor_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer

context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :identity_proof,
:discoverable, :olm
:discoverable, :olm, :suspended

attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured, :featured_tags,
Expand All @@ -23,6 +23,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
attribute :devices, unless: :instance_actor?
attribute :moved_to, if: :moved?
attribute :also_known_as, if: :also_known_as?
attribute :suspended, if: :suspended?

class EndpointsSerializer < ActivityPub::Serializer
include RoutingHelper
Expand All @@ -39,7 +40,7 @@ def shared_inbox
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?

delegate :moved?, :instance_actor?, to: :object
delegate :suspended?, :instance_actor?, to: :object

def id
object.instance_actor? ? instance_actor_url : account_url(object)
Expand Down Expand Up @@ -93,12 +94,16 @@ def preferred_username
object.username
end

def discoverable
object.suspended? ? false : (object.discoverable || false)
end

def name
object.display_name
object.suspended? ? '' : object.display_name
end

def summary
Formatter.instance.simplified_format(object)
object.suspended? ? '' : Formatter.instance.simplified_format(object)
end

def icon
Expand All @@ -113,36 +118,44 @@ def public_key
object
end

def suspended
object.suspended?
end

def url
object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object)
end

def avatar_exists?
object.avatar?
!object.suspended? && object.avatar?
end

def header_exists?
object.header?
!object.suspended? && object.header?
end

def manually_approves_followers
object.locked
object.suspended? ? false : object.locked
end

def virtual_tags
object.emojis + object.tags
object.suspended? ? [] : (object.emojis + object.tags)
end

def virtual_attachments
object.fields + object.identity_proofs.active
object.suspended? ? [] : (object.fields + object.identity_proofs.active)
end

def moved_to
ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
end

def moved?
!object.suspended? && object.moved?
end

def also_known_as?
!object.also_known_as.empty?
!object.suspended? && !object.also_known_as.empty?
end

class CustomEmojiSerializer < ActivityPub::EmojiSerializer
Expand Down
41 changes: 34 additions & 7 deletions app/services/activitypub/process_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ def call(username, domain, json, options = {})

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
Expand All @@ -37,8 +38,9 @@ def call(username, domain, json, options = {})
after_protocol_change! if protocol_changed?
after_key_change! if key_changed? && !@options[:signed_with_known_key]
clear_tombstones! if key_changed?
after_suspension! 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
Expand All @@ -64,8 +66,9 @@ 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_attributes! unless @account.suspended?
set_fetchable_attributes! unless @options[:only_keys] || @account.suspended?

@account.save_with_optional_media!
end
Expand Down Expand Up @@ -99,6 +102,18 @@ def set_fetchable_attributes!
@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
Expand All @@ -107,6 +122,14 @@ def after_key_change!
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
Expand Down Expand Up @@ -227,6 +250,10 @@ def key_changed?
!@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
Expand Down
10 changes: 9 additions & 1 deletion app/services/activitypub/process_collection_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def call(body, account, **options)
@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'
Expand All @@ -28,6 +28,14 @@ def different_actor?
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri
end

def suspended_actor?
@account.suspended? && !(@account.suspension_origin_remote? && activity_allowed_while_suspended?)
end

def activity_allowed_while_suspended?
%w(Delete Update).include?(@json['type']) && (value_or_id(@json['object']) == @account.uri)
end

def process_items(items)
items.reverse_each.map { |item| process_item(item) }.compact
end
Expand Down
Loading

0 comments on commit db01b5d

Please sign in to comment.