From e55dce3176b7ac0a23a8a652c2626707a1b74dbb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Jul 2018 02:16:06 +0200 Subject: [PATCH] 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 +- .../activitypub/delete_serializer.rb | 6 +- .../activitypub/undo_announce_serializer.rb | 6 +- .../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 ++++ .../activitypub/distribution_worker.rb | 12 +++ .../activitypub/update_distribution_worker.rb | 10 ++- config/locales/en.yml | 8 ++ config/locales/simple_form.en.yml | 2 + config/navigation.rb | 1 + config/routes.rb | 7 ++ db/migrate/20180711152640_create_relays.rb | 12 +++ db/schema.rb | 11 ++- spec/fabricators/relay_fabricator.rb | 4 + spec/models/relay_spec.rb | 4 + 23 files changed, 309 insertions(+), 8 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 create mode 100644 db/migrate/20180711152640_create_relays.rb create mode 100644 spec/fabricators/relay_fabricator.rb create mode 100644 spec/models/relay_spec.rb 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, diff --git a/config/locales/en.yml b/config/locales/en.yml index a03b12a39..ec08f0d78 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -261,6 +261,14 @@ en: expired: Expired title: Filter title: Invites + relays: + add_new: Add new relay + description_html: A federation relay is an intermediary server that exchanges large volumes of public toots between servers that subscribe and publish to it. It can help small and medium servers discover content from the fediverse, which would otherwise require local users manually following other people on remote servers. + enable_hint: Once enabled, your server will subscribe to all public toots from this relay, and will begin sending this server's public toots to it. + inbox_url: Relay URL + setup: Setup a relay connection + status: Status + title: Relays report_notes: created_msg: Report note successfully created! destroyed_msg: Report note successfully deleted! diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7d9a5d617..9ff548f40 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -13,6 +13,7 @@ en: other: %{count} characters left fields: You can have up to 4 items displayed as a table on your profile header: PNG, GIF or JPG. At most 2MB. Will be downscaled to 700x335px + inbox_url: Copy the URL from the frontpage of the relay you want to use irreversible: Filtered toots will disappear irreversibly, even if filter is later removed locale: The language of the user interface, e-mails and push notifications locked: Requires you to manually approve followers @@ -52,6 +53,7 @@ en: expires_in: Expire after fields: Profile metadata header: Header + inbox_url: URL of the relay inbox irreversible: Drop instead of hide locale: Interface language locked: Lock account diff --git a/config/navigation.rb b/config/navigation.rb index 3f2e913c6..a13ad6f43 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -36,6 +36,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), proc { current_user.admin? ? edit_admin_settings_url : admin_custom_emojis_url }, if: proc { current_user.staff? } do |admin| admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? } admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} + admin.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays} admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? } diff --git a/config/routes.rb b/config/routes.rb index fd26b4aa7..3d0da1a85 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -131,6 +131,13 @@ Rails.application.routes.draw do resource :settings, only: [:edit, :update] resources :invites, only: [:index, :create, :destroy] + resources :relays, only: [:index, :new, :create, :destroy] do + member do + post :enable + post :disable + end + end + resources :instances, only: [:index] do collection do post :resubscribe diff --git a/db/migrate/20180711152640_create_relays.rb b/db/migrate/20180711152640_create_relays.rb new file mode 100644 index 000000000..8762f473a --- /dev/null +++ b/db/migrate/20180711152640_create_relays.rb @@ -0,0 +1,12 @@ +class CreateRelays < ActiveRecord::Migration[5.2] + def change + create_table :relays do |t| + t.string :inbox_url, default: '', null: false + t.boolean :enabled, default: false, null: false, index: true + + t.string :follow_activity_id + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 02032c548..e0da669c4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_07_07_154237) do +ActiveRecord::Schema.define(version: 2018_07_11_152640) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -371,6 +371,15 @@ ActiveRecord::Schema.define(version: 2018_07_07_154237) do t.index ["status_id", "preview_card_id"], name: "index_preview_cards_statuses_on_status_id_and_preview_card_id" end + create_table "relays", force: :cascade do |t| + t.string "inbox_url", default: "", null: false + t.boolean "enabled", default: false, null: false + t.string "follow_activity_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["enabled"], name: "index_relays_on_enabled" + end + create_table "report_notes", force: :cascade do |t| t.text "content", null: false t.bigint "report_id", null: false diff --git a/spec/fabricators/relay_fabricator.rb b/spec/fabricators/relay_fabricator.rb new file mode 100644 index 000000000..2c9df4ad3 --- /dev/null +++ b/spec/fabricators/relay_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:relay) do + inbox_url "https://example.com/inbox" + enabled true +end diff --git a/spec/models/relay_spec.rb b/spec/models/relay_spec.rb new file mode 100644 index 000000000..12dc0f20f --- /dev/null +++ b/spec/models/relay_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe Relay, type: :model do +end