From 7696f77245c2302787d239da50248385b3292a5e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 20 Jun 2019 02:52:34 +0200 Subject: Add moderation API (#9387) Fix #8580 Fix #7143 --- .../v1/admin/account_actions_controller_spec.rb | 57 ++++++++ .../api/v1/admin/accounts_controller_spec.rb | 147 +++++++++++++++++++++ .../api/v1/admin/reports_controller_spec.rb | 109 +++++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 spec/controllers/api/v1/admin/account_actions_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/accounts_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/reports_controller_spec.rb (limited to 'spec/controllers/api/v1') diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb new file mode 100644 index 000000000..a5a8f4bb0 --- /dev/null +++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'POST #create' do + before do + post :create, params: { account_id: account.id, type: 'disable' } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'performs action against account' do + expect(account.reload.user_disabled?).to be true + end + + it 'logs action' do + log_item = Admin::ActionLog.last + + expect(log_item).to_not be_nil + expect(log_item.action).to eq :disable + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end + end +end diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb new file mode 100644 index 000000000..f3f9946ba --- /dev/null +++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb @@ -0,0 +1,147 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #approve' do + before do + account.user.update(approved: false) + post :approve, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'approves user' do + expect(account.reload.user_approved?).to be true + end + end + + describe 'POST #reject' do + before do + account.user.update(approved: false) + post :reject, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'removes user' do + expect(User.where(id: account.user.id).count).to eq 0 + end + end + + describe 'POST #enable' do + before do + account.user.update(disabled: true) + post :enable, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'enables user' do + expect(account.reload.user_disabled?).to be false + end + end + + describe 'POST #unsuspend' do + before do + account.touch(:suspended_at) + post :unsuspend, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsuspends account' do + expect(account.reload.suspended?).to be false + end + end + + describe 'POST #unsilence' do + before do + account.touch(:silenced_at) + post :unsilence, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsilences account' do + expect(account.reload.silenced?).to be false + end + end +end diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb new file mode 100644 index 000000000..4ed3c5dc4 --- /dev/null +++ b/spec/controllers/api/v1/admin/reports_controller_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::ReportsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:report) { Fabricate(:report) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #resolve' do + before do + post :resolve, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #reopen' do + before do + post :reopen, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #assign_to_self' do + before do + post :assign_to_self, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #unassign' do + before do + post :unassign, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end +end -- cgit From 3fd6ab99e69e736e10c7022f07dfa34b60a4e54d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 5 Jul 2019 02:14:56 +0200 Subject: Remove deprecated REST API `GET /api/v1/timelines/direct` (#11212) --- .../api/v1/timelines/direct_controller.rb | 63 ---------------------- app/models/status.rb | 41 -------------- config/routes.rb | 1 - .../api/v1/timelines/direct_controller_spec.rb | 17 ------ spec/models/status_spec.rb | 50 ----------------- 5 files changed, 172 deletions(-) delete mode 100644 app/controllers/api/v1/timelines/direct_controller.rb delete mode 100644 spec/controllers/api/v1/timelines/direct_controller_spec.rb (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb deleted file mode 100644 index d8a76d153..000000000 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Timelines::DirectController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] - before_action :require_user!, only: [:show] - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } - - respond_to :json - - def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) - end - - private - - def load_statuses - cached_direct_statuses - end - - def cached_direct_statuses - cache_collection direct_statuses, Status - end - - def direct_statuses - direct_timeline_statuses - end - - def direct_timeline_statuses - # this query requires built in pagination. - Status.as_direct_timeline( - current_account, - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id], - true # returns array of cache_ids object - ) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_params(core_params) - params.permit(:local, :limit).merge(core_params) - end - - def next_path - api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id) - end - - def prev_path - api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id) - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id - end -end diff --git a/app/models/status.rb b/app/models/status.rb index fb9bbc9a9..2258e2d07 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -280,47 +280,6 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) - # direct timeline is mix of direct message from_me and to_me. - # 2 queries are executed with pagination. - # constant expression using arel_table is required for partial index - - # _from_me part does not require any timeline filters - query_from_me = where(account_id: account.id) - .where(Status.arel_table[:visibility].eq(3)) - .limit(limit) - .order('statuses.id DESC') - - # _to_me part requires mute and block filter. - # FIXME: may we check mutes.hide_notifications? - query_to_me = Status - .joins(:mentions) - .merge(Mention.where(account_id: account.id)) - .where(Status.arel_table[:visibility].eq(3)) - .limit(limit) - .order('mentions.status_id DESC') - .not_excluded_by_account(account) - - if max_id.present? - query_from_me = query_from_me.where('statuses.id < ?', max_id) - query_to_me = query_to_me.where('mentions.status_id < ?', max_id) - end - - if since_id.present? - query_from_me = query_from_me.where('statuses.id > ?', since_id) - query_to_me = query_to_me.where('mentions.status_id > ?', since_id) - end - - if cache_ids - # returns array of cache_ids object that have id and updated_at - (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) - else - # returns ActiveRecord.Relation - items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) - Status.where(id: items.map(&:id)) - end - end - def as_public_timeline(account = nil, local_only = false) query = timeline_scope(local_only).without_replies diff --git a/config/routes.rb b/config/routes.rb index 764db8db2..29db54a32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -299,7 +299,6 @@ Rails.application.routes.draw do end namespace :timelines do - resource :direct, only: :show, controller: :direct resource :home, only: :show, controller: :home resource :public, only: :show, controller: :public resources :tag, only: :show diff --git a/spec/controllers/api/v1/timelines/direct_controller_spec.rb b/spec/controllers/api/v1/timelines/direct_controller_spec.rb deleted file mode 100644 index a22c2cbea..000000000 --- a/spec/controllers/api/v1/timelines/direct_controller_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::Timelines::DirectController, type: :controller do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } - - describe 'GET #show' do - it 'returns 200' do - allow(controller).to receive(:doorkeeper_token) { token } - get :show - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index e8cf18af9..a8c567414 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -339,56 +339,6 @@ RSpec.describe Status, type: :model do end end - describe '.as_direct_timeline' do - let(:account) { Fabricate(:account) } - let(:followed) { Fabricate(:account) } - let(:not_followed) { Fabricate(:account) } - - before do - Fabricate(:follow, account: account, target_account: followed) - - @self_public_status = Fabricate(:status, account: account, visibility: :public) - @self_direct_status = Fabricate(:status, account: account, visibility: :direct) - @followed_public_status = Fabricate(:status, account: followed, visibility: :public) - @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct) - @not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct) - - @results = Status.as_direct_timeline(account) - end - - it 'does not include public statuses from self' do - expect(@results).to_not include(@self_public_status) - end - - it 'includes direct statuses from self' do - expect(@results).to include(@self_direct_status) - end - - it 'does not include public statuses from followed' do - expect(@results).to_not include(@followed_public_status) - end - - it 'does not include direct statuses not mentioning recipient from followed' do - expect(@results).to_not include(@followed_direct_status) - end - - it 'does not include direct statuses not mentioning recipient from non-followed' do - expect(@results).to_not include(@not_followed_direct_status) - end - - it 'includes direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: @followed_direct_status) - results2 = Status.as_direct_timeline(account) - expect(results2).to include(@followed_direct_status) - end - - it 'includes direct statuses mentioning recipient from non-followed' do - Fabricate(:mention, account: account, status: @not_followed_direct_status) - results2 = Status.as_direct_timeline(account) - expect(results2).to include(@not_followed_direct_status) - end - end - describe '.as_public_timeline' do it 'only includes statuses with public visibility' do public_status = Fabricate(:status, visibility: :public) -- cgit From 0c1b1069c96d3452d43d922a737c522d10e4747b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 5 Jul 2019 02:15:24 +0200 Subject: Remove deprecated REST API `GET /api/v1/statuses/:id/card` (#11213) --- app/controllers/api/v1/statuses_controller.rb | 14 ++------------ config/routes.rb | 1 - spec/controllers/api/v1/statuses_controller_spec.rb | 14 -------------- 3 files changed, 2 insertions(+), 27 deletions(-) (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index b0e134554..71a505c26 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy] - before_action :require_user!, except: [:show, :context, :card] - before_action :set_status, only: [:show, :context, :card] + before_action :require_user!, except: [:show, :context] + before_action :set_status, only: [:show, :context] respond_to :json @@ -33,16 +33,6 @@ class Api::V1::StatusesController < Api::BaseController render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) end - def card - @card = @status.preview_cards.first - - if @card.nil? - render_empty - else - render json: @card, serializer: REST::PreviewCardSerializer - end - end - def create @status = PostStatusService.new.call(current_user.account, text: status_params[:status], diff --git a/config/routes.rb b/config/routes.rb index 1f97dea4c..9ab5ba7f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -294,7 +294,6 @@ Rails.application.routes.draw do member do get :context - get :card end end diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index 8bc3b0c67..9ff5fcd3b 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -91,13 +91,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do expect(response).to have_http_status(404) end end - - describe 'GET #card' do - it 'returns http unautharized' do - get :card, params: { id: status.id } - expect(response).to have_http_status(404) - end - end end context 'with a public status' do @@ -120,13 +113,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do expect(response).to have_http_status(200) end end - - describe 'GET #card' do - it 'returns http success' do - get :card, params: { id: status.id } - expect(response).to have_http_status(200) - end - end end end end -- cgit From 23aeef52cc4540b4514e9f3b935b21f0530a3746 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 6 Jul 2019 23:26:16 +0200 Subject: Remove Salmon and PubSubHubbub (#11205) * Remove Salmon and PubSubHubbub endpoints * Add error when trying to follow OStatus accounts * Fix new accounts not being created in ResolveAccountService --- app/controllers/activitypub/inboxes_controller.rb | 1 - app/controllers/admin/accounts_controller.rb | 16 +- app/controllers/api/push_controller.rb | 73 ------ app/controllers/api/salmon_controller.rb | 37 --- app/controllers/api/subscriptions_controller.rb | 51 ----- app/controllers/api/v1/follows_controller.rb | 31 --- app/lib/ostatus/activity/base.rb | 71 ------ app/lib/ostatus/activity/creation.rb | 219 ------------------ app/lib/ostatus/activity/deletion.rb | 16 -- app/lib/ostatus/activity/general.rb | 20 -- app/lib/ostatus/activity/post.rb | 23 -- app/lib/ostatus/activity/remote.rb | 11 - app/lib/ostatus/activity/share.rb | 26 --- app/lib/ostatus/atom_serializer.rb | 2 - app/models/account.rb | 3 +- app/serializers/webfinger_serializer.rb | 1 - app/services/authorize_follow_service.rb | 12 +- app/services/batched_remove_status_service.rb | 34 +-- app/services/block_domain_service.rb | 1 - app/services/block_service.rb | 12 +- app/services/concerns/author_extractor.rb | 23 -- app/services/concerns/stream_entry_renderer.rb | 7 - app/services/favourite_service.rb | 6 - app/services/fetch_remote_account_service.rb | 28 --- app/services/fetch_remote_status_service.rb | 28 --- app/services/follow_service.rb | 24 +- app/services/post_status_service.rb | 1 - app/services/process_feed_service.rb | 31 --- app/services/process_interaction_service.rb | 151 ------------ app/services/process_mentions_service.rb | 7 - app/services/pubsubhubbub/subscribe_service.rb | 53 ----- app/services/pubsubhubbub/unsubscribe_service.rb | 31 --- app/services/reblog_service.rb | 4 - app/services/reject_follow_service.rb | 12 +- app/services/remove_status_service.rb | 13 -- app/services/resolve_account_service.rb | 131 +---------- app/services/send_interaction_service.rb | 39 ---- app/services/subscribe_service.rb | 58 ----- app/services/unblock_service.rb | 12 +- app/services/unfavourite_service.rb | 13 +- app/services/unfollow_service.rb | 16 +- app/services/unsubscribe_service.rb | 36 --- app/services/update_remote_profile_service.rb | 66 ------ app/services/verify_salmon_service.rb | 26 --- app/views/accounts/show.html.haml | 1 - .../admin/subscriptions/_subscription.html.haml | 18 -- app/views/admin/subscriptions/index.html.haml | 16 -- app/views/well_known/webfinger/show.xml.ruby | 5 - app/workers/after_remote_follow_request_worker.rb | 24 +- app/workers/after_remote_follow_worker.rb | 24 +- app/workers/notification_worker.rb | 4 +- app/workers/processing_worker.rb | 4 +- app/workers/pubsubhubbub/confirmation_worker.rb | 75 +----- app/workers/pubsubhubbub/delivery_worker.rb | 74 +----- app/workers/pubsubhubbub/distribution_worker.rb | 25 +- .../pubsubhubbub/raw_distribution_worker.rb | 15 +- app/workers/pubsubhubbub/subscribe_worker.rb | 27 +-- app/workers/pubsubhubbub/unsubscribe_worker.rb | 8 +- app/workers/remote_profile_update_worker.rb | 6 +- app/workers/salmon_worker.rb | 6 +- app/workers/scheduler/subscriptions_scheduler.rb | 10 +- config/locales/en.yml | 7 - config/navigation.rb | 1 - config/routes.rb | 14 -- config/sidekiq.yml | 3 - spec/controllers/admin/accounts_controller_spec.rb | 38 ---- .../admin/subscriptions_controller_spec.rb | 32 --- spec/controllers/api/push_controller_spec.rb | 59 ----- spec/controllers/api/salmon_controller_spec.rb | 65 ------ .../api/subscriptions_controller_spec.rb | 68 ------ spec/controllers/api/v1/follows_controller_spec.rb | 51 ----- spec/fixtures/requests/webfinger.txt | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 145 ------------ spec/services/authorize_follow_service_spec.rb | 7 - .../services/batched_remove_status_service_spec.rb | 13 -- spec/services/block_service_spec.rb | 7 - spec/services/favourite_service_spec.rb | 7 - spec/services/fetch_remote_account_service_spec.rb | 40 ---- spec/services/follow_service_spec.rb | 68 ------ spec/services/import_service_spec.rb | 30 ++- spec/services/post_status_service_spec.rb | 2 - spec/services/process_feed_service_spec.rb | 252 --------------------- spec/services/process_interaction_service_spec.rb | 151 ------------ spec/services/process_mentions_service_spec.rb | 4 - .../pubsubhubbub/subscribe_service_spec.rb | 71 ------ .../pubsubhubbub/unsubscribe_service_spec.rb | 46 ---- spec/services/reblog_service_spec.rb | 4 - spec/services/reject_follow_service_spec.rb | 7 - spec/services/remove_status_service_spec.rb | 13 -- spec/services/resolve_account_service_spec.rb | 88 +------ spec/services/send_interaction_service_spec.rb | 7 - spec/services/subscribe_service_spec.rb | 43 ---- spec/services/unblock_service_spec.rb | 7 - spec/services/unfollow_service_spec.rb | 7 - spec/services/unsubscribe_service_spec.rb | 37 --- .../services/update_remote_profile_service_spec.rb | 84 ------- .../after_remote_follow_request_worker_spec.rb | 59 ----- spec/workers/after_remote_follow_worker_spec.rb | 59 ----- .../pubsubhubbub/confirmation_worker_spec.rb | 88 ------- spec/workers/pubsubhubbub/delivery_worker_spec.rb | 68 ------ .../pubsubhubbub/distribution_worker_spec.rb | 46 ---- .../scheduler/subscriptions_scheduler_spec.rb | 19 -- 102 files changed, 69 insertions(+), 3568 deletions(-) delete mode 100644 app/controllers/api/push_controller.rb delete mode 100644 app/controllers/api/salmon_controller.rb delete mode 100644 app/controllers/api/subscriptions_controller.rb delete mode 100644 app/controllers/api/v1/follows_controller.rb delete mode 100644 app/lib/ostatus/activity/base.rb delete mode 100644 app/lib/ostatus/activity/creation.rb delete mode 100644 app/lib/ostatus/activity/deletion.rb delete mode 100644 app/lib/ostatus/activity/general.rb delete mode 100644 app/lib/ostatus/activity/post.rb delete mode 100644 app/lib/ostatus/activity/remote.rb delete mode 100644 app/lib/ostatus/activity/share.rb delete mode 100644 app/services/concerns/author_extractor.rb delete mode 100644 app/services/concerns/stream_entry_renderer.rb delete mode 100644 app/services/process_feed_service.rb delete mode 100644 app/services/process_interaction_service.rb delete mode 100644 app/services/pubsubhubbub/subscribe_service.rb delete mode 100644 app/services/pubsubhubbub/unsubscribe_service.rb delete mode 100644 app/services/send_interaction_service.rb delete mode 100644 app/services/subscribe_service.rb delete mode 100644 app/services/unsubscribe_service.rb delete mode 100644 app/services/update_remote_profile_service.rb delete mode 100644 app/services/verify_salmon_service.rb delete mode 100644 app/views/admin/subscriptions/_subscription.html.haml delete mode 100644 app/views/admin/subscriptions/index.html.haml delete mode 100644 spec/controllers/admin/subscriptions_controller_spec.rb delete mode 100644 spec/controllers/api/push_controller_spec.rb delete mode 100644 spec/controllers/api/salmon_controller_spec.rb delete mode 100644 spec/controllers/api/subscriptions_controller_spec.rb delete mode 100644 spec/controllers/api/v1/follows_controller_spec.rb delete mode 100644 spec/services/process_feed_service_spec.rb delete mode 100644 spec/services/process_interaction_service_spec.rb delete mode 100644 spec/services/pubsubhubbub/subscribe_service_spec.rb delete mode 100644 spec/services/pubsubhubbub/unsubscribe_service_spec.rb delete mode 100644 spec/services/send_interaction_service_spec.rb delete mode 100644 spec/services/subscribe_service_spec.rb delete mode 100644 spec/services/unsubscribe_service_spec.rb delete mode 100644 spec/services/update_remote_profile_service_spec.rb delete mode 100644 spec/workers/after_remote_follow_request_worker_spec.rb delete mode 100644 spec/workers/after_remote_follow_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/confirmation_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/delivery_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/distribution_worker_spec.rb delete mode 100644 spec/workers/scheduler/subscriptions_scheduler_spec.rb (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index a0b7532c2..e2cd8eaed 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -44,7 +44,6 @@ class ActivityPub::InboxesController < Api::BaseController ResolveAccountWorker.perform_async(signed_request_account.acct) end - Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed? DeliveryFailureTracker.track_inverse_success!(signed_request_account) end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 0c7760d77..2fa1dfe5f 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,8 +2,8 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] - before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] + before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] + before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index @@ -19,18 +19,6 @@ module Admin @warnings = @account.targeted_account_warnings.latest.custom end - def subscribe - authorize @account, :subscribe? - Pubsubhubbub::SubscribeWorker.perform_async(@account.id) - redirect_to admin_account_path(@account.id) - end - - def unsubscribe - authorize @account, :unsubscribe? - Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) - redirect_to admin_account_path(@account.id) - end - def memorialize authorize @account, :memorialize? @account.memorialize! diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb deleted file mode 100644 index e04d19125..000000000 --- a/app/controllers/api/push_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -class Api::PushController < Api::BaseController - include SignatureVerification - - def update - response, status = process_push_request - render plain: response, status: status - end - - private - - def process_push_request - case hub_mode - when 'subscribe' - Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain) - when 'unsubscribe' - Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback) - else - ["Unknown mode: #{hub_mode}", 422] - end - end - - def hub_mode - params['hub.mode'] - end - - def hub_topic - params['hub.topic'] - end - - def hub_callback - params['hub.callback'] - end - - def hub_lease_seconds - params['hub.lease_seconds'] - end - - def hub_secret - params['hub.secret'] - end - - def account_from_topic - if hub_topic.present? && local_domain? && account_feed_path? - Account.find_local(hub_topic_params[:username]) - end - end - - def hub_topic_params - @_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path) - end - - def hub_topic_uri - @_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize - end - - def local_domain? - TagManager.instance.web_domain?(hub_topic_domain) - end - - def verified_domain - return signed_request_account.domain if signed_request_account - end - - def hub_topic_domain - hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '') - end - - def account_feed_path? - hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom' - end -end diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb deleted file mode 100644 index ac5f3268d..000000000 --- a/app/controllers/api/salmon_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Api::SalmonController < Api::BaseController - include SignatureVerification - - before_action :set_account - respond_to :txt - - def update - if verify_payload? - process_salmon - head 202 - elsif payload.present? - render plain: signature_verification_failure_reason, status: 401 - else - head 400 - end - end - - private - - def set_account - @account = Account.find(params[:id]) - end - - def payload - @_payload ||= request.body.read - end - - def verify_payload? - payload.present? && VerifySalmonService.new.call(payload) - end - - def process_salmon - SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8')) - end -end diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb deleted file mode 100644 index 89007f3d6..000000000 --- a/app/controllers/api/subscriptions_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class Api::SubscriptionsController < Api::BaseController - before_action :set_account - respond_to :txt - - def show - if subscription.valid?(params['hub.topic']) - @account.update(subscription_expires_at: future_expires) - render plain: encoded_challenge, status: 200 - else - head 404 - end - end - - def update - if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE']) - ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8')) - end - - head 200 - end - - private - - def subscription - @_subscription ||= @account.subscription( - api_subscription_url(@account.id) - ) - end - - def body - @_body ||= request.body.read - end - - def encoded_challenge - HTMLEntities.new.encode(params['hub.challenge']) - end - - def future_expires - Time.now.utc + lease_seconds_or_default - end - - def lease_seconds_or_default - (params['hub.lease_seconds'] || 1.day).to_i.seconds - end - - def set_account - @account = Account.find(params[:id]) - end -end diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb deleted file mode 100644 index 5420c0533..000000000 --- a/app/controllers/api/v1/follows_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::FollowsController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow, :'write:follows' } - before_action :require_user! - - respond_to :json - - def create - raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? - - @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) - - if @account.nil? - username, domain = target_uri.split('@') - @account = Account.find_remote!(username, domain) - end - - render json: @account, serializer: REST::AccountSerializer - end - - private - - def target_uri - follow_params[:uri].strip.gsub(/\A@/, '') - end - - def follow_params - params.permit(:uri) - end -end diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb deleted file mode 100644 index db70f1998..000000000 --- a/app/lib/ostatus/activity/base.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Base - include Redisable - - def initialize(xml, account = nil, **options) - @xml = xml - @account = account - @options = options - end - - def status? - [:activity, :note, :comment].include?(type) - end - - def verb - raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::VERBS.key(raw) - rescue - :post - end - - def type - raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::TYPES.key(raw) - rescue - :activity - end - - def id - @xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content - end - - def url - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' } - link.nil? ? nil : link['href'] - end - - def activitypub_uri - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) } - link.nil? ? nil : link['href'] - end - - def activitypub_uri? - activitypub_uri.present? - end - - private - - def find_status(uri) - if OStatus::TagManager.instance.local_id?(uri) - local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status') - return Status.find_by(id: local_id) - elsif ActivityPub::TagManager.instance.local_uri?(uri) - local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri) - return Status.find_by(id: local_id) - end - - Status.find_by(uri: uri) - end - - def find_activitypub_status(uri, href) - tag_matches = /tag:([^,:]+)[^:]*:objectId=([\d]+)/.match(uri) - href_matches = %r{/users/([^/]+)}.match(href) - - unless tag_matches.nil? || href_matches.nil? - uri = "https://#{tag_matches[1]}/users/#{href_matches[1]}/statuses/#{tag_matches[2]}" - Status.find_by(uri: uri) - end - end -end diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb deleted file mode 100644 index 60de712db..000000000 --- a/app/lib/ostatus/activity/creation.rb +++ /dev/null @@ -1,219 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Creation < OStatus::Activity::Base - def perform - if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") - Rails.logger.debug "Delete for status #{id} was queued, ignoring" - return [nil, false] - end - - return [nil, false] if @account.suspended? || invalid_origin? - - RedisLock.acquire(lock_options) do |lock| - if lock.acquired? - # Return early if status already exists in db - @status = find_status(id) - return [@status, false] unless @status.nil? - @status = process_status - else - raise Mastodon::RaceConditionError - end - end - - [@status, true] - end - - def process_status - Rails.logger.debug "Creating remote status #{id}" - cached_reblog = reblog - status = nil - - # Skip if the reblogged status is not public - return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?) - - media_attachments = save_media.take(4) - - ApplicationRecord.transaction do - status = Status.create!( - uri: id, - url: url, - account: @account, - reblog: cached_reblog, - text: content, - spoiler_text: content_warning, - created_at: published, - override_timestamps: @options[:override_timestamps], - reply: thread?, - language: content_language, - visibility: visibility_scope, - conversation: find_or_create_conversation, - thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, - media_attachment_ids: media_attachments.map(&:id), - sensitive: sensitive? - ) - - save_mentions(status) - save_hashtags(status) - save_emojis(status) - end - - if thread? && status.thread.nil? && Request.valid_url?(thread.second) - Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}" - ThreadResolveWorker.perform_async(status.id, thread.second) - end - - Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" - - LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? - - # Only continue if the status is supposed to have arrived in real-time. - # Note that if @options[:override_timestamps] isn't set, the status - # may have a lower snowflake id than other existing statuses, potentially - # "hiding" it from paginated API calls - return status unless @options[:override_timestamps] || status.within_realtime_window? - - DistributionWorker.perform_async(status.id) - - status - end - - def content - @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content - end - - def content_language - @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en' - end - - def content_warning - @xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || '' - end - - def visibility_scope - @xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public - end - - def published - @xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content - end - - def thread? - !@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil? - end - - def thread - thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS) - [thr['ref'], thr['href']] - end - - private - - def sensitive? - # OStatus-specific convention (not standard) - @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' } - end - - def find_or_create_conversation - uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content - return if uri.nil? - - if OStatus::TagManager.instance.local_id?(uri) - local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') - return Conversation.find_by(id: local_id) - end - - Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) - end - - def save_mentions(parent) - processed_account_ids = [] - - @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type'] - - mentioned_account = account_from_href(link['href']) - - next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id) - - mentioned_account.mentions.where(status: parent).first_or_create(status: parent) - - # So we can skip duplicate mentions - processed_account_ids << mentioned_account.id - end - end - - def save_hashtags(parent) - tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) - ProcessHashtagsService.new.call(parent, tags) - end - - def save_media - do_not_download = DomainBlock.reject_media?(@account.domain) - media_attachments = [] - - @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next unless link['href'] - - media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href']) - parsed_url = Addressable::URI.parse(link['href']).normalize - - next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? - - media.save - media_attachments << media - - next if do_not_download - - begin - media.file_remote_url = link['href'] - media.save! - rescue ActiveRecord::RecordInvalid - next - end - end - - media_attachments - end - - def save_emojis(parent) - do_not_download = DomainBlock.reject_media?(parent.account.domain) - - return if do_not_download - - @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next unless link['href'] && link['name'] - - shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) - - next unless emoji.nil? - - emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) - emoji.image_remote_url = link['href'] - emoji.save - end - end - - def account_from_href(href) - url = Addressable::URI.parse(href).normalize - - if TagManager.instance.web_domain?(url.host) - Account.find_local(url.path.gsub('/users/', '')) - else - Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href) - end - end - - def invalid_origin? - return false unless id.start_with?('http') # Legacy IDs cannot be checked - - needle = Addressable::URI.parse(id).normalized_host - - !(needle.casecmp(@account.domain).zero? || - needle.casecmp(Addressable::URI.parse(@account.remote_url.presence || @account.uri).normalized_host).zero?) - end - - def lock_options - { redis: Redis.current, key: "create:#{id}" } - end -end diff --git a/app/lib/ostatus/activity/deletion.rb b/app/lib/ostatus/activity/deletion.rb deleted file mode 100644 index c98f5ee0a..000000000 --- a/app/lib/ostatus/activity/deletion.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Deletion < OStatus::Activity::Base - def perform - Rails.logger.debug "Deleting remote status #{id}" - - status = Status.find_by(uri: id, account: @account) - status ||= Status.find_by(uri: activitypub_uri, account: @account) if activitypub_uri? - - if status.nil? - redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) - else - RemoveStatusService.new.call(status) - end - end -end diff --git a/app/lib/ostatus/activity/general.rb b/app/lib/ostatus/activity/general.rb deleted file mode 100644 index 8a6aabc33..000000000 --- a/app/lib/ostatus/activity/general.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::General < OStatus::Activity::Base - def specialize - special_class&.new(@xml, @account, @options) - end - - private - - def special_class - case verb - when :post - OStatus::Activity::Post - when :share - OStatus::Activity::Share - when :delete - OStatus::Activity::Deletion - end - end -end diff --git a/app/lib/ostatus/activity/post.rb b/app/lib/ostatus/activity/post.rb deleted file mode 100644 index 755ed8656..000000000 --- a/app/lib/ostatus/activity/post.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Post < OStatus::Activity::Creation - def perform - status, just_created = super - - if just_created - status.mentions.includes(:account).each do |mention| - mentioned_account = mention.account - next unless mentioned_account.local? - NotifyService.new.call(mentioned_account, mention) - end - end - - status - end - - private - - def reblog - nil - end -end diff --git a/app/lib/ostatus/activity/remote.rb b/app/lib/ostatus/activity/remote.rb deleted file mode 100644 index 5b204b6d8..000000000 --- a/app/lib/ostatus/activity/remote.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Remote < OStatus::Activity::Base - def perform - if activitypub_uri? - find_status(activitypub_uri) || FetchRemoteStatusService.new.call(url) - else - find_status(id) || FetchRemoteStatusService.new.call(url) - end - end -end diff --git a/app/lib/ostatus/activity/share.rb b/app/lib/ostatus/activity/share.rb deleted file mode 100644 index 5ca601415..000000000 --- a/app/lib/ostatus/activity/share.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Share < OStatus::Activity::Creation - def perform - return if reblog.nil? - - status, just_created = super - NotifyService.new.call(reblog.account, status) if reblog.account.local? && just_created - status - end - - def object - @xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS) - end - - private - - def reblog - return @reblog if defined? @reblog - - original_status = OStatus::Activity::Remote.new(object).perform - return if original_status.nil? - - @reblog = original_status.reblog? ? original_status.reblog : original_status - end -end diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 9a05d96cf..f5c0e85ca 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -53,8 +53,6 @@ class OStatus::AtomSerializer append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 - append_element(feed, 'link', nil, rel: :hub, href: api_push_url) - append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id)) stream_entries.each do |stream_entry| feed << entry(stream_entry) diff --git a/app/models/account.rb b/app/models/account.rb index c588451fc..d6772eb98 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -164,8 +164,7 @@ class Account < ApplicationRecord end def refresh! - return if local? - ResolveAccountService.new.call(acct) + ResolveAccountService.new.call(acct) unless local? end def silenced? diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 8c0b07702..4220f697e 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'salmon', href: api_salmon_url(object.id) }, { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, ] diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 29b8700c7..49bef727e 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -11,25 +11,17 @@ class AuthorizeFollowService < BaseService follow_request.authorize! end - create_notification(follow_request) unless source_account.local? + create_notification(follow_request) if !source_account.local? && source_account.activitypub? 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 + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) end def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer)) end - - def build_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)) - end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index e328b1739..cb66debc8 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class BatchedRemoveStatusService < BaseService - include StreamEntryRenderer include Redisable # Delete given statuses and reblogs of them @@ -18,10 +17,7 @@ class BatchedRemoveStatusService < BaseService @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a } @tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) } - @stream_entry_batches = [] - @salmon_batches = [] - @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } - @activity_xml = {} + @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } # Ensure that rendered XML reflects destroyed state statuses.each do |status| @@ -39,28 +35,16 @@ class BatchedRemoveStatusService < BaseService unpush_from_home_timelines(account, account_statuses) unpush_from_list_timelines(account, account_statuses) - - batch_stream_entries(account, account_statuses) if account.local? end # Cannot be batched statuses.each do |status| unpush_from_public_timelines(status) - batch_salmon_slaps(status) if status.local? end - - Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch } - NotificationWorker.push_bulk(@salmon_batches) { |batch| batch } end private - def batch_stream_entries(account, statuses) - statuses.each do |status| - @stream_entry_batches << [build_xml(status.stream_entry), account.id] - end - end - def unpush_from_home_timelines(account, statuses) recipients = account.followers_for_local_distribution.to_a @@ -101,20 +85,4 @@ class BatchedRemoveStatusService < BaseService end end end - - def batch_salmon_slaps(status) - return if @mentions[status.id].empty? - - recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id) - - recipients.each do |recipient_id| - @salmon_batches << [build_xml(status.stream_entry), status.account_id, recipient_id] - end - end - - def build_xml(stream_entry) - return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id) - - @activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry) - end end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index c6eef04d4..c5e5e5761 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -44,7 +44,6 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account| - UnsubscribeService.new.call(account) if account.subscribed? SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at) end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 9050a4858..da06361c2 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -13,25 +13,17 @@ class BlockService < BaseService block = account.block!(target_account) BlockWorker.perform_async(account.id, target_account.id) - create_notification(block) unless target_account.local? + create_notification(block) if !target_account.local? && target_account.activitypub? 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 + ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url) end def build_json(block) Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer)) end - - def build_xml(block) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block)) - end end diff --git a/app/services/concerns/author_extractor.rb b/app/services/concerns/author_extractor.rb deleted file mode 100644 index c2419e9ec..000000000 --- a/app/services/concerns/author_extractor.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module AuthorExtractor - def author_from_xml(xml, update_profile = true) - return nil if xml.nil? - - # Try for acct - acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: OStatus::TagManager::XMLNS)&.content - - # Try + - if acct.blank? - username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: OStatus::TagManager::XMLNS)&.content - uri = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: OStatus::TagManager::XMLNS)&.content - - return nil if username.blank? || uri.blank? - - domain = Addressable::URI.parse(uri).normalized_host - acct = "#{username}@#{domain}" - end - - ResolveAccountService.new.call(acct, update_profile: update_profile) - end -end diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb deleted file mode 100644 index 9f6c8a082..000000000 --- a/app/services/concerns/stream_entry_renderer.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module StreamEntryRenderer - def stream_entry_to_xml(stream_entry) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(stream_entry, true)) - end -end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 128a24ad6..02b26458a 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -30,8 +30,6 @@ class FavouriteService < BaseService 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 @@ -46,8 +44,4 @@ class FavouriteService < BaseService def build_json(favourite) Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer)) end - - def build_xml(favourite) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite)) - end end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index cfc560022..a7f95603d 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchRemoteAccountService < BaseService - include AuthorExtractor - def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? resource_url, resource_options, protocol = FetchAtomService.new.call(url) @@ -12,34 +10,8 @@ class FetchRemoteAccountService < BaseService end case protocol - when :ostatus - process_atom(resource_url, **resource_options) when :activitypub ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options) end end - - private - - def process_atom(url, prefetched_body:) - xml = Nokogiri::XML(prefetched_body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: OStatus::TagManager::XMLNS), false) - - UpdateRemoteProfileService.new.call(xml, account) if account.present? && trusted_domain?(url, account) - - account - rescue TypeError - Rails.logger.debug "Unparseable URL given: #{url}" - nil - rescue Nokogiri::XML::XPath::SyntaxError - Rails.logger.debug 'Invalid XML or missing namespace' - nil - end - - def trusted_domain?(url, account) - domain = Addressable::URI.parse(url).normalized_host - domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? - end end diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index 9c3008035..aac39dfd5 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchRemoteStatusService < BaseService - include AuthorExtractor - def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? resource_url, resource_options, protocol = FetchAtomService.new.call(url) @@ -12,34 +10,8 @@ class FetchRemoteStatusService < BaseService end case protocol - when :ostatus - process_atom(resource_url, **resource_options) when :activitypub ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) end end - - private - - def process_atom(url, prefetched_body:) - Rails.logger.debug "Processing Atom for remote status at #{url}" - - xml = Nokogiri::XML(prefetched_body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - domain = Addressable::URI.parse(url).normalized_host - - return nil unless !account.nil? && confirmed_domain?(domain, account) - - statuses = ProcessFeedService.new.call(prefetched_body, account) - statuses.first - rescue Nokogiri::XML::XPath::SyntaxError - Rails.logger.debug 'Invalid XML or missing namespace' - nil - end - - def confirmed_domain?(domain, account) - account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? - end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 0305e2d62..8e118f5d3 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -13,7 +13,7 @@ class FollowService < BaseService target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? - raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? + raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?) if source_account.following?(target_account) # We're already following this account, but we'll call follow! again to @@ -32,7 +32,7 @@ class FollowService < BaseService if target_account.locked? || target_account.activitypub? request_follow(source_account, target_account, reblogs: reblogs) - else + elsif target_account.local? direct_follow(source_account, target_account, reblogs: reblogs) end end @@ -44,9 +44,6 @@ class FollowService < BaseService if target_account.local? LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name) - 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 @@ -57,27 +54,12 @@ class FollowService < BaseService def direct_follow(source_account, target_account, reblogs: true) follow = source_account.follow!(target_account, reblogs: reblogs) - if target_account.local? - LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) - else - Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed? - NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id) - AfterRemoteFollowWorker.perform_async(follow.id) - end - + LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) MergeWorker.perform_async(target_account.id, source_account.id) follow end - def build_follow_request_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request)) - end - - def build_follow_xml(follow) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow)) - end - def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer)) end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 7830aee11..34ec6d504 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -88,7 +88,6 @@ class PostStatusService < BaseService def postprocess_status! 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) PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll end diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb deleted file mode 100644 index 30a9dd85e..000000000 --- a/app/services/process_feed_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class ProcessFeedService < BaseService - def call(body, account, **options) - @options = options - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - update_author(body, account) - process_entries(xml, account) - end - - private - - def update_author(body, account) - RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) - end - - def process_entries(xml, account) - xml.xpath('//xmlns:entry', xmlns: OStatus::TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact - end - - def process_entry(xml, account) - activity = OStatus::Activity::General.new(xml, account, @options) - activity.specialize&.perform if activity.status? - rescue ActiveRecord::RecordInvalid => e - Rails.logger.debug "Nothing was saved for #{activity.id} because: #{e}" - nil - end -end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb deleted file mode 100644 index 1fca3832b..000000000 --- a/app/services/process_interaction_service.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -class ProcessInteractionService < BaseService - include AuthorExtractor - include Authorization - - # Record locally the remote interaction with our user - # @param [String] envelope Salmon envelope - # @param [Account] target_account Account the Salmon was addressed to - def call(envelope, target_account) - body = salmon.unpack(envelope) - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - - return if account.nil? || account.suspended? - - if salmon.verify(envelope, account.keypair) - RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) - - case verb(xml) - when :follow - follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain) - when :request_friend - follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain) - when :authorize - authorize_follow_request!(account, target_account) - when :reject - reject_follow_request!(account, target_account) - when :unfollow - unfollow!(account, target_account) - when :favorite - favourite!(xml, account) - when :unfavorite - unfavourite!(xml, account) - when :post - add_post!(body, account) if mentions_account?(xml, target_account) - when :share - add_post!(body, account) unless status(xml).nil? - when :delete - delete_post!(xml, account) - when :block - reflect_block!(account, target_account) - when :unblock - reflect_unblock!(account, target_account) - end - end - rescue HTTP::Error, OStatus2::BadSalmonError, Mastodon::NotPermittedError - nil - end - - private - - def mentions_account?(xml, account) - xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each { |mention_link| return true if [OStatus::TagManager.instance.uri_for(account), OStatus::TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) } - false - end - - def verb(xml) - raw = xml.at_xpath('//activity:verb', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::VERBS.key(raw) - rescue - :post - end - - def follow!(account, target_account) - follow = account.follow!(target_account) - FollowRequest.find_by(account: account, target_account: target_account)&.destroy - NotifyService.new.call(target_account, follow) - end - - def follow_request!(account, target_account) - return if account.requested?(target_account) - - follow_request = FollowRequest.create!(account: account, target_account: target_account) - NotifyService.new.call(target_account, follow_request) - end - - def authorize_follow_request!(account, target_account) - follow_request = FollowRequest.find_by(account: target_account, target_account: account) - follow_request&.authorize! - Pubsubhubbub::SubscribeWorker.perform_async(account.id) unless account.subscribed? - end - - def reject_follow_request!(account, target_account) - follow_request = FollowRequest.find_by(account: target_account, target_account: account) - follow_request&.reject! - end - - def unfollow!(account, target_account) - account.unfollow!(target_account) - FollowRequest.find_by(account: account, target_account: target_account)&.destroy - end - - def reflect_block!(account, target_account) - UnfollowService.new.call(target_account, account) if target_account.following?(account) - account.block!(target_account) - end - - def reflect_unblock!(account, target_account) - UnblockService.new.call(account, target_account) - end - - def delete_post!(xml, account) - status = Status.find(xml.at_xpath('//xmlns:id', xmlns: OStatus::TagManager::XMLNS).content) - - return if status.nil? - - authorize_with account, status, :destroy? - - RemovalWorker.perform_async(status.id) - end - - def favourite!(xml, from_account) - current_status = status(xml) - - return if current_status.nil? - - favourite = current_status.favourites.where(account: from_account).first_or_create!(account: from_account) - NotifyService.new.call(current_status.account, favourite) - end - - def unfavourite!(xml, from_account) - current_status = status(xml) - - return if current_status.nil? - - favourite = current_status.favourites.where(account: from_account).first - favourite&.destroy - end - - def add_post!(body, account) - ProcessingWorker.perform_async(account.id, body.force_encoding('UTF-8')) - end - - def status(xml) - uri = activity_id(xml) - return nil unless OStatus::TagManager.instance.local_id?(uri) - Status.find(OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')) - end - - def activity_id(xml) - xml.at_xpath('//activity:object', activity: OStatus::TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content - end - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index bc607dff3..da52bff6a 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ProcessMentionsService < BaseService - include StreamEntryRenderer include Payloadable # Scan status for mentions and fetch remote mentioned users, create @@ -49,17 +48,11 @@ class ProcessMentionsService < BaseService if mentioned_account.local? LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name) - elsif mentioned_account.ostatus? && !@status.stream_entry.hidden? - NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id) elsif mentioned_account.activitypub? ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url) end end - def ostatus_xml - @ostatus_xml ||= stream_entry_to_xml(@status.stream_entry) - end - def activitypub_json return @activitypub_json if defined?(@activitypub_json) @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account)) diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb deleted file mode 100644 index 550da6328..000000000 --- a/app/services/pubsubhubbub/subscribe_service.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class Pubsubhubbub::SubscribeService < BaseService - URL_PATTERN = /\A#{URI.regexp(%w(http https))}\z/ - - attr_reader :account, :callback, :secret, - :lease_seconds, :domain - - def call(account, callback, secret, lease_seconds, verified_domain = nil) - @account = account - @callback = Addressable::URI.parse(callback).normalize.to_s - @secret = secret - @lease_seconds = lease_seconds - @domain = verified_domain - - process_subscribe - end - - private - - def process_subscribe - if account.nil? - ['Invalid topic URL', 422] - elsif !valid_callback? - ['Invalid callback URL', 422] - elsif blocked_domain? - ['Callback URL not allowed', 403] - else - confirm_subscription - ['', 202] - end - end - - def confirm_subscription - subscription = locate_subscription - Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds) - end - - def valid_callback? - callback.present? && callback =~ URL_PATTERN - end - - def blocked_domain? - DomainBlock.blocked? Addressable::URI.parse(callback).host - end - - def locate_subscription - subscription = Subscription.find_or_initialize_by(account: account, callback_url: callback) - subscription.domain = domain - subscription.save! - subscription - end -end diff --git a/app/services/pubsubhubbub/unsubscribe_service.rb b/app/services/pubsubhubbub/unsubscribe_service.rb deleted file mode 100644 index 646150f7b..000000000 --- a/app/services/pubsubhubbub/unsubscribe_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Pubsubhubbub::UnsubscribeService < BaseService - attr_reader :account, :callback - - def call(account, callback) - @account = account - @callback = Addressable::URI.parse(callback).normalize.to_s - - process_unsubscribe - end - - private - - def process_unsubscribe - if account.nil? - ['Invalid topic URL', 422] - else - confirm_unsubscribe unless subscription.nil? - ['', 202] - end - end - - def confirm_unsubscribe - Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe') - end - - def subscription - @_subscription ||= Subscription.find_by(account: account, callback_url: callback) - end -end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 9cf4bc128..3bb460fca 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -2,7 +2,6 @@ class ReblogService < BaseService include Authorization - include StreamEntryRenderer include Payloadable # Reblog a status and notify its remote author @@ -24,7 +23,6 @@ class ReblogService < BaseService reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility) DistributionWorker.perform_async(reblog.id) - Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) ActivityPub::DistributionWorker.perform_async(reblog.id) create_notification(reblog) @@ -40,8 +38,6 @@ class ReblogService < BaseService if reblogged_status.account.local? LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name) - 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? && !reblogged_status.account.following?(reblog.account) ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index f87d0ba91..bc0000c8c 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -6,25 +6,17 @@ class RejectFollowService < BaseService def call(source_account, target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.reject! - create_notification(follow_request) unless source_account.local? + create_notification(follow_request) if !source_account.local? && source_account.activitypub? 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 + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) end def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer)) end - - def build_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)) - end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 81adc5aae..a8c9100b3 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RemoveStatusService < BaseService - include StreamEntryRenderer include Redisable include Payloadable @@ -78,11 +77,6 @@ class RemoveStatusService < BaseService target_accounts << @status.reblog.account if @status.reblog? && !@status.reblog.account.local? target_accounts.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(&:preferred_inbox_url)) do |target_account| [signed_activity_json, @account.id, target_account.preferred_inbox_url] @@ -90,9 +84,6 @@ class RemoveStatusService < BaseService end def remove_from_remote_followers - # OStatus - Pubsubhubbub::RawDistributionWorker.perform_async(salmon_xml, @account.id) - # ActivityPub ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| [signed_activity_json, @account.id, inbox_url] @@ -111,10 +102,6 @@ class RemoveStatusService < BaseService end end - def salmon_xml - @salmon_xml ||= stream_entry_to_xml(@stream_entry) - end - def signed_activity_json @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account)) end diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index e557706da..0ea31a0d8 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +require_relative '../models/account' + class ResolveAccountService < BaseService - include OStatus2::MagicKey include JsonLdHelper - DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' - # Find or create a local account for a remote user. # When creating, look up the user's webfinger and fetch all # important information from their feed @@ -48,18 +47,16 @@ class ResolveAccountService < BaseService return end - return if links_missing? || auto_suspend? return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) + return unless activitypub_ready? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) - if activitypub_ready? || @account&.activitypub? - handle_activitypub - else - handle_ostatus - end + next unless @account.nil? || @account.activitypub? + + handle_activitypub else raise Mastodon::RaceConditionError end @@ -73,38 +70,12 @@ class ResolveAccountService < BaseService private - def links_missing? - !(activitypub_ready? || ostatus_ready?) - end - - def ostatus_ready? - !(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? || - @webfinger.link('salmon').nil? || - @webfinger.link('http://webfinger.net/rel/profile-page').nil? || - @webfinger.link('magic-public-key').nil? || - canonical_uri.nil? || - hub_url.nil?) - end - def webfinger_update_due? @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) end def activitypub_ready? - !@webfinger.link('self').nil? && - ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) && - !actor_json.nil? && - actor_json['inbox'].present? - end - - def handle_ostatus - create_account if @account.nil? - update_account - update_account_profile if update_profile? - end - - def update_profile? - @options[:update_profile] + !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) end def handle_activitypub @@ -115,89 +86,10 @@ class ResolveAccountService < BaseService nil end - def create_account - Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}" - - @account = Account.new(username: @username, domain: @domain) - @account.suspended_at = domain_block.created_at if auto_suspend? - @account.silenced_at = domain_block.created_at if auto_silence? - @account.private_key = nil - end - - def update_account - @account.last_webfingered_at = Time.now.utc - @account.protocol = :ostatus - @account.remote_url = atom_url - @account.salmon_url = salmon_url - @account.url = url - @account.public_key = public_key - @account.uri = canonical_uri - @account.hub_url = hub_url - @account.save! - end - - def auto_suspend? - domain_block&.suspend? - end - - def auto_silence? - domain_block&.silence? - end - - def domain_block - return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.rule_for(@domain) - end - - def atom_url - @atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href - end - - def salmon_url - @salmon_url ||= @webfinger.link('salmon').href - end - def actor_url @actor_url ||= @webfinger.link('self').href end - def url - @url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href - end - - def public_key - @public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href) - end - - def canonical_uri - return @canonical_uri if defined?(@canonical_uri) - - author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri') - - if author_uri.nil? - owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS) - author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil? - end - - @canonical_uri = author_uri.nil? ? nil : author_uri.content - end - - def hub_url - return @hub_url if defined?(@hub_url) - - hubs = atom.xpath('//xmlns:link[@rel="hub"]') - @hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href'] - end - - def atom_body - return @atom_body if defined?(@atom_body) - - @atom_body = Request.new(:get, atom_url).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response.code == 200 - response.body_with_limit - end - end - def actor_json return @actor_json if defined?(@actor_json) @@ -205,15 +97,6 @@ class ResolveAccountService < BaseService @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil end - def atom - return @atom if defined?(@atom) - @atom = Nokogiri::XML(atom_body) - end - - def update_account_profile - RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false) - end - def lock_options { redis: Redis.current, key: "resolve:#{@username}@#{@domain}" } end diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb deleted file mode 100644 index 3419043e5..000000000 --- a/app/services/send_interaction_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class SendInteractionService < BaseService - # Send an Atom representation of an interaction to a remote Salmon endpoint - # @param [String] Entry XML - # @param [Account] source_account - # @param [Account] target_account - def call(xml, source_account, target_account) - @xml = xml - @source_account = source_account - @target_account = target_account - - return if !target_account.ostatus? || block_notification? - - build_request.perform do |delivery| - raise Mastodon::UnexpectedResponseError, delivery unless delivery.code > 199 && delivery.code < 300 - end - end - - private - - def build_request - request = Request.new(:post, @target_account.salmon_url, body: envelope) - request.add_headers('Content-Type' => 'application/magic-envelope+xml') - request - end - - def envelope - salmon.pack(@xml, @source_account.keypair) - end - - def block_notification? - DomainBlock.blocked?(@target_account.domain) - end - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb deleted file mode 100644 index 83fd64396..000000000 --- a/app/services/subscribe_service.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -class SubscribeService < BaseService - def call(account) - return if account.hub_url.blank? - - @account = account - @account.secret = SecureRandom.hex - - build_request.perform do |response| - if response_failed_permanently? response - # We're not allowed to subscribe. Fail and move on. - @account.secret = '' - @account.save! - elsif response_successful? response - # The subscription will be confirmed asynchronously. - @account.save! - else - # The response was either a 429 rate limit, or a 5xx error. - # We need to retry at a later time. Fail loudly! - raise Mastodon::UnexpectedResponseError, response - end - end - end - - private - - def build_request - request = Request.new(:post, @account.hub_url, form: subscription_params) - request.on_behalf_of(some_local_account) if some_local_account - request - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'subscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - 'hub.secret': @account.secret, - 'hub.lease_seconds': 7.days.seconds, - } - end - - def some_local_account - @some_local_account ||= Account.local.without_suspended.first - end - - # Any response in the 3xx or 4xx range, except for 429 (rate limit) - def response_failed_permanently?(response) - (response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests? - end - - # Any response in the 2xx range - def response_successful?(response) - response.status.success? - end -end diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index 95a858e9f..c263ac8af 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -7,25 +7,17 @@ class UnblockService < BaseService return unless account.blocking?(target_account) unblock = account.unblock!(target_account) - create_notification(unblock) unless target_account.local? + create_notification(unblock) if !target_account.local? && target_account.activitypub? 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 + ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url) end def build_json(unblock) Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer)) end - - def build_xml(block) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block)) - end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index dcc890b7d..37917a64f 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -6,7 +6,7 @@ class UnfavouriteService < BaseService def call(account, status) favourite = Favourite.find_by!(account: account, status: status) favourite.destroy! - create_notification(favourite) unless status.local? + create_notification(favourite) if !status.account.local? && status.account.activitypub? favourite end @@ -14,19 +14,10 @@ class UnfavouriteService < BaseService 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 + ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) end def build_json(favourite) Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer)) end - - def build_xml(favourite) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite)) - end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 17dc29735..b7033d7eb 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -21,8 +21,8 @@ class UnfollowService < BaseService return unless follow follow.destroy! - create_notification(follow) unless @target_account.local? - create_reject_notification(follow) if @target_account.local? && !@source_account.local? + create_notification(follow) if !@target_account.local? && @target_account.activitypub? + create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub? UnmergeWorker.perform_async(@target_account.id, @source_account.id) follow end @@ -38,16 +38,10 @@ class UnfollowService < BaseService end 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 + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url) end def create_reject_notification(follow) - # Rejecting an already-existing follow request - return unless follow.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url) end @@ -58,8 +52,4 @@ class UnfollowService < BaseService def build_reject_json(follow) Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) end - - def build_xml(follow) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow)) - end end diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb deleted file mode 100644 index 95c1fb4fc..000000000 --- a/app/services/unsubscribe_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class UnsubscribeService < BaseService - def call(account) - return if account.hub_url.blank? - - @account = account - - begin - build_request.perform do |response| - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{response.status}" unless response.status.success? - end - rescue HTTP::Error, OpenSSL::SSL::SSLError => e - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}" - end - - @account.secret = '' - @account.subscription_expires_at = nil - @account.save! - end - - private - - def build_request - Request.new(:post, @account.hub_url, form: subscription_params) - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'unsubscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - } - end -end diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb deleted file mode 100644 index 403395a0d..000000000 --- a/app/services/update_remote_profile_service.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class UpdateRemoteProfileService < BaseService - attr_reader :account, :remote_profile - - def call(body, account, resubscribe = false) - @account = account - @remote_profile = RemoteProfile.new(body) - - return if remote_profile.root.nil? - - update_account unless remote_profile.author.nil? - - old_hub_url = account.hub_url - account.hub_url = remote_profile.hub_link if remote_profile.hub_link.present? && remote_profile.hub_link != old_hub_url - - account.save_with_optional_media! - - Pubsubhubbub::SubscribeWorker.perform_async(account.id) if resubscribe && account.hub_url != old_hub_url - end - - private - - def update_account - account.display_name = remote_profile.display_name || '' - account.note = remote_profile.note || '' - account.locked = remote_profile.locked? - - if !account.suspended? && !DomainBlock.reject_media?(account.domain) - if remote_profile.avatar.present? - account.avatar_remote_url = remote_profile.avatar - else - account.avatar_remote_url = '' - account.avatar.destroy - end - - if remote_profile.header.present? - account.header_remote_url = remote_profile.header - else - account.header_remote_url = '' - account.header.destroy - end - - save_emojis if remote_profile.emojis.present? - end - end - - def save_emojis - do_not_download = DomainBlock.reject_media?(account.domain) - - return if do_not_download - - remote_profile.emojis.each do |link| - next unless link['href'] && link['name'] - - shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain) - - next unless emoji.nil? - - emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain) - emoji.image_remote_url = link['href'] - emoji.save - end - end -end diff --git a/app/services/verify_salmon_service.rb b/app/services/verify_salmon_service.rb deleted file mode 100644 index 205b35d8b..000000000 --- a/app/services/verify_salmon_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class VerifySalmonService < BaseService - include AuthorExtractor - - def call(payload) - body = salmon.unpack(payload) - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - - if account.nil? - false - else - salmon.verify(payload, account.keypair) - end - end - - private - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 950e61847..de7d2a8ba 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,7 +7,6 @@ - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex' }/ - %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/rss+xml', href: account_url(@account, format: 'rss') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml deleted file mode 100644 index 1dec8e396..000000000 --- a/app/views/admin/subscriptions/_subscription.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%tr - %td - %samp= subscription.account.acct - %td - %samp= subscription.callback_url - %td - - if subscription.confirmed? - %i.fa.fa-check - %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" } - %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) } - = precede subscription.expired? ? '-' : '' do - = time_ago_in_words(subscription.expires_at) - %td - - if subscription.last_successful_delivery_at? - %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) } - = l subscription.last_successful_delivery_at - - else - %i.fa.fa-times diff --git a/app/views/admin/subscriptions/index.html.haml b/app/views/admin/subscriptions/index.html.haml deleted file mode 100644 index 83704c8ee..000000000 --- a/app/views/admin/subscriptions/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :page_title do - = t('admin.subscriptions.title') - -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.subscriptions.topic') - %th= t('admin.subscriptions.callback_url') - %th= t('admin.subscriptions.confirmed') - %th= t('admin.subscriptions.expires_in') - %th= t('admin.subscriptions.last_delivery') - %tbody - = render @subscriptions - -= paginate @subscriptions diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index 968c8c138..c82cdb7b3 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd| link['href'] = account_url(@account) end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'salmon' - link['href'] = api_salmon_url(@account.id) - end - xrd << Ox::Element.new('Link').tap do |link| link['rel'] = 'magic-public-key' link['href'] = "data:application/magic-public-key,#{@account.magic_key}" diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb index 84eb6ade2..ce9c65834 100644 --- a/app/workers/after_remote_follow_request_worker.rb +++ b/app/workers/after_remote_follow_request_worker.rb @@ -5,27 +5,5 @@ class AfterRemoteFollowRequestWorker sidekiq_options queue: 'pull', retry: 5 - attr_reader :follow_request - - def perform(follow_request_id) - @follow_request = FollowRequest.find(follow_request_id) - process_follow_service if processing_required? - rescue ActiveRecord::RecordNotFound - true - end - - private - - def process_follow_service - follow_request.destroy - FollowService.new.call(follow_request.account, updated_account.acct) - end - - def processing_required? - !updated_account.nil? && !updated_account.locked? - end - - def updated_account - @_updated_account ||= FetchRemoteAccountService.new.call(follow_request.target_account.remote_url) - end + def perform(follow_request_id); end end diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb index edab83f85..d9719f2bf 100644 --- a/app/workers/after_remote_follow_worker.rb +++ b/app/workers/after_remote_follow_worker.rb @@ -5,27 +5,5 @@ class AfterRemoteFollowWorker sidekiq_options queue: 'pull', retry: 5 - attr_reader :follow - - def perform(follow_id) - @follow = Follow.find(follow_id) - process_follow_service if processing_required? - rescue ActiveRecord::RecordNotFound - true - end - - private - - def process_follow_service - follow.destroy - FollowService.new.call(follow.account, updated_account.acct) - end - - def updated_account - @_updated_account ||= FetchRemoteAccountService.new.call(follow.target_account.remote_url) - end - - def processing_required? - !updated_account.nil? && updated_account.locked? - end + def perform(follow_id); end end diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb index da1d6ab45..1c0f001cf 100644 --- a/app/workers/notification_worker.rb +++ b/app/workers/notification_worker.rb @@ -5,7 +5,5 @@ class NotificationWorker sidekiq_options queue: 'push', retry: 5 - def perform(xml, source_account_id, target_account_id) - SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id)) - end + def perform(xml, source_account_id, target_account_id); end end diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb index 978c3aba2..cf3bd8397 100644 --- a/app/workers/processing_worker.rb +++ b/app/workers/processing_worker.rb @@ -5,7 +5,5 @@ class ProcessingWorker sidekiq_options backtrace: true - def perform(account_id, body) - ProcessFeedService.new.call(body, Account.find(account_id), override_timestamps: true) - end + def perform(account_id, body); end end diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb index c0e7b677e..783a8c95f 100644 --- a/app/workers/pubsubhubbub/confirmation_worker.rb +++ b/app/workers/pubsubhubbub/confirmation_worker.rb @@ -2,81 +2,8 @@ class Pubsubhubbub::ConfirmationWorker include Sidekiq::Worker - include RoutingHelper sidekiq_options queue: 'push', retry: false - attr_reader :subscription, :mode, :secret, :lease_seconds - - def perform(subscription_id, mode, secret = nil, lease_seconds = nil) - @subscription = Subscription.find(subscription_id) - @mode = mode - @secret = secret - @lease_seconds = lease_seconds - process_confirmation - end - - private - - def process_confirmation - prepare_subscription - - callback_get_with_params - logger.debug "Confirming PuSH subscription for #{subscription.callback_url} with challenge #{challenge}: #{@callback_response_body}" - - update_subscription - end - - def update_subscription - if successful_subscribe? - subscription.save! - elsif successful_unsubscribe? - subscription.destroy! - end - end - - def successful_subscribe? - subscribing? && response_matches_challenge? - end - - def successful_unsubscribe? - (unsubscribing? && response_matches_challenge?) || !subscription.confirmed? - end - - def response_matches_challenge? - @callback_response_body == challenge - end - - def subscribing? - mode == 'subscribe' - end - - def unsubscribing? - mode == 'unsubscribe' - end - - def callback_get_with_params - Request.new(:get, subscription.callback_url, params: callback_params).perform do |response| - @callback_response_body = response.body_with_limit - end - end - - def callback_params - { - 'hub.topic': account_url(subscription.account, format: :atom), - 'hub.mode': mode, - 'hub.challenge': challenge, - 'hub.lease_seconds': subscription.lease_seconds, - } - end - - def prepare_subscription - subscription.secret = secret - subscription.lease_seconds = lease_seconds - subscription.confirmed = true - end - - def challenge - @_challenge ||= SecureRandom.hex - end + def perform(subscription_id, mode, secret = nil, lease_seconds = nil); end end diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 619bfa48a..1260060bd 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -2,80 +2,8 @@ class Pubsubhubbub::DeliveryWorker include Sidekiq::Worker - include RoutingHelper sidekiq_options queue: 'push', retry: 3, dead: false - sidekiq_retry_in do |count| - 5 * (count + 1) - end - - attr_reader :subscription, :payload - - def perform(subscription_id, payload) - @subscription = Subscription.find(subscription_id) - @payload = payload - process_delivery unless blocked_domain? - rescue => e - raise e.class, "Delivery failed for #{subscription&.callback_url}: #{e.message}", e.backtrace[0] - end - - private - - def process_delivery - callback_post_payload do |payload_delivery| - raise Mastodon::UnexpectedResponseError, payload_delivery unless response_successful? payload_delivery - end - - subscription.touch(:last_successful_delivery_at) - end - - def callback_post_payload(&block) - request = Request.new(:post, subscription.callback_url, body: payload) - request.add_headers(headers) - request.perform(&block) - end - - def blocked_domain? - DomainBlock.blocked?(host) - end - - def host - Addressable::URI.parse(subscription.callback_url).normalized_host - end - - def headers - { - 'Content-Type' => 'application/atom+xml', - 'Link' => link_header, - }.merge(signature_headers.to_h) - end - - def link_header - LinkHeader.new([hub_link_header, self_link_header]).to_s - end - - def hub_link_header - [api_push_url, [%w(rel hub)]] - end - - def self_link_header - [account_url(subscription.account, format: :atom), [%w(rel self)]] - end - - def signature_headers - { 'X-Hub-Signature' => payload_signature } if subscription.secret? - end - - def payload_signature - "sha1=#{hmac_payload_digest}" - end - - def hmac_payload_digest - OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret, payload) - end - - def response_successful?(payload_delivery) - payload_delivery.code > 199 && payload_delivery.code < 300 - end + def perform(subscription_id, payload); end end diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb index fed5e917d..75bac5d6f 100644 --- a/app/workers/pubsubhubbub/distribution_worker.rb +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -5,28 +5,5 @@ class Pubsubhubbub::DistributionWorker sidekiq_options queue: 'push' - def perform(stream_entry_ids) - stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.hidden? } - - return if stream_entries.empty? - - @account = stream_entries.first.account - @subscriptions = active_subscriptions.to_a - - distribute_public!(stream_entries) - end - - private - - def distribute_public!(stream_entries) - @payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries)) - - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription_id| - [subscription_id, @payload] - end - end - - def active_subscriptions - Subscription.where(account: @account).active.pluck(:id) - end + def perform(stream_entry_ids); end end diff --git a/app/workers/pubsubhubbub/raw_distribution_worker.rb b/app/workers/pubsubhubbub/raw_distribution_worker.rb index 16962a623..ece9c80ac 100644 --- a/app/workers/pubsubhubbub/raw_distribution_worker.rb +++ b/app/workers/pubsubhubbub/raw_distribution_worker.rb @@ -5,18 +5,5 @@ class Pubsubhubbub::RawDistributionWorker sidekiq_options queue: 'push' - def perform(xml, source_account_id) - @account = Account.find(source_account_id) - @subscriptions = active_subscriptions.to_a - - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription| - [subscription.id, xml] - end - end - - private - - def active_subscriptions - Subscription.where(account: @account).active.select('id, callback_url, domain') - end + def perform(xml, source_account_id); end end diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 2e176d1c1..b861b5e67 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -5,30 +5,5 @@ class Pubsubhubbub::SubscribeWorker sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false - sidekiq_retry_in do |count| - case count - when 0 - 30.minutes.seconds - when 1 - 2.hours.seconds - when 2 - 12.hours.seconds - else - 24.hours.seconds * (count - 2) - end - end - - sidekiq_retries_exhausted do |msg, _e| - account = Account.find(msg['args'].first) - Sidekiq.logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing" - ::UnsubscribeService.new.call(account) - end - - def perform(account_id) - account = Account.find(account_id) - logger.debug "PuSH re-subscribing to #{account.acct}" - ::SubscribeService.new.call(account) - rescue => e - raise e.class, "Subscribe failed for #{account&.acct}: #{e.message}", e.backtrace[0] - end + def perform(account_id); end end diff --git a/app/workers/pubsubhubbub/unsubscribe_worker.rb b/app/workers/pubsubhubbub/unsubscribe_worker.rb index a271715b7..0c1c263f6 100644 --- a/app/workers/pubsubhubbub/unsubscribe_worker.rb +++ b/app/workers/pubsubhubbub/unsubscribe_worker.rb @@ -5,11 +5,5 @@ class Pubsubhubbub::UnsubscribeWorker sidekiq_options queue: 'push', retry: false, unique: :until_executed, dead: false - def perform(account_id) - account = Account.find(account_id) - logger.debug "PuSH unsubscribing from #{account.acct}" - ::UnsubscribeService.new.call(account) - rescue ActiveRecord::RecordNotFound - true - end + def perform(account_id); end end diff --git a/app/workers/remote_profile_update_worker.rb b/app/workers/remote_profile_update_worker.rb index 03585ad2d..01e8daf8f 100644 --- a/app/workers/remote_profile_update_worker.rb +++ b/app/workers/remote_profile_update_worker.rb @@ -5,9 +5,5 @@ class RemoteProfileUpdateWorker sidekiq_options queue: 'pull' - def perform(account_id, body, resubscribe) - UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe) - rescue ActiveRecord::RecordNotFound - true - end + def perform(account_id, body, resubscribe); end end diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb index d37d40432..10200b06c 100644 --- a/app/workers/salmon_worker.rb +++ b/app/workers/salmon_worker.rb @@ -5,9 +5,5 @@ class SalmonWorker sidekiq_options backtrace: true - def perform(account_id, body) - ProcessInteractionService.new.call(body, Account.find(account_id)) - rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord::RecordNotFound - true - end + def perform(account_id, body); end end diff --git a/app/workers/scheduler/subscriptions_scheduler.rb b/app/workers/scheduler/subscriptions_scheduler.rb index d5873bccb..6903cadc7 100644 --- a/app/workers/scheduler/subscriptions_scheduler.rb +++ b/app/workers/scheduler/subscriptions_scheduler.rb @@ -5,13 +5,5 @@ class Scheduler::SubscriptionsScheduler sidekiq_options unique: :until_executed, retry: 0 - def perform - Pubsubhubbub::SubscribeWorker.push_bulk(expiring_accounts.pluck(:id)) - end - - private - - def expiring_accounts - Account.expiring(1.day.from_now).partitioned - end + def perform; end end diff --git a/config/locales/en.yml b/config/locales/en.yml index d4f1855aa..611f36fdd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -469,13 +469,6 @@ en: no_status_selected: No statuses were changed as none were selected title: Account statuses with_media: With media - subscriptions: - callback_url: Callback URL - confirmed: Confirmed - expires_in: Expires in - last_delivery: Last delivery - title: WebSub - topic: Topic tags: accounts: Accounts hidden: Hidden diff --git a/config/navigation.rb b/config/navigation.rb index df1024189..ef845d1fc 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -48,7 +48,6 @@ SimpleNavigation::Configuration.run do |navigation| s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings} s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays} - s.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index 9ab5ba7f0..4b6d464c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,7 +154,6 @@ Rails.application.routes.draw do namespace :admin do get '/dashboard', to: 'dashboard#index' - resources :subscriptions, only: [:index] resources :domain_blocks, only: [:new, :create, :show, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] @@ -191,8 +190,6 @@ Rails.application.routes.draw do resources :accounts, only: [:index, :show] do member do - post :subscribe - post :unsubscribe post :enable post :unsilence post :unsuspend @@ -257,16 +254,6 @@ Rails.application.routes.draw do get '/admin', to: redirect('/admin/dashboard', status: 302) namespace :api do - # PubSubHubbub outgoing subscriptions - resources :subscriptions, only: [:show] - post '/subscriptions/:id', to: 'subscriptions#update' - - # PubSubHubbub incoming subscriptions - post '/push', to: 'push#update', as: :push - - # Salmon - post '/salmon/:id', to: 'salmon#update', as: :salmon - # OEmbed get '/oembed', to: 'oembed#show', as: :oembed @@ -318,7 +305,6 @@ Rails.application.routes.draw do get '/search', to: 'search#index', as: :search - resources :follows, only: [:create] resources :media, only: [:create, :update] resources :blocks, only: [:index] resources :mutes, only: [:index] diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 0ec1742ab..a16dea967 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -9,9 +9,6 @@ scheduled_statuses_scheduler: every: '5m' class: Scheduler::ScheduledStatusesScheduler - subscriptions_scheduler: - cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *' - class: Scheduler::SubscriptionsScheduler media_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::MediaCleanupScheduler diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index a348ab3d7..608606ff9 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -75,44 +75,6 @@ RSpec.describe Admin::AccountsController, type: :controller do end end - describe 'POST #subscribe' do - subject { post :subscribe, params: { id: account.id } } - - let(:current_user) { Fabricate(:user, admin: admin) } - let(:account) { Fabricate(:account) } - - context 'when user is admin' do - let(:admin) { true } - - it { is_expected.to redirect_to admin_account_path(account.id) } - end - - context 'when user is not admin' do - let(:admin) { false } - - it { is_expected.to have_http_status :forbidden } - end - end - - describe 'POST #unsubscribe' do - subject { post :unsubscribe, params: { id: account.id } } - - let(:current_user) { Fabricate(:user, admin: admin) } - let(:account) { Fabricate(:account) } - - context 'when user is admin' do - let(:admin) { true } - - it { is_expected.to redirect_to admin_account_path(account.id) } - end - - context 'when user is not admin' do - let(:admin) { false } - - it { is_expected.to have_http_status :forbidden } - end - end - describe 'POST #memorialize' do subject { post :memorialize, params: { id: account.id } } diff --git a/spec/controllers/admin/subscriptions_controller_spec.rb b/spec/controllers/admin/subscriptions_controller_spec.rb deleted file mode 100644 index 967152abe..000000000 --- a/spec/controllers/admin/subscriptions_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe Admin::SubscriptionsController, type: :controller do - render_views - - describe 'GET #index' do - around do |example| - default_per_page = Subscription.default_per_page - Subscription.paginates_per 1 - example.run - Subscription.paginates_per default_per_page - end - - before do - sign_in Fabricate(:user, admin: true), scope: :user - end - - it 'renders subscriptions' do - Fabricate(:subscription) - specified = Fabricate(:subscription) - - get :index - - subscriptions = assigns(:subscriptions) - expect(subscriptions.count).to eq 1 - expect(subscriptions[0]).to eq specified - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb deleted file mode 100644 index d769d8554..000000000 --- a/spec/controllers/api/push_controller_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::PushController, type: :controller do - describe 'POST #update' do - context 'with hub.mode=subscribe' do - it 'creates a subscription' do - service = double(call: ['', 202]) - allow(Pubsubhubbub::SubscribeService).to receive(:new).and_return(service) - account = Fabricate(:account) - account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom" - post :update, params: { - 'hub.mode' => 'subscribe', - 'hub.topic' => account_topic_url, - 'hub.callback' => 'https://callback.host/api', - 'hub.lease_seconds' => '3600', - 'hub.secret' => 'as1234df', - } - - expect(service).to have_received(:call).with( - account, - 'https://callback.host/api', - 'as1234df', - '3600', - nil - ) - expect(response).to have_http_status(202) - end - end - - context 'with hub.mode=unsubscribe' do - it 'unsubscribes the account' do - service = double(call: ['', 202]) - allow(Pubsubhubbub::UnsubscribeService).to receive(:new).and_return(service) - account = Fabricate(:account) - account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom" - post :update, params: { - 'hub.mode' => 'unsubscribe', - 'hub.topic' => account_topic_url, - 'hub.callback' => 'https://callback.host/api', - } - - expect(service).to have_received(:call).with( - account, - 'https://callback.host/api', - ) - expect(response).to have_http_status(202) - end - end - - context 'with unknown mode' do - it 'returns an unknown mode error' do - post :update, params: { 'hub.mode' => 'fake' } - - expect(response).to have_http_status(422) - expect(response.body).to match(/Unknown mode/) - end - end - end -end diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb deleted file mode 100644 index 235a29af0..000000000 --- a/spec/controllers/api/salmon_controller_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::SalmonController, type: :controller do - render_views - - let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account } - - before do - stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - end - - describe 'POST #update' do - context 'with valid post data' do - before do - post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) - end - - it 'contains XML in the request body' do - expect(request.body.read).to be_a String - end - - it 'returns http success' do - expect(response).to have_http_status(202) - end - - it 'creates remote account' do - expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil - end - - it 'creates status' do - expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil - end - - it 'creates mention for target account' do - expect(account.mentions.count).to eq 1 - end - end - - context 'with empty post data' do - before do - post :update, params: { id: account.id }, body: '' - end - - it 'returns http client error' do - expect(response).to have_http_status(400) - end - end - - context 'with invalid post data' do - before do - service = double(call: false) - allow(VerifySalmonService).to receive(:new).and_return(service) - - post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) - end - - it 'returns http client error' do - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb deleted file mode 100644 index 7a4252fe6..000000000 --- a/spec/controllers/api/subscriptions_controller_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::SubscriptionsController, type: :controller do - render_views - - let(:account) { Fabricate(:account, username: 'gargron', domain: 'quitter.no', remote_url: 'topic_url', secret: 'abc') } - - describe 'GET #show' do - context 'with valid subscription' do - before do - get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'echoes back the challenge' do - expect(response.body).to match '456' - end - end - - context 'with invalid subscription' do - before do - expect_any_instance_of(Account).to receive_message_chain(:subscription, :valid?).and_return(false) - get :show, params: { :id => account.id } - end - - it 'returns http success' do - expect(response).to have_http_status(404) - end - end - end - - describe 'POST #update' do - let(:feed) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } - - 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(: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)}" - - post :update, params: { id: account.id }, body: feed - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates statuses for feed' do - expect(account.statuses.count).to_not eq 0 - end - end -end diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb deleted file mode 100644 index 089e0fe5e..000000000 --- a/spec/controllers/api/v1/follows_controller_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::V1::FollowsController, type: :controller 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:follows') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'POST #create' do - before do - stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {}) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:post, "https://quitter.no/main/salmon/user/7477").to_return(:status => 200, :body => "", :headers => {}) - - post :create, params: { uri: 'gargron@quitter.no' } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates account for remote user' do - expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil - end - - it 'creates a follow relation between user and remote user' do - expect(user.account.following?(Account.find_by(username: 'gargron', domain: 'quitter.no'))).to be true - end - - it 'sends a salmon slap to the remote user' do - expect(a_request(:post, "https://quitter.no/main/salmon/user/7477")).to have_been_made - end - - it 'subscribes to remote hub' do - expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made - end - - it 'returns http success if already following, too' do - post :create, params: { uri: 'gargron@quitter.no' } - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/fixtures/requests/webfinger.txt b/spec/fixtures/requests/webfinger.txt index edb8a2dbb..f337ecae6 100644 --- a/spec/fixtures/requests/webfinger.txt +++ b/spec/fixtures/requests/webfinger.txt @@ -8,4 +8,4 @@ Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; -{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} \ No newline at end of file +{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index 891871c1c..74ab7576f 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -406,28 +406,6 @@ RSpec.describe OStatus::AtomSerializer do scope = entry.nodes.find { |node| node.name == 'mastodon:scope' } expect(scope.text).to eq 'public' end - - it 'returns element whose rendered view triggers creation when processed' do - remote_account = Account.create!(username: 'username') - remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true) - entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test - xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote.test') - - remote_status.destroy! - remote_account.destroy! - - account = Account.create!( - domain: 'remote.test', - username: 'username', - last_webfingered_at: Time.now.utc - ) - - ProcessFeedService.new.call(xml, account) - - expect(Status.find_by(uri: "https://remote.test/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status - end end context 'if status is not present' do @@ -683,24 +661,6 @@ RSpec.describe OStatus::AtomSerializer do end end - it 'appends link element for hub' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'hub' } - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/api/push' - end - - it 'appends link element for Salmon' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'salmon' } - expect(link[:href]).to start_with 'https://cb6e6126.ngrok.io/api/salmon/' - end - it 'appends stream entries' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) @@ -784,18 +744,6 @@ RSpec.describe OStatus::AtomSerializer do object = block_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers block when processed' do - block = Fabricate(:block) - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - xml = OStatus::AtomSerializer.render(block_salmon) - envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) - block.destroy! - - ProcessInteractionService.new.call(envelope, block.target_account) - - expect(block.account.blocking?(block.target_account)).to be true - end end describe '#unblock_salmon' do @@ -871,17 +819,6 @@ RSpec.describe OStatus::AtomSerializer do object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers block when processed' do - block = Fabricate(:block) - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - xml = OStatus::AtomSerializer.render(unblock_salmon) - envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) - - ProcessInteractionService.new.call(envelope, block.target_account) - - expect { block.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#favourite_salmon' do @@ -964,17 +901,6 @@ RSpec.describe OStatus::AtomSerializer do expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote' expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote' end - - it 'returns element whose rendered view triggers favourite when processed' do - favourite = Fabricate(:favourite) - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - xml = OStatus::AtomSerializer.render(favourite_salmon) - envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) - favourite.destroy! - - ProcessInteractionService.new.call(envelope, favourite.status.account) - expect(favourite.account.favourited?(favourite.status)).to be true - end end describe '#unfavourite_salmon' do @@ -1064,16 +990,6 @@ RSpec.describe OStatus::AtomSerializer do expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote' expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote' end - - it 'returns element whose rendered view triggers unfavourite when processed' do - favourite = Fabricate(:favourite) - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - xml = OStatus::AtomSerializer.render(unfavourite_salmon) - envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) - - ProcessInteractionService.new.call(envelope, favourite.status.account) - expect { favourite.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#follow_salmon' do @@ -1143,18 +1059,6 @@ RSpec.describe OStatus::AtomSerializer do expect(follow_salmon.title.text).to eq 'account started following target_account@remote' expect(follow_salmon.content.text).to eq 'account started following target_account@remote' end - - it 'returns element whose rendered view triggers follow when processed' do - follow = Fabricate(:follow) - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - xml = OStatus::AtomSerializer.render(follow_salmon) - follow.destroy! - envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) - - ProcessInteractionService.new.call(envelope, follow.target_account) - - expect(follow.account.following?(follow.target_account)).to be true - end end describe '#unfollow_salmon' do @@ -1251,19 +1155,6 @@ RSpec.describe OStatus::AtomSerializer do object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers unfollow when processed' do - follow = Fabricate(:follow) - follow.destroy! - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - xml = OStatus::AtomSerializer.render(unfollow_salmon) - follow.account.follow!(follow.target_account) - envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) - - ProcessInteractionService.new.call(envelope, follow.target_account) - - expect(follow.account.following?(follow.target_account)).to be false - end end describe '#follow_request_salmon' do @@ -1294,18 +1185,6 @@ RSpec.describe OStatus::AtomSerializer do follow_request_salmon = serialize(follow_request) expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote' end - - it 'returns element whose rendered view triggers follow request when processed' do - follow_request = Fabricate(:follow_request) - follow_request_salmon = serialize(follow_request) - xml = OStatus::AtomSerializer.render(follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.account.keypair) - follow_request.destroy! - - ProcessInteractionService.new.call(envelope, follow_request.target_account) - - expect(follow_request.account.requested?(follow_request.target_account)).to eq true - end end end @@ -1364,18 +1243,6 @@ RSpec.describe OStatus::AtomSerializer do verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] end - - it 'returns element whose rendered view creates follow from follow request when processed' do - follow_request = Fabricate(:follow_request) - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - xml = OStatus::AtomSerializer.render(authorize_follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) - - ProcessInteractionService.new.call(envelope, follow_request.account) - - expect(follow_request.account.following?(follow_request.target_account)).to eq true - expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#reject_follow_request_salmon' do @@ -1427,18 +1294,6 @@ RSpec.describe OStatus::AtomSerializer do verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] end - - it 'returns element whose rendered view deletes follow request when processed' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - xml = OStatus::AtomSerializer.render(reject_follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) - - ProcessInteractionService.new.call(envelope, follow_request.account) - - expect(follow_request.account.following?(follow_request.target_account)).to eq false - expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#object' do diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index 562ef0041..ce56d57a6 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -38,13 +38,6 @@ RSpec.describe AuthorizeFollowService, type: :service do it 'creates follow relation' do expect(bob.following?(sender)).to be true end - - it 'sends a follow request authorization salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:authorize]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index e53623449..d52e7f484 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -49,19 +49,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once) end - it 'sends PuSH update to PuSH subscribers' do - expect(a_request(:post, 'http://example.com/push').with { |req| - matches = req.body.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.at_least_once - 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) - xml.match(OStatus::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 diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index 6584bb90e..de20dd026 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -28,13 +28,6 @@ RSpec.describe BlockService, type: :service do it 'creates a blocking relation' do expect(sender.blocking?(bob)).to be true end - - it 'sends a block salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:block]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 0a20ccf6e..4c29ea77b 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe FavouriteService, type: :service do it 'creates a favourite' do expect(status.favourites.first).to_not be_nil end - - it 'sends a salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:favorite]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 3cd86708b..37e9910d4 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -36,36 +36,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do include_examples 'return Account' end - context 'protocol is :ostatus' do - let(:prefetched_body) { xml } - let(:protocol) { :ostatus } - - before do - stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) - stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - end - - include_examples 'return Account' - - it 'does not update account information if XML comes from an unverified domain' do - feed_xml = <<-XML.squish - - - - http://activitystrea.ms/schema/1.0/person - http://kickass.zone/users/localhost - localhost - localhost - Villain!!! - - - XML - - returned_account = described_class.new.call('https://real-fake-domains.com/alice', feed_xml, :ostatus) - expect(returned_account.display_name).to_not eq 'Villain!!!' - end - end - context 'when prefetched_body is nil' do context 'protocol is :activitypub' do before do @@ -75,15 +45,5 @@ RSpec.describe FetchRemoteAccountService, type: :service do include_examples 'return Account' end - - context 'protocol is :ostatus' do - before do - stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' }) - stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) - stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - end - - include_examples 'return Account' - end end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 3c4ec59be..86c85293e 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -96,74 +96,6 @@ RSpec.describe FollowService, type: :service do end end - context 'remote OStatus account' do - describe 'locked account' do - 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 => {}) - subject.call(sender, bob.acct) - end - - it 'creates a follow request' do - expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil - end - - it 'sends a follow request salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:request_friend]) - }).to have_been_made.once - end - end - - describe 'unlocked account' do - 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 => {}) - stub_request(:post, "http://hub.example.com/").to_return(status: 202) - subject.call(sender, bob.acct) - end - - it 'creates a following relation' do - expect(sender.following?(bob)).to be true - end - - it 'sends a follow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:follow]) - }).to have_been_made.once - end - - it 'subscribes to PuSH' do - expect(a_request(:post, "http://hub.example.com/")).to have_been_made.once - end - end - - describe 'already followed account' do - 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) - subject.call(sender, bob.acct) - end - - it 'keeps a following relation' do - expect(sender.following?(bob)).to be true - end - - it 'does not send a follow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/")).not_to have_been_made - end - - it 'does not subscribe to PuSH' do - expect(a_request(:post, "http://hub.example.com/")).not_to have_been_made - 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 } diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index 5cf2dadf0..5355133f4 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -3,7 +3,11 @@ require 'rails_helper' RSpec.describe ImportService, type: :service do let!(:account) { Fabricate(:account, locked: false) } let!(:bob) { Fabricate(:account, username: 'bob', locked: false) } - let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false) } + let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') } + + before do + stub_request(:post, "https://example.com/inbox").to_return(status: 200) + end context 'import old-style list of muted users' do subject { ImportService.new } @@ -95,7 +99,8 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, including boosts' do subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -106,7 +111,8 @@ RSpec.describe ImportService, type: :service do it 'follows the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -117,7 +123,8 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -136,9 +143,10 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, respecting boosts' do subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end @@ -148,9 +156,10 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end @@ -160,9 +169,10 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index facbe977f..bf06f50e9 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -144,7 +144,6 @@ RSpec.describe PostStatusService, type: :service 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) @@ -152,7 +151,6 @@ RSpec.describe PostStatusService, type: :service do status = subject.call(account, text: "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(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id) end diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb deleted file mode 100644 index 9d3465f3f..000000000 --- a/spec/services/process_feed_service_spec.rb +++ /dev/null @@ -1,252 +0,0 @@ -require 'rails_helper' - -RSpec.describe ProcessFeedService, type: :service do - subject { ProcessFeedService.new } - - describe 'processing a feed' do - let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } - let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') } - - before do - stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:head, "http://kickass.zone/media/2").to_return(:status => 404) - stub_request(:head, "http://kickass.zone/media/3").to_return(:status => 404) - stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt')) - stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt')) - end - - context 'when domain does not reject media' do - before do - subject.call(body, account) - end - - it 'updates remote user\'s account information' do - account.reload - expect(account.display_name).to eq '::1' - expect(account).to have_attached_file(:avatar) - expect(account.avatar_file_name).not_to be_nil - end - - it 'creates posts' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil - end - - it 'marks replies as replies' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') - expect(status.reply?).to be true - end - - it 'sets account being replied to when possible' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') - expect(status.in_reply_to_account_id).to eq status.account_id - end - - it 'ignores delete statuses unless they existed before' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Status')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=12:objectType=Status')).to be_nil - end - - it 'does not create statuses for follows' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow')).to be_nil - end - - it 'does not create statuses for favourites' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite')).to be_nil - end - - it 'creates posts with media' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') - - expect(status).to_not be_nil - expect(status.media_attachments.first).to have_attached_file(:file) - expect(status.media_attachments.first.image?).to be true - expect(status.media_attachments.first.file_file_name).not_to be_nil - end - end - - context 'when domain is set to reject media' do - let!(:domain_block) { Fabricate(:domain_block, domain: 'kickass.zone', reject_media: true) } - - before do - subject.call(body, account) - end - - it 'updates remote user\'s account information' do - account.reload - expect(account.display_name).to eq '::1' - end - - it 'rejects remote user\'s avatar' do - account.reload - expect(account.display_name).to eq '::1' - expect(account.avatar_file_name).to be_nil - end - - it 'creates posts' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil - end - - it 'creates posts with remote-only media' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') - - expect(status).to_not be_nil - expect(status.media_attachments.first.file_file_name).to be_nil - expect(status.media_attachments.first.unknown?).to be true - end - end - end - - it 'does not accept tampered reblogs' do - good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - real_body = < - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - -XML - - 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') - - body = < - - tag:talon.xyz,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - - https://talon.xyz/users/sombra - http://activitystrea.ms/schema/1.0/person - https://talon.xyz/users/sombra - sombra - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - Overwatch SUCKS AHAHA - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch SUCKS AHAHA - - - -XML - created_statuses = subject.call(body, bad_actor) - - expect(created_statuses.first.reblog?).to be true - expect(created_statuses.first.account_id).to eq bad_actor.id - expect(created_statuses.first.reblog.account_id).to eq good_actor.id - expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks' - end - - it 'ignores reblogs if it failed to retrieve reblogged statuses' do - stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404) - - actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - body = < - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - Overwatch rocks - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - - -XML - - created_statuses = subject.call(body, actor) - - expect(created_statuses).to eq [] - end - - it 'ignores statuses with an out-of-order delete' do - sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - delete_body = < - - tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/delete - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - -XML - - status_body = < - - tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - -XML - - subject.call(delete_body, sender) - created_statuses = subject.call(status_body, sender) - - expect(created_statuses).to be_empty - end -end diff --git a/spec/services/process_interaction_service_spec.rb b/spec/services/process_interaction_service_spec.rb deleted file mode 100644 index b858c19d0..000000000 --- a/spec/services/process_interaction_service_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -require 'rails_helper' - -RSpec.describe ProcessInteractionService, type: :service do - let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account } - let(:sender) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } - let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') } - - subject { ProcessInteractionService.new } - - describe 'status delete slap' do - let(:remote_status) { Fabricate(:status, account: remote_sender) } - let(:envelope) { OStatus2::Salmon.new.pack(payload, sender.keypair) } - let(:payload) { - <<~XML - - - carol@localdomain.com - carol - https://webdomain.com/users/carol - - - #{remote_status.id} - http://activitystrea.ms/schema/1.0/delete - - XML - } - - before do - receiver.update(locked: true) - remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) - end - - it 'deletes a record' do - expect(RemovalWorker).to receive(:perform_async).with(remote_status.id) - subject.call(envelope, receiver) - end - end - - describe 'follow request slap' do - before do - receiver.update(locked: true) - - payload = < - - bob - https://cb6e6126.ngrok.io/users/bob - - - someIdHere - http://activitystrea.ms/schema/1.0/request-friend - -XML - - envelope = OStatus2::Salmon.new.pack(payload, sender.keypair) - subject.call(envelope, receiver) - end - - it 'creates a record' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil - end - end - - describe 'follow request slap from known remote user identified by email' do - before do - receiver.update(locked: true) - # Copy already-generated key - remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) - - payload = < - - carol@localdomain.com - carol - https://webdomain.com/users/carol - - - someIdHere - http://activitystrea.ms/schema/1.0/request-friend - -XML - - envelope = OStatus2::Salmon.new.pack(payload, remote_sender.keypair) - subject.call(envelope, receiver) - end - - it 'creates a record' do - expect(FollowRequest.find_by(account: remote_sender, target_account: receiver)).to_not be_nil - end - end - - describe 'follow request authorization slap' do - before do - receiver.update(locked: true) - FollowRequest.create(account: sender, target_account: receiver) - - payload = < - - alice - https://cb6e6126.ngrok.io/users/alice - - - someIdHere - http://activitystrea.ms/schema/1.0/authorize - -XML - - envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) - subject.call(envelope, sender) - end - - it 'creates a follow relationship' do - expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil - end - - it 'removes the follow request' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil - end - end - - describe 'follow request rejection slap' do - before do - receiver.update(locked: true) - FollowRequest.create(account: sender, target_account: receiver) - - payload = < - - alice - https://cb6e6126.ngrok.io/users/alice - - - someIdHere - http://activitystrea.ms/schema/1.0/reject - -XML - - envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) - subject.call(envelope, sender) - end - - it 'does not create a follow relationship' do - expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil - end - - it 'removes the follow request' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil - end - end -end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 8a6bb44ac..35a804f2b 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -18,10 +18,6 @@ RSpec.describe ProcessMentionsService, type: :service do 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 context 'OStatus with private toot' do diff --git a/spec/services/pubsubhubbub/subscribe_service_spec.rb b/spec/services/pubsubhubbub/subscribe_service_spec.rb deleted file mode 100644 index 01c956230..000000000 --- a/spec/services/pubsubhubbub/subscribe_service_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::SubscribeService, type: :service do - describe '#call' do - subject { described_class.new } - let(:user_account) { Fabricate(:account) } - - context 'with a nil account' do - it 'returns the invalid topic status results' do - result = service_call(account: nil) - - expect(result).to eq invalid_topic_status - end - end - - context 'with an invalid callback url' do - it 'returns invalid callback status when callback is blank' do - result = service_call(callback: '') - - expect(result).to eq invalid_callback_status - end - it 'returns invalid callback status when callback is not a URI' do - result = service_call(callback: 'invalid-hostname') - - expect(result).to eq invalid_callback_status - end - end - - context 'with a blocked domain in the callback' do - it 'returns callback not allowed' do - Fabricate(:domain_block, domain: 'test.host', severity: :suspend) - result = service_call(callback: 'https://test.host/api') - - expect(result).to eq not_allowed_callback_status - end - end - - context 'with a valid account and callback' do - it 'returns success status and confirms subscription' do - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - subscription = Fabricate(:subscription, account: user_account) - - result = service_call(callback: subscription.callback_url) - expect(result).to eq success_status - expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'subscribe', 'asdf', 3600) - end - end - end - - def service_call(account: user_account, callback: 'https://callback.host', secret: 'asdf', lease_seconds: 3600) - subject.call(account, callback, secret, lease_seconds) - end - - def invalid_topic_status - ['Invalid topic URL', 422] - end - - def invalid_callback_status - ['Invalid callback URL', 422] - end - - def not_allowed_callback_status - ['Callback URL not allowed', 403] - end - - def success_status - ['', 202] - end -end diff --git a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb b/spec/services/pubsubhubbub/unsubscribe_service_spec.rb deleted file mode 100644 index 7ed9fc5af..000000000 --- a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::UnsubscribeService, type: :service do - describe '#call' do - subject { described_class.new } - - context 'with a nil account' do - it 'returns an invalid topic status' do - result = subject.call(nil, 'callback.host') - - expect(result).to eq invalid_topic_status - end - end - - context 'with a valid account' do - let(:account) { Fabricate(:account) } - - it 'returns a valid topic status and does not run confirm when no subscription' do - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - result = subject.call(account, 'callback.host') - - expect(result).to eq valid_topic_status - expect(Pubsubhubbub::ConfirmationWorker).not_to have_received(:perform_async) - end - - it 'returns a valid topic status and does run confirm when there is a subscription' do - subscription = Fabricate(:subscription, account: account, callback_url: 'callback.host') - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - result = subject.call(account, 'callback.host') - - expect(result).to eq valid_topic_status - expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'unsubscribe') - end - end - - def invalid_topic_status - ['Invalid topic URL', 422] - end - - def valid_topic_status - ['', 202] - end - end -end diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 9d84c41d5..58fb46f0f 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -46,10 +46,6 @@ RSpec.describe ReblogService, type: :service do it 'creates a reblog' do expect(status.reblogs.count).to eq 1 end - - it 'sends a Salmon slap for a remote reblog' do - expect(a_request(:post, 'http://salmon.example.com')).to have_been_made - end end context 'ActivityPub' do diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index e5ac37ed9..1aec060db 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -38,13 +38,6 @@ RSpec.describe RejectFollowService, type: :service do it 'does not create follow relation' do expect(bob.following?(sender)).to be false end - - it 'sends a follow request rejection salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:reject]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 7bba83a60..48191d47c 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -32,23 +32,10 @@ RSpec.describe RemoveStatusService, type: :service do expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) end - it 'sends PuSH update to PuSH subscribers' do - expect(a_request(:post, 'http://example.com/push').with { |req| - req.body.match(OStatus::TagManager::VERBS[:delete]) - }).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) - xml.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.once - end - it 'sends delete activity to rebloggers' do expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 27a85af7c..7a64f4161 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -6,19 +6,13 @@ RSpec.describe ResolveAccountService, type: :service do before do stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404) - stub_request(:get, "https://redirected.com/.well-known/host-meta").to_return(request_fixture('redirected.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:gargron@redirected.com").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker1@redirected.com").to_return(request_fixture('webfinger-hacker1.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker2@redirected.com").to_return(request_fixture('webfinger-hacker2.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "https://localdomain.com/.well-known/host-meta").to_return(request_fixture('localdomain-hostmeta.txt')) - stub_request(:get, "https://localdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(status: 404) - stub_request(:get, "https://webdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(request_fixture('localdomain-webfinger.txt')) - stub_request(:get, "https://webdomain.com/users/foo.atom").to_return(request_fixture('localdomain-feed.txt')) + stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) + stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) + stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) + stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) + stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) end it 'raises error if no such user can be resolved via webfinger' do @@ -29,74 +23,7 @@ RSpec.describe ResolveAccountService, type: :service do expect(subject.call('catsrgr8@example.com')).to be_nil 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' - end - - it 'prevents hijacking inexisting accounts' do - expect(subject.call('hacker2@redirected.com')).to be_nil - end - - 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 - context 'with an ActivityPub account' do - before do - stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) - stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) - stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) - stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) - end - - it 'fallback to OStatus if actor json could not be fetched' do - stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404) - - account = subject.call('foo@ap.example.com') - - expect(account.ostatus?).to eq true - expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' - end - - it 'fallback to OStatus if actor json did not have inbox_url' do - stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt')) - - account = subject.call('foo@ap.example.com') - - expect(account.ostatus?).to eq true - expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' - end - it 'returns new remote account' do account = subject.call('foo@ap.example.com') @@ -124,13 +51,14 @@ RSpec.describe ResolveAccountService, type: :service do it 'processes one remote account at a time using locks' do wait_for_start = true fail_occurred = false - return_values = [] + return_values = Concurrent::Array.new threads = Array.new(5) do Thread.new do true while wait_for_start + begin - return_values << described_class.new.call('foo@localdomain.com') + return_values << described_class.new.call('foo@ap.example.com') rescue ActiveRecord::RecordNotUnique fail_occurred = true end diff --git a/spec/services/send_interaction_service_spec.rb b/spec/services/send_interaction_service_spec.rb deleted file mode 100644 index 710d8184c..000000000 --- a/spec/services/send_interaction_service_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'rails_helper' - -RSpec.describe SendInteractionService, type: :service do - subject { SendInteractionService.new } - - it 'sends an XML envelope to the Salmon end point of remote user' -end diff --git a/spec/services/subscribe_service_spec.rb b/spec/services/subscribe_service_spec.rb deleted file mode 100644 index 10bdb1ba8..000000000 --- a/spec/services/subscribe_service_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'rails_helper' - -RSpec.describe SubscribeService, type: :service do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } - subject { SubscribeService.new } - - it 'sends subscription request to PuSH hub' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - subject.call(account) - expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once - end - - it 'generates and keeps PuSH secret on successful call' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - subject.call(account) - expect(account.secret).to_not be_blank - end - - it 'fails silently if PuSH hub forbids subscription' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 403) - subject.call(account) - end - - it 'fails silently if PuSH hub is not found' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 404) - subject.call(account) - end - - it 'fails loudly if there is a network error' do - stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) - expect { subject.call(account) }.to raise_error HTTP::Error - end - - it 'fails loudly if PuSH hub is unavailable' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 503) - expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError - end - - it 'fails loudly if rate limited' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 429) - expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError - end -end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index 5835b912b..6350c6834 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe UnblockService, type: :service do it 'destroys the blocking relation' do expect(sender.blocking?(bob)).to be false end - - it 'sends an unblock salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:unblock]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index 8a2881ab1..84b5dafbc 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe UnfollowService, type: :service do it 'destroys the following relation' do expect(sender.following?(bob)).to be false end - - it 'sends an unfollow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:unfollow]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/unsubscribe_service_spec.rb b/spec/services/unsubscribe_service_spec.rb deleted file mode 100644 index 54d4b1b53..000000000 --- a/spec/services/unsubscribe_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'rails_helper' - -RSpec.describe UnsubscribeService, type: :service do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } - subject { UnsubscribeService.new } - - it 'removes the secret and resets expiration on account' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 204) - subject.call(account) - account.reload - - expect(account.secret).to be_blank - expect(account.subscription_expires_at).to be_blank - end - - it 'logs error on subscription failure' do - logger = stub_logger - stub_request(:post, 'http://hub.example.com/').to_return(status: 404) - subject.call(account) - - expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) - end - - it 'logs error on connection failure' do - logger = stub_logger - stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) - subject.call(account) - - expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) - end - - def stub_logger - double(debug: nil).tap do |logger| - allow(Rails).to receive(:logger).and_return(logger) - end - end -end diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb deleted file mode 100644 index f3ea70b80..000000000 --- a/spec/services/update_remote_profile_service_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'rails_helper' - -RSpec.describe UpdateRemoteProfileService, type: :service do - let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } - - subject { UpdateRemoteProfileService.new } - - before do - stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt')) - end - - context 'with updated details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - - before do - subject.call(xml, remote_account) - end - - it 'downloads new avatar' do - expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made - end - - it 'sets the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - end - - context 'with unchanged details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') } - - before do - subject.call(xml, remote_account) - end - - it 'does not re-download avatar' do - expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made.once - end - - it 'sets the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - end - - context 'with updated details from a domain set to reject media' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', reject_media: true) } - - before do - subject.call(xml, remote_account) - end - - it 'does not the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to be_nil - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - - it 'does not set store the avatar' do - expect(remote_account.reload.avatar_file_name).to be_nil - end - end -end diff --git a/spec/workers/after_remote_follow_request_worker_spec.rb b/spec/workers/after_remote_follow_request_worker_spec.rb deleted file mode 100644 index bd623cca5..000000000 --- a/spec/workers/after_remote_follow_request_worker_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AfterRemoteFollowRequestWorker do - subject { described_class.new } - let(:follow_request) { Fabricate(:follow_request) } - describe 'perform' do - context 'when the follow_request does not exist' do - it 'catches a raise and returns true' do - allow(FollowService).to receive(:new) - result = subject.perform('aaa') - - expect(result).to eq(true) - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account cannot be updated' do - it 'returns nil and does not call service when account is nil' do - allow(FollowService).to receive(:new) - service = double(call: nil) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - - it 'returns nil and does not call service when account is locked' do - allow(FollowService).to receive(:new) - service = double(call: double(locked?: true)) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account is updated' do - it 'calls the follow service and destroys the follow' do - follow_service = double(call: nil) - allow(FollowService).to receive(:new).and_return(follow_service) - account = Fabricate(:account, locked: false) - service = double(call: account) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(follow_service).to have_received(:call).with(follow_request.account, account.acct) - expect { follow_request.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/workers/after_remote_follow_worker_spec.rb b/spec/workers/after_remote_follow_worker_spec.rb deleted file mode 100644 index d93c469f9..000000000 --- a/spec/workers/after_remote_follow_worker_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AfterRemoteFollowWorker do - subject { described_class.new } - let(:follow) { Fabricate(:follow) } - describe 'perform' do - context 'when the follow does not exist' do - it 'catches a raise and returns true' do - allow(FollowService).to receive(:new) - result = subject.perform('aaa') - - expect(result).to eq(true) - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account cannot be updated' do - it 'returns nil and does not call service when account is nil' do - allow(FollowService).to receive(:new) - service = double(call: nil) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - - it 'returns nil and does not call service when account is not locked' do - allow(FollowService).to receive(:new) - service = double(call: double(locked?: false)) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account is updated' do - it 'calls the follow service and destroys the follow' do - follow_service = double(call: nil) - allow(FollowService).to receive(:new).and_return(follow_service) - account = Fabricate(:account, locked: true) - service = double(call: account) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(follow_service).to have_received(:call).with(follow.account, account.acct) - expect { follow.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb b/spec/workers/pubsubhubbub/confirmation_worker_spec.rb deleted file mode 100644 index 1eecdd2b5..000000000 --- a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::ConfirmationWorker do - include RoutingHelper - - subject { described_class.new } - - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example.com/api', confirmed: false, expires_at: 3.days.from_now, secret: nil) } - - describe 'perform' do - describe 'with subscribe mode' do - it 'confirms and updates subscription when challenge matches' do - stub_random_value - stub_request(:get, url_for_mode('subscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: challenge_value, headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'subscribe', 'asdf', seconds) - - subscription.reload - expect(subscription.secret).to eq 'asdf' - expect(subscription.confirmed).to eq true - expect(subscription.expires_at).to be_within(5).of(10.days.from_now) - end - - it 'does not update subscription when challenge does not match' do - stub_random_value - stub_request(:get, url_for_mode('subscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: 'wrong value', headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'subscribe', 'asdf', seconds) - - subscription.reload - expect(subscription.secret).to be_blank - expect(subscription.confirmed).to eq false - expect(subscription.expires_at).to be_within(5).of(3.days.from_now) - end - end - - describe 'with unsubscribe mode' do - it 'confirms and destroys subscription when challenge matches' do - stub_random_value - stub_request(:get, url_for_mode('unsubscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: challenge_value, headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) - - expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not destroy subscription when challenge does not match' do - stub_random_value - stub_request(:get, url_for_mode('unsubscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: 'wrong value', headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) - - expect { subscription.reload }.not_to raise_error - end - end - end - - def url_for_mode(mode) - "http://example.com/api?hub.challenge=#{challenge_value}&hub.lease_seconds=863999&hub.mode=#{mode}&hub.topic=https://#{Rails.configuration.x.local_domain}/users/alice.atom" - end - - def stub_random_value - allow(SecureRandom).to receive(:hex).and_return(challenge_value) - end - - def challenge_value - '1a2s3d4f' - end - - def http_headers - { 'Connection' => 'close', 'Host' => 'example.com' } - end -end diff --git a/spec/workers/pubsubhubbub/delivery_worker_spec.rb b/spec/workers/pubsubhubbub/delivery_worker_spec.rb deleted file mode 100644 index c0e0d5186..000000000 --- a/spec/workers/pubsubhubbub/delivery_worker_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::DeliveryWorker do - include RoutingHelper - subject { described_class.new } - - let(:payload) { 'test' } - - describe 'perform' do - it 'raises when subscription does not exist' do - expect { subject.perform 123, payload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not attempt to deliver when domain blocked' do - _domain_block = Fabricate(:domain_block, domain: 'example.com', severity: :suspend) - subscription = Fabricate(:subscription, callback_url: 'https://example.com/api', last_successful_delivery_at: 2.days.ago) - - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(2.days.ago) - end - - it 'raises when request fails' do - subscription = Fabricate(:subscription) - - stub_request_to_respond_with(subscription, 500) - expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError - end - - it 'updates subscriptions when delivery succeeds' do - subscription = Fabricate(:subscription) - - stub_request_to_respond_with(subscription, 200) - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) - end - - it 'updates subscription without a secret when delivery succeeds' do - subscription = Fabricate(:subscription, secret: nil) - - stub_request_to_respond_with(subscription, 200) - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) - end - - def stub_request_to_respond_with(subscription, code) - stub_request(:post, 'http://example.com/callback') - .with(body: payload, headers: expected_headers(subscription)) - .to_return(status: code, body: '', headers: {}) - end - - def expected_headers(subscription) - { - 'Connection' => 'close', - 'Content-Type' => 'application/atom+xml', - 'Host' => 'example.com', - 'Link' => "; rel=\"hub\", ; rel=\"self\"", - }.tap do |basic| - known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload) - basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret? - end - end - end -end diff --git a/spec/workers/pubsubhubbub/distribution_worker_spec.rb b/spec/workers/pubsubhubbub/distribution_worker_spec.rb deleted file mode 100644 index 584485079..000000000 --- a/spec/workers/pubsubhubbub/distribution_worker_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' - -describe Pubsubhubbub::DistributionWorker do - subject { Pubsubhubbub::DistributionWorker.new } - - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example2.com') } - let!(:anonymous_subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example1.com', confirmed: true, lease_seconds: 3600) } - let!(:subscription_with_follower) { Fabricate(:subscription, account: alice, callback_url: 'http://example2.com', confirmed: true, lease_seconds: 3600) } - - before do - bob.follow!(alice) - end - - describe 'with public status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :public) } - - it 'delivers payload to all subscriptions' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id]) - end - end - - context 'when OStatus privacy is not used' do - describe 'with private status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) } - - it 'does not deliver anything' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) - end - end - - describe 'with direct status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) } - - it 'does not deliver payload' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) - end - end - end -end diff --git a/spec/workers/scheduler/subscriptions_scheduler_spec.rb b/spec/workers/scheduler/subscriptions_scheduler_spec.rb deleted file mode 100644 index a7d1046de..000000000 --- a/spec/workers/scheduler/subscriptions_scheduler_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -describe Scheduler::SubscriptionsScheduler do - subject { Scheduler::SubscriptionsScheduler.new } - - let!(:expiring_account1) { Fabricate(:account, subscription_expires_at: 20.minutes.from_now, domain: 'example.com', followers_count: 1, hub_url: 'http://hub.example.com') } - let!(:expiring_account2) { Fabricate(:account, subscription_expires_at: 4.hours.from_now, domain: 'example.org', followers_count: 1, hub_url: 'http://hub.example.org') } - - before do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - stub_request(:post, 'http://hub.example.org/').to_return(status: 202) - end - - it 're-subscribes for all expiring accounts' do - subject.perform - expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once - expect(a_request(:post, 'http://hub.example.org/')).to have_been_made.once - end -end -- cgit From e445a8af64908b2bdb721bec74c113e8258a129b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 6 Sep 2019 13:55:51 +0200 Subject: Add timeline read markers API (#11762) Fix #4093 --- app/controllers/api/v1/markers_controller.rb | 44 +++++++++++++++ app/javascript/mastodon/actions/markers.js | 30 ++++++++++ app/javascript/mastodon/features/ui/index.js | 5 +- app/models/marker.rb | 23 ++++++++ app/models/user.rb | 1 + app/serializers/rest/marker_serializer.rb | 13 +++++ config/routes.rb | 1 + db/migrate/20190904222339_create_markers.rb | 14 +++++ db/schema.rb | 14 ++++- spec/controllers/api/v1/markers_controller_spec.rb | 65 ++++++++++++++++++++++ spec/fabricators/marker_fabricator.rb | 6 ++ spec/models/marker_spec.rb | 5 ++ 12 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/markers_controller.rb create mode 100644 app/javascript/mastodon/actions/markers.js create mode 100644 app/models/marker.rb create mode 100644 app/serializers/rest/marker_serializer.rb create mode 100644 db/migrate/20190904222339_create_markers.rb create mode 100644 spec/controllers/api/v1/markers_controller_spec.rb create mode 100644 spec/fabricators/marker_fabricator.rb create mode 100644 spec/models/marker_spec.rb (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb new file mode 100644 index 000000000..28c2ec791 --- /dev/null +++ b/app/controllers/api/v1/markers_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class Api::V1::MarkersController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:index] + before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, except: [:index] + + before_action :require_user! + + def index + @markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker } + render json: serialize_map(@markers) + end + + def create + Marker.transaction do + @markers = {} + + resource_params.each_pair do |timeline, timeline_params| + @markers[timeline] = current_user.markers.find_or_initialize_by(timeline: timeline) + @markers[timeline].update!(timeline_params) + end + end + + render json: serialize_map(@markers) + rescue ActiveRecord::StaleObjectError + render json: { error: 'Conflict during update, please try again' }, status: 409 + end + + private + + def serialize_map(map) + serialized = {} + + map.each_pair do |key, value| + serialized[key] = ActiveModelSerializers::SerializableResource.new(value, serializer: REST::MarkerSerializer).as_json + end + + Oj.dump(serialized) + end + + def resource_params + params.slice(*Marker::TIMELINES).permit(*Marker::TIMELINES.map { |timeline| { timeline.to_sym => [:last_read_id] } }) + end +end diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js new file mode 100644 index 000000000..c3a5fe86f --- /dev/null +++ b/app/javascript/mastodon/actions/markers.js @@ -0,0 +1,30 @@ +export const submitMarkers = () => (dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = {}; + + const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); + const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']); + + if (lastHomeId) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId) { + params.notifications = { + last_read_id: lastNotificationId, + }; + } + + if (Object.keys(params).length === 0) { + return; + } + + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.send(JSON.stringify(params)); +}; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 49c5c8d0e..63c5622b6 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -16,6 +16,7 @@ import { expandNotifications } from '../../actions/notifications'; import { fetchFilters } from '../../actions/filters'; import { clearHeight } from '../../actions/height_cache'; import { focusApp, unfocusApp } from 'mastodon/actions/app'; +import { submitMarkers } from 'mastodon/actions/markers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -241,7 +242,9 @@ class UI extends React.PureComponent { }; handleBeforeUnload = e => { - const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props; + const { intl, dispatch, isComposing, hasComposingText, hasMediaAttachments } = this.props; + + dispatch(submitMarkers()); if (isComposing && (hasComposingText || hasMediaAttachments)) { // Setting returnValue to any string causes confirmation dialog. diff --git a/app/models/marker.rb b/app/models/marker.rb new file mode 100644 index 000000000..a5bd2176a --- /dev/null +++ b/app/models/marker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: markers +# +# id :bigint(8) not null, primary key +# user_id :bigint(8) +# timeline :string default(""), not null +# last_read_id :bigint(8) default(0), not null +# lock_version :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Marker < ApplicationRecord + TIMELINES = %w(home notifications).freeze + + belongs_to :user + + validates :timeline, :last_read_id, presence: true + validates :timeline, inclusion: { in: TIMELINES } +end diff --git a/app/models/user.rb b/app/models/user.rb index a4a20d975..95f1d8fc5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -74,6 +74,7 @@ class User < ApplicationRecord has_many :applications, class_name: 'Doorkeeper::Application', as: :owner has_many :backups, inverse_of: :user has_many :invites, inverse_of: :user + has_many :markers, inverse_of: :user, dependent: :destroy has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? } diff --git a/app/serializers/rest/marker_serializer.rb b/app/serializers/rest/marker_serializer.rb new file mode 100644 index 000000000..2eaf3d507 --- /dev/null +++ b/app/serializers/rest/marker_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::MarkerSerializer < ActiveModel::Serializer + attributes :last_read_id, :version, :updated_at + + def last_read_id + object.last_read_id.to_s + end + + def version + object.lock_version + end +end diff --git a/config/routes.rb b/config/routes.rb index c5326052e..74a162f32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -314,6 +314,7 @@ Rails.application.routes.draw do resources :trends, only: [:index] resources :filters, only: [:index, :create, :show, :update, :destroy] resources :endorsements, only: [:index] + resources :markers, only: [:index, :create] namespace :apps do get :verify_credentials, to: 'credentials#show' diff --git a/db/migrate/20190904222339_create_markers.rb b/db/migrate/20190904222339_create_markers.rb new file mode 100644 index 000000000..71ca70ac3 --- /dev/null +++ b/db/migrate/20190904222339_create_markers.rb @@ -0,0 +1,14 @@ +class CreateMarkers < ActiveRecord::Migration[5.2] + def change + create_table :markers do |t| + t.references :user, foreign_key: { on_delete: :cascade, index: false } + t.string :timeline, default: '', null: false + t.bigint :last_read_id, default: 0, null: false + t.integer :lock_version, default: 0, null: false + + t.timestamps + end + + add_index :markers, [:user_id, :timeline], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 5576f70bf..834dddd7b 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: 2019_09_01_040524) do +ActiveRecord::Schema.define(version: 2019_09_04_222339) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -366,6 +366,17 @@ ActiveRecord::Schema.define(version: 2019_09_01_040524) do t.index ["account_id"], name: "index_lists_on_account_id" end + create_table "markers", force: :cascade do |t| + t.bigint "user_id" + t.string "timeline", default: "", null: false + t.bigint "last_read_id", default: 0, null: false + t.integer "lock_version", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id", "timeline"], name: "index_markers_on_user_id_and_timeline", unique: true + t.index ["user_id"], name: "index_markers_on_user_id" + end + create_table "media_attachments", force: :cascade do |t| t.bigint "status_id" t.string "file_file_name" @@ -791,6 +802,7 @@ ActiveRecord::Schema.define(version: 2019_09_01_040524) do add_foreign_key "list_accounts", "follows", on_delete: :cascade add_foreign_key "list_accounts", "lists", on_delete: :cascade add_foreign_key "lists", "accounts", on_delete: :cascade + add_foreign_key "markers", "users", on_delete: :cascade add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify add_foreign_key "media_attachments", "scheduled_statuses", on_delete: :nullify add_foreign_key "media_attachments", "statuses", on_delete: :nullify diff --git a/spec/controllers/api/v1/markers_controller_spec.rb b/spec/controllers/api/v1/markers_controller_spec.rb new file mode 100644 index 000000000..556a75b9b --- /dev/null +++ b/spec/controllers/api/v1/markers_controller_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +RSpec.describe Api::V1::MarkersController, type: :controller do + render_views + + let!(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses write:statuses') } + + before { allow(controller).to receive(:doorkeeper_token) { token } } + + describe 'GET #index' do + before do + Fabricate(:marker, timeline: 'home', last_read_id: 123, user: user) + Fabricate(:marker, timeline: 'notifications', last_read_id: 456, user: user) + + get :index, params: { timeline: %w(home notifications) } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns markers' do + json = body_as_json + + expect(json.key?(:home)).to be true + expect(json[:home][:last_read_id]).to eq '123' + expect(json.key?(:notifications)).to be true + expect(json[:notifications][:last_read_id]).to eq '456' + end + end + + describe 'POST #create' do + context 'when no marker exists' do + before do + post :create, params: { home: { last_read_id: '69420' } } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'creates a marker' do + expect(user.markers.first.timeline).to eq 'home' + expect(user.markers.first.last_read_id).to eq 69420 + end + end + + context 'when a marker exists' do + before do + post :create, params: { home: { last_read_id: '69420' } } + post :create, params: { home: { last_read_id: '70120' } } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'updates a marker' do + expect(user.markers.first.timeline).to eq 'home' + expect(user.markers.first.last_read_id).to eq 70120 + end + end + end +end diff --git a/spec/fabricators/marker_fabricator.rb b/spec/fabricators/marker_fabricator.rb new file mode 100644 index 000000000..0c94150e0 --- /dev/null +++ b/spec/fabricators/marker_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:marker) do + user + timeline 'home' + last_read_id 0 + lock_version 0 +end diff --git a/spec/models/marker_spec.rb b/spec/models/marker_spec.rb new file mode 100644 index 000000000..d716aa75c --- /dev/null +++ b/spec/models/marker_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Marker, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end -- cgit From 4faaa5b25e54f863fdf3a68edc5ca664e5a84e4c Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 10 Sep 2019 20:56:42 +0200 Subject: Add updated relationship to follow request API responses (#11800) Fixes #11747 --- app/controllers/api/v1/follow_requests_controller.rb | 8 ++++++-- spec/controllers/api/v1/follow_requests_controller_spec.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index e6888154e..0ee6e531f 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -14,12 +14,12 @@ class Api::V1::FollowRequestsController < Api::BaseController def authorize AuthorizeFollowService.new.call(account, current_account) NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account)) - render_empty + render json: account, serializer: REST::RelationshipSerializer, relationships: relationships end def reject RejectFollowService.new.call(account, current_account) - render_empty + render json: account, serializer: REST::RelationshipSerializer, relationships: relationships end private @@ -28,6 +28,10 @@ class Api::V1::FollowRequestsController < Api::BaseController Account.find(params[:id]) end + def relationships(**options) + AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options) + end + def load_accounts default_accounts.merge(paginated_follow_requests).to_a end diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb index 87292d9ce..ae92a9627 100644 --- a/spec/controllers/api/v1/follow_requests_controller_spec.rb +++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb @@ -38,6 +38,12 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do it 'allows follower to follow' do expect(follower.following?(user.account)).to be true end + + it 'returns JSON with followed_by=true' do + json = body_as_json + + expect(json[:followed_by]).to be true + end end describe 'POST #reject' do @@ -54,5 +60,11 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do it 'removes follow request' do expect(FollowRequest.where(target_account: user.account, account: follower).count).to eq 0 end + + it 'returns JSON with followed_by=false' do + json = body_as_json + + expect(json[:followed_by]).to be false + end end end -- cgit From 18331fefa2246facc818226043b1f9cc67cf6c1a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Sep 2019 16:11:13 +0200 Subject: Remove deprecated `GET /api/v1/search` API (#11823) Use `GET /api/v2/search` instead --- app/controllers/api/v1/search_controller.rb | 32 ----------------------- app/controllers/api/v2/search_controller.rb | 28 ++++++++++++++++++-- app/serializers/rest/search_serializer.rb | 7 +---- app/serializers/rest/v2/search_serializer.rb | 7 ----- config/routes.rb | 2 -- spec/controllers/api/v1/search_controller_spec.rb | 22 ---------------- 6 files changed, 27 insertions(+), 71 deletions(-) delete mode 100644 app/controllers/api/v1/search_controller.rb delete mode 100644 app/serializers/rest/v2/search_serializer.rb delete mode 100644 spec/controllers/api/v1/search_controller_spec.rb (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb deleted file mode 100644 index 6131cbbb6..000000000 --- a/app/controllers/api/v1/search_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::SearchController < Api::BaseController - include Authorization - - RESULTS_LIMIT = 20 - - before_action -> { doorkeeper_authorize! :read, :'read:search' } - before_action :require_user! - - respond_to :json - - def index - @search = Search.new(search_results) - render json: @search, serializer: REST::SearchSerializer - end - - private - - def search_results - SearchService.new.call( - params[:q], - current_account, - limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve)) - ) - end - - def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) - end -end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 9aa6edc69..c14cd22d7 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -1,8 +1,32 @@ # frozen_string_literal: true -class Api::V2::SearchController < Api::V1::SearchController +class Api::V2::SearchController < Api::BaseController + include Authorization + + RESULTS_LIMIT = 20 + + before_action -> { doorkeeper_authorize! :read, :'read:search' } + before_action :require_user! + + respond_to :json + def index @search = Search.new(search_results) - render json: @search, serializer: REST::V2::SearchSerializer + render json: @search, serializer: REST::SearchSerializer + end + + private + + def search_results + SearchService.new.call( + params[:q], + current_account, + limit_param(RESULTS_LIMIT), + search_params.merge(resolve: truthy_param?(:resolve)) + ) + end + + def search_params + params.permit(:type, :offset, :min_id, :max_id, :account_id) end end diff --git a/app/serializers/rest/search_serializer.rb b/app/serializers/rest/search_serializer.rb index 157f543ae..ee9b421eb 100644 --- a/app/serializers/rest/search_serializer.rb +++ b/app/serializers/rest/search_serializer.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true class REST::SearchSerializer < ActiveModel::Serializer - attributes :hashtags - has_many :accounts, serializer: REST::AccountSerializer has_many :statuses, serializer: REST::StatusSerializer - - def hashtags - object.hashtags.map(&:name) - end + has_many :hashtags, serializer: REST::TagSerializer end diff --git a/app/serializers/rest/v2/search_serializer.rb b/app/serializers/rest/v2/search_serializer.rb deleted file mode 100644 index cdb6b3a53..000000000 --- a/app/serializers/rest/v2/search_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class REST::V2::SearchSerializer < ActiveModel::Serializer - has_many :accounts, serializer: REST::AccountSerializer - has_many :statuses, serializer: REST::StatusSerializer - has_many :hashtags, serializer: REST::TagSerializer -end diff --git a/config/routes.rb b/config/routes.rb index d22a9e56a..a4dee2842 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -311,8 +311,6 @@ Rails.application.routes.draw do end end - get '/search', to: 'search#index', as: :search - resources :media, only: [:create, :update] resources :blocks, only: [:index] resources :mutes, only: [:index] diff --git a/spec/controllers/api/v1/search_controller_spec.rb b/spec/controllers/api/v1/search_controller_spec.rb deleted file mode 100644 index c9e544cc7..000000000 --- a/spec/controllers/api/v1/search_controller_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::SearchController, type: :controller do - render_views - - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - it 'returns http success' do - get :index, params: { q: 'test' } - - expect(response).to have_http_status(200) - end - end -end -- cgit From b83e2df6b59ccd7cbe8f9145e06b75547dc1101a Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 13 Sep 2019 20:18:25 +0200 Subject: Fix tests being broken because of different default setting --- spec/controllers/api/v1/timelines/public_controller_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'spec/controllers/api/v1') diff --git a/spec/controllers/api/v1/timelines/public_controller_spec.rb b/spec/controllers/api/v1/timelines/public_controller_spec.rb index 737aedba6..b8e9d8674 100644 --- a/spec/controllers/api/v1/timelines/public_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/public_controller_spec.rb @@ -44,6 +44,10 @@ describe Api::V1::Timelines::PublicController do context 'without a user context' do let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil) } + before do + Setting.timeline_preview = true + end + describe 'GET #show' do it 'returns http success' do get :show -- cgit From dfea7368c934f600bd0b6b93b4a6c008a4e265b0 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 13 Nov 2019 23:02:10 +0100 Subject: Add bookmarks (#7107) * Add backend support for bookmarks Bookmarks behave like favourites, except they aren't shared with other users and do not have an associated counter. * Add spec for bookmark endpoints * Add front-end support for bookmarks * Introduce OAuth scopes for bookmarks * Add bookmarks to archive takeout * Fix migration * Coding style fixes * Fix rebase issue * Update bookmarked_statuses to latest UI changes * Update bookmark actions to properly reflect status changes in state * Add bookmarks item to single-column layout * Make active bookmarks red --- app/controllers/api/v1/bookmarks_controller.rb | 67 +++++++++++++ .../api/v1/statuses/bookmarks_controller.rb | 39 ++++++++ app/javascript/mastodon/actions/bookmarks.js | 90 ++++++++++++++++++ app/javascript/mastodon/actions/interactions.js | 80 ++++++++++++++++ .../mastodon/components/status_action_bar.js | 7 ++ .../mastodon/containers/status_container.js | 10 ++ .../mastodon/features/bookmarked_statuses/index.js | 104 +++++++++++++++++++++ .../mastodon/features/getting_started/index.js | 4 +- .../features/status/components/action_bar.js | 7 ++ app/javascript/mastodon/features/status/index.js | 11 +++ .../features/ui/components/columns_area.js | 2 + .../features/ui/components/navigation_panel.js | 1 + app/javascript/mastodon/features/ui/index.js | 2 + .../mastodon/features/ui/util/async-components.js | 4 + app/javascript/mastodon/reducers/status_lists.js | 29 ++++++ app/javascript/mastodon/reducers/statuses.js | 6 ++ app/javascript/styles/mastodon/components.scss | 4 + app/javascript/styles/mastodon/variables.scss | 2 + app/models/bookmark.rb | 26 ++++++ app/models/concerns/account_associations.rb | 1 + app/models/concerns/account_interactions.rb | 4 + app/models/status.rb | 5 + app/presenters/status_relationships_presenter.rb | 2 + app/serializers/rest/status_serializer.rb | 9 ++ app/services/backup_service.rb | 21 +++++ config/initializers/doorkeeper.rb | 2 + config/locales/doorkeeper.en.yml | 2 + config/routes.rb | 4 + db/migrate/20180831171112_create_bookmarks.rb | 17 ++++ db/schema.rb | 12 +++ .../api/v1/bookmarks_controller_spec.rb | 78 ++++++++++++++++ .../api/v1/statuses/bookmarks_controller_spec.rb | 57 +++++++++++ spec/fabricators/bookmark_fabricator.rb | 4 + 33 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/bookmarks_controller.rb create mode 100644 app/controllers/api/v1/statuses/bookmarks_controller.rb create mode 100644 app/javascript/mastodon/actions/bookmarks.js create mode 100644 app/javascript/mastodon/features/bookmarked_statuses/index.js create mode 100644 app/models/bookmark.rb create mode 100644 db/migrate/20180831171112_create_bookmarks.rb create mode 100644 spec/controllers/api/v1/bookmarks_controller_spec.rb create mode 100644 spec/controllers/api/v1/statuses/bookmarks_controller_spec.rb create mode 100644 spec/fabricators/bookmark_fabricator.rb (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb new file mode 100644 index 000000000..cf4cba8dd --- /dev/null +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class Api::V1::BookmarksController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:bookmarks' } + before_action :require_user! + after_action :insert_pagination_headers + + respond_to :json + + def index + @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + end + + private + + def load_statuses + cached_bookmarks + end + + def cached_bookmarks + cache_collection( + Status.reorder(nil).joins(:bookmarks).merge(results), + Status + ) + end + + def results + @_results ||= account_bookmarks.paginate_by_max_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def account_bookmarks + current_account.bookmarks + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_bookmarks_url pagination_params(since_id: pagination_since_id) unless results.empty? + end + + def pagination_max_id + results.last.id + end + + def pagination_since_id + results.first.id + end + + def records_continue? + results.size == limit_param(DEFAULT_STATUSES_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb new file mode 100644 index 000000000..bb9729cf5 --- /dev/null +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::BookmarksController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' } + before_action :require_user! + + respond_to :json + + def create + @status = bookmarked_status + render json: @status, serializer: REST::StatusSerializer + end + + def destroy + @status = requested_status + @bookmarks_map = { @status.id => false } + + bookmark = Bookmark.find_by!(account: current_user.account, status: @status) + bookmark.destroy! + + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, bookmarks_map: @bookmarks_map) + end + + private + + def bookmarked_status + authorize_with current_user.account, requested_status, :show? + + bookmark = Bookmark.find_or_create_by!(account: current_user.account, status: requested_status) + + bookmark.status.reload + end + + def requested_status + Status.find(params[:status_id]) + end +end diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js new file mode 100644 index 000000000..544ed2ff2 --- /dev/null +++ b/app/javascript/mastodon/actions/bookmarks.js @@ -0,0 +1,90 @@ +import api, { getLinks } from '../api'; +import { importFetchedStatuses } from './importer'; + +export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; +export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; +export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL'; + +export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST'; +export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS'; +export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL'; + +export function fetchBookmarkedStatuses() { + return (dispatch, getState) => { + if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) { + return; + } + + dispatch(fetchBookmarkedStatusesRequest()); + + api(getState).get('/api/v1/bookmarks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchBookmarkedStatusesFail(error)); + }); + }; +}; + +export function fetchBookmarkedStatusesRequest() { + return { + type: BOOKMARKED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchBookmarkedStatusesSuccess(statuses, next) { + return { + type: BOOKMARKED_STATUSES_FETCH_SUCCESS, + statuses, + next, + }; +}; + +export function fetchBookmarkedStatusesFail(error) { + return { + type: BOOKMARKED_STATUSES_FETCH_FAIL, + error, + }; +}; + +export function expandBookmarkedStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) { + return; + } + + dispatch(expandBookmarkedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandBookmarkedStatusesFail(error)); + }); + }; +}; + +export function expandBookmarkedStatusesRequest() { + return { + type: BOOKMARKED_STATUSES_EXPAND_REQUEST, + }; +}; + +export function expandBookmarkedStatusesSuccess(statuses, next) { + return { + type: BOOKMARKED_STATUSES_EXPAND_SUCCESS, + statuses, + next, + }; +}; + +export function expandBookmarkedStatusesFail(error) { + return { + type: BOOKMARKED_STATUSES_EXPAND_FAIL, + error, + }; +}; diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 2dc4c574c..28c6b1a62 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -33,6 +33,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST'; export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; export const UNPIN_FAIL = 'UNPIN_FAIL'; +export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST'; +export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS'; +export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL'; + +export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; +export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; +export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; + export function reblog(status) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -187,6 +195,78 @@ export function unfavouriteFail(status, error) { }; }; +export function bookmark(status) { + return function (dispatch, getState) { + dispatch(bookmarkRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) { + dispatch(importFetchedStatus(response.data)); + dispatch(bookmarkSuccess(status, response.data)); + }).catch(function (error) { + dispatch(bookmarkFail(status, error)); + }); + }; +}; + +export function unbookmark(status) { + return (dispatch, getState) => { + dispatch(unbookmarkRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unbookmarkSuccess(status, response.data)); + }).catch(error => { + dispatch(unbookmarkFail(status, error)); + }); + }; +}; + +export function bookmarkRequest(status) { + return { + type: BOOKMARK_REQUEST, + status: status, + }; +}; + +export function bookmarkSuccess(status, response) { + return { + type: BOOKMARK_SUCCESS, + status: status, + response: response, + }; +}; + +export function bookmarkFail(status, error) { + return { + type: BOOKMARK_FAIL, + status: status, + error: error, + }; +}; + +export function unbookmarkRequest(status) { + return { + type: UNBOOKMARK_REQUEST, + status: status, + }; +}; + +export function unbookmarkSuccess(status, response) { + return { + type: UNBOOKMARK_SUCCESS, + status: status, + response: response, + }; +}; + +export function unbookmarkFail(status, error) { + return { + type: UNBOOKMARK_FAIL, + status: status, + error: error, + }; +}; + export function fetchReblogs(id) { return (dispatch, getState) => { dispatch(fetchReblogsRequest(id)); diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0bfbd8879..4fa2c1158 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -23,6 +23,7 @@ const messages = defineMessages({ cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, @@ -66,6 +67,7 @@ class StatusActionBar extends ImmutablePureComponent { onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, + onBookmark: PropTypes.func, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -114,6 +116,10 @@ class StatusActionBar extends ImmutablePureComponent { window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } + handleBookmarkClick = () => { + this.props.onBookmark(this.props.status); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -253,6 +259,7 @@ class StatusActionBar extends ImmutablePureComponent { {shareButton} +
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index fb22676e0..16ba02e12 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -9,8 +9,10 @@ import { import { reblog, favourite, + bookmark, unreblog, unfavourite, + unbookmark, pin, unpin, } from '../actions/interactions'; @@ -90,6 +92,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onBookmark (status) { + if (status.get('bookmarked')) { + dispatch(unbookmark(status)); + } else { + dispatch(bookmark(status)); + } + }, + onPin (status) { if (status.get('pinned')) { dispatch(unpin(status)); diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.js b/app/javascript/mastodon/features/bookmarked_statuses/index.js new file mode 100644 index 000000000..c37cb9176 --- /dev/null +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks'; +import Column from '../ui/components/column'; +import ColumnHeader from '../../components/column_header'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import StatusList from '../../components/status_list'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { debounce } from 'lodash'; + +const messages = defineMessages({ + heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, +}); + +const mapStateToProps = state => ({ + statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), + hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Bookmarks extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + statusIds: ImmutablePropTypes.list.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + }; + + componentWillMount () { + this.props.dispatch(fetchBookmarkedStatuses()); + } + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('BOOKMARKS', {})); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + + handleLoadMore = debounce(() => { + this.props.dispatch(expandBookmarkedStatuses()); + }, 300, { leading: true }) + + render () { + const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; + const pinned = !!columnId; + + const emptyMessage = ; + + return ( + + + + + + ); + } + +} diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 67ec7665b..adbc147d1 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -22,6 +22,7 @@ const messages = defineMessages({ settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, @@ -126,11 +127,12 @@ class GettingStarted extends ImmutablePureComponent { navItems.push( , + , , ); - height += 48*3; + height += 48*4; if (myAccount.get('locked') || unreadFollowRequests > 0) { navItems.push(); diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 3e511b7a6..1b81cd245 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -17,6 +17,7 @@ const messages = defineMessages({ cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, @@ -43,6 +44,7 @@ class ActionBar extends React.PureComponent { onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, + onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -67,6 +69,10 @@ class ActionBar extends React.PureComponent { this.props.onFavourite(this.props.status); } + handleBookmarkClick = (e) => { + this.props.onBookmark(this.props.status, e); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -198,6 +204,7 @@ class ActionBar extends React.PureComponent {
{shareButton} +
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 029057d40..9fb3fe305 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -13,6 +13,8 @@ import Column from '../ui/components/column'; import { favourite, unfavourite, + bookmark, + unbookmark, reblog, unreblog, pin, @@ -232,6 +234,14 @@ class Status extends ImmutablePureComponent { } } + handleBookmarkClick = (status) => { + if (status.get('bookmarked')) { + this.props.dispatch(unbookmark(status)); + } else { + this.props.dispatch(bookmark(status)); + } + } + handleDeleteClick = (status, history, withRedraft = false) => { const { dispatch, intl } = this.props; @@ -499,6 +509,7 @@ class Status extends ImmutablePureComponent { onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} + onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index f31425c70..8bc4bfc0e 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -21,6 +21,7 @@ import { HashtagTimeline, DirectTimeline, FavouritedStatuses, + BookmarkedStatuses, ListTimeline, Directory, } from '../../ui/util/async-components'; @@ -40,6 +41,7 @@ const componentMap = { 'HASHTAG': HashtagTimeline, 'DIRECT': DirectTimeline, 'FAVOURITES': FavouritedStatuses, + 'BOOKMARKS': BookmarkedStatuses, 'LIST': ListTimeline, 'DIRECTORY': Directory, }; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 51e3ec037..0c12852f5 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -17,6 +17,7 @@ const NavigationPanel = () => ( + {profile_directory && } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index a45ba91eb..b0e38c5cb 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -41,6 +41,7 @@ import { FollowRequests, GenericNotFound, FavouritedStatuses, + BookmarkedStatuses, ListTimeline, Blocks, DomainBlocks, @@ -190,6 +191,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index bb0fcb859..986efda1e 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -90,6 +90,10 @@ export function FavouritedStatuses () { return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); } +export function BookmarkedStatuses () { + return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses'); +} + export function Blocks () { return import(/* webpackChunkName: "features/blocks" */'../../blocks'); } diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 6c5f33557..9f8f28dee 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -6,6 +6,14 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_FAIL, } from '../actions/favourites'; +import { + BOOKMARKED_STATUSES_FETCH_REQUEST, + BOOKMARKED_STATUSES_FETCH_SUCCESS, + BOOKMARKED_STATUSES_FETCH_FAIL, + BOOKMARKED_STATUSES_EXPAND_REQUEST, + BOOKMARKED_STATUSES_EXPAND_SUCCESS, + BOOKMARKED_STATUSES_EXPAND_FAIL, +} from '../actions/bookmarks'; import { PINNED_STATUSES_FETCH_SUCCESS, } from '../actions/pin_statuses'; @@ -13,6 +21,8 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { FAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS, + BOOKMARK_SUCCESS, + UNBOOKMARK_SUCCESS, PIN_SUCCESS, UNPIN_SUCCESS, } from '../actions/interactions'; @@ -23,6 +33,11 @@ const initialState = ImmutableMap({ loaded: false, items: ImmutableList(), }), + bookmarks: ImmutableMap({ + next: null, + loaded: false, + items: ImmutableList(), + }), pins: ImmutableMap({ next: null, loaded: false, @@ -71,10 +86,24 @@ export default function statusLists(state = initialState, action) { return normalizeList(state, 'favourites', action.statuses, action.next); case FAVOURITED_STATUSES_EXPAND_SUCCESS: return appendToList(state, 'favourites', action.statuses, action.next); + case BOOKMARKED_STATUSES_FETCH_REQUEST: + case BOOKMARKED_STATUSES_EXPAND_REQUEST: + return state.setIn(['bookmarks', 'isLoading'], true); + case BOOKMARKED_STATUSES_FETCH_FAIL: + case BOOKMARKED_STATUSES_EXPAND_FAIL: + return state.setIn(['bookmarks', 'isLoading'], false); + case BOOKMARKED_STATUSES_FETCH_SUCCESS: + return normalizeList(state, 'bookmarks', action.statuses, action.next); + case BOOKMARKED_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'bookmarks', action.statuses, action.next); case FAVOURITE_SUCCESS: return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: return removeOneFromList(state, 'favourites', action.status); + case BOOKMARK_SUCCESS: + return prependOneToList(state, 'bookmarks', action.status); + case UNBOOKMARK_SUCCESS: + return removeOneFromList(state, 'bookmarks', action.status); case PINNED_STATUSES_FETCH_SUCCESS: return normalizeList(state, 'pins', action.statuses, action.next); case PIN_SUCCESS: diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 372673bc0..772f98bcb 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -4,6 +4,8 @@ import { FAVOURITE_REQUEST, FAVOURITE_FAIL, UNFAVOURITE_SUCCESS, + BOOKMARK_REQUEST, + BOOKMARK_FAIL, } from '../actions/interactions'; import { STATUS_MUTE_SUCCESS, @@ -43,6 +45,10 @@ export default function statuses(state = initialState, action) { return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1); case FAVOURITE_FAIL: return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false); + case BOOKMARK_REQUEST: + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true); + case BOOKMARK_FAIL: + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false); case REBLOG_REQUEST: return state.setIn([action.status.get('id'), 'reblogged'], true); case REBLOG_FAIL: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ded668aea..98ffe1c20 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1592,6 +1592,10 @@ a.account__display-name { color: $gold-star; } +.bookmark-icon.active { + color: $red-bookmark; +} + .no-reduce-motion .icon-button.star-icon { &.activate { & > .fa-star { diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index a82c44229..8602c3dde 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -6,6 +6,8 @@ $error-red: #df405a !default; // Cerise $warning-red: #ff5050 !default; // Sunset Orange $gold-star: #ca8f04 !default; // Dark Goldenrod +$red-bookmark: $warning-red; + // Values from the classic Mastodon UI $classic-base-color: #282c37; // Midnight Express $classic-primary-color: #9baec8; // Echo Blue diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb new file mode 100644 index 000000000..01dc48ee7 --- /dev/null +++ b/app/models/bookmark.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: bookmarks +# +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# status_id :integer not null +# + +class Bookmark < ApplicationRecord + include Paginable + + update_index('statuses#status', :status) if Chewy.enabled? + + belongs_to :account, inverse_of: :bookmarks + belongs_to :status, inverse_of: :bookmarks + + validates :status_id, uniqueness: { scope: :account_id } + + before_validation do + self.status = status.reblog if status&.reblog? + end +end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index c9cc5c610..499edbf4e 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -13,6 +13,7 @@ module AccountAssociations # Timelines has_many :statuses, inverse_of: :account, dependent: :destroy has_many :favourites, inverse_of: :account, dependent: :destroy + has_many :bookmarks, inverse_of: :account, dependent: :destroy has_many :mentions, inverse_of: :account, dependent: :destroy has_many :notifications, inverse_of: :account, dependent: :destroy has_many :conversations, class_name: 'AccountConversation', dependent: :destroy, inverse_of: :account diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index ad2909d91..f27d39483 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -186,6 +186,10 @@ module AccountInteractions status.proper.favourites.where(account: self).exists? end + def bookmarked?(status) + status.proper.bookmarks.where(account: self).exists? + end + def reblogged?(status) status.proper.reblogs.where(account: self).exists? end diff --git a/app/models/status.rb b/app/models/status.rb index 0c01a5389..1cb381400 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -54,6 +54,7 @@ class Status < ApplicationRecord belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true has_many :favourites, inverse_of: :status, dependent: :destroy + has_many :bookmarks, inverse_of: :status, dependent: :destroy has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread has_many :mentions, dependent: :destroy, inverse_of: :status @@ -302,6 +303,10 @@ class Status < ApplicationRecord Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true } end + def bookmarks_map(status_ids, account_id) + Bookmark.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h + end + def reblogs_map(status_ids, account_id) unscoped.select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).each_with_object({}) { |s, h| h[s.reblog_of_id] = true } end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index b04e10e2f..64e688d87 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -7,6 +7,7 @@ class StatusRelationshipsPresenter if current_account_id.nil? @reblogs_map = {} @favourites_map = {} + @bookmarks_map = {} @mutes_map = {} @pins_map = {} else @@ -17,6 +18,7 @@ class StatusRelationshipsPresenter @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] || {}) + @bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_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 diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 2dc4a1b61..08bc4d82a 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -9,6 +9,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? attribute :muted, if: :current_user? + attribute :bookmarked, if: :current_user? attribute :pinned, if: :pinnable? attribute :content, unless: :source_requested? @@ -93,6 +94,14 @@ class REST::StatusSerializer < ActiveModel::Serializer end end + def bookmarked + if instance_options && instance_options[:bookmarks] + instance_options[:bookmarks].bookmarks_map[object.id] || false + else + current_user.account.bookmarked?(object) + end + end + def pinned if instance_options && instance_options[:relationships] instance_options[:relationships].pins_map[object.id] || false diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 12e4fa8b4..fe26d7aa0 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -45,6 +45,7 @@ class BackupService < BaseService dump_media_attachments!(tar) dump_outbox!(tar) dump_likes!(tar) + dump_bookmarks!(tar) dump_actor!(tar) end end @@ -85,6 +86,7 @@ class BackupService < BaseService actor[:image][:url] = 'header' + File.extname(actor[:image][:url]) if actor[:image] actor[:outbox] = 'outbox.json' actor[:likes] = 'likes.json' + actor[:bookmarks] = 'bookmarks.json' download_to_tar(tar, account.avatar, 'avatar' + File.extname(account.avatar.path)) if account.avatar.exists? download_to_tar(tar, account.header, 'header' + File.extname(account.header.path)) if account.header.exists? @@ -115,6 +117,25 @@ class BackupService < BaseService end end + def dump_bookmarks!(tar) + collection = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + + Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses| + statuses.each do |status| + collection[:totalItems] += 1 + collection[:orderedItems] << ActivityPub::TagManager.instance.uri_for(status) + end + + GC.start + end + + json = Oj.dump(collection) + + tar.add_file_simple('bookmarks.json', 0o444, json.bytesize) do |io| + io.write(json) + end + end + def collection_presenter ActivityPub::CollectionPresenter.new( id: 'outbox.json', diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 914b3c001..a5c9caa4a 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -58,6 +58,7 @@ Doorkeeper.configure do optional_scopes :write, :'write:accounts', :'write:blocks', + :'write:bookmarks', :'write:conversations', :'write:favourites', :'write:filters', @@ -71,6 +72,7 @@ Doorkeeper.configure do :read, :'read:accounts', :'read:blocks', + :'read:bookmarks', :'read:favourites', :'read:filters', :'read:follows', diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index d9b7c2c8e..4e9c83a8f 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -125,6 +125,7 @@ en: read: read all your account's data read:accounts: see accounts information read:blocks: see your blocks + read:bookmarks: see your bookmarks read:favourites: see your favourites read:filters: see your filters read:follows: see your follows @@ -137,6 +138,7 @@ en: write: modify all your account's data write:accounts: modify your profile write:blocks: block accounts and domains + write:bookmarks: bookmark statuses write:favourites: favourite statuses write:filters: create filters write:follows: follow people diff --git a/config/routes.rb b/config/routes.rb index e43e201a5..5411cff58 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -289,6 +289,9 @@ Rails.application.routes.draw do resource :favourite, only: :create post :unfavourite, to: 'favourites#destroy' + resource :bookmark, only: :create + post :unbookmark, to: 'bookmarks#destroy' + resource :mute, only: :create post :unmute, to: 'mutes#destroy' @@ -324,6 +327,7 @@ Rails.application.routes.draw do resources :blocks, only: [:index] resources :mutes, only: [:index] resources :favourites, only: [:index] + resources :bookmarks, only: [:index] resources :reports, only: [:create] resources :trends, only: [:index] resources :filters, only: [:index, :create, :show, :update, :destroy] diff --git a/db/migrate/20180831171112_create_bookmarks.rb b/db/migrate/20180831171112_create_bookmarks.rb new file mode 100644 index 000000000..27c7339c9 --- /dev/null +++ b/db/migrate/20180831171112_create_bookmarks.rb @@ -0,0 +1,17 @@ +class CreateBookmarks < ActiveRecord::Migration[5.1] + def change + create_table :bookmarks do |t| + t.references :account, null: false + t.references :status, null: false + + t.timestamps + end + + safety_assured do + add_foreign_key :bookmarks, :accounts, column: :account_id, on_delete: :cascade + add_foreign_key :bookmarks, :statuses, column: :status_id, on_delete: :cascade + end + + add_index :bookmarks, [:account_id, :status_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 1bcc003a5..84c76e4ce 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -217,6 +217,16 @@ ActiveRecord::Schema.define(version: 2019_10_31_163205) do t.index ["target_account_id"], name: "index_blocks_on_target_account_id" end + create_table "bookmarks", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "status_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "status_id"], name: "index_bookmarks_on_account_id_and_status_id", unique: true + t.index ["account_id"], name: "index_bookmarks_on_account_id" + t.index ["status_id"], name: "index_bookmarks_on_status_id" + end + create_table "conversation_mutes", force: :cascade do |t| t.bigint "conversation_id", null: false t.bigint "account_id", null: false @@ -834,6 +844,8 @@ ActiveRecord::Schema.define(version: 2019_10_31_163205) do add_foreign_key "backups", "users", on_delete: :nullify add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade + add_foreign_key "bookmarks", "accounts", on_delete: :cascade + add_foreign_key "bookmarks", "statuses", on_delete: :cascade add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade add_foreign_key "custom_filters", "accounts", on_delete: :cascade diff --git a/spec/controllers/api/v1/bookmarks_controller_spec.rb b/spec/controllers/api/v1/bookmarks_controller_spec.rb new file mode 100644 index 000000000..79601b6e6 --- /dev/null +++ b/spec/controllers/api/v1/bookmarks_controller_spec.rb @@ -0,0 +1,78 @@ +require 'rails_helper' + +RSpec.describe Api::V1::BookmarksController, type: :controller do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:bookmarks') } + + describe 'GET #index' do + context 'without token' do + it 'returns http unauthorized' do + get :index + expect(response).to have_http_status :unauthorized + end + end + + context 'with token' do + context 'without read scope' do + before do + allow(controller).to receive(:doorkeeper_token) do + Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: '') + end + end + + it 'returns http forbidden' do + get :index + expect(response).to have_http_status :forbidden + end + end + + context 'without valid resource owner' do + before do + token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') + user.destroy! + + allow(controller).to receive(:doorkeeper_token) { token } + end + + it 'returns http unprocessable entity' do + get :index + expect(response).to have_http_status :unprocessable_entity + end + end + + context 'with read scope and valid resource owner' do + before do + allow(controller).to receive(:doorkeeper_token) do + Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') + end + end + + it 'shows bookmarks owned by the user' do + bookmarked_by_user = Fabricate(:bookmark, account: user.account) + bookmarked_by_others = Fabricate(:bookmark) + + get :index + + expect(assigns(:statuses)).to match_array [bookmarked_by_user.status] + end + + it 'adds pagination headers if necessary' do + bookmark = Fabricate(:bookmark, account: user.account) + + get :index, params: { limit: 1 } + + expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq "http://test.host/api/v1/bookmarks?limit=1&max_id=#{bookmark.id}" + expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/bookmarks?limit=1&since_id=#{bookmark.id}" + end + + it 'does not add pagination headers if not necessary' do + get :index + + expect(response.headers['Link']).to eq nil + end + end + end + end +end diff --git a/spec/controllers/api/v1/statuses/bookmarks_controller_spec.rb b/spec/controllers/api/v1/statuses/bookmarks_controller_spec.rb new file mode 100644 index 000000000..b79853718 --- /dev/null +++ b/spec/controllers/api/v1/statuses/bookmarks_controller_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Statuses::BookmarksController 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:bookmarks', 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 bookmarked attribute' do + expect(user.account.bookmarked?(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.to_s + expect(hash_body[:bookmarked]).to be true + end + end + + describe 'POST #destroy' do + let(:status) { Fabricate(:status, account: user.account) } + + before do + Bookmark.find_or_create_by!(account: user.account, status: status) + post :destroy, params: { status_id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates the bookmarked attribute' do + expect(user.account.bookmarked?(status)).to be false + end + end + end +end diff --git a/spec/fabricators/bookmark_fabricator.rb b/spec/fabricators/bookmark_fabricator.rb new file mode 100644 index 000000000..12cbc5bfa --- /dev/null +++ b/spec/fabricators/bookmark_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:bookmark) do + account + status +end -- cgit From fd93a9c871e0c758603b5a939dc192aeefddab1d Mon Sep 17 00:00:00 2001 From: Jennifer Glauche <=^.^=@github19.jglauche.de> Date: Sat, 16 Nov 2019 19:02:09 +0100 Subject: make it not return http 400 when passing and empty source argument (#12259) * make it not return http 400 when passing and empty source argument * create a spec for the empty source hash bug * compact checks for nil, empty? parameters * use nil.blank? instead checking for nil --- app/controllers/api/v1/accounts/credentials_controller.rb | 2 +- .../api/v1/accounts/credentials_controller_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index e77f57910..64b5cb747 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController end def user_settings_params - return nil unless params.key?(:source) + return nil if params[:source].blank? source_params = params.require(:source) diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index 19ac32612..ebd462a03 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -59,6 +59,19 @@ describe Api::V1::Accounts::CredentialsController do end end + describe 'with empty source list' do + before do + patch :update, params: { + display_name: "I'm a cat", + source: {}, + }, as: :json + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + describe 'with invalid data' do before do patch :update, params: { note: 'This is too long. ' * 30 } -- cgit From 5a2c0707f163f14565d186db48bf0b4fe0b05b4f Mon Sep 17 00:00:00 2001 From: Gomasy Date: Mon, 18 Nov 2019 01:09:42 +0900 Subject: Support min_id-based pagination for bookmarks (#12381) * Support min_id-based pagination for bookmarks * Fix spec --- app/controllers/api/v1/bookmarks_controller.rb | 7 +++---- spec/controllers/api/v1/bookmarks_controller_spec.rb | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'spec/controllers/api/v1') diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index cf4cba8dd..e1b244e76 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -26,10 +26,9 @@ class Api::V1::BookmarksController < Api::BaseController end def results - @_results ||= account_bookmarks.paginate_by_max_id( + @_results ||= account_bookmarks.paginate_by_id( limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] + params_slice(:max_id, :since_id, :min_id) ) end @@ -46,7 +45,7 @@ class Api::V1::BookmarksController < Api::BaseController end def prev_path - api_v1_bookmarks_url pagination_params(since_id: pagination_since_id) unless results.empty? + api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty? end def pagination_max_id diff --git a/spec/controllers/api/v1/bookmarks_controller_spec.rb b/spec/controllers/api/v1/bookmarks_controller_spec.rb index 79601b6e6..d7c5847b0 100644 --- a/spec/controllers/api/v1/bookmarks_controller_spec.rb +++ b/spec/controllers/api/v1/bookmarks_controller_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Api::V1::BookmarksController, type: :controller do get :index, params: { limit: 1 } expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq "http://test.host/api/v1/bookmarks?limit=1&max_id=#{bookmark.id}" - expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/bookmarks?limit=1&since_id=#{bookmark.id}" + expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq "http://test.host/api/v1/bookmarks?limit=1&min_id=#{bookmark.id}" end it 'does not add pagination headers if not necessary' do -- cgit