From 50948b46aabc0756d85bc6641f0bd3bcc09bf7d4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Tue, 20 Sep 2022 23:51:21 +0200
Subject: Add ability to filter followed accounts' posts by language (#19095)
---
spec/controllers/api/v1/accounts_controller_spec.rb | 11 +++++++++++
.../settings/exports/following_accounts_controller_spec.rb | 2 +-
2 files changed, 12 insertions(+), 1 deletion(-)
(limited to 'spec/controllers')
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 5d5c245c5..d6bbcefd7 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -145,6 +145,17 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
expect(json[:showing_reblogs]).to be false
expect(json[:notifying]).to be true
end
+
+ it 'changes languages option' do
+ post :follow, params: { id: other_account.id, languages: %w(en es) }
+
+ json = body_as_json
+
+ expect(json[:following]).to be true
+ expect(json[:showing_reblogs]).to be false
+ expect(json[:notifying]).to be false
+ expect(json[:languages]).to match_array %w(en es)
+ end
end
end
diff --git a/spec/controllers/settings/exports/following_accounts_controller_spec.rb b/spec/controllers/settings/exports/following_accounts_controller_spec.rb
index 78858e772..bfe010555 100644
--- a/spec/controllers/settings/exports/following_accounts_controller_spec.rb
+++ b/spec/controllers/settings/exports/following_accounts_controller_spec.rb
@@ -11,7 +11,7 @@ describe Settings::Exports::FollowingAccountsController do
sign_in user, scope: :user
get :index, format: :csv
- expect(response.body).to eq "Account address,Show boosts\nusername@domain,true\n"
+ expect(response.body).to eq "Account address,Show boosts,Notify on new posts,Languages\nusername@domain,true,false,\n"
end
end
end
--
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 'spec/controllers')
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 43b5d5e38d2b8ad8f1d1ad0911c3c1718159c912 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Thu, 29 Sep 2022 04:39:33 +0200
Subject: Add logged-out access to the web UI (#18961)
---
app/controllers/home_controller.rb | 18 ++---
app/javascript/mastodon/actions/accounts.js | 6 +-
app/javascript/mastodon/actions/markers.js | 10 +--
app/javascript/mastodon/components/logo.js | 4 +-
app/javascript/mastodon/containers/mastodon.js | 2 +-
.../mastodon/features/account/components/header.js | 30 +++++---
.../mastodon/features/community_timeline/index.js | 6 ++
.../mastodon/features/directory/index.js | 6 ++
app/javascript/mastodon/features/explore/index.js | 6 ++
app/javascript/mastodon/features/explore/links.js | 11 +++
.../mastodon/features/explore/results.js | 18 ++++-
.../mastodon/features/explore/suggestions.js | 11 +++
app/javascript/mastodon/features/explore/tags.js | 11 +++
.../mastodon/features/hashtag_timeline/index.js | 18 ++++-
.../mastodon/features/public_timeline/index.js | 6 ++
app/javascript/mastodon/features/status/index.js | 24 ++++++-
.../features/ui/components/columns_area.js | 4 +-
.../features/ui/components/compose_panel.js | 22 +++++-
.../features/ui/components/document_title.js | 41 -----------
.../mastodon/features/ui/components/link_footer.js | 46 +++++++++---
.../features/ui/components/navigation_panel.js | 83 ++++++++++++++++------
.../features/ui/components/sign_in_banner.js | 11 +++
app/javascript/mastodon/features/ui/index.js | 43 ++++++++---
app/javascript/mastodon/initial_state.js | 2 +
app/javascript/styles/mastodon/_mixins.scss | 1 +
app/javascript/styles/mastodon/components.scss | 56 +++++++++++++--
app/lib/permalink_redirector.rb | 4 --
app/serializers/initial_state_serializer.rb | 11 ++-
app/views/home/index.html.haml | 12 ++--
package.json | 1 +
spec/controllers/home_controller_spec.rb | 20 ++----
spec/lib/permalink_redirector_spec.rb | 4 +-
yarn.lock | 20 ++++++
33 files changed, 423 insertions(+), 145 deletions(-)
delete mode 100644 app/javascript/mastodon/features/ui/components/document_title.js
create mode 100644 app/javascript/mastodon/features/ui/components/sign_in_banner.js
(limited to 'spec/controllers')
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 7e443eb9e..29478209d 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -2,8 +2,8 @@
class HomeController < ApplicationController
before_action :redirect_unauthenticated_to_permalinks!
- before_action :authenticate_user!
before_action :set_referrer_policy_header
+ before_action :set_instance_presenter
def index
@body_classes = 'app-body'
@@ -14,20 +14,16 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks!
return if user_signed_in?
- redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
- end
+ redirect_path = PermalinkRedirector.new(request.path).redirect_path
- def default_redirect_path
- if request.path.start_with?('/web') || whitelist_mode?
- new_user_session_path
- elsif single_user_mode?
- short_account_path(Account.local.without_suspended.where('id > 0').first)
- else
- about_path
- end
+ 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
end
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index eedf61dc9..f61f06e40 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -536,10 +536,12 @@ export function expandFollowingFail(id, error) {
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
- const loadedRelationships = getState().get('relationships');
+ const state = getState();
+ const loadedRelationships = state.get('relationships');
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+ const signedIn = !!state.getIn(['meta', 'me']);
- if (newAccountIds.length === 0) {
+ if (!signedIn || newAccountIds.length === 0) {
return;
}
diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js
index 16a3df8f6..b7f406cb8 100644
--- a/app/javascript/mastodon/actions/markers.js
+++ b/app/javascript/mastodon/actions/markers.js
@@ -1,6 +1,7 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
+import { List as ImmutableList } from 'immutable';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
@@ -11,7 +12,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
- if (Object.keys(params).length === 0) {
+ if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
@@ -63,7 +64,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const _buildParams = (state) => {
const params = {};
- const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
+ const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
@@ -82,9 +83,10 @@ const _buildParams = (state) => {
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
- const params = _buildParams(getState());
+ const accessToken = getState().getIn(['meta', 'access_token'], '');
+ const params = _buildParams(getState());
- if (Object.keys(params).length === 0) {
+ if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
diff --git a/app/javascript/mastodon/components/logo.js b/app/javascript/mastodon/components/logo.js
index d1c7f08a9..3570b3644 100644
--- a/app/javascript/mastodon/components/logo.js
+++ b/app/javascript/mastodon/components/logo.js
@@ -1,8 +1,8 @@
import React from 'react';
const Logo = () => (
-
+
{ 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
From 7c152acb2cc545a87610de349a94e14f45fbed5d Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Sat, 22 Oct 2022 11:44:41 +0200
Subject: Change settings area to be separated into categories in admin UI
(#19407)
And update all descriptions
---
app/controllers/admin/settings/about_controller.rb | 9 ++
.../admin/settings/appearance_controller.rb | 9 ++
.../admin/settings/branding_controller.rb | 9 ++
.../admin/settings/content_retention_controller.rb | 9 ++
.../admin/settings/discovery_controller.rb | 9 ++
.../admin/settings/registrations_controller.rb | 9 ++
app/controllers/admin/settings_controller.rb | 10 +-
app/controllers/admin/site_uploads_controller.rb | 2 +-
app/helpers/admin/settings_helper.rb | 7 --
app/javascript/styles/mastodon/admin.scss | 82 +++++++++++++----
app/javascript/styles/mastodon/components.scss | 5 +
app/javascript/styles/mastodon/forms.scss | 16 +++-
app/models/form/admin_settings.rb | 57 +++++++-----
.../rest/extended_description_serializer.rb | 12 ++-
app/views/admin/settings/about/show.html.haml | 33 +++++++
app/views/admin/settings/appearance/show.html.haml | 34 +++++++
app/views/admin/settings/branding/show.html.haml | 39 ++++++++
.../settings/content_retention/show.html.haml | 22 +++++
app/views/admin/settings/discovery/show.html.haml | 40 ++++++++
app/views/admin/settings/edit.html.haml | 102 ---------------------
.../admin/settings/registrations/show.html.haml | 27 ++++++
app/views/admin/settings/shared/_links.html.haml | 8 ++
app/views/layouts/admin.html.haml | 11 ++-
config/locales/en.yml | 87 +++++-------------
config/locales/simple_form.en.yml | 37 ++++++++
config/navigation.rb | 2 +-
config/routes.rb | 13 ++-
.../admin/settings/branding_controller_spec.rb | 53 +++++++++++
spec/controllers/admin/settings_controller_spec.rb | 71 --------------
29 files changed, 528 insertions(+), 296 deletions(-)
create mode 100644 app/controllers/admin/settings/about_controller.rb
create mode 100644 app/controllers/admin/settings/appearance_controller.rb
create mode 100644 app/controllers/admin/settings/branding_controller.rb
create mode 100644 app/controllers/admin/settings/content_retention_controller.rb
create mode 100644 app/controllers/admin/settings/discovery_controller.rb
create mode 100644 app/controllers/admin/settings/registrations_controller.rb
create mode 100644 app/views/admin/settings/about/show.html.haml
create mode 100644 app/views/admin/settings/appearance/show.html.haml
create mode 100644 app/views/admin/settings/branding/show.html.haml
create mode 100644 app/views/admin/settings/content_retention/show.html.haml
create mode 100644 app/views/admin/settings/discovery/show.html.haml
delete mode 100644 app/views/admin/settings/edit.html.haml
create mode 100644 app/views/admin/settings/registrations/show.html.haml
create mode 100644 app/views/admin/settings/shared/_links.html.haml
create mode 100644 spec/controllers/admin/settings/branding_controller_spec.rb
delete mode 100644 spec/controllers/admin/settings_controller_spec.rb
(limited to 'spec/controllers')
diff --git a/app/controllers/admin/settings/about_controller.rb b/app/controllers/admin/settings/about_controller.rb
new file mode 100644
index 000000000..86327fe39
--- /dev/null
+++ b/app/controllers/admin/settings/about_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::AboutController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_about_path
+ end
+end
diff --git a/app/controllers/admin/settings/appearance_controller.rb b/app/controllers/admin/settings/appearance_controller.rb
new file mode 100644
index 000000000..39b2448d8
--- /dev/null
+++ b/app/controllers/admin/settings/appearance_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::AppearanceController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_appearance_path
+ end
+end
diff --git a/app/controllers/admin/settings/branding_controller.rb b/app/controllers/admin/settings/branding_controller.rb
new file mode 100644
index 000000000..4a4d76f49
--- /dev/null
+++ b/app/controllers/admin/settings/branding_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::BrandingController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_branding_path
+ end
+end
diff --git a/app/controllers/admin/settings/content_retention_controller.rb b/app/controllers/admin/settings/content_retention_controller.rb
new file mode 100644
index 000000000..b88336a2c
--- /dev/null
+++ b/app/controllers/admin/settings/content_retention_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::ContentRetentionController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_content_retention_path
+ end
+end
diff --git a/app/controllers/admin/settings/discovery_controller.rb b/app/controllers/admin/settings/discovery_controller.rb
new file mode 100644
index 000000000..be4b57f79
--- /dev/null
+++ b/app/controllers/admin/settings/discovery_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::DiscoveryController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_discovery_path
+ end
+end
diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb
new file mode 100644
index 000000000..b4a74349c
--- /dev/null
+++ b/app/controllers/admin/settings/registrations_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::RegistrationsController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_registrations_path
+ end
+end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index dc1c79b7f..338a3638c 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -2,7 +2,7 @@
module Admin
class SettingsController < BaseController
- def edit
+ def show
authorize :settings, :show?
@admin_settings = Form::AdminSettings.new
@@ -15,14 +15,18 @@ module Admin
if @admin_settings.save
flash[:notice] = I18n.t('generic.changes_saved_msg')
- redirect_to edit_admin_settings_path
+ redirect_to after_update_redirect_path
else
- render :edit
+ render :show
end
end
private
+ def after_update_redirect_path
+ raise NotImplementedError
+ end
+
def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end
diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb
index cacecedb0..a5d2cf41c 100644
--- a/app/controllers/admin/site_uploads_controller.rb
+++ b/app/controllers/admin/site_uploads_controller.rb
@@ -9,7 +9,7 @@ module Admin
@site_upload.destroy!
- redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
+ redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end
private
diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb
index baf14ab25..a133b4e7d 100644
--- a/app/helpers/admin/settings_helper.rb
+++ b/app/helpers/admin/settings_helper.rb
@@ -1,11 +1,4 @@
# frozen_string_literal: true
module Admin::SettingsHelper
- def site_upload_delete_hint(hint, var)
- upload = SiteUpload.find_by(var: var.to_s)
- return hint unless upload
-
- link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
- safe_join([hint, link], '
'.html_safe)
- end
end
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 1c5494cde..affe1c79c 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -188,21 +188,70 @@ $content-width: 840px;
padding-top: 30px;
}
- &-heading {
- display: flex;
+ &__heading {
padding-bottom: 36px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
- margin: -15px -15px 40px 0;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
+ margin-bottom: 40px;
- & > * {
- margin-top: 15px;
- margin-right: 15px;
+ &__row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ margin: -15px -15px 0 0;
+
+ & > * {
+ margin-top: 15px;
+ margin-right: 15px;
+ }
}
- &-actions {
+ &__tabs {
+ margin-top: 30px;
+ margin-bottom: -31px;
+
+ & > div {
+ display: flex;
+ gap: 10px;
+ }
+
+ a {
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+ padding: 7px 15px;
+ border-radius: 4px;
+ color: $darker-text-color;
+ text-decoration: none;
+ position: relative;
+ font-weight: 500;
+ gap: 5px;
+ white-space: nowrap;
+
+ &.selected {
+ font-weight: 700;
+ color: $primary-text-color;
+
+ &::after {
+ content: "";
+ display: block;
+ width: 100%;
+ border-bottom: 1px solid $ui-highlight-color;
+ position: absolute;
+ bottom: -5px;
+ left: 0;
+ }
+ }
+
+ &:hover,
+ &:focus,
+ &:active {
+ background: lighten($ui-base-color, 4%);
+ }
+ }
+ }
+
+ &__actions {
display: inline-flex;
& > :not(:first-child) {
@@ -228,11 +277,7 @@ $content-width: 840px;
color: $secondary-text-color;
font-size: 24px;
line-height: 36px;
- font-weight: 400;
-
- @media screen and (max-width: $no-columns-breakpoint) {
- font-weight: 700;
- }
+ font-weight: 700;
}
h3 {
@@ -437,6 +482,11 @@ body,
}
}
+ & > div {
+ display: flex;
+ gap: 5px;
+ }
+
strong {
font-weight: 500;
text-transform: uppercase;
@@ -1143,7 +1193,7 @@ a.name-tag,
path:first-child {
fill: rgba($highlight-text-color, 0.25) !important;
- fill-opacity: 1 !important;
+ fill-opacity: 100% !important;
}
path:last-child {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 587eba663..5d0ff8536 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -29,6 +29,11 @@
background: transparent;
padding: 0;
cursor: pointer;
+ text-decoration: none;
+
+ &--destructive {
+ color: $error-value-color;
+ }
&:hover,
&:active {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 69a0b22d6..25c9c9fe5 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -254,7 +254,7 @@ code {
& > label {
font-family: inherit;
- font-size: 16px;
+ font-size: 14px;
color: $primary-text-color;
display: block;
font-weight: 500;
@@ -291,6 +291,20 @@ code {
.input:last-child {
margin-bottom: 0;
}
+
+ &__thumbnail {
+ display: block;
+ margin: 0;
+ margin-bottom: 10px;
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+ background: url("images/void.png");
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
}
.fields-row {
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index b6bb3d795..957a32b7c 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -8,7 +8,6 @@ class Form::AdminSettings
site_contact_email
site_title
site_short_description
- site_description
site_extended_description
site_terms
registrations_mode
@@ -53,45 +52,55 @@ class Form::AdminSettings
attr_accessor(*KEYS)
- validates :site_short_description, :site_description, html: { wrap_with: :p }
- validates :site_extended_description, :site_terms, :closed_registrations_message, html: true
- validates :registrations_mode, inclusion: { in: %w(open approved none) }
- validates :site_contact_email, :site_contact_username, presence: true
- validates :site_contact_username, existing_username: true
- validates :bootstrap_timeline_accounts, existing_username: { multiple: true }
- validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }
- validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }
- validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true
+ validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) }
+ validates :site_contact_email, :site_contact_username, presence: true, if: -> { defined?(@site_contact_username) || defined?(@site_contact_email) }
+ validates :site_contact_username, existing_username: true, if: -> { defined?(@site_contact_username) }
+ validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) }
+ validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
+ validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
+ validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
+ validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) }
- def initialize(_attributes = {})
- super
- initialize_attributes
+ KEYS.each do |key|
+ define_method(key) do
+ return instance_variable_get("@#{key}") if instance_variable_defined?("@#{key}")
+
+ stored_value = begin
+ if UPLOAD_KEYS.include?(key)
+ SiteUpload.where(var: key).first_or_initialize(var: key)
+ else
+ Setting.public_send(key)
+ end
+ end
+
+ instance_variable_set("@#{key}", stored_value)
+ end
+ end
+
+ UPLOAD_KEYS.each do |key|
+ define_method("#{key}=") do |file|
+ value = public_send(key)
+ value.file = file
+ end
end
def save
return false unless valid?
KEYS.each do |key|
- value = instance_variable_get("@#{key}")
+ next unless instance_variable_defined?("@#{key}")
- if UPLOAD_KEYS.include?(key) && !value.nil?
- upload = SiteUpload.where(var: key).first_or_initialize(var: key)
- upload.update(file: value)
+ if UPLOAD_KEYS.include?(key)
+ public_send(key).save
else
setting = Setting.where(var: key).first_or_initialize(var: key)
- setting.update(value: typecast_value(key, value))
+ setting.update(value: typecast_value(key, instance_variable_get("@#{key}")))
end
end
end
private
- def initialize_attributes
- KEYS.each do |key|
- instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil?
- end
- end
-
def typecast_value(key, value)
if BOOLEAN_KEYS.include?(key)
value == '1'
diff --git a/app/serializers/rest/extended_description_serializer.rb b/app/serializers/rest/extended_description_serializer.rb
index 0c3649033..c0fa3450d 100644
--- a/app/serializers/rest/extended_description_serializer.rb
+++ b/app/serializers/rest/extended_description_serializer.rb
@@ -8,6 +8,16 @@ class REST::ExtendedDescriptionSerializer < ActiveModel::Serializer
end
def content
- object.text
+ if object.text.present?
+ markdown.render(object.text)
+ else
+ ''
+ end
+ end
+
+ private
+
+ def markdown
+ @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML)
end
end
diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml
new file mode 100644
index 000000000..366d213f6
--- /dev/null
+++ b/app/views/admin/settings/about/show.html.haml
@@ -0,0 +1,33 @@
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+ = t('admin.settings.about.title')
+
+- content_for :heading do
+ %h2= t('admin.settings.title')
+ = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_about_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
+
+ %p.lead= t('admin.settings.about.preamble')
+
+ .fields-group
+ = f.input :site_extended_description, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
+
+ %p.hint
+ = t 'admin.settings.about.rules_hint'
+ = link_to t('admin.settings.about.manage_rules'), admin_rules_path
+
+ .fields-row
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+
+ .fields-group
+ = f.input :site_terms, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/appearance/show.html.haml b/app/views/admin/settings/appearance/show.html.haml
new file mode 100644
index 000000000..d321c4b04
--- /dev/null
+++ b/app/views/admin/settings/appearance/show.html.haml
@@ -0,0 +1,34 @@
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+ = t('admin.settings.appearance.title')
+
+- content_for :heading do
+ %h2= t('admin.settings.title')
+ = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_appearance_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
+
+ %p.lead= t('admin.settings.appearance.preamble')
+
+ .fields-group
+ = f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
+
+ .fields-group
+ = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
+
+ .fields-row
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :mascot, as: :file, wrapper: :with_block_label
+
+ .fields-row__column.fields-row__column-6.fields-group
+ - if @admin_settings.mascot.persisted?
+ = image_tag @admin_settings.mascot.file.url, class: 'fields-group__thumbnail'
+ = link_to admin_site_upload_path(@admin_settings.mascot), data: { method: :delete }, class: 'link-button link-button--destructive' do
+ = fa_icon 'trash fw'
+ = t('admin.site_uploads.delete')
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/branding/show.html.haml b/app/views/admin/settings/branding/show.html.haml
new file mode 100644
index 000000000..74a6fadf9
--- /dev/null
+++ b/app/views/admin/settings/branding/show.html.haml
@@ -0,0 +1,39 @@
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+ = t('admin.settings.branding.title')
+
+- content_for :heading do
+ %h2= t('admin.settings.title')
+ = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
+
+ %p.lead= t('admin.settings.branding.preamble')
+
+ .fields-group
+ = f.input :site_title, wrapper: :with_label
+
+ .fields-row
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :site_contact_username, wrapper: :with_label
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :site_contact_email, wrapper: :with_label
+
+ .fields-group
+ = f.input :site_short_description, wrapper: :with_block_label, as: :text, input_html: { rows: 2, maxlength: 200 }
+
+ .fields-row
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :thumbnail, as: :file, wrapper: :with_block_label
+ .fields-row__column.fields-row__column-6.fields-group
+ - if @admin_settings.thumbnail.persisted?
+ = image_tag @admin_settings.thumbnail.file.url(:'@1x'), class: 'fields-group__thumbnail'
+ = link_to admin_site_upload_path(@admin_settings.thumbnail), data: { method: :delete }, class: 'link-button link-button--destructive' do
+ = fa_icon 'trash fw'
+ = t('admin.site_uploads.delete')
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/content_retention/show.html.haml b/app/views/admin/settings/content_retention/show.html.haml
new file mode 100644
index 000000000..36856127f
--- /dev/null
+++ b/app/views/admin/settings/content_retention/show.html.haml
@@ -0,0 +1,22 @@
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+ = t('admin.settings.content_retention.title')
+
+- content_for :heading do
+ %h2= t('admin.settings.title')
+ = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_content_retention_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
+
+ %p.lead= t('admin.settings.content_retention.preamble')
+
+ .fields-group
+ = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
+ = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
+ = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml
new file mode 100644
index 000000000..e63c853fb
--- /dev/null
+++ b/app/views/admin/settings/discovery/show.html.haml
@@ -0,0 +1,40 @@
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+ = t('admin.settings.discovery.title')
+
+- content_for :heading do
+ %h2= t('admin.settings.title')
+ = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_discovery_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
+
+ %p.lead= t('admin.settings.discovery.preamble')
+
+ %h4= t('admin.settings.discovery.trends')
+
+ .fields-group
+ = f.input :trends, as: :boolean, wrapper: :with_label
+
+ .fields-group
+ = f.input :trendable_by_default, as: :boolean, wrapper: :with_label
+
+ %h4= t('admin.settings.discovery.public_timelines')
+
+ .fields-group
+ = f.input :timeline_preview, as: :boolean, wrapper: :with_label
+
+ %h4= t('admin.settings.discovery.follow_recommendations')
+
+ .fields-group
+ = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label
+
+ %h4= t('admin.settings.discovery.profile_directory')
+
+ .fields-group
+ = f.input :profile_directory, as: :boolean, wrapper: :with_label
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
deleted file mode 100644
index 15b1a2498..000000000
--- a/app/views/admin/settings/edit.html.haml
+++ /dev/null
@@ -1,102 +0,0 @@
-- content_for :header_tags do
- = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
-
-- content_for :page_title do
- = t('admin.settings.title')
-
- - content_for :heading_actions do
- = button_tag t('generic.save_changes'), class: 'button', form: 'edit_admin'
-
-= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch, id: 'edit_admin' } do |f|
- = render 'shared/error_messages', object: @admin_settings
-
- .fields-group
- = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
-
- .fields-row
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :theme, collection: Themes.instance.names, label: t('simple_form.labels.defaults.setting_theme'), label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, label: t('admin.settings.registrations_mode.title'), include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
-
- .fields-row
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :site_contact_username, wrapper: :with_label, label: t('admin.settings.contact_information.username')
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :site_contact_email, wrapper: :with_label, label: t('admin.settings.contact_information.email')
-
- .fields-group
- = f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
-
- .fields-group
- = f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 2 }
-
- .fields-row
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail)
-
- .fields-row
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: site_upload_delete_hint(t('admin.settings.mascot.desc_html'), :mascot)
-
- %hr.spacer/
-
- .fields-group
- = f.input :require_invite_text, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.require_invite_text.title'), hint: t('admin.settings.registrations.require_invite_text.desc_html'), disabled: !approved_registrations?
-
- %hr.spacer/
-
- .fields-group
- = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
-
- %hr.spacer/
-
- - unless whitelist_mode?
- .fields-group
- = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
-
- - unless whitelist_mode?
- .fields-group
- = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true
-
- .fields-group
- = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html'), recommended: true
-
- .fields-group
- = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
-
- .fields-group
- = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
-
- .fields-group
- = f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
-
- .fields-group
- = f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html'), recommended: :not_recommended
-
- .fields-group
- = f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
-
- %hr.spacer/
-
- .fields-row
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
- .fields-row__column.fields-row__column-6.fields-group
- = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
-
- .fields-group
- = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
- = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
- = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
- = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
-
- %hr.spacer/
-
- .fields-group
- = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
- = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
- = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
-
- .actions
- = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml
new file mode 100644
index 000000000..0129332d7
--- /dev/null
+++ b/app/views/admin/settings/registrations/show.html.haml
@@ -0,0 +1,27 @@
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+ = t('admin.settings.registrations.title')
+
+- content_for :heading do
+ %h2= t('admin.settings.title')
+ = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f|
+ = render 'shared/error_messages', object: @admin_settings
+
+ %p.lead= t('admin.settings.registrations.preamble')
+
+ .fields-row
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
+
+ .fields-row__column.fields-row__column-6.fields-group
+ = f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations?
+
+ .fields-group
+ = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 }
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/shared/_links.html.haml b/app/views/admin/settings/shared/_links.html.haml
new file mode 100644
index 000000000..1294c26ce
--- /dev/null
+++ b/app/views/admin/settings/shared/_links.html.haml
@@ -0,0 +1,8 @@
+.content__heading__tabs
+ = render_navigation renderer: :links do |primary|
+ - primary.item :branding, safe_join([fa_icon('pencil fw'), t('admin.settings.branding.title')]), admin_settings_branding_path
+ - primary.item :about, safe_join([fa_icon('file-text fw'), t('admin.settings.about.title')]), admin_settings_about_path
+ - primary.item :registrations, safe_join([fa_icon('users fw'), t('admin.settings.registrations.title')]), admin_settings_registrations_path
+ - primary.item :discovery, safe_join([fa_icon('search fw'), t('admin.settings.discovery.title')]), admin_settings_discovery_path
+ - primary.item :content_retention, safe_join([fa_icon('history fw'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path
+ - primary.item :appearance, safe_join([fa_icon('desktop fw'), t('admin.settings.appearance.title')]), admin_settings_appearance_path
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index e577b9803..59021ad88 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -22,15 +22,16 @@
.content-wrapper
.content
- .content-heading
+ .content__heading
- if content_for?(:heading)
= yield :heading
- else
- %h2= yield :page_title
+ .content__heading__row
+ %h2= yield :page_title
- - if :heading_actions
- .content-heading-actions
- = yield :heading_actions
+ - if content_for?(:heading_actions)
+ .content__heading__actions
+ = yield :heading_actions
= render 'application/flashes'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 412178ca3..70850d478 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -667,79 +667,40 @@ en:
empty: No server rules have been defined yet.
title: Server rules
settings:
- activity_api_enabled:
- desc_html: Counts of locally published posts, active users, and new registrations in weekly buckets
- title: Publish aggregate statistics about user activity in the API
- bootstrap_timeline_accounts:
- desc_html: Separate multiple usernames by comma. These accounts will be guaranteed to be shown in follow recommendations
- title: Recommend these accounts to new users
- contact_information:
- email: Business e-mail
- username: Contact username
- custom_css:
- desc_html: Modify the look with CSS loaded on every page
- title: Custom CSS
- default_noindex:
- desc_html: Affects all users who have not changed this setting themselves
- title: Opt users out of search engine indexing by default
+ about:
+ manage_rules: Manage server rules
+ preamble: Provide in-depth information about how the server is operated, moderated, funded.
+ rules_hint: There is a dedicated area for rules that your users are expected to adhere to.
+ title: About
+ appearance:
+ preamble: Customize Mastodon's web interface.
+ title: Appearance
+ branding:
+ preamble: Your server's branding differentiates it from other servers in the network. This information may be displayed across a variety of environments, such as Mastodon's web interface, native applications, in link previews on other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear, short and concise.
+ title: Branding
+ content_retention:
+ preamble: Control how user-generated content is stored in Mastodon.
+ title: Content retention
+ discovery:
+ follow_recommendations: Follow recommendations
+ preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server.
+ profile_directory: Profile directory
+ public_timelines: Public timelines
+ title: Discovery
+ trends: Trends
domain_blocks:
all: To everyone
disabled: To no one
- title: Show domain blocks
users: To logged-in local users
- domain_blocks_rationale:
- title: Show rationale
- mascot:
- desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot
- title: Mascot image
- peers_api_enabled:
- desc_html: Domain names this server has encountered in the fediverse
- title: Publish list of discovered servers in the API
- preview_sensitive_media:
- desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive
- title: Show sensitive media in OpenGraph previews
- profile_directory:
- desc_html: Allow users to be discoverable
- title: Enable profile directory
registrations:
- closed_message:
- desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags
- title: Closed registration message
- require_invite_text:
- desc_html: When registrations require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
- title: Require new users to enter a reason to join
+ preamble: Control who can create an account on your server.
+ title: Registrations
registrations_mode:
modes:
approved: Approval required for sign up
none: Nobody can sign up
open: Anyone can sign up
- title: Registrations mode
- site_description:
- desc_html: Introductory paragraph on the API. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <a>
and <em>
.
- title: Server description
- site_description_extended:
- desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags
- title: Custom extended information
- site_short_description:
- desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph.
- title: Short server description
- site_terms:
- desc_html: You can write your own privacy policy. You can use HTML tags
- title: Custom privacy policy
- site_title: Server name
- thumbnail:
- desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
- title: Server thumbnail
- timeline_preview:
- desc_html: Display link to public timeline on landing page and allow API access to the public timeline without authentication
- title: Allow unauthenticated access to public timeline
- title: Site settings
- trendable_by_default:
- desc_html: Specific trending content can still be explicitly disallowed
- title: Allow trends without prior review
- trends:
- desc_html: Publicly display previously reviewed content that is currently trending
- title: Trends
+ title: Server Settings
site_uploads:
delete: Delete uploaded file
destroyed_msg: Site upload successfully deleted!
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index db5b45e41..64281d7b7 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -75,8 +75,25 @@ en:
warn: Hide the filtered content behind a warning mentioning the filter's title
form_admin_settings:
backups_retention_period: Keep generated user archives for the specified number of days.
+ bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
+ closed_registrations_message: Displayed when sign-ups are closed
content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible.
+ custom_css: You can apply custom styles on the web version of Mastodon.
+ mascot: Overrides the illustration in the advanced web interface.
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
+ profile_directory: The profile directory lists all users who have opted-in to be discoverable.
+ require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
+ site_contact_email: How people can reach you for legal or support inquiries.
+ site_contact_username: How people can reach you on Mastodon.
+ site_extended_description: Any additional information that may be useful to visitors and your users. Can be structured with Markdown syntax.
+ site_short_description: A short description to help uniquely identify your server. Who is running it, who is it for?
+ site_terms: Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax.
+ site_title: How people may refer to your server besides its domain name.
+ theme: Theme that logged out visitors and new users see.
+ thumbnail: A roughly 2:1 image displayed alongside your server information.
+ timeline_preview: Logged out visitors will be able to browse the most recent public posts available on the server.
+ trendable_by_default: Skip manual review of trending content. Individual items can still be removed from trends after the fact.
+ trends: Trends show which posts, hashtags and news stories are gaining traction on your server.
form_challenge:
current_password: You are entering a secure area
imports:
@@ -213,8 +230,28 @@ en:
warn: Hide with a warning
form_admin_settings:
backups_retention_period: User archive retention period
+ bootstrap_timeline_accounts: Always recommend these accounts to new users
+ closed_registrations_message: Custom message when sign-ups are not available
content_cache_retention_period: Content cache retention period
+ custom_css: Custom CSS
+ mascot: Custom mascot (legacy)
media_cache_retention_period: Media cache retention period
+ profile_directory: Enable profile directory
+ registrations_mode: Who can sign-up
+ require_invite_text: Require a reason to join
+ show_domain_blocks: Show domain blocks
+ show_domain_blocks_rationale: Show why domains were blocked
+ site_contact_email: Contact e-mail
+ site_contact_username: Contact username
+ site_extended_description: Extended description
+ site_short_description: Server description
+ site_terms: Privacy Policy
+ site_title: Server name
+ theme: Default theme
+ thumbnail: Server thumbnail
+ timeline_preview: Allow unauthenticated access to public timelines
+ trendable_by_default: Allow trends without prior review
+ trends: Enable trends
interactions:
must_be_follower: Block notifications from non-followers
must_be_following: Block notifications from people you don't follow
diff --git a/config/navigation.rb b/config/navigation.rb
index 706de0471..e901fb932 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -52,7 +52,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) } do |s|
s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) }
- s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings}
+ s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings}
s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) }
s.item :roles, safe_join([fa_icon('vcard fw'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) }
s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }
diff --git a/config/routes.rb b/config/routes.rb
index 1ed585f19..b44479e77 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -241,7 +241,18 @@ Rails.application.routes.draw do
end
end
- resource :settings, only: [:edit, :update]
+ get '/settings', to: redirect('/admin/settings/branding')
+ get '/settings/edit', to: redirect('/admin/settings/branding')
+
+ namespace :settings do
+ resource :branding, only: [:show, :update], controller: 'branding'
+ resource :registrations, only: [:show, :update], controller: 'registrations'
+ resource :content_retention, only: [:show, :update], controller: 'content_retention'
+ resource :about, only: [:show, :update], controller: 'about'
+ resource :appearance, only: [:show, :update], controller: 'appearance'
+ resource :discovery, only: [:show, :update], controller: 'discovery'
+ end
+
resources :site_uploads, only: [:destroy]
resources :invites, only: [:index, :create, :destroy] do
diff --git a/spec/controllers/admin/settings/branding_controller_spec.rb b/spec/controllers/admin/settings/branding_controller_spec.rb
new file mode 100644
index 000000000..ee1c441bc
--- /dev/null
+++ b/spec/controllers/admin/settings/branding_controller_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Admin::Settings::BrandingController, type: :controller do
+ render_views
+
+ describe 'When signed in as an admin' do
+ before do
+ sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
+ end
+
+ describe 'GET #show' do
+ it 'returns http success' do
+ get :show
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe 'PUT #update' do
+ before do
+ allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
+ end
+
+ around do |example|
+ before = Setting.site_short_description
+ Setting.site_short_description = nil
+ example.run
+ Setting.site_short_description = before
+ Setting.new_setting_key = nil
+ end
+
+ it 'cannot create a setting value for a non-admin key' do
+ expect(Setting.new_setting_key).to be_blank
+
+ patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
+
+ expect(response).to redirect_to(admin_settings_branding_path)
+ expect(Setting.new_setting_key).to be_nil
+ end
+
+ it 'creates a settings value that didnt exist before for eligible key' do
+ expect(Setting.site_short_description).to be_blank
+
+ patch :update, params: { form_admin_settings: { site_short_description: 'New key value' } }
+
+ expect(response).to redirect_to(admin_settings_branding_path)
+ expect(Setting.site_short_description).to eq 'New key value'
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
deleted file mode 100644
index 46749f76c..000000000
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::SettingsController, type: :controller do
- render_views
-
- describe 'When signed in as an admin' do
- before do
- sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
- end
-
- describe 'GET #edit' do
- it 'returns http success' do
- get :edit
-
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'PUT #update' do
- before do
- allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
- end
-
- describe 'for a record that doesnt exist' do
- around do |example|
- before = Setting.site_extended_description
- Setting.site_extended_description = nil
- example.run
- Setting.site_extended_description = before
- Setting.new_setting_key = nil
- end
-
- it 'cannot create a setting value for a non-admin key' do
- expect(Setting.new_setting_key).to be_blank
-
- patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
-
- expect(response).to redirect_to(edit_admin_settings_path)
- expect(Setting.new_setting_key).to be_nil
- end
-
- it 'creates a settings value that didnt exist before for eligible key' do
- expect(Setting.site_extended_description).to be_blank
-
- patch :update, params: { form_admin_settings: { site_extended_description: 'New key value' } }
-
- expect(response).to redirect_to(edit_admin_settings_path)
- expect(Setting.site_extended_description).to eq 'New key value'
- end
- end
-
- context do
- around do |example|
- site_title = Setting.site_title
- example.run
- Setting.site_title = site_title
- end
-
- it 'updates a settings value' do
- Setting.site_title = 'Original'
- patch :update, params: { form_admin_settings: { site_title: 'New title' } }
-
- expect(response).to redirect_to(edit_admin_settings_path)
- expect(Setting.site_title).to eq 'New title'
- end
- end
- end
- end
-end
--
cgit
From 1ae508bf2faae016b88d15e273b0dc01de4fd7af Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Wed, 26 Oct 2022 12:10:02 +0200
Subject: Change unauthenticated search to not support pagination in REST API
(#19326)
- Only exact search matches for queries with < 5 characters
- Do not support queries with `offset` (pagination)
- Return HTTP 401 on truthy `resolve` instead of overriding to false
---
app/controllers/api/v2/search_controller.rb | 13 ++++-
app/services/account_search_service.rb | 5 ++
spec/controllers/api/v2/search_controller_spec.rb | 62 ++++++++++++++++++++---
3 files changed, 71 insertions(+), 9 deletions(-)
(limited to 'spec/controllers')
diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb
index e384ecbaf..4d20aeb10 100644
--- a/app/controllers/api/v2/search_controller.rb
+++ b/app/controllers/api/v2/search_controller.rb
@@ -6,6 +6,7 @@ class Api::V2::SearchController < Api::BaseController
RESULTS_LIMIT = 20
before_action -> { authorize_if_got_token! :read, :'read:search' }
+ before_action :validate_search_params!
def index
@search = Search.new(search_results)
@@ -18,12 +19,22 @@ class Api::V2::SearchController < Api::BaseController
private
+ def validate_search_params!
+ params.require(:q)
+
+ return if user_signed_in?
+
+ return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present?
+
+ render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve)
+ end
+
def search_results
SearchService.new.call(
params[:q],
current_account,
limit_param(RESULTS_LIMIT),
- search_params.merge(resolve: user_signed_in? ? truthy_param?(:resolve) : false, exclude_unreviewed: truthy_param?(:exclude_unreviewed))
+ search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
)
end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index 4dcae20eb..35b2e05f5 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -3,6 +3,9 @@
class AccountSearchService < BaseService
attr_reader :query, :limit, :offset, :options, :account
+ # Min. number of characters to look for non-exact matches
+ MIN_QUERY_LENGTH = 5
+
def call(query, account = nil, options = {})
@acct_hint = query&.start_with?('@')
@query = query&.strip&.gsub(/\A@/, '')
@@ -135,6 +138,8 @@ class AccountSearchService < BaseService
end
def limit_for_non_exact_results
+ return 0 if @account.nil? && query.size < MIN_QUERY_LENGTH
+
if exact_match?
limit - 1
else
diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb
index fa20e1e51..d417ea58c 100644
--- a/spec/controllers/api/v2/search_controller_spec.rb
+++ b/spec/controllers/api/v2/search_controller_spec.rb
@@ -5,18 +5,64 @@ require 'rails_helper'
RSpec.describe Api::V2::SearchController, type: :controller do
render_views
- let(:user) { Fabricate(:user) }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') }
+ context 'with token' do
+ let(:user) { Fabricate(:user) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') }
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
+ before do
+ allow(controller).to receive(:doorkeeper_token) { token }
+ end
+
+ describe 'GET #index' do
+ before do
+ get :index, params: { q: 'test' }
+ end
+
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+ end
end
- describe 'GET #index' do
- it 'returns http success' do
- get :index, params: { q: 'test' }
+ context 'without token' do
+ describe 'GET #index' do
+ let(:search_params) {}
+
+ before do
+ get :index, params: search_params
+ end
+
+ context 'with a `q` shorter than 5 characters' do
+ let(:search_params) { { q: 'test' } }
+
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'with a `q` equal to or longer than 5 characters' do
+ let(:search_params) { { q: 'test1' } }
+
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+
+ context 'with truthy `resolve`' do
+ let(:search_params) { { q: 'test1', resolve: '1' } }
+
+ it 'returns http unauthorized' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'with `offset`' do
+ let(:search_params) { { q: 'test1', offset: 1 } }
- expect(response).to have_http_status(200)
+ it 'returns http unauthorized' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
end
end
end
--
cgit
From bbdf61c9e46dda1c38388f804ebb3f437d705816 Mon Sep 17 00:00:00 2001
From: Claire
Date: Fri, 28 Oct 2022 20:12:04 +0200
Subject: Revert Home controller test to upstream
---
spec/controllers/home_controller_spec.rb | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
(limited to 'spec/controllers')
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
index 914ca6307..d845ae01d 100644
--- a/spec/controllers/home_controller_spec.rb
+++ b/spec/controllers/home_controller_spec.rb
@@ -7,16 +7,9 @@ RSpec.describe HomeController, type: :controller do
subject { get :index }
context 'when not signed in' do
- context 'when requested path is tag timeline' do
- it 'returns http success' do
- @request.path = '/web/timelines/tag/name'
- is_expected.to have_http_status(:success)
- end
- end
-
- it 'redirects to about page' do
+ it 'returns http success' do
@request.path = '/'
- is_expected.to redirect_to(about_path)
+ is_expected.to have_http_status(:success)
end
end
--
cgit
From 40c7f3e830538951862dc73074d1045a82395ab0 Mon Sep 17 00:00:00 2001
From: Eugen Rochko
Date: Sun, 30 Oct 2022 02:44:32 +0200
Subject: Fix account action type validation (#19476)
* Fix account action type validation
Fix #19143
* Fix #19145
* Fix code style issues
---
app/models/admin/account_action.rb | 9 +++--
.../v1/admin/account_actions_controller_spec.rb | 44 ++++++++++++++--------
2 files changed, 34 insertions(+), 19 deletions(-)
(limited to 'spec/controllers')
diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index aed3bc0c7..bce0d6e17 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -25,6 +25,8 @@ class Admin::AccountAction
alias send_email_notification? send_email_notification
alias include_statuses? include_statuses
+ validates :type, :target_account, :current_account, presence: true
+
def initialize(attributes = {})
@send_email_notification = true
@include_statuses = true
@@ -41,13 +43,15 @@ class Admin::AccountAction
end
def save!
+ raise ActiveRecord::RecordInvalid, self unless valid?
+
ApplicationRecord.transaction do
process_action!
process_strike!
+ process_reports!
end
process_email!
- process_reports!
process_queue!
end
@@ -106,9 +110,8 @@ class Admin::AccountAction
# Otherwise, we will mark all unresolved reports about
# the account as resolved.
- reports.each { |report| authorize(report, :update?) }
-
reports.each do |report|
+ authorize(report, :update?)
log_action(:resolve, report)
report.resolve!(current_account)
end
diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb
index 199395f55..462c2cfa9 100644
--- a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb
@@ -30,28 +30,40 @@ RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do
end
describe 'POST #create' do
- before do
- post :create, params: { account_id: account.id, type: 'disable' }
- end
+ context do
+ before do
+ post :create, params: { account_id: account.id, type: 'disable' }
+ end
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'performs action against account' do
+ expect(account.reload.user_disabled?).to be true
+ end
+
+ it 'logs action' do
+ log_item = Admin::ActionLog.last
- it 'performs action against account' do
- expect(account.reload.user_disabled?).to be true
+ expect(log_item).to_not be_nil
+ expect(log_item.action).to eq :disable
+ expect(log_item.account_id).to eq user.account_id
+ expect(log_item.target_id).to eq account.user.id
+ end
end
- it 'logs action' do
- log_item = Admin::ActionLog.last
+ context 'with no type' do
+ before do
+ post :create, params: { account_id: account.id }
+ end
- expect(log_item).to_not be_nil
- expect(log_item.action).to eq :disable
- expect(log_item.account_id).to eq user.account_id
- expect(log_item.target_id).to eq account.user.id
+ it 'returns http unprocessable entity' do
+ expect(response).to have_http_status(422)
+ end
end
end
end
--
cgit
From 1dca08b76f25d15365127ded37202d783a50e298 Mon Sep 17 00:00:00 2001
From: Claire
Date: Thu, 3 Nov 2022 16:06:42 +0100
Subject: Fix admin action logs page (#19649)
* Add tests
* Fix crash when trying to display orphaned action logs
* Add migration for older admin action logs
---
app/helpers/admin/action_logs_helper.rb | 20 ++-
.../20221101190723_backfill_admin_action_logs.rb | 150 +++++++++++++++++++++
db/schema.rb | 2 +-
lib/tasks/tests.rake | 51 +++++--
.../admin/action_logs_controller_spec.rb | 13 ++
5 files changed, 221 insertions(+), 15 deletions(-)
create mode 100644 db/post_migrate/20221101190723_backfill_admin_action_logs.rb
(limited to 'spec/controllers')
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index fd1977ac5..215ecea0d 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -4,15 +4,19 @@ module Admin::ActionLogsHelper
def log_target(log)
case log.target_type
when 'Account'
- link_to log.human_identifier, admin_account_path(log.target_id)
+ link_to (log.human_identifier.presence || I18n.t('admin.action_logs.deleted_account')), admin_account_path(log.target_id)
when 'User'
- link_to log.human_identifier, admin_account_path(log.route_param)
+ if log.route_param.present?
+ link_to log.human_identifier, admin_account_path(log.route_param)
+ else
+ I18n.t('admin.action_logs.deleted_account')
+ end
when 'UserRole'
link_to log.human_identifier, admin_roles_path(log.target_id)
when 'Report'
- link_to "##{log.human_identifier}", admin_report_path(log.target_id)
+ link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
- link_to log.human_identifier, "https://#{log.human_identifier}"
+ link_to log.human_identifier, "https://#{log.human_identifier.presence}"
when 'Status'
link_to log.human_identifier, log.permalink
when 'AccountWarning'
@@ -22,9 +26,13 @@ module Admin::ActionLogsHelper
when 'IpBlock', 'Instance', 'CustomEmoji'
log.human_identifier
when 'CanonicalEmailBlock'
- content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier)
+ content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier)
when 'Appeal'
- link_to log.human_identifier, disputes_strike_path(log.route_param)
+ if log.route_param.present?
+ link_to log.human_identifier, disputes_strike_path(log.route_param.presence)
+ else
+ I18n.t('admin.action_logs.deleted_account')
+ end
end
end
end
diff --git a/db/post_migrate/20221101190723_backfill_admin_action_logs.rb b/db/post_migrate/20221101190723_backfill_admin_action_logs.rb
new file mode 100644
index 000000000..9a64d1715
--- /dev/null
+++ b/db/post_migrate/20221101190723_backfill_admin_action_logs.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+class BackfillAdminActionLogs < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ class Account < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ has_one :user, inverse_of: :account
+
+ def local?
+ domain.nil?
+ end
+
+ def acct
+ local? ? username : "#{username}@#{domain}"
+ end
+ end
+
+ class User < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account
+ end
+
+ class Status < ApplicationRecord
+ include RoutingHelper
+
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account
+
+ def local?
+ attributes['local'] || attributes['uri'].nil?
+ end
+
+ def uri
+ local? ? activity_account_status_url(account, self) : attributes['uri']
+ end
+ end
+
+ class DomainBlock < ApplicationRecord; end
+ class DomainAllow < ApplicationRecord; end
+ class EmailDomainBlock < ApplicationRecord; end
+ class UnavailableDomain < ApplicationRecord; end
+
+ class AccountWarning < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account
+ end
+
+ class Announcement < ApplicationRecord; end
+ class IpBlock < ApplicationRecord; end
+ class CustomEmoji < ApplicationRecord; end
+ class CanonicalEmailBlock < ApplicationRecord; end
+
+ class Appeal < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+ belongs_to :account
+ end
+
+ class AdminActionLog < ApplicationRecord
+ # Dummy class, to make migration possible across version changes
+
+ # Cannot use usual polymorphic support because of namespacing issues
+ belongs_to :status, foreign_key: :target_id
+ belongs_to :account, foreign_key: :target_id
+ belongs_to :user, foreign_key: :user_id
+ belongs_to :domain_block, foreign_key: :target_id
+ belongs_to :domain_allow, foreign_key: :target_id
+ belongs_to :email_domain_block, foreign_key: :target_id
+ belongs_to :unavailable_domain, foreign_key: :target_id
+ belongs_to :account_warning, foreign_key: :target_id
+ belongs_to :announcement, foreign_key: :target_id
+ belongs_to :ip_block, foreign_key: :target_id
+ belongs_to :custom_emoji, foreign_key: :target_id
+ belongs_to :canonical_email_block, foreign_key: :target_id
+ belongs_to :appeal, foreign_key: :target_id
+ end
+
+ def up
+ safety_assured do
+ AdminActionLog.includes(:account).where(target_type: 'Account', human_identifier: nil).find_each do |log|
+ next if log.account.nil?
+ log.update(human_identifier: log.account.acct)
+ end
+
+ AdminActionLog.includes(user: :account).where(target_type: 'User', human_identifier: nil).find_each do |log|
+ next if log.user.nil?
+ log.update(human_identifier: log.user.account.acct, route_param: log.user.account_id)
+ end
+
+ Admin::ActionLog.where(target_type: 'Report', human_identifier: nil).in_batches.update_all('human_identifier = target_id::text')
+
+ AdminActionLog.includes(:domain_block).where(target_type: 'DomainBlock').find_each do |log|
+ next if log.domain_block.nil?
+ log.update(human_identifier: log.domain_block.domain)
+ end
+
+ AdminActionLog.includes(:domain_allow).where(target_type: 'DomainAllow').find_each do |log|
+ next if log.domain_allow.nil?
+ log.update(human_identifier: log.domain_allow.domain)
+ end
+
+ AdminActionLog.includes(:email_domain_block).where(target_type: 'EmailDomainBlock').find_each do |log|
+ next if log.email_domain_block.nil?
+ log.update(human_identifier: log.email_domain_block.domain)
+ end
+
+ AdminActionLog.includes(:unavailable_domain).where(target_type: 'UnavailableDomain').find_each do |log|
+ next if log.unavailable_domain.nil?
+ log.update(human_identifier: log.unavailable_domain.domain)
+ end
+
+ AdminActionLog.includes(status: :account).where(target_type: 'Status', human_identifier: nil).find_each do |log|
+ next if log.status.nil?
+ log.update(human_identifier: log.status.account.acct, permalink: log.status.uri)
+ end
+
+ AdminActionLog.includes(account_warning: :account).where(target_type: 'AccountWarning', human_identifier: nil).find_each do |log|
+ next if log.account_warning.nil?
+ log.update(human_identifier: log.account_warning.account.acct)
+ end
+
+ AdminActionLog.includes(:announcement).where(target_type: 'Announcement', human_identifier: nil).find_each do |log|
+ next if log.announcement.nil?
+ log.update(human_identifier: log.announcement.text)
+ end
+
+ AdminActionLog.includes(:ip_block).where(target_type: 'IpBlock', human_identifier: nil).find_each do |log|
+ next if log.ip_block.nil?
+ log.update(human_identifier: "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}")
+ end
+
+ AdminActionLog.includes(:custom_emoji).where(target_type: 'CustomEmoji', human_identifier: nil).find_each do |log|
+ next if log.custom_emoji.nil?
+ log.update(human_identifier: log.custom_emoji.shortcode)
+ end
+
+ AdminActionLog.includes(:canonical_email_block).where(target_type: 'CanonicalEmailBlock', human_identifier: nil).find_each do |log|
+ next if log.canonical_email_block.nil?
+ log.update(human_identifier: log.canonical_email_block.canonical_email_hash)
+ end
+
+ AdminActionLog.includes(appeal: :account).where(target_type: 'Appeal', human_identifier: nil).find_each do |log|
+ next if log.appeal.nil?
+ log.update(human_identifier: log.appeal.account.acct, route_param: log.appeal.account_warning_id)
+ end
+ end
+ end
+
+ def down; end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d7e40b133..12ec37c11 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_10_25_171544) do
+ActiveRecord::Schema.define(version: 2022_11_01_190723) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake
index 65bff6a8e..96d2f7112 100644
--- a/lib/tasks/tests.rake
+++ b/lib/tasks/tests.rake
@@ -53,6 +53,41 @@ namespace :tests do
VALUES
(1, 2, 'test', '{ "home", "public" }', true, true, now(), now()),
(2, 2, 'take', '{ "home" }', false, false, now(), now());
+
+ -- Orphaned admin action logs
+
+ INSERT INTO "admin_action_logs"
+ (account_id, action, target_type, target_id, created_at, updated_at)
+ VALUES
+ (1, 'destroy', 'Account', 1312, now(), now()),
+ (1, 'destroy', 'User', 1312, now(), now()),
+ (1, 'destroy', 'Report', 1312, now(), now()),
+ (1, 'destroy', 'DomainBlock', 1312, now(), now()),
+ (1, 'destroy', 'EmailDomainBlock', 1312, now(), now()),
+ (1, 'destroy', 'Status', 1312, now(), now()),
+ (1, 'destroy', 'CustomEmoji', 1312, now(), now());
+
+ -- Admin action logs with linked objects
+
+ INSERT INTO "domain_blocks"
+ (id, domain, created_at, updated_at)
+ VALUES
+ (1, 'example.org', now(), now());
+
+ INSERT INTO "email_domain_blocks"
+ (id, domain, created_at, updated_at)
+ VALUES
+ (1, 'example.org', now(), now());
+
+ INSERT INTO "admin_action_logs"
+ (account_id, action, target_type, target_id, created_at, updated_at)
+ VALUES
+ (1, 'destroy', 'Account', 1, now(), now()),
+ (1, 'destroy', 'User', 1, now(), now()),
+ (1, 'destroy', 'DomainBlock', 1312, now(), now()),
+ (1, 'destroy', 'EmailDomainBlock', 1312, now(), now()),
+ (1, 'destroy', 'Status', 1, now(), now()),
+ (1, 'destroy', 'CustomEmoji', 3, now(), now());
SQL
end
@@ -207,18 +242,18 @@ namespace :tests do
-- custom emoji
INSERT INTO "custom_emojis"
- (shortcode, created_at, updated_at)
+ (id, shortcode, created_at, updated_at)
VALUES
- ('test', now(), now()),
- ('Test', now(), now()),
- ('blobcat', now(), now());
+ (1, 'test', now(), now()),
+ (2, 'Test', now(), now()),
+ (3, 'blobcat', now(), now());
INSERT INTO "custom_emojis"
- (shortcode, domain, uri, created_at, updated_at)
+ (id, shortcode, domain, uri, created_at, updated_at)
VALUES
- ('blobcat', 'remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
- ('blobcat', 'Remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
- ('Blobcat', 'remote.org', 'https://remote.org/emoji/Blobcat', now(), now());
+ (4, 'blobcat', 'remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
+ (5, 'blobcat', 'Remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
+ (6, 'Blobcat', 'remote.org', 'https://remote.org/emoji/Blobcat', now(), now());
-- favourites
diff --git a/spec/controllers/admin/action_logs_controller_spec.rb b/spec/controllers/admin/action_logs_controller_spec.rb
index c1957258f..7cd8cdf46 100644
--- a/spec/controllers/admin/action_logs_controller_spec.rb
+++ b/spec/controllers/admin/action_logs_controller_spec.rb
@@ -3,6 +3,19 @@
require 'rails_helper'
describe Admin::ActionLogsController, type: :controller do
+ render_views
+
+ # Action logs typically cause issues when their targets are not in the database
+ let!(:account) { Fabricate(:account) }
+
+ let!(:orphaned_logs) do
+ %w(
+ Account User UserRole Report DomainBlock DomainAllow
+ EmailDomainBlock UnavailableDomain Status AccountWarning
+ Announcement IpBlock Instance CustomEmoji CanonicalEmailBlock Appeal
+ ).map { |type| Admin::ActionLog.new(account: account, action: 'destroy', target_type: type, target_id: 1312).save! }
+ end
+
describe 'GET #index' do
it 'returns 200' do
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
--
cgit