Allow viewing and severing relationships with suspended accounts (#27667)
This commit is contained in:
		@@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
 | 
			
		||||
  before_action :require_user!
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    accounts = Account.without_suspended.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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
 | 
			
		||||
        buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
 | 
			
		||||
      } else if (defaultAction === 'block') {
 | 
			
		||||
        buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
 | 
			
		||||
      } else if (!account.get('moved') || following) {
 | 
			
		||||
      } else if (!account.get('suspended') && !account.get('moved') || following) {
 | 
			
		||||
        buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -289,7 +289,7 @@ class Header extends ImmutablePureComponent {
 | 
			
		||||
      lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 {
 | 
			
		||||
 | 
			
		||||
        <div className='account__header__image'>
 | 
			
		||||
          <div className='account__header__info'>
 | 
			
		||||
            {!suspended && info}
 | 
			
		||||
            {info}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
 | 
			
		||||
@@ -407,18 +409,16 @@ class Header extends ImmutablePureComponent {
 | 
			
		||||
              <Avatar account={suspended || hidden ? undefined : account} size={90} />
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            {!suspended && (
 | 
			
		||||
              <div className='account__header__tabs__buttons'>
 | 
			
		||||
                {!hidden && (
 | 
			
		||||
                  <>
 | 
			
		||||
                    {actionBtn}
 | 
			
		||||
                    {bellBtn}
 | 
			
		||||
                  </>
 | 
			
		||||
                )}
 | 
			
		||||
            <div className='account__header__tabs__buttons'>
 | 
			
		||||
              {!hidden && (
 | 
			
		||||
                <>
 | 
			
		||||
                  {actionBtn}
 | 
			
		||||
                  {bellBtn}
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
              <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='account__header__tabs__name'>
 | 
			
		||||
 
 | 
			
		||||
@@ -301,6 +301,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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
							
								
								
									
										133
									
								
								spec/requests/api/v1/accounts/relationships_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								spec/requests/api/v1/accounts/relationships_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
		Reference in New Issue
	
	Block a user