diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2018-07-13 02:16:06 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-13 02:16:06 +0200 |
commit | e55dce3176b7ac0a23a8a652c2626707a1b74dbb (patch) | |
tree | 12d129849083dc0d17e372cb13d549c39dc40d51 | |
parent | 401559c376078ef98e11c3034977b835b4086b5b (diff) |
Add federation relay support (#7998)
* Add federation relay support * Add admin UI for managing relays * Include actor on relay-related activities * Fix i18n
23 files changed, 309 insertions, 8 deletions
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 <strong>federation relay</strong> is an intermediary server that exchanges large volumes of public toots between servers that subscribe and publish to it. <strong>It can help small and medium servers discover content from the fediverse</strong>, 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: <span class="name-counter">%{count}</span> 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 |