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 = () => ( - - + + ); diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index f4bef4686..08241522c 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -26,7 +26,7 @@ const createIdentityContext = state => ({ signedIn: !!state.meta.me, accountId: state.meta.me, accessToken: state.meta.access_token, - permissions: state.role.permissions, + permissions: state.role ? state.role.permissions : 0, }); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 8f2753c35..e407a0d55 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Button from 'mastodon/components/button'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me } from 'mastodon/initial_state'; +import { autoPlayGif, me, title, domain } from 'mastodon/initial_state'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import IconButton from 'mastodon/components/icon_button'; @@ -15,6 +15,7 @@ import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -54,6 +55,14 @@ const messages = defineMessages({ languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, }); +const titleFromAccount = account => { + const displayName = account.get('display_name'); + const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct'); + const prefix = displayName.trim().length === 0 ? account.get('username') : displayName; + + return `${prefix} (@${acct})`; +}; + const dateFormatOptions = { month: 'short', day: 'numeric', @@ -132,6 +141,7 @@ class Header extends ImmutablePureComponent { render () { const { account, hidden, intl, domain } = this.props; + const { signedIn } = this.context.identity; if (!account) { return null; @@ -162,12 +172,12 @@ class Header extends ImmutablePureComponent { } if (me !== account.get('id')) { - if (!account.get('relationship')) { // Wait until the relationship is loaded + if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded actionBtn = ''; } else if (account.getIn(['relationship', 'requested'])) { actionBtn = ; } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = ; + actionBtn = ; } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = ; } @@ -183,7 +193,7 @@ class Header extends ImmutablePureComponent { lockedIcon = ; } - if (account.get('id') !== me) { + if (signedIn && account.get('id') !== me) { menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); menu.push(null); @@ -206,7 +216,7 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); - } else { + } else if (signedIn) { if (account.getIn(['relationship', 'following'])) { if (!account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'showing_reblogs'])) { @@ -239,7 +249,7 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } - if (account.get('acct') !== account.get('username')) { + if (signedIn && account.get('acct') !== account.get('username')) { const domain = account.get('acct').split('@')[1]; menu.push(null); @@ -298,7 +308,7 @@ class Header extends ImmutablePureComponent { )} - + )} @@ -327,7 +337,7 @@ class Header extends ImmutablePureComponent { )} - {account.get('id') !== me && } + {(account.get('id') !== me && signedIn) && } {account.get('note').length > 0 && account.get('note') !== '

' &&
} @@ -359,6 +369,10 @@ class Header extends ImmutablePureComponent {
)} + + + {titleFromAccount(account)} - {title} + ); } diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 30f776048..f9d50e64c 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -9,6 +9,8 @@ import { expandCommunityTimeline } from '../../actions/timelines'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; import { connectCommunityStream } from '../../actions/streaming'; +import { Helmet } from 'react-helmet'; +import { title } from 'mastodon/initial_state'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -128,6 +130,10 @@ class CommunityTimeline extends React.PureComponent { emptyMessage={} bindToDocument={!multiColumn} /> + + + {intl.formatMessage(messages.title)} - {title} + ); } diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js index 94d7d1a9c..36f46c510 100644 --- a/app/javascript/mastodon/features/directory/index.js +++ b/app/javascript/mastodon/features/directory/index.js @@ -13,6 +13,8 @@ import RadioButton from 'mastodon/components/radio_button'; import LoadMore from 'mastodon/components/load_more'; import ScrollContainer from 'mastodon/containers/scroll_container'; import LoadingIndicator from 'mastodon/components/loading_indicator'; +import { title } from 'mastodon/initial_state'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, @@ -165,6 +167,10 @@ class Directory extends React.PureComponent { /> {multiColumn && !pinned ? {scrollableArea} : scrollableArea} + + + {intl.formatMessage(messages.title)} - {title} + ); } diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js index 8082f2d99..e1d1eb563 100644 --- a/app/javascript/mastodon/features/explore/index.js +++ b/app/javascript/mastodon/features/explore/index.js @@ -11,6 +11,8 @@ import Statuses from './statuses'; import Suggestions from './suggestions'; import Search from 'mastodon/features/compose/containers/search_container'; import SearchResults from './results'; +import { Helmet } from 'react-helmet'; +import { title } from 'mastodon/initial_state'; const messages = defineMessages({ title: { id: 'explore.title', defaultMessage: 'Explore' }, @@ -81,6 +83,10 @@ class Explore extends React.PureComponent { + + + {intl.formatMessage(messages.title)} - {title} + )} diff --git a/app/javascript/mastodon/features/explore/links.js b/app/javascript/mastodon/features/explore/links.js index 6649fb6e4..d3aaa9cdd 100644 --- a/app/javascript/mastodon/features/explore/links.js +++ b/app/javascript/mastodon/features/explore/links.js @@ -5,6 +5,7 @@ import Story from './components/story'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchTrendingLinks } from 'mastodon/actions/trends'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ links: state.getIn(['trends', 'links', 'items']), @@ -28,6 +29,16 @@ class Links extends React.PureComponent { render () { const { isLoading, links } = this.props; + if (!isLoading && links.isEmpty()) { + return ( +
+
+ +
+
+ ); + } + return (
{isLoading ? () : links.map(link => ( diff --git a/app/javascript/mastodon/features/explore/results.js b/app/javascript/mastodon/features/explore/results.js index 1286020f5..0dc108918 100644 --- a/app/javascript/mastodon/features/explore/results.js +++ b/app/javascript/mastodon/features/explore/results.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { expandSearch } from 'mastodon/actions/search'; import Account from 'mastodon/containers/account_container'; @@ -10,10 +10,17 @@ import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import { List as ImmutableList } from 'immutable'; import LoadMore from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; +import { title } from 'mastodon/initial_state'; +import { Helmet } from 'react-helmet'; + +const messages = defineMessages({ + title: { id: 'search_results.title', defaultMessage: 'Search for {q}' }, +}); const mapStateToProps = state => ({ isLoading: state.getIn(['search', 'isLoading']), results: state.getIn(['search', 'results']), + q: state.getIn(['search', 'searchTerm']), }); const appendLoadMore = (id, list, onLoadMore) => { @@ -37,6 +44,7 @@ const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', resul )), onLoadMore); export default @connect(mapStateToProps) +@injectIntl class Results extends React.PureComponent { static propTypes = { @@ -44,6 +52,8 @@ class Results extends React.PureComponent { isLoading: PropTypes.bool, multiColumn: PropTypes.bool, dispatch: PropTypes.func.isRequired, + q: PropTypes.string, + intl: PropTypes.object, }; state = { @@ -64,7 +74,7 @@ class Results extends React.PureComponent { } render () { - const { isLoading, results } = this.props; + const { intl, isLoading, q, results } = this.props; const { type } = this.state; let filteredResults = ImmutableList(); @@ -106,6 +116,10 @@ class Results extends React.PureComponent {
{isLoading ? : filteredResults}
+ + + {intl.formatMessage(messages.title, { q })} - {title} + ); } diff --git a/app/javascript/mastodon/features/explore/suggestions.js b/app/javascript/mastodon/features/explore/suggestions.js index 0c6a7ef8a..e6ad09974 100644 --- a/app/javascript/mastodon/features/explore/suggestions.js +++ b/app/javascript/mastodon/features/explore/suggestions.js @@ -5,6 +5,7 @@ import AccountCard from 'mastodon/features/directory/components/account_card'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchSuggestions } from 'mastodon/actions/suggestions'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ suggestions: state.getIn(['suggestions', 'items']), @@ -28,6 +29,16 @@ class Suggestions extends React.PureComponent { render () { const { isLoading, suggestions } = this.props; + if (!isLoading && suggestions.isEmpty()) { + return ( +
+
+ +
+
+ ); + } + return (
{isLoading ? : suggestions.map(suggestion => ( diff --git a/app/javascript/mastodon/features/explore/tags.js b/app/javascript/mastodon/features/explore/tags.js index c0ad9fc6e..6cd3a6fb1 100644 --- a/app/javascript/mastodon/features/explore/tags.js +++ b/app/javascript/mastodon/features/explore/tags.js @@ -5,6 +5,7 @@ import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchTrendingHashtags } from 'mastodon/actions/trends'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ hashtags: state.getIn(['trends', 'tags', 'items']), @@ -28,6 +29,16 @@ class Tags extends React.PureComponent { render () { const { isLoading, hashtags } = this.props; + if (!isLoading && hashtags.isEmpty()) { + return ( +
+
+ +
+
+ ); + } + return (
{isLoading ? () : hashtags.map(hashtag => ( diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index dc8a61640..7069e0341 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -14,6 +14,8 @@ import { isEqual } from 'lodash'; import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags'; import Icon from 'mastodon/components/icon'; import classNames from 'classnames'; +import { title } from 'mastodon/initial_state'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, @@ -31,6 +33,10 @@ class HashtagTimeline extends React.PureComponent { disconnects = []; + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { params: PropTypes.object.isRequired, columnId: PropTypes.string, @@ -158,6 +164,11 @@ class HashtagTimeline extends React.PureComponent { handleFollow = () => { const { dispatch, params, tag } = this.props; const { id } = params; + const { signedIn } = this.context.identity; + + if (!signedIn) { + return; + } if (tag.get('following')) { dispatch(unfollowHashtag(id)); @@ -170,6 +181,7 @@ class HashtagTimeline extends React.PureComponent { const { hasUnread, columnId, multiColumn, tag, intl } = this.props; const { id, local } = this.props.params; const pinned = !!columnId; + const { signedIn } = this.context.identity; let followButton; @@ -177,7 +189,7 @@ class HashtagTimeline extends React.PureComponent { const following = tag.get('following'); followButton = ( - ); @@ -208,6 +220,10 @@ class HashtagTimeline extends React.PureComponent { emptyMessage={} bindToDocument={!multiColumn} /> + + + {`#${id}`} - {title} + ); } diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index b1d5518af..2f926678c 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -9,6 +9,8 @@ import { expandPublicTimeline } from '../../actions/timelines'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; import { connectPublicStream } from '../../actions/streaming'; +import { Helmet } from 'react-helmet'; +import { title } from 'mastodon/initial_state'; const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, @@ -131,6 +133,10 @@ class PublicTimeline extends React.PureComponent { emptyMessage={} bindToDocument={!multiColumn} /> + + + {intl.formatMessage(messages.title)} - {title} + ); } diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 5ff7e060e..748dc7a92 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -56,10 +56,11 @@ import { openModal } from '../../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; -import { boostModal, deleteModal } from '../../initial_state'; +import { boostModal, deleteModal, title } from '../../initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import Icon from 'mastodon/components/icon'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -156,6 +157,23 @@ const makeMapStateToProps = () => { return mapStateToProps; }; +const truncate = (str, num) => { + if (str.length > num) { + return str.slice(0, num) + '…'; + } else { + return str; + } +}; + +const titleFromStatus = status => { + const displayName = status.getIn(['account', 'display_name']); + const username = status.getIn(['account', 'username']); + const prefix = displayName.trim().length === 0 ? username : displayName; + const text = status.get('search_index'); + + return `${prefix}: "${truncate(text, 30)}"`; +}; + export default @injectIntl @connect(makeMapStateToProps) class Status extends ImmutablePureComponent { @@ -605,6 +623,10 @@ class Status extends ImmutablePureComponent { {descendants}
+ + + {titleFromStatus(status)} - {title} + ); } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 68017a5f1..83e10e003 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -60,6 +60,7 @@ class ColumnsArea extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, }; static propTypes = { @@ -212,11 +213,12 @@ class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn, isModalOpen, intl } = this.props; const { shouldAnimate, renderComposePanel } = this.state; + const { signedIn } = this.context.identity; const columnIndex = getIndex(this.context.router.history.location.pathname); if (singleColumn) { - const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : ; + const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : ; const content = columnIndex !== -1 ? ( diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.js index 3d0c48c7a..1c128188f 100644 --- a/app/javascript/mastodon/features/ui/components/compose_panel.js +++ b/app/javascript/mastodon/features/ui/components/compose_panel.js @@ -10,6 +10,10 @@ import { changeComposing } from 'mastodon/actions/compose'; export default @connect() class ComposePanel extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object.isRequired, + }; + static propTypes = { dispatch: PropTypes.func.isRequired, }; @@ -23,11 +27,25 @@ class ComposePanel extends React.PureComponent { } render() { + const { signedIn } = this.context.identity; + return (
- - + + {!signedIn && ( + +
+ + )} + + {signedIn && ( + + + + + )} +
); diff --git a/app/javascript/mastodon/features/ui/components/document_title.js b/app/javascript/mastodon/features/ui/components/document_title.js deleted file mode 100644 index cd081b20c..000000000 --- a/app/javascript/mastodon/features/ui/components/document_title.js +++ /dev/null @@ -1,41 +0,0 @@ -import { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { title } from 'mastodon/initial_state'; - -const mapStateToProps = state => ({ - unread: state.getIn(['missed_updates', 'unread']), -}); - -export default @connect(mapStateToProps) -class DocumentTitle extends PureComponent { - - static propTypes = { - unread: PropTypes.number.isRequired, - }; - - componentDidMount () { - this._sideEffects(); - } - - componentDidUpdate() { - this._sideEffects(); - } - - _sideEffects () { - const { unread } = this.props; - - if (unread > 99) { - document.title = `(*) ${title}`; - } else if (unread > 0) { - document.title = `(${unread}) ${title}`; - } else { - document.title = title; - } - } - - render () { - return null; - } - -} diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js index bbb9b122a..95cd6cf8e 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.js +++ b/app/javascript/mastodon/features/ui/components/link_footer.js @@ -49,20 +49,46 @@ class LinkFooter extends React.PureComponent { render () { const { withHotkeys } = this.props; + const { signedIn, permissions } = this.context.identity; + const items = []; + + if ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) { + items.push(); + } + + if (withHotkeys) { + items.push(); + } + + if (signedIn) { + items.push(); + } + + if (!limitedFederationMode) { + items.push(); + } + + if (profileDirectory) { + items.push(); + } + + items.push(); + items.push(); + + if (signedIn) { + items.push(); + } + + items.push(); + + if (signedIn) { + items.push(); + } return (
    - {((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) &&
  • ·
  • } - {withHotkeys &&
  • ·
  • } -
  • ·
  • - {!limitedFederationMode &&
  • ·
  • } - {profileDirectory &&
  • ·
  • } -
  • ·
  • -
  • ·
  • -
  • ·
  • -
  • ·
  • -
  • +
  • {items.reduce((prev, curr) => [prev, ' · ', curr])}

diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index fe4ed5d77..00ae04761 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -1,5 +1,6 @@ import React from 'react'; -import { NavLink, withRouter } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { NavLink, Link } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import Icon from 'mastodon/components/icon'; import { showTrends } from 'mastodon/initial_state'; @@ -7,30 +8,68 @@ import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container'; +import Logo from 'mastodon/components/logo'; +import SignInBanner from './sign_in_banner'; -const NavigationPanel = () => ( -

- - - - - - - - - - +export default class NavigationPanel extends React.Component { - + static contextTypes = { + router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, + }; -
+ render () { + const { signedIn } = this.context.identity; - - + return ( +
+ - {showTrends &&
} - {showTrends && } -
-); +
-export default withRouter(NavigationPanel); + {signedIn && ( + + + + + + )} + + + + + + {!signedIn && ( + +
+ +
+ )} + + {signedIn && ( + + + + + + + + +
+ + + +
+ )} + + {showTrends && ( + +
+ + + )} +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/sign_in_banner.js b/app/javascript/mastodon/features/ui/components/sign_in_banner.js new file mode 100644 index 000000000..c8403a8ad --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/sign_in_banner.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +const SignInBanner = () => ( +
+

+ +
+); + +export default SignInBanner; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 9a901f12a..5825db1e4 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -20,7 +20,6 @@ import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodo import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; -import DocumentTitle from './components/document_title'; import PictureInPicture from 'mastodon/features/picture_in_picture'; import { Compose, @@ -53,8 +52,9 @@ import { Explore, FollowRecommendations, } from './util/async-components'; -import { me } from '../../initial_state'; +import { me, title } from '../../initial_state'; import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; +import { Helmet } from 'react-helmet'; // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. @@ -110,6 +110,10 @@ const keyMap = { class SwitchingColumnsArea extends React.PureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { children: PropTypes.node, location: PropTypes.object, @@ -145,12 +149,25 @@ class SwitchingColumnsArea extends React.PureComponent { render () { const { children, mobile } = this.props; - const redirect = mobile ? : ; + const { signedIn } = this.context.identity; + + let redirect; + + if (signedIn) { + if (mobile) { + redirect = ; + } else { + redirect = ; + } + } else { + redirect = ; + } return ( {redirect} + @@ -208,6 +225,7 @@ class UI extends React.PureComponent { static contextTypes = { router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, }; static propTypes = { @@ -343,6 +361,8 @@ class UI extends React.PureComponent { } componentDidMount () { + const { signedIn } = this.context.identity; + window.addEventListener('focus', this.handleWindowFocus, false); window.addEventListener('blur', this.handleWindowBlur, false); window.addEventListener('beforeunload', this.handleBeforeUnload, false); @@ -359,16 +379,18 @@ class UI extends React.PureComponent { } // On first launch, redirect to the follow recommendations page - if (this.props.firstLaunch) { + if (signedIn && this.props.firstLaunch) { this.context.router.history.replace('/start'); this.props.dispatch(closeOnboarding()); } - this.props.dispatch(fetchMarkers()); - this.props.dispatch(expandHomeTimeline()); - this.props.dispatch(expandNotifications()); + if (signedIn) { + this.props.dispatch(fetchMarkers()); + this.props.dispatch(expandHomeTimeline()); + this.props.dispatch(expandNotifications()); - setTimeout(() => this.props.dispatch(fetchRules()), 3000); + setTimeout(() => this.props.dispatch(fetchRules()), 3000); + } this.hotkeys.__mousetrap__.stopCallback = (e, element) => { return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); @@ -546,7 +568,10 @@ class UI extends React.PureComponent { - + + + {title} +
); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 709975270..9cc75b6cb 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -3,6 +3,7 @@ const initialState = element && JSON.parse(element.textContent); const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop]; +export const domain = getMeta('domain'); export const reduceMotion = getMeta('reduce_motion'); export const autoPlayGif = getMeta('auto_play_gif'); export const displayMedia = getMeta('display_media'); @@ -26,5 +27,6 @@ export const title = getMeta('title'); export const cropImages = getMeta('crop_images'); export const disableSwiping = getMeta('disable_swiping'); export const languages = initialState && initialState.languages; +export const server = initialState && initialState.server; export default initialState; diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss index 68cad0fde..dcfab6bd0 100644 --- a/app/javascript/styles/mastodon/_mixins.scss +++ b/app/javascript/styles/mastodon/_mixins.scss @@ -20,6 +20,7 @@ font-family: inherit; background: $ui-base-color; color: $darker-text-color; + border-radius: 4px; font-size: 14px; margin: 0; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f5377a858..1f1a5a5ca 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -126,6 +126,7 @@ &:hover { border-color: lighten($ui-primary-color, 4%); color: lighten($darker-text-color, 4%); + text-decoration: none; } &:disabled { @@ -700,6 +701,15 @@ transition: height 0.4s ease, opacity 0.4s ease; } +.sign-in-banner { + padding: 10px; + + p { + color: $darker-text-color; + margin-bottom: 20px; + } +} + .emojione { font-size: inherit; vertical-align: middle; @@ -2214,6 +2224,7 @@ a.account__display-name { > .scrollable { background: $ui-base-color; + border-radius: 0 0 4px 4px; } } @@ -2660,6 +2671,26 @@ a.account__display-name { height: calc(100% - 10px); overflow-y: hidden; + .hero-widget { + box-shadow: none; + + &__text, + &__img, + &__img img { + border-radius: 0; + } + + &__text { + padding: 15px; + color: $secondary-text-color; + + strong { + font-weight: 700; + color: $primary-text-color; + } + } + } + .navigation-bar { padding-top: 20px; padding-bottom: 20px; @@ -2667,10 +2698,6 @@ a.account__display-name { min-height: 20px; } - .flex-spacer { - background: transparent; - } - .compose-form { flex: 1; overflow-y: hidden; @@ -2709,6 +2736,14 @@ a.account__display-name { flex: 0 0 auto; } + .logo { + height: 30px; + width: auto; + } +} + +.navigation-panel, +.compose-panel { hr { flex: 0 0 auto; border: 0; @@ -2836,6 +2871,7 @@ a.account__display-name { box-sizing: border-box; width: 100%; background: lighten($ui-base-color, 4%); + border-radius: 4px 4px 0 0; color: $highlight-text-color; cursor: pointer; flex: 0 0 auto; @@ -3031,6 +3067,17 @@ a.account__display-name { color: $highlight-text-color; } } + + &--logo { + background: transparent; + padding: 10px; + + &:hover, + &:focus, + &:active { + background: transparent; + } + } } .column-link__icon { @@ -3551,6 +3598,7 @@ a.status-card.compact:hover { display: flex; font-size: 16px; background: lighten($ui-base-color, 4%); + border-radius: 4px 4px 0 0; flex: 0 0 auto; cursor: pointer; position: relative; diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb index e48bce060..6d15f3963 100644 --- a/app/lib/permalink_redirector.rb +++ b/app/lib/permalink_redirector.rb @@ -17,10 +17,6 @@ class PermalinkRedirector 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]) - elsif path_segments[1] == 'timelines' && path_segments[2] == 'tag' && path_segments[3].present? - find_tag_url_by_name(path_segments[3]) - elsif path_segments[1] == 'tags' && path_segments[2].present? - find_tag_url_by_name(path_segments[2]) end end end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 5eda87757..df076ffc6 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class InitialStateSerializer < ActiveModel::Serializer + include RoutingHelper + attributes :meta, :compose, :accounts, :media_attachments, :settings, - :languages + :languages, :server has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer has_one :role, serializer: REST::RoleSerializer @@ -82,6 +84,13 @@ class InitialStateSerializer < ActiveModel::Serializer LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] } end + def server + { + hero: instance_presenter.hero&.file&.url || instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), + description: instance_presenter.site_short_description.presence || I18n.t('about.about_mastodon_html'), + } + end + private def instance_presenter diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 3d6283fba..19c5191d8 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,10 +1,14 @@ - content_for :header_tags do - = preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous' - = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous' - = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous' - = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous' + - if user_signed_in? + = preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous' + = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous' + = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous' + = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous' + + = render partial: 'shared/og' %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} + = render_initial_state = javascript_pack_tag 'application', crossorigin: 'anonymous' diff --git a/package.json b/package.json index ca1786038..bef027d26 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "punycode": "^2.1.0", "react": "^16.14.0", "react-dom": "^16.14.0", + "react-helmet": "^6.1.0", "react-hotkeys": "^1.1.4", "react-immutable-proptypes": "^2.2.0", "react-immutable-pure-component": "^2.2.2", diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 70c5c42c5..d845ae01d 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -7,27 +7,21 @@ RSpec.describe HomeController, type: :controller do subject { get :index } context 'when not signed in' do - context 'when requested path is tag timeline' do - it 'redirects to the tag\'s permalink' do - @request.path = '/web/timelines/tag/name' - is_expected.to redirect_to '/tags/name' - 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 context 'when signed in' do let(:user) { Fabricate(:user) } - before { sign_in(user) } + before do + sign_in(user) + end - it 'assigns @body_classes' do - subject - expect(assigns(:body_classes)).to eq 'app-body' + it 'returns http success' do + is_expected.to have_http_status(:success) end end end diff --git a/spec/lib/permalink_redirector_spec.rb b/spec/lib/permalink_redirector_spec.rb index b916b33b2..abda57da4 100644 --- a/spec/lib/permalink_redirector_spec.rb +++ b/spec/lib/permalink_redirector_spec.rb @@ -21,7 +21,7 @@ describe PermalinkRedirector do it 'returns path for legacy tag links' do redirector = described_class.new('web/timelines/tag/hoge') - expect(redirector.redirect_path).to eq '/tags/hoge' + expect(redirector.redirect_path).to be_nil end it 'returns path for pretty account links' do @@ -36,7 +36,7 @@ describe PermalinkRedirector do it 'returns path for pretty tag links' do redirector = described_class.new('web/tags/hoge') - expect(redirector.redirect_path).to eq '/tags/hoge' + expect(redirector.redirect_path).to be_nil end end end diff --git a/yarn.lock b/yarn.lock index 90302b284..3628dd560 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9194,6 +9194,21 @@ react-event-listener@^0.6.0: prop-types "^15.6.0" warning "^4.0.1" +react-fast-compare@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" + react-hotkeys@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72" @@ -9368,6 +9383,11 @@ react-select@^5.4.0: prop-types "^15.6.0" react-transition-group "^4.3.0" +react-side-effect@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" + integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== + react-sparklines@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" -- cgit From 36f4c32a38ed85e5e658b34d36eac40a6147bc0c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 29 Sep 2022 06:22:12 +0200 Subject: Change path of privacy policy page (#19249) --- app/controllers/about_controller.rb | 6 ++---- app/controllers/privacy_controller.rb | 22 ++++++++++++++++++++++ .../mastodon/features/ui/components/link_footer.js | 2 +- app/views/about/terms.html.haml | 9 --------- app/views/layouts/public.html.haml | 5 ++--- app/views/privacy/show.html.haml | 9 +++++++++ app/views/settings/deletes/show.html.haml | 2 +- .../confirmation_instructions.html.haml | 2 +- .../user_mailer/confirmation_instructions.text.erb | 4 ++-- config/locales/en.yml | 9 ++++----- config/routes.rb | 4 +++- spec/controllers/about_controller_spec.rb | 10 ---------- 12 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 app/controllers/privacy_controller.rb delete mode 100644 app/views/about/terms.html.haml create mode 100644 app/views/privacy/show.html.haml (limited to 'spec/controllers') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index d7e78d6b9..4fc2fbe34 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -8,10 +8,10 @@ class AboutController < ApplicationController before_action :require_open_federation!, only: [:show, :more] before_action :set_body_classes, only: :show before_action :set_instance_presenter - before_action :set_expires_in, only: [:more, :terms] + before_action :set_expires_in, only: [:more] before_action :set_registration_form_time, only: :show - skip_before_action :require_functional!, only: [:more, :terms] + skip_before_action :require_functional!, only: [:more] def show; end @@ -26,8 +26,6 @@ class AboutController < ApplicationController @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? end - def terms; end - helper_method :display_blocks? helper_method :display_blocks_rationale? helper_method :public_fetch_mode? diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb new file mode 100644 index 000000000..ced84dbe5 --- /dev/null +++ b/app/controllers/privacy_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class PrivacyController < ApplicationController + layout 'public' + + before_action :set_instance_presenter + before_action :set_expires_in + + skip_before_action :require_functional! + + def show; end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def set_expires_in + expires_in 0, public: true + end +end diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js index 8817bb6c1..dd05d03dd 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.js +++ b/app/javascript/mastodon/features/ui/components/link_footer.js @@ -73,7 +73,7 @@ class LinkFooter extends React.PureComponent { } items.push(); - items.push(); + items.push(); if (signedIn) { items.push(); diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml deleted file mode 100644 index 9d076a91b..000000000 --- a/app/views/about/terms.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- content_for :page_title do - = t('terms.title', instance: site_hostname) - -.grid - .column-0 - .box-widget - .rich-formatting= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html') - .column-1 - = render 'application/sidebar' diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 83640de1a..14f86c970 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -33,8 +33,7 @@ .column-0 %h4= t 'footer.resources' %ul - %li= link_to t('about.terms'), terms_path - %li= link_to t('about.privacy_policy'), terms_path + %li= link_to t('about.privacy_policy'), privacy_policy_path .column-1 %h4= t 'footer.developers' %ul @@ -57,6 +56,6 @@ .legal-xs = link_to "v#{Mastodon::Version.to_s}", Mastodon::Version.source_url · - = link_to t('about.privacy_policy'), terms_path + = 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 new file mode 100644 index 000000000..9d076a91b --- /dev/null +++ b/app/views/privacy/show.html.haml @@ -0,0 +1,9 @@ +- content_for :page_title do + = t('terms.title', instance: site_hostname) + +.grid + .column-0 + .box-widget + .rich-formatting= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html') + .column-1 + = render 'application/sidebar' diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml index 08792e0af..ddf090879 100644 --- a/app/views/settings/deletes/show.html.haml +++ b/app/views/settings/deletes/show.html.haml @@ -16,7 +16,7 @@ %li.positive-hint= t('deletes.warning.email_contact_html', email: Setting.site_contact_email) %li.positive-hint= t('deletes.warning.username_available') - %p.hint= t('deletes.warning.more_details_html', terms_path: terms_path) + %p.hint= t('deletes.warning.more_details_html', terms_path: privacy_policy_path) %hr.spacer/ diff --git a/app/views/user_mailer/confirmation_instructions.html.haml b/app/views/user_mailer/confirmation_instructions.html.haml index 39a83faff..447e689b4 100644 --- a/app/views/user_mailer/confirmation_instructions.html.haml +++ b/app/views/user_mailer/confirmation_instructions.html.haml @@ -77,4 +77,4 @@ %tbody %tr %td.column-cell.text-center - %p= t 'devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: terms_url + %p= t 'devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: privacy_policy_url diff --git a/app/views/user_mailer/confirmation_instructions.text.erb b/app/views/user_mailer/confirmation_instructions.text.erb index aad91cd9d..a1b2ba7d2 100644 --- a/app/views/user_mailer/confirmation_instructions.text.erb +++ b/app/views/user_mailer/confirmation_instructions.text.erb @@ -6,7 +6,7 @@ => <%= confirmation_url(@resource, confirmation_token: @token, redirect_to_app: @resource.created_by_application ? 'true' : nil) %> -<%= strip_tags(t('devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: terms_url)) %> +<%= strip_tags(t('devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: privacy_policy_url)) %> => <%= about_more_url %> -=> <%= terms_url %> +=> <%= privacy_policy_url %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f047f523..dd341e0c8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -28,7 +28,7 @@ en: learn_more: Learn more logged_in_as_html: You are currently logged in as %{username}. logout_before_registering: You are already logged in. - privacy_policy: Privacy policy + privacy_policy: Privacy Policy rules: Server rules rules_html: 'Below is a summary of rules you need to follow if you want to have an account on this server of Mastodon:' see_whats_happening: See what's happening @@ -39,7 +39,6 @@ en: other: posts status_count_before: Who published tagline: Decentralized social network - terms: Terms of service unavailable_content: Moderated servers unavailable_content_description: domain: Server @@ -797,8 +796,8 @@ en: 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, terms of service or other legalese. You can use HTML tags - title: Custom terms of service + 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 @@ -1720,7 +1719,7 @@ en:

This document is CC-BY-SA. It was last updated May 26, 2022.

Originally adapted from the Discourse privacy policy.

- title: "%{instance} Terms of Service and Privacy Policy" + title: "%{instance} Privacy Policy" themes: contrast: Mastodon (High contrast) default: Mastodon (Dark) diff --git a/config/routes.rb b/config/routes.rb index 9491c5177..5d0b3004b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -640,7 +640,9 @@ Rails.application.routes.draw do get '/about', to: 'about#show' get '/about/more', to: 'about#more' - get '/terms', to: 'about#terms' + + get '/privacy-policy', to: 'privacy#show', as: :privacy_policy + get '/terms', to: redirect('/privacy-policy') match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb index 03dddd8c1..40e395a64 100644 --- a/spec/controllers/about_controller_spec.rb +++ b/spec/controllers/about_controller_spec.rb @@ -31,16 +31,6 @@ RSpec.describe AboutController, type: :controller do end end - describe 'GET #terms' do - before do - get :terms - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - describe 'helper_method :new_user' do it 'returns a new User' do user = @controller.view_context.new_user -- cgit