diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb
new file mode 100644
index 0000000000..1b02d3c361
--- /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 560b11ddf4..42f5072967 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 0000000000..76143bb27a
--- /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 0000000000..bd75e21977
--- /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 dfea9db4ab..ddf59be970 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 2bb65135f7..5012a8383f 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 839847e22c..4fc042727a 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 ebc667d963..48d7a19294 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 2380991691..fb889140b2 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 708d15e37d..0a98f5fb9d 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 0000000000..d974c80a61
--- /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 0000000000..1636a53f85
--- /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 0000000000..126794acfe
--- /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 14bb933c00..c2bfd4f2f1 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 f3377dcec5..87efafb3e8 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 a03b12a397..ec08f0d78a 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 7d9a5d617e..9ff548f40e 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 3f2e913c62..a13ad6f437 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 fd26b4aa74..3d0da1a857 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 0000000000..8762f473a2
--- /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 02032c5482..e0da669c46 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 0000000000..2c9df4ad3a
--- /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 0000000000..12dc0f20f6
--- /dev/null
+++ b/spec/models/relay_spec.rb
@@ -0,0 +1,4 @@
+require 'rails_helper'
+
+RSpec.describe Relay, type: :model do
+end