Add federation relay support (#7998)
* Add federation relay support * Add admin UI for managing relays * Include actor on relay-related activities * Fix i18n
This commit is contained in:
		
							
								
								
									
										58
									
								
								app/controllers/admin/relays_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								app/controllers/admin/relays_controller.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Admin
 | 
			
		||||
  class RelaysController < BaseController
 | 
			
		||||
    before_action :set_relay, except: [:index, :new, :create]
 | 
			
		||||
 | 
			
		||||
    def index
 | 
			
		||||
      authorize :relay, :update?
 | 
			
		||||
      @relays = Relay.all
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def new
 | 
			
		||||
      authorize :relay, :update?
 | 
			
		||||
      @relay = Relay.new(inbox_url: Relay::PRESET_RELAY)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def create
 | 
			
		||||
      authorize :relay, :update?
 | 
			
		||||
 | 
			
		||||
      @relay = Relay.new(resource_params)
 | 
			
		||||
 | 
			
		||||
      if @relay.save
 | 
			
		||||
        @relay.enable!
 | 
			
		||||
        redirect_to admin_relays_path
 | 
			
		||||
      else
 | 
			
		||||
        render action: :new
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def destroy
 | 
			
		||||
      authorize :relay, :update?
 | 
			
		||||
      @relay.destroy
 | 
			
		||||
      redirect_to admin_relays_path
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def enable
 | 
			
		||||
      authorize :relay, :update?
 | 
			
		||||
      @relay.enable!
 | 
			
		||||
      redirect_to admin_relays_path
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def disable
 | 
			
		||||
      authorize :relay, :update?
 | 
			
		||||
      @relay.disable!
 | 
			
		||||
      redirect_to admin_relays_path
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def set_relay
 | 
			
		||||
      @relay = Relay.find(params[:id])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def resource_params
 | 
			
		||||
      params.require(:relay).permit(:inbox_url)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -165,6 +165,11 @@
 | 
			
		||||
      color: $valid-value-color;
 | 
			
		||||
      font-weight: 500;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .negative-hint {
 | 
			
		||||
      color: $error-value-color;
 | 
			
		||||
      font-weight: 500;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .simple_form {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										74
									
								
								app/models/relay.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/models/relay.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
# == Schema Information
 | 
			
		||||
#
 | 
			
		||||
# Table name: relays
 | 
			
		||||
#
 | 
			
		||||
#  id                 :bigint(8)        not null, primary key
 | 
			
		||||
#  inbox_url          :string           default(""), not null
 | 
			
		||||
#  enabled            :boolean          default(FALSE), not null
 | 
			
		||||
#  follow_activity_id :string
 | 
			
		||||
#  created_at         :datetime         not null
 | 
			
		||||
#  updated_at         :datetime         not null
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class Relay < ApplicationRecord
 | 
			
		||||
  PRESET_RELAY = 'https://relay.joinmastodon.org/inbox'
 | 
			
		||||
 | 
			
		||||
  validates :inbox_url, presence: true, uniqueness: true, url: true, if: :will_save_change_to_inbox_url?
 | 
			
		||||
 | 
			
		||||
  scope :enabled, -> { where(enabled: true) }
 | 
			
		||||
 | 
			
		||||
  before_destroy :ensure_disabled
 | 
			
		||||
 | 
			
		||||
  def enable!
 | 
			
		||||
    activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
 | 
			
		||||
    payload     = Oj.dump(follow_activity(activity_id))
 | 
			
		||||
 | 
			
		||||
    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
 | 
			
		||||
    update(enabled: true, follow_activity_id: activity_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def disable!
 | 
			
		||||
    activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
 | 
			
		||||
    payload     = Oj.dump(unfollow_activity(activity_id))
 | 
			
		||||
 | 
			
		||||
    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
 | 
			
		||||
    update(enabled: false, follow_activity_id: nil)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def follow_activity(activity_id)
 | 
			
		||||
    {
 | 
			
		||||
      '@context': ActivityPub::TagManager::CONTEXT,
 | 
			
		||||
      id: activity_id,
 | 
			
		||||
      type: 'Follow',
 | 
			
		||||
      actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
 | 
			
		||||
      object: ActivityPub::TagManager::COLLECTIONS[:public],
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unfollow_activity(activity_id)
 | 
			
		||||
    {
 | 
			
		||||
      '@context': ActivityPub::TagManager::CONTEXT,
 | 
			
		||||
      id: activity_id,
 | 
			
		||||
      type: 'Undo',
 | 
			
		||||
      actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
 | 
			
		||||
      object: {
 | 
			
		||||
        id: follow_activity_id,
 | 
			
		||||
        type: 'Follow',
 | 
			
		||||
        actor: ActivityPub::TagManager.instance.uri_for(some_local_account),
 | 
			
		||||
        object: ActivityPub::TagManager::COLLECTIONS[:public],
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def some_local_account
 | 
			
		||||
    @some_local_account ||= Account.local.find_by(suspended: false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ensure_disabled
 | 
			
		||||
    return unless enabled?
 | 
			
		||||
    disable!
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										7
									
								
								app/policies/relay_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/policies/relay_policy.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class RelayPolicy < ApplicationPolicy
 | 
			
		||||
  def update?
 | 
			
		||||
    admin?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
 | 
			
		||||
  attributes :id, :type, :actor
 | 
			
		||||
  attributes :id, :type, :actor, :to
 | 
			
		||||
  attribute :virtual_object, key: :object
 | 
			
		||||
 | 
			
		||||
  def id
 | 
			
		||||
@@ -19,4 +19,8 @@ class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
 | 
			
		||||
  def virtual_object
 | 
			
		||||
    actor
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to
 | 
			
		||||
    [ActivityPub::TagManager::COLLECTIONS[:public]]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ class ActivityPub::DeleteSerializer < ActiveModel::Serializer
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  attributes :id, :type, :actor
 | 
			
		||||
  attributes :id, :type, :actor, :to
 | 
			
		||||
 | 
			
		||||
  has_one :object, serializer: TombstoneSerializer
 | 
			
		||||
 | 
			
		||||
@@ -32,4 +32,8 @@ class ActivityPub::DeleteSerializer < ActiveModel::Serializer
 | 
			
		||||
  def actor
 | 
			
		||||
    ActivityPub::TagManager.instance.uri_for(object.account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to
 | 
			
		||||
    [ActivityPub::TagManager::COLLECTIONS[:public]]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ActivityPub::UndoAnnounceSerializer < ActiveModel::Serializer
 | 
			
		||||
  attributes :id, :type, :actor
 | 
			
		||||
  attributes :id, :type, :actor, :to
 | 
			
		||||
 | 
			
		||||
  has_one :object, serializer: ActivityPub::ActivitySerializer
 | 
			
		||||
 | 
			
		||||
@@ -16,4 +16,8 @@ class ActivityPub::UndoAnnounceSerializer < ActiveModel::Serializer
 | 
			
		||||
  def actor
 | 
			
		||||
    ActivityPub::TagManager.instance.uri_for(object.account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to
 | 
			
		||||
    [ActivityPub::TagManager::COLLECTIONS[:public]]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ActivityPub::UpdateSerializer < ActiveModel::Serializer
 | 
			
		||||
  attributes :id, :type, :actor
 | 
			
		||||
  attributes :id, :type, :actor, :to
 | 
			
		||||
 | 
			
		||||
  has_one :object, serializer: ActivityPub::ActorSerializer
 | 
			
		||||
 | 
			
		||||
@@ -16,4 +16,8 @@ class ActivityPub::UpdateSerializer < ActiveModel::Serializer
 | 
			
		||||
  def actor
 | 
			
		||||
    ActivityPub::TagManager.instance.uri_for(object)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to
 | 
			
		||||
    [ActivityPub::TagManager::COLLECTIONS[:public]]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,18 @@ class RemoveStatusService < BaseService
 | 
			
		||||
    ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url|
 | 
			
		||||
      [signed_activity_json, @account.id, inbox_url]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    relay! if relayable?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def relayable?
 | 
			
		||||
    @status.public_visibility?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def relay!
 | 
			
		||||
    ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
 | 
			
		||||
      [signed_activity_json, @account.id, inbox_url]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def salmon_xml
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,13 @@ class SuspendAccountService < BaseService
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def purge_content!
 | 
			
		||||
    ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id) if @account.local?
 | 
			
		||||
    if @account.local?
 | 
			
		||||
      ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id)
 | 
			
		||||
 | 
			
		||||
      ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
 | 
			
		||||
        [delete_actor_json, @account.id, inbox_url]
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    @account.statuses.reorder(nil).find_in_batches do |statuses|
 | 
			
		||||
      BatchedRemoveStatusService.new.call(statuses)
 | 
			
		||||
@@ -59,12 +65,14 @@ class SuspendAccountService < BaseService
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def delete_actor_json
 | 
			
		||||
    return @delete_actor_json if defined?(@delete_actor_json)
 | 
			
		||||
 | 
			
		||||
    payload = ActiveModelSerializers::SerializableResource.new(
 | 
			
		||||
      @account,
 | 
			
		||||
      serializer: ActivityPub::DeleteActorSerializer,
 | 
			
		||||
      adapter: ActivityPub::Adapter
 | 
			
		||||
    ).as_json
 | 
			
		||||
 | 
			
		||||
    Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
 | 
			
		||||
    @delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								app/views/admin/relays/_relay.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/views/admin/relays/_relay.html.haml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
%tr
 | 
			
		||||
  %td
 | 
			
		||||
    %samp= relay.inbox_url
 | 
			
		||||
  %td
 | 
			
		||||
    - if relay.enabled?
 | 
			
		||||
      %span.positive-hint
 | 
			
		||||
        = fa_icon('check')
 | 
			
		||||
        = ' '
 | 
			
		||||
        = t 'admin.relays.enabled'
 | 
			
		||||
    - else
 | 
			
		||||
      %span.negative-hint
 | 
			
		||||
        = fa_icon('times')
 | 
			
		||||
        = ' '
 | 
			
		||||
        = t 'admin.relays.disabled'
 | 
			
		||||
  %td
 | 
			
		||||
    - if relay.enabled?
 | 
			
		||||
      = table_link_to 'power-off', t('admin.relays.disable'), disable_admin_relay_path(relay), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
 | 
			
		||||
    - else
 | 
			
		||||
      = table_link_to 'power-off', t('admin.relays.enable'), enable_admin_relay_path(relay), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
 | 
			
		||||
 | 
			
		||||
    = table_link_to 'times', t('admin.relays.delete'), admin_relay_path(relay), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
 | 
			
		||||
							
								
								
									
										20
									
								
								app/views/admin/relays/index.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/views/admin/relays/index.html.haml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
- content_for :page_title do
 | 
			
		||||
  = t('admin.relays.title')
 | 
			
		||||
 | 
			
		||||
.simple_form
 | 
			
		||||
  %p.hint= t('admin.relays.description_html')
 | 
			
		||||
  = link_to @relays.empty? ? t('admin.relays.setup') : t('admin.relays.add_new'), new_admin_relay_path, class: 'block-button'
 | 
			
		||||
 | 
			
		||||
- unless @relays.empty?
 | 
			
		||||
  %hr.spacer
 | 
			
		||||
 | 
			
		||||
  .table-wrapper
 | 
			
		||||
    %table.table
 | 
			
		||||
      %thead
 | 
			
		||||
        %tr
 | 
			
		||||
          %th= t('admin.relays.inbox_url')
 | 
			
		||||
          %th= t('admin.relays.status')
 | 
			
		||||
          %th
 | 
			
		||||
      %tbody
 | 
			
		||||
        = render @relays
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								app/views/admin/relays/new.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/views/admin/relays/new.html.haml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
- content_for :page_title do
 | 
			
		||||
  = t('admin.relays.add_new')
 | 
			
		||||
 | 
			
		||||
= simple_form_for @relay, url: admin_relays_path do |f|
 | 
			
		||||
  = render 'shared/error_messages', object: @relay
 | 
			
		||||
 | 
			
		||||
  .field-group
 | 
			
		||||
    = f.input :inbox_url, as: :string, wrapper: :with_block_label
 | 
			
		||||
 | 
			
		||||
  .actions
 | 
			
		||||
    = f.button :button, t('admin.relays.save_and_enable'), type: :submit
 | 
			
		||||
 | 
			
		||||
  %p.hint.subtle-hint= t('admin.relays.enable_hint')
 | 
			
		||||
@@ -14,6 +14,8 @@ class ActivityPub::DistributionWorker
 | 
			
		||||
    ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
 | 
			
		||||
      [signed_payload, @account.id, inbox_url]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    relay! if relayable?
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
@@ -24,6 +26,10 @@ class ActivityPub::DistributionWorker
 | 
			
		||||
    @status.direct_visibility?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def relayable?
 | 
			
		||||
    @status.public_visibility?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def inboxes
 | 
			
		||||
    @inboxes ||= @account.followers.inboxes
 | 
			
		||||
  end
 | 
			
		||||
@@ -39,4 +45,10 @@ class ActivityPub::DistributionWorker
 | 
			
		||||
      adapter: ActivityPub::Adapter
 | 
			
		||||
    ).as_json
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def relay!
 | 
			
		||||
    ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
 | 
			
		||||
      [signed_payload, @account.id, inbox_url]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,11 @@ class ActivityPub::UpdateDistributionWorker
 | 
			
		||||
    @account = Account.find(account_id)
 | 
			
		||||
 | 
			
		||||
    ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
 | 
			
		||||
      [payload, @account.id, inbox_url]
 | 
			
		||||
      [signed_payload, @account.id, inbox_url]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
 | 
			
		||||
      [signed_payload, @account.id, inbox_url]
 | 
			
		||||
    end
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
@@ -21,6 +25,10 @@ class ActivityPub::UpdateDistributionWorker
 | 
			
		||||
    @inboxes ||= @account.followers.inboxes
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def signed_payload
 | 
			
		||||
    @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def payload
 | 
			
		||||
    @payload ||= ActiveModelSerializers::SerializableResource.new(
 | 
			
		||||
      @account,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user