Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit b796d6b

Browse files
authored
Merge pull request #41 from MozillaSocial/admin-action-statuses
Add Admin-level API for actioning Statuses
2 parents 20952f9 + 3eacba2 commit b796d6b

8 files changed

Lines changed: 308 additions & 2 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
class Api::V1::Admin::StatusActionsController < Api::BaseController
4+
# modeled on api/v1/admin/account_actions_controller.rb
5+
6+
include Authorization
7+
8+
# only support a subset of StatusBatchAction types
9+
ALLOWED_TYPES = %w(
10+
delete
11+
sensitive
12+
).freeze
13+
14+
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:statuses' }
15+
before_action :set_status
16+
17+
after_action :verify_authorized
18+
19+
def create
20+
authorize [:admin, @status], :update?
21+
raise ActiveRecord::RecordInvalid unless valid_type?
22+
23+
status_batch_action = Admin::StatusBatchAction.new(resource_params)
24+
status_batch_action.status_ids = [@status.id]
25+
status_batch_action.current_account = current_account
26+
status_batch_action.save!
27+
28+
render_empty
29+
end
30+
31+
private
32+
33+
def valid_type?
34+
params[:type] && ALLOWED_TYPES.include?(params[:type])
35+
end
36+
37+
def set_status
38+
@status = Status.find(params[:status_id])
39+
end
40+
41+
def resource_params
42+
params.permit(
43+
:type,
44+
:report_id,
45+
:text,
46+
:send_email_notification
47+
)
48+
end
49+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
class Api::V1::Admin::StatusesController < Api::BaseController
4+
# modeled on api/v1/admin/accounts_controller.rb
5+
6+
include Authorization
7+
include AccountableConcern
8+
9+
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:statuses' }, only: [:show]
10+
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:statuses' }, except: [:show]
11+
before_action :set_status
12+
13+
after_action :verify_authorized
14+
15+
def show
16+
authorize [:admin, @status], :show?
17+
render json: @status, serializer: REST::StatusSerializer
18+
end
19+
20+
def destroy
21+
# modeled on handle_delete from status_batch_action.rb
22+
authorize [:admin, @status], :destroy?
23+
ApplicationRecord.transaction do
24+
@status.discard_with_reblogs
25+
log_action :destroy, @status
26+
Tombstone.find_or_create_by(uri: @status.uri, account: @status.account, by_moderator: true)
27+
end
28+
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
29+
30+
RemovalWorker.perform_async(@status.id, { 'preserve' => @status.account.local?, 'immediate' => !@status.account.local? })
31+
32+
render json: json
33+
end
34+
35+
def unsensitive
36+
# modeled on undo_mark_statuses_as_sensitive from approve_appeal_service.rb
37+
authorize [:admin, @status], :update?
38+
representative_account = Account.representative
39+
ApplicationRecord.transaction do
40+
UpdateStatusService.new.call(@status, representative_account.id, sensitive: false) if @status.with_media?
41+
log_action :unsensitive, @status
42+
end
43+
render json: @status, serializer: REST::StatusSerializer
44+
end
45+
46+
private
47+
48+
def set_status
49+
@status = Status.find(params[:id])
50+
end
51+
end

app/models/admin/status_batch_action.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def process_action!
3131
case type
3232
when 'delete'
3333
handle_delete!
34-
when 'mark_as_sensitive'
34+
when 'mark_as_sensitive', 'sensitive'
3535
handle_mark_as_sensitive!
3636
when 'report'
3737
handle_report!

app/models/status.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def non_sensitive_with_media?
273273
end
274274

275275
def reported?
276-
@reported ||= Report.where(target_account: account).unresolved.where('? = ANY(status_ids)', id).exists?
276+
@reported ||= Report.where(target_account: account).where('? = ANY(status_ids)', id).exists?
277277
end
278278

279279
def emojis

config/initializers/doorkeeper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
:'admin:read',
9999
:'admin:read:accounts',
100100
:'admin:read:reports',
101+
:'admin:read:statuses',
101102
:'admin:read:domain_allows',
102103
:'admin:read:domain_blocks',
103104
:'admin:read:ip_blocks',
@@ -106,6 +107,7 @@
106107
:'admin:write',
107108
:'admin:write:accounts',
108109
:'admin:write:reports',
110+
:'admin:write:statuses',
109111
:'admin:write:domain_allows',
110112
:'admin:write:domain_blocks',
111113
:'admin:write:ip_blocks',

config/routes/api.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@
217217
resource :action, only: [:create], controller: 'account_actions'
218218
end
219219

220+
resources :statuses, only: [:show, :destroy] do
221+
member do
222+
post :unsensitive
223+
end
224+
225+
resource :action, only: [:create], controller: 'status_actions'
226+
end
227+
220228
resources :reports, only: [:index, :update, :show] do
221229
member do
222230
post :assign_to_self
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Api::V1::Admin::StatusesController do
6+
render_views
7+
8+
let(:role) { UserRole.find_by(name: 'Moderator') }
9+
let(:user) { Fabricate(:user, role: role) }
10+
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
11+
12+
before do
13+
allow(controller).to receive(:doorkeeper_token) { token }
14+
end
15+
16+
describe 'GET #show' do
17+
let(:scopes) { 'admin:read:statuses' }
18+
let(:status) { Fabricate(:status) }
19+
20+
before do
21+
get :show, params: { id: status.id }
22+
end
23+
24+
it_behaves_like 'forbidden for wrong scope', 'read:statuses' # non-admin scope
25+
it_behaves_like 'forbidden for wrong role', ''
26+
27+
it 'returns http success' do
28+
expect(response).to have_http_status(200)
29+
end
30+
end
31+
32+
describe 'DELETE #destroy' do
33+
let(:scopes) { 'admin:write:statuses' }
34+
let(:status) { Fabricate(:status) }
35+
36+
before do
37+
post :destroy, params: { id: status.id }
38+
end
39+
40+
it_behaves_like 'forbidden for wrong scope', 'admin:read:statuses'
41+
it_behaves_like 'forbidden for wrong scope', 'write:statuses' # non-admin scope
42+
it_behaves_like 'forbidden for wrong role', ''
43+
44+
it 'returns http success' do
45+
expect(response).to have_http_status(200)
46+
end
47+
48+
it 'removes the status' do
49+
expect(Status.find_by(id: status.id)).to be_nil
50+
end
51+
end
52+
53+
describe 'POST #unsensitive' do
54+
let(:scopes) { 'admin:write:statuses' }
55+
let(:media) { Fabricate(:media_attachment) }
56+
let(:status) { Fabricate(:status, media_attachments: [media], sensitive: true) }
57+
58+
before do
59+
post :unsensitive, params: { id: status.id }
60+
end
61+
62+
it_behaves_like 'forbidden for wrong scope', 'admin:read:statuses'
63+
it_behaves_like 'forbidden for wrong scope', 'write:statuses' # non-admin scope
64+
it_behaves_like 'forbidden for wrong role', ''
65+
66+
it 'returns http success' do
67+
expect(response).to have_http_status(200)
68+
end
69+
70+
it 'unmarks status as sensitive' do
71+
expect(status.reload.sensitive?).to be false
72+
end
73+
end
74+
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Status actions' do
6+
let(:role) { UserRole.find_by(name: 'Moderator') }
7+
let(:user) { Fabricate(:user, role: role) }
8+
let(:scopes) { 'admin:write admin:write:statuses' }
9+
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
10+
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
11+
let(:mailer) { instance_double(ActionMailer::MessageDelivery, deliver_later!: nil) }
12+
13+
before do
14+
allow(UserMailer).to receive(:warning).with(status.account.user, anything).and_return(mailer)
15+
end
16+
17+
shared_examples 'a successful notification delivery' do
18+
it 'notifies the user about the action taken' do
19+
subject
20+
21+
expect(UserMailer).to have_received(:warning).with(status.account.user, anything).once
22+
expect(mailer).to have_received(:deliver_later!).once
23+
end
24+
end
25+
26+
shared_examples 'a successful logged action' do |action_type, target_type|
27+
it 'logs action' do
28+
subject
29+
30+
log_item = Admin::ActionLog.where(action: action_type).last
31+
32+
expect(log_item).to be_present
33+
expect(log_item.account_id).to eq(user.account_id)
34+
expect(log_item.target_id).to eq(target_type == :status ? status.id : report.id)
35+
end
36+
end
37+
38+
describe 'POST /api/v1/admin/statuses/:id/action' do
39+
subject do
40+
post "/api/v1/admin/statuses/#{status.id}/action", headers: headers, params: params
41+
end
42+
43+
let(:account) { Fabricate(:account, domain: nil) } # local account for email notification
44+
let(:media) { Fabricate(:media_attachment) }
45+
let(:status) { Fabricate(:status, media_attachments: [media], account: account) }
46+
47+
context 'with type of delete' do
48+
let(:params) { { type: 'delete' } }
49+
50+
it_behaves_like 'forbidden for wrong scope', 'admin:read:statuses'
51+
it_behaves_like 'forbidden for wrong scope', 'write:statuses' # non-admin scope
52+
it_behaves_like 'forbidden for wrong role', ''
53+
it_behaves_like 'a successful logged action', :destroy, :status
54+
55+
it 'returns http success' do
56+
subject
57+
58+
expect(response).to have_http_status(200)
59+
end
60+
61+
it 'deletes the status' do
62+
expect { subject }.to change { Status.find_by(id: status.id) }.from(status).to(nil)
63+
end
64+
end
65+
66+
context 'with type of sensitive' do
67+
let(:params) { { type: 'sensitive' } }
68+
69+
it_behaves_like 'forbidden for wrong scope', 'admin:read:statuses'
70+
it_behaves_like 'forbidden for wrong scope', 'write:statuses' # non-admin scope
71+
it_behaves_like 'forbidden for wrong role', ''
72+
it_behaves_like 'a successful logged action', :update, :status
73+
74+
it 'returns http success' do
75+
subject
76+
77+
expect(response).to have_http_status(200)
78+
end
79+
80+
it 'marks the status as sensitive' do
81+
expect { subject }.to change { status.reload.sensitive? }.from(false).to(true)
82+
end
83+
end
84+
85+
context 'with no type' do
86+
let(:params) { {} }
87+
88+
it 'returns http unprocessable entity' do
89+
subject
90+
91+
expect(response).to have_http_status(422)
92+
end
93+
end
94+
95+
context 'with invalid type' do
96+
let(:params) { { type: 'invalid' } }
97+
98+
it 'returns http unprocessable entity' do
99+
subject
100+
101+
expect(response).to have_http_status(422)
102+
end
103+
end
104+
105+
context 'with notification delivery' do
106+
let(:params) { { type: 'delete', send_email_notification: true } }
107+
108+
it_behaves_like 'a successful notification delivery'
109+
end
110+
111+
context 'with report' do
112+
let(:report) { Fabricate(:report) }
113+
let(:params) { { type: 'delete', report_id: report.id } }
114+
115+
it_behaves_like 'a successful logged action', :resolve, :report
116+
117+
it 'resolves report' do
118+
expect { subject }.to change { report.reload.unresolved? }.from(true).to(false)
119+
end
120+
end
121+
end
122+
end

0 commit comments

Comments
 (0)