From dd7ef0dc41584089a97444d8192bc61505108e6c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 8 Aug 2017 21:52:15 +0200 Subject: Add ActivityPub inbox (#4216) * Add ActivityPub inbox * Handle ActivityPub deletes * Handle ActivityPub creates * Handle ActivityPub announces * Stubs for handling all activities that need to be handled * Add ActivityPub actor resolving * Handle conversation URI passing in ActivityPub * Handle content language in ActivityPub * Send accept header when fetching actor, handle JSON parse errors * Test for ActivityPub::FetchRemoteAccountService * Handle public key and icon/image when embedded/as array/as resolvable URI * Implement ActivityPub::FetchRemoteStatusService * Add stubs for more interactions * Undo activities implemented * Handle out of order activities * Hook up ActivityPub to ResolveRemoteAccountService, handle Update Account activities * Add fragment IDs to all transient activity serializers * Add tests and fixes * Add stubs for missing tests * Add more tests * Add more tests --- .../activitypub/inboxes_controller_spec.rb | 7 +++++++ .../activitypub/outboxes_controller_spec.rb | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 spec/controllers/activitypub/inboxes_controller_spec.rb create mode 100644 spec/controllers/activitypub/outboxes_controller_spec.rb (limited to 'spec/controllers') diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb new file mode 100644 index 000000000..5c12fea7d --- /dev/null +++ b/spec/controllers/activitypub/inboxes_controller_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::InboxesController, type: :controller do + describe 'POST #create' do + pending + end +end diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb new file mode 100644 index 000000000..f98e4a8c3 --- /dev/null +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::OutboxesController, type: :controller do + let!(:account) { Fabricate(:account) } + + before do + Fabricate(:status, account: account) + end + + describe 'GET #show' do + before do + get :show, params: { account_username: account.username } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + end +end -- cgit From fdea173237cfcd3a6b36f6ebccb0cb1a21cf9294 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Aug 2017 23:54:14 +0200 Subject: Add Digest header to requests with body, handle acct and URI keyId (#4565) --- app/controllers/concerns/signature_verification.rb | 23 ++++++- app/lib/request.rb | 24 ++++++- .../concerns/signature_verification_spec.rb | 78 ++++++++++++++++------ 3 files changed, 100 insertions(+), 25 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index abe845d93..aeb8da879 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -31,7 +31,7 @@ module SignatureVerification return end - account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, '')) + account = account_from_key_id(signature_params['keyId']) if account.nil? @signed_request_account = nil @@ -49,6 +49,10 @@ module SignatureVerification end end + def request_body + @request_body ||= request.raw_post + end + private def build_signed_string(signed_headers) @@ -57,6 +61,8 @@ module SignatureVerification signed_headers.split(' ').map do |signed_header| if signed_header == Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + elsif signed_header == 'digest' + "digest: #{body_digest}" else "#{signed_header}: #{request.headers[to_header_name(signed_header)]}" end @@ -73,6 +79,10 @@ module SignatureVerification (Time.now.utc - time_sent).abs <= 30 end + def body_digest + "SHA-256=#{Digest::SHA256.base64digest(request_body)}" + end + def to_header_name(name) name.split(/-/).map(&:capitalize).join('-') end @@ -81,7 +91,14 @@ module SignatureVerification signature_params['keyId'].blank? || signature_params['signature'].blank? || signature_params['algorithm'].blank? || - signature_params['algorithm'] != 'rsa-sha256' || - !signature_params['keyId'].start_with?('acct:') + signature_params['algorithm'] != 'rsa-sha256' + end + + def account_from_key_id(key_id) + if key_id.start_with?('acct:') + ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, '')) + elsif !ActivityPub::TagManager.instance.local_uri?(key_id) + ActivityPub::FetchRemoteAccountService.new.call(key_id) + end end end diff --git a/app/lib/request.rb b/app/lib/request.rb index e73c5ac20..c01e07925 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -12,15 +12,21 @@ class Request @headers = {} set_common_headers! + set_digest! if options.key?(:body) end - def on_behalf_of(account) + def on_behalf_of(account, key_id_format = :acct) raise ArgumentError unless account.local? - @account = account + + @account = account + @key_id_format = key_id_format + + self end def add_headers(new_headers) @headers.merge!(new_headers) + self end def perform @@ -40,8 +46,11 @@ class Request @headers['Date'] = Time.now.utc.httpdate end + def set_digest! + @headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}" + end + def signature - key_id = @account.to_webfinger_s algorithm = 'rsa-sha256' signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string)) @@ -60,6 +69,15 @@ class Request @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})" 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 + end + def timeout { write: 10, connect: 10, read: 10 } end diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index b371795ab..64648621e 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -16,7 +16,7 @@ describe ApplicationController, type: :controller do end before do - routes.draw { get 'success' => 'anonymous#success' } + routes.draw { match via: [:get, :post], 'success' => 'anonymous#success' } end context 'without signature header' do @@ -40,34 +40,74 @@ describe ApplicationController, type: :controller do context 'with signature header' do let!(:author) { Fabricate(:account) } - before do - get :success + context 'without body' do + before do + get :success - fake_request = Request.new(:get, request.url) - fake_request.on_behalf_of(author) + fake_request = Request.new(:get, request.url) + fake_request.on_behalf_of(author) - request.headers.merge!(fake_request.headers) - end + request.headers.merge!(fake_request.headers) + end - describe '#signed_request?' do - it 'returns true' do - expect(controller.signed_request?).to be true + describe '#signed_request?' do + it 'returns true' do + expect(controller.signed_request?).to be true + end + end + + describe '#signed_request_account' do + it 'returns an account' do + expect(controller.signed_request_account).to eq author + end + + it 'returns nil when path does not match' do + request.path = '/alternative-path' + expect(controller.signed_request_account).to be_nil + end + + it 'returns nil when method does not match' do + post :success + expect(controller.signed_request_account).to be_nil + end end end - describe '#signed_request_account' do - it 'returns an account' do - expect(controller.signed_request_account).to eq author + context 'with body' do + before do + post :success, body: 'Hello world' + + fake_request = Request.new(:post, request.url, body: 'Hello world') + fake_request.on_behalf_of(author) + + request.headers.merge!(fake_request.headers) end - it 'returns nil when path does not match' do - request.path = '/alternative-path' - expect(controller.signed_request_account).to be_nil + describe '#signed_request?' do + it 'returns true' do + expect(controller.signed_request?).to be true + end end - it 'returns nil when method does not match' do - post :success - expect(controller.signed_request_account).to be_nil + describe '#signed_request_account' do + it 'returns an account' do + expect(controller.signed_request_account).to eq author + end + + it 'returns nil when path does not match' do + request.path = '/alternative-path' + expect(controller.signed_request_account).to be_nil + end + + it 'returns nil when method does not match' do + get :success + expect(controller.signed_request_account).to be_nil + end + + it 'returns nil when body has been tampered' do + request.headers['RAW_POST_DATA'] = 'doo doo doo' + expect(controller.signed_request_account).to be_nil + end end end end -- cgit From b7370ac8baa643d93ea727699b3b11f9d3a55bea Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Aug 2017 00:44:41 +0200 Subject: ActivityPub delivery (#4566) * Deliver ActivityPub Like * Deliver ActivityPub Undo-Like * Deliver ActivityPub Create/Announce activities * Deliver ActivityPub creates from mentions * Deliver ActivityPub Block/Undo-Block * Deliver ActivityPub Accept/Reject-Follow * Deliver ActivityPub Undo-Follow * Deliver ActivityPub Follow * Deliver ActivityPub Delete activities Incidentally fix #889 * Adjust BatchedRemoveStatusService for ActivityPub * Add tests for ActivityPub workers * Add tests for FollowService * Add tests for FavouriteService, UnfollowService and PostStatusService * Add tests for ReblogService, BlockService, UnblockService, ProcessMentionsService * Add tests for AuthorizeFollowService, RejectFollowService, RemoveStatusService * Add tests for BatchedRemoveStatusService * Deliver updates to a local account to ActivityPub followers * Minor adjustments --- .../api/v1/accounts/credentials_controller.rb | 3 +- app/controllers/settings/profiles_controller.rb | 1 + app/lib/activitypub/activity.rb | 2 +- app/models/account.rb | 4 ++ app/services/authorize_follow_service.rb | 19 ++++++- app/services/batched_remove_status_service.rb | 43 ++++++++++++-- app/services/block_service.rb | 19 ++++++- app/services/favourite_service.rb | 28 ++++++--- app/services/follow_service.rb | 14 ++++- app/services/post_status_service.rb | 1 + app/services/process_mentions_service.rb | 28 ++++++--- app/services/reblog_service.rb | 28 +++++++-- app/services/reject_follow_service.rb | 19 ++++++- app/services/remove_status_service.rb | 49 +++++++++++++--- app/services/unblock_service.rb | 19 ++++++- app/services/unfavourite_service.rb | 22 +++++++- app/services/unfollow_service.rb | 19 ++++++- app/workers/activitypub/delivery_worker.rb | 37 ++++++++++++ app/workers/activitypub/distribution_worker.rb | 38 +++++++++++++ app/workers/activitypub/processing_worker.rb | 2 +- .../activitypub/update_distribution_worker.rb | 31 ++++++++++ .../api/v1/accounts/credentials_controller_spec.rb | 6 ++ .../settings/profiles_controller_spec.rb | 2 + spec/services/authorize_follow_service_spec.rb | 24 +++++++- .../services/batched_remove_status_service_spec.rb | 7 +++ spec/services/block_service_spec.rb | 19 ++++++- spec/services/favourite_service_spec.rb | 22 +++++++- spec/services/follow_service_spec.rb | 25 ++++++-- spec/services/post_status_service_spec.rb | 8 ++- spec/services/process_mentions_service_spec.rb | 46 +++++++++++---- spec/services/reblog_service_spec.rb | 49 ++++++++++++---- spec/services/reject_follow_service_spec.rb | 24 +++++++- spec/services/remove_status_service_spec.rb | 8 +++ .../resolve_remote_account_service_spec.rb | 66 ++++++++++++---------- spec/services/unblock_service_spec.rb | 22 +++++++- spec/services/unfollow_service_spec.rb | 22 +++++++- spec/workers/activitypub/delivery_worker_spec.rb | 23 ++++++++ .../activitypub/distribution_worker_spec.rb | 48 ++++++++++++++++ spec/workers/activitypub/processing_worker_spec.rb | 15 +++++ .../activitypub/thread_resolve_worker_spec.rb | 16 ++++++ .../activitypub/update_distribution_worker_spec.rb | 20 +++++++ 41 files changed, 785 insertions(+), 113 deletions(-) create mode 100644 app/workers/activitypub/delivery_worker.rb create mode 100644 app/workers/activitypub/distribution_worker.rb create mode 100644 app/workers/activitypub/update_distribution_worker.rb create mode 100644 spec/workers/activitypub/delivery_worker_spec.rb create mode 100644 spec/workers/activitypub/distribution_worker_spec.rb create mode 100644 spec/workers/activitypub/processing_worker_spec.rb create mode 100644 spec/workers/activitypub/thread_resolve_worker_spec.rb create mode 100644 spec/workers/activitypub/update_distribution_worker_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 073808532..90a580c33 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -10,8 +10,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController end def update - current_account.update!(account_params) @account = current_account + @account.update!(account_params) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 0367e3593..c751c64ae 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -15,6 +15,7 @@ class Settings::ProfilesController < ApplicationController def update if @account.update(account_params) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else render :show diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 5debe023a..f8de8060c 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -93,7 +93,7 @@ class ActivityPub::Activity end def distribute_to_followers(status) - DistributionWorker.perform_async(status.id) + ::DistributionWorker.perform_async(status.id) end def delete_arrived_first?(uri) diff --git a/app/models/account.rb b/app/models/account.rb index 163bd1c0e..a7264353e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -171,6 +171,10 @@ class Account < ApplicationRecord reorder(nil).pluck('distinct accounts.domain') end + def inboxes + reorder(nil).where(protocol: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)") + end + def triadic_closures(account, limit: 5, offset: 0) sql = <<-SQL.squish WITH first_degree AS ( diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 41815a393..db35b6030 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -4,11 +4,28 @@ class AuthorizeFollowService < BaseService def call(source_account, target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.authorize! - NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local? + create_notification(follow_request) unless source_account.local? + follow_request end private + def create_notification(follow_request) + if follow_request.account.ostatus? + NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id) + elsif follow_request.account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) + end + end + + def build_json(follow_request) + ActiveModelSerializers::SerializableResource.new( + follow_request, + serializer: ActivityPub::AcceptFollowSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(follow_request) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)) end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index ab810c628..e6c8c9208 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -15,9 +15,11 @@ class BatchedRemoveStatusService < BaseService @mentions = statuses.map { |s| [s.id, s.mentions.includes(:account).to_a] }.to_h @tags = statuses.map { |s| [s.id, s.tags.pluck(:name)] }.to_h - @stream_entry_batches = [] - @salmon_batches = [] - @json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h + @stream_entry_batches = [] + @salmon_batches = [] + @activity_json_batches = [] + @json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h + @activity_json = {} # Ensure that rendered XML reflects destroyed state Status.where(id: statuses.map(&:id)).in_batches.destroy_all @@ -27,7 +29,11 @@ class BatchedRemoveStatusService < BaseService account = account_statuses.first.account unpush_from_home_timelines(account_statuses) - batch_stream_entries(account_statuses) if account.local? + + if account.local? + batch_stream_entries(account_statuses) + batch_activity_json(account, account_statuses) + end end # Cannot be batched @@ -38,6 +44,7 @@ class BatchedRemoveStatusService < BaseService Pubsubhubbub::DistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch } NotificationWorker.push_bulk(@salmon_batches) { |batch| batch } + ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch } end private @@ -50,6 +57,22 @@ class BatchedRemoveStatusService < BaseService end end + def batch_activity_json(account, statuses) + account.followers.inboxes.each do |inbox_url| + statuses.each do |status| + @activity_json_batches << [build_json(status), account.id, inbox_url] + end + end + + statuses.each do |status| + other_recipients = (status.mentions + status.reblogs).map(&:account).reject(&:local?).select(&:activitypub?).uniq(&:id) + + other_recipients.each do |target_account| + @activity_json_batches << [build_json(status), account.id, target_account.inbox_url] + end + end + end + def unpush_from_home_timelines(statuses) account = statuses.first.account recipients = account.followers.local.pluck(:id) @@ -79,7 +102,7 @@ class BatchedRemoveStatusService < BaseService return if @mentions[status.id].empty? payload = stream_entry_to_xml(status.stream_entry.reload) - recipients = @mentions[status.id].map(&:account).reject(&:local?).uniq(&:domain).map(&:id) + recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id) recipients.each do |recipient_id| @salmon_batches << [payload, status.account_id, recipient_id] @@ -111,4 +134,14 @@ class BatchedRemoveStatusService < BaseService def redis Redis.current end + + def build_json(status) + return @activity_json[status.id] if @activity_json.key?(status.id) + + @activity_json[status.id] = ActiveModelSerializers::SerializableResource.new( + status, + serializer: ActivityPub::DeleteSerializer, + adapter: ActivityPub::Adapter + ).to_json + end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 5d7bf6a3b..f2253226b 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -12,11 +12,28 @@ class BlockService < BaseService block = account.block!(target_account) BlockWorker.perform_async(account.id, target_account.id) - NotificationWorker.perform_async(build_xml(block), account.id, target_account.id) unless target_account.local? + create_notification(block) unless target_account.local? + block end private + def create_notification(block) + if block.target_account.ostatus? + NotificationWorker.perform_async(build_xml(block), block.account_id, block.target_account_id) + elsif block.target_account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url) + end + end + + def build_json(block) + ActiveModelSerializers::SerializableResource.new( + block, + serializer: ActivityPub::BlockSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(block) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block)) end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 291f9e56e..4aa935170 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -15,18 +15,32 @@ class FavouriteService < BaseService return favourite unless favourite.nil? favourite = Favourite.create!(account: account, status: status) - - if status.local? - NotifyService.new.call(favourite.status.account, favourite) - else - NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) - end - + create_notification(favourite) favourite end private + def create_notification(favourite) + status = favourite.status + + if status.account.local? + NotifyService.new.call(status.account, favourite) + elsif status.account.ostatus? + NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id) + elsif status.account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) + end + end + + def build_json(favourite) + ActiveModelSerializers::SerializableResource.new( + favourite, + serializer: ActivityPub::LikeSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(favourite) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite)) end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 3155feaa4..2be625cd8 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -14,7 +14,7 @@ class FollowService < BaseService return if source_account.following?(target_account) - if target_account.locked? + if target_account.locked? || target_account.activitypub? request_follow(source_account, target_account) else direct_follow(source_account, target_account) @@ -28,9 +28,11 @@ class FollowService < BaseService if target_account.local? NotifyService.new.call(target_account, follow_request) - else + elsif target_account.ostatus? NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id) AfterRemoteFollowRequestWorker.perform_async(follow_request.id) + elsif target_account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url) end follow_request @@ -63,4 +65,12 @@ class FollowService < BaseService def build_follow_xml(follow) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow)) end + + def build_json(follow_request) + ActiveModelSerializers::SerializableResource.new( + follow_request, + serializer: ActivityPub::FollowSerializer, + adapter: ActivityPub::Adapter + ).to_json + end end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 951a38e19..5ff93f21e 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -39,6 +39,7 @@ class PostStatusService < BaseService LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? DistributionWorker.perform_async(status.id) Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id) + ActivityPub::DistributionWorker.perform_async(status.id) if options[:idempotency].present? redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id) diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 438033d22..407fa8c18 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -28,18 +28,32 @@ class ProcessMentionsService < BaseService end status.mentions.includes(:account).each do |mention| - mentioned_account = mention.account - - if mentioned_account.local? - NotifyService.new.call(mentioned_account, mention) - else - NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id) - end + create_notification(status, mention) end end private + def create_notification(status, mention) + mentioned_account = mention.account + + if mentioned_account.local? + NotifyService.new.call(mentioned_account, mention) + elsif mentioned_account.ostatus? + NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id) + elsif mentioned_account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url) + end + end + + def build_json(status) + ActiveModelSerializers::SerializableResource.new( + status, + serializer: ActivityPub::ActivitySerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def follow_remote_account_service @follow_remote_account_service ||= ResolveRemoteAccountService.new end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index ba24b1f9d..7f886af7c 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -21,13 +21,31 @@ class ReblogService < BaseService DistributionWorker.perform_async(reblog.id) Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) + ActivityPub::DistributionWorker.perform_async(reblog.id) - if reblogged_status.local? - NotifyService.new.call(reblog.reblog.account, reblog) - else - NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), account.id, reblog.reblog.account_id) + create_notification(reblog) + reblog + end + + private + + def create_notification(reblog) + reblogged_status = reblog.reblog + + if reblogged_status.account.local? + NotifyService.new.call(reblogged_status.account, reblog) + elsif reblogged_status.account.ostatus? + NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), reblog.account_id, reblogged_status.account_id) + elsif reblogged_status.account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) end + end - reblog + def build_json(reblog) + ActiveModelSerializers::SerializableResource.new( + reblog, + serializer: ActivityPub::ActivitySerializer, + adapter: ActivityPub::Adapter + ).to_json end end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index fd7e66c23..a91266aa4 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -4,11 +4,28 @@ class RejectFollowService < BaseService def call(source_account, target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.reject! - NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local? + create_notification(follow_request) unless source_account.local? + follow_request end private + def create_notification(follow_request) + if follow_request.account.ostatus? + NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id) + elsif follow_request.account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) + end + end + + def build_json(follow_request) + ActiveModelSerializers::SerializableResource.new( + follow_request, + serializer: ActivityPub::RejectFollowSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(follow_request) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)) end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index a5281f586..fcccbaa24 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -22,8 +22,10 @@ class RemoveStatusService < BaseService return unless @account.local? - remove_from_mentioned(@stream_entry.reload) - Pubsubhubbub::DistributionWorker.perform_async(@stream_entry.id) + @stream_entry = @stream_entry.reload + + remove_from_remote_followers + remove_from_remote_affected end private @@ -38,15 +40,48 @@ class RemoveStatusService < BaseService end end - def remove_from_mentioned(stream_entry) - salmon_xml = stream_entry_to_xml(stream_entry) - target_accounts = @mentions.map(&:account).reject(&:local?).uniq(&:domain) + def remove_from_remote_affected + # People who got mentioned in the status, or who + # reblogged it from someone else might not follow + # the author and wouldn't normally receive the + # delete notification - so here, we explicitly + # send it to them + + target_accounts = (@mentions.map(&:account).reject(&:local?) + @reblogs.map(&:account).reject(&:local?)).uniq(&:id) + + # Ostatus + NotificationWorker.push_bulk(target_accounts.select(&:ostatus?).uniq(&:domain)) do |target_account| + [salmon_xml, @account.id, target_account.id] + end + + # ActivityPub + ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:inbox_url)) do |inbox_url| + [activity_json, @account.id, inbox_url] + end + end + + def remove_from_remote_followers + # OStatus + Pubsubhubbub::DistributionWorker.perform_async(@stream_entry.id) - NotificationWorker.push_bulk(target_accounts) do |target_account| - [salmon_xml, stream_entry.account_id, target_account.id] + # ActivityPub + ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| + [activity_json, @account.id, inbox_url] end end + def salmon_xml + @salmon_xml ||= stream_entry_to_xml(@stream_entry) + end + + def activity_json + @activity_json ||= ActiveModelSerializers::SerializableResource.new( + @status, + serializer: ActivityPub::DeleteSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def remove_reblogs # We delete reblogs of the status before the original status, # because once original status is gone, reblogs will disappear diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index ff15c7275..72fc5ab15 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -5,11 +5,28 @@ class UnblockService < BaseService return unless account.blocking?(target_account) unblock = account.unblock!(target_account) - NotificationWorker.perform_async(build_xml(unblock), account.id, target_account.id) unless target_account.local? + create_notification(unblock) unless target_account.local? + unblock end private + def create_notification(unblock) + if unblock.target_account.ostatus? + NotificationWorker.perform_async(build_xml(unblock), unblock.account_id, unblock.target_account_id) + elsif unblock.target_account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url) + end + end + + def build_json(unblock) + ActiveModelSerializers::SerializableResource.new( + unblock, + serializer: ActivityPub::UndoBlockSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(block) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block)) end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index 564aaee46..e53798e66 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -4,14 +4,30 @@ class UnfavouriteService < BaseService def call(account, status) favourite = Favourite.find_by!(account: account, status: status) favourite.destroy! - - NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) unless status.local? - + create_notification(favourite) unless status.local? favourite end private + def create_notification(favourite) + status = favourite.status + + if status.account.ostatus? + NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id) + elsif status.account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) + end + end + + def build_json(favourite) + ActiveModelSerializers::SerializableResource.new( + favourite, + serializer: ActivityPub::UndoLikeSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(favourite) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite)) end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 388909586..10af75146 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -7,12 +7,29 @@ class UnfollowService < BaseService def call(source_account, target_account) follow = source_account.unfollow!(target_account) return unless follow - NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local? + create_notification(follow) unless target_account.local? UnmergeWorker.perform_async(target_account.id, source_account.id) + follow end private + def create_notification(follow) + if follow.target_account.ostatus? + NotificationWorker.perform_async(build_xml(follow), follow.account_id, follow.target_account_id) + elsif follow.target_account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url) + end + end + + def build_json(follow) + ActiveModelSerializers::SerializableResource.new( + follow, + serializer: ActivityPub::UndoFollowSerializer, + adapter: ActivityPub::Adapter + ).to_json + end + def build_xml(follow) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow)) end diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb new file mode 100644 index 000000000..cd67b6710 --- /dev/null +++ b/app/workers/activitypub/delivery_worker.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class ActivityPub::DeliveryWorker + include Sidekiq::Worker + + sidekiq_options queue: 'push', retry: 5, dead: false + + HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze + + def perform(json, source_account_id, inbox_url) + @json = json + @source_account = Account.find(source_account_id) + @inbox_url = inbox_url + + perform_request + + raise Mastodon::UnexpectedResponseError, @response unless response_successful? + rescue => e + raise e.class, "Delivery failed for #{inbox_url}: #{e.message}" + end + + private + + def build_request + request = Request.new(:post, @inbox_url, body: @json) + request.on_behalf_of(@source_account, :uri) + request.add_headers(HEADERS) + end + + def perform_request + @response = build_request.perform + end + + def response_successful? + @response.code > 199 && @response.code < 300 + end +end diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb new file mode 100644 index 000000000..004dd25d1 --- /dev/null +++ b/app/workers/activitypub/distribution_worker.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class ActivityPub::DistributionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'push' + + def perform(status_id) + @status = Status.find(status_id) + @account = @status.account + + return if skip_distribution? + + ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + [payload, @account.id, inbox_url] + end + rescue ActiveRecord::RecordNotFound + true + end + + private + + def skip_distribution? + @status.direct_visibility? + end + + def inboxes + @inboxes ||= @account.followers.inboxes + end + + def payload + @payload ||= ActiveModelSerializers::SerializableResource.new( + @status, + serializer: ActivityPub::ActivitySerializer, + adapter: ActivityPub::Adapter + ).to_json + end +end diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb index 7656ab56a..bb9adf64b 100644 --- a/app/workers/activitypub/processing_worker.rb +++ b/app/workers/activitypub/processing_worker.rb @@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker sidekiq_options backtrace: true def perform(account_id, body) - ProcessCollectionService.new.call(body, Account.find(account_id)) + ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id)) end end diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb new file mode 100644 index 000000000..f3377dcec --- /dev/null +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ActivityPub::UpdateDistributionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'push' + + def perform(account_id) + @account = Account.find(account_id) + + ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + [payload, @account.id, inbox_url] + end + rescue ActiveRecord::RecordNotFound + true + end + + private + + def inboxes + @inboxes ||= @account.followers.inboxes + end + + def payload + @payload ||= ActiveModelSerializers::SerializableResource.new( + @account, + serializer: ActivityPub::UpdateSerializer, + adapter: ActivityPub::Adapter + ).to_json + end +end diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index 4a3100348..bc89772b9 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -20,6 +20,8 @@ describe Api::V1::Accounts::CredentialsController do describe 'PATCH #update' do describe 'with valid data' do before do + allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) + patch :update, params: { display_name: "Alice Isn't Dead", note: "Hi!\n\nToot toot!", @@ -40,6 +42,10 @@ describe Api::V1::Accounts::CredentialsController do expect(user.account.avatar).to exist expect(user.account.header).to exist end + + it 'queues up an account update distribution' do + expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id) + end end describe 'with invalid data' do diff --git a/spec/controllers/settings/profiles_controller_spec.rb b/spec/controllers/settings/profiles_controller_spec.rb index e502dbda7..ee3315be6 100644 --- a/spec/controllers/settings/profiles_controller_spec.rb +++ b/spec/controllers/settings/profiles_controller_spec.rb @@ -17,11 +17,13 @@ RSpec.describe Settings::ProfilesController, type: :controller do describe 'PUT #update' do it 'updates the user profile' do + allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) account = Fabricate(:account, user: @user, display_name: 'Old name') put :update, params: { account: { display_name: 'New name' } } expect(account.reload.display_name).to eq 'New name' expect(response).to redirect_to(settings_profile_path) + expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id) end end end diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index 3f3a2bc56..d74eb41a2 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe AuthorizeFollowService do end end - describe 'remote' do + describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do @@ -46,4 +46,26 @@ RSpec.describe AuthorizeFollowService do }).to have_been_made.once end end + + describe 'remote ActivityPub' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } + + before do + FollowRequest.create(account: bob, target_account: sender) + stub_request(:post, bob.inbox_url).to_return(status: 200) + subject.call(bob, sender) + end + + it 'removes follow request' do + expect(bob.requested?(sender)).to be false + end + + it 'creates follow relation' do + expect(bob.following?(sender)).to be true + end + + it 'sends an accept activity' do + expect(a_request(:post, bob.inbox_url)).to have_been_made.once + end + end end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index c20085e25..2484d4b58 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe BatchedRemoveStatusService do let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } let!(:jeff) { Fabricate(:account) } + let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let(:status1) { PostStatusService.new.call(alice, 'Hello @bob@example.com') } let(:status2) { PostStatusService.new.call(alice, 'Another status') } @@ -15,9 +16,11 @@ RSpec.describe BatchedRemoveStatusService do stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) + stub_request(:post, 'http://example.com/inbox').to_return(status: 200) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.follow!(alice) + hank.follow!(alice) status1 status2 @@ -58,4 +61,8 @@ RSpec.describe BatchedRemoveStatusService do xml.match(TagManager::VERBS[:delete]) }).to have_been_made.once end + + it 'sends delete activity to followers' do + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once + end end diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index 2a54e032e..bd2ab3d53 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -17,7 +17,7 @@ RSpec.describe BlockService do end end - describe 'remote' do + describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do @@ -36,4 +36,21 @@ RSpec.describe BlockService do }).to have_been_made.once end end + + describe 'remote ActivityPub' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } + + before do + stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + subject.call(sender, bob) + end + + it 'creates a blocking relation' do + expect(sender.blocking?(bob)).to be true + end + + it 'sends a block activity' do + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + end + end end diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 36f1b64d4..2ab1f32ca 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -18,8 +18,8 @@ RSpec.describe FavouriteService do end end - describe 'remote' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } + describe 'remote OStatus' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') } before do @@ -38,4 +38,22 @@ RSpec.describe FavouriteService do }).to have_been_made.once end end + + describe 'remote ActivityPub' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :activitypub, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } + let(:status) { Fabricate(:status, account: bob) } + + before do + stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {}) + subject.call(sender, status) + end + + it 'creates a favourite' do + expect(status.favourites.first).to_not be_nil + end + + it 'sends a like activity' do + expect(a_request(:post, "http://example.com/inbox")).to have_been_made.once + end + end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 32dedb3ad..1e2378031 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -44,9 +44,9 @@ RSpec.describe FollowService do end end - context 'remote account' do + context 'remote OStatus account' do describe 'locked account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) @@ -66,7 +66,7 @@ RSpec.describe FollowService do end describe 'unlocked account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } before do stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) @@ -91,7 +91,7 @@ RSpec.describe FollowService do end describe 'already followed account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } before do sender.follow!(bob) @@ -111,4 +111,21 @@ RSpec.describe FollowService do end end end + + context 'remote ActivityPub account' do + let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } + + before do + stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {}) + subject.call(sender, bob.acct) + end + + it 'creates follow request' do + expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil + end + + it 'sends a follow activity to the inbox' do + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + end + end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 57876dcc2..4182c4e1f 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -100,16 +100,18 @@ RSpec.describe PostStatusService do expect(hashtags_service).to have_received(:call).with(status) end - it 'pings PuSH hubs' do + it 'gets distributed' do allow(DistributionWorker).to receive(:perform_async) allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async) + allow(ActivityPub::DistributionWorker).to receive(:perform_async) + account = Fabricate(:account) status = subject.call(account, "test status update") expect(DistributionWorker).to have_received(:perform_async).with(status.id) - expect(Pubsubhubbub::DistributionWorker). - to have_received(:perform_async).with(status.stream_entry.id) + expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id) + expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id) end it 'crawls links' do diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 984d13746..09f8fa45b 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -1,22 +1,44 @@ require 'rails_helper' RSpec.describe ProcessMentionsService do - let(:account) { Fabricate(:account, username: 'alice') } - let(:remote_user) { Fabricate(:account, username: 'remote_user', domain: 'example.com', salmon_url: 'http://salmon.example.com') } - let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") } + let(:account) { Fabricate(:account, username: 'alice') } + let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") } - subject { ProcessMentionsService.new } + context 'OStatus' do + let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') } - before do - stub_request(:post, remote_user.salmon_url) - subject.(status) - end + subject { ProcessMentionsService.new } + + before do + stub_request(:post, remote_user.salmon_url) + subject.call(status) + end - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 + it 'creates a mention' do + expect(remote_user.mentions.where(status: status).count).to eq 1 + end + + it 'posts to remote user\'s Salmon end point' do + expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once + end end - it 'posts to remote user\'s Salmon end point' do - expect(a_request(:post, remote_user.salmon_url)).to have_been_made + context 'ActivityPub' do + let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + + subject { ProcessMentionsService.new } + + before do + stub_request(:post, remote_user.inbox_url) + subject.call(status) + end + + it 'creates a mention' do + expect(remote_user.mentions.where(status: status).count).to eq 1 + end + + it 'sends activity to the inbox' do + expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once + end end end diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 5f89169e9..0ad5c5f6b 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -2,22 +2,49 @@ require 'rails_helper' RSpec.describe ReblogService do let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') } - let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') } - subject { ReblogService.new } + context 'OStatus' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') } + let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') } - before do - stub_request(:post, 'http://salmon.example.com') + subject { ReblogService.new } - subject.(alice, status) - end + before do + stub_request(:post, 'http://salmon.example.com') + subject.call(alice, status) + end + + it 'creates a reblog' do + expect(status.reblogs.count).to eq 1 + end - it 'creates a reblog' do - expect(status.reblogs.count).to eq 1 + it 'sends a Salmon slap for a remote reblog' do + expect(a_request(:post, 'http://salmon.example.com')).to have_been_made + end end - it 'sends a Salmon slap for a remote reblog' do - expect(a_request(:post, 'http://salmon.example.com')).to have_been_made + context 'ActivityPub' do + let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + let(:status) { Fabricate(:status, account: bob) } + + subject { ReblogService.new } + + before do + stub_request(:post, bob.inbox_url) + allow(ActivityPub::DistributionWorker).to receive(:perform_async) + subject.call(alice, status) + end + + it 'creates a reblog' do + expect(status.reblogs.count).to eq 1 + end + + it 'distributes to followers' do + expect(ActivityPub::DistributionWorker).to have_received(:perform_async) + end + + it 'sends an announce activity to the author' do + expect(a_request(:post, bob.inbox_url)).to have_been_made.once + end end end diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index 50749b633..2e06345b3 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe RejectFollowService do end end - describe 'remote' do + describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do @@ -46,4 +46,26 @@ RSpec.describe RejectFollowService do }).to have_been_made.once end end + + describe 'remote ActivityPub' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } + + before do + FollowRequest.create(account: bob, target_account: sender) + stub_request(:post, bob.inbox_url).to_return(status: 200) + subject.call(bob, sender) + end + + it 'removes follow request' do + expect(bob.requested?(sender)).to be false + end + + it 'does not create follow relation' do + expect(bob.following?(sender)).to be false + end + + it 'sends a reject activity' do + expect(a_request(:post, bob.inbox_url)).to have_been_made.once + end + end end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index a3bce7613..dc6b350cb 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -6,13 +6,17 @@ RSpec.describe RemoveStatusService do let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } let!(:jeff) { Fabricate(:account) } + let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } before do stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) + stub_request(:post, 'http://example.com/inbox').to_return(status: 200) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.follow!(alice) + hank.follow!(alice) + @status = PostStatusService.new.call(alice, 'Hello @bob@example.com') subject.call(@status) end @@ -31,6 +35,10 @@ RSpec.describe RemoveStatusService do }).to have_been_made end + it 'sends delete activity to followers' do + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice + end + it 'sends Salmon slap to previously mentioned users' do expect(a_request(:post, "http://example.com/salmon").with { |req| xml = OStatus2::Salmon.new.unpack(req.body) diff --git a/spec/services/resolve_remote_account_service_spec.rb b/spec/services/resolve_remote_account_service_spec.rb index c3b902b34..d0eab2310 100644 --- a/spec/services/resolve_remote_account_service_spec.rb +++ b/spec/services/resolve_remote_account_service_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe ResolveRemoteAccountService do - subject { ResolveRemoteAccountService.new } + subject { described_class.new } before do stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) @@ -29,29 +29,6 @@ RSpec.describe ResolveRemoteAccountService do expect(subject.call('catsrgr8@example.com')).to be_nil end - it 'returns an already existing remote account' do - old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no') - returned_account = subject.call('gargron@quitter.no') - - expect(old_account.id).to eq returned_account.id - end - - it 'returns a new remote account' do - account = subject.call('gargron@quitter.no') - - expect(account.username).to eq 'gargron' - expect(account.domain).to eq 'quitter.no' - expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' - end - - it 'follows a legitimate account redirection' do - account = subject.call('gargron@redirected.com') - - expect(account.username).to eq 'gargron' - expect(account.domain).to eq 'quitter.no' - expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' - end - it 'prevents hijacking existing accounts' do account = subject.call('hacker1@redirected.com') expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477' @@ -61,12 +38,41 @@ RSpec.describe ResolveRemoteAccountService do expect(subject.call('hacker2@redirected.com')).to be_nil end - it 'returns a new remote account' do - account = subject.call('foo@localdomain.com') + context 'with an OStatus account' do + it 'returns an already existing remote account' do + old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no') + returned_account = subject.call('gargron@quitter.no') + + expect(old_account.id).to eq returned_account.id + end + + it 'returns a new remote account' do + account = subject.call('gargron@quitter.no') + + expect(account.username).to eq 'gargron' + expect(account.domain).to eq 'quitter.no' + expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' + end + + it 'follows a legitimate account redirection' do + account = subject.call('gargron@redirected.com') + + expect(account.username).to eq 'gargron' + expect(account.domain).to eq 'quitter.no' + expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' + end + + it 'returns a new remote account' do + account = subject.call('foo@localdomain.com') + + expect(account.username).to eq 'foo' + expect(account.domain).to eq 'localdomain.com' + expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom' + end + end - expect(account.username).to eq 'foo' - expect(account.domain).to eq 'localdomain.com' - expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom' + context 'with an ActivityPub account' do + pending end it 'processes one remote account at a time using locks' do @@ -78,7 +84,7 @@ RSpec.describe ResolveRemoteAccountService do Thread.new do true while wait_for_start begin - return_values << ResolveRemoteAccountService.new.call('foo@localdomain.com') + return_values << described_class.new.call('foo@localdomain.com') rescue ActiveRecord::RecordNotUnique fail_occurred = true end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index 1b9ae1239..def4981e7 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe UnblockService do end end - describe 'remote' do + describe 'remote OStatus' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do @@ -28,7 +28,7 @@ RSpec.describe UnblockService do end it 'destroys the blocking relation' do - expect(sender.following?(bob)).to be false + expect(sender.blocking?(bob)).to be false end it 'sends an unblock salmon slap' do @@ -38,4 +38,22 @@ RSpec.describe UnblockService do }).to have_been_made.once end end + + describe 'remote ActivityPub' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } + + before do + sender.block!(bob) + stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + subject.call(sender, bob) + end + + it 'destroys the blocking relation' do + expect(sender.blocking?(bob)).to be false + end + + it 'sends an unblock activity' do + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + end + end end diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index 8ec2148a1..29040431e 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -18,8 +18,8 @@ RSpec.describe UnfollowService do end end - describe 'remote' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } + describe 'remote OStatus' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } before do sender.follow!(bob) @@ -38,4 +38,22 @@ RSpec.describe UnfollowService do }).to have_been_made.once end end + + describe 'remote ActivityPub' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } + + before do + sender.follow!(bob) + stub_request(:post, 'http://example.com/inbox').to_return(status: 200) + subject.call(sender, bob) + end + + it 'destroys the following relation' do + expect(sender.following?(bob)).to be false + end + + it 'sends an unfollow activity' do + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once + end + end end diff --git a/spec/workers/activitypub/delivery_worker_spec.rb b/spec/workers/activitypub/delivery_worker_spec.rb new file mode 100644 index 000000000..351be185c --- /dev/null +++ b/spec/workers/activitypub/delivery_worker_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ActivityPub::DeliveryWorker do + subject { described_class.new } + + let(:sender) { Fabricate(:account) } + let(:payload) { 'test' } + + describe 'perform' do + it 'performs a request' do + stub_request(:post, 'https://example.com/api').to_return(status: 200) + subject.perform(payload, sender.id, 'https://example.com/api') + expect(a_request(:post, 'https://example.com/api')).to have_been_made.once + end + + it 'raises when request fails' do + stub_request(:post, 'https://example.com/api').to_return(status: 500) + expect { subject.perform(payload, sender.id, 'https://example.com/api') }.to raise_error Mastodon::UnexpectedResponseError + end + end +end diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb new file mode 100644 index 000000000..368ca025a --- /dev/null +++ b/spec/workers/activitypub/distribution_worker_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +describe ActivityPub::DistributionWorker do + subject { described_class.new } + + let(:status) { Fabricate(:status) } + let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') } + + describe '#perform' do + before do + allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) + follower.follow!(status.account) + end + + context 'with public status' do + before do + status.update(visibility: :public) + end + + it 'delivers to followers' do + subject.perform(status.id) + expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) + end + end + + context 'with private status' do + before do + status.update(visibility: :private) + end + + it 'delivers to followers' do + subject.perform(status.id) + expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) + end + end + + context 'with direct status' do + before do + status.update(visibility: :direct) + end + + it 'does nothing' do + subject.perform(status.id) + expect(ActivityPub::DeliveryWorker).to_not have_received(:push_bulk) + end + end + end +end diff --git a/spec/workers/activitypub/processing_worker_spec.rb b/spec/workers/activitypub/processing_worker_spec.rb new file mode 100644 index 000000000..b42c0bdbc --- /dev/null +++ b/spec/workers/activitypub/processing_worker_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe ActivityPub::ProcessingWorker do + subject { described_class.new } + + let(:account) { Fabricate(:account) } + + describe '#perform' do + it 'delegates to ActivityPub::ProcessCollectionService' do + allow(ActivityPub::ProcessCollectionService).to receive(:new).and_return(double(:service, call: nil)) + subject.perform(account.id, '') + expect(ActivityPub::ProcessCollectionService).to have_received(:new) + end + end +end diff --git a/spec/workers/activitypub/thread_resolve_worker_spec.rb b/spec/workers/activitypub/thread_resolve_worker_spec.rb new file mode 100644 index 000000000..b954cb62c --- /dev/null +++ b/spec/workers/activitypub/thread_resolve_worker_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe ActivityPub::ThreadResolveWorker do + subject { described_class.new } + + let(:status) { Fabricate(:status) } + let(:parent) { Fabricate(:status) } + + describe '#perform' do + it 'gets parent from ActivityPub::FetchRemoteStatusService and glues them together' do + allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(double(:service, call: parent)) + subject.perform(status.id, 'http://example.com/123') + expect(status.reload.in_reply_to_id).to eq parent.id + end + end +end diff --git a/spec/workers/activitypub/update_distribution_worker_spec.rb b/spec/workers/activitypub/update_distribution_worker_spec.rb new file mode 100644 index 000000000..688a424d5 --- /dev/null +++ b/spec/workers/activitypub/update_distribution_worker_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +describe ActivityPub::UpdateDistributionWorker do + subject { described_class.new } + + let(:account) { Fabricate(:account) } + let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') } + + describe '#perform' do + before do + allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) + follower.follow!(account) + end + + it 'delivers to followers' do + subject.perform(account.id) + expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) + end + end +end -- cgit From a2aeacbfeed5dc7070c37a22bb2c4bac1a58a526 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Aug 2017 00:45:04 +0200 Subject: Add alternate links to ActivityPub resources from HTML/HEAD variants (#4586) --- app/controllers/concerns/account_controller_concern.rb | 8 ++++++++ app/controllers/statuses_controller.rb | 7 ++++++- app/controllers/stream_entries_controller.rb | 7 ++++++- app/views/accounts/show.html.haml | 1 + app/views/stream_entries/show.html.haml | 1 + spec/controllers/concerns/account_controller_concern_spec.rb | 2 +- spec/controllers/stream_entries_controller_spec.rb | 2 +- 7 files changed, 24 insertions(+), 4 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index d36fc8c93..5b9981aa2 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -23,6 +23,7 @@ module AccountControllerConcern [ webfinger_account_link, atom_account_url_link, + actor_url_link, ] ) end @@ -41,6 +42,13 @@ module AccountControllerConcern ] end + def actor_url_link + [ + ActivityPub::TagManager.instance.uri_for(@account), + [%w(rel alternate), %w(type application/activity+json)], + ] + end + def webfinger_account_url webfinger_url(resource: @account.to_webfinger_s) end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 8e0ce0ec3..0cce2ba23 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -36,7 +36,12 @@ class StatusesController < ApplicationController end def set_link_headers - response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) + response.headers['Link'] = LinkHeader.new( + [ + [account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], + [ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]], + ] + ) end def set_status diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 3eb91d830..ccb15495e 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -38,7 +38,12 @@ class StreamEntriesController < ApplicationController end def set_link_headers - response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) + response.headers['Link'] = LinkHeader.new( + [ + [account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], + [ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]], + ] + ) end def set_stream_entry diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 150c14791..74e695fc3 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,6 +7,7 @@ %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ + %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ %meta{ property: 'og:type', content: 'profile' }/ = render 'og', account: @account, url: short_account_url(@account, only_path: false) diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml index 80ea30eb1..5ef72f804 100644 --- a/app/views/stream_entries/show.html.haml +++ b/app/views/stream_entries/show.html.haml @@ -4,6 +4,7 @@ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/ + %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@stream_entry.activity) }/ %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:type', content: 'article' }/ diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb index bdc181edc..ae46f9ba6 100644 --- a/spec/controllers/concerns/account_controller_concern_spec.rb +++ b/spec/controllers/concerns/account_controller_concern_spec.rb @@ -33,7 +33,7 @@ describe ApplicationController, type: :controller do it 'sets link headers' do account = Fabricate(:account, username: 'username') get 'success', params: { account_username: 'username' } - expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml"' + expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml", ; rel="alternate"; type="application/activity+json"' end it 'returns http success' do diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb index 2cc428e0c..808cf667c 100644 --- a/spec/controllers/stream_entries_controller_spec.rb +++ b/spec/controllers/stream_entries_controller_spec.rb @@ -21,7 +21,7 @@ RSpec.describe StreamEntriesController, type: :controller do get route, params: { account_username: alice.username, id: status.stream_entry.id } - expect(response.headers['Link'].to_s).to eq "; rel=\"alternate\"; type=\"application/atom+xml\"" + expect(response.headers['Link'].to_s).to eq "; rel=\"alternate\"; type=\"application/atom+xml\", ; rel=\"alternate\"; type=\"application/activity+json\"" end end -- cgit From 4e75f0d88932511ad154773f4c77a485367ed36c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Aug 2017 02:29:36 +0200 Subject: Hook up URL-based resource look-up to ActivityPub (#4589) --- app/helpers/jsonld_helper.rb | 8 ++- .../activitypub/fetch_remote_account_service.rb | 4 +- .../activitypub/fetch_remote_status_service.rb | 4 +- app/services/fetch_atom_service.rb | 72 ++++++++++++++-------- app/services/fetch_remote_account_service.rb | 15 +++-- app/services/fetch_remote_status_service.rb | 15 +++-- .../api/subscriptions_controller_spec.rb | 26 ++++---- spec/services/process_feed_service_spec.rb | 5 +- 8 files changed, 92 insertions(+), 57 deletions(-) (limited to 'spec/controllers') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index b0db025bc..c750a7038 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -16,7 +16,11 @@ module JsonLdHelper def fetch_resource(uri) response = build_request(uri).perform return if response.code != 200 - Oj.load(response.to_s, mode: :strict) + body_to_json(response.to_s) + end + + def body_to_json(body) + body.nil? ? nil : Oj.load(body, mode: :strict) rescue Oj::ParseError nil end @@ -25,7 +29,7 @@ module JsonLdHelper def build_request(uri) request = Request.new(:get, uri) - request.add_headers('Accept' => 'application/activity+json') + request.add_headers('Accept' => 'application/activity+json, application/ld+json') request end end diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index e443b9463..3eeca585e 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -5,8 +5,8 @@ class ActivityPub::FetchRemoteAccountService < BaseService # Should be called when uri has already been checked for locality # Does a WebFinger roundtrip on each call - def call(uri) - @json = fetch_resource(uri) + def call(uri, prefetched_json = nil) + @json = body_to_json(prefetched_json) || fetch_resource(uri) return unless supported_context? && expected_type? diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 80305c53d..993e5389c 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -4,8 +4,8 @@ class ActivityPub::FetchRemoteStatusService < BaseService include JsonLdHelper # Should be called when uri has already been checked for locality - def call(uri) - @json = fetch_resource(uri) + def call(uri, prefetched_json = nil) + @json = body_to_json(prefetched_json) || fetch_resource(uri) return unless supported_context? && expected_type? diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb index 3ac441e3e..c6a4dc2e9 100644 --- a/app/services/fetch_atom_service.rb +++ b/app/services/fetch_atom_service.rb @@ -4,18 +4,10 @@ class FetchAtomService < BaseService def call(url) return if url.blank? - response = Request.new(:head, url).perform + @url = url - Rails.logger.debug "Remote status HEAD request returned code #{response.code}" - - response = Request.new(:get, url).perform if response.code == 405 - - Rails.logger.debug "Remote status GET request returned code #{response.code}" - - return nil if response.code != 200 - return [url, fetch(url)] if response.mime_type == 'application/atom+xml' - return process_headers(url, response) if response['Link'].present? - process_html(fetch(url)) + perform_request + process_response rescue OpenSSL::SSL::SSLError => e Rails.logger.debug "SSL error: #{e}" nil @@ -26,27 +18,57 @@ class FetchAtomService < BaseService private - def process_html(body) - Rails.logger.debug 'Processing HTML' + def perform_request + @response = Request.new(:get, @url) + .add_headers('Accept' => 'application/activity+json, application/ld+json, application/atom+xml, text/html') + .perform + end - page = Nokogiri::HTML(body) - alternate_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' } + def process_response(terminal = false) + return nil if @response.code != 200 - return nil if alternate_link.nil? - [alternate_link['href'], fetch(alternate_link['href'])] + if @response.mime_type == 'application/atom+xml' + [@url, @response.to_s, :ostatus] + elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@response.mime_type) + [@url, @response.to_s, :activitypub] + elsif @response['Link'] && !terminal + process_headers + elsif @response.mime_type == 'text/html' && !terminal + process_html + end end - def process_headers(url, response) - Rails.logger.debug 'Processing link header' + def process_html + page = Nokogiri::HTML(@response.to_s) - link_header = LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link']) - alternate_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml)) + json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } + atom_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' } - return process_html(fetch(url)) if alternate_link.nil? - [alternate_link.href, fetch(alternate_link.href)] + if !json_link.nil? + @url = json_link['href'] + perform_request + process_response(true) + elsif !atom_link.nil? + @url = atom_link['href'] + perform_request + process_response(true) + end end - def fetch(url) - Request.new(:get, url).perform.to_s + def process_headers + link_header = LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link']) + + json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) + atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml)) + + if !json_link.nil? + @url = json_link.href + perform_request + process_response(true) + elsif !atom_link.nil? + @url = atom_link.href + perform_request + process_response(true) + end end end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index 8eed0d454..41b5374b4 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -5,14 +5,19 @@ class FetchRemoteAccountService < BaseService def call(url, prefetched_body = nil) if prefetched_body.nil? - atom_url, body = FetchAtomService.new.call(url) + resource_url, body, protocol = FetchAtomService.new.call(url) else - atom_url = url - body = prefetched_body + resource_url = url + body = prefetched_body + protocol = :ostatus end - return nil if atom_url.nil? - process_atom(atom_url, body) + case protocol + when :ostatus + process_atom(resource_url, body) + when :activitypub + ActivityPub::FetchRemoteAccountService.new.call(resource_url, body) + end end private diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index b9f5f97b1..30d8d2538 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -5,14 +5,19 @@ class FetchRemoteStatusService < BaseService def call(url, prefetched_body = nil) if prefetched_body.nil? - atom_url, body = FetchAtomService.new.call(url) + resource_url, body, protocol = FetchAtomService.new.call(url) else - atom_url = url - body = prefetched_body + resource_url = url + body = prefetched_body + protocol = :ostatus end - return nil if atom_url.nil? - process_atom(atom_url, body) + case protocol + when :ostatus + process_atom(resource_url, body) + when :activitypub + ActivityPub::FetchRemoteStatusService.new.call(resource_url, body) + end end private diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb index 76f9740ca..d90da9e32 100644 --- a/spec/controllers/api/subscriptions_controller_spec.rb +++ b/spec/controllers/api/subscriptions_controller_spec.rb @@ -38,19 +38,19 @@ RSpec.describe Api::SubscriptionsController, type: :controller do before do stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:head, "https://quitter.no/notice/1269244").to_return(status: 404) - stub_request(:head, "https://quitter.no/notice/1265331").to_return(status: 404) - stub_request(:head, "https://community.highlandarrow.com/notice/54411").to_return(status: 404) - stub_request(:head, "https://community.highlandarrow.com/notice/53857").to_return(status: 404) - stub_request(:head, "https://community.highlandarrow.com/notice/51852").to_return(status: 404) - stub_request(:head, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404) - stub_request(:head, "https://community.highlandarrow.com/notice/50467").to_return(status: 404) - stub_request(:head, "https://quitter.no/notice/1243309").to_return(status: 404) - stub_request(:head, "https://quitter.no/user/7477").to_return(status: 404) - stub_request(:head, "https://community.highlandarrow.com/user/1").to_return(status: 404) - stub_request(:head, "https://social.umeahackerspace.se/user/2").to_return(status: 404) - stub_request(:head, "https://gs.kawa-kun.com/user/2").to_return(status: 404) - stub_request(:head, "https://mastodon.social/users/Gargron").to_return(status: 404) + stub_request(:get, "https://quitter.no/notice/1269244").to_return(status: 404) + stub_request(:get, "https://quitter.no/notice/1265331").to_return(status: 404) + stub_request(:get, "https://community.highlandarrow.com/notice/54411").to_return(status: 404) + stub_request(:get, "https://community.highlandarrow.com/notice/53857").to_return(status: 404) + stub_request(:get, "https://community.highlandarrow.com/notice/51852").to_return(status: 404) + stub_request(:get, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404) + stub_request(:get, "https://community.highlandarrow.com/notice/50467").to_return(status: 404) + stub_request(:get, "https://quitter.no/notice/1243309").to_return(status: 404) + stub_request(:get, "https://quitter.no/user/7477").to_return(status: 404) + stub_request(:any, "https://community.highlandarrow.com/user/1").to_return(status: 404) + stub_request(:any, "https://social.umeahackerspace.se/user/2").to_return(status: 404) + stub_request(:any, "https://gs.kawa-kun.com/user/2").to_return(status: 404) + stub_request(:any, "https://mastodon.social/users/Gargron").to_return(status: 404) request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}" request.env['RAW_POST_DATA'] = feed diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb index 5e34370ee..aca675dc6 100644 --- a/spec/services/process_feed_service_spec.rb +++ b/spec/services/process_feed_service_spec.rb @@ -124,8 +124,7 @@ RSpec.describe ProcessFeedService do XML - stub_request(:head, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, headers: { 'Content-Type' => 'application/atom+xml' }) - stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body) + stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' }) bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz') @@ -168,7 +167,7 @@ XML end it 'ignores reblogs if it failed to retreive reblogged statuses' do - stub_request(:head, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404) + stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404) actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') -- cgit From 6df8bd277b52b3ac025597ec08ee9e342a8eb32c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 14 Aug 2017 04:16:43 +0200 Subject: Set correct content-type for ActivityPub JSON (#4592) --- app/controllers/accounts_controller.rb | 2 +- app/controllers/activitypub/outboxes_controller.rb | 2 +- app/controllers/follower_accounts_controller.rb | 2 +- app/controllers/following_accounts_controller.rb | 2 +- app/controllers/statuses_controller.rb | 4 ++-- app/controllers/tags_controller.rb | 2 +- config/initializers/mime_types.rb | 2 +- spec/controllers/accounts_controller_spec.rb | 4 ++++ spec/controllers/activitypub/outboxes_controller_spec.rb | 4 ++++ 9 files changed, 16 insertions(+), 8 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index c270eb000..4dc0a783d 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -17,7 +17,7 @@ class AccountsController < ApplicationController end format.json do - render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter + render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 30b91f370..9f97ff622 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -7,7 +7,7 @@ class ActivityPub::OutboxesController < Api::BaseController @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = cache_collection(@statuses, Status) - render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end private diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 5edb4d67c..0e1949897 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -10,7 +10,7 @@ class FollowerAccountsController < ApplicationController format.html format.json do - render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 7cafe5fda..d4593093f 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -10,7 +10,7 @@ class FollowingAccountsController < ApplicationController format.html format.json do - render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 0cce2ba23..aa24f23c9 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -20,13 +20,13 @@ class StatusesController < ApplicationController end format.json do - render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter + render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end def activity - render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter + render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end private diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 2cd85e185..3001b2ee3 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -12,7 +12,7 @@ class TagsController < ApplicationController format.html format.json do - render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 30e91ad63..58a6c0063 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. -Mime::Type.register 'application/json', :json, %w(text/x-json application/jsonrequest application/jrd+json application/activity+json) +Mime::Type.register 'application/json', :json, %w(text/x-json application/jsonrequest application/jrd+json application/activity+json application/ld+json) Mime::Type.register 'text/xml', :xml, %w(application/xml application/atom+xml application/xrd+xml) diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index d61c8c9bd..2c0df0ef3 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -48,6 +48,10 @@ RSpec.describe AccountsController, type: :controller do it 'returns http success with Activity Streams 2.0' do expect(response).to have_http_status(:success) end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end end context 'html' do diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb index f98e4a8c3..a25998021 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -15,5 +15,9 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do it 'returns http success' do expect(response).to have_http_status(:success) end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end end end -- cgit From 2edfdab6e6d70598a19d59f8a2f47ecae8add243 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 18 Aug 2017 17:42:59 +0900 Subject: Don't send Link header when don't know prev and next links (#4633) --- app/controllers/api/base_controller.rb | 2 +- spec/controllers/api/v1/favourites_controller_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 6ede63c79..7cfe8fe71 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -43,7 +43,7 @@ class Api::BaseController < ApplicationController links = [] links << [next_path, [%w(rel next)]] if next_path links << [prev_path, [%w(rel prev)]] if prev_path - response.headers['Link'] = LinkHeader.new(links) + response.headers['Link'] = LinkHeader.new(links) unless links.empty? end def limit_param(default_limit) diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb index 3de045377..46cf70f4d 100644 --- a/spec/controllers/api/v1/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/favourites_controller_spec.rb @@ -70,8 +70,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do it 'does not add pagination headers if not necessary' do get :index - expect(response.headers['Link'].find_link(['rel', 'next'])).to eq nil - expect(response.headers['Link'].find_link(['rel', 'prev'])).to eq nil + expect(response.headers['Link']).to eq nil end end end -- cgit From 74e5078795cd5bc8a10e2c22355379ff5ca6d21c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 21 Aug 2017 00:41:08 +0200 Subject: Fix #4637 - Re-add missing doorkeeper_authorize for /api/v1/verify_credentials (#4650) --- .../api/v1/accounts/credentials_controller.rb | 1 + .../api/v1/accounts/credentials_controller_spec.rb | 94 +++++++++++++--------- 2 files changed, 59 insertions(+), 36 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 90a580c33..bea83cd2a 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::CredentialsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read }, except: [:update] before_action -> { doorkeeper_authorize! :write }, only: [:update] before_action :require_user! diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index bc89772b9..461b8b34b 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -4,57 +4,79 @@ describe Api::V1::Accounts::CredentialsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #show' do - it 'returns http success' do - get :show - expect(response).to have_http_status(:success) + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } end - end - - describe 'PATCH #update' do - describe 'with valid data' do - before do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) - - patch :update, params: { - display_name: "Alice Isn't Dead", - note: "Hi!\n\nToot toot!", - avatar: fixture_file_upload('files/avatar.gif', 'image/gif'), - header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'), - } - end + describe 'GET #show' do it 'returns http success' do + get :show expect(response).to have_http_status(:success) end + end + + describe 'PATCH #update' do + describe 'with valid data' do + before do + allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) + + patch :update, params: { + display_name: "Alice Isn't Dead", + note: "Hi!\n\nToot toot!", + avatar: fixture_file_upload('files/avatar.gif', 'image/gif'), + header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'), + } + end - it 'updates account info' do - user.account.reload + it 'returns http success' do + expect(response).to have_http_status(:success) + end - expect(user.account.display_name).to eq("Alice Isn't Dead") - expect(user.account.note).to eq("Hi!\n\nToot toot!") - expect(user.account.avatar).to exist - expect(user.account.header).to exist + it 'updates account info' do + user.account.reload + + expect(user.account.display_name).to eq("Alice Isn't Dead") + expect(user.account.note).to eq("Hi!\n\nToot toot!") + expect(user.account.avatar).to exist + expect(user.account.header).to exist + end + + it 'queues up an account update distribution' do + expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id) + end end - it 'queues up an account update distribution' do - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id) + describe 'with invalid data' do + before do + patch :update, params: { note: 'This is too long. ' * 10 } + end + + it 'returns http unprocessable entity' do + expect(response).to have_http_status(:unprocessable_entity) + end end end + end - describe 'with invalid data' do - before do - patch :update, params: { note: 'This is too long. ' * 10 } + context 'without an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { nil } + end + + describe 'GET #show' do + it 'returns http unauthorized' do + get :show + expect(response).to have_http_status(:unauthorized) end + end - it 'returns http unprocessable entity' do - expect(response).to have_http_status(:unprocessable_entity) + describe 'PATCH #update' do + it 'returns http unauthorized' do + patch :update, params: { note: 'Foo' } + expect(response).to have_http_status(:unauthorized) end end end -- cgit From 871c0d251a6d27c4591785ae446738a8d6c553ab Mon Sep 17 00:00:00 2001 From: Colin Mitchell Date: Tue, 22 Aug 2017 12:33:57 -0400 Subject: Application prefs section (#2758) * Add code for creating/managing apps to settings section * Add specs for app changes * Fix controller spec * Fix view file I pasted over by mistake * Add locale strings. Add 'my apps' to nav * Add Client ID/Secret to App page. Add some visual separation * Fix rubocop warnings * Fix embarrassing typo I lost an `end` statement while fixing a merge conflict. * Add code for creating/managing apps to settings section - Add specs for app changes - Add locale strings. Add 'my apps' to nav - Add Client ID/Secret to App page. Add some visual separation - Fix some bugs/warnings * Update to match code standards * Trigger notification * Add warning about not sharing API secrets * Tweak spec a bit * Cleanup fixture creation by using let! * Remove unused key * Add foreign key for application<->user --- .../settings/applications_controller.rb | 65 ++++++++ app/models/user.rb | 13 ++ app/views/settings/applications/_fields.html.haml | 4 + app/views/settings/applications/index.html.haml | 20 +++ app/views/settings/applications/new.html.haml | 9 ++ app/views/settings/applications/show.html.haml | 28 ++++ config/initializers/doorkeeper.rb | 2 +- config/locales/doorkeeper.en.yml | 7 +- config/locales/en.yml | 11 ++ config/navigation.rb | 1 + config/routes.rb | 5 + .../20170427011934_re_add_owner_to_application.rb | 8 + db/schema.rb | 7 +- .../settings/applications_controller_spec.rb | 166 +++++++++++++++++++++ spec/models/user_spec.rb | 20 +++ 15 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 app/controllers/settings/applications_controller.rb create mode 100644 app/views/settings/applications/_fields.html.haml create mode 100644 app/views/settings/applications/index.html.haml create mode 100644 app/views/settings/applications/new.html.haml create mode 100644 app/views/settings/applications/show.html.haml create mode 100644 db/migrate/20170427011934_re_add_owner_to_application.rb create mode 100644 spec/controllers/settings/applications_controller_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb new file mode 100644 index 000000000..b8f114455 --- /dev/null +++ b/app/controllers/settings/applications_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class Settings::ApplicationsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + + def index + @applications = current_user.applications.page(params[:page]) + end + + def new + @application = Doorkeeper::Application.new( + redirect_uri: Doorkeeper.configuration.native_redirect_uri, + scopes: 'read write follow' + ) + end + + def show + @application = current_user.applications.find(params[:id]) + end + + def create + @application = current_user.applications.build(application_params) + if @application.save + redirect_to settings_applications_path, notice: I18n.t('application.created') + else + render :new + end + end + + def update + @application = current_user.applications.find(params[:id]) + if @application.update_attributes(application_params) + redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg') + else + render :show + end + end + + def destroy + @application = current_user.applications.find(params[:id]) + @application.destroy + redirect_to settings_applications_path, notice: t('application.destroyed') + end + + def regenerate + @application = current_user.applications.find(params[:application_id]) + @access_token = current_user.token_for_app(@application) + @access_token.destroy + + redirect_to settings_application_path(@application), notice: t('access_token.regenerated') + end + + private + + def application_params + params.require(:doorkeeper_application).permit( + :name, + :redirect_uri, + :scopes, + :website + ) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 96a2d09b7..02b1b26ee 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,6 +46,8 @@ class User < ApplicationRecord belongs_to :account, inverse_of: :user, required: true accepts_nested_attributes_for :account + has_many :applications, class_name: 'Doorkeeper::Application', as: :owner + validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? @@ -108,6 +110,17 @@ class User < ApplicationRecord settings.noindex end + def token_for_app(a) + return nil if a.nil? || a.owner != self + Doorkeeper::AccessToken + .find_or_create_by(application_id: a.id, resource_owner_id: id) do |t| + + t.scopes = a.scopes + t.expires_in = Doorkeeper.configuration.access_token_expires_in + t.use_refresh_token = Doorkeeper.configuration.refresh_token_enabled? + end + end + def activate_session(request) session_activations.activate(session_id: SecureRandom.hex, user_agent: request.user_agent, diff --git a/app/views/settings/applications/_fields.html.haml b/app/views/settings/applications/_fields.html.haml new file mode 100644 index 000000000..9dbe23466 --- /dev/null +++ b/app/views/settings/applications/_fields.html.haml @@ -0,0 +1,4 @@ += f.input :name, hint: t('activerecord.attributes.doorkeeper/application.name') += f.input :website, hint: t('activerecord.attributes.doorkeeper/application.website') += f.input :redirect_uri, hint: t('activerecord.attributes.doorkeeper/application.redirect_uri') += f.input :scopes, hint: t('activerecord.attributes.doorkeeper/application.scopes') diff --git a/app/views/settings/applications/index.html.haml b/app/views/settings/applications/index.html.haml new file mode 100644 index 000000000..17035f96c --- /dev/null +++ b/app/views/settings/applications/index.html.haml @@ -0,0 +1,20 @@ +- content_for :page_title do + = t('doorkeeper.applications.index.title') + +%table.table + %thead + %tr + %th= t('doorkeeper.applications.index.application') + %th= t('doorkeeper.applications.index.scopes') + %th= t('doorkeeper.applications.index.created_at') + %th + %tbody + - @applications.each do |application| + %tr + %td= link_to application.name, settings_application_path(application) + %th= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('
').html_safe + %td= l application.created_at + %td= table_link_to 'show', t('doorkeeper.applications.index.show'), settings_application_path(application) + %td= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } += paginate @applications += link_to t('add_new'), new_settings_application_path, class: 'button' diff --git a/app/views/settings/applications/new.html.haml b/app/views/settings/applications/new.html.haml new file mode 100644 index 000000000..61406a31f --- /dev/null +++ b/app/views/settings/applications/new.html.haml @@ -0,0 +1,9 @@ +- content_for :page_title do + = t('doorkeeper.applications.new.title') + +.form-container + = simple_form_for @application, url: settings_applications_path do |f| + = render 'fields', f:f + + .actions + = f.button :button, t('.create'), type: :submit diff --git a/app/views/settings/applications/show.html.haml b/app/views/settings/applications/show.html.haml new file mode 100644 index 000000000..9f1a11986 --- /dev/null +++ b/app/views/settings/applications/show.html.haml @@ -0,0 +1,28 @@ +- content_for :page_title do + = t('doorkeeper.applications.show.title', name: @application.name) + + +%p.hint= t('application.warning') + +%div + %h3= t('application.uid') + %code= @application.uid + +%div + %h3= t('application.secret') + %code= @application.secret + +%div + %h3= t('access_token.your_token') + %code= current_user.token_for_app(@application).token + += link_to t('access_token.regenerate'), settings_application_regenerate_path(@application), method: :put, class: 'button' + +%hr + += simple_form_for @application, url: settings_application_path(@application), method: :put do |f| + = render 'fields', f:f + + .actions + = f.button :button, t('generic.save_changes'), type: :submit + diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 056a3651a..689e2ac4a 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -50,7 +50,7 @@ Doorkeeper.configure do # Optional parameter :confirmation => true (default false) if you want to enforce ownership of # a registered application # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support - # enable_application_owner :confirmation => true + enable_application_owner # Define access token scopes for your provider # For more information go to diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 6412b8b48..fa0a7babf 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -3,8 +3,10 @@ en: activerecord: attributes: doorkeeper/application: - name: Name + name: Application Name + website: Application Website redirect_uri: Redirect URI + scopes: Scopes errors: models: doorkeeper/application: @@ -37,9 +39,12 @@ en: name: Name new: New Application title: Your applications + show: Show + delete: Delete new: title: New Application show: + title: 'Application: %{name}' actions: Actions application_id: Application Id callback_urls: Callback urls diff --git a/config/locales/en.yml b/config/locales/en.yml index 97f46c3af..fbcef03bd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,6 +33,10 @@ en: user_count_after: users user_count_before: Home to what_is_mastodon: What is Mastodon? + access_token: + your_token: Your Access Token + regenerate: Regenerate Access Token + regenerated: Access Token Regenerated accounts: follow: Follow followers: Followers @@ -226,6 +230,12 @@ en: settings: 'Change e-mail preferences: %{link}' signature: Mastodon notifications from %{instance} view: 'View:' + application: + created: Application Created + destroyed: Application Destroyed + uid: Client ID + secret: Client Secret + warning: Be very careful with this data. Never share it with anyone other than authorized applications! applications: invalid_url: The provided URL is invalid auth: @@ -423,6 +433,7 @@ en: preferences: Preferences settings: Settings two_factor_authentication: Two-factor Authentication + your_apps: Your applications statuses: open_in_web: Open in web over_character_limit: character limit of %{max} exceeded diff --git a/config/navigation.rb b/config/navigation.rb index 535d033f5..6e04843ec 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -12,6 +12,7 @@ SimpleNavigation::Configuration.run do |navigation| settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url + settings.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url end diff --git a/config/routes.rb b/config/routes.rb index 1a39dfeac..e8bc968f4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,11 @@ Rails.application.routes.draw do end resource :follower_domains, only: [:show, :update] + + resources :applications do + put :regenerate + end + resource :delete, only: [:show, :destroy] resources :sessions, only: [:destroy] diff --git a/db/migrate/20170427011934_re_add_owner_to_application.rb b/db/migrate/20170427011934_re_add_owner_to_application.rb new file mode 100644 index 000000000..a41d71d2a --- /dev/null +++ b/db/migrate/20170427011934_re_add_owner_to_application.rb @@ -0,0 +1,8 @@ +class ReAddOwnerToApplication < ActiveRecord::Migration[5.0] + def change + add_column :oauth_applications, :owner_id, :integer, null: true + add_column :oauth_applications, :owner_type, :string, null: true + add_index :oauth_applications, [:owner_id, :owner_type] + add_foreign_key :oauth_applications, :users, column: :owner_id, on_delete: :cascade + end +end diff --git a/db/schema.rb b/db/schema.rb index 2501e451d..929a5fd01 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -216,8 +216,11 @@ ActiveRecord::Schema.define(version: 20170720000000) do t.string "scopes", default: "", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "superapp", default: false, null: false - t.string "website" + t.boolean "superapp", default: false, null: false + t.string "website" + t.integer "owner_id" + t.string "owner_type" + t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb new file mode 100644 index 000000000..fa27e6ec6 --- /dev/null +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -0,0 +1,166 @@ +require 'rails_helper' + +describe Settings::ApplicationsController do + render_views + + let!(:user) { Fabricate(:user) } + let!(:app) { Fabricate(:application, owner: user) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + let!(:other_app) { Fabricate(:application) } + + it 'shows apps' do + get :index + expect(response).to have_http_status(:success) + expect(assigns(:applications)).to include(app) + expect(assigns(:applications)).to_not include(other_app) + end + end + + + describe 'GET #show' do + it 'returns http success' do + get :show, params: { id: app.id } + expect(response).to have_http_status(:success) + expect(assigns[:application]).to eql(app) + end + + it 'returns 404 if you dont own app' do + app.update!(owner: nil) + + get :show, params: { id: app.id } + expect(response.status).to eq 404 + end + end + + describe 'GET #new' do + it 'works' do + get :new + expect(response).to have_http_status(:success) + end + end + + describe 'POST #create' do + context 'success' do + def call_create + post :create, params: { + doorkeeper_application: { + name: 'My New App', + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + website: 'http://google.com', + scopes: 'read write follow' + } + } + response + end + + it 'creates an entry in the database' do + expect { call_create }.to change(Doorkeeper::Application, :count) + end + + it 'redirects back to applications page' do + expect(call_create).to redirect_to(settings_applications_path) + end + end + + context 'failure' do + before do + post :create, params: { + doorkeeper_application: { + name: '', + redirect_uri: '', + website: '', + scopes: '' + } + } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'renders form again' do + expect(response).to render_template(:new) + end + end + end + + describe 'PATCH #update' do + context 'success' do + let(:opts) { + { + website: 'https://foo.bar/' + } + } + + def call_update + patch :update, params: { + id: app.id, + doorkeeper_application: opts + } + response + end + + it 'updates existing application' do + call_update + expect(app.reload.website).to eql(opts[:website]) + end + + it 'redirects back to applications page' do + expect(call_update).to redirect_to(settings_applications_path) + end + end + + context 'failure' do + before do + patch :update, params: { + id: app.id, + doorkeeper_application: { + name: '', + redirect_uri: '', + website: '', + scopes: '' + } + } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'renders form again' do + expect(response).to render_template(:show) + end + end + end + + describe 'destroy' do + before do + post :destroy, params: { id: app.id } + end + + it 'redirects back to applications page' do + expect(response).to redirect_to(settings_applications_path) + end + + it 'removes the app' do + expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil + end + end + + describe 'regenerate' do + let(:token) { user.token_for_app(app) } + before do + expect(token).to_not be_nil + put :regenerate, params: { application_id: app.id } + end + + it 'should create new token' do + expect(user.token_for_app(app)).to_not eql(token) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ef45818b9..99aeca01b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -286,4 +286,24 @@ RSpec.describe User, type: :model do Fabricate(:user) end end + + describe 'token_for_app' do + let(:user) { Fabricate(:user) } + let(:app) { Fabricate(:application, owner: user) } + + it 'returns a token' do + expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken) + end + + it 'persists a token' do + t = user.token_for_app(app) + expect(user.token_for_app(app)).to eql(t) + end + + it 'is nil if user does not own app' do + app.update!(owner: nil) + + expect(user.token_for_app(app)).to be_nil + end + end end -- cgit From c1b086a538d128e9fbceab4fc6686611a4f2710f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 23 Aug 2017 00:59:35 +0200 Subject: Fix up the applications area (#4664) - Section it into "Development" area - Improve UI of application form, index, and details --- .../settings/applications_controller.rb | 21 ++++++------ app/views/settings/applications/_fields.html.haml | 15 +++++--- app/views/settings/applications/index.html.haml | 11 +++--- app/views/settings/applications/new.html.haml | 11 +++--- app/views/settings/applications/show.html.haml | 40 ++++++++++++---------- config/locales/doorkeeper.en.yml | 19 +++++----- config/locales/en.yml | 23 ++++++------- config/locales/ja.yml | 6 ++-- config/locales/oc.yml | 16 ++++----- config/locales/pl.yml | 8 ++--- config/navigation.rb | 5 ++- config/routes.rb | 6 ++-- db/schema.rb | 11 +++--- .../settings/applications_controller_spec.rb | 2 +- 14 files changed, 102 insertions(+), 92 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index b8f114455..894222c2a 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -4,6 +4,7 @@ class Settings::ApplicationsController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_application, only: [:show, :update, :destroy, :regenerate] def index @applications = current_user.applications.page(params[:page]) @@ -16,22 +17,20 @@ class Settings::ApplicationsController < ApplicationController ) end - def show - @application = current_user.applications.find(params[:id]) - end + def show; end def create @application = current_user.applications.build(application_params) + if @application.save - redirect_to settings_applications_path, notice: I18n.t('application.created') + redirect_to settings_applications_path, notice: I18n.t('applications.created') else render :new end end def update - @application = current_user.applications.find(params[:id]) - if @application.update_attributes(application_params) + if @application.update(application_params) redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg') else render :show @@ -39,21 +38,23 @@ class Settings::ApplicationsController < ApplicationController end def destroy - @application = current_user.applications.find(params[:id]) @application.destroy - redirect_to settings_applications_path, notice: t('application.destroyed') + redirect_to settings_applications_path, notice: I18n.t('applications.destroyed') end def regenerate - @application = current_user.applications.find(params[:application_id]) @access_token = current_user.token_for_app(@application) @access_token.destroy - redirect_to settings_application_path(@application), notice: t('access_token.regenerated') + redirect_to settings_application_path(@application), notice: I18n.t('applications.token_regenerated') end private + def set_application + @application = current_user.applications.find(params[:id]) + end + def application_params params.require(:doorkeeper_application).permit( :name, diff --git a/app/views/settings/applications/_fields.html.haml b/app/views/settings/applications/_fields.html.haml index 9dbe23466..536f69e04 100644 --- a/app/views/settings/applications/_fields.html.haml +++ b/app/views/settings/applications/_fields.html.haml @@ -1,4 +1,11 @@ -= f.input :name, hint: t('activerecord.attributes.doorkeeper/application.name') -= f.input :website, hint: t('activerecord.attributes.doorkeeper/application.website') -= f.input :redirect_uri, hint: t('activerecord.attributes.doorkeeper/application.redirect_uri') -= f.input :scopes, hint: t('activerecord.attributes.doorkeeper/application.scopes') +.fields-group + = f.input :name, placeholder: t('activerecord.attributes.doorkeeper/application.name') + = f.input :website, placeholder: t('activerecord.attributes.doorkeeper/application.website') + +.fields-group + = f.input :redirect_uri, wrapper: :with_block_label, label: t('activerecord.attributes.doorkeeper/application.redirect_uri'), hint: t('doorkeeper.applications.help.redirect_uri') + + %p.hint= t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: Doorkeeper.configuration.native_redirect_uri) + +.fields-group + = f.input :scopes, wrapper: :with_label, label: t('activerecord.attributes.doorkeeper/application.scopes'), hint: t('doorkeeper.applications.help.scopes') diff --git a/app/views/settings/applications/index.html.haml b/app/views/settings/applications/index.html.haml index 17035f96c..eea550388 100644 --- a/app/views/settings/applications/index.html.haml +++ b/app/views/settings/applications/index.html.haml @@ -6,15 +6,14 @@ %tr %th= t('doorkeeper.applications.index.application') %th= t('doorkeeper.applications.index.scopes') - %th= t('doorkeeper.applications.index.created_at') %th %tbody - @applications.each do |application| %tr %td= link_to application.name, settings_application_path(application) - %th= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('
').html_safe - %td= l application.created_at - %td= table_link_to 'show', t('doorkeeper.applications.index.show'), settings_application_path(application) - %td= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } + %th= application.scopes + %td + = table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } + = paginate @applications -= link_to t('add_new'), new_settings_application_path, class: 'button' += link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button' diff --git a/app/views/settings/applications/new.html.haml b/app/views/settings/applications/new.html.haml index 61406a31f..5274a430c 100644 --- a/app/views/settings/applications/new.html.haml +++ b/app/views/settings/applications/new.html.haml @@ -1,9 +1,8 @@ - content_for :page_title do = t('doorkeeper.applications.new.title') + += simple_form_for @application, url: settings_applications_path do |f| + = render 'fields', f: f -.form-container - = simple_form_for @application, url: settings_applications_path do |f| - = render 'fields', f:f - - .actions - = f.button :button, t('.create'), type: :submit + .actions + = f.button :button, t('doorkeeper.applications.buttons.submit'), type: :submit diff --git a/app/views/settings/applications/show.html.haml b/app/views/settings/applications/show.html.haml index 9f1a11986..4d8555111 100644 --- a/app/views/settings/applications/show.html.haml +++ b/app/views/settings/applications/show.html.haml @@ -1,27 +1,29 @@ - content_for :page_title do = t('doorkeeper.applications.show.title', name: @application.name) - -%p.hint= t('application.warning') - -%div - %h3= t('application.uid') - %code= @application.uid - -%div - %h3= t('application.secret') - %code= @application.secret - -%div - %h3= t('access_token.your_token') - %code= current_user.token_for_app(@application).token - -= link_to t('access_token.regenerate'), settings_application_regenerate_path(@application), method: :put, class: 'button' - -%hr +%p.hint= t('applications.warning') + +%table.table + %tbody + %tr + %th= t('doorkeeper.applications.show.application_id') + %td + %code= @application.uid + %tr + %th= t('doorkeeper.applications.show.secret') + %td + %code= @application.secret + %tr + %th{ rowspan: 2}= t('applications.your_token') + %td + %code= current_user.token_for_app(@application).token + %tr + %td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post + +%hr/ = simple_form_for @application, url: settings_application_path(@application), method: :put do |f| - = render 'fields', f:f + = render 'fields', f: f .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index fa0a7babf..788d1bb40 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -3,10 +3,10 @@ en: activerecord: attributes: doorkeeper/application: - name: Application Name - website: Application Website + name: Application name redirect_uri: Redirect URI scopes: Scopes + website: Application website errors: models: doorkeeper/application: @@ -36,20 +36,19 @@ en: scopes: Separate scopes with spaces. Leave blank to use the default scopes. index: callback_url: Callback URL + delete: Delete name: Name - new: New Application - title: Your applications + new: New application show: Show - delete: Delete + title: Your applications new: - title: New Application + title: New application show: - title: 'Application: %{name}' actions: Actions - application_id: Application Id - callback_urls: Callback urls + application_id: Client key + callback_urls: Callback URLs scopes: Scopes - secret: Secret + secret: Client secret title: 'Application: %{name}' authorizations: buttons: diff --git a/config/locales/en.yml b/config/locales/en.yml index fbcef03bd..97bb14186 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,24 +33,20 @@ en: user_count_after: users user_count_before: Home to what_is_mastodon: What is Mastodon? - access_token: - your_token: Your Access Token - regenerate: Regenerate Access Token - regenerated: Access Token Regenerated accounts: follow: Follow followers: Followers following: Following + media: Media nothing_here: There is nothing here! people_followed_by: People whom %{name} follows people_who_follow: People who follow %{name} posts: Toots posts_with_replies: Toots with replies - media: Media - roles: - admin: Admin remote_follow: Remote follow reserved_username: The username is reserved + roles: + admin: Admin unfollow: Unfollow admin: accounts: @@ -230,14 +226,14 @@ en: settings: 'Change e-mail preferences: %{link}' signature: Mastodon notifications from %{instance} view: 'View:' - application: - created: Application Created - destroyed: Application Destroyed - uid: Client ID - secret: Client Secret - warning: Be very careful with this data. Never share it with anyone other than authorized applications! applications: + created: Application successfully created + destroyed: Application successfully deleted invalid_url: The provided URL is invalid + regenerate_token: Regenerate access token + token_regenerated: Access token successfully regenerated + warning: Be very careful with this data. Never share it with anyone! + your_token: Your access token auth: agreement_html: By signing up you agree to our terms of service and privacy policy. change_password: Security @@ -426,6 +422,7 @@ en: authorized_apps: Authorized apps back: Back to Mastodon delete: Account deletion + development: Development edit_profile: Edit profile export: Data export followers: Authorized followers diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 0f0b0ad4a..2ee99db45 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -37,16 +37,16 @@ ja: follow: フォロー followers: フォロワー following: フォロー中 + media: メディア nothing_here: 何もありません people_followed_by: "%{name} さんがフォロー中のアカウント" people_who_follow: "%{name} さんをフォロー中のアカウント" posts: トゥート posts_with_replies: トゥートと返信 - media: メディア - roles: - admin: Admin remote_follow: リモートフォロー reserved_username: このユーザー名は予約されています。 + roles: + admin: Admin unfollow: フォロー解除 admin: accounts: diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 9038d887a..65ea4525a 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -37,16 +37,16 @@ oc: follow: Sègre followers: Seguidors following: Abonaments + media: Mèdias nothing_here: I a pas res aquí ! people_followed_by: Lo mond que %{name} sèc people_who_follow: Lo mond que sègon %{name} posts: Tuts posts_with_replies: Tuts amb responsas - media: Mèdias - roles: - admin: Admin remote_follow: Sègre a distància reserved_username: Aqueste nom d’utilizaire es reservat + roles: + admin: Admin unfollow: Quitar de sègre admin: accounts: @@ -221,7 +221,7 @@ oc: body: "%{reporter} a senhalat %{target}" subject: Novèl senhalament per %{instance} (#%{id}) application_mailer: - salutation: '%{name},' + salutation: "%{name}," settings: 'Cambiar las preferéncias de corrièl : %{link}' signature: Notificacion de Mastodon sus %{instance} view: 'Veire :' @@ -234,13 +234,13 @@ oc: delete_account_html: Se volètz suprimir vòstre compte, podètz o far aquí. Vos demandarem que confirmetz. didnt_get_confirmation: Avètz pas recebut las instruccions de confirmacion ? forgot_password: Senhal oblidat ? + invalid_reset_password_token: Lo geton de reïnicializacion es invalid o acabat. Tornatz demandar un geton se vos plai. login: Se connectar logout: Se desconnectar register: Se marcar resend_confirmation: Tornar mandar las instruccions de confirmacion reset_password: Reïnicializar lo senhal set_new_password: Picar un nòu senhal - invalid_reset_password_token: Lo geton de reïnicializacion es invalid o acabat. Tornatz demandar un geton se vos plai. authorize_follow: error: O planhèm, i a agut una error al moment de cercar lo compte follow: Sègre @@ -337,12 +337,12 @@ oc: x_months: one: Fa un mes other: Fa %{count} meses - x_years: - one: Fa un an - other: Fa %{count} ans x_seconds: one: Fa una segonda other: Fa %{count} segondas + x_years: + one: Fa un an + other: Fa %{count} ans deletes: bad_password_msg: Ben ensajat pirata ! Senhal incorrècte confirm_password: Picatz vòstre senhal actual per verificar vòstra identitat diff --git a/config/locales/pl.yml b/config/locales/pl.yml index c005cdb01..b7f4898b0 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -37,16 +37,16 @@ pl: follow: Śledź followers: Śledzących following: Śledzi + media: Zawartość multimedialna nothing_here: Niczego tu nie ma! people_followed_by: Konta śledzone przez %{name} people_who_follow: Osoby, które śledzą konto %{name} posts: Wpisy posts_with_replies: Wpisy z odpowiedziami - media: Zawartość multimedialna - roles: - admin: Administrator remote_follow: Śledź zdalnie reserved_username: Ta nazwa użytkownika jest zarezerwowana. + roles: + admin: Administrator unfollow: Przestań śledzić admin: accounts: @@ -126,8 +126,8 @@ pl: severity: Priorytet show: affected_accounts: - one: Dotyczy jednego konta w bazie danych many: Dotyczy %{count} kont w bazie danych + one: Dotyczy jednego konta w bazie danych other: Dotyczy %{count} kont w bazie danych retroactive: silence: Odwołaj wyciszenie wszystkich kont w tej domenie diff --git a/config/navigation.rb b/config/navigation.rb index 6e04843ec..4b454b3fc 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -12,10 +12,13 @@ SimpleNavigation::Configuration.run do |navigation| settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url - settings.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url end + primary.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url do |development| + development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications} + end + primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin| admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} diff --git a/config/routes.rb b/config/routes.rb index e8bc968f4..94a4ac88e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,8 +80,10 @@ Rails.application.routes.draw do resource :follower_domains, only: [:show, :update] - resources :applications do - put :regenerate + resources :applications, except: [:edit] do + member do + post :regenerate + end end resource :delete, only: [:show, :destroy] diff --git a/db/schema.rb b/db/schema.rb index 929a5fd01..98b07e282 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -216,11 +216,11 @@ ActiveRecord::Schema.define(version: 20170720000000) do t.string "scopes", default: "", null: false t.datetime "created_at" t.datetime "updated_at" - t.boolean "superapp", default: false, null: false - t.string "website" - t.integer "owner_id" - t.string "owner_type" - t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree + t.boolean "superapp", default: false, null: false + t.string "website" + t.integer "owner_id" + t.string "owner_type" + t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end @@ -423,6 +423,7 @@ ActiveRecord::Schema.define(version: 20170720000000) do add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id", on_delete: :cascade add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id", on_delete: :cascade add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id", on_delete: :cascade + add_foreign_key "oauth_applications", "users", column: "owner_id", on_delete: :cascade add_foreign_key "preview_cards", "statuses", on_delete: :cascade add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", on_delete: :nullify add_foreign_key "reports", "accounts", column: "target_account_id", on_delete: :cascade diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index fa27e6ec6..7902a4334 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -156,7 +156,7 @@ describe Settings::ApplicationsController do let(:token) { user.token_for_app(app) } before do expect(token).to_not be_nil - put :regenerate, params: { application_id: app.id } + post :regenerate, params: { id: app.id } end it 'should create new token' do -- cgit From 80393a23d0a0c296d4356a2a21cf8504435265bf Mon Sep 17 00:00:00 2001 From: nullkal Date: Wed, 23 Aug 2017 22:16:20 +0900 Subject: Use checkboxes for application scope setting (#4671) --- .../settings/applications_controller.rb | 6 +++++ app/views/settings/applications/_fields.html.haml | 14 ++++++++-- .../settings/applications_controller_spec.rb | 30 +++++++++++++++++++--- 3 files changed, 44 insertions(+), 6 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 894222c2a..8fc9a0fa9 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -5,6 +5,7 @@ class Settings::ApplicationsController < ApplicationController before_action :authenticate_user! before_action :set_application, only: [:show, :update, :destroy, :regenerate] + before_action :prepare_scopes, only: [:create, :update] def index @applications = current_user.applications.page(params[:page]) @@ -63,4 +64,9 @@ class Settings::ApplicationsController < ApplicationController :website ) end + + def prepare_scopes + scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) + params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array + end end diff --git a/app/views/settings/applications/_fields.html.haml b/app/views/settings/applications/_fields.html.haml index 536f69e04..83297a1ae 100644 --- a/app/views/settings/applications/_fields.html.haml +++ b/app/views/settings/applications/_fields.html.haml @@ -7,5 +7,15 @@ %p.hint= t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: Doorkeeper.configuration.native_redirect_uri) -.fields-group - = f.input :scopes, wrapper: :with_label, label: t('activerecord.attributes.doorkeeper/application.scopes'), hint: t('doorkeeper.applications.help.scopes') +.field-group + = f.input :scopes, + label: t('activerecord.attributes.doorkeeper/application.scopes'), + collection: Doorkeeper.configuration.scopes, + wrapper: :with_label, + include_blank: false, + selected: f.object.scopes.all, + required: false, + as: :check_boxes, + collection_wrapper_tag: 'ul', + item_wrapper_tag: 'li' + diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index 7902a4334..ca66f8d23 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -45,7 +45,7 @@ describe Settings::ApplicationsController do end describe 'POST #create' do - context 'success' do + context 'success (passed scopes as a String)' do def call_create post :create, params: { doorkeeper_application: { @@ -61,7 +61,29 @@ describe Settings::ApplicationsController do it 'creates an entry in the database' do expect { call_create }.to change(Doorkeeper::Application, :count) end - + + it 'redirects back to applications page' do + expect(call_create).to redirect_to(settings_applications_path) + end + end + + context 'success (passed scopes as an Array)' do + def call_create + post :create, params: { + doorkeeper_application: { + name: 'My New App', + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + website: 'http://google.com', + scopes: [ 'read', 'write', 'follow' ] + } + } + response + end + + it 'creates an entry in the database' do + expect { call_create }.to change(Doorkeeper::Application, :count) + end + it 'redirects back to applications page' do expect(call_create).to redirect_to(settings_applications_path) end @@ -74,7 +96,7 @@ describe Settings::ApplicationsController do name: '', redirect_uri: '', website: '', - scopes: '' + scopes: [] } } end @@ -123,7 +145,7 @@ describe Settings::ApplicationsController do name: '', redirect_uri: '', website: '', - scopes: '' + scopes: [] } } end -- cgit From c66fe2aeba84af5ab47c20298ddc8dceaf0e179f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 24 Aug 2017 13:31:55 +0200 Subject: Minor performance improvement for test suite (#4678) --- app/models/account.rb | 2 +- .../api/v1/accounts/relationships_controller_spec.rb | 4 ++-- spec/spec_helper.rb | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) (limited to 'spec/controllers') diff --git a/app/models/account.rb b/app/models/account.rb index c3be975fb..0c9c6aed4 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -268,7 +268,7 @@ class Account < ApplicationRecord def generate_keys return unless local? - keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048) + keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 512 : 2048) self.private_key = keypair.to_pem self.public_key = keypair.public_key.to_pem end diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb index 3a9607317..a9073b197 100644 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb @@ -50,14 +50,14 @@ describe Api::V1::Accounts::RelationshipsController do json = body_as_json expect(json).to be_a Enumerable - expect(json.first[:id]).to be simon.id + expect(json.first[:id]).to eq simon.id expect(json.first[:following]).to be true expect(json.first[:followed_by]).to be false expect(json.first[:muting]).to be false expect(json.first[:requested]).to be false expect(json.first[:domain_blocking]).to be false - expect(json.second[:id]).to be lewis.id + expect(json.second[:id]).to eq lewis.id expect(json.second[:following]).to be false expect(json.second[:followed_by]).to be true expect(json.second[:muting]).to be false diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2bc462121..eecaec4ac 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,15 @@ require 'simplecov' +GC.disable + SimpleCov.start 'rails' do add_group 'Services', 'app/services' add_group 'Presenters', 'app/presenters' add_group 'Validators', 'app/validators' end +gc_counter = -1 + RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true @@ -22,8 +26,21 @@ RSpec.configure do |config| end config.after :suite do + gc_counter = 0 FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"]) end + + config.after :each do + gc_counter += 1 + + if gc_counter > 19 + GC.enable + GC.start + GC.disable + + gc_counter = 0 + end + end end def body_as_json -- cgit From b01a19fe392e0dd16d6b3da3f0b56369f7837cc9 Mon Sep 17 00:00:00 2001 From: unarist Date: Thu, 24 Aug 2017 23:21:42 +0900 Subject: Fetch reblogs as Announce activity instead of Note object (#4672) * Process Create / Announce activity in FetchRemoteStatusService * Use activity URL in ActivityPub for reblogs * Redirect to the original status on StatusesController#show --- app/controllers/statuses_controller.rb | 5 ++ app/lib/activitypub/tag_manager.rb | 8 +++ app/serializers/activitypub/activity_serializer.rb | 2 +- .../activitypub/fetch_remote_status_service.rb | 30 ++++++--- spec/controllers/statuses_controller_spec.rb | 12 ++++ .../fetch_remote_status_service_spec.rb | 72 +++++++++++++++++++++- 6 files changed, 118 insertions(+), 11 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index aa24f23c9..a9768d092 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -9,6 +9,7 @@ class StatusesController < ApplicationController before_action :set_status before_action :set_link_headers before_action :check_account_suspension + before_action :redirect_to_original, only: [:show] def show respond_to do |format| @@ -58,4 +59,8 @@ class StatusesController < ApplicationController def check_account_suspension gone if @account.suspended? end + + def redirect_to_original + redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog? + end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 3c16006cb..de575d9e6 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -19,6 +19,7 @@ class ActivityPub::TagManager when :person short_account_url(target) when :note, :comment, :activity + return activity_account_status_url(target.account, target) if target.reblog? short_account_status_url(target.account, target) end end @@ -30,10 +31,17 @@ class ActivityPub::TagManager when :person account_url(target) when :note, :comment, :activity + return activity_account_status_url(target.account, target) if target.reblog? account_status_url(target.account, target) end end + def activity_uri_for(target) + return nil unless %i(note comment activity).include?(target.object_type) && target.local? + + activity_account_status_url(target.account, target) + end + # Primary audience of a status # Public statuses go out to primarily the public collection # Unlisted and private statuses go out primarily to the followers collection diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index 69e2160c5..d20ee9920 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer def id - [ActivityPub::TagManager.instance.uri_for(object), '/activity'].join + [ActivityPub::TagManager.instance.activity_uri_for(object)].join end def type diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 993e5389c..c114515cd 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -7,21 +7,33 @@ class ActivityPub::FetchRemoteStatusService < BaseService def call(uri, prefetched_json = nil) @json = body_to_json(prefetched_json) || fetch_resource(uri) - return unless supported_context? && expected_type? + return unless supported_context? - attributed_to = first_of_value(@json['attributedTo']) - attributed_to = attributed_to['id'] if attributed_to.is_a?(Hash) + activity = activity_json + actor_id = value_or_id(activity['actor']) - return unless trustworthy_attribution?(uri, attributed_to) + return unless expected_type?(activity) && trustworthy_attribution?(uri, actor_id) - actor = ActivityPub::TagManager.instance.uri_to_resource(attributed_to, Account) - actor = ActivityPub::FetchRemoteAccountService.new.call(attributed_to) if actor.nil? + actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account) + actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id) if actor.nil? - ActivityPub::Activity::Create.new({ 'object' => @json }, actor).perform + ActivityPub::Activity.factory(activity, actor).perform end private + def activity_json + if %w(Note Article).include? @json['type'] + { + 'type' => 'Create', + 'actor' => first_of_value(@json['attributedTo']), + 'object' => @json, + } + else + @json + end + end + def trustworthy_attribution?(uri, attributed_to) Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero? end @@ -30,7 +42,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService super(@json) end - def expected_type? - %w(Note Article).include? @json['type'] + def expected_type?(json) + %w(Create Announce).include? json['type'] end end diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 88d365624..95fb4d594 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -30,6 +30,18 @@ describe StatusesController do end end + context 'status is a reblog' do + it 'redirects to the original status' do + original_account = Fabricate(:account, domain: 'example.com') + original_status = Fabricate(:status, account: original_account, uri: 'tag:example.com,2017:foo', url: 'https://example.com/123') + status = Fabricate(:status, reblog: original_status) + + get :show, params: { account_username: status.account.username, id: status.id } + + expect(response).to redirect_to(original_status.url) + end + end + context 'account is not suspended and status is permitted' do it 'assigns @account' do status = Fabricate(:status) diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 47a33b6cb..3b22257ed 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -1,5 +1,75 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteStatusService do - pending + let(:sender) { Fabricate(:account) } + let(:recipient) { Fabricate(:account) } + let(:valid_domain) { Rails.configuration.x.local_domain } + + let(:note) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: "https://#{valid_domain}/@foo/1234", + type: 'Note', + content: 'Lorem ipsum', + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + } + end + + let(:create) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: "https://#{valid_domain}/@foo/1234/activity", + type: 'Create', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: note, + } + end + + subject { described_class.new } + + describe '#call' do + before do + subject.call(object[:id], Oj.dump(object)) + end + + context 'with Note object' do + let(:object) { note } + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + end + end + + context 'with Create activity' do + let(:object) { create } + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + end + end + + context 'with Announce activity' do + let(:status) { Fabricate(:status, account: recipient) } + + let(:object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: "https://#{valid_domain}/@foo/1234/activity", + type: 'Announce', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: ActivityPub::TagManager.instance.uri_for(status), + } + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(status)).to be true + end + end + end end -- cgit From 9caa90025fd9f1ef46a74f31cefd19335e291e76 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 25 Aug 2017 01:41:18 +0200 Subject: Pinned statuses (#4675) * Pinned statuses * yarn manage:translations --- app/controllers/accounts_controller.rb | 25 +++++-- .../api/v1/accounts/statuses_controller.rb | 5 ++ app/controllers/api/v1/statuses/pins_controller.rb | 28 ++++++++ app/javascript/mastodon/actions/interactions.js | 78 ++++++++++++++++++++++ app/javascript/mastodon/components/status.js | 1 + .../mastodon/components/status_action_bar.js | 11 +++ .../mastodon/containers/status_container.js | 10 +++ .../features/status/components/action_bar.js | 11 +++ app/javascript/mastodon/features/status/index.js | 11 +++ app/javascript/mastodon/locales/ar.json | 2 + app/javascript/mastodon/locales/bg.json | 2 + app/javascript/mastodon/locales/ca.json | 2 + app/javascript/mastodon/locales/de.json | 2 + .../mastodon/locales/defaultMessages.json | 16 +++++ app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/locales/eo.json | 2 + app/javascript/mastodon/locales/es.json | 2 + app/javascript/mastodon/locales/fa.json | 2 + app/javascript/mastodon/locales/fi.json | 2 + app/javascript/mastodon/locales/fr.json | 2 + app/javascript/mastodon/locales/he.json | 2 + app/javascript/mastodon/locales/hr.json | 2 + app/javascript/mastodon/locales/hu.json | 2 + app/javascript/mastodon/locales/id.json | 2 + app/javascript/mastodon/locales/io.json | 2 + app/javascript/mastodon/locales/it.json | 2 + app/javascript/mastodon/locales/ja.json | 2 + app/javascript/mastodon/locales/ko.json | 2 + app/javascript/mastodon/locales/nl.json | 2 + app/javascript/mastodon/locales/no.json | 2 + app/javascript/mastodon/locales/oc.json | 2 + app/javascript/mastodon/locales/pl.json | 2 + app/javascript/mastodon/locales/pt-BR.json | 2 + app/javascript/mastodon/locales/pt.json | 2 + app/javascript/mastodon/locales/ru.json | 2 + app/javascript/mastodon/locales/th.json | 2 + app/javascript/mastodon/locales/tr.json | 2 + app/javascript/mastodon/locales/uk.json | 2 + app/javascript/mastodon/locales/zh-CN.json | 2 + app/javascript/mastodon/locales/zh-HK.json | 2 + app/javascript/mastodon/locales/zh-TW.json | 2 + app/javascript/mastodon/reducers/statuses.js | 4 ++ app/models/account.rb | 4 ++ app/models/concerns/account_interactions.rb | 4 ++ app/models/status.rb | 4 ++ app/models/status_pin.rb | 16 +++++ app/presenters/status_relationships_presenter.rb | 19 ++++-- app/serializers/rest/status_serializer.rb | 16 +++++ app/validators/status_pin_validator.rb | 9 +++ app/views/accounts/show.html.haml | 3 + app/views/stream_entries/_status.html.haml | 7 ++ config/locales/en.yml | 5 ++ config/routes.rb | 13 ++-- db/migrate/20170823162448_create_status_pins.rb | 10 +++ db/schema.rb | 12 +++- .../api/v1/accounts/statuses_controller_spec.rb | 36 +++++++--- .../api/v1/statuses/pins_controller_spec.rb | 57 ++++++++++++++++ spec/fabricators/status_pin_fabricator.rb | 4 ++ spec/models/status_pin_spec.rb | 41 ++++++++++++ 59 files changed, 493 insertions(+), 29 deletions(-) create mode 100644 app/controllers/api/v1/statuses/pins_controller.rb create mode 100644 app/models/status_pin.rb create mode 100644 app/validators/status_pin_validator.rb create mode 100644 db/migrate/20170823162448_create_status_pins.rb create mode 100644 spec/controllers/api/v1/statuses/pins_controller_spec.rb create mode 100644 spec/fabricators/status_pin_fabricator.rb create mode 100644 spec/models/status_pin_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index c6b98628e..f4ca239ba 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,14 +7,17 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do + @pinned_statuses = [] + if current_account && @account.blocking?(current_account) @statuses = [] return end - @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) - @statuses = cache_collection(@statuses, Status) - @next_url = next_url unless @statuses.empty? + @pinned_statuses = cache_collection(@account.pinned_statuses.limit(1), Status) unless media_requested? + @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = cache_collection(@statuses, Status) + @next_url = next_url unless @statuses.empty? end format.atom do @@ -32,8 +35,8 @@ class AccountsController < ApplicationController def filtered_statuses default_statuses.tap do |statuses| - statuses.merge!(only_media_scope) if request.path.ends_with?('/media') - statuses.merge!(no_replies_scope) unless request.path.ends_with?('/with_replies') + statuses.merge!(only_media_scope) if media_requested? + statuses.merge!(no_replies_scope) unless replies_requested? end end @@ -58,12 +61,20 @@ class AccountsController < ApplicationController end def next_url - if request.path.ends_with?('/media') + if media_requested? short_account_media_url(@account, max_id: @statuses.last.id) - elsif request.path.ends_with?('/with_replies') + elsif replies_requested? short_account_with_replies_url(@account, max_id: @statuses.last.id) else short_account_url(@account, max_id: @statuses.last.id) end end + + def media_requested? + request.path.ends_with?('/media') + end + + def replies_requested? + request.path.ends_with?('/with_replies') + end end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index d9ae5c089..095f6937b 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -29,6 +29,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def account_statuses default_statuses.tap do |statuses| statuses.merge!(only_media_scope) if params[:only_media] + statuses.merge!(pinned_scope) if params[:pinned] statuses.merge!(no_replies_scope) if params[:exclude_replies] end end @@ -53,6 +54,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController @account.media_attachments.attached.reorder(nil).select(:status_id).distinct end + def pinned_scope + @account.pinned_statuses + end + def no_replies_scope Status.without_replies end diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb new file mode 100644 index 000000000..3de1009b8 --- /dev/null +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::PinsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write } + before_action :require_user! + before_action :set_status + + respond_to :json + + def create + StatusPin.create!(account: current_account, status: @status) + render json: @status, serializer: REST::StatusSerializer + end + + def destroy + pin = StatusPin.find_by(account: current_account, status: @status) + pin&.destroy! + render json: @status, serializer: REST::StatusSerializer + end + + private + + def set_status + @status = Status.find(params[:status_id]) + end +end diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 36eec4934..7b5f4bd9c 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -24,6 +24,14 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; +export const PIN_REQUEST = 'PIN_REQUEST'; +export const PIN_SUCCESS = 'PIN_SUCCESS'; +export const PIN_FAIL = 'PIN_FAIL'; + +export const UNPIN_REQUEST = 'UNPIN_REQUEST'; +export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; +export const UNPIN_FAIL = 'UNPIN_FAIL'; + export function reblog(status) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -233,3 +241,73 @@ export function fetchFavouritesFail(id, error) { error, }; }; + +export function pin(status) { + return (dispatch, getState) => { + dispatch(pinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { + dispatch(pinSuccess(status, response.data)); + }).catch(error => { + dispatch(pinFail(status, error)); + }); + }; +}; + +export function pinRequest(status) { + return { + type: PIN_REQUEST, + status, + }; +}; + +export function pinSuccess(status, response) { + return { + type: PIN_SUCCESS, + status, + response, + }; +}; + +export function pinFail(status, error) { + return { + type: PIN_FAIL, + status, + error, + }; +}; + +export function unpin (status) { + return (dispatch, getState) => { + dispatch(unpinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { + dispatch(unpinSuccess(status, response.data)); + }).catch(error => { + dispatch(unpinFail(status, error)); + }); + }; +}; + +export function unpinRequest(status) { + return { + type: UNPIN_REQUEST, + status, + }; +}; + +export function unpinSuccess(status, response) { + return { + type: UNPIN_SUCCESS, + status, + response, + }; +}; + +export function unpinFail(status, error) { + return { + type: UNPIN_FAIL, + status, + error, + }; +}; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 38a4aafc1..b4f523f72 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -31,6 +31,7 @@ export default class Status extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onPin: PropTypes.func, onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, onBlock: PropTypes.func, diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0d8c9add4..6436d6ebe 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -21,6 +21,8 @@ const messages = defineMessages({ report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, + unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, }); @injectIntl @@ -41,6 +43,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onBlock: PropTypes.func, onReport: PropTypes.func, onMuteConversation: PropTypes.func, + onPin: PropTypes.func, me: PropTypes.number, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -77,6 +80,10 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onDelete(this.props.status); } + handlePinClick = () => { + this.props.onPin(this.props.status); + } + handleMentionClick = () => { this.props.onMention(this.props.status.get('account'), this.context.router.history); } @@ -121,6 +128,10 @@ export default class StatusActionBar extends ImmutablePureComponent { } if (status.getIn(['account', 'id']) === me) { + if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + } + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index b150165aa..c488b6ce7 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -11,6 +11,8 @@ import { favourite, unreblog, unfavourite, + pin, + unpin, } from '../actions/interactions'; import { blockAccount, @@ -72,6 +74,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onPin (status) { + if (status.get('pinned')) { + dispatch(unpin(status)); + } else { + dispatch(pin(status)); + } + }, + onDelete (status) { if (!this.deleteModal) { dispatch(deleteStatus(status.get('id'))); diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 91ac64de2..c4a614677 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -14,6 +14,8 @@ const messages = defineMessages({ favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, share: { id: 'status.share', defaultMessage: 'Share' }, + pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, + unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, }); @injectIntl @@ -31,6 +33,7 @@ export default class ActionBar extends React.PureComponent { onDelete: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onReport: PropTypes.func, + onPin: PropTypes.func, me: PropTypes.number.isRequired, intl: PropTypes.object.isRequired, }; @@ -59,6 +62,10 @@ export default class ActionBar extends React.PureComponent { this.props.onReport(this.props.status); } + handlePinClick = () => { + this.props.onPin(this.props.status); + } + handleShare = () => { navigator.share({ text: this.props.status.get('search_index'), @@ -72,6 +79,10 @@ export default class ActionBar extends React.PureComponent { let menu = []; if (me === status.getIn(['account', 'id'])) { + if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + } + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index cbabdd5bc..84e717a12 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -12,6 +12,8 @@ import { unfavourite, reblog, unreblog, + pin, + unpin, } from '../../actions/interactions'; import { replyCompose, @@ -87,6 +89,14 @@ export default class Status extends ImmutablePureComponent { } } + handlePin = (status) => { + if (status.get('pinned')) { + this.props.dispatch(unpin(status)); + } else { + this.props.dispatch(pin(status)); + } + } + handleReplyClick = (status) => { this.props.dispatch(replyCompose(status, this.context.router.history)); } @@ -187,6 +197,7 @@ export default class Status extends ImmutablePureComponent { onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} + onPin={this.handlePin} /> {descendants} diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index f5cf77f92..fa8cda97d 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -168,6 +168,7 @@ "status.mention": "أذكُر @{name}", "status.mute_conversation": "Mute conversation", "status.open": "وسع هذه المشاركة", + "status.pin": "Pin on profile", "status.reblog": "رَقِّي", "status.reblogged_by": "{name} رقى", "status.reply": "ردّ", @@ -179,6 +180,7 @@ "status.show_less": "إعرض أقلّ", "status.show_more": "أظهر المزيد", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "تحرير", "tabs_bar.federated_timeline": "الموحَّد", "tabs_bar.home": "الرئيسية", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index e6788f9eb..4aa097d31 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -168,6 +168,7 @@ "status.mention": "Споменаване", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Споделяне", "status.reblogged_by": "{name} сподели", "status.reply": "Отговор", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Съставяне", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Начало", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 95b3c60bf..d9cb7c7a3 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -168,6 +168,7 @@ "status.mention": "Esmentar @{name}", "status.mute_conversation": "Silenciar conversació", "status.open": "Ampliar aquest estat", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} ha retootejat", "status.reply": "Respondre", @@ -179,6 +180,7 @@ "status.show_less": "Mostra menys", "status.show_more": "Mostra més", "status.unmute_conversation": "Activar conversació", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compondre", "tabs_bar.federated_timeline": "Federada", "tabs_bar.home": "Inici", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 67a99b765..a5232552f 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -168,6 +168,7 @@ "status.mention": "Erwähnen", "status.mute_conversation": "Mute conversation", "status.open": "Öffnen", + "status.pin": "Pin on profile", "status.reblog": "Teilen", "status.reblogged_by": "{name} teilte", "status.reply": "Antworten", @@ -179,6 +180,7 @@ "status.show_less": "Weniger anzeigen", "status.show_more": "Mehr anzeigen", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Schreiben", "tabs_bar.federated_timeline": "Föderation", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index ef76f6e5b..fdb8aefe1 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -189,6 +189,14 @@ { "defaultMessage": "Unmute conversation", "id": "status.unmute_conversation" + }, + { + "defaultMessage": "Pin on profile", + "id": "status.pin" + }, + { + "defaultMessage": "Unpin from profile", + "id": "status.unpin" } ], "path": "app/javascript/mastodon/components/status_action_bar.json" @@ -1035,6 +1043,14 @@ { "defaultMessage": "Share", "id": "status.share" + }, + { + "defaultMessage": "Pin on profile", + "id": "status.pin" + }, + { + "defaultMessage": "Unpin from profile", + "id": "status.unpin" } ], "path": "app/javascript/mastodon/features/status/components/action_bar.json" diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 2ea2062d3..595063888 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -168,6 +168,7 @@ "status.mention": "Mention @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} boosted", "status.reply": "Reply", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 960d747ec..ed323f406 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -168,6 +168,7 @@ "status.mention": "Mencii @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Diskonigi", "status.reblogged_by": "{name} diskonigita", "status.reply": "Respondi", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Ekskribi", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Hejmo", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 212d16639..2fee29148 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar", "status.mute_conversation": "Mute conversation", "status.open": "Expandir estado", + "status.pin": "Pin on profile", "status.reblog": "Retoot", "status.reblogged_by": "Retooteado por {name}", "status.reply": "Responder", @@ -179,6 +180,7 @@ "status.show_less": "Mostrar menos", "status.show_more": "Mostrar más", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Redactar", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Inicio", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 5ada62f93..89fa014e4 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -168,6 +168,7 @@ "status.mention": "نام‌بردن از @{name}", "status.mute_conversation": "بی‌صداکردن گفتگو", "status.open": "این نوشته را باز کن", + "status.pin": "Pin on profile", "status.reblog": "بازبوقیدن", "status.reblogged_by": "‫{name}‬ بازبوقید", "status.reply": "پاسخ", @@ -179,6 +180,7 @@ "status.show_less": "نهفتن", "status.show_more": "نمایش", "status.unmute_conversation": "باصداکردن گفتگو", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "بنویسید", "tabs_bar.federated_timeline": "همگانی", "tabs_bar.home": "خانه", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index cb9e9c2a6..1c1334899 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -168,6 +168,7 @@ "status.mention": "Mainitse @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Buustaa", "status.reblogged_by": "{name} buustasi", "status.reply": "Vastaa", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Luo", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Koti", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 34a89a69f..479b8de7d 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -168,6 +168,7 @@ "status.mention": "Mentionner", "status.mute_conversation": "Masquer la conversation", "status.open": "Déplier ce statut", + "status.pin": "Pin on profile", "status.reblog": "Partager", "status.reblogged_by": "{name} a partagé :", "status.reply": "Répondre", @@ -179,6 +180,7 @@ "status.show_less": "Replier", "status.show_more": "Déplier", "status.unmute_conversation": "Ne plus masquer la conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Composer", "tabs_bar.federated_timeline": "Fil public global", "tabs_bar.home": "Accueil", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 34266d8e1..1e221af9c 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -168,6 +168,7 @@ "status.mention": "פניה אל @{name}", "status.mute_conversation": "השתקת שיחה", "status.open": "הרחבת הודעה", + "status.pin": "Pin on profile", "status.reblog": "הדהוד", "status.reblogged_by": "הודהד על ידי {name}", "status.reply": "תגובה", @@ -179,6 +180,7 @@ "status.show_less": "הראה פחות", "status.show_more": "הראה יותר", "status.unmute_conversation": "הסרת השתקת שיחה", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "חיבור", "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי", "tabs_bar.home": "בבית", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index f69b096d4..2effecb1e 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -168,6 +168,7 @@ "status.mention": "Spomeni @{name}", "status.mute_conversation": "Utišaj razgovor", "status.open": "Proširi ovaj status", + "status.pin": "Pin on profile", "status.reblog": "Podigni", "status.reblogged_by": "{name} je podigao", "status.reply": "Odgovori", @@ -179,6 +180,7 @@ "status.show_less": "Pokaži manje", "status.show_more": "Pokaži više", "status.unmute_conversation": "Poništi utišavanje razgovora", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Sastavi", "tabs_bar.federated_timeline": "Federalni", "tabs_bar.home": "Dom", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 4d2a50963..59a7b8deb 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -168,6 +168,7 @@ "status.mention": "Említés", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Reblog", "status.reblogged_by": "{name} reblogolta", "status.reply": "Válasz", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Összeállítás", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Kezdőlap", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 532739e3c..9dd66b6cd 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -168,6 +168,7 @@ "status.mention": "Balasan @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Tampilkan status ini", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "di-boost {name}", "status.reply": "Balas", @@ -179,6 +180,7 @@ "status.show_less": "Tampilkan lebih sedikit", "status.show_more": "Tampilkan semua", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Tulis", "tabs_bar.federated_timeline": "Gabungan", "tabs_bar.home": "Beranda", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index a5e363e40..07184ae81 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Detaligar ca mesajo", + "status.pin": "Pin on profile", "status.reblog": "Repetar", "status.reblogged_by": "{name} repetita", "status.reply": "Respondar", @@ -179,6 +180,7 @@ "status.show_less": "Montrar mine", "status.show_more": "Montrar plue", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Kompozar", "tabs_bar.federated_timeline": "Federata", "tabs_bar.home": "Hemo", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 329eb82ca..369ae7f32 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -168,6 +168,7 @@ "status.mention": "Nomina @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Espandi questo post", + "status.pin": "Pin on profile", "status.reblog": "Condividi", "status.reblogged_by": "{name} ha condiviso", "status.reply": "Rispondi", @@ -179,6 +180,7 @@ "status.show_less": "Mostra meno", "status.show_more": "Mostra di più", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Scrivi", "tabs_bar.federated_timeline": "Federazione", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 757190c90..c35b0def3 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -168,6 +168,7 @@ "status.mention": "返信", "status.mute_conversation": "会話をミュート", "status.open": "詳細を表示", + "status.pin": "Pin on profile", "status.reblog": "ブースト", "status.reblogged_by": "{name}さんにブーストされました", "status.reply": "返信", @@ -179,6 +180,7 @@ "status.show_less": "隠す", "status.show_more": "もっと見る", "status.unmute_conversation": "会話のミュートを解除", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "投稿", "tabs_bar.federated_timeline": "連合", "tabs_bar.home": "ホーム", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 47d0d4087..52ba1e70f 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -168,6 +168,7 @@ "status.mention": "답장", "status.mute_conversation": "이 대화를 뮤트", "status.open": "상세 정보 표시", + "status.pin": "Pin on profile", "status.reblog": "부스트", "status.reblogged_by": "{name}님이 부스트 했습니다", "status.reply": "답장", @@ -179,6 +180,7 @@ "status.show_less": "숨기기", "status.show_more": "더 보기", "status.unmute_conversation": "이 대화의 뮤트 해제하기", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "포스트", "tabs_bar.federated_timeline": "연합", "tabs_bar.home": "홈", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 4d68c7992..fb4127831 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -168,6 +168,7 @@ "status.mention": "Vermeld @{name}", "status.mute_conversation": "Negeer conversatie", "status.open": "Toot volledig tonen", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} boostte", "status.reply": "Reageren", @@ -179,6 +180,7 @@ "status.show_less": "Minder tonen", "status.show_more": "Meer tonen", "status.unmute_conversation": "Conversatie niet meer negeren", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Schrijven", "tabs_bar.federated_timeline": "Globaal", "tabs_bar.home": "Start", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 9453e65ff..2d6224c48 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -168,6 +168,7 @@ "status.mention": "Nevn @{name}", "status.mute_conversation": "Demp samtale", "status.open": "Utvid denne statusen", + "status.pin": "Pin on profile", "status.reblog": "Fremhev", "status.reblogged_by": "Fremhevd av {name}", "status.reply": "Svar", @@ -179,6 +180,7 @@ "status.show_less": "Vis mindre", "status.show_more": "Vis mer", "status.unmute_conversation": "Ikke demp samtale", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Komponer", "tabs_bar.federated_timeline": "Felles", "tabs_bar.home": "Hjem", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 5e5e28af0..34e1a8c47 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar", "status.mute_conversation": "Rescondre la conversacion", "status.open": "Desplegar aqueste estatut", + "status.pin": "Pin on profile", "status.reblog": "Partejar", "status.reblogged_by": "{name} a partejat :", "status.reply": "Respondre", @@ -179,6 +180,7 @@ "status.show_less": "Tornar plegar", "status.show_more": "Desplegar", "status.unmute_conversation": "Conversacions amb silenci levat", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compausar", "tabs_bar.federated_timeline": "Flux public global", "tabs_bar.home": "Acuèlh", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index af38bbb6c..8a8d0f38a 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -168,6 +168,7 @@ "status.mention": "Wspomnij o @{name}", "status.mute_conversation": "Wycisz konwersację", "status.open": "Rozszerz ten status", + "status.pin": "Pin on profile", "status.reblog": "Podbij", "status.reblogged_by": "{name} podbił", "status.reply": "Odpowiedz", @@ -179,6 +180,7 @@ "status.show_less": "Pokaż mniej", "status.show_more": "Pokaż więcej", "status.unmute_conversation": "Cofnij wyciszenie konwersacji", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Napisz", "tabs_bar.federated_timeline": "Globalne", "tabs_bar.home": "Strona główna", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 55d2f05de..8a299e272 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expandir", + "status.pin": "Pin on profile", "status.reblog": "Partilhar", "status.reblogged_by": "{name} partilhou", "status.reply": "Responder", @@ -179,6 +180,7 @@ "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 55d2f05de..8a299e272 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expandir", + "status.pin": "Pin on profile", "status.reblog": "Partilhar", "status.reblogged_by": "{name} partilhou", "status.reply": "Responder", @@ -179,6 +180,7 @@ "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index af38fc723..822f116c7 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -168,6 +168,7 @@ "status.mention": "Упомянуть @{name}", "status.mute_conversation": "Заглушить тред", "status.open": "Развернуть статус", + "status.pin": "Pin on profile", "status.reblog": "Продвинуть", "status.reblogged_by": "{name} продвинул(а)", "status.reply": "Ответить", @@ -179,6 +180,7 @@ "status.show_less": "Свернуть", "status.show_more": "Развернуть", "status.unmute_conversation": "Снять глушение с треда", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Написать", "tabs_bar.federated_timeline": "Глобальная", "tabs_bar.home": "Главная", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index aa0929f82..9c985eec9 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -168,6 +168,7 @@ "status.mention": "Mention @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} boosted", "status.reply": "Reply", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 37ce8597e..41c9d44a7 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -168,6 +168,7 @@ "status.mention": "Bahset @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Bu gönderiyi genişlet", + "status.pin": "Pin on profile", "status.reblog": "Boost'la", "status.reblogged_by": "{name} boost etti", "status.reply": "Cevapla", @@ -179,6 +180,7 @@ "status.show_less": "Daha azı", "status.show_more": "Daha fazlası", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Oluştur", "tabs_bar.federated_timeline": "Federe", "tabs_bar.home": "Ana sayfa", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index fea7bd94e..6087e3a1e 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -168,6 +168,7 @@ "status.mention": "Згадати", "status.mute_conversation": "Заглушити діалог", "status.open": "Розгорнути допис", + "status.pin": "Pin on profile", "status.reblog": "Передмухнути", "status.reblogged_by": "{name} передмухнув(-ла)", "status.reply": "Відповісти", @@ -179,6 +180,7 @@ "status.show_less": "Згорнути", "status.show_more": "Розгорнути", "status.unmute_conversation": "Зняти глушення з діалогу", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Написати", "tabs_bar.federated_timeline": "Глобальна", "tabs_bar.home": "Головна", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index e7c431454..2e3b4b0b8 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -168,6 +168,7 @@ "status.mention": "提及 @{name}", "status.mute_conversation": "Mute conversation", "status.open": "展开嘟文", + "status.pin": "Pin on profile", "status.reblog": "转嘟", "status.reblogged_by": "{name} 转嘟", "status.reply": "回应", @@ -179,6 +180,7 @@ "status.show_less": "减少显示", "status.show_more": "显示更多", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "撰写", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主页", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 7312aae82..1ab3b3f9d 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -168,6 +168,7 @@ "status.mention": "提及 @{name}", "status.mute_conversation": "Mute conversation", "status.open": "展開文章", + "status.pin": "Pin on profile", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推", "status.reply": "回應", @@ -179,6 +180,7 @@ "status.show_less": "減少顯示", "status.show_more": "顯示更多", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "撰寫", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主頁", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 1c2e35272..571a2383d 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -168,6 +168,7 @@ "status.mention": "提到 @{name}", "status.mute_conversation": "消音對話", "status.open": "展開這個狀態", + "status.pin": "Pin on profile", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推了", "status.reply": "回應", @@ -179,6 +180,7 @@ "status.show_less": "看少點", "status.show_more": "看更多", "status.unmute_conversation": "不消音對話", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "編輯", "tabs_bar.federated_timeline": "聯盟", "tabs_bar.home": "家", diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 3e40b0b42..38691dc43 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -7,6 +7,8 @@ import { FAVOURITE_SUCCESS, FAVOURITE_FAIL, UNFAVOURITE_SUCCESS, + PIN_SUCCESS, + UNPIN_SUCCESS, } from '../actions/interactions'; import { STATUS_FETCH_SUCCESS, @@ -114,6 +116,8 @@ export default function statuses(state = initialState, action) { case UNREBLOG_SUCCESS: case FAVOURITE_SUCCESS: case UNFAVOURITE_SUCCESS: + case PIN_SUCCESS: + case UNPIN_SUCCESS: return normalizeStatus(state, action.response); case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); diff --git a/app/models/account.rb b/app/models/account.rb index 0c9c6aed4..b83aa1159 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -77,6 +77,10 @@ class Account < ApplicationRecord has_many :mentions, inverse_of: :account, dependent: :destroy has_many :notifications, inverse_of: :account, dependent: :destroy + # Pinned statuses + has_many :status_pins, inverse_of: :account, dependent: :destroy + has_many :pinned_statuses, through: :status_pins, class_name: 'Status', source: :status + # Media has_many :media_attachments, dependent: :destroy diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 9ffed2910..b26520f5b 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -138,4 +138,8 @@ module AccountInteractions def reblogged?(status) status.proper.reblogs.where(account: self).exists? end + + def pinned?(status) + status_pins.where(status: status).exists? + end end diff --git a/app/models/status.rb b/app/models/status.rb index 24eaf7071..3dc83ad1f 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -164,6 +164,10 @@ class Status < ApplicationRecord ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h end + def pins_map(status_ids, account_id) + StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h + end + def reload_stale_associations!(cached_items) account_ids = [] diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb new file mode 100644 index 000000000..c9a669344 --- /dev/null +++ b/app/models/status_pin.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: status_pins +# +# id :integer not null, primary key +# account_id :integer not null +# status_id :integer not null +# + +class StatusPin < ApplicationRecord + belongs_to :account, required: true + belongs_to :status, required: true + + validates_with StatusPinValidator +end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index 03294015f..10b449504 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -1,19 +1,24 @@ # frozen_string_literal: true class StatusRelationshipsPresenter - attr_reader :reblogs_map, :favourites_map, :mutes_map + attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map - def initialize(statuses, current_account_id = nil, reblogs_map: {}, favourites_map: {}, mutes_map: {}) + def initialize(statuses, current_account_id = nil, options = {}) if current_account_id.nil? @reblogs_map = {} @favourites_map = {} @mutes_map = {} + @pins_map = {} else - status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq - conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq - @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(reblogs_map) - @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(favourites_map) - @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(mutes_map) + statuses = statuses.compact + status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq + conversation_ids = statuses.map(&:conversation_id).compact.uniq + pinnable_status_ids = statuses.map(&:proper).select { |s| s.account_id == current_account_id && %w(public unlisted).include?(s.visibility) }.map(&:id) + + @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) + @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {}) + @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {}) + @pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {}) end end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 246b12a90..298a3bb40 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -8,6 +8,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? attribute :muted, if: :current_user? + attribute :pinned, if: :pinnable? belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application @@ -57,6 +58,21 @@ class REST::StatusSerializer < ActiveModel::Serializer end end + def pinned + if instance_options && instance_options[:relationships] + instance_options[:relationships].pins_map[object.id] || false + else + current_user.account.pinned?(object) + end + end + + def pinnable? + current_user? && + current_user.account_id == object.account_id && + !object.reblog? && + %w(public unlisted).include?(object.visibility) + end + class ApplicationSerializer < ActiveModel::Serializer attributes :name, :website end diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb new file mode 100644 index 000000000..f557df6af --- /dev/null +++ b/app/validators/status_pin_validator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class StatusPinValidator < ActiveModel::Validator + def validate(pin) + pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog? + pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id + pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility) + end +end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index ec44f4c74..e0f9f869a 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -30,6 +30,9 @@ = render 'nothing_here' - else .activity-stream.with-header + - if params[:page].to_i.zero? + = render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } + = render partial: 'stream_entries/status', collection: @statuses, as: :status - if @statuses.size == 20 diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 50a373743..e2e1fdd12 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -1,4 +1,5 @@ :ruby + pinned ||= false include_threads ||= false is_predecessor ||= false is_successor ||= false @@ -25,6 +26,12 @@ = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do %strong.emojify= display_name(status.account) = t('stream_entries.reblogged') + - elsif pinned + .pre-header + .pre-header__icon + = fa_icon('thumb-tack fw') + %span + = t('stream_entries.pinned') = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper diff --git a/config/locales/en.yml b/config/locales/en.yml index 97bb14186..96d08e6b2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -434,6 +434,10 @@ en: statuses: open_in_web: Open in web over_character_limit: character limit of %{max} exceeded + pin_errors: + ownership: Someone else's toot cannot be pinned + private: Non-public toot cannot be pinned + reblog: A boost cannot be pinned show_more: Show more visibilities: private: Followers-only @@ -444,6 +448,7 @@ en: unlisted_long: Everyone can see, but not listed on public timelines stream_entries: click_to_show: Click to show + pinned: Pinned toot reblogged: boosted sensitive_content: Sensitive content terms: diff --git a/config/routes.rb b/config/routes.rb index 94a4ac88e..7588805c0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -162,6 +162,9 @@ Rails.application.routes.draw do resource :mute, only: :create post :unmute, to: 'mutes#destroy' + + resource :pin, only: :create + post :unpin, to: 'pins#destroy' end member do @@ -175,7 +178,8 @@ Rails.application.routes.draw do resource :public, only: :show, controller: :public resources :tag, only: :show end - resources :streaming, only: [:index] + + resources :streaming, only: [:index] get '/search', to: 'search#index', as: :search @@ -210,6 +214,7 @@ Rails.application.routes.draw do resource :search, only: :show, controller: :search resources :relationships, only: :index end + resources :accounts, only: [:show] do resources :statuses, only: :index, controller: 'accounts/statuses' resources :followers, only: :index, controller: 'accounts/follower_accounts' @@ -245,7 +250,7 @@ Rails.application.routes.draw do root 'home#index' match '*unmatched_route', - via: :all, - to: 'application#raise_not_found', - format: false + via: :all, + to: 'application#raise_not_found', + format: false end diff --git a/db/migrate/20170823162448_create_status_pins.rb b/db/migrate/20170823162448_create_status_pins.rb new file mode 100644 index 000000000..9a6d4a7b9 --- /dev/null +++ b/db/migrate/20170823162448_create_status_pins.rb @@ -0,0 +1,10 @@ +class CreateStatusPins < ActiveRecord::Migration[5.1] + def change + create_table :status_pins do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false + t.belongs_to :status, foreign_key: { on_delete: :cascade }, null: false + end + + add_index :status_pins, [:account_id, :status_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 98b07e282..d0e72be0f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170720000000) do +ActiveRecord::Schema.define(version: 20170823162448) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -282,6 +282,14 @@ ActiveRecord::Schema.define(version: 20170720000000) do t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true end + create_table "status_pins", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "status_id", null: false + t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true + t.index ["account_id"], name: "index_status_pins_on_account_id" + t.index ["status_id"], name: "index_status_pins_on_status_id" + end + create_table "statuses", force: :cascade do |t| t.string "uri" t.integer "account_id", null: false @@ -430,6 +438,8 @@ ActiveRecord::Schema.define(version: 20170720000000) do add_foreign_key "reports", "accounts", on_delete: :cascade add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade add_foreign_key "session_activations", "users", on_delete: :cascade + add_foreign_key "status_pins", "accounts", on_delete: :cascade + add_foreign_key "status_pins", "statuses", on_delete: :cascade add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", on_delete: :nullify add_foreign_key "statuses", "accounts", on_delete: :cascade add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb index 8b4fd6a5b..c49a77ac3 100644 --- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb @@ -18,21 +18,37 @@ describe Api::V1::Accounts::StatusesController do expect(response).to have_http_status(:success) expect(response.headers['Link'].links.size).to eq(2) end - end - describe 'GET #index with only media' do - it 'returns http success' do - get :index, params: { account_id: user.account.id, only_media: true } + context 'with only media' do + it 'returns http success' do + get :index, params: { account_id: user.account.id, only_media: true } - expect(response).to have_http_status(:success) + expect(response).to have_http_status(:success) + end end - end - describe 'GET #index with exclude replies' do - it 'returns http success' do - get :index, params: { account_id: user.account.id, exclude_replies: true } + context 'with exclude replies' do + before do + Fabricate(:status, account: user.account, thread: Fabricate(:status)) + end - expect(response).to have_http_status(:success) + it 'returns http success' do + get :index, params: { account_id: user.account.id, exclude_replies: true } + + expect(response).to have_http_status(:success) + end + end + + context 'with only pinned' do + before do + Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account)) + end + + it 'returns http success' do + get :index, params: { account_id: user.account.id, pinned: true } + + expect(response).to have_http_status(:success) + end end end end diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb new file mode 100644 index 000000000..2e170da24 --- /dev/null +++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Statuses::PinsController do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } + + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + let(:status) { Fabricate(:status, account: user.account) } + + before do + post :create, params: { status_id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates the pinned attribute' do + expect(user.account.pinned?(status)).to be true + end + + it 'return json with updated attributes' do + hash_body = body_as_json + + expect(hash_body[:id]).to eq status.id + expect(hash_body[:pinned]).to be true + end + end + + describe 'POST #destroy' do + let(:status) { Fabricate(:status, account: user.account) } + + before do + Fabricate(:status_pin, status: status, account: user.account) + post :destroy, params: { status_id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates the pinned attribute' do + expect(user.account.pinned?(status)).to be false + end + end + end +end diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb new file mode 100644 index 000000000..6a9006c9f --- /dev/null +++ b/spec/fabricators/status_pin_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:status_pin) do + account + status +end diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb new file mode 100644 index 000000000..6f54f80f9 --- /dev/null +++ b/spec/models/status_pin_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe StatusPin, type: :model do + describe 'validations' do + it 'allows pins of own statuses' do + account = Fabricate(:account) + status = Fabricate(:status, account: account) + + expect(StatusPin.new(account: account, status: status).save).to be true + end + + it 'does not allow pins of statuses by someone else' do + account = Fabricate(:account) + status = Fabricate(:status) + + expect(StatusPin.new(account: account, status: status).save).to be false + end + + it 'does not allow pins of reblogs' do + account = Fabricate(:account) + status = Fabricate(:status, account: account) + reblog = Fabricate(:status, reblog: status) + + expect(StatusPin.new(account: account, status: reblog).save).to be false + end + + it 'does not allow pins of private statuses' do + account = Fabricate(:account) + status = Fabricate(:status, account: account, visibility: :private) + + expect(StatusPin.new(account: account, status: status).save).to be false + end + + it 'does not allow pins of direct statuses' do + account = Fabricate(:account) + status = Fabricate(:status, account: account, visibility: :direct) + + expect(StatusPin.new(account: account, status: status).save).to be false + end + end +end -- cgit From c2af1381130caae0acc02db853580a2bdab61078 Mon Sep 17 00:00:00 2001 From: nullkal Date: Sat, 26 Aug 2017 01:50:52 +0900 Subject: Allow multiple pinned statuses to be shown and make them be ordered b… (#4690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow multiple pinned statuses to be shown and make them be ordered by pinned date * Set timestamps NOT NULL * Make single-line pinned_statuses * Spec for pinned_statuses * Remove redundant empty line --- app/controllers/accounts_controller.rb | 2 +- app/models/account.rb | 2 +- app/models/status_pin.rb | 2 ++ .../20170824103029_add_timestamps_to_status_pins.rb | 5 +++++ db/schema.rb | 4 +++- spec/controllers/accounts_controller_spec.rb | 15 +++++++++++++++ 6 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20170824103029_add_timestamps_to_status_pins.rb (limited to 'spec/controllers') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index f4ca239ba..8dad12f11 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -14,7 +14,7 @@ class AccountsController < ApplicationController return end - @pinned_statuses = cache_collection(@account.pinned_statuses.limit(1), Status) unless media_requested? + @pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested? @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = cache_collection(@statuses, Status) @next_url = next_url unless @statuses.empty? diff --git a/app/models/account.rb b/app/models/account.rb index b83aa1159..529334559 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -79,7 +79,7 @@ class Account < ApplicationRecord # Pinned statuses has_many :status_pins, inverse_of: :account, dependent: :destroy - has_many :pinned_statuses, through: :status_pins, class_name: 'Status', source: :status + has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status # Media has_many :media_attachments, dependent: :destroy diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb index c9a669344..a72c19750 100644 --- a/app/models/status_pin.rb +++ b/app/models/status_pin.rb @@ -6,6 +6,8 @@ # id :integer not null, primary key # account_id :integer not null # status_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null # class StatusPin < ApplicationRecord diff --git a/db/migrate/20170824103029_add_timestamps_to_status_pins.rb b/db/migrate/20170824103029_add_timestamps_to_status_pins.rb new file mode 100644 index 000000000..09f0fbeaf --- /dev/null +++ b/db/migrate/20170824103029_add_timestamps_to_status_pins.rb @@ -0,0 +1,5 @@ +class AddTimestampsToStatusPins < ActiveRecord::Migration[5.1] + def change + add_timestamps :status_pins, null: false, default: -> { 'CURRENT_TIMESTAMP' } + end +end diff --git a/db/schema.rb b/db/schema.rb index d0e72be0f..7754894bc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170823162448) do +ActiveRecord::Schema.define(version: 20170824103029) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -285,6 +285,8 @@ ActiveRecord::Schema.define(version: 20170823162448) do create_table "status_pins", force: :cascade do |t| t.bigint "account_id", null: false t.bigint "status_id", null: false + t.datetime "created_at", default: -> { "now()" }, null: false + t.datetime "updated_at", default: -> { "now()" }, null: false t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true t.index ["account_id"], name: "index_status_pins_on_account_id" t.index ["status_id"], name: "index_status_pins_on_status_id" diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 2c0df0ef3..4e37b1b5f 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -10,6 +10,13 @@ RSpec.describe AccountsController, type: :controller do let!(:status2) { Status.create!(account: alice, text: 'Boop', thread: status1) } let!(:status3) { Status.create!(account: alice, text: 'Picture!') } let!(:status4) { Status.create!(account: alice, text: 'Mentioning @alice') } + let!(:status5) { Status.create!(account: alice, text: 'Kitsune') } + let!(:status6) { Status.create!(account: alice, text: 'Neko') } + let!(:status7) { Status.create!(account: alice, text: 'Tanuki') } + + let!(:status_pin1) { StatusPin.create!(account: alice, status: status5, created_at: 5.days.ago) } + let!(:status_pin2) { StatusPin.create!(account: alice, status: status6, created_at: 2.years.ago) } + let!(:status_pin3) { StatusPin.create!(account: alice, status: status7, created_at: 10.minutes.ago) } before do status3.media_attachments.create!(account: alice, file: fixture_file_upload('files/attachment.jpg', 'image/jpeg')) @@ -70,6 +77,14 @@ RSpec.describe AccountsController, type: :controller do expect(statuses[1]).to eq status2 end + it 'assigns @pinned_statuses' do + pinned_statuses = assigns(:pinned_statuses).to_a + expect(pinned_statuses.size).to eq 3 + expect(pinned_statuses[0]).to eq status7 + expect(pinned_statuses[1]).to eq status5 + expect(pinned_statuses[2]).to eq status6 + end + it 'returns http success' do expect(response).to have_http_status(:success) end -- cgit From ce9a5f358ed1ea5039c1de0bc896dc60622852f7 Mon Sep 17 00:00:00 2001 From: abcang Date: Tue, 29 Aug 2017 02:12:09 +0900 Subject: rescue HTTP::ConnectionError in RemoteFollowController#create (#4726) --- app/models/remote_follow.rb | 2 +- spec/controllers/remote_follow_controller_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'spec/controllers') diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb index 8366d43c5..c3f867743 100644 --- a/app/models/remote_follow.rb +++ b/app/models/remote_follow.rb @@ -42,7 +42,7 @@ class RemoteFollow def acct_resource @_acct_resource ||= Goldfinger.finger("acct:#{acct}") - rescue Goldfinger::Error + rescue Goldfinger::Error, HTTP::ConnectionError nil end diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb index 915c86f8e..86b1eb8d0 100644 --- a/spec/controllers/remote_follow_controller_spec.rb +++ b/spec/controllers/remote_follow_controller_spec.rb @@ -87,6 +87,14 @@ describe RemoteFollowController do expect(response).to render_template(:new) expect(response.body).to include(I18n.t('remote_follow.missing_resource')) end + + it 'renders new when occur HTTP::ConnectionError' do + allow(Goldfinger).to receive(:finger).with('acct:user@unknown').and_raise(HTTP::ConnectionError) + post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } } + + expect(response).to render_template(:new) + expect(response.body).to include(I18n.t('remote_follow.missing_resource')) + end end end -- cgit From e95bdec7c5da63930fc2e08e67e4358fec19296d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 30 Aug 2017 10:23:43 +0200 Subject: Update status embeds (#4742) - Use statuses controller for embeds instead of stream entries controller - Prefer /@:username/:id/embed URL for embeds - Use /@:username as author_url in OEmbed - Add follow link to embeds which opens web intent in new window - Use redis cache in development - Cache entire embed --- app/controllers/api/oembed_controller.rb | 8 ++-- app/controllers/statuses_controller.rb | 5 ++ app/controllers/stream_entries_controller.rb | 5 +- app/helpers/stream_entries_helper.rb | 2 +- app/javascript/packs/public.js | 7 +++ app/javascript/styles/stream_entries.scss | 30 ++++++++++++ app/lib/status_finder.rb | 34 +++++++++++++ app/lib/stream_entry_finder.rb | 34 ------------- app/serializers/oembed_serializer.rb | 4 +- .../stream_entries/_detailed_status.html.haml | 5 ++ app/views/stream_entries/embed.html.haml | 5 +- config/brakeman.ignore | 50 ++++++++++---------- config/environments/development.rb | 5 +- config/routes.rb | 2 + spec/controllers/stream_entries_controller_spec.rb | 6 +-- spec/lib/status_finder_spec.rb | 55 ++++++++++++++++++++++ spec/lib/stream_entry_finder_spec.rb | 55 ---------------------- 17 files changed, 179 insertions(+), 133 deletions(-) create mode 100644 app/lib/status_finder.rb delete mode 100644 app/lib/stream_entry_finder.rb create mode 100644 spec/lib/status_finder_spec.rb delete mode 100644 spec/lib/stream_entry_finder_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index f8c87dd16..37a163cd3 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -4,14 +4,14 @@ class Api::OEmbedController < Api::BaseController respond_to :json def show - @stream_entry = find_stream_entry.stream_entry - render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default + @status = status_finder.status + render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default end private - def find_stream_entry - StreamEntryFinder.new(params[:url]) + def status_finder + StatusFinder.new(params[:url]) end def maxwidth_or_default diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index a9768d092..65206ea96 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -30,6 +30,11 @@ class StatusesController < ApplicationController render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end + def embed + response.headers['X-Frame-Options'] = 'ALLOWALL' + render 'stream_entries/embed', layout: 'embedded' + end + private def set_account diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index ccb15495e..cc579dbc8 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -25,10 +25,7 @@ class StreamEntriesController < ApplicationController end def embed - response.headers['X-Frame-Options'] = 'ALLOWALL' - return gone if @stream_entry.activity.nil? - - render layout: 'embedded' + redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301 end private diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 4ef7cffb0..445114985 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module StreamEntriesHelper - EMBEDDED_CONTROLLER = 'stream_entries' + EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' def display_name(account) diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index d8a0f4eee..ce12041e6 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -38,6 +38,13 @@ function main() { content.title = dateTimeFormat.format(datetime); content.textContent = relativeFormat.format(datetime); }); + + [].forEach.call(document.querySelectorAll('.logo-button'), (content) => { + content.addEventListener('click', (e) => { + e.preventDefault(); + window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); + }); + }); }); delegate(document, '.video-player video', 'click', ({ target }) => { diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index 1192e2a80..7048ab110 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -421,3 +421,33 @@ } } } + +.button.button-secondary.logo-button { + position: absolute; + right: 14px; + top: 14px; + font-size: 14px; + + svg { + width: 20px; + height: auto; + vertical-align: middle; + margin-right: 5px; + + path:first-child { + fill: $ui-primary-color; + } + + path:last-child { + fill: $simple-background-color; + } + } + + &:active, + &:focus, + &:hover { + svg path:first-child { + fill: lighten($ui-primary-color, 4%); + } + } +} diff --git a/app/lib/status_finder.rb b/app/lib/status_finder.rb new file mode 100644 index 000000000..bd910f12b --- /dev/null +++ b/app/lib/status_finder.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class StatusFinder + attr_reader :url + + def initialize(url) + @url = url + end + + def status + verify_action! + + case recognized_params[:controller] + when 'stream_entries' + StreamEntry.find(recognized_params[:id]).status + when 'statuses' + Status.find(recognized_params[:id]) + else + raise ActiveRecord::RecordNotFound + end + end + + private + + def recognized_params + Rails.application.routes.recognize_path(url) + end + + def verify_action! + unless recognized_params[:action] == 'show' + raise ActiveRecord::RecordNotFound + end + end +end diff --git a/app/lib/stream_entry_finder.rb b/app/lib/stream_entry_finder.rb deleted file mode 100644 index 0ea33229c..000000000 --- a/app/lib/stream_entry_finder.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -class StreamEntryFinder - attr_reader :url - - def initialize(url) - @url = url - end - - def stream_entry - verify_action! - - case recognized_params[:controller] - when 'stream_entries' - StreamEntry.find(recognized_params[:id]) - when 'statuses' - Status.find(recognized_params[:id]).stream_entry - else - raise ActiveRecord::RecordNotFound - end - end - - private - - def recognized_params - Rails.application.routes.recognize_path(url) - end - - def verify_action! - unless recognized_params[:action] == 'show' - raise ActiveRecord::RecordNotFound - end - end -end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb index 78376d253..0c2ced859 100644 --- a/app/serializers/oembed_serializer.rb +++ b/app/serializers/oembed_serializer.rb @@ -21,7 +21,7 @@ class OEmbedSerializer < ActiveModel::Serializer end def author_url - account_url(object.account) + short_account_url(object.account) end def provider_name @@ -38,7 +38,7 @@ class OEmbedSerializer < ActiveModel::Serializer def html tag :iframe, - src: embed_account_stream_entry_url(object.account, object), + src: embed_short_account_status_url(object.account, object), style: 'width: 100%; overflow: hidden', frameborder: '0', scrolling: 'no', diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 193cc6470..107202b75 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -1,4 +1,9 @@ .detailed-status.light + - if embedded_view? + = link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do + = render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg') + = t('accounts.follow') + = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do %div .avatar diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml index 5df82528b..b703c15d2 100644 --- a/app/views/stream_entries/embed.html.haml +++ b/app/views/stream_entries/embed.html.haml @@ -1,2 +1,3 @@ -.activity-stream.activity-stream-headless - = render @type, @type.to_sym => @stream_entry.activity, centered: true +- cache @stream_entry.activity do + .activity-stream.activity-stream-headless + = render "stream_entries/#{@type}", @type.to_sym => @stream_entry.activity, centered: true diff --git a/config/brakeman.ignore b/config/brakeman.ignore index f9bc77069..dbb59dd07 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -1,5 +1,24 @@ { "ignored_warnings": [ + { + "warning_type": "Dynamic Render Path", + "warning_code": 15, + "fingerprint": "44d3f14e05d8fbb5b23e13ac02f15aa38b2a2f0f03b9ba76bab7f98e155a4a4e", + "check_name": "Render", + "message": "Render path contains parameter value", + "file": "app/views/stream_entries/embed.html.haml", + "line": 3, + "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", + "code": "render(action => \"stream_entries/#{Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase}\", { Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity, :centered => true })", + "render_path": [{"type":"controller","class":"StatusesController","method":"embed","line":35,"file":"app/controllers/statuses_controller.rb"}], + "location": { + "type": "template", + "template": "stream_entries/embed" + }, + "user_input": "params[:id]", + "confidence": "Weak", + "note": "" + }, { "warning_type": "Dynamic Render Path", "warning_code": 15, @@ -7,10 +26,10 @@ "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/admin/accounts/index.html.haml", - "line": 32, + "line": 63, "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "code": "render(action => filtered_accounts.page(params[:page]), {})", - "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":7,"file":"app/controllers/admin/accounts_controller.rb"}], + "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":10,"file":"app/controllers/admin/accounts_controller.rb"}], "location": { "type": "template", "template": "admin/accounts/index" @@ -39,25 +58,6 @@ "confidence": "High", "note": "" }, - { - "warning_type": "Dynamic Render Path", - "warning_code": 15, - "fingerprint": "c417f9d44ab05dd9cf3d5ec9df2324a5036774c151181787b32c4c940623191b", - "check_name": "Render", - "message": "Render path contains parameter value", - "file": "app/views/stream_entries/embed.html.haml", - "line": 2, - "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", - "code": "render(action => Account.find_local!(params[:account_username]).stream_entries.where(:activity_type => \"Status\").find(params[:id]).activity_type.downcase, { Account.find_local!(params[:account_username]).stream_entries.where(:activity_type => \"Status\").find(params[:id]).activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).stream_entries.where(:activity_type => \"Status\").find(params[:id]).activity, :centered => true })", - "render_path": [{"type":"controller","class":"StreamEntriesController","method":"embed","line":32,"file":"app/controllers/stream_entries_controller.rb"}], - "location": { - "type": "template", - "template": "stream_entries/embed" - }, - "user_input": "params[:id]", - "confidence": "Weak", - "note": "" - }, { "warning_type": "Dynamic Render Path", "warning_code": 15, @@ -84,10 +84,10 @@ "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/stream_entries/show.html.haml", - "line": 19, + "line": 23, "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "code": "render(partial => \"stream_entries/#{Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase}\", { :locals => ({ Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity, :include_threads => true }) })", - "render_path": [{"type":"controller","class":"StatusesController","method":"show","line":15,"file":"app/controllers/statuses_controller.rb"}], + "render_path": [{"type":"controller","class":"StatusesController","method":"show","line":20,"file":"app/controllers/statuses_controller.rb"}], "location": { "type": "template", "template": "stream_entries/show" @@ -97,6 +97,6 @@ "note": "" } ], - "updated": "2017-05-07 08:26:06 +0900", - "brakeman_version": "3.6.1" + "updated": "2017-08-30 05:14:04 +0200", + "brakeman_version": "3.7.2" } diff --git a/config/environments/development.rb b/config/environments/development.rb index 4c60965c8..59bc2c3e2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,9 +16,10 @@ Rails.application.configure do if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true - config.cache_store = :memory_store + config.cache_store = :redis_store, ENV['REDIS_URL'], REDIS_CACHE_PARAMS + config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}", } else config.action_controller.perform_caching = false diff --git a/config/routes.rb b/config/routes.rb index 7588805c0..f8f145e1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,6 +44,7 @@ Rails.application.routes.draw do resources :statuses, only: [:show] do member do get :activity + get :embed end end @@ -59,6 +60,7 @@ Rails.application.routes.draw do get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies get '/@:username/media', to: 'accounts#show', as: :short_account_media get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status + get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status namespace :settings do resource :profile, only: [:show, :update] diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb index 808cf667c..f81e2be7b 100644 --- a/spec/controllers/stream_entries_controller_spec.rb +++ b/spec/controllers/stream_entries_controller_spec.rb @@ -88,14 +88,12 @@ RSpec.describe StreamEntriesController, type: :controller do describe 'GET #embed' do include_examples 'before_action', :embed - it 'returns embedded view of status' do + it 'redirects to new embed page' do status = Fabricate(:status) get :embed, params: { account_username: status.account.username, id: status.stream_entry.id } - expect(response).to have_http_status(:success) - expect(response.headers['X-Frame-Options']).to eq 'ALLOWALL' - expect(response).to render_template(layout: 'embedded') + expect(response).to redirect_to(embed_short_account_status_url(status.account, status)) end end end diff --git a/spec/lib/status_finder_spec.rb b/spec/lib/status_finder_spec.rb new file mode 100644 index 000000000..5c2f2dbe8 --- /dev/null +++ b/spec/lib/status_finder_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe StatusFinder do + include RoutingHelper + + describe '#status' do + context 'with a status url' do + let(:status) { Fabricate(:status) } + let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) } + subject { described_class.new(url) } + + it 'finds the stream entry' do + expect(subject.status).to eq(status) + end + + it 'raises an error if action is not :show' do + recognized = Rails.application.routes.recognize_path(url) + expect(recognized).to receive(:[]).with(:action).and_return(:create) + expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized) + + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with a stream entry url' do + let(:stream_entry) { Fabricate(:stream_entry) } + let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } + subject { described_class.new(url) } + + it 'finds the stream entry' do + expect(subject.status).to eq(stream_entry.status) + end + end + + context 'with a plausible url' do + let(:url) { 'https://example.com/users/test/updates/123/embed' } + subject { described_class.new(url) } + + it 'raises an error' do + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with an unrecognized url' do + let(:url) { 'https://example.com/about' } + subject { described_class.new(url) } + + it 'raises an error' do + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/lib/stream_entry_finder_spec.rb b/spec/lib/stream_entry_finder_spec.rb deleted file mode 100644 index 64e03c36a..000000000 --- a/spec/lib/stream_entry_finder_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StreamEntryFinder do - include RoutingHelper - - describe '#stream_entry' do - context 'with a status url' do - let(:status) { Fabricate(:status) } - let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) } - subject { described_class.new(url) } - - it 'finds the stream entry' do - expect(subject.stream_entry).to eq(status.stream_entry) - end - - it 'raises an error if action is not :show' do - recognized = Rails.application.routes.recognize_path(url) - expect(recognized).to receive(:[]).with(:action).and_return(:create) - expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized) - - expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with a stream entry url' do - let(:stream_entry) { Fabricate(:stream_entry) } - let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } - subject { described_class.new(url) } - - it 'finds the stream entry' do - expect(subject.stream_entry).to eq(stream_entry) - end - end - - context 'with a plausible url' do - let(:url) { 'https://example.com/users/test/updates/123/embed' } - subject { described_class.new(url) } - - it 'raises an error' do - expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with an unrecognized url' do - let(:url) { 'https://example.com/about' } - subject { described_class.new(url) } - - it 'raises an error' do - expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end -- cgit From 6a4e2db661f47a318bbf93a07ba9f16f7bac3ee0 Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 3 Sep 2017 00:42:47 +0900 Subject: Raise an error for remote url in StatusFinder (#4776) * Raise an error for remote url in StatusFinder Previous implementation had allowed remote url with status id which also exists on local. Then that bug leads /api/web/embed to return wrong embed url. * Fix oembed_controller_spec --- app/lib/status_finder.rb | 2 ++ spec/controllers/api/oembed_controller_spec.rb | 1 + spec/lib/status_finder_spec.rb | 10 ++++++++++ 3 files changed, 13 insertions(+) (limited to 'spec/controllers') diff --git a/app/lib/status_finder.rb b/app/lib/status_finder.rb index bd910f12b..4d1aed297 100644 --- a/app/lib/status_finder.rb +++ b/app/lib/status_finder.rb @@ -10,6 +10,8 @@ class StatusFinder def status verify_action! + raise ActiveRecord::RecordNotFound unless TagManager.instance.local_url?(url) + case recognized_params[:controller] when 'stream_entries' StreamEntry.find(recognized_params[:id]).status diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb index 43631a7e5..7af4a6a5b 100644 --- a/spec/controllers/api/oembed_controller_spec.rb +++ b/spec/controllers/api/oembed_controller_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Api::OEmbedController, type: :controller do describe 'GET #show' do before do + request.host = Rails.configuration.x.local_domain get :show, params: { url: account_stream_entry_url(alice, status.stream_entry) }, format: :json end diff --git a/spec/lib/status_finder_spec.rb b/spec/lib/status_finder_spec.rb index 5c2f2dbe8..3ef086736 100644 --- a/spec/lib/status_finder_spec.rb +++ b/spec/lib/status_finder_spec.rb @@ -34,6 +34,16 @@ describe StatusFinder do end end + context 'with a remote url even if id exists on local' do + let(:status) { Fabricate(:status) } + let(:url) { "https://example.com/users/test/statuses/#{status.id}" } + subject { described_class.new(url) } + + it 'raises an error' do + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + context 'with a plausible url' do let(:url) { 'https://example.com/users/test/updates/123/embed' } subject { described_class.new(url) } -- cgit From 2293466edd0972c2069628c55baec9b0cb861445 Mon Sep 17 00:00:00 2001 From: nullkal Date: Mon, 4 Sep 2017 19:53:18 +0900 Subject: Show pinned statuses only in the top of the profile page (#4803) * Show pinned statuses only in the top of the profile page * Refactor AccountsController#show_pinned_statuses? --- app/controllers/accounts_controller.rb | 6 +++++- spec/controllers/accounts_controller_spec.rb | 31 ++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 8dad12f11..afa0417fa 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -14,7 +14,7 @@ class AccountsController < ApplicationController return end - @pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested? + @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = cache_collection(@statuses, Status) @next_url = next_url unless @statuses.empty? @@ -33,6 +33,10 @@ class AccountsController < ApplicationController private + def show_pinned_statuses? + [replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none? + end + def filtered_statuses default_statuses.tap do |statuses| statuses.merge!(only_media_scope) if media_requested? diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 4e37b1b5f..92f888590 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -61,7 +61,29 @@ RSpec.describe AccountsController, type: :controller do end end - context 'html' do + context 'html without since_id nor max_id' do + before do + get :show, params: { username: alice.username } + end + + it 'assigns @account' do + expect(assigns(:account)).to eq alice + end + + it 'assigns @pinned_statuses' do + pinned_statuses = assigns(:pinned_statuses).to_a + expect(pinned_statuses.size).to eq 3 + expect(pinned_statuses[0]).to eq status7 + expect(pinned_statuses[1]).to eq status5 + expect(pinned_statuses[2]).to eq status6 + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + end + + context 'html with since_id and max_id' do before do get :show, params: { username: alice.username, max_id: status4.id, since_id: status1.id } end @@ -77,12 +99,9 @@ RSpec.describe AccountsController, type: :controller do expect(statuses[1]).to eq status2 end - it 'assigns @pinned_statuses' do + it 'assigns an empty array to @pinned_statuses' do pinned_statuses = assigns(:pinned_statuses).to_a - expect(pinned_statuses.size).to eq 3 - expect(pinned_statuses[0]).to eq status7 - expect(pinned_statuses[1]).to eq status5 - expect(pinned_statuses[2]).to eq status6 + expect(pinned_statuses.size).to eq 0 end it 'returns http success' do -- cgit From 9b994c4aee379f7998d2ddb562a08ccff92e0b0b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 5 Sep 2017 17:48:13 +0200 Subject: Fix #4794 - Fake instant follow in API response when account is believed unlocked (#4799) --- app/controllers/api/v1/accounts_controller.rb | 10 ++++++++++ app/presenters/account_relationships_presenter.rb | 14 +++++++------- spec/controllers/api/v1/accounts_controller_spec.rb | 7 +++++++ 3 files changed, 24 insertions(+), 7 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index f621aa245..656cacd8a 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -14,6 +14,16 @@ class Api::V1::AccountsController < Api::BaseController def follow FollowService.new.call(current_user.account, @account.acct) + + unless @account.locked? + relationships = AccountRelationshipsPresenter.new( + [@account.id], + current_user.account_id, + following_map: { @account.id => true }, + requested_map: { @account.id => false } + ) + end + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index 657807863..a30558bac 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -4,12 +4,12 @@ class AccountRelationshipsPresenter attr_reader :following, :followed_by, :blocking, :muting, :requested, :domain_blocking - def initialize(account_ids, current_account_id) - @following = Account.following_map(account_ids, current_account_id) - @followed_by = Account.followed_by_map(account_ids, current_account_id) - @blocking = Account.blocking_map(account_ids, current_account_id) - @muting = Account.muting_map(account_ids, current_account_id) - @requested = Account.requested_map(account_ids, current_account_id) - @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id) + def initialize(account_ids, current_account_id, options = {}) + @following = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {}) + @followed_by = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {}) + @blocking = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {}) + @muting = Account.muting_map(account_ids, current_account_id).merge(options[:muting_map] || {}) + @requested = Account.requested_map(account_ids, current_account_id).merge(options[:requested_map] || {}) + @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id).merge(options[:domain_blocking_map] || {}) end end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index c13509e7b..05df2f844 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -28,6 +28,13 @@ RSpec.describe Api::V1::AccountsController, type: :controller do expect(response).to have_http_status(:success) end + it 'returns JSON with following=true and requested=false' do + json = body_as_json + + expect(json[:following]).to be true + expect(json[:requested]).to be false + end + it 'creates a following relation between user and target user' do expect(user.account.following?(other_account)).to be true end -- cgit