diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb
new file mode 100644
index 00000000000..1b02d3c361a
--- /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 560b11ddf4e..42f50729678 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 00000000000..76143bb27a2
--- /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 00000000000..bd75e21977a
--- /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 dfea9db4abf..ddf59be9709 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 2bb65135f7c..5012a8383ff 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 839847e22c0..4fc042727ad 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 ebc667d9630..48d7a192946 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 23809916913..fb889140b24 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 708d15e37d9..0a98f5fb9d1 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 00000000000..d974c80a61b
--- /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 00000000000..1636a53f852
--- /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 00000000000..126794acfe5
--- /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 14bb933c006..c2bfd4f2f13 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 f3377dcec5a..87efafb3e8b 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 a03b12a3972..ec08f0d78a9 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 7d9a5d617e5..9ff548f40e7 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 3f2e913c629..a13ad6f4370 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 fd26b4aa74c..3d0da1a857c 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 00000000000..8762f473a29
--- /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 02032c54826..e0da669c46c 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 00000000000..2c9df4ad3a1
--- /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 00000000000..12dc0f20f6e
--- /dev/null
+++ b/spec/models/relay_spec.rb
@@ -0,0 +1,4 @@
+require 'rails_helper'
+
+RSpec.describe Relay, type: :model do
+end