Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add federation relay support #7998

Merged
merged 4 commits into from Jul 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 58 additions & 0 deletions 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
5 changes: 5 additions & 0 deletions app/javascript/styles/mastodon/admin.scss
Expand Up @@ -165,6 +165,11 @@
color: $valid-value-color;
font-weight: 500;
}

.negative-hint {
color: $error-value-color;
font-weight: 500;
}
}

.simple_form {
Expand Down
74 changes: 74 additions & 0 deletions 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)
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the Follow activity does not contain the actor, it seems? It seems the actor is implied at the relay side based on who signed the message... I'd prefer just implicitly mentioning it in the Follow..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you meant "explicitly" in the last sentence?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some_local_account is also confusing. It is already present in ReportService, but we are going to have two activities (Follow for this case and Flag for ReportService). I think it is a good time to have account-independent Service if Mastodon is going to have more and more such activities.

'@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
7 changes: 7 additions & 0 deletions app/policies/relay_policy.rb
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class RelayPolicy < ApplicationPolicy
def update?
admin?
end
end
6 changes: 5 additions & 1 deletion 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
Expand All @@ -19,4 +19,8 @@ def actor
def virtual_object
actor
end

def to
[ActivityPub::TagManager::COLLECTIONS[:public]]
end
end
6 changes: 5 additions & 1 deletion app/serializers/activitypub/delete_serializer.rb
Expand Up @@ -17,7 +17,7 @@ def atom_uri
end
end

attributes :id, :type, :actor
attributes :id, :type, :actor, :to

has_one :object, serializer: TombstoneSerializer

Expand All @@ -32,4 +32,8 @@ def type
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end

def to
[ActivityPub::TagManager::COLLECTIONS[:public]]
end
end
6 changes: 5 additions & 1 deletion 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

Expand All @@ -16,4 +16,8 @@ def type
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end

def to
[ActivityPub::TagManager::COLLECTIONS[:public]]
end
end
6 changes: 5 additions & 1 deletion 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

Expand All @@ -16,4 +16,8 @@ def type
def actor
ActivityPub::TagManager.instance.uri_for(object)
end

def to
[ActivityPub::TagManager::COLLECTIONS[:public]]
end
end
12 changes: 12 additions & 0 deletions app/services/remove_status_service.rb
Expand Up @@ -90,6 +90,18 @@ def remove_from_remote_followers
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
Expand Down
12 changes: 10 additions & 2 deletions app/services/suspend_account_service.rb
Expand Up @@ -22,7 +22,13 @@ def purge_user!
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)
Expand Down Expand Up @@ -59,12 +65,14 @@ def destroy_all(association)
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
21 changes: 21 additions & 0 deletions 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') }
20 changes: 20 additions & 0 deletions 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

13 changes: 13 additions & 0 deletions 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')
12 changes: 12 additions & 0 deletions app/workers/activitypub/distribution_worker.rb
Expand Up @@ -14,6 +14,8 @@ def perform(status_id)
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[signed_payload, @account.id, inbox_url]
end

relay! if relayable?
rescue ActiveRecord::RecordNotFound
true
end
Expand All @@ -24,6 +26,10 @@ def skip_distribution?
@status.direct_visibility?
end

def relayable?
@status.public_visibility?
end

def inboxes
@inboxes ||= @account.followers.inboxes
end
Expand All @@ -39,4 +45,10 @@ def payload
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
10 changes: 9 additions & 1 deletion app/workers/activitypub/update_distribution_worker.rb
Expand Up @@ -9,7 +9,11 @@ def perform(account_id)
@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
Expand All @@ -21,6 +25,10 @@ def inboxes
@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,
Expand Down