diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index 1d3992a285..320084efb5 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action :require_user!
def index
- accounts = Account.where(id: account_ids).select('id')
+ scope = Account.where(id: account_ids).select('id')
+ scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
# .where doesn't guarantee that our results are in the same order
# we requested them, so return the "right" order to the requestor.
- @accounts = accounts.index_by(&:id).values_at(*account_ids).compact
+ @accounts = scope.index_by(&:id).values_at(*account_ids).compact
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
end
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index 4a985a41ef..e0448f004c 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) {
dispatch(fetchRelationshipsRequest(newAccountIds));
- api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
+ api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx
index aa18ce79a5..f82dd9153a 100644
--- a/app/javascript/mastodon/components/account.jsx
+++ b/app/javascript/mastodon/components/account.jsx
@@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
buttons = ;
} else if (defaultAction === 'block') {
buttons = ;
- } else if (!account.get('moved') || following) {
+ } else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = ;
}
}
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
index e546c75693..7594135a4e 100644
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ b/app/javascript/mastodon/features/account/components/header.jsx
@@ -289,7 +289,7 @@ class Header extends ImmutablePureComponent {
lockedIcon = ;
}
- if (signedIn && account.get('id') !== me) {
+ if (signedIn && account.get('id') !== me && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
@@ -299,7 +299,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
}
- if ('share' in navigator) {
+ if ('share' in navigator && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null);
}
@@ -347,7 +347,9 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
}
- menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
+ if (!account.get('suspended')) {
+ menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
+ }
}
if (signedIn && isRemote) {
@@ -395,7 +397,7 @@ class Header extends ImmutablePureComponent {
- {!suspended && info}
+ {info}
{!(suspended || hidden) &&
}
@@ -407,18 +409,16 @@ class Header extends ImmutablePureComponent {
- {!suspended && (
-
- {!hidden && (
- <>
- {actionBtn}
- {bellBtn}
- >
- )}
+
+ {!hidden && (
+ <>
+ {actionBtn}
+ {bellBtn}
+ >
+ )}
-
-
- )}
+
+
diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx
index f88e9042ef..bef14ea276 100644
--- a/app/javascript/mastodon/features/video/index.jsx
+++ b/app/javascript/mastodon/features/video/index.jsx
@@ -469,6 +469,10 @@ class Video extends PureComponent {
};
_syncVideoToVolumeState = (volume = null, muted = null) => {
+ if (!this.video) {
+ return;
+ }
+
this.video.volume = volume ?? this.state.volume;
this.video.muted = muted ?? this.state.muted;
};
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 319b386645..9a2743ed5b 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -43,6 +43,7 @@ end
Sidekiq.logger.level = ::Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s)
SidekiqUniqueJobs.configure do |config|
+ config.enabled = !Rails.env.test?
config.reaper = :ruby
config.reaper_count = 1000
config.reaper_interval = 600
diff --git a/config/routes/api.rb b/config/routes/api.rb
index f4e4b204ad..0ac9173e3e 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -303,6 +303,10 @@ namespace :api, format: false do
resources :statuses, only: [:show, :destroy]
end
+ namespace :accounts do
+ resources :relationships, only: :index
+ end
+
namespace :admin do
resources :accounts, only: [:index]
end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index cc9e3198b6..542a748784 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -7,66 +7,44 @@ RSpec.describe AccountsController do
let(:account) { Fabricate(:account) }
- shared_examples 'unapproved account check' do
+ describe 'unapproved account check' do
before { account.user.update(approved: false) }
it 'returns http not found' do
- get :show, params: { username: account.username, format: format }
-
- expect(response).to have_http_status(404)
+ %w(html json rss).each do |format|
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(404)
+ end
end
end
- shared_examples 'permanently suspended account check' do
+ describe 'permanently suspended account check' do
before do
account.suspend!
account.deletion_request.destroy
end
it 'returns http gone' do
- get :show, params: { username: account.username, format: format }
-
- expect(response).to have_http_status(410)
+ %w(html json rss).each do |format|
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(410)
+ end
end
end
- shared_examples 'temporarily suspended account check' do |code: 403|
+ describe 'temporarily suspended account check' do
before { account.suspend! }
it 'returns appropriate http response code' do
- get :show, params: { username: account.username, format: format }
+ { html: 403, json: 200, rss: 403 }.each do |format, code|
+ get :show, params: { username: account.username, format: format }
- expect(response).to have_http_status(code)
+ expect(response).to have_http_status(code)
+ end
end
end
describe 'GET #show' do
- context 'with basic account status checks' do
- context 'with HTML' do
- let(:format) { 'html' }
-
- it_behaves_like 'unapproved account check'
- it_behaves_like 'permanently suspended account check'
- it_behaves_like 'temporarily suspended account check'
- end
-
- context 'with JSON' do
- let(:format) { 'json' }
-
- it_behaves_like 'unapproved account check'
- it_behaves_like 'permanently suspended account check'
- it_behaves_like 'temporarily suspended account check', code: 200
- end
-
- context 'with RSS' do
- let(:format) { 'rss' }
-
- it_behaves_like 'unapproved account check'
- it_behaves_like 'permanently suspended account check'
- it_behaves_like 'temporarily suspended account check'
- end
- end
-
context 'with existing statuses' do
let!(:status) { Fabricate(:status, account: account) }
let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) }
@@ -227,22 +205,15 @@ RSpec.describe AccountsController do
context 'with RSS' do
let(:format) { 'rss' }
- shared_examples 'common RSS response' do
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
- end
-
context 'with a normal account in an RSS request' do
before do
get :show, params: { username: account.username, format: format }
end
- it_behaves_like 'common RSS response'
+ it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses', :aggregate_failures do
+ expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_media)
expect(response.body).to include_status_tag(status_self_reply)
expect(response.body).to include_status_tag(status)
@@ -259,9 +230,10 @@ RSpec.describe AccountsController do
get :show, params: { username: account.username, format: format }
end
- it_behaves_like 'common RSS response'
+ it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with replies', :aggregate_failures do
+ expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_media)
expect(response.body).to include_status_tag(status_reply)
expect(response.body).to include_status_tag(status_self_reply)
@@ -278,9 +250,10 @@ RSpec.describe AccountsController do
get :show, params: { username: account.username, format: format }
end
- it_behaves_like 'common RSS response'
+ it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with media', :aggregate_failures do
+ expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_media)
expect(response.body).to_not include_status_tag(status_direct)
expect(response.body).to_not include_status_tag(status_private)
@@ -302,9 +275,10 @@ RSpec.describe AccountsController do
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
- it_behaves_like 'common RSS response'
+ it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with a tag', :aggregate_failures do
+ expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_tag)
expect(response.body).to_not include_status_tag(status_direct)
expect(response.body).to_not include_status_tag(status_media)
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
deleted file mode 100644
index 5ba6f2a1f8..0000000000
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Accounts::RelationshipsController do
- render_views
-
- let(:user) { Fabricate(:user) }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
-
- describe 'GET #index' do
- let(:simon) { Fabricate(:account) }
- let(:lewis) { Fabricate(:account) }
-
- before do
- user.account.follow!(simon)
- lewis.follow!(user.account)
- end
-
- context 'when provided only one ID' do
- before do
- get :index, params: { id: simon.id }
- end
-
- it 'returns JSON with correct data', :aggregate_failures do
- json = body_as_json
-
- expect(response).to have_http_status(200)
- expect(json).to be_a Enumerable
- expect(json.first[:following]).to be true
- expect(json.first[:followed_by]).to be false
- end
- end
-
- context 'when provided multiple IDs' do
- before do
- get :index, params: { id: [simon.id, lewis.id] }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- context 'when there is returned JSON data' do
- let(:json) { body_as_json }
-
- it 'returns an enumerable json with correct elements', :aggregate_failures do
- expect(json).to be_a Enumerable
-
- expect_simon_item_one
- expect_lewis_item_two
- end
-
- def expect_simon_item_one
- expect(json.first[:id]).to eq simon.id.to_s
- expect(json.first[:following]).to be true
- expect(json.first[:showing_reblogs]).to be true
- expect(json.first[:followed_by]).to be false
- expect(json.first[:muting]).to be false
- expect(json.first[:requested]).to be false
- expect(json.first[:domain_blocking]).to be false
- end
-
- def expect_lewis_item_two
- expect(json.second[:id]).to eq lewis.id.to_s
- expect(json.second[:following]).to be false
- expect(json.second[:showing_reblogs]).to be false
- expect(json.second[:followed_by]).to be true
- expect(json.second[:muting]).to be false
- expect(json.second[:requested]).to be false
- expect(json.second[:domain_blocking]).to be false
- end
- end
-
- it 'returns JSON with correct data on cached requests too' do
- get :index, params: { id: [simon.id] }
-
- json = body_as_json
-
- expect(json).to be_a Enumerable
- expect(json.first[:following]).to be true
- expect(json.first[:showing_reblogs]).to be true
- end
-
- it 'returns JSON with correct data after change too' do
- user.account.unfollow!(simon)
-
- get :index, params: { id: [simon.id] }
-
- json = body_as_json
-
- expect(json).to be_a Enumerable
- expect(json.first[:following]).to be false
- expect(json.first[:showing_reblogs]).to be false
- end
- end
- end
-end
diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb
new file mode 100644
index 0000000000..bb78e3b3e4
--- /dev/null
+++ b/spec/requests/api/v1/accounts/relationships_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'GET /api/v1/accounts/relationships' do
+ subject do
+ get '/api/v1/accounts/relationships', headers: headers, params: params
+ end
+
+ let(:user) { Fabricate(:user) }
+ let(:scopes) { 'read:follows' }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ let(:simon) { Fabricate(:account) }
+ let(:lewis) { Fabricate(:account) }
+ let(:bob) { Fabricate(:account, suspended: true) }
+
+ before do
+ user.account.follow!(simon)
+ lewis.follow!(user.account)
+ end
+
+ context 'when provided only one ID' do
+ let(:params) { { id: simon.id } }
+
+ it 'returns JSON with correct data', :aggregate_failures do
+ subject
+
+ json = body_as_json
+
+ expect(response).to have_http_status(200)
+ expect(json).to be_a Enumerable
+ expect(json.first[:following]).to be true
+ expect(json.first[:followed_by]).to be false
+ end
+ end
+
+ context 'when provided multiple IDs' do
+ let(:params) { { id: [simon.id, lewis.id, bob.id] } }
+
+ context 'when there is returned JSON data' do
+ let(:json) { body_as_json }
+
+ context 'with default parameters' do
+ it 'returns an enumerable json with correct elements, excluding suspended accounts', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(json).to be_a Enumerable
+ expect(json.size).to eq 2
+
+ expect_simon_item_one
+ expect_lewis_item_two
+ end
+ end
+
+ context 'with `with_suspended` parameter' do
+ let(:params) { { id: [simon.id, lewis.id, bob.id], with_suspended: true } }
+
+ it 'returns an enumerable json with correct elements, including suspended accounts', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(json).to be_a Enumerable
+ expect(json.size).to eq 3
+
+ expect_simon_item_one
+ expect_lewis_item_two
+ expect_bob_item_three
+ end
+ end
+
+ def expect_simon_item_one
+ expect(json.first[:id]).to eq simon.id.to_s
+ expect(json.first[:following]).to be true
+ expect(json.first[:showing_reblogs]).to be true
+ expect(json.first[:followed_by]).to be false
+ expect(json.first[:muting]).to be false
+ expect(json.first[:requested]).to be false
+ expect(json.first[:domain_blocking]).to be false
+ end
+
+ def expect_lewis_item_two
+ expect(json.second[:id]).to eq lewis.id.to_s
+ expect(json.second[:following]).to be false
+ expect(json.second[:showing_reblogs]).to be false
+ expect(json.second[:followed_by]).to be true
+ expect(json.second[:muting]).to be false
+ expect(json.second[:requested]).to be false
+ expect(json.second[:domain_blocking]).to be false
+ end
+
+ def expect_bob_item_three
+ expect(json.third[:id]).to eq bob.id.to_s
+ expect(json.third[:following]).to be false
+ expect(json.third[:showing_reblogs]).to be false
+ expect(json.third[:followed_by]).to be false
+ expect(json.third[:muting]).to be false
+ expect(json.third[:requested]).to be false
+ expect(json.third[:domain_blocking]).to be false
+ end
+ end
+
+ it 'returns JSON with correct data on cached requests too' do
+ subject
+ subject
+
+ expect(response).to have_http_status(200)
+
+ json = body_as_json
+
+ expect(json).to be_a Enumerable
+ expect(json.first[:following]).to be true
+ expect(json.first[:showing_reblogs]).to be true
+ end
+
+ it 'returns JSON with correct data after change too' do
+ subject
+ user.account.unfollow!(simon)
+
+ get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] }
+
+ expect(response).to have_http_status(200)
+
+ json = body_as_json
+
+ expect(json).to be_a Enumerable
+ expect(json.first[:following]).to be false
+ expect(json.first[:showing_reblogs]).to be false
+ end
+ end
+end