From 0396acf39ea902688374fac65fa7ef5dc4c05512 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Thu, 25 Aug 2022 20:39:40 +0200
Subject: Add audit log entries for user roles (#19040)
* Refactor audit log schema
* Add audit log entries for user roles
---
app/controllers/concerns/accountable_concern.rb | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
(limited to 'app/controllers/concerns')
diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb
index 87d62478d..c1349915f 100644
--- a/app/controllers/concerns/accountable_concern.rb
+++ b/app/controllers/concerns/accountable_concern.rb
@@ -3,7 +3,11 @@
module AccountableConcern
extend ActiveSupport::Concern
- def log_action(action, target, options = {})
- Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
+ def log_action(action, target)
+ Admin::ActionLog.create(
+ account: current_account,
+ action: action,
+ target: target
+ )
end
end
--
cgit
From 1145dbd327ae9b56357cc488801d723051f58e0b Mon Sep 17 00:00:00 2001
From: Claire
Date: Tue, 20 Sep 2022 23:30:26 +0200
Subject: Improve error reporting and logging when processing remote accounts
(#15605)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add a more descriptive PrivateNetworkAddressError exception class
* Remove unnecessary exception class to rescue clause
* Remove unnecessary include to JsonLdHelper
* Give more neutral error message when too many webfinger redirects
* Remove unnecessary guard condition
* Rework how “ActivityPub::FetchRemoteAccountService” handles errors
Add “suppress_errors” keyword argument to avoid raising errors in
ActivityPub::FetchRemoteAccountService#call (default/previous behavior).
* Rework how “ActivityPub::FetchRemoteKeyService” handles errors
Add “suppress_errors” keyword argument to avoid raising errors in
ActivityPub::FetchRemoteKeyService#call (default/previous behavior).
* Fix Webfinger::RedirectError not being a subclass of Webfinger::Error
* Add suppress_errors option to ResolveAccountService
Defaults to true (to preserve previous behavior). If set to false,
errors will be raised instead of caught, allowing the caller to be
informed of what went wrong.
* Return more precise error when failing to fetch account signing AP payloads
* Add tests
* Fixes
* Refactor error handling a bit
* Fix various issues
* Add specific error when provided Digest is not 256 bits of base64-encoded data
* Please CodeClimate
* Improve webfinger error reporting
---
app/controllers/concerns/signature_verification.rb | 46 ++++++++++++++-----
app/lib/request.rb | 6 +--
app/lib/webfinger.rb | 2 +-
.../activitypub/fetch_remote_account_service.rb | 38 +++++++++++-----
.../activitypub/fetch_remote_key_service.rb | 34 +++++++++-----
.../activitypub/process_account_service.rb | 2 -
app/services/resolve_account_service.rb | 12 ++---
lib/exceptions.rb | 9 ++++
.../fetch_remote_account_service_spec.rb | 52 ++++++++++++++++++++++
spec/services/resolve_account_service_spec.rb | 4 +-
10 files changed, 158 insertions(+), 47 deletions(-)
(limited to 'app/controllers/concerns')
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 4dd0cac55..89dc828f4 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -93,11 +93,15 @@ module SignatureVerification
return account unless verify_signature(account, signature, compare_signed_string).nil?
- @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
- @signed_request_account = nil
+ fail_with! "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
rescue SignatureVerificationError => e
- @signature_verification_failure_reason = e.message
- @signed_request_account = nil
+ fail_with! e.message
+ rescue HTTP::Error, OpenSSL::SSL::SSLError => e
+ fail_with! "Failed to fetch remote data: #{e.message}"
+ rescue Mastodon::UnexptectedResponseError
+ fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
+ rescue Stoplight::Error::RedLight
+ fail_with! 'Fetching attempt skipped because of recent connection failure'
end
def request_body
@@ -106,6 +110,11 @@ module SignatureVerification
private
+ def fail_with!(message)
+ @signature_verification_failure_reason = message
+ @signed_request_account = nil
+ end
+
def signature_params
@signature_params ||= begin
raw_signature = request.headers['Signature']
@@ -138,7 +147,17 @@ module SignatureVerification
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
sha256 = digests.assoc('sha-256')
raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
- raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" if body_digest != sha256[1]
+
+ return if body_digest == sha256[1]
+
+ digest_size = begin
+ Base64.strict_decode64(sha256[1].strip).length
+ rescue ArgumentError
+ raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
+ end
+
+ raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
+ raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
def verify_signature(account, signature, compare_signed_string)
@@ -216,19 +235,20 @@ module SignatureVerification
end
if key_id.start_with?('acct:')
- stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
+ stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
- account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
+ account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account
end
- rescue Mastodon::HostValidationError
- nil
+ rescue Mastodon::PrivateNetworkAddressError => e
+ raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
+ rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
+ raise SignatureVerificationError, e.message
end
def stoplight_wrap_request(&block)
Stoplight("source:#{request.remote_ip}", &block)
- .with_fallback { nil }
.with_threshold(1)
.with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
@@ -237,6 +257,10 @@ module SignatureVerification
def account_refresh_key(account)
return if account.local? || !account.activitypub?
- ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
+ ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true, suppress_errors: false)
+ rescue Mastodon::PrivateNetworkAddressError => e
+ raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
+ rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, Webfinger::Error => e
+ raise SignatureVerificationError, e.message
end
end
diff --git a/app/lib/request.rb b/app/lib/request.rb
index f5123d776..eac04c798 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -208,7 +208,7 @@ class Request
addresses.each do |address|
begin
- check_private_address(address)
+ check_private_address(address, host)
sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s)
@@ -264,10 +264,10 @@ class Request
alias new open
- def check_private_address(address)
+ def check_private_address(address, host)
addr = IPAddr.new(address.to_s)
return if private_address_exceptions.any? { |range| range.include?(addr) }
- raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(addr)
+ raise Mastodon::PrivateNetworkAddressError, host if PrivateAddressCheck.private_address?(addr)
end
def private_address_exceptions
diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb
index a681e0815..7c0c10c33 100644
--- a/app/lib/webfinger.rb
+++ b/app/lib/webfinger.rb
@@ -3,7 +3,7 @@
class Webfinger
class Error < StandardError; end
class GoneError < Error; end
- class RedirectError < StandardError; end
+ class RedirectError < Error; end
class Response
attr_reader :uri
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index 9d01f5386..d7d739c59 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -5,10 +5,12 @@ class ActivityPub::FetchRemoteAccountService < BaseService
include DomainControlHelper
include WebfingerHelper
+ class Error < StandardError; end
+
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
# Does a WebFinger roundtrip on each call, unless `only_key` is true
- def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false)
+ def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
return if domain_not_allowed?(uri)
return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
@@ -18,38 +20,50 @@ class ActivityPub::FetchRemoteAccountService < BaseService
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
+ rescue Oj::ParseError
+ raise Error, "Error parsing JSON-LD document #{uri}"
end
- return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?)
+ raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
+ raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
+ raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
+ raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
@uri = @json['id']
@username = @json['preferredUsername']
@domain = Addressable::URI.parse(@uri).normalized_host
- return unless only_key || verified_webfinger?
+ check_webfinger! unless only_key
ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
- rescue Oj::ParseError
- nil
+ rescue Error => e
+ Rails.logger.debug "Fetching account #{uri} failed: #{e.message}"
+ raise unless suppress_errors
end
private
- def verified_webfinger?
+ def check_webfinger!
webfinger = webfinger!("acct:#{@username}@#{@domain}")
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
- return webfinger.link('self', 'href') == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
+ if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
+ raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
+ return
+ end
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
@username, @domain = split_acct(webfinger.subject)
- return false unless @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
- return false if webfinger.link('self', 'href') != @uri
+ unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
+ raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
+ end
- true
- rescue Webfinger::Error
- false
+ raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
+ rescue Webfinger::RedirectError => e
+ raise Error, e.message
+ rescue Webfinger::Error => e
+ raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
end
def split_acct(acct)
diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb
index c48288b3b..01008d883 100644
--- a/app/services/activitypub/fetch_remote_key_service.rb
+++ b/app/services/activitypub/fetch_remote_key_service.rb
@@ -3,9 +3,11 @@
class ActivityPub::FetchRemoteKeyService < BaseService
include JsonLdHelper
+ class Error < StandardError; end
+
# Returns account that owns the key
- def call(uri, id: true, prefetched_body: nil)
- return if uri.blank?
+ def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
+ raise Error, 'No key URI given' if uri.blank?
if prefetched_body.nil?
if id
@@ -13,7 +15,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
if person?
@json = fetch_resource(@json['id'], true)
elsif uri != @json['id']
- return
+ raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
end
else
@json = fetch_resource(uri, id)
@@ -22,21 +24,29 @@ class ActivityPub::FetchRemoteKeyService < BaseService
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
- return unless supported_context?(@json) && expected_type?
- return find_account(@json['id'], @json) if person?
+ raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
+ raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
+ raise Error, "Unexpected object type for key #{uri}" unless expected_type?
+ return find_account(@json['id'], @json, suppress_errors) if person?
@owner = fetch_resource(owner_uri, true)
- return unless supported_context?(@owner) && confirmed_owner?
+ raise Error, "Unable to fetch actor JSON #{owner_uri}" if @owner.nil?
+ raise Error, "Unsupported JSON-LD context for document #{owner_uri}" unless supported_context?(@owner)
+ raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
+ raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?
- find_account(owner_uri, @owner)
+ find_account(owner_uri, @owner, suppress_errors)
+ rescue Error => e
+ Rails.logger.debug "Fetching key #{uri} failed: #{e.message}"
+ raise unless suppress_errors
end
private
- def find_account(uri, prefetched_body)
+ def find_account(uri, prefetched_body, suppress_errors)
account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
- account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body)
+ account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
account
end
@@ -56,7 +66,11 @@ class ActivityPub::FetchRemoteKeyService < BaseService
@owner_uri ||= value_or_id(@json['owner'])
end
+ def expected_owner_type?
+ equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
+ end
+
def confirmed_owner?
- equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && value_or_id(@owner['publicKey']) == @json['id']
+ value_or_id(@owner['publicKey']) == @json['id']
end
end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 34750dba6..456b3524b 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -32,8 +32,6 @@ class ActivityPub::ProcessAccountService < BaseService
process_duplicate_accounts! if @options[:verified_webfinger]
end
- return if @account.nil?
-
after_protocol_change! if protocol_changed?
after_key_change! if key_changed? && !@options[:signed_with_known_key]
clear_tombstones! if key_changed?
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index b55e45409..e3b370968 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class ResolveAccountService < BaseService
- include JsonLdHelper
include DomainControlHelper
include WebfingerHelper
include Redisable
@@ -13,6 +12,7 @@ class ResolveAccountService < BaseService
# @param [Hash] options
# @option options [Boolean] :redirected Do not follow further Webfinger redirects
# @option options [Boolean] :skip_webfinger Do not attempt any webfinger query or refreshing account data
+ # @option options [Boolean] :suppress_errors When failing, return nil instead of raising an error
# @return [Account]
def call(uri, options = {})
return if uri.blank?
@@ -52,15 +52,15 @@ class ResolveAccountService < BaseService
# either needs to be created, or updated from fresh data
fetch_account!
- rescue Webfinger::Error, Oj::ParseError => e
+ rescue Webfinger::Error => e
Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
- nil
+ raise unless @options[:suppress_errors]
end
private
def process_options!(uri, options)
- @options = options
+ @options = { suppress_errors: true }.merge(options)
if uri.is_a?(Account)
@account = uri
@@ -96,7 +96,7 @@ class ResolveAccountService < BaseService
@username, @domain = split_acct(@webfinger.subject)
unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
- raise Webfinger::RedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
+ raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
end
rescue Webfinger::GoneError
@gone = true
@@ -110,7 +110,7 @@ class ResolveAccountService < BaseService
return unless activitypub_ready?
with_lock("resolve:#{@username}@#{@domain}") do
- @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url)
+ @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
end
@account
diff --git a/lib/exceptions.rb b/lib/exceptions.rb
index 0c677b660..3c5ba226b 100644
--- a/lib/exceptions.rb
+++ b/lib/exceptions.rb
@@ -25,4 +25,13 @@ module Mastodon
end
end
end
+
+ class PrivateNetworkAddressError < HostValidationError
+ attr_reader :host
+
+ def initialize(host)
+ @host = host
+ super()
+ end
+ end
end
diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb
index aa13f0a9b..ec6f1f41d 100644
--- a/spec/services/activitypub/fetch_remote_account_service_spec.rb
+++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb
@@ -119,6 +119,58 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
include_examples 'sets profile data'
end
+ context 'when WebFinger returns a different URI' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'does not create account' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'when WebFinger returns a different URI after a redirection' do
+ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'looks up "redirected" webfinger' do
+ account
+ expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
+ end
+
+ it 'does not create account' do
+ expect(account).to be_nil
+ end
+ end
+
context 'with wrong id' do
it 'does not create account' do
expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb
index 8c302e1d8..654606bea 100644
--- a/spec/services/resolve_account_service_spec.rb
+++ b/spec/services/resolve_account_service_spec.rb
@@ -137,8 +137,8 @@ RSpec.describe ResolveAccountService, type: :service do
stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' })
end
- it 'returns new remote account' do
- expect { subject.call('Foo@redirected.example.com') }.to raise_error Webfinger::RedirectError
+ it 'does not return a new remote account' do
+ expect(subject.call('Foo@redirected.example.com')).to be_nil
end
end
--
cgit
From 84aff598ea0b5670ef2a0d1009bca9c9136c2d50 Mon Sep 17 00:00:00 2001
From: Claire
Date: Wed, 21 Sep 2022 14:48:35 +0200
Subject: Fix typo in SignatureVerification (#19209)
Fix regression from #15605
---
app/controllers/concerns/signature_verification.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'app/controllers/concerns')
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 89dc828f4..4da068aed 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -98,7 +98,7 @@ module SignatureVerification
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
fail_with! "Failed to fetch remote data: #{e.message}"
- rescue Mastodon::UnexptectedResponseError
+ rescue Mastodon::UnexpectedResponseError
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
rescue Stoplight::Error::RedLight
fail_with! 'Fetching attempt skipped because of recent connection failure'
--
cgit
From 8cf7006d4efbcfdd4a4ab688db1bcc73a2915a47 Mon Sep 17 00:00:00 2001
From: Claire
Date: Wed, 21 Sep 2022 22:45:57 +0200
Subject: Refactor ActivityPub handling to prepare for non-Account actors
(#19212)
* Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService
ActivityPub::FetchRemoteAccountService is kept as a wrapper for when the actor is
specifically required to be an Account
* Refactor SignatureVerification to allow non-Account actors
* fixup! Move ActivityPub::FetchRemoteAccountService to ActivityPub::FetchRemoteActorService
* Refactor ActivityPub::FetchRemoteKeyService to potentially return non-Account actors
* Refactor inbound ActivityPub payload processing to accept non-Account actors
* Refactor inbound ActivityPub processing to accept activities relayed through non-Account
* Refactor how Account key URIs are built
* Refactor Request and drop unused key_id_format parameter
* Rename ActivityPub::Dereferencer `signature_account` to `signature_actor`
---
app/controllers/accounts_controller.rb | 2 +-
app/controllers/activitypub/claims_controller.rb | 2 +-
.../activitypub/collections_controller.rb | 2 +-
.../followers_synchronizations_controller.rb | 2 +-
app/controllers/activitypub/inboxes_controller.rb | 10 +-
app/controllers/activitypub/outboxes_controller.rb | 2 +-
app/controllers/activitypub/replies_controller.rb | 2 +-
app/controllers/concerns/signature_verification.rb | 52 +++---
app/controllers/follower_accounts_controller.rb | 2 +-
app/controllers/following_accounts_controller.rb | 2 +-
app/controllers/statuses_controller.rb | 2 +-
app/controllers/tags_controller.rb | 2 +-
app/lib/activitypub/activity.rb | 10 +-
app/lib/activitypub/dereferencer.rb | 6 +-
app/lib/activitypub/linked_data_signature.rb | 6 +-
app/lib/activitypub/tag_manager.rb | 8 +
app/lib/request.rb | 18 +--
.../activitypub/public_key_serializer.rb | 2 +-
.../activitypub/fetch_remote_account_service.rb | 78 +--------
.../activitypub/fetch_remote_actor_service.rb | 80 +++++++++
.../activitypub/fetch_remote_key_service.rb | 22 +--
.../activitypub/process_collection_service.rb | 11 +-
app/services/fetch_resource_service.rb | 2 +-
app/services/keys/claim_service.rb | 2 +-
app/services/resolve_url_service.rb | 4 +-
app/workers/activitypub/delivery_worker.rb | 2 +-
app/workers/activitypub/processing_worker.rb | 12 +-
spec/controllers/accounts_controller_spec.rb | 2 +-
.../activitypub/collections_controller_spec.rb | 2 +-
.../followers_synchronizations_controller_spec.rb | 2 +-
.../activitypub/inboxes_controller_spec.rb | 2 +-
.../activitypub/outboxes_controller_spec.rb | 2 +-
.../activitypub/replies_controller_spec.rb | 2 +-
.../concerns/signature_verification_spec.rb | 45 ++++++
spec/controllers/statuses_controller_spec.rb | 2 +-
spec/lib/activitypub/activity/announce_spec.rb | 2 +-
spec/lib/activitypub/dereferencer_spec.rb | 8 +-
spec/lib/activitypub/linked_data_signature_spec.rb | 14 +-
.../activitypub/fetch_remote_actor_service_spec.rb | 180 +++++++++++++++++++++
.../activitypub/process_collection_service_spec.rb | 6 +-
spec/services/fetch_resource_service_spec.rb | 2 +-
41 files changed, 436 insertions(+), 180 deletions(-)
create mode 100644 app/services/activitypub/fetch_remote_actor_service.rb
create mode 100644 spec/services/activitypub/fetch_remote_actor_service_spec.rb
(limited to 'app/controllers/concerns')
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index fe7d934dc..d92f91b30 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -7,7 +7,7 @@ class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureAuthentication
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
before_action :set_body_classes
diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb
index 08ad952df..339333e46 100644
--- a/app/controllers/activitypub/claims_controller.rb
+++ b/app/controllers/activitypub/claims_controller.rb
@@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
- before_action :require_signature!
+ before_action :require_account_signature!
before_action :set_claim_result
def create
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index e4e994a98..d94a285ea 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_items
before_action :set_size
before_action :set_type
diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb
index 940b77cf0..4e445bcb1 100644
--- a/app/controllers/activitypub/followers_synchronizations_controller.rb
+++ b/app/controllers/activitypub/followers_synchronizations_controller.rb
@@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!
+ before_action :require_account_signature!
before_action :set_items
before_action :set_cache_headers
diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index 92dcb5ac7..5ee85474e 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
- before_action :require_signature!
+ before_action :require_actor_signature!
skip_before_action :authenticate_user!
def create
@@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def upgrade_account
- if signed_request_account.ostatus?
+ if signed_request_account&.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
- DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
+ DeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
end
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
- return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
+ return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil?
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
@@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def process_payload
- ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
+ ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name)
end
end
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index cd3992502..60d201f76 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
- before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_statuses
before_action :set_cache_headers
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index 4ff7cfa08..8e0f9de2e 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
DESCENDANTS_LIMIT = 60
- before_action :require_signature!, if: :authorized_fetch_mode?
+ before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 4da068aed..2394574b3 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -45,10 +45,14 @@ module SignatureVerification
end
end
- def require_signature!
+ def require_account_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
+ def require_actor_signature!
+ render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
+ end
+
def signed_request?
request.headers['Signature'].present?
end
@@ -68,7 +72,11 @@ module SignatureVerification
end
def signed_request_account
- return @signed_request_account if defined?(@signed_request_account)
+ signed_request_actor.is_a?(Account) ? signed_request_actor : nil
+ end
+
+ def signed_request_actor
+ return @signed_request_actor if defined?(@signed_request_actor)
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
@@ -78,22 +86,22 @@ module SignatureVerification
verify_signature_strength!
verify_body_digest!
- account = account_from_key_id(signature_params['keyId'])
+ actor = actor_from_key_id(signature_params['keyId'])
- raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
+ raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string
- return account unless verify_signature(account, signature, compare_signed_string).nil?
+ return actor unless verify_signature(actor, signature, compare_signed_string).nil?
- account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
+ actor = stoplight_wrap_request { actor_refresh_key!(actor) }
- raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
+ raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
- return account unless verify_signature(account, signature, compare_signed_string).nil?
+ return actor unless verify_signature(actor, signature, compare_signed_string).nil?
- fail_with! "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
+ fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
rescue SignatureVerificationError => e
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
@@ -112,7 +120,7 @@ module SignatureVerification
def fail_with!(message)
@signature_verification_failure_reason = message
- @signed_request_account = nil
+ @signed_request_actor = nil
end
def signature_params
@@ -160,10 +168,10 @@ module SignatureVerification
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
- def verify_signature(account, signature, compare_signed_string)
- if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
- @signed_request_account = account
- @signed_request_account
+ def verify_signature(actor, signature, compare_signed_string)
+ if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
+ @signed_request_actor = actor
+ @signed_request_actor
end
rescue OpenSSL::PKey::RSAError
nil
@@ -226,7 +234,7 @@ module SignatureVerification
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
- def account_from_key_id(key_id)
+ def actor_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@@ -237,13 +245,13 @@ module SignatureVerification
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
- account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
+ account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
- rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
+ rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
@@ -255,12 +263,14 @@ module SignatureVerification
.run
end
- def account_refresh_key(account)
- return if account.local? || !account.activitypub?
- ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true, suppress_errors: false)
+ def actor_refresh_key!(actor)
+ return if actor.local? || !actor.activitypub?
+ return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale?
+
+ ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
- rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteAccountService::Error, Webfinger::Error => e
+ rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index f3f8336c9..da7bb4ed2 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 69f0321f8..c37e3b68c 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index c52170d08..7d9db4d5b 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -8,7 +8,7 @@ class StatusesController < ApplicationController
layout 'public'
- before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index b82da8f0c..6dbc2667a 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -8,7 +8,7 @@ class TagsController < ApplicationController
layout 'public'
- before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
+ before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local
before_action :set_tag
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 7ff06ea39..f4c67cccd 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -116,12 +116,12 @@ class ActivityPub::Activity
def dereference_object!
return unless @object.is_a?(String)
- dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_account: signed_fetch_account)
+ dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_actor: signed_fetch_actor)
@object = dereferencer.object unless dereferencer.object.nil?
end
- def signed_fetch_account
+ def signed_fetch_actor
return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present?
first_mentioned_local_account || first_local_follower
@@ -163,15 +163,15 @@ class ActivityPub::Activity
end
def followed_by_local_accounts?
- @account.passive_relationships.exists? || @options[:relayed_through_account]&.passive_relationships&.exists?
+ @account.passive_relationships.exists? || (@options[:relayed_through_actor].is_a?(Account) && @options[:relayed_through_actor].passive_relationships&.exists?)
end
def requested_through_relay?
- @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
+ @options[:relayed_through_actor] && Relay.find_by(inbox_url: @options[:relayed_through_actor].inbox_url)&.enabled?
end
def reject_payload!
- Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
+ Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_actor] && "via #{@options[:relayed_through_actor].uri}"}")
nil
end
end
diff --git a/app/lib/activitypub/dereferencer.rb b/app/lib/activitypub/dereferencer.rb
index bea69608f..4d7756d71 100644
--- a/app/lib/activitypub/dereferencer.rb
+++ b/app/lib/activitypub/dereferencer.rb
@@ -3,10 +3,10 @@
class ActivityPub::Dereferencer
include JsonLdHelper
- def initialize(uri, permitted_origin: nil, signature_account: nil)
+ def initialize(uri, permitted_origin: nil, signature_actor: nil)
@uri = uri
@permitted_origin = permitted_origin
- @signature_account = signature_account
+ @signature_actor = signature_actor
end
def object
@@ -46,7 +46,7 @@ class ActivityPub::Dereferencer
req.add_headers('Accept' => 'application/activity+json, application/ld+json')
req.add_headers(headers) if headers
- req.on_behalf_of(@signature_account) if @signature_account
+ req.on_behalf_of(@signature_actor) if @signature_actor
req.perform do |res|
if res.code == 200
diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb
index e853a970e..f90adaf6c 100644
--- a/app/lib/activitypub/linked_data_signature.rb
+++ b/app/lib/activitypub/linked_data_signature.rb
@@ -9,7 +9,7 @@ class ActivityPub::LinkedDataSignature
@json = json.with_indifferent_access
end
- def verify_account!
+ def verify_actor!
return unless @json['signature'].is_a?(Hash)
type = @json['signature']['type']
@@ -18,7 +18,7 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
- creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
+ creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
return if creator.nil?
@@ -35,7 +35,7 @@ class ActivityPub::LinkedDataSignature
def sign!(creator, sign_with: nil)
options = {
'type' => 'RsaSignature2017',
- 'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join,
+ 'creator' => ActivityPub::TagManager.instance.key_uri_for(creator),
'created' => Time.now.utc.iso8601,
}
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index f6b9741fa..3d6b28ef5 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -44,6 +44,10 @@ class ActivityPub::TagManager
end
end
+ def key_uri_for(target)
+ [uri_for(target), '#main-key'].join
+ end
+
def uri_for_username(username)
account_url(username: username)
end
@@ -155,6 +159,10 @@ class ActivityPub::TagManager
path_params[param]
end
+ def uri_to_actor(uri)
+ uri_to_resource(uri, Account)
+ end
+
def uri_to_resource(uri, klass)
return if uri.nil?
diff --git a/app/lib/request.rb b/app/lib/request.rb
index eac04c798..648aa3085 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -40,12 +40,11 @@ class Request
set_digest! if options.key?(:body)
end
- def on_behalf_of(account, key_id_format = :uri, sign_with: nil)
- raise ArgumentError, 'account must not be nil' if account.nil?
+ def on_behalf_of(actor, sign_with: nil)
+ raise ArgumentError, 'actor must not be nil' if actor.nil?
- @account = account
- @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair
- @key_id_format = key_id_format
+ @actor = actor
+ @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
self
end
@@ -79,7 +78,7 @@ class Request
end
def headers
- (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
+ (@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
end
class << self
@@ -128,12 +127,7 @@ class Request
end
def key_id
- case @key_id_format
- when :acct
- @account.to_webfinger_s
- when :uri
- [ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
- end
+ ActivityPub::TagManager.instance.key_uri_for(@actor)
end
def http_client
diff --git a/app/serializers/activitypub/public_key_serializer.rb b/app/serializers/activitypub/public_key_serializer.rb
index 62ed49e81..8621517e7 100644
--- a/app/serializers/activitypub/public_key_serializer.rb
+++ b/app/serializers/activitypub/public_key_serializer.rb
@@ -6,7 +6,7 @@ class ActivityPub::PublicKeySerializer < ActivityPub::Serializer
attributes :id, :owner, :public_key_pem
def id
- [ActivityPub::TagManager.instance.uri_for(object), '#main-key'].join
+ ActivityPub::TagManager.instance.key_uri_for(object)
end
def owner
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index d7d739c59..ca7a8c6ca 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -1,80 +1,12 @@
# frozen_string_literal: true
-class ActivityPub::FetchRemoteAccountService < BaseService
- include JsonLdHelper
- include DomainControlHelper
- include WebfingerHelper
-
- class Error < StandardError; end
-
- SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
-
+class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
- return if domain_not_allowed?(uri)
- return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
-
- @json = begin
- if prefetched_body.nil?
- fetch_resource(uri, id)
- else
- body_to_json(prefetched_body, compare_id: id ? uri : nil)
- end
- rescue Oj::ParseError
- raise Error, "Error parsing JSON-LD document #{uri}"
- end
-
- raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
- raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
- raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
- raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
-
- @uri = @json['id']
- @username = @json['preferredUsername']
- @domain = Addressable::URI.parse(@uri).normalized_host
-
- check_webfinger! unless only_key
-
- ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
- rescue Error => e
- Rails.logger.debug "Fetching account #{uri} failed: #{e.message}"
- raise unless suppress_errors
- end
-
- private
-
- def check_webfinger!
- webfinger = webfinger!("acct:#{@username}@#{@domain}")
- confirmed_username, confirmed_domain = split_acct(webfinger.subject)
-
- if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
- raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
- return
- end
-
- webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
- @username, @domain = split_acct(webfinger.subject)
-
- unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
- raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
- end
-
- raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
- rescue Webfinger::RedirectError => e
- raise Error, e.message
- rescue Webfinger::Error => e
- raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
- end
-
- def split_acct(acct)
- acct.gsub(/\Aacct:/, '').split('@')
- end
-
- def supported_context?
- super(@json)
- end
+ actor = super
+ return actor if actor.nil? || actor.is_a?(Account)
- def expected_type?
- equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
+ Rails.logger.debug "Fetching account #{uri} failed: Expected Account, got #{actor.class.name}"
+ raise Error, "Expected Account, got #{actor.class.name}" unless suppress_errors
end
end
diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb
new file mode 100644
index 000000000..17bf2f287
--- /dev/null
+++ b/app/services/activitypub/fetch_remote_actor_service.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchRemoteActorService < BaseService
+ include JsonLdHelper
+ include DomainControlHelper
+ include WebfingerHelper
+
+ class Error < StandardError; end
+
+ SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
+
+ # Does a WebFinger roundtrip on each call, unless `only_key` is true
+ def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true)
+ return if domain_not_allowed?(uri)
+ return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
+
+ @json = begin
+ if prefetched_body.nil?
+ fetch_resource(uri, id)
+ else
+ body_to_json(prefetched_body, compare_id: id ? uri : nil)
+ end
+ rescue Oj::ParseError
+ raise Error, "Error parsing JSON-LD document #{uri}"
+ end
+
+ raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
+ raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
+ raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
+ raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
+
+ @uri = @json['id']
+ @username = @json['preferredUsername']
+ @domain = Addressable::URI.parse(@uri).normalized_host
+
+ check_webfinger! unless only_key
+
+ ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key)
+ rescue Error => e
+ Rails.logger.debug "Fetching actor #{uri} failed: #{e.message}"
+ raise unless suppress_errors
+ end
+
+ private
+
+ def check_webfinger!
+ webfinger = webfinger!("acct:#{@username}@#{@domain}")
+ confirmed_username, confirmed_domain = split_acct(webfinger.subject)
+
+ if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
+ raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
+ return
+ end
+
+ webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
+ @username, @domain = split_acct(webfinger.subject)
+
+ unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
+ raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})"
+ end
+
+ raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
+ rescue Webfinger::RedirectError => e
+ raise Error, e.message
+ rescue Webfinger::Error => e
+ raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
+ end
+
+ def split_acct(acct)
+ acct.gsub(/\Aacct:/, '').split('@')
+ end
+
+ def supported_context?
+ super(@json)
+ end
+
+ def expected_type?
+ equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
+ end
+end
diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb
index 01008d883..fe8f60b55 100644
--- a/app/services/activitypub/fetch_remote_key_service.rb
+++ b/app/services/activitypub/fetch_remote_key_service.rb
@@ -5,7 +5,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
class Error < StandardError; end
- # Returns account that owns the key
+ # Returns actor that owns the key
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank?
@@ -27,7 +27,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
raise Error, "Unexpected object type for key #{uri}" unless expected_type?
- return find_account(@json['id'], @json, suppress_errors) if person?
+ return find_actor(@json['id'], @json, suppress_errors) if person?
@owner = fetch_resource(owner_uri, true)
@@ -36,7 +36,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?
- find_account(owner_uri, @owner, suppress_errors)
+ find_actor(owner_uri, @owner, suppress_errors)
rescue Error => e
Rails.logger.debug "Fetching key #{uri} failed: #{e.message}"
raise unless suppress_errors
@@ -44,18 +44,18 @@ class ActivityPub::FetchRemoteKeyService < BaseService
private
- def find_account(uri, prefetched_body, suppress_errors)
- account = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
- account ||= ActivityPub::FetchRemoteAccountService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
- account
+ def find_actor(uri, prefetched_body, suppress_errors)
+ actor = ActivityPub::TagManager.instance.uri_to_actor(uri)
+ actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
+ actor
end
def expected_type?
- person? || public_key?
+ actor? || public_key?
end
- def person?
- equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
+ def actor?
+ equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
end
def public_key?
@@ -67,7 +67,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
end
def expected_owner_type?
- equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
+ equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
end
def confirmed_owner?
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index eb008c40a..fffe30195 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -3,8 +3,8 @@
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
- def call(body, account, **options)
- @account = account
+ def call(body, actor, **options)
+ @account = actor
@json = original_json = Oj.load(body, mode: :strict)
@options = options
@@ -16,6 +16,7 @@ class ActivityPub::ProcessCollectionService < BaseService
end
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
+ return unless @account.is_a?(Account)
if @json['signature'].present?
# We have verified the signature, but in the compaction step above, might
@@ -66,8 +67,10 @@ class ActivityPub::ProcessCollectionService < BaseService
end
def verify_account!
- @options[:relayed_through_account] = @account
- @account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
+ @options[:relayed_through_actor] = @account
+ @account = ActivityPub::LinkedDataSignature.new(@json).verify_actor!
+ @account = nil unless @account.is_a?(Account)
+ @account
rescue JSON::LD::JsonLdError => e
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
nil
diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb
index 6c0093cd4..73204e55d 100644
--- a/app/services/fetch_resource_service.rb
+++ b/app/services/fetch_resource_service.rb
@@ -47,7 +47,7 @@ class FetchResourceService < BaseService
body = response.body_with_limit
json = body_to_json(body)
- [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
+ [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
elsif !terminal
link_header = response['Link'] && parse_link_header(response)
diff --git a/app/services/keys/claim_service.rb b/app/services/keys/claim_service.rb
index 69568a0d1..ae9e24a24 100644
--- a/app/services/keys/claim_service.rb
+++ b/app/services/keys/claim_service.rb
@@ -72,7 +72,7 @@ class Keys::ClaimService < BaseService
def build_post_request(uri)
Request.new(:post, uri).tap do |request|
- request.on_behalf_of(@source_account, :uri)
+ request.on_behalf_of(@source_account)
request.add_headers(HEADERS)
end
end
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index e2c745673..37c856cf8 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -20,8 +20,8 @@ class ResolveURLService < BaseService
private
def process_url
- if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
- ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
+ if equals_or_includes_any?(type, ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
+ ActivityPub::FetchRemoteActorService.new.call(resource_url, prefetched_body: body)
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil?
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 788f2cf80..d9153132b 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -37,7 +37,7 @@ class ActivityPub::DeliveryWorker
def build_request(http_client)
Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request|
- request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with])
+ request.on_behalf_of(@source_account, sign_with: @options[:sign_with])
request.add_headers(HEADERS)
request.add_headers({ 'Collection-Synchronization' => synchronization_header }) if ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] != 'true' && @options[:synchronize_followers]
end
diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb
index 37e316354..4d06ad079 100644
--- a/app/workers/activitypub/processing_worker.rb
+++ b/app/workers/activitypub/processing_worker.rb
@@ -5,11 +5,15 @@ class ActivityPub::ProcessingWorker
sidekiq_options backtrace: true, retry: 8
- def perform(account_id, body, delivered_to_account_id = nil)
- account = Account.find_by(id: account_id)
- return if account.nil?
+ def perform(actor_id, body, delivered_to_account_id = nil, actor_type = 'Account')
+ case actor_type
+ when 'Account'
+ actor = Account.find_by(id: actor_id)
+ end
- ActivityPub::ProcessCollectionService.new.call(body, account, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
+ return if actor.nil?
+
+ ActivityPub::ProcessCollectionService.new.call(body, actor, override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
rescue ActiveRecord::RecordInvalid => e
Rails.logger.debug "Error processing incoming ActivityPub object: #{e}"
end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 662a89927..12266c800 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -420,7 +420,7 @@ RSpec.describe AccountsController, type: :controller do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
get :show, params: { username: account.username, format: format }
end
diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb
index 4d87f80ce..f78d9abbf 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/controllers/activitypub/collections_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
end
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
Fabricate(:status_pin, account: account)
Fabricate(:status_pin, account: account)
diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
index e233bd560..c19bb8cae 100644
--- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
+++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll
end
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
index 973ad83bb..2f023197b 100644
--- a/spec/controllers/activitypub/inboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/inboxes_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
let(:remote_account) { nil }
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'POST #create' do
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index 04f036447..74bf46a5e 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb
index a35957f24..aee1a8b1a 100644
--- a/spec/controllers/activitypub/replies_controller_spec.rb
+++ b/spec/controllers/activitypub/replies_controller_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
before do
stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
- allow(controller).to receive(:signed_request_account).and_return(remote_querier)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
Fabricate(:status, thread: status, visibility: :public)
Fabricate(:status, thread: status, visibility: :public)
diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb
index 05fb1445b..6e73643b4 100644
--- a/spec/controllers/concerns/signature_verification_spec.rb
+++ b/spec/controllers/concerns/signature_verification_spec.rb
@@ -3,6 +3,16 @@
require 'rails_helper'
describe ApplicationController, type: :controller do
+ class WrappedActor
+ attr_reader :wrapped_account
+
+ def initialize(wrapped_account)
+ @wrapped_account = wrapped_account
+ end
+
+ delegate :uri, :keypair, to: :wrapped_account
+ end
+
controller do
include SignatureVerification
@@ -73,6 +83,41 @@ describe ApplicationController, type: :controller do
end
end
+ context 'with a valid actor that is not an Account' do
+ let(:actor) { WrappedActor.new(author) }
+
+ before do
+ get :success
+
+ fake_request = Request.new(:get, request.url)
+ fake_request.on_behalf_of(author)
+
+ request.headers.merge!(fake_request.headers)
+
+ allow(ActivityPub::TagManager.instance).to receive(:uri_to_actor).with(anything) do
+ actor
+ end
+ end
+
+ describe '#signed_request?' do
+ it 'returns true' do
+ expect(controller.signed_request?).to be true
+ end
+ end
+
+ describe '#signed_request_account' do
+ it 'returns nil' do
+ expect(controller.signed_request_account).to be_nil
+ end
+ end
+
+ describe '#signed_request_actor' do
+ it 'returns the expected actor' do
+ expect(controller.signed_request_actor).to eq actor
+ end
+ end
+ end
+
context 'with request older than a day' do
before do
get :success
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 05fae67fa..6ed5d4bbb 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -426,7 +426,7 @@ describe StatusesController do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
- allow(controller).to receive(:signed_request_account).and_return(remote_account)
+ allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
context 'when account blocks account' do
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
index 41806b258..e9cd6c68c 100644
--- a/spec/lib/activitypub/activity/announce_spec.rb
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -115,7 +115,7 @@ RSpec.describe ActivityPub::Activity::Announce do
let(:object_json) { 'https://example.com/actor/hello-world' }
- subject { described_class.new(json, sender, relayed_through_account: relay_account) }
+ subject { described_class.new(json, sender, relayed_through_actor: relay_account) }
before do
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
diff --git a/spec/lib/activitypub/dereferencer_spec.rb b/spec/lib/activitypub/dereferencer_spec.rb
index ce30513d7..e50b497c7 100644
--- a/spec/lib/activitypub/dereferencer_spec.rb
+++ b/spec/lib/activitypub/dereferencer_spec.rb
@@ -4,10 +4,10 @@ RSpec.describe ActivityPub::Dereferencer do
describe '#object' do
let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
let(:permitted_origin) { 'https://example.com' }
- let(:signature_account) { nil }
+ let(:signature_actor) { nil }
let(:uri) { nil }
- subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
+ subject { described_class.new(uri, permitted_origin: permitted_origin, signature_actor: signature_actor).object }
before do
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
@@ -21,7 +21,7 @@ RSpec.describe ActivityPub::Dereferencer do
end
context 'with signature account' do
- let(:signature_account) { Fabricate(:account) }
+ let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
@@ -52,7 +52,7 @@ RSpec.describe ActivityPub::Dereferencer do
end
context 'with signature account' do
- let(:signature_account) { Fabricate(:account) }
+ let(:signature_actor) { Fabricate(:account) }
it 'makes signed request' do
subject
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
index 2222c46fb..d55a7c7fa 100644
--- a/spec/lib/activitypub/linked_data_signature_spec.rb
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
stub_jsonld_contexts!
end
- describe '#verify_account!' do
+ describe '#verify_actor!' do
context 'when signature matches' do
let(:raw_signature) do
{
@@ -32,7 +32,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
it 'returns creator' do
- expect(subject.verify_account!).to eq sender
+ expect(subject.verify_actor!).to eq sender
end
end
@@ -40,7 +40,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { nil }
it 'returns nil' do
- expect(subject.verify_account!).to be_nil
+ expect(subject.verify_actor!).to be_nil
end
end
@@ -55,7 +55,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') }
it 'returns nil' do
- expect(subject.verify_account!).to be_nil
+ expect(subject.verify_actor!).to be_nil
end
end
end
@@ -73,14 +73,14 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end
it 'can be verified again' do
- expect(described_class.new(subject).verify_account!).to eq sender
+ expect(described_class.new(subject).verify_actor!).to eq sender
end
end
- def sign(from_account, options, document)
+ def sign(from_actor, options, document)
options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT)))
document_hash = Digest::SHA256.hexdigest(canonicalize(document))
to_be_verified = options_hash + document_hash
- Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
+ Base64.strict_encode64(from_actor.keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_verified))
end
end
diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb
new file mode 100644
index 000000000..20117c66d
--- /dev/null
+++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb
@@ -0,0 +1,180 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
+ subject { ActivityPub::FetchRemoteActorService.new }
+
+ let!(:actor) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/alice',
+ type: 'Person',
+ preferredUsername: 'alice',
+ name: 'Alice',
+ summary: 'Foo bar',
+ inbox: 'http://example.com/alice/inbox',
+ }
+ end
+
+ describe '#call' do
+ let(:account) { subject.call('https://example.com/alice', id: true) }
+
+ shared_examples 'sets profile data' do
+ it 'returns an account' do
+ expect(account).to be_an Account
+ end
+
+ it 'sets display name' do
+ expect(account.display_name).to eq 'Alice'
+ end
+
+ it 'sets note' do
+ expect(account.note).to eq 'Foo bar'
+ end
+
+ it 'sets URL' do
+ expect(account.url).to eq 'https://example.com/alice'
+ end
+ end
+
+ context 'when the account does not have a inbox' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+ before do
+ actor[:inbox] = nil
+
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'returns nil' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'when URI and WebFinger share the same host' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'sets username and domain from webfinger' do
+ expect(account.username).to eq 'alice'
+ expect(account.domain).to eq 'example.com'
+ end
+
+ include_examples 'sets profile data'
+ end
+
+ context 'when WebFinger presents different domain than URI' do
+ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'looks up "redirected" webfinger' do
+ account
+ expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
+ end
+
+ it 'sets username and domain from final webfinger' do
+ expect(account.username).to eq 'alice'
+ expect(account.domain).to eq 'iscool.af'
+ end
+
+ include_examples 'sets profile data'
+ end
+
+ context 'when WebFinger returns a different URI' do
+ let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'does not create account' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'when WebFinger returns a different URI after a redirection' do
+ let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
+
+ before do
+ stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
+ end
+
+ it 'fetches resource' do
+ account
+ expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
+ end
+
+ it 'looks up webfinger' do
+ account
+ expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
+ end
+
+ it 'looks up "redirected" webfinger' do
+ account
+ expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
+ end
+
+ it 'does not create account' do
+ expect(account).to be_nil
+ end
+ end
+
+ context 'with wrong id' do
+ it 'does not create account' do
+ expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
index 3eccaab5b..093a188a2 100644
--- a/spec/services/activitypub/process_collection_service_spec.rb
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
it 'does not process payload if no signature exists' do
- expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
+ expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
expect(ActivityPub::Activity).not_to receive(:factory)
subject.call(json, forwarder)
@@ -77,7 +77,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'processes payload with actor if valid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' }
- expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor)
+ expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(actor)
expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
subject.call(json, forwarder)
@@ -86,7 +86,7 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
it 'does not process payload if invalid signature exists' do
payload['signature'] = { 'type' => 'RsaSignature2017' }
- expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
+ expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
expect(ActivityPub::Activity).not_to receive(:factory)
subject.call(json, forwarder)
diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb
index ded05ffbc..c0c96ab69 100644
--- a/spec/services/fetch_resource_service_spec.rb
+++ b/spec/services/fetch_resource_service_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe FetchResourceService, type: :service do
it 'signs request' do
subject
- expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made
+ expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.key_uri_for(Account.representative))}"/ })).to have_been_made
end
context 'when content type is application/atom+xml' do
--
cgit
From 62782babd08bc2385a604e275bf88af925d137c1 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Thu, 6 Oct 2022 02:26:34 +0200
Subject: Change public statuses pages to mount the web UI (#19301)
---
.../concerns/web_app_controller_concern.rb | 18 ++++++++
app/controllers/home_controller.rb | 11 ++---
app/controllers/statuses_controller.rb | 12 +-----
app/views/home/index.html.haml | 18 +-------
app/views/shared/_web_app.html.haml | 17 ++++++++
app/views/statuses/show.html.haml | 7 +---
spec/views/statuses/show.html.haml_spec.rb | 48 ----------------------
7 files changed, 42 insertions(+), 89 deletions(-)
create mode 100644 app/controllers/concerns/web_app_controller_concern.rb
create mode 100644 app/views/shared/_web_app.html.haml
(limited to 'app/controllers/concerns')
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
new file mode 100644
index 000000000..8a6c73af3
--- /dev/null
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module WebAppControllerConcern
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_body_classes
+ before_action :set_referrer_policy_header
+ end
+
+ def set_body_classes
+ @body_classes = 'app-body'
+ end
+
+ def set_referrer_policy_header
+ response.headers['Referrer-Policy'] = 'origin'
+ end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 29478209d..b4d6578b9 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,13 +1,12 @@
# frozen_string_literal: true
class HomeController < ApplicationController
+ include WebAppControllerConcern
+
before_action :redirect_unauthenticated_to_permalinks!
- before_action :set_referrer_policy_header
before_action :set_instance_presenter
- def index
- @body_classes = 'app-body'
- end
+ def index; end
private
@@ -19,10 +18,6 @@ class HomeController < ApplicationController
redirect_to(redirect_path) if redirect_path.present?
end
- def set_referrer_policy_header
- response.headers['Referrer-Policy'] = 'origin'
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 7d9db4d5b..181c76c9a 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -5,17 +5,15 @@ class StatusesController < ApplicationController
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
-
- layout 'public'
+ include WebAppControllerConcern
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
before_action :redirect_to_original, only: :show
- before_action :set_referrer_policy_header, only: :show
before_action :set_cache_headers
- before_action :set_body_classes
+ before_action :set_body_classes, only: :embed
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
@@ -28,8 +26,6 @@ class StatusesController < ApplicationController
respond_to do |format|
format.html do
expires_in 10.seconds, public: true if current_account.nil?
- set_ancestors
- set_descendants
end
format.json do
@@ -77,8 +73,4 @@ class StatusesController < ApplicationController
def redirect_to_original
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end
-
- def set_referrer_policy_header
- response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
- end
end
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 19c5191d8..76a02e0f0 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,20 +1,4 @@
- content_for :header_tags do
- - if user_signed_in?
- = preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous'
- = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
- = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
- = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
-
= render partial: 'shared/og'
- %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
-
- = render_initial_state
- = javascript_pack_tag 'application', crossorigin: 'anonymous'
-
-.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
- %noscript
- = image_pack_tag 'logo.svg', alt: 'Mastodon'
-
- %div
- = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
+= render 'shared/web_app'
diff --git a/app/views/shared/_web_app.html.haml b/app/views/shared/_web_app.html.haml
new file mode 100644
index 000000000..998cee9fa
--- /dev/null
+++ b/app/views/shared/_web_app.html.haml
@@ -0,0 +1,17 @@
+- content_for :header_tags do
+ - if user_signed_in?
+ = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
+ = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
+ = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
+
+ %meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
+
+ = render_initial_state
+ = javascript_pack_tag 'application', crossorigin: 'anonymous'
+
+.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
+ %noscript
+ = image_pack_tag 'logo.svg', alt: 'Mastodon'
+
+ %div
+ = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml
index 7ef7b09a2..5a3c94b84 100644
--- a/app/views/statuses/show.html.haml
+++ b/app/views/statuses/show.html.haml
@@ -17,9 +17,4 @@
= render 'og_description', activity: @status
= render 'og_image', activity: @status, account: @account
-.grid
- .column-0
- .activity-stream.h-entry
- = render partial: 'status', locals: { status: @status, include_threads: true }
- .column-1
- = render 'application/sidebar'
+= render 'shared/web_app'
diff --git a/spec/views/statuses/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb
index 879a26959..a69843216 100644
--- a/spec/views/statuses/show.html.haml_spec.rb
+++ b/spec/views/statuses/show.html.haml_spec.rb
@@ -15,54 +15,6 @@ describe 'statuses/show.html.haml', without_verify_partial_doubles: true do
assign(:instance_presenter, InstancePresenter.new)
end
- it 'has valid author h-card and basic data for a detailed_status' do
- alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
- bob = Fabricate(:account, username: 'bob', display_name: 'Bob')
- status = Fabricate(:status, account: alice, text: 'Hello World')
- media = Fabricate(:media_attachment, account: alice, status: status, type: :video)
- reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
-
- assign(:status, status)
- assign(:account, alice)
- assign(:descendant_threads, [])
-
- render
-
- mf2 = Microformats.parse(rendered)
-
- expect(mf2.entry.url.to_s).not_to be_empty
- expect(mf2.entry.author.name.to_s).to eq alice.display_name
- expect(mf2.entry.author.url.to_s).not_to be_empty
- end
-
- it 'has valid h-cites for p-in-reply-to and p-comment' do
- alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
- bob = Fabricate(:account, username: 'bob', display_name: 'Bob')
- carl = Fabricate(:account, username: 'carl', display_name: 'Carl')
- status = Fabricate(:status, account: alice, text: 'Hello World')
- media = Fabricate(:media_attachment, account: alice, status: status, type: :video)
- reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
- comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob')
-
- assign(:status, reply)
- assign(:account, alice)
- assign(:ancestors, reply.ancestors(1, bob))
- assign(:descendant_threads, [{ statuses: reply.descendants(1) }])
-
- render
-
- mf2 = Microformats.parse(rendered)
-
- expect(mf2.entry.url.to_s).not_to be_empty
- expect(mf2.entry.comment.url.to_s).not_to be_empty
- expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name
- expect(mf2.entry.comment.author.url.to_s).not_to be_empty
-
- expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty
- expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name
- expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty
- end
-
it 'has valid opengraph tags' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
status = Fabricate(:status, account: alice, text: 'Hello World')
--
cgit
From 839f893168ab221b08fa439012189e6c29a2721a Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Thu, 20 Oct 2022 14:35:29 +0200
Subject: Change public accounts pages to mount the web UI (#19319)
* Change public accounts pages to mount the web UI
* Fix handling of remote usernames in routes
- When logged in, serve web app
- When logged out, redirect to permalink
- Fix `app-body` class not being set sometimes due to name conflict
* Fix missing `multiColumn` prop
* Fix failing test
* Use `discoverable` attribute to control indexing directives
* Fix `` not using `multiColumn`
* Add `noindex` to accounts in REST API
* Change noindex directive to not be rendered by default before a route is mounted
* Add loading indicator for detailed status in web UI
* Fix missing indicator appearing while account is loading in web UI
---
app/controllers/about_controller.rb | 8 +
app/controllers/account_follow_controller.rb | 12 -
app/controllers/account_unfollow_controller.rb | 12 -
app/controllers/accounts_controller.rb | 58 --
.../concerns/account_controller_concern.rb | 3 +-
.../concerns/web_app_controller_concern.rb | 13 +-
app/controllers/follower_accounts_controller.rb | 5 +-
app/controllers/following_accounts_controller.rb | 5 +-
app/controllers/home_controller.rb | 13 +-
app/controllers/privacy_controller.rb | 8 +
app/controllers/remote_follow_controller.rb | 41 --
app/controllers/remote_interaction_controller.rb | 55 --
app/controllers/statuses_controller.rb | 2 +-
app/controllers/tags_controller.rb | 10 +-
app/helpers/accounts_helper.rb | 50 +-
.../mastodon/components/error_boundary.js | 7 +
.../mastodon/components/missing_indicator.js | 5 +
app/javascript/mastodon/containers/mastodon.js | 2 +-
app/javascript/mastodon/features/about/index.js | 6 +-
.../mastodon/features/account/components/header.js | 5 +-
.../mastodon/features/account_timeline/index.js | 12 +-
.../mastodon/features/bookmarked_statuses/index.js | 1 +
.../mastodon/features/community_timeline/index.js | 1 +
app/javascript/mastodon/features/compose/index.js | 5 +
.../mastodon/features/direct_timeline/index.js | 1 +
.../mastodon/features/directory/index.js | 1 +
.../mastodon/features/domain_blocks/index.js | 6 +
app/javascript/mastodon/features/explore/index.js | 1 +
.../mastodon/features/favourited_statuses/index.js | 1 +
.../mastodon/features/favourites/index.js | 5 +
.../features/follow_recommendations/index.js | 5 +
.../mastodon/features/follow_requests/index.js | 5 +
.../mastodon/features/getting_started/index.js | 1 +
.../mastodon/features/hashtag_timeline/index.js | 1 +
.../mastodon/features/home_timeline/index.js | 3 +-
.../mastodon/features/keyboard_shortcuts/index.js | 5 +
.../mastodon/features/list_timeline/index.js | 1 +
app/javascript/mastodon/features/lists/index.js | 1 +
app/javascript/mastodon/features/mutes/index.js | 5 +
.../mastodon/features/notifications/index.js | 1 +
.../mastodon/features/pinned_statuses/index.js | 4 +
.../mastodon/features/privacy_policy/index.js | 6 +-
.../mastodon/features/public_timeline/index.js | 1 +
app/javascript/mastodon/features/reblogs/index.js | 5 +
app/javascript/mastodon/features/status/index.js | 17 +-
.../features/ui/components/bundle_column_error.js | 27 +-
.../features/ui/components/column_loading.js | 6 +-
.../features/ui/components/columns_area.js | 4 +-
.../mastodon/features/ui/components/modal_root.js | 21 +-
app/javascript/mastodon/features/ui/index.js | 4 +-
.../mastodon/features/ui/util/async-components.js | 8 +
.../features/ui/util/react_router_helpers.js | 4 +-
app/javascript/mastodon/main.js | 8 -
app/javascript/mastodon/reducers/statuses.js | 6 +
app/javascript/mastodon/selectors/index.js | 2 +-
.../service_worker/web_push_notifications.js | 26 +-
app/javascript/packs/public.js | 29 -
app/javascript/styles/application.scss | 1 -
app/javascript/styles/contrast/diff.scss | 4 -
app/javascript/styles/mastodon-light/diff.scss | 89 ---
app/javascript/styles/mastodon/containers.scss | 782 ---------------------
app/javascript/styles/mastodon/footer.scss | 152 ----
app/javascript/styles/mastodon/rtl.scss | 74 --
app/javascript/styles/mastodon/statuses.scss | 3 +-
app/lib/permalink_redirector.rb | 36 +-
app/models/account.rb | 1 +
app/models/user.rb | 4 +
app/serializers/rest/account_serializer.rb | 7 +-
app/views/about/show.html.haml | 3 +
app/views/accounts/_bio.html.haml | 21 -
app/views/accounts/_header.html.haml | 43 --
app/views/accounts/_moved.html.haml | 20 -
app/views/accounts/show.html.haml | 76 +-
app/views/follower_accounts/index.html.haml | 18 +-
app/views/following_accounts/index.html.haml | 18 +-
app/views/home/index.html.haml | 3 +
app/views/layouts/public.html.haml | 60 --
app/views/privacy/show.html.haml | 3 +
app/views/remote_follow/new.html.haml | 20 -
app/views/remote_interaction/new.html.haml | 24 -
app/views/statuses/_detailed_status.html.haml | 6 +-
app/views/statuses/_simple_status.html.haml | 6 +-
app/views/statuses/show.html.haml | 2 +-
app/views/tags/show.html.haml | 5 +
config/locales/en.yml | 40 --
config/routes.rb | 57 +-
package.json | 1 -
spec/controllers/account_follow_controller_spec.rb | 64 --
.../account_unfollow_controller_spec.rb | 64 --
spec/controllers/accounts_controller_spec.rb | 194 -----
.../authorize_interactions_controller_spec.rb | 4 +-
.../follower_accounts_controller_spec.rb | 21 -
.../following_accounts_controller_spec.rb | 21 -
spec/controllers/remote_follow_controller_spec.rb | 135 ----
.../remote_interaction_controller_spec.rb | 39 -
spec/controllers/tags_controller_spec.rb | 7 +-
spec/features/profile_spec.rb | 26 +-
spec/lib/permalink_redirector_spec.rb | 31 +-
spec/requests/account_show_page_spec.rb | 15 -
spec/routing/accounts_routing_spec.rb | 88 ++-
yarn.lock | 5 -
101 files changed, 389 insertions(+), 2464 deletions(-)
delete mode 100644 app/controllers/account_follow_controller.rb
delete mode 100644 app/controllers/account_unfollow_controller.rb
delete mode 100644 app/controllers/remote_follow_controller.rb
delete mode 100644 app/controllers/remote_interaction_controller.rb
delete mode 100644 app/javascript/styles/mastodon/footer.scss
delete mode 100644 app/views/accounts/_bio.html.haml
delete mode 100644 app/views/accounts/_header.html.haml
delete mode 100644 app/views/accounts/_moved.html.haml
delete mode 100644 app/views/layouts/public.html.haml
delete mode 100644 app/views/remote_follow/new.html.haml
delete mode 100644 app/views/remote_interaction/new.html.haml
create mode 100644 app/views/tags/show.html.haml
delete mode 100644 spec/controllers/account_follow_controller_spec.rb
delete mode 100644 spec/controllers/account_unfollow_controller_spec.rb
delete mode 100644 spec/controllers/remote_follow_controller_spec.rb
delete mode 100644 spec/controllers/remote_interaction_controller_spec.rb
(limited to 'app/controllers/concerns')
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 0fbc6a800..104348614 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -5,7 +5,15 @@ class AboutController < ApplicationController
skip_before_action :require_functional!
+ before_action :set_instance_presenter
+
def show
expires_in 0, public: true unless user_signed_in?
end
+
+ private
+
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
end
diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb
deleted file mode 100644
index 33394074d..000000000
--- a/app/controllers/account_follow_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class AccountFollowController < ApplicationController
- include AccountControllerConcern
-
- before_action :authenticate_user!
-
- def create
- FollowService.new.call(current_user.account, @account, with_rate_limit: true)
- redirect_to account_path(@account)
- end
-end
diff --git a/app/controllers/account_unfollow_controller.rb b/app/controllers/account_unfollow_controller.rb
deleted file mode 100644
index 378ec86dc..000000000
--- a/app/controllers/account_unfollow_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class AccountUnfollowController < ApplicationController
- include AccountControllerConcern
-
- before_action :authenticate_user!
-
- def create
- UnfollowService.new.call(current_user.account, @account)
- redirect_to account_path(@account)
- end
-end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index d92f91b30..5ceea5d3c 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -9,7 +9,6 @@ class AccountsController < ApplicationController
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
- before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional!, unless: :whitelist_mode?
@@ -18,24 +17,6 @@ class AccountsController < ApplicationController
respond_to do |format|
format.html do
expires_in 0, public: true unless user_signed_in?
-
- @pinned_statuses = []
- @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
- @featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
-
- if current_account && @account.blocking?(current_account)
- @statuses = []
- return
- end
-
- @pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
- @statuses = cached_filtered_status_page
- @rss_url = rss_url
-
- unless @statuses.empty?
- @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
- @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
- end
end
format.rss do
@@ -55,18 +36,6 @@ class AccountsController < ApplicationController
private
- def set_body_classes
- @body_classes = 'with-modals'
- end
-
- def show_pinned_statuses?
- [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
- end
-
- def filtered_pinned_statuses
- @account.pinned_statuses.where(visibility: [:public, :unlisted])
- end
-
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
@@ -113,26 +82,6 @@ class AccountsController < ApplicationController
end
end
- def older_url
- pagination_url(max_id: @statuses.last.id)
- end
-
- def newer_url
- pagination_url(min_id: @statuses.first.id)
- end
-
- def pagination_url(max_id: nil, min_id: nil)
- if tag_requested?
- short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
- elsif media_requested?
- short_account_media_url(@account, max_id: max_id, min_id: min_id)
- elsif replies_requested?
- short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
- else
- short_account_url(@account, max_id: max_id, min_id: min_id)
- end
- end
-
def media_requested?
request.path.split('.').first.end_with?('/media') && !tag_requested?
end
@@ -145,13 +94,6 @@ class AccountsController < ApplicationController
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
- def cached_filtered_status_pins
- cache_collection(
- filtered_pinned_statuses,
- Status
- )
- end
-
def cached_filtered_status_page
cache_collection_paginated_by_id(
filtered_statuses,
diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb
index 11eac0eb6..2f7d84df0 100644
--- a/app/controllers/concerns/account_controller_concern.rb
+++ b/app/controllers/concerns/account_controller_concern.rb
@@ -3,13 +3,12 @@
module AccountControllerConcern
extend ActiveSupport::Concern
+ include WebAppControllerConcern
include AccountOwnedConcern
FOLLOW_PER_PAGE = 12
included do
- layout 'public'
-
before_action :set_instance_presenter
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
index 8a6c73af3..c671ce785 100644
--- a/app/controllers/concerns/web_app_controller_concern.rb
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -4,15 +4,24 @@ module WebAppControllerConcern
extend ActiveSupport::Concern
included do
- before_action :set_body_classes
+ before_action :redirect_unauthenticated_to_permalinks!
+ before_action :set_app_body_class
before_action :set_referrer_policy_header
end
- def set_body_classes
+ def set_app_body_class
@body_classes = 'app-body'
end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'origin'
end
+
+ def redirect_unauthenticated_to_permalinks!
+ return if user_signed_in?
+
+ redirect_path = PermalinkRedirector.new(request.path).redirect_path
+
+ redirect_to(redirect_path) if redirect_path.present?
+ end
end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index da7bb4ed2..e4d8cc495 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -3,6 +3,7 @@
class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
+ include WebAppControllerConcern
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
@@ -14,10 +15,6 @@ class FollowerAccountsController < ApplicationController
respond_to do |format|
format.html do
expires_in 0, public: true unless user_signed_in?
-
- next if @account.hide_collections?
-
- follows
end
format.json do
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index c37e3b68c..f84dca1e5 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -3,6 +3,7 @@
class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
+ include WebAppControllerConcern
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
@@ -14,10 +15,6 @@ class FollowingAccountsController < ApplicationController
respond_to do |format|
format.html do
expires_in 0, public: true unless user_signed_in?
-
- next if @account.hide_collections?
-
- follows
end
format.json do
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index b4d6578b9..d8ee82a7a 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -3,21 +3,14 @@
class HomeController < ApplicationController
include WebAppControllerConcern
- before_action :redirect_unauthenticated_to_permalinks!
before_action :set_instance_presenter
- def index; end
+ def index
+ expires_in 0, public: true unless user_signed_in?
+ end
private
- def redirect_unauthenticated_to_permalinks!
- return if user_signed_in?
-
- redirect_path = PermalinkRedirector.new(request.path).redirect_path
-
- redirect_to(redirect_path) if redirect_path.present?
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb
index bc98bca51..2c98bf3bf 100644
--- a/app/controllers/privacy_controller.rb
+++ b/app/controllers/privacy_controller.rb
@@ -5,7 +5,15 @@ class PrivacyController < ApplicationController
skip_before_action :require_functional!
+ before_action :set_instance_presenter
+
def show
expires_in 0, public: true if current_account.nil?
end
+
+ private
+
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
deleted file mode 100644
index db1604644..000000000
--- a/app/controllers/remote_follow_controller.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-class RemoteFollowController < ApplicationController
- include AccountOwnedConcern
-
- layout 'modal'
-
- before_action :set_body_classes
-
- skip_before_action :require_functional!
-
- def new
- @remote_follow = RemoteFollow.new(session_params)
- end
-
- def create
- @remote_follow = RemoteFollow.new(resource_params)
-
- if @remote_follow.valid?
- session[:remote_follow] = @remote_follow.acct
- redirect_to @remote_follow.subscribe_address_for(@account)
- else
- render :new
- end
- end
-
- private
-
- def resource_params
- params.require(:remote_follow).permit(:acct)
- end
-
- def session_params
- { acct: session[:remote_follow] || current_account&.username }
- end
-
- def set_body_classes
- @body_classes = 'modal-layout'
- @hide_header = true
- end
-end
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
deleted file mode 100644
index 6c29a2b9f..000000000
--- a/app/controllers/remote_interaction_controller.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-class RemoteInteractionController < ApplicationController
- include Authorization
-
- layout 'modal'
-
- before_action :authenticate_user!, if: :whitelist_mode?
- before_action :set_interaction_type
- before_action :set_status
- before_action :set_body_classes
-
- skip_before_action :require_functional!, unless: :whitelist_mode?
-
- def new
- @remote_follow = RemoteFollow.new(session_params)
- end
-
- def create
- @remote_follow = RemoteFollow.new(resource_params)
-
- if @remote_follow.valid?
- session[:remote_follow] = @remote_follow.acct
- redirect_to @remote_follow.interact_address_for(@status)
- else
- render :new
- end
- end
-
- private
-
- def resource_params
- params.require(:remote_follow).permit(:acct)
- end
-
- def session_params
- { acct: session[:remote_follow] || current_account&.username }
- end
-
- def set_status
- @status = Status.find(params[:id])
- authorize @status, :show?
- rescue Mastodon::NotPermittedError
- not_found
- end
-
- def set_body_classes
- @body_classes = 'modal-layout'
- @hide_header = true
- end
-
- def set_interaction_type
- @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
- end
-end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 181c76c9a..bb4e5b01f 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
class StatusesController < ApplicationController
+ include WebAppControllerConcern
include StatusControllerConcern
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
- include WebAppControllerConcern
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 2890c179d..f0a099350 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -2,18 +2,16 @@
class TagsController < ApplicationController
include SignatureVerification
+ include WebAppControllerConcern
PAGE_SIZE = 20
PAGE_SIZE_MAX = 200
- layout 'public'
-
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local
before_action :set_tag
before_action :set_statuses
- before_action :set_body_classes
before_action :set_instance_presenter
skip_before_action :require_functional!, unless: :whitelist_mode?
@@ -21,7 +19,7 @@ class TagsController < ApplicationController
def show
respond_to do |format|
format.html do
- redirect_to web_path("tags/#{@tag.name}")
+ expires_in 0, public: true unless user_signed_in?
end
format.rss do
@@ -54,10 +52,6 @@ class TagsController < ApplicationController
end
end
- def set_body_classes
- @body_classes = 'with-modals'
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
index 59664373d..6301919a9 100644
--- a/app/helpers/accounts_helper.rb
+++ b/app/helpers/accounts_helper.rb
@@ -20,54 +20,10 @@ module AccountsHelper
end
def account_action_button(account)
- if user_signed_in?
- if account.id == current_user.account_id
- link_to settings_profile_url, class: 'button logo-button' do
- safe_join([logo_as_symbol, t('settings.edit_profile')])
- end
- elsif current_account.following?(account) || current_account.requested?(account)
- link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
- safe_join([logo_as_symbol, t('accounts.unfollow')])
- end
- elsif !(account.memorial? || account.moved?)
- link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
- safe_join([logo_as_symbol, t('accounts.follow')])
- end
- end
- elsif !(account.memorial? || account.moved?)
- link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
- safe_join([logo_as_symbol, t('accounts.follow')])
- end
- end
- end
-
- def minimal_account_action_button(account)
- if user_signed_in?
- return if account.id == current_user.account_id
-
- if current_account.following?(account) || current_account.requested?(account)
- link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
- fa_icon('user-times fw')
- end
- elsif !(account.memorial? || account.moved?)
- link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
- fa_icon('user-plus fw')
- end
- end
- elsif !(account.memorial? || account.moved?)
- link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
- fa_icon('user-plus fw')
- end
- end
- end
+ return if account.memorial? || account.moved?
- def account_badge(account)
- if account.bot?
- content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
- elsif account.group?
- content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
- elsif account.user_role&.highlighted?
- content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles')
+ link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
+ safe_join([logo_as_symbol, t('accounts.follow')])
end
end
diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js
index ca4a2cfe1..02d5616d6 100644
--- a/app/javascript/mastodon/components/error_boundary.js
+++ b/app/javascript/mastodon/components/error_boundary.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { version, source_url } from 'mastodon/initial_state';
import StackTrace from 'stacktrace-js';
+import { Helmet } from 'react-helmet';
export default class ErrorBoundary extends React.PureComponent {
@@ -84,6 +85,7 @@ export default class ErrorBoundary extends React.PureComponent {
)}
+
{ likelyBrowserAddonIssue ? (
@@ -91,8 +93,13 @@ export default class ErrorBoundary extends React.PureComponent {
)}
+
Mastodon v{version} · ·
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/components/missing_indicator.js b/app/javascript/mastodon/components/missing_indicator.js
index 7b0101bab..05e0d653d 100644
--- a/app/javascript/mastodon/components/missing_indicator.js
+++ b/app/javascript/mastodon/components/missing_indicator.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import illustration from 'mastodon/../images/elephant_ui_disappointed.svg';
import classNames from 'classnames';
+import { Helmet } from 'react-helmet';
const MissingIndicator = ({ fullPage }) => (
@@ -14,6 +15,10 @@ const MissingIndicator = ({ fullPage }) => (
+
+
+
+
);
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 8e5a1fa3a..730695c49 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -78,7 +78,7 @@ export default class Mastodon extends React.PureComponent {
-
+
diff --git a/app/javascript/mastodon/features/about/index.js b/app/javascript/mastodon/features/about/index.js
index e9212565a..75fed9b95 100644
--- a/app/javascript/mastodon/features/about/index.js
+++ b/app/javascript/mastodon/features/about/index.js
@@ -94,6 +94,7 @@ class About extends React.PureComponent {
}),
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
+ multiColumn: PropTypes.bool,
};
componentDidMount () {
@@ -108,11 +109,11 @@ class About extends React.PureComponent {
}
render () {
- const { intl, server, extendedDescription, domainBlocks } = this.props;
+ const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
const isLoading = server.get('isLoading');
return (
-
+
`${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
@@ -212,6 +213,7 @@ class About extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 44c53f9ce..954cb0ee7 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -270,7 +270,9 @@ class Header extends ImmutablePureComponent {
const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
- const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
+ const isLocal = account.get('acct').indexOf('@') === -1;
+ const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
+ const isIndexable = !account.get('noindex');
let badge;
@@ -373,6 +375,7 @@ class Header extends ImmutablePureComponent {
{titleFromAccount(account)}
+
);
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 51fb76f1f..437cee95c 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -142,19 +142,17 @@ class AccountTimeline extends ImmutablePureComponent {
render () {
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
- if (!isAccount) {
+ if (isLoading && statusIds.isEmpty()) {
return (
-
-
+
);
- }
-
- if (!statusIds && isLoading) {
+ } else if (!isLoading && !isAccount) {
return (
-
+
+
);
}
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.js b/app/javascript/mastodon/features/bookmarked_statuses/index.js
index 0e466e5ed..097be17c9 100644
--- a/app/javascript/mastodon/features/bookmarked_statuses/index.js
+++ b/app/javascript/mastodon/features/bookmarked_statuses/index.js
@@ -99,6 +99,7 @@ class Bookmarks extends ImmutablePureComponent {
{intl.formatMessage(messages.heading)}
+
);
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
index 757521802..7b3f8845f 100644
--- a/app/javascript/mastodon/features/community_timeline/index.js
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -151,6 +151,7 @@ class CommunityTimeline extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index c27556a0e..763c715de 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -18,6 +18,7 @@ import { mascot } from '../../initial_state';
import Icon from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
import Column from 'mastodon/components/column';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -145,6 +146,10 @@ class Compose extends React.PureComponent {
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js
index cfaa9c4c5..8dcc43e28 100644
--- a/app/javascript/mastodon/features/direct_timeline/index.js
+++ b/app/javascript/mastodon/features/direct_timeline/index.js
@@ -98,6 +98,7 @@ class DirectTimeline extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js
index 0ce7919b6..b45faa049 100644
--- a/app/javascript/mastodon/features/directory/index.js
+++ b/app/javascript/mastodon/features/directory/index.js
@@ -169,6 +169,7 @@ class Directory extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js
index edb80aef4..43b275c2d 100644
--- a/app/javascript/mastodon/features/domain_blocks/index.js
+++ b/app/javascript/mastodon/features/domain_blocks/index.js
@@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import DomainContainer from '../../containers/domain_container';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import ScrollableList from '../../components/scrollable_list';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
@@ -59,6 +60,7 @@ class Blocks extends ImmutablePureComponent {
return (
+
,
)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js
index 566be631e..1c7049e97 100644
--- a/app/javascript/mastodon/features/explore/index.js
+++ b/app/javascript/mastodon/features/explore/index.js
@@ -84,6 +84,7 @@ class Explore extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
)}
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index f1d32eff1..3741f68f6 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -99,6 +99,7 @@ class Favourites extends ImmutablePureComponent {
{intl.formatMessage(messages.heading)}
+
);
diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js
index 673317f04..ad10744da 100644
--- a/app/javascript/mastodon/features/favourites/index.js
+++ b/app/javascript/mastodon/features/favourites/index.js
@@ -11,6 +11,7 @@ import LoadingIndicator from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
@@ -80,6 +81,10 @@ class Favourites extends ImmutablePureComponent {
,
)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/follow_recommendations/index.js b/app/javascript/mastodon/features/follow_recommendations/index.js
index 32b55eeb3..5f7baa64c 100644
--- a/app/javascript/mastodon/features/follow_recommendations/index.js
+++ b/app/javascript/mastodon/features/follow_recommendations/index.js
@@ -12,6 +12,7 @@ import Column from 'mastodon/features/ui/components/column';
import Account from './components/account';
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
import Button from 'mastodon/components/button';
+import { Helmet } from 'react-helmet';
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
@@ -104,6 +105,10 @@ class FollowRecommendations extends ImmutablePureComponent {
)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js
index 1f9b635bb..d16aa7737 100644
--- a/app/javascript/mastodon/features/follow_requests/index.js
+++ b/app/javascript/mastodon/features/follow_requests/index.js
@@ -12,6 +12,7 @@ import AccountAuthorizeContainer from './containers/account_authorize_container'
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
import ScrollableList from '../../components/scrollable_list';
import { me } from '../../initial_state';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
@@ -87,6 +88,10 @@ class FollowRequests extends ImmutablePureComponent {
,
)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 42a5b581f..f002ffc77 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -138,6 +138,7 @@ class GettingStarted extends ImmutablePureComponent {
{intl.formatMessage(messages.menu)}
+
);
diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js
index 0f7df5036..ec524be8f 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.js
@@ -228,6 +228,7 @@ class HashtagTimeline extends React.PureComponent {
#{id}
+
);
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 68770b739..838ed7dd8 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -20,7 +20,7 @@ const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
-});
+});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
@@ -167,6 +167,7 @@ class HomeTimeline extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
index 2a32577ba..9a870478d 100644
--- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js
+++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
@@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ColumnHeader from 'mastodon/components/column_header';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
@@ -164,6 +165,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index f0a7a0c7f..fd9d33df7 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -212,6 +212,7 @@ class ListTimeline extends React.PureComponent {
{title}
+
);
diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js
index 389a0c5c8..017595ba0 100644
--- a/app/javascript/mastodon/features/lists/index.js
+++ b/app/javascript/mastodon/features/lists/index.js
@@ -80,6 +80,7 @@ class Lists extends ImmutablePureComponent {
{intl.formatMessage(messages.heading)}
+
);
diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js
index c21433cc4..65df6149f 100644
--- a/app/javascript/mastodon/features/mutes/index.js
+++ b/app/javascript/mastodon/features/mutes/index.js
@@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container';
import { fetchMutes, expandMutes } from '../../actions/mutes';
import ScrollableList from '../../components/scrollable_list';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
@@ -72,6 +73,10 @@ class Mutes extends ImmutablePureComponent {
,
)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 4577bcb2d..f1bc5f160 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -281,6 +281,7 @@ class Notifications extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/pinned_statuses/index.js b/app/javascript/mastodon/features/pinned_statuses/index.js
index 67b13f10a..c6790ea06 100644
--- a/app/javascript/mastodon/features/pinned_statuses/index.js
+++ b/app/javascript/mastodon/features/pinned_statuses/index.js
@@ -8,6 +8,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
@@ -54,6 +55,9 @@ class PinnedStatuses extends ImmutablePureComponent {
hasMore={hasMore}
bindToDocument={!multiColumn}
/>
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/privacy_policy/index.js b/app/javascript/mastodon/features/privacy_policy/index.js
index eee4255f4..3df487e8f 100644
--- a/app/javascript/mastodon/features/privacy_policy/index.js
+++ b/app/javascript/mastodon/features/privacy_policy/index.js
@@ -15,6 +15,7 @@ class PrivacyPolicy extends React.PureComponent {
static propTypes = {
intl: PropTypes.object,
+ multiColumn: PropTypes.bool,
};
state = {
@@ -32,11 +33,11 @@ class PrivacyPolicy extends React.PureComponent {
}
render () {
- const { intl } = this.props;
+ const { intl, multiColumn } = this.props;
const { isLoading, content, lastUpdated } = this.state;
return (
-
+
@@ -51,6 +52,7 @@ class PrivacyPolicy extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
index 8dbef98c0..a41be07e1 100644
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ b/app/javascript/mastodon/features/public_timeline/index.js
@@ -153,6 +153,7 @@ class PublicTimeline extends React.PureComponent {
{intl.formatMessage(messages.title)}
+
);
diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js
index 7704a049f..70d338ef1 100644
--- a/app/javascript/mastodon/features/reblogs/index.js
+++ b/app/javascript/mastodon/features/reblogs/index.js
@@ -11,6 +11,7 @@ import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
@@ -80,6 +81,10 @@ class Reblogs extends ImmutablePureComponent {
,
)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index f9a97c9b5..02f390c6a 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { createSelector } from 'reselect';
import { fetchStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator';
+import LoadingIndicator from 'mastodon/components/loading_indicator';
import DetailedStatus from './components/detailed_status';
import ActionBar from './components/action_bar';
import Column from '../ui/components/column';
@@ -145,6 +146,7 @@ const makeMapStateToProps = () => {
}
return {
+ isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
status,
ancestorsIds,
descendantsIds,
@@ -187,6 +189,7 @@ class Status extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
+ isLoading: PropTypes.bool,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
@@ -566,9 +569,17 @@ class Status extends ImmutablePureComponent {
render () {
let ancestors, descendants;
- const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
+ const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
if (status === null) {
return (
@@ -586,6 +597,9 @@ class Status extends ImmutablePureComponent {
descendants = {this.renderChildren(descendantsIds)}
;
}
+ const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
+ const isIndexable = !status.getIn(['account', 'noindex']);
+
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
@@ -659,6 +673,7 @@ class Status extends ImmutablePureComponent {
{titleFromStatus(status)}
+
);
diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
index f39ebd900..ab6d4aa44 100644
--- a/app/javascript/mastodon/features/ui/components/bundle_column_error.js
+++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
@@ -1,11 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
-
-import Column from './column';
-import ColumnHeader from './column_header';
-import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
-import IconButton from '../../../components/icon_button';
+import Column from 'mastodon/components/column';
+import ColumnHeader from 'mastodon/components/column_header';
+import IconButton from 'mastodon/components/icon_button';
+import { Helmet } from 'react-helmet';
const messages = defineMessages({
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
@@ -18,6 +17,7 @@ class BundleColumnError extends React.PureComponent {
static propTypes = {
onRetry: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
+ multiColumn: PropTypes.bool,
}
handleRetry = () => {
@@ -25,16 +25,25 @@ class BundleColumnError extends React.PureComponent {
}
render () {
- const { intl: { formatMessage } } = this.props;
+ const { multiColumn, intl: { formatMessage } } = this.props;
return (
-
-
-
+
+
+
{formatMessage(messages.body)}
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js
index 0cdfd05d8..e5ed22584 100644
--- a/app/javascript/mastodon/features/ui/components/column_loading.js
+++ b/app/javascript/mastodon/features/ui/components/column_loading.js
@@ -10,6 +10,7 @@ export default class ColumnLoading extends ImmutablePureComponent {
static propTypes = {
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
icon: PropTypes.string,
+ multiColumn: PropTypes.bool,
};
static defaultProps = {
@@ -18,10 +19,11 @@ export default class ColumnLoading extends ImmutablePureComponent {
};
render() {
- let { title, icon } = this.props;
+ let { title, icon, multiColumn } = this.props;
+
return (
-
+
);
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index cc1bc83e0..9ee6fca43 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -139,11 +139,11 @@ class ColumnsArea extends ImmutablePureComponent {
}
renderLoading = columnId => () => {
- return columnId === 'COMPOSE' ? : ;
+ return columnId === 'COMPOSE' ? : ;
}
renderError = (props) => {
- return ;
+ return ;
}
render () {
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 5c273ffa4..2224a8207 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -11,9 +11,7 @@ import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import AudioModal from './audio_modal';
import ConfirmationModal from './confirmation_modal';
-import SubscribedLanguagesModal from 'mastodon/features/subscribed_languages_modal';
import FocalPointModal from './focal_point_modal';
-import InteractionModal from 'mastodon/features/interaction_modal';
import {
MuteModal,
BlockModal,
@@ -23,7 +21,10 @@ import {
ListAdder,
CompareHistoryModal,
FilterModal,
+ InteractionModal,
+ SubscribedLanguagesModal,
} from 'mastodon/features/ui/util/async-components';
+import { Helmet } from 'react-helmet';
const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@@ -41,8 +42,8 @@ const MODAL_COMPONENTS = {
'LIST_ADDER': ListAdder,
'COMPARE_HISTORY': CompareHistoryModal,
'FILTER': FilterModal,
- 'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }),
- 'INTERACTION': () => Promise.resolve({ default: InteractionModal }),
+ 'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
+ 'INTERACTION': InteractionModal,
};
export default class ModalRoot extends React.PureComponent {
@@ -111,9 +112,15 @@ export default class ModalRoot extends React.PureComponent {
return (
{visible && (
-
- {(SpecificComponent) => }
-
+ <>
+
+ {(SpecificComponent) => }
+
+
+
+
+
+ >
)}
);
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 8f9f38036..003991857 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -197,8 +197,8 @@ class SwitchingColumnsArea extends React.PureComponent {
-
-
+
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index c79dc014c..7686a69ea 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -166,6 +166,14 @@ export function FilterModal () {
return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal');
}
+export function InteractionModal () {
+ return import(/*webpackChunkName: "modals/interaction_modal" */'../../interaction_modal');
+}
+
+export function SubscribedLanguagesModal () {
+ return import(/*webpackChunkName: "modals/subscribed_languages_modal" */'../../subscribed_languages_modal');
+}
+
export function About () {
return import(/*webpackChunkName: "features/about" */'../../about');
}
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
index d452b871f..a65d79def 100644
--- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -53,7 +53,9 @@ export class WrappedRoute extends React.Component {
}
renderLoading = () => {
- return ;
+ const { multiColumn } = this.props;
+
+ return ;
}
renderError = (props) => {
diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index f33375b50..d0337ce0c 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -12,14 +12,6 @@ const perf = require('mastodon/performance');
function main() {
perf.start('main()');
- if (window.history && history.replaceState) {
- const { pathname, search, hash } = window.location;
- const path = pathname + search + hash;
- if (!(/^\/web($|\/)/).test(path)) {
- history.replaceState(null, document.title, `/web${path}`);
- }
- }
-
return ready(async () => {
const mountNode = document.getElementById('mastodon');
const props = JSON.parse(mountNode.getAttribute('data-props'));
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 7efb49d85..c30c1e2cc 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -15,6 +15,8 @@ import {
STATUS_COLLAPSE,
STATUS_TRANSLATE_SUCCESS,
STATUS_TRANSLATE_UNDO,
+ STATUS_FETCH_REQUEST,
+ STATUS_FETCH_FAIL,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
@@ -37,6 +39,10 @@ const initialState = ImmutableMap();
export default function statuses(state = initialState, action) {
switch(action.type) {
+ case STATUS_FETCH_REQUEST:
+ return state.setIn([action.id, 'isLoading'], true);
+ case STATUS_FETCH_FAIL:
+ return state.delete(action.id);
case STATUS_IMPORT:
return importStatus(state, action.status);
case STATUSES_IMPORT:
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 3dd7f4897..bf46c810e 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -41,7 +41,7 @@ export const makeGetStatus = () => {
],
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
- if (!statusBase) {
+ if (!statusBase || statusBase.get('isLoading')) {
return null;
}
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index 9b75e9b9d..f12595777 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -15,7 +15,7 @@ const notify = options =>
icon: '/android-chrome-192x192.png',
tag: GROUP_TAG,
data: {
- url: (new URL('/web/notifications', self.location)).href,
+ url: (new URL('/notifications', self.location)).href,
count: notifications.length + 1,
preferred_locale: options.data.preferred_locale,
},
@@ -90,7 +90,7 @@ export const handlePush = (event) => {
options.tag = notification.id;
options.badge = '/badge.png';
options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined;
- options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/@${notification.account.acct}/${notification.status.id}` : `/web/@${notification.account.acct}` };
+ options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/${notification.status.id}` : `/@${notification.account.acct}` };
if (notification.status && notification.status.spoiler_text || notification.status.sensitive) {
options.data.hiddenBody = htmlToPlainText(notification.status.content);
@@ -115,7 +115,7 @@ export const handlePush = (event) => {
tag: notification_id,
timestamp: new Date(),
badge: '/badge.png',
- data: { access_token, preferred_locale, url: '/web/notifications' },
+ data: { access_token, preferred_locale, url: '/notifications' },
});
}),
);
@@ -166,24 +166,10 @@ const removeActionFromNotification = (notification, action) => {
const openUrl = url =>
self.clients.matchAll({ type: 'window' }).then(clientList => {
- if (clientList.length !== 0) {
- const webClients = clientList.filter(client => /\/web\//.test(client.url));
-
- if (webClients.length !== 0) {
- const client = findBestClient(webClients);
- const { pathname } = new URL(url, self.location);
-
- if (pathname.startsWith('/web/')) {
- return client.focus().then(client => client.postMessage({
- type: 'navigate',
- path: pathname.slice('/web/'.length - 1),
- }));
- }
- } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
- const client = findBestClient(clientList);
+ if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
+ const client = findBestClient(clientList);
- return client.navigate(url).then(client => client.focus());
- }
+ return client.navigate(url).then(client => client.focus());
}
return self.clients.openWindow(url);
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index e42468e0c..5ff45fa55 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -33,7 +33,6 @@ function main() {
const { messages } = getLocale();
const React = require('react');
const ReactDOM = require('react-dom');
- const Rellax = require('rellax');
const { createBrowserHistory } = require('history');
const scrollToDetailedStatus = () => {
@@ -112,12 +111,6 @@ function main() {
scrollToDetailedStatus();
}
- const parallaxComponents = document.querySelectorAll('.parallax');
-
- if (parallaxComponents.length > 0 ) {
- new Rellax('.parallax', { speed: -1 });
- }
-
delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
const password = document.getElementById('registration_user_password');
const confirmation = document.getElementById('registration_user_password_confirmation');
@@ -168,28 +161,6 @@ function main() {
});
});
- delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
- if (button !== 0) {
- return true;
- }
- window.location.href = target.href;
- return false;
- });
-
- delegate(document, '.modal-button', 'click', e => {
- e.preventDefault();
-
- let href;
-
- if (e.target.nodeName !== 'A') {
- href = e.target.parentNode.href;
- } else {
- href = e.target.href;
- }
-
- window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
- });
-
delegate(document, '#account_display_name', 'input', ({ target }) => {
const name = document.querySelector('.card .display-name strong');
if (name) {
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index e9f596e2f..81a040108 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -8,7 +8,6 @@
@import 'mastodon/branding';
@import 'mastodon/containers';
@import 'mastodon/lists';
-@import 'mastodon/footer';
@import 'mastodon/widgets';
@import 'mastodon/forms';
@import 'mastodon/accounts';
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
index 22f5bcc94..27eb837df 100644
--- a/app/javascript/styles/contrast/diff.scss
+++ b/app/javascript/styles/contrast/diff.scss
@@ -68,10 +68,6 @@
color: $darker-text-color;
}
-.public-layout .public-account-header__tabs__tabs .counter.active::after {
- border-bottom: 4px solid $ui-highlight-color;
-}
-
.compose-form .autosuggest-textarea__textarea::placeholder,
.compose-form .spoiler-input__input::placeholder {
color: $inverted-text-color;
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index 4b27e6b4f..20e973b8b 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -655,95 +655,6 @@ html {
}
}
-.public-layout {
- .account__section-headline {
- border: 1px solid lighten($ui-base-color, 8%);
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border-top: 0;
- }
- }
-
- .header,
- .public-account-header,
- .public-account-bio {
- box-shadow: none;
- }
-
- .public-account-bio,
- .hero-widget__text {
- background: $account-background-color;
- }
-
- .header {
- background: $ui-base-color;
- border: 1px solid lighten($ui-base-color, 8%);
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border: 0;
- }
-
- .brand {
- &:hover,
- &:focus,
- &:active {
- background: lighten($ui-base-color, 4%);
- }
- }
- }
-
- .public-account-header {
- &__image {
- background: lighten($ui-base-color, 12%);
-
- &::after {
- box-shadow: none;
- }
- }
-
- &__bar {
- &::before {
- background: $account-background-color;
- border: 1px solid lighten($ui-base-color, 8%);
- border-top: 0;
- }
-
- .avatar img {
- border-color: $account-background-color;
- }
-
- @media screen and (max-width: $no-columns-breakpoint) {
- background: $account-background-color;
- border: 1px solid lighten($ui-base-color, 8%);
- border-top: 0;
- }
- }
-
- &__tabs {
- &__name {
- h1,
- h1 small {
- color: $white;
-
- @media screen and (max-width: $no-columns-breakpoint) {
- color: $primary-text-color;
- }
- }
- }
- }
-
- &__extra {
- .public-account-bio {
- border: 0;
- }
-
- .public-account-bio .account__header__fields {
- border-color: lighten($ui-base-color, 8%);
- }
- }
- }
-}
-
.notification__filter-bar button.active::after,
.account__section-headline a.active::after {
border-color: transparent transparent $white;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 8e5ed03f0..b49b93984 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -104,785 +104,3 @@
margin-left: 10px;
}
}
-
-.grid-3 {
- display: grid;
- grid-gap: 10px;
- grid-template-columns: 3fr 1fr;
- grid-auto-columns: 25%;
- grid-auto-rows: max-content;
-
- .column-0 {
- grid-column: 1 / 3;
- grid-row: 1;
- }
-
- .column-1 {
- grid-column: 1;
- grid-row: 2;
- }
-
- .column-2 {
- grid-column: 2;
- grid-row: 2;
- }
-
- .column-3 {
- grid-column: 1 / 3;
- grid-row: 3;
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- grid-gap: 0;
- grid-template-columns: minmax(0, 100%);
-
- .column-0 {
- grid-column: 1;
- }
-
- .column-1 {
- grid-column: 1;
- grid-row: 3;
- }
-
- .column-2 {
- grid-column: 1;
- grid-row: 2;
- }
-
- .column-3 {
- grid-column: 1;
- grid-row: 4;
- }
- }
-}
-
-.grid-4 {
- display: grid;
- grid-gap: 10px;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- grid-auto-columns: 25%;
- grid-auto-rows: max-content;
-
- .column-0 {
- grid-column: 1 / 5;
- grid-row: 1;
- }
-
- .column-1 {
- grid-column: 1 / 4;
- grid-row: 2;
- }
-
- .column-2 {
- grid-column: 4;
- grid-row: 2;
- }
-
- .column-3 {
- grid-column: 2 / 5;
- grid-row: 3;
- }
-
- .column-4 {
- grid-column: 1;
- grid-row: 3;
- }
-
- .landing-page__call-to-action {
- min-height: 100%;
- }
-
- .flash-message {
- margin-bottom: 10px;
- }
-
- @media screen and (max-width: 738px) {
- grid-template-columns: minmax(0, 50%) minmax(0, 50%);
-
- .landing-page__call-to-action {
- padding: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .row__information-board {
- width: 100%;
- justify-content: center;
- align-items: center;
- }
-
- .row__mascot {
- display: none;
- }
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- grid-gap: 0;
- grid-template-columns: minmax(0, 100%);
-
- .column-0 {
- grid-column: 1;
- }
-
- .column-1 {
- grid-column: 1;
- grid-row: 3;
- }
-
- .column-2 {
- grid-column: 1;
- grid-row: 2;
- }
-
- .column-3 {
- grid-column: 1;
- grid-row: 5;
- }
-
- .column-4 {
- grid-column: 1;
- grid-row: 4;
- }
- }
-}
-
-.public-layout {
- @media screen and (max-width: $no-gap-breakpoint) {
- padding-top: 48px;
- }
-
- .container {
- max-width: 960px;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- padding: 0;
- }
- }
-
- .header {
- background: lighten($ui-base-color, 8%);
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
- border-radius: 4px;
- height: 48px;
- margin: 10px 0;
- display: flex;
- align-items: stretch;
- justify-content: center;
- flex-wrap: nowrap;
- overflow: hidden;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- position: fixed;
- width: 100%;
- top: 0;
- left: 0;
- margin: 0;
- border-radius: 0;
- box-shadow: none;
- z-index: 110;
- }
-
- & > div {
- flex: 1 1 33.3%;
- min-height: 1px;
- }
-
- .nav-left {
- display: flex;
- align-items: stretch;
- justify-content: flex-start;
- flex-wrap: nowrap;
- }
-
- .nav-center {
- display: flex;
- align-items: stretch;
- justify-content: center;
- flex-wrap: nowrap;
- }
-
- .nav-right {
- display: flex;
- align-items: stretch;
- justify-content: flex-end;
- flex-wrap: nowrap;
- }
-
- .brand {
- display: block;
- padding: 15px;
-
- .logo {
- display: block;
- height: 18px;
- width: auto;
- position: relative;
- bottom: -2px;
- fill: $primary-text-color;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- height: 20px;
- }
- }
-
- &:hover,
- &:focus,
- &:active {
- background: lighten($ui-base-color, 12%);
- }
- }
-
- .nav-link {
- display: flex;
- align-items: center;
- padding: 0 1rem;
- font-size: 12px;
- font-weight: 500;
- text-decoration: none;
- color: $darker-text-color;
- white-space: nowrap;
- text-align: center;
-
- &:hover,
- &:focus,
- &:active {
- text-decoration: underline;
- color: $primary-text-color;
- }
-
- @media screen and (max-width: 550px) {
- &.optional {
- display: none;
- }
- }
- }
-
- .nav-button {
- background: lighten($ui-base-color, 16%);
- margin: 8px;
- margin-left: 0;
- border-radius: 4px;
-
- &:hover,
- &:focus,
- &:active {
- text-decoration: none;
- background: lighten($ui-base-color, 20%);
- }
- }
- }
-
- $no-columns-breakpoint: 600px;
-
- .grid {
- display: grid;
- grid-gap: 10px;
- grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr);
- grid-auto-columns: 25%;
- grid-auto-rows: max-content;
-
- .column-0 {
- grid-row: 1;
- grid-column: 1;
- }
-
- .column-1 {
- grid-row: 1;
- grid-column: 2;
- }
-
- @media screen and (max-width: $no-columns-breakpoint) {
- grid-template-columns: 100%;
- grid-gap: 0;
-
- .column-1 {
- display: none;
- }
- }
- }
-
- .page-header {
- @media screen and (max-width: $no-gap-breakpoint) {
- border-bottom: 0;
- }
- }
-
- .public-account-header {
- overflow: hidden;
- margin-bottom: 10px;
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-
- &.inactive {
- opacity: 0.5;
-
- .public-account-header__image,
- .avatar {
- filter: grayscale(100%);
- }
-
- .logo-button {
- background-color: $secondary-text-color;
- }
- }
-
- .logo-button {
- padding: 3px 15px;
- }
-
- &__image {
- border-radius: 4px 4px 0 0;
- overflow: hidden;
- height: 300px;
- position: relative;
- background: darken($ui-base-color, 12%);
-
- &::after {
- content: "";
- display: block;
- position: absolute;
- width: 100%;
- height: 100%;
- box-shadow: inset 0 -1px 1px 1px rgba($base-shadow-color, 0.15);
- top: 0;
- left: 0;
- }
-
- img {
- object-fit: cover;
- display: block;
- width: 100%;
- height: 100%;
- margin: 0;
- border-radius: 4px 4px 0 0;
- }
-
- @media screen and (max-width: 600px) {
- height: 200px;
- }
- }
-
- &--no-bar {
- margin-bottom: 0;
-
- .public-account-header__image,
- .public-account-header__image img {
- border-radius: 4px;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border-radius: 0;
- }
- }
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- margin-bottom: 0;
- box-shadow: none;
-
- &__image::after {
- display: none;
- }
-
- &__image,
- &__image img {
- border-radius: 0;
- }
- }
-
- &__bar {
- position: relative;
- margin-top: -80px;
- display: flex;
- justify-content: flex-start;
-
- &::before {
- content: "";
- display: block;
- background: lighten($ui-base-color, 4%);
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 60px;
- border-radius: 0 0 4px 4px;
- z-index: -1;
- }
-
- .avatar {
- display: block;
- width: 120px;
- height: 120px;
- padding-left: 20px - 4px;
- flex: 0 0 auto;
-
- img {
- display: block;
- width: 100%;
- height: 100%;
- margin: 0;
- border-radius: 50%;
- border: 4px solid lighten($ui-base-color, 4%);
- background: darken($ui-base-color, 8%);
- }
- }
-
- @media screen and (max-width: 600px) {
- margin-top: 0;
- background: lighten($ui-base-color, 4%);
- border-radius: 0 0 4px 4px;
- padding: 5px;
-
- &::before {
- display: none;
- }
-
- .avatar {
- width: 48px;
- height: 48px;
- padding: 7px 0;
- padding-left: 10px;
-
- img {
- border: 0;
- border-radius: 4px;
- }
-
- @media screen and (max-width: 360px) {
- display: none;
- }
- }
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border-radius: 0;
- }
-
- @media screen and (max-width: $no-columns-breakpoint) {
- flex-wrap: wrap;
- }
- }
-
- &__tabs {
- flex: 1 1 auto;
- margin-left: 20px;
-
- &__name {
- padding-top: 20px;
- padding-bottom: 8px;
-
- h1 {
- font-size: 20px;
- line-height: 18px * 1.5;
- color: $primary-text-color;
- font-weight: 500;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- text-shadow: 1px 1px 1px $base-shadow-color;
-
- small {
- display: block;
- font-size: 14px;
- color: $primary-text-color;
- font-weight: 400;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
- }
-
- @media screen and (max-width: 600px) {
- margin-left: 15px;
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- &__name {
- padding-top: 0;
- padding-bottom: 0;
-
- h1 {
- font-size: 16px;
- line-height: 24px;
- text-shadow: none;
-
- small {
- color: $darker-text-color;
- }
- }
- }
- }
-
- &__tabs {
- display: flex;
- justify-content: flex-start;
- align-items: stretch;
- height: 58px;
-
- .details-counters {
- display: flex;
- flex-direction: row;
- min-width: 300px;
- }
-
- @media screen and (max-width: $no-columns-breakpoint) {
- .details-counters {
- display: none;
- }
- }
-
- .counter {
- min-width: 33.3%;
- box-sizing: border-box;
- flex: 0 0 auto;
- color: $darker-text-color;
- padding: 10px;
- border-right: 1px solid lighten($ui-base-color, 4%);
- cursor: default;
- text-align: center;
- position: relative;
-
- a {
- display: block;
- }
-
- &:last-child {
- border-right: 0;
- }
-
- &::after {
- display: block;
- content: "";
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- border-bottom: 4px solid $ui-primary-color;
- opacity: 0.5;
- transition: all 400ms ease;
- }
-
- &.active {
- &::after {
- border-bottom: 4px solid $highlight-text-color;
- opacity: 1;
- }
-
- &.inactive::after {
- border-bottom-color: $secondary-text-color;
- }
- }
-
- &:hover {
- &::after {
- opacity: 1;
- transition-duration: 100ms;
- }
- }
-
- a {
- text-decoration: none;
- color: inherit;
- }
-
- .counter-label {
- font-size: 12px;
- display: block;
- }
-
- .counter-number {
- font-weight: 500;
- font-size: 18px;
- margin-bottom: 5px;
- color: $primary-text-color;
- font-family: $font-display, sans-serif;
- }
- }
-
- .spacer {
- flex: 1 1 auto;
- height: 1px;
- }
-
- &__buttons {
- padding: 7px 8px;
- }
- }
- }
-
- &__extra {
- display: none;
- margin-top: 4px;
-
- .public-account-bio {
- border-radius: 0;
- box-shadow: none;
- background: transparent;
- margin: 0 -5px;
-
- .account__header__fields {
- border-top: 1px solid lighten($ui-base-color, 12%);
- }
-
- .roles {
- display: none;
- }
- }
-
- &__links {
- margin-top: -15px;
- font-size: 14px;
- color: $darker-text-color;
-
- a {
- display: inline-block;
- color: $darker-text-color;
- text-decoration: none;
- padding: 15px;
- font-weight: 500;
-
- strong {
- font-weight: 700;
- color: $primary-text-color;
- }
- }
- }
-
- @media screen and (max-width: $no-columns-breakpoint) {
- display: block;
- flex: 100%;
- }
- }
- }
-
- .account__section-headline {
- border-radius: 4px 4px 0 0;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- border-radius: 0;
- }
- }
-
- .detailed-status__meta {
- margin-top: 25px;
- }
-
- .public-account-bio {
- background: lighten($ui-base-color, 8%);
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
- border-radius: 4px;
- overflow: hidden;
- margin-bottom: 10px;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- box-shadow: none;
- margin-bottom: 0;
- border-radius: 0;
- }
-
- .account__header__fields {
- margin: 0;
- border-top: 0;
-
- a {
- color: $highlight-text-color;
- }
-
- dl:first-child .verified {
- border-radius: 0 4px 0 0;
- }
-
- .verified a {
- color: $valid-value-color;
- }
- }
-
- .account__header__content {
- padding: 20px;
- padding-bottom: 0;
- color: $primary-text-color;
- }
-
- &__extra,
- .roles {
- padding: 20px;
- font-size: 14px;
- color: $darker-text-color;
- }
-
- .roles {
- padding-bottom: 0;
- }
- }
-
- .directory__list {
- display: grid;
- grid-gap: 10px;
- grid-template-columns: minmax(0, 50%) minmax(0, 50%);
-
- .account-card {
- display: flex;
- flex-direction: column;
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- display: block;
-
- .account-card {
- margin-bottom: 10px;
- display: block;
- }
- }
- }
-
- .card-grid {
- display: flex;
- flex-wrap: wrap;
- min-width: 100%;
- margin: 0 -5px;
-
- & > div {
- box-sizing: border-box;
- flex: 1 0 auto;
- width: 300px;
- padding: 0 5px;
- margin-bottom: 10px;
- max-width: 33.333%;
-
- @media screen and (max-width: 900px) {
- max-width: 50%;
- }
-
- @media screen and (max-width: 600px) {
- max-width: 100%;
- }
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- margin: 0;
- border-top: 1px solid lighten($ui-base-color, 8%);
-
- & > div {
- width: 100%;
- padding: 0;
- margin-bottom: 0;
- border-bottom: 1px solid lighten($ui-base-color, 8%);
-
- &:last-child {
- border-bottom: 0;
- }
-
- .card__bar {
- background: $ui-base-color;
-
- &:hover,
- &:active,
- &:focus {
- background: lighten($ui-base-color, 4%);
- }
- }
- }
- }
- }
-}
diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss
deleted file mode 100644
index 0c3e42033..000000000
--- a/app/javascript/styles/mastodon/footer.scss
+++ /dev/null
@@ -1,152 +0,0 @@
-.public-layout {
- .footer {
- text-align: left;
- padding-top: 20px;
- padding-bottom: 60px;
- font-size: 12px;
- color: lighten($ui-base-color, 34%);
-
- @media screen and (max-width: $no-gap-breakpoint) {
- padding-left: 20px;
- padding-right: 20px;
- }
-
- .grid {
- display: grid;
- grid-gap: 10px;
- grid-template-columns: 1fr 1fr 2fr 1fr 1fr;
-
- .column-0 {
- grid-column: 1;
- grid-row: 1;
- min-width: 0;
- }
-
- .column-1 {
- grid-column: 2;
- grid-row: 1;
- min-width: 0;
- }
-
- .column-2 {
- grid-column: 3;
- grid-row: 1;
- min-width: 0;
- text-align: center;
-
- h4 a {
- color: lighten($ui-base-color, 34%);
- }
- }
-
- .column-3 {
- grid-column: 4;
- grid-row: 1;
- min-width: 0;
- }
-
- .column-4 {
- grid-column: 5;
- grid-row: 1;
- min-width: 0;
- }
-
- @media screen and (max-width: 690px) {
- grid-template-columns: 1fr 2fr 1fr;
-
- .column-0,
- .column-1 {
- grid-column: 1;
- }
-
- .column-1 {
- grid-row: 2;
- }
-
- .column-2 {
- grid-column: 2;
- }
-
- .column-3,
- .column-4 {
- grid-column: 3;
- }
-
- .column-4 {
- grid-row: 2;
- }
- }
-
- @media screen and (max-width: 600px) {
- .column-1 {
- display: block;
- }
- }
-
- @media screen and (max-width: $no-gap-breakpoint) {
- .column-0,
- .column-1,
- .column-3,
- .column-4 {
- display: none;
- }
-
- .column-2 h4 {
- display: none;
- }
- }
- }
-
- .legal-xs {
- display: none;
- text-align: center;
- padding-top: 20px;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- display: block;
- }
- }
-
- h4 {
- text-transform: uppercase;
- font-weight: 700;
- margin-bottom: 8px;
- color: $darker-text-color;
-
- a {
- color: inherit;
- text-decoration: none;
- }
- }
-
- ul a,
- .legal-xs a {
- text-decoration: none;
- color: lighten($ui-base-color, 34%);
-
- &:hover,
- &:active,
- &:focus {
- text-decoration: underline;
- }
- }
-
- .brand {
- .logo {
- display: block;
- height: 36px;
- width: auto;
- margin: 0 auto;
- color: lighten($ui-base-color, 34%);
- }
-
- &:hover,
- &:focus,
- &:active {
- .logo {
- color: lighten($ui-base-color, 38%);
- }
- }
- }
- }
-}
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 98eb1511c..ccec8e95e 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -53,16 +53,6 @@ body.rtl {
right: -26px;
}
- .landing-page__logo {
- margin-right: 0;
- margin-left: 20px;
- }
-
- .landing-page .features-list .features-list__row .visual {
- margin-left: 0;
- margin-right: 15px;
- }
-
.column-link__icon,
.column-header__icon {
margin-right: 0;
@@ -350,44 +340,6 @@ body.rtl {
margin-left: 45px;
}
- .landing-page .header-wrapper .mascot {
- right: 60px;
- left: auto;
- }
-
- .landing-page__call-to-action .row__information-board {
- direction: rtl;
- }
-
- .landing-page .header .hero .floats .float-1 {
- left: -120px;
- right: auto;
- }
-
- .landing-page .header .hero .floats .float-2 {
- left: 210px;
- right: auto;
- }
-
- .landing-page .header .hero .floats .float-3 {
- left: 110px;
- right: auto;
- }
-
- .landing-page .header .links .brand img {
- left: 0;
- }
-
- .landing-page .fa-external-link {
- padding-right: 5px;
- padding-left: 0 !important;
- }
-
- .landing-page .features #mastodon-timeline {
- margin-right: 0;
- margin-left: 30px;
- }
-
@media screen and (min-width: 631px) {
.column,
.drawer {
@@ -415,32 +367,6 @@ body.rtl {
padding-right: 0;
}
- .public-layout {
- .header {
- .nav-button {
- margin-left: 8px;
- margin-right: 0;
- }
- }
-
- .public-account-header__tabs {
- margin-left: 0;
- margin-right: 20px;
- }
- }
-
- .landing-page__information {
- .account__display-name {
- margin-right: 0;
- margin-left: 5px;
- }
-
- .account__avatar-wrapper {
- margin-left: 12px;
- margin-right: 0;
- }
- }
-
.card__bar .display-name {
margin-left: 0;
margin-right: 15px;
diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss
index a3237a630..ce71d11e4 100644
--- a/app/javascript/styles/mastodon/statuses.scss
+++ b/app/javascript/styles/mastodon/statuses.scss
@@ -137,8 +137,7 @@ a.button.logo-button {
justify-content: center;
}
-.embed,
-.public-layout {
+.embed {
.status__content[data-spoiler="folded"] {
.e-content {
display: none;
diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb
index 6d15f3963..cf1a37625 100644
--- a/app/lib/permalink_redirector.rb
+++ b/app/lib/permalink_redirector.rb
@@ -8,16 +8,14 @@ class PermalinkRedirector
end
def redirect_path
- if path_segments[0] == 'web'
- if path_segments[1].present? && path_segments[1].start_with?('@') && path_segments[2] =~ /\d/
- find_status_url_by_id(path_segments[2])
- elsif path_segments[1].present? && path_segments[1].start_with?('@')
- find_account_url_by_name(path_segments[1])
- elsif path_segments[1] == 'statuses' && path_segments[2] =~ /\d/
- find_status_url_by_id(path_segments[2])
- elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/
- find_account_url_by_id(path_segments[2])
- end
+ if path_segments[0].present? && path_segments[0].start_with?('@') && path_segments[1] =~ /\d/
+ find_status_url_by_id(path_segments[1])
+ elsif path_segments[0].present? && path_segments[0].start_with?('@')
+ find_account_url_by_name(path_segments[0])
+ elsif path_segments[0] == 'statuses' && path_segments[1] =~ /\d/
+ find_status_url_by_id(path_segments[1])
+ elsif path_segments[0] == 'accounts' && path_segments[1] =~ /\d/
+ find_account_url_by_id(path_segments[1])
end
end
@@ -29,18 +27,12 @@ class PermalinkRedirector
def find_status_url_by_id(id)
status = Status.find_by(id: id)
-
- return unless status&.distributable?
-
- ActivityPub::TagManager.instance.url_for(status)
+ ActivityPub::TagManager.instance.url_for(status) if status&.distributable? && !status.account.local?
end
def find_account_url_by_id(id)
account = Account.find_by(id: id)
-
- return unless account
-
- ActivityPub::TagManager.instance.url_for(account)
+ ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local?
end
def find_account_url_by_name(name)
@@ -48,12 +40,6 @@ class PermalinkRedirector
domain = nil if TagManager.instance.local_domain?(domain)
account = Account.find_remote(username, domain)
- return unless account
-
- ActivityPub::TagManager.instance.url_for(account)
- end
-
- def find_tag_url_by_name(name)
- tag_path(CGI.unescape(name))
+ ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local?
end
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 1be7b4d12..df7fa8d50 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -134,6 +134,7 @@ class Account < ApplicationRecord
:role,
:locale,
:shows_application?,
+ :prefers_noindex?,
to: :user,
prefix: true,
allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index 4767189a0..6d566b1c2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -281,6 +281,10 @@ class User < ApplicationRecord
save!
end
+ def prefers_noindex?
+ setting_noindex
+ end
+
def preferred_posting_language
valid_locale_cascade(settings.default_language, locale, I18n.locale)
end
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index c52a89d87..e521dacaa 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -14,6 +14,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
attribute :suspended, if: :suspended?
attribute :silenced, key: :limited, if: :silenced?
+ attribute :noindex, if: :local?
class FieldSerializer < ActiveModel::Serializer
include FormattingHelper
@@ -103,7 +104,11 @@ class REST::AccountSerializer < ActiveModel::Serializer
object.silenced?
end
- delegate :suspended?, :silenced?, to: :object
+ def noindex
+ object.user_prefers_noindex?
+ end
+
+ delegate :suspended?, :silenced?, :local?, to: :object
def moved_and_not_nested?
object.moved? && object.moved_to_account.moved_to_account_id.nil?
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index aff28b9a9..05d8989ad 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -1,4 +1,7 @@
- content_for :page_title do
= t('about.title')
+- content_for :header_tags do
+ = render partial: 'shared/og'
+
= render partial: 'shared/web_app'
diff --git a/app/views/accounts/_bio.html.haml b/app/views/accounts/_bio.html.haml
deleted file mode 100644
index e2539b1d4..000000000
--- a/app/views/accounts/_bio.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- fields = account.fields
-
-.public-account-bio
- - unless fields.empty?
- .account__header__fields
- - fields.each do |field|
- %dl
- %dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis)
- %dd{ title: field.value, class: custom_field_classes(field) }
- - if field.verified?
- %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
- = fa_icon 'check'
- = prerender_custom_emojis(account_field_value_format(field), account.emojis)
-
- = account_badge(account)
-
- - if account.note.present?
- .account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis)
-
- .public-account-bio__extra
- = t 'accounts.joined', date: l(account.created_at, format: :month)
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
deleted file mode 100644
index d9966723a..000000000
--- a/app/views/accounts/_header.html.haml
+++ /dev/null
@@ -1,43 +0,0 @@
-.public-account-header{:class => ("inactive" if account.moved?)}
- .public-account-header__image
- = image_tag (prefers_autoplay? ? account.header_original_url : account.header_static_url), class: 'parallax'
- .public-account-header__bar
- = link_to short_account_url(account), class: 'avatar' do
- = image_tag (prefers_autoplay? ? account.avatar_original_url : account.avatar_static_url), id: 'profile_page_avatar', data: { original: full_asset_url(account.avatar_original_url), static: full_asset_url(account.avatar_static_url), autoplay: prefers_autoplay? }
- .public-account-header__tabs
- .public-account-header__tabs__name
- %h1
- = display_name(account, custom_emojify: true)
- %small
- = acct(account)
- = fa_icon('lock') if account.locked?
- .public-account-header__tabs__tabs
- .details-counters
- .counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) }
- = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
- %span.counter-number= friendly_number_to_human account.statuses_count
- %span.counter-label= t('accounts.posts', count: account.statuses_count)
-
- .counter{ class: active_nav_class(account_following_index_url(account)) }
- = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
- %span.counter-number= friendly_number_to_human account.following_count
- %span.counter-label= t('accounts.following', count: account.following_count)
-
- .counter{ class: active_nav_class(account_followers_url(account)) }
- = link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do
- %span.counter-number= friendly_number_to_human account.followers_count
- %span.counter-label= t('accounts.followers', count: account.followers_count)
- .spacer
- .public-account-header__tabs__tabs__buttons
- = account_action_button(account)
-
- .public-account-header__extra
- = render 'accounts/bio', account: account
-
- .public-account-header__extra__links
- = link_to account_following_index_url(account) do
- %strong= friendly_number_to_human account.following_count
- = t('accounts.following', count: account.following_count)
- = link_to account_followers_url(account) do
- %strong= friendly_number_to_human account.followers_count
- = t('accounts.followers', count: account.followers_count)
diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml
deleted file mode 100644
index 2f46e0dd0..000000000
--- a/app/views/accounts/_moved.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- moved_to_account = account.moved_to_account
-
-.moved-account-widget
- .moved-account-widget__message
- = fa_icon 'suitcase'
- = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.pretty_acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention'))
-
- .moved-account-widget__card
- = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
- .detailed-status__display-avatar
- .account__avatar-overlay
- .account__avatar-overlay-base
- = image_tag moved_to_account.avatar_static_url
- .account__avatar-overlay-overlay
- = image_tag account.avatar_static_url
-
- %span.display-name
- %bdi
- %strong.emojify= display_name(moved_to_account, custom_emojify: true)
- %span @#{moved_to_account.pretty_acct}
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 7fa688bd3..a51dcd7be 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -2,85 +2,13 @@
= "#{display_name(@account)} (#{acct(@account)})"
- content_for :header_tags do
- - if @account.user&.setting_noindex
+ - if @account.user_prefers_noindex?
%meta{ name: 'robots', content: 'noindex, noarchive' }/
%link{ rel: 'alternate', type: 'application/rss+xml', href: @rss_url }/
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
- - if @older_url
- %link{ rel: 'next', href: @older_url }/
- - if @newer_url
- %link{ rel: 'prev', href: @newer_url }/
-
= opengraph 'og:type', 'profile'
= render 'og', account: @account, url: short_account_url(@account, only_path: false)
-
-= render 'header', account: @account, with_bio: true
-
-.grid
- .column-0
- .h-feed
- %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
-
- .account__section-headline
- = active_link_to t('accounts.posts_tab_heading'), short_account_url(@account)
- = active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account)
- = active_link_to t('accounts.media'), short_account_media_url(@account)
-
- - if user_signed_in? && @account.blocking?(current_account)
- .nothing-here.nothing-here--under-tabs= t('accounts.unavailable')
- - elsif @statuses.empty?
- = nothing_here 'nothing-here--under-tabs'
- - else
- .activity-stream.activity-stream--under-tabs
- - if params[:page].to_i.zero?
- = render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true }
-
- - if @newer_url
- .entry= link_to_newer @newer_url
-
- = render partial: 'statuses/status', collection: @statuses, as: :status
-
- - if @older_url
- .entry= link_to_older @older_url
-
- .column-1
- - if @account.memorial?
- .memoriam-widget= t('in_memoriam_html')
- - elsif @account.moved?
- = render 'moved', account: @account
-
- = render 'bio', account: @account
-
- - if @endorsed_accounts.empty? && @account.id == current_account&.id
- .placeholder-widget= t('accounts.endorsements_hint')
- - elsif !@endorsed_accounts.empty?
- .endorsements-widget
- %h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true))
-
- - @endorsed_accounts.each do |account|
- = account_link_to account
-
- - if @featured_hashtags.empty? && @account.id == current_account&.id
- .placeholder-widget
- = t('accounts.featured_tags_hint')
- = link_to settings_featured_tags_path do
- = t('featured_tags.add_new')
- = fa_icon 'chevron-right fw'
- - else
- - @featured_hashtags.each do |featured_tag|
- .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
- = link_to short_account_tag_path(@account, featured_tag.tag) do
- %h4
- = fa_icon 'hashtag'
- = featured_tag.display_name
- %small
- - if featured_tag.last_status_at.nil?
- = t('accounts.nothing_here')
- - else
- %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
- .trends__item__current= friendly_number_to_human featured_tag.statuses_count
-
- = render 'application/sidebar'
+= render partial: 'shared/web_app'
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index 92de35a9f..d93540c02 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -1,20 +1,6 @@
-- content_for :page_title do
- = t('accounts.people_who_follow', name: display_name(@account))
-
- content_for :header_tags do
%meta{ name: 'robots', content: 'noindex' }/
- = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
-= render 'accounts/header', account: @account
-
-- if @account.hide_collections?
- .nothing-here= t('accounts.network_hidden')
-- elsif user_signed_in? && @account.blocking?(current_account)
- .nothing-here= t('accounts.unavailable')
-- elsif @follows.empty?
- = nothing_here
-- else
- .card-grid
- = render partial: 'application/card', collection: @follows.map(&:account), as: :account
+ = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
- = paginate @follows
+= render 'shared/web_app'
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 9bb1a9edd..d93540c02 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -1,20 +1,6 @@
-- content_for :page_title do
- = t('accounts.people_followed_by', name: display_name(@account))
-
- content_for :header_tags do
%meta{ name: 'robots', content: 'noindex' }/
- = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
-= render 'accounts/header', account: @account
-
-- if @account.hide_collections?
- .nothing-here= t('accounts.network_hidden')
-- elsif user_signed_in? && @account.blocking?(current_account)
- .nothing-here= t('accounts.unavailable')
-- elsif @follows.empty?
- = nothing_here
-- else
- .card-grid
- = render partial: 'application/card', collection: @follows.map(&:target_account), as: :account
+ = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
- = paginate @follows
+= render 'shared/web_app'
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 76a02e0f0..45990cd10 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,4 +1,7 @@
- content_for :header_tags do
+ - unless request.path == '/'
+ %meta{ name: 'robots', content: 'noindex' }/
+
= render partial: 'shared/og'
= render 'shared/web_app'
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
deleted file mode 100644
index 9b9e725e9..000000000
--- a/app/views/layouts/public.html.haml
+++ /dev/null
@@ -1,60 +0,0 @@
-- content_for :header_tags do
- = render_initial_state
- = javascript_pack_tag 'public', crossorigin: 'anonymous'
-
-- content_for :content do
- .public-layout
- - unless @hide_navbar
- .container
- %nav.header
- .nav-left
- = link_to root_url, class: 'brand' do
- = logo_as_symbol(:wordmark)
-
- - unless whitelist_mode?
- = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
- = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
-
- .nav-center
-
- .nav-right
- - if user_signed_in?
- = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
- - else
- = link_to_login t('auth.login'), class: 'webapp-btn nav-link nav-button'
- = link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button'
-
- .container= yield
-
- .container
- .footer
- .grid
- .column-0
- %h4= t 'footer.resources'
- %ul
- %li= link_to t('about.privacy_policy'), privacy_policy_path
- .column-1
- %h4= t 'footer.developers'
- %ul
- %li= link_to t('about.documentation'), 'https://docs.joinmastodon.org/'
- %li= link_to t('about.api'), 'https://docs.joinmastodon.org/client/intro/'
- .column-2
- %h4= link_to t('about.what_is_mastodon'), 'https://joinmastodon.org/'
- = link_to logo_as_symbol, root_url, class: 'brand'
- .column-3
- %h4= site_hostname
- %ul
- - unless whitelist_mode?
- %li= link_to t('about.about_this'), about_more_path
- %li= "v#{Mastodon::Version.to_s}"
- .column-4
- %h4= t 'footer.more'
- %ul
- %li= link_to t('about.source_code'), Mastodon::Version.source_url
- %li= link_to t('about.apps'), 'https://joinmastodon.org/apps'
- .legal-xs
- = link_to "v#{Mastodon::Version.to_s}", Mastodon::Version.source_url
- ·
- = link_to t('about.privacy_policy'), privacy_policy_path
-
-= render template: 'layouts/application'
diff --git a/app/views/privacy/show.html.haml b/app/views/privacy/show.html.haml
index cfc285925..95e506641 100644
--- a/app/views/privacy/show.html.haml
+++ b/app/views/privacy/show.html.haml
@@ -1,4 +1,7 @@
- content_for :page_title do
= t('privacy_policy.title')
+- content_for :header_tags do
+ = render partial: 'shared/og'
+
= render 'shared/web_app'
diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml
deleted file mode 100644
index 4e9601f6a..000000000
--- a/app/views/remote_follow/new.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- content_for :header_tags do
- %meta{ name: 'robots', content: 'noindex' }/
-
-.form-container
- .follow-prompt
- %h2= t('remote_follow.prompt')
-
- = render partial: 'application/card', locals: { account: @account }
-
- = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f|
- = render 'shared/error_messages', object: @remote_follow
-
- = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' }
-
- .actions
- = f.button :button, t('remote_follow.proceed'), type: :submit
-
- %p.hint.subtle-hint
- = t('remote_follow.reason_html', instance: site_hostname)
- = t('remote_follow.no_account_html', sign_up_path: available_sign_up_path)
diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml
deleted file mode 100644
index 2cc0fcb93..000000000
--- a/app/views/remote_interaction/new.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header_tags do
- %meta{ name: 'robots', content: 'noindex' }/
-
-.form-container
- .follow-prompt
- %h2= t("remote_interaction.#{@interaction_type}.prompt")
-
- .public-layout
- .activity-stream.activity-stream--highlighted
- = render 'statuses/status', status: @status
-
- = simple_form_for @remote_follow, as: :remote_follow, url: remote_interaction_path(@status) do |f|
- = render 'shared/error_messages', object: @remote_follow
-
- = hidden_field_tag :type, @interaction_type
-
- = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' }
-
- .actions
- = f.button :button, t("remote_interaction.#{@interaction_type}.proceed"), type: :submit
-
- %p.hint.subtle-hint
- = t('remote_follow.reason_html', instance: site_hostname)
- = t('remote_follow.no_account_html', sign_up_path: available_sign_up_path)
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index c67f0e4d9..37001b022 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -56,7 +56,7 @@
- else
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
·
- = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
+ %span.detailed-status__link
- if status.in_reply_to_id.nil?
= fa_icon('reply')
- else
@@ -65,12 +65,12 @@
= " "
·
- if status.public_visibility? || status.unlisted_visibility?
- = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
+ %span.detailed-status__link
= fa_icon('retweet')
%span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
= " "
·
- = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
+ %span.detailed-status__link
= fa_icon('star')
%span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
= " "
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index f16d2c186..bfde3a260 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -53,18 +53,18 @@
= t 'statuses.show_thread'
.status__action-bar
- = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button icon-button--with-counter modal-button' do
+ %span.status__action-bar-button.icon-button.icon-button--with-counter
- if status.in_reply_to_id.nil?
= fa_icon 'reply fw'
- else
= fa_icon 'reply-all fw'
%span.icon-button__counter= obscured_counter status.replies_count
- = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
+ %span.status__action-bar-button.icon-button
- if status.distributable?
= fa_icon 'retweet fw'
- elsif status.private_visibility? || status.limited_visibility?
= fa_icon 'lock fw'
- else
= fa_icon 'at fw'
- = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
+ %span.status__action-bar-button.icon-button
= fa_icon 'star fw'
diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml
index 5a3c94b84..106c41725 100644
--- a/app/views/statuses/show.html.haml
+++ b/app/views/statuses/show.html.haml
@@ -2,7 +2,7 @@
= t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false))
- content_for :header_tags do
- - if @account.user&.setting_noindex
+ - if @account.user_prefers_noindex?
%meta{ name: 'robots', content: 'noindex, noarchive' }/
%link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: short_account_status_url(@account, @status), format: 'json') }/
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
new file mode 100644
index 000000000..4b4967a8f
--- /dev/null
+++ b/app/views/tags/show.html.haml
@@ -0,0 +1,5 @@
+- content_for :header_tags do
+ %meta{ name: 'robots', content: 'noindex' }/
+ = render partial: 'shared/og'
+
+= render partial: 'shared/web_app'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 504f1b364..412178ca3 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -2,47 +2,26 @@
en:
about:
about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!'
- api: API
- apps: Mobile apps
contact_missing: Not set
contact_unavailable: N/A
- documentation: Documentation
hosted_on: Mastodon hosted on %{domain}
- privacy_policy: Privacy Policy
- source_code: Source code
title: About
- what_is_mastodon: What is Mastodon?
accounts:
- choices_html: "%{name}'s choices:"
- endorsements_hint: You can endorse people you follow from the web interface, and they will show up here.
- featured_tags_hint: You can feature specific hashtags that will be displayed here.
follow: Follow
followers:
one: Follower
other: Followers
following: Following
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 suspended.
- joined: Joined %{date}
last_active: last active
link_verified_on: Ownership of this link was checked on %{date}
- media: Media
- moved_html: "%{name} has moved to %{new_profile_link}:"
- network_hidden: This information is not available
nothing_here: There is nothing here!
- people_followed_by: People whom %{name} follows
- people_who_follow: People who follow %{name}
pin_errors:
following: You must be already following the person you want to endorse
posts:
one: Post
other: Posts
posts_tab_heading: Posts
- posts_with_replies: Posts and replies
- roles:
- bot: Bot
- group: Group
- unavailable: Profile unavailable
- unfollow: Unfollow
admin:
account_actions:
action: Perform action
@@ -1176,9 +1155,6 @@ en:
hint: This filter applies to select individual posts regardless of other criteria. You can add more posts to this filter from the web interface.
title: Filtered posts
footer:
- developers: Developers
- more: More…
- resources: Resources
trending_now: Trending now
generic:
all: All
@@ -1221,7 +1197,6 @@ en:
following: Following list
muting: Muting list
upload: Upload
- in_memoriam_html: In Memoriam.
invites:
delete: Deactivate
expired: Expired
@@ -1402,22 +1377,7 @@ en:
remove_selected_follows: Unfollow selected users
status: Account status
remote_follow:
- acct: Enter your username@domain you want to act from
missing_resource: Could not find the required redirect URL for your account
- no_account_html: Don't have an account? You can sign up here
- proceed: Proceed to follow
- prompt: 'You are going to follow:'
- reason_html: "Why is this step necessary? %{instance}
might not be the server where you are registered, so we need to redirect you to your home server first."
- remote_interaction:
- favourite:
- proceed: Proceed to favourite
- prompt: 'You want to favourite this post:'
- reblog:
- proceed: Proceed to boost
- prompt: 'You want to boost this post:'
- reply:
- proceed: Proceed to reply
- prompt: 'You want to reply to this post:'
reports:
errors:
invalid_rules: does not reference valid rules
diff --git a/config/routes.rb b/config/routes.rb
index 29ec0f8a5..1ed585f19 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,31 @@
require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web'
+# Paths of routes on the web app that to not require to be indexed or
+# have alternative format representations requiring separate controllers
+WEB_APP_PATHS = %w(
+ /getting-started
+ /keyboard-shortcuts
+ /home
+ /public
+ /public/local
+ /conversations
+ /lists/(*any)
+ /notifications
+ /favourites
+ /bookmarks
+ /pinned
+ /start
+ /directory
+ /explore/(*any)
+ /search
+ /publish
+ /follow_requests
+ /blocks
+ /domain_blocks
+ /mutes
+).freeze
+
Rails.application.routes.draw do
root 'home#index'
@@ -59,9 +84,6 @@ Rails.application.routes.draw do
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
resources :accounts, path: 'users', only: [:show], param: :username do
- get :remote_follow, to: 'remote_follow#new'
- post :remote_follow, to: 'remote_follow#create'
-
resources :statuses, only: [:show] do
member do
get :activity
@@ -85,16 +107,21 @@ Rails.application.routes.draw do
resource :inbox, only: [:create], module: :activitypub
- get '/@:username', to: 'accounts#show', as: :short_account
- get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
- get '/@:username/media', to: 'accounts#show', as: :short_account_media
- get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
- get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
- get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
+ constraints(username: /[^@\/.]+/) do
+ get '/@:username', to: 'accounts#show', as: :short_account
+ get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
+ get '/@:username/media', to: 'accounts#show', as: :short_account_media
+ get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
+ end
- get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction
- post '/interact/:id', to: 'remote_interaction#create'
+ constraints(account_username: /[^@\/.]+/) do
+ get '/@:account_username/following', to: 'following_accounts#index'
+ get '/@:account_username/followers', to: 'follower_accounts#index'
+ get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
+ get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
+ end
+ get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false
get '/settings', to: redirect('/settings/profile')
namespace :settings do
@@ -187,9 +214,6 @@ Rails.application.routes.draw do
resource :relationships, only: [:show, :update]
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]
- get '/explore', to: redirect('/web/explore')
- get '/public', to: redirect('/web/public')
- get '/public/local', to: redirect('/web/public/local')
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy
resource :authorize_interaction, only: [:show, :create]
@@ -642,8 +666,11 @@ Rails.application.routes.draw do
end
end
- get '/web/(*any)', to: 'home#index', as: :web
+ WEB_APP_PATHS.each do |path|
+ get path, to: 'home#index'
+ end
+ get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web
get '/about', to: 'about#show'
get '/about/more', to: redirect('/about')
diff --git a/package.json b/package.json
index 5d8f20abf..0a57336d6 100644
--- a/package.json
+++ b/package.json
@@ -115,7 +115,6 @@
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.4.1",
"regenerator-runtime": "^0.13.9",
- "rellax": "^1.12.1",
"requestidlecallback": "^0.3.0",
"reselect": "^4.1.6",
"rimraf": "^3.0.2",
diff --git a/spec/controllers/account_follow_controller_spec.rb b/spec/controllers/account_follow_controller_spec.rb
deleted file mode 100644
index d33cd0499..000000000
--- a/spec/controllers/account_follow_controller_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'rails_helper'
-
-describe AccountFollowController do
- render_views
-
- let(:user) { Fabricate(:user) }
- let(:alice) { Fabricate(:account, username: 'alice') }
-
- describe 'POST #create' do
- let(:service) { double }
-
- subject { post :create, params: { account_username: alice.username } }
-
- before do
- allow(FollowService).to receive(:new).and_return(service)
- allow(service).to receive(:call)
- end
-
- context 'when account is permanently suspended' do
- before do
- alice.suspend!
- alice.deletion_request.destroy
- subject
- end
-
- it 'returns http gone' do
- expect(response).to have_http_status(410)
- end
- end
-
- context 'when account is temporarily suspended' do
- before do
- alice.suspend!
- subject
- end
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- context 'when signed out' do
- before do
- subject
- end
-
- it 'does not follow' do
- expect(FollowService).not_to receive(:new)
- end
- end
-
- context 'when signed in' do
- before do
- sign_in(user)
- subject
- end
-
- it 'redirects to account path' do
- expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true)
- expect(response).to redirect_to(account_path(alice))
- end
- end
- end
-end
diff --git a/spec/controllers/account_unfollow_controller_spec.rb b/spec/controllers/account_unfollow_controller_spec.rb
deleted file mode 100644
index a11f7aa68..000000000
--- a/spec/controllers/account_unfollow_controller_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'rails_helper'
-
-describe AccountUnfollowController do
- render_views
-
- let(:user) { Fabricate(:user) }
- let(:alice) { Fabricate(:account, username: 'alice') }
-
- describe 'POST #create' do
- let(:service) { double }
-
- subject { post :create, params: { account_username: alice.username } }
-
- before do
- allow(UnfollowService).to receive(:new).and_return(service)
- allow(service).to receive(:call)
- end
-
- context 'when account is permanently suspended' do
- before do
- alice.suspend!
- alice.deletion_request.destroy
- subject
- end
-
- it 'returns http gone' do
- expect(response).to have_http_status(410)
- end
- end
-
- context 'when account is temporarily suspended' do
- before do
- alice.suspend!
- subject
- end
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- context 'when signed out' do
- before do
- subject
- end
-
- it 'does not unfollow' do
- expect(UnfollowService).not_to receive(:new)
- end
- end
-
- context 'when signed in' do
- before do
- sign_in(user)
- subject
- end
-
- it 'redirects to account path' do
- expect(service).to have_received(:call).with(user.account, alice)
- expect(response).to redirect_to(account_path(alice))
- end
- end
- end
-end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 12266c800..defa8b2d3 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -99,100 +99,6 @@ RSpec.describe AccountsController, type: :controller do
end
it_behaves_like 'common response characteristics'
-
- it 'renders public status' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
- end
-
- it 'renders self-reply' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
- end
-
- it 'renders status with media' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
- end
-
- it 'renders reblog' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
- end
-
- it 'renders pinned status' do
- expect(response.body).to include(I18n.t('stream_entries.pinned'))
- end
-
- it 'does not render private status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
- end
-
- it 'does not render direct status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
- end
-
- it 'does not render reply to someone else' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
- end
- end
-
- context 'when signed-in' do
- let(:user) { Fabricate(:user) }
-
- before do
- sign_in(user)
- end
-
- context 'when user follows account' do
- before do
- user.account.follow!(account)
- get :show, params: { username: account.username, format: format }
- end
-
- it 'does not render private status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
- end
- end
-
- context 'when user is blocked' do
- before do
- account.block!(user.account)
- get :show, params: { username: account.username, format: format }
- end
-
- it 'renders unavailable message' do
- expect(response.body).to include(I18n.t('accounts.unavailable'))
- end
-
- it 'does not render public status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
- end
-
- it 'does not render self-reply' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
- end
-
- it 'does not render status with media' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
- end
-
- it 'does not render reblog' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
- end
-
- it 'does not render pinned status' do
- expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
- end
-
- it 'does not render private status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
- end
-
- it 'does not render direct status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
- end
-
- it 'does not render reply to someone else' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
- end
- end
end
context 'with replies' do
@@ -202,38 +108,6 @@ RSpec.describe AccountsController, type: :controller do
end
it_behaves_like 'common response characteristics'
-
- it 'renders public status' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
- end
-
- it 'renders self-reply' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
- end
-
- it 'renders status with media' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
- end
-
- it 'renders reblog' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
- end
-
- it 'does not render pinned status' do
- expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
- end
-
- it 'does not render private status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
- end
-
- it 'does not render direct status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
- end
-
- it 'renders reply to someone else' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply))
- end
end
context 'with media' do
@@ -243,38 +117,6 @@ RSpec.describe AccountsController, type: :controller do
end
it_behaves_like 'common response characteristics'
-
- it 'does not render public status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
- end
-
- it 'does not render self-reply' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
- end
-
- it 'renders status with media' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
- end
-
- it 'does not render reblog' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
- end
-
- it 'does not render pinned status' do
- expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
- end
-
- it 'does not render private status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
- end
-
- it 'does not render direct status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
- end
-
- it 'does not render reply to someone else' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
- end
end
context 'with tag' do
@@ -289,42 +131,6 @@ RSpec.describe AccountsController, type: :controller do
end
it_behaves_like 'common response characteristics'
-
- it 'does not render public status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
- end
-
- it 'does not render self-reply' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
- end
-
- it 'does not render status with media' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
- end
-
- it 'does not render reblog' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
- end
-
- it 'does not render pinned status' do
- expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
- end
-
- it 'does not render private status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
- end
-
- it 'does not render direct status' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
- end
-
- it 'does not render reply to someone else' do
- expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
- end
-
- it 'renders status with tag' do
- expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag))
- end
end
end
diff --git a/spec/controllers/authorize_interactions_controller_spec.rb b/spec/controllers/authorize_interactions_controller_spec.rb
index 99f3f6ffc..44f52df69 100644
--- a/spec/controllers/authorize_interactions_controller_spec.rb
+++ b/spec/controllers/authorize_interactions_controller_spec.rb
@@ -39,7 +39,7 @@ describe AuthorizeInteractionsController do
end
it 'sets resource from url' do
- account = Account.new
+ account = Fabricate(:account)
service = double
allow(ResolveURLService).to receive(:new).and_return(service)
allow(service).to receive(:call).with('http://example.com').and_return(account)
@@ -51,7 +51,7 @@ describe AuthorizeInteractionsController do
end
it 'sets resource from acct uri' do
- account = Account.new
+ account = Fabricate(:account)
service = double
allow(ResolveAccountService).to receive(:new).and_return(service)
allow(service).to receive(:call).with('found@hostname').and_return(account)
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index 4d2a6e01a..ab2e82e85 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -34,27 +34,6 @@ describe FollowerAccountsController do
expect(response).to have_http_status(403)
end
end
-
- it 'assigns follows' do
- expect(response).to have_http_status(200)
-
- assigned = assigns(:follows).to_a
- expect(assigned.size).to eq 2
- expect(assigned[0]).to eq follow1
- expect(assigned[1]).to eq follow0
- end
-
- it 'does not assign blocked users' do
- user = Fabricate(:user)
- user.account.block!(follower0)
- sign_in(user)
-
- expect(response).to have_http_status(200)
-
- assigned = assigns(:follows).to_a
- expect(assigned.size).to eq 1
- expect(assigned[0]).to eq follow1
- end
end
context 'when format is json' do
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index bb6d221ca..e43dbf882 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -34,27 +34,6 @@ describe FollowingAccountsController do
expect(response).to have_http_status(403)
end
end
-
- it 'assigns follows' do
- expect(response).to have_http_status(200)
-
- assigned = assigns(:follows).to_a
- expect(assigned.size).to eq 2
- expect(assigned[0]).to eq follow1
- expect(assigned[1]).to eq follow0
- end
-
- it 'does not assign blocked users' do
- user = Fabricate(:user)
- user.account.block!(followee0)
- sign_in(user)
-
- expect(response).to have_http_status(200)
-
- assigned = assigns(:follows).to_a
- expect(assigned.size).to eq 1
- expect(assigned[0]).to eq follow1
- end
end
context 'when format is json' do
diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb
deleted file mode 100644
index 01d43f48c..000000000
--- a/spec/controllers/remote_follow_controller_spec.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe RemoteFollowController do
- render_views
-
- describe '#new' do
- it 'returns success when session is empty' do
- account = Fabricate(:account)
- get :new, params: { account_username: account.to_param }
-
- expect(response).to have_http_status(200)
- expect(response).to render_template(:new)
- expect(assigns(:remote_follow).acct).to be_nil
- end
-
- it 'populates the remote follow with session data when session exists' do
- session[:remote_follow] = 'user@example.com'
- account = Fabricate(:account)
- get :new, params: { account_username: account.to_param }
-
- expect(response).to have_http_status(200)
- expect(response).to render_template(:new)
- expect(assigns(:remote_follow).acct).to eq 'user@example.com'
- end
- end
-
- describe '#create' do
- before do
- @account = Fabricate(:account, username: 'test_user')
- end
-
- context 'with a valid acct' do
- context 'when webfinger values are wrong' do
- it 'renders new when redirect url is nil' do
- resource_with_nil_link = double(link: nil)
- allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_nil_link)
- post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
-
- expect(response).to render_template(:new)
- expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
- end
-
- it 'renders new when template is nil' do
- resource_with_link = double(link: nil)
- allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
- post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
-
- expect(response).to render_template(:new)
- expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
- end
- end
-
- context 'when webfinger values are good' do
- before do
- resource_with_link = double(link: 'http://example.com/follow_me?acct={uri}')
- allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
- post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
- end
-
- it 'saves the session' do
- expect(session[:remote_follow]).to eq 'user@example.com'
- end
-
- it 'redirects to the remote location' do
- expect(response).to redirect_to("http://example.com/follow_me?acct=https%3A%2F%2F#{Rails.configuration.x.local_domain}%2Fusers%2Ftest_user")
- end
- end
- end
-
- context 'with an invalid acct' do
- it 'renders new when acct is missing' do
- post :create, params: { account_username: @account.to_param, remote_follow: { acct: '' } }
-
- expect(response).to render_template(:new)
- end
-
- it 'renders new with error when webfinger fails' do
- allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Webfinger::Error)
- post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
-
- expect(response).to render_template(:new)
- expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
- end
-
- it 'renders new when occur HTTP::ConnectionError' do
- allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@unknown').and_raise(HTTP::ConnectionError)
- post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } }
-
- expect(response).to render_template(:new)
- expect(response.body).to include(I18n.t('remote_follow.missing_resource'))
- end
- end
- end
-
- context 'with a permanently suspended account' do
- before do
- @account = Fabricate(:account)
- @account.suspend!
- @account.deletion_request.destroy
- end
-
- it 'returns http gone on GET to #new' do
- get :new, params: { account_username: @account.to_param }
-
- expect(response).to have_http_status(410)
- end
-
- it 'returns http gone on POST to #create' do
- post :create, params: { account_username: @account.to_param }
-
- expect(response).to have_http_status(410)
- end
- end
-
- context 'with a temporarily suspended account' do
- before do
- @account = Fabricate(:account)
- @account.suspend!
- end
-
- it 'returns http forbidden on GET to #new' do
- get :new, params: { account_username: @account.to_param }
-
- expect(response).to have_http_status(403)
- end
-
- it 'returns http forbidden on POST to #create' do
- post :create, params: { account_username: @account.to_param }
-
- expect(response).to have_http_status(403)
- end
- end
-end
diff --git a/spec/controllers/remote_interaction_controller_spec.rb b/spec/controllers/remote_interaction_controller_spec.rb
deleted file mode 100644
index bb0074b11..000000000
--- a/spec/controllers/remote_interaction_controller_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe RemoteInteractionController, type: :controller do
- render_views
-
- let(:status) { Fabricate(:status) }
-
- describe 'GET #new' do
- it 'returns 200' do
- get :new, params: { id: status.id }
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'POST #create' do
- context '@remote_follow is valid' do
- it 'returns 302' do
- allow_any_instance_of(RemoteFollow).to receive(:valid?) { true }
- allow_any_instance_of(RemoteFollow).to receive(:addressable_template) do
- Addressable::Template.new('https://hoge.com')
- end
-
- post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } }
- expect(response).to have_http_status(302)
- end
- end
-
- context '@remote_follow is invalid' do
- it 'returns 200' do
- allow_any_instance_of(RemoteFollow).to receive(:valid?) { false }
- post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } }
-
- expect(response).to have_http_status(200)
- end
- end
- end
-end
diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb
index 1fd8494d6..547bcfb39 100644
--- a/spec/controllers/tags_controller_spec.rb
+++ b/spec/controllers/tags_controller_spec.rb
@@ -10,16 +10,15 @@ RSpec.describe TagsController, type: :controller do
let!(:late) { Fabricate(:status, tags: [tag], text: 'late #test') }
context 'when tag exists' do
- it 'redirects to web version' do
+ it 'returns http success' do
get :show, params: { id: 'test', max_id: late.id }
- expect(response).to redirect_to('/web/tags/test')
+ expect(response).to have_http_status(200)
end
end
context 'when tag does not exist' do
- it 'returns http missing for non-existent tag' do
+ it 'returns http not found' do
get :show, params: { id: 'none' }
-
expect(response).to have_http_status(404)
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index b6de3e9d1..ec4f9a53f 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -18,36 +18,16 @@ feature 'Profile' do
visit account_path('alice')
is_expected.to have_title("alice (@alice@#{local_domain})")
-
- within('.public-account-header h1') do
- is_expected.to have_content("alice @alice@#{local_domain}")
- end
-
- bio_elem = first('.public-account-bio')
- expect(bio_elem).to have_content(alice_bio)
- # The bio has hashtags made clickable
- expect(bio_elem).to have_link('cryptology')
- expect(bio_elem).to have_link('science')
- # Nicknames are make clickable
- expect(bio_elem).to have_link('@alice')
- expect(bio_elem).to have_link('@bob')
- # Nicknames not on server are not clickable
- expect(bio_elem).not_to have_link('@pepe')
end
scenario 'I can change my account' do
visit settings_profile_path
+
fill_in 'Display name', with: 'Bob'
fill_in 'Bio', with: 'Bob is silent'
- first('.btn[type=submit]').click
- is_expected.to have_content 'Changes successfully saved!'
- # View my own public profile and see the changes
- click_link "Bob @bob@#{local_domain}"
+ first('button[type=submit]').click
- within('.public-account-header h1') do
- is_expected.to have_content("Bob @bob@#{local_domain}")
- end
- expect(first('.public-account-bio')).to have_content('Bob is silent')
+ is_expected.to have_content 'Changes successfully saved!'
end
end
diff --git a/spec/lib/permalink_redirector_spec.rb b/spec/lib/permalink_redirector_spec.rb
index abda57da4..a00913656 100644
--- a/spec/lib/permalink_redirector_spec.rb
+++ b/spec/lib/permalink_redirector_spec.rb
@@ -3,40 +3,31 @@
require 'rails_helper'
describe PermalinkRedirector do
+ let(:remote_account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://example.com/@alice', id: 2) }
+
describe '#redirect_url' do
before do
- account = Fabricate(:account, username: 'alice', id: 1)
- Fabricate(:status, account: account, id: 123)
+ Fabricate(:status, account: remote_account, id: 123, url: 'https://example.com/status-123')
end
it 'returns path for legacy account links' do
- redirector = described_class.new('web/accounts/1')
- expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice'
+ redirector = described_class.new('accounts/2')
+ expect(redirector.redirect_path).to eq 'https://example.com/@alice'
end
it 'returns path for legacy status links' do
- redirector = described_class.new('web/statuses/123')
- expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123'
- end
-
- it 'returns path for legacy tag links' do
- redirector = described_class.new('web/timelines/tag/hoge')
- expect(redirector.redirect_path).to be_nil
+ redirector = described_class.new('statuses/123')
+ expect(redirector.redirect_path).to eq 'https://example.com/status-123'
end
it 'returns path for pretty account links' do
- redirector = described_class.new('web/@alice')
- expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice'
+ redirector = described_class.new('@alice@example.com')
+ expect(redirector.redirect_path).to eq 'https://example.com/@alice'
end
it 'returns path for pretty status links' do
- redirector = described_class.new('web/@alice/123')
- expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123'
- end
-
- it 'returns path for pretty tag links' do
- redirector = described_class.new('web/tags/hoge')
- expect(redirector.redirect_path).to be_nil
+ redirector = described_class.new('@alice/123')
+ expect(redirector.redirect_path).to eq 'https://example.com/status-123'
end
end
end
diff --git a/spec/requests/account_show_page_spec.rb b/spec/requests/account_show_page_spec.rb
index 4e51cf7ef..e84c46c47 100644
--- a/spec/requests/account_show_page_spec.rb
+++ b/spec/requests/account_show_page_spec.rb
@@ -3,17 +3,6 @@
require 'rails_helper'
describe 'The account show page' do
- it 'Has an h-feed with correct number of h-entry objects in it' do
- alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
- _status = Fabricate(:status, account: alice, text: 'Hello World')
- _status2 = Fabricate(:status, account: alice, text: 'Hello World Again')
- _status3 = Fabricate(:status, account: alice, text: 'Are You Still There World?')
-
- get '/@alice'
-
- expect(h_feed_entries.size).to eq(3)
- end
-
it 'has valid opengraph tags' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
_status = Fabricate(:status, account: alice, text: 'Hello World')
@@ -33,8 +22,4 @@ describe 'The account show page' do
def head_section
Nokogiri::Slop(response.body).html.head
end
-
- def h_feed_entries
- Nokogiri::HTML(response.body).search('.h-feed .h-entry')
- end
end
diff --git a/spec/routing/accounts_routing_spec.rb b/spec/routing/accounts_routing_spec.rb
index d04cb27f0..3f0e9b3e9 100644
--- a/spec/routing/accounts_routing_spec.rb
+++ b/spec/routing/accounts_routing_spec.rb
@@ -1,31 +1,83 @@
require 'rails_helper'
describe 'Routes under accounts/' do
- describe 'the route for accounts who are followers of an account' do
- it 'routes to the followers action with the right username' do
- expect(get('/users/name/followers')).
- to route_to('follower_accounts#index', account_username: 'name')
+ context 'with local username' do
+ let(:username) { 'alice' }
+
+ it 'routes /@:username' do
+ expect(get("/@#{username}")).to route_to('accounts#show', username: username)
end
- end
- describe 'the route for accounts who are followed by an account' do
- it 'routes to the following action with the right username' do
- expect(get('/users/name/following')).
- to route_to('following_accounts#index', account_username: 'name')
+ it 'routes /@:username.json' do
+ expect(get("/@#{username}.json")).to route_to('accounts#show', username: username, format: 'json')
+ end
+
+ it 'routes /@:username.rss' do
+ expect(get("/@#{username}.rss")).to route_to('accounts#show', username: username, format: 'rss')
+ end
+
+ it 'routes /@:username/:id' do
+ expect(get("/@#{username}/123")).to route_to('statuses#show', account_username: username, id: '123')
+ end
+
+ it 'routes /@:username/:id/embed' do
+ expect(get("/@#{username}/123/embed")).to route_to('statuses#embed', account_username: username, id: '123')
+ end
+
+ it 'routes /@:username/following' do
+ expect(get("/@#{username}/following")).to route_to('following_accounts#index', account_username: username)
+ end
+
+ it 'routes /@:username/followers' do
+ expect(get("/@#{username}/followers")).to route_to('follower_accounts#index', account_username: username)
+ end
+
+ it 'routes /@:username/with_replies' do
+ expect(get("/@#{username}/with_replies")).to route_to('accounts#show', username: username)
+ end
+
+ it 'routes /@:username/media' do
+ expect(get("/@#{username}/media")).to route_to('accounts#show', username: username)
end
- end
- describe 'the route for following an account' do
- it 'routes to the follow create action with the right username' do
- expect(post('/users/name/follow')).
- to route_to('account_follow#create', account_username: 'name')
+ it 'routes /@:username/tagged/:tag' do
+ expect(get("/@#{username}/tagged/foo")).to route_to('accounts#show', username: username, tag: 'foo')
end
end
- describe 'the route for unfollowing an account' do
- it 'routes to the unfollow create action with the right username' do
- expect(post('/users/name/unfollow')).
- to route_to('account_unfollow#create', account_username: 'name')
+ context 'with remote username' do
+ let(:username) { 'alice@example.com' }
+
+ it 'routes /@:username' do
+ expect(get("/@#{username}")).to route_to('home#index', username_with_domain: username)
+ end
+
+ it 'routes /@:username/:id' do
+ expect(get("/@#{username}/123")).to route_to('home#index', username_with_domain: username, any: '123')
+ end
+
+ it 'routes /@:username/:id/embed' do
+ expect(get("/@#{username}/123/embed")).to route_to('home#index', username_with_domain: username, any: '123/embed')
+ end
+
+ it 'routes /@:username/following' do
+ expect(get("/@#{username}/following")).to route_to('home#index', username_with_domain: username, any: 'following')
+ end
+
+ it 'routes /@:username/followers' do
+ expect(get("/@#{username}/followers")).to route_to('home#index', username_with_domain: username, any: 'followers')
+ end
+
+ it 'routes /@:username/with_replies' do
+ expect(get("/@#{username}/with_replies")).to route_to('home#index', username_with_domain: username, any: 'with_replies')
+ end
+
+ it 'routes /@:username/media' do
+ expect(get("/@#{username}/media")).to route_to('home#index', username_with_domain: username, any: 'media')
+ end
+
+ it 'routes /@:username/tagged/:tag' do
+ expect(get("/@#{username}/tagged/foo")).to route_to('home#index', username_with_domain: username, any: 'tagged/foo')
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 6ae965464..98666f23d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9663,11 +9663,6 @@ regjsparser@^0.8.2:
dependencies:
jsesc "~0.5.0"
-rellax@^1.12.1:
- version "1.12.1"
- resolved "https://registry.yarnpkg.com/rellax/-/rellax-1.12.1.tgz#1b433ef7ac4aa3573449a33efab391c112f6b34d"
- integrity sha512-XBIi0CDpW5FLTujYjYBn1CIbK2CJL6TsAg/w409KghP2LucjjzBjsujXDAjyBLWgsfupfUcL5WzdnIPcGfK7XA==
-
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
--
cgit