From e55dce3176b7ac0a23a8a652c2626707a1b74dbb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Jul 2018 02:16:06 +0200 Subject: Add federation relay support (#7998) * Add federation relay support * Add admin UI for managing relays * Include actor on relay-related activities * Fix i18n --- app/controllers/admin/relays_controller.rb | 58 +++++++++++++++++ app/javascript/styles/mastodon/admin.scss | 5 ++ app/models/relay.rb | 74 ++++++++++++++++++++++ app/policies/relay_policy.rb | 7 ++ .../activitypub/delete_actor_serializer.rb | 6 +- app/serializers/activitypub/delete_serializer.rb | 6 +- .../activitypub/undo_announce_serializer.rb | 6 +- app/serializers/activitypub/update_serializer.rb | 6 +- app/services/remove_status_service.rb | 12 ++++ app/services/suspend_account_service.rb | 12 +++- app/views/admin/relays/_relay.html.haml | 21 ++++++ app/views/admin/relays/index.html.haml | 20 ++++++ app/views/admin/relays/new.html.haml | 13 ++++ app/workers/activitypub/distribution_worker.rb | 12 ++++ .../activitypub/update_distribution_worker.rb | 10 ++- 15 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 app/controllers/admin/relays_controller.rb create mode 100644 app/models/relay.rb create mode 100644 app/policies/relay_policy.rb create mode 100644 app/views/admin/relays/_relay.html.haml create mode 100644 app/views/admin/relays/index.html.haml create mode 100644 app/views/admin/relays/new.html.haml (limited to 'app') diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb new file mode 100644 index 000000000..1b02d3c36 --- /dev/null +++ b/app/controllers/admin/relays_controller.rb @@ -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 diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 560b11ddf..42f507296 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -165,6 +165,11 @@ color: $valid-value-color; font-weight: 500; } + + .negative-hint { + color: $error-value-color; + font-weight: 500; + } } .simple_form { diff --git a/app/models/relay.rb b/app/models/relay.rb new file mode 100644 index 000000000..76143bb27 --- /dev/null +++ b/app/models/relay.rb @@ -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 diff --git a/app/policies/relay_policy.rb b/app/policies/relay_policy.rb new file mode 100644 index 000000000..bd75e2197 --- /dev/null +++ b/app/policies/relay_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RelayPolicy < ApplicationPolicy + def update? + admin? + end +end diff --git a/app/serializers/activitypub/delete_actor_serializer.rb b/app/serializers/activitypub/delete_actor_serializer.rb index dfea9db4a..ddf59be97 100644 --- a/app/serializers/activitypub/delete_actor_serializer.rb +++ b/app/serializers/activitypub/delete_actor_serializer.rb @@ -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 diff --git a/app/serializers/activitypub/delete_serializer.rb b/app/serializers/activitypub/delete_serializer.rb index 2bb65135f..5012a8383 100644 --- a/app/serializers/activitypub/delete_serializer.rb +++ b/app/serializers/activitypub/delete_serializer.rb @@ -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 diff --git a/app/serializers/activitypub/undo_announce_serializer.rb b/app/serializers/activitypub/undo_announce_serializer.rb index 839847e22..4fc042727 100644 --- a/app/serializers/activitypub/undo_announce_serializer.rb +++ b/app/serializers/activitypub/undo_announce_serializer.rb @@ -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 diff --git a/app/serializers/activitypub/update_serializer.rb b/app/serializers/activitypub/update_serializer.rb index ebc667d96..48d7a1929 100644 --- a/app/serializers/activitypub/update_serializer.rb +++ b/app/serializers/activitypub/update_serializer.rb @@ -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 diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 238099169..fb889140b 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -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 diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 708d15e37..0a98f5fb9 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -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 diff --git a/app/views/admin/relays/_relay.html.haml b/app/views/admin/relays/_relay.html.haml new file mode 100644 index 000000000..d974c80a6 --- /dev/null +++ b/app/views/admin/relays/_relay.html.haml @@ -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') } diff --git a/app/views/admin/relays/index.html.haml b/app/views/admin/relays/index.html.haml new file mode 100644 index 000000000..1636a53f8 --- /dev/null +++ b/app/views/admin/relays/index.html.haml @@ -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 + diff --git a/app/views/admin/relays/new.html.haml b/app/views/admin/relays/new.html.haml new file mode 100644 index 000000000..126794acf --- /dev/null +++ b/app/views/admin/relays/new.html.haml @@ -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') diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 14bb933c0..c2bfd4f2f 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -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 diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb index f3377dcec..87efafb3e 100644 --- a/app/workers/activitypub/update_distribution_worker.rb +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -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, -- cgit