Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

Commit

Permalink
Add ActivityPub actor representing the entire server (mastodon#11321)
Browse files Browse the repository at this point in the history
* Add support for an instance actor

* Skip username validation for local Application accounts

* Add migration script to create instance actor

* Make Codeclimate happy

* Switch to id -99 for instance actor

* Remove unused `icon` and `image` attributes from instance actor

* Use if/elsif/else instead of return + ternary operator

* Add instance actor to fresh installs

* Use instance actor as instance representative

Use instance actor for forwarding reports, relay operations, and spam
auto-reporting.

* Seed database in test environment

* Fix single-user mode

* Fix tests

* Fix specs to accomodate for an extra `Account`

* Auto-reject follows on instance actor

Following an instance actor might make sense, but we are not handling that
right now, so auto-reject.

* Fix webfinger lookup and serialization for instance actor

* Rename instance actor

* Make it clear in the HTML view that the instance actor should not be blocked

* Raise cache time for instance actor as there's no dynamic content

* Re-use /about/more with a flash message for instance actor profile
  • Loading branch information
ClearlyClaire authored and Gargron committed Jul 18, 2019
1 parent 15c7478 commit 730c405
Show file tree
Hide file tree
Showing 23 changed files with 141 additions and 52 deletions.
4 changes: 3 additions & 1 deletion app/controllers/about_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class AboutController < ApplicationController

def show; end

def more; end
def more
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
end

def terms; end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def not_acceptable
end

def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
end

def use_seamless_external_login?
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def default_redirect_path
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.local.without_suspended.first)
short_account_path(Account.local.without_suspended.where('id > 0').first)
else
about_path
end
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/instance_actors_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class InstanceActorsController < ApplicationController
include AccountControllerConcern

def show
expires_in 10.minutes, public: true
render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
end

private

def set_account
@account = Account.find(-99)
end

def restrict_fields_to
%i(id type preferred_username inbox public_key endpoints url manually_approves_followers)
end
end
4 changes: 4 additions & 0 deletions app/javascript/styles/mastodon/containers.scss
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@
min-height: 100%;
}

.flash-message {
margin-bottom: 10px;
}

@media screen and (max-width: 738px) {
grid-template-columns: minmax(0, 50%) minmax(0, 50%);

Expand Down
2 changes: 1 addition & 1 deletion app/lib/activitypub/activity/follow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def perform

return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)

if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved?
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
reject_follow_request!(target_account)
return
end
Expand Down
5 changes: 3 additions & 2 deletions app/lib/activitypub/tag_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def url_for(target)

case target.object_type
when :person
short_account_url(target)
target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
short_account_status_url(target.account, target)
Expand All @@ -29,7 +29,7 @@ def uri_for(target)

case target.object_type
when :person
account_url(target)
target.instance_actor? ? instance_actor_url : account_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
account_status_url(target.account, target)
Expand Down Expand Up @@ -119,6 +119,7 @@ def local_uri?(uri)

def uri_to_local_id(uri, param = :id)
path_params = Rails.application.routes.recognize_path(uri)
path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors'
path_params[param]
end

Expand Down
6 changes: 6 additions & 0 deletions app/lib/webfinger_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ def username
def username_from_url
if account_show_page?
path_params[:username]
elsif instance_actor_page?
Rails.configuration.x.local_domain
else
raise ActiveRecord::RecordNotFound
end
end

def instance_actor_page?
path_params[:controller] == 'instance_actors'
end

def account_show_page?
path_params[:controller] == 'accounts' && path_params[:action] == 'show'
end
Expand Down
8 changes: 6 additions & 2 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class Account < ApplicationRecord
validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? }

# Local user validations
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? }
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
Expand Down Expand Up @@ -139,6 +139,10 @@ def bot?
%w(Application Service).include? actor_type
end

def instance_actor?
id == -99
end

alias bot bot?

def bot=(val)
Expand Down Expand Up @@ -498,7 +502,7 @@ def prepare_username
end

def generate_keys
return unless local? && !Rails.env.test?
return unless local? && private_key.blank? && public_key.blank?

keypair = OpenSSL::PKey::RSA.new(2048)
self.private_key = keypair.to_pem
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/account_finder_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def find_remote!(username, domain)
end

def representative
find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.without_suspended.first
Account.find(-99)
end

def find_local(username)
Expand Down
14 changes: 10 additions & 4 deletions app/serializers/activitypub/actor_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ def shared_inbox
delegate :moved?, to: :object

def id
account_url(object)
object.instance_actor? ? instance_actor_url : account_url(object)
end

def type
object.bot? ? 'Service' : 'Person'
if object.instance_actor?
'Application'
elsif object.bot?
'Service'
else
'Person'
end
end

def following
Expand All @@ -55,7 +61,7 @@ def followers
end

def inbox
account_inbox_url(object)
object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object)
end

def outbox
Expand Down Expand Up @@ -95,7 +101,7 @@ def public_key
end

def url
short_account_url(object)
object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object)
end

def avatar_exists?
Expand Down
25 changes: 18 additions & 7 deletions app/serializers/webfinger_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,26 @@ def subject
end

def aliases
[short_account_url(object), account_url(object)]
if object.instance_actor?
[instance_actor_url]
else
[short_account_url(object), account_url(object)]
end
end

def links
[
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
{ rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
{ rel: 'self', type: 'application/activity+json', href: account_url(object) },
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
]
if object.instance_actor?
[
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: about_more_url(instance_actor: true) },
{ rel: 'self', type: 'application/activity+json', href: instance_actor_url },
]
else
[
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
{ rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
{ rel: 'self', type: 'application/activity+json', href: account_url(object) },
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
]
end
end
end
2 changes: 2 additions & 0 deletions app/views/about/more.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
= mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email

.column-3
= render 'application/flashes'

.box-widget
.rich-formatting= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html')
57 changes: 37 additions & 20 deletions app/views/well_known/webfinger/show.xml.ruby
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,47 @@ doc << Ox::Element.new('XRD').tap do |xrd|
xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'

xrd << (Ox::Element.new('Subject') << @account.to_webfinger_s)
xrd << (Ox::Element.new('Alias') << short_account_url(@account))
xrd << (Ox::Element.new('Alias') << account_url(@account))

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://webfinger.net/rel/profile-page'
link['type'] = 'text/html'
link['href'] = short_account_url(@account)
end
if @account.instance_actor?
xrd << (Ox::Element.new('Alias') << instance_actor_url)

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://schemas.google.com/g/2010#updates-from'
link['type'] = 'application/atom+xml'
link['href'] = account_url(@account, format: 'atom')
end
xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://webfinger.net/rel/profile-page'
link['type'] = 'text/html'
link['href'] = about_more_url(instance_actor: true)
end

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'self'
link['type'] = 'application/activity+json'
link['href'] = account_url(@account)
end
xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'self'
link['type'] = 'application/activity+json'
link['href'] = instance_actor_url
end
else
xrd << (Ox::Element.new('Alias') << short_account_url(@account))
xrd << (Ox::Element.new('Alias') << account_url(@account))

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://webfinger.net/rel/profile-page'
link['type'] = 'text/html'
link['href'] = short_account_url(@account)
end

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://schemas.google.com/g/2010#updates-from'
link['type'] = 'application/atom+xml'
link['href'] = account_url(@account, format: 'atom')
end

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'self'
link['type'] = 'application/activity+json'
link['href'] = account_url(@account)
end

xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://ostatus.org/schema/1.0/subscribe'
link['template'] = "#{authorize_interaction_url}?acct={uri}"
xrd << Ox::Element.new('Link').tap do |link|
link['rel'] = 'http://ostatus.org/schema/1.0/subscribe'
link['template'] = "#{authorize_interaction_url}?acct={uri}"
end
end
end

Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ en:
generic_description: "%{domain} is one server in the network"
get_apps: Try a mobile app
hosted_on: Mastodon hosted on %{domain}
instance_actor_flash: |
This account is a virtual actor used to represent the server itself and not any individual user.
It is used for federation purposes and should not be blocked unless you want to block the whole instance, in which case you should use a domain block.
learn_more: Learn more
privacy_policy: Privacy policy
see_whats_happening: See what's happening
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
get 'intent', to: 'intents#show'
get 'custom.css', to: 'custom_css#show', as: :custom_css

resource :instance_actor, path: 'actor', only: [:show] do
resource :inbox, only: [:create], module: :activitypub
end

devise_scope :user do
get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20190715164535_add_instance_actor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddInstanceActor < ActiveRecord::Migration[5.2]
def up
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
end

def down
Account.find_by(id: -99, actor_type: 'Application').destroy!
end
end
2 changes: 1 addition & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_07_06_233204) do
ActiveRecord::Schema.define(version: 2019_07_15_164535) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down
4 changes: 3 additions & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow')

domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
Account.create!(id: -99, actor_type: 'Application', locked: true, username: domain)

if Rails.env.development?
domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
admin = Account.where(username: 'admin').first_or_initialize(username: 'admin')
admin.save(validate: false)
User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, admin: true, account: admin, agreement: true, approved: true).save!
Expand Down
Loading

0 comments on commit 730c405

Please sign in to comment.