* Add moderation warnings Replace individual routes for disabling, silencing, and suspending a user, as well as the report update route, with a unified account action controller that allows you to select an action (none, disable, silence, suspend) as well as whether it should generate an e-mail notification with optional custom text. That notification, with the optional custom text, is saved as a warning. Additionally, there are warning presets you can configure to save time when performing the above. * Use Account#local_username_and_domainpull/4/head
@ -0,0 +1,36 @@ | |||||
# frozen_string_literal: true | |||||
module Admin | |||||
class AccountActionsController < BaseController | |||||
before_action :set_account | |||||
def new | |||||
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true) | |||||
@warning_presets = AccountWarningPreset.all | |||||
end | |||||
def create | |||||
account_action = Admin::AccountAction.new(resource_params) | |||||
account_action.target_account = @account | |||||
account_action.current_account = current_account | |||||
account_action.save! | |||||
if account_action.with_report? | |||||
redirect_to admin_report_path(account_action.report) | |||||
else | |||||
redirect_to admin_account_path(@account.id) | |||||
end | |||||
end | |||||
private | |||||
def set_account | |||||
@account = Account.find(params[:account_id]) | |||||
end | |||||
def resource_params | |||||
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification) | |||||
end | |||||
end | |||||
end |
@ -1,27 +0,0 @@ | |||||
# frozen_string_literal: true | |||||
module Admin | |||||
class SilencesController < BaseController | |||||
before_action :set_account | |||||
def create | |||||
authorize @account, :silence? | |||||
@account.update!(silenced: true) | |||||
log_action :silence, @account | |||||
redirect_to admin_accounts_path | |||||
end | |||||
def destroy | |||||
authorize @account, :unsilence? | |||||
@account.update!(silenced: false) | |||||
log_action :unsilence, @account | |||||
redirect_to admin_accounts_path | |||||
end | |||||
private | |||||
def set_account | |||||
@account = Account.find(params[:account_id]) | |||||
end | |||||
end | |||||
end |
@ -1,60 +0,0 @@ | |||||
# frozen_string_literal: true | |||||
module Admin | |||||
class SuspensionsController < BaseController | |||||
before_action :set_account | |||||
def new | |||||
@suspension = Form::AdminSuspensionConfirmation.new(report_id: params[:report_id]) | |||||
end | |||||
def create | |||||
authorize @account, :suspend? | |||||
@suspension = Form::AdminSuspensionConfirmation.new(suspension_params) | |||||
if suspension_params[:acct] == @account.acct | |||||
resolve_report! if suspension_params[:report_id].present? | |||||
perform_suspend! | |||||
mark_reports_resolved! | |||||
redirect_to admin_accounts_path | |||||
else | |||||
flash.now[:alert] = I18n.t('admin.suspensions.bad_acct_msg') | |||||
render :new | |||||
end | |||||
end | |||||
def destroy | |||||
authorize @account, :unsuspend? | |||||
@account.unsuspend! | |||||
log_action :unsuspend, @account | |||||
redirect_to admin_accounts_path | |||||
end | |||||
private | |||||
def set_account | |||||
@account = Account.find(params[:account_id]) | |||||
end | |||||
def suspension_params | |||||
params.require(:form_admin_suspension_confirmation).permit(:acct, :report_id) | |||||
end | |||||
def resolve_report! | |||||
report = Report.find(suspension_params[:report_id]) | |||||
report.resolve!(current_account) | |||||
log_action :resolve, report | |||||
end | |||||
def perform_suspend! | |||||
@account.suspend! | |||||
Admin::SuspensionWorker.perform_async(@account.id) | |||||
log_action :suspend, @account | |||||
end | |||||
def mark_reports_resolved! | |||||
Report.where(target_account: @account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id) | |||||
end | |||||
end | |||||
end |
@ -0,0 +1,58 @@ | |||||
# frozen_string_literal: true | |||||
module Admin | |||||
class WarningPresetsController < BaseController | |||||
before_action :set_warning_preset, except: [:index, :create] | |||||
def index | |||||
authorize :account_warning_preset, :index? | |||||
@warning_presets = AccountWarningPreset.all | |||||
@warning_preset = AccountWarningPreset.new | |||||
end | |||||
def create | |||||
authorize :account_warning_preset, :create? | |||||
@warning_preset = AccountWarningPreset.new(warning_preset_params) | |||||
if @warning_preset.save | |||||
redirect_to admin_warning_presets_path | |||||
else | |||||
@warning_presets = AccountWarningPreset.all | |||||
render :index | |||||
end | |||||
end | |||||
def edit | |||||
authorize @warning_preset, :update? | |||||
end | |||||
def update | |||||
authorize @warning_preset, :update? | |||||
if @warning_preset.update(warning_preset_params) | |||||
redirect_to admin_warning_presets_path | |||||
else | |||||
render :edit | |||||
end | |||||
end | |||||
def destroy | |||||
authorize @warning_preset, :destroy? | |||||
@warning_preset.destroy! | |||||
redirect_to admin_warning_presets_path | |||||
end | |||||
private | |||||
def set_warning_preset | |||||
@warning_preset = AccountWarningPreset.find(params[:id]) | |||||
end | |||||
def warning_preset_params | |||||
params.require(:account_warning_preset).permit(:text) | |||||
end | |||||
end | |||||
end |
@ -0,0 +1,4 @@ | |||||
<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> | |||||
<path d="M0 0h24v24H0z" fill="none"/> | |||||
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/> | |||||
</svg> |
@ -0,0 +1,23 @@ | |||||
# frozen_string_literal: true | |||||
# == Schema Information | |||||
# | |||||
# Table name: account_warnings | |||||
# | |||||
# id :bigint(8) not null, primary key | |||||
# account_id :bigint(8) | |||||
# target_account_id :bigint(8) | |||||
# action :integer default("none"), not null | |||||
# text :text default(""), not null | |||||
# created_at :datetime not null | |||||
# updated_at :datetime not null | |||||
# | |||||
class AccountWarning < ApplicationRecord | |||||
enum action: %i(none disable silence suspend), _suffix: :action | |||||
belongs_to :account, inverse_of: :account_warnings | |||||
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings | |||||
scope :latest, -> { order(created_at: :desc) } | |||||
scope :custom, -> { where.not(text: '') } | |||||
end |
@ -0,0 +1,15 @@ | |||||
# frozen_string_literal: true | |||||
# == Schema Information | |||||
# | |||||
# Table name: account_warning_presets | |||||
# | |||||
# id :bigint(8) not null, primary key | |||||
# text :text default(""), not null | |||||
# created_at :datetime not null | |||||
# updated_at :datetime not null | |||||
# | |||||
class AccountWarningPreset < ApplicationRecord | |||||
validates :text, presence: true | |||||
end |
@ -0,0 +1,134 @@ | |||||
# frozen_string_literal: true | |||||
class Admin::AccountAction | |||||
include ActiveModel::Model | |||||
include AccountableConcern | |||||
include Authorization | |||||
TYPES = %w( | |||||
none | |||||
disable | |||||
silence | |||||
suspend | |||||
).freeze | |||||
attr_accessor :target_account, | |||||
:current_account, | |||||
:type, | |||||
:text, | |||||
:report_id, | |||||
:warning_preset_id, | |||||
:send_email_notification | |||||
attr_reader :warning | |||||
def save! | |||||
ApplicationRecord.transaction do | |||||
process_action! | |||||
process_warning! | |||||
end | |||||
queue_email! | |||||
process_reports! | |||||
end | |||||
def report | |||||
@report ||= Report.find(report_id) if report_id.present? | |||||
end | |||||
def with_report? | |||||
!report.nil? | |||||
end | |||||
class << self | |||||
def types_for_account(account) | |||||
if account.local? | |||||
TYPES | |||||
else | |||||
TYPES - %w(none disable) | |||||
end | |||||
end | |||||
end | |||||
private | |||||
def process_action! | |||||
case type | |||||
when 'disable' | |||||
handle_disable! | |||||
when 'silence' | |||||
handle_silence! | |||||
when 'suspend' | |||||
handle_suspend! | |||||
end | |||||
end | |||||
def process_warning! | |||||
return unless warnable? | |||||
authorize(target_account, :warn?) | |||||
@warning = AccountWarning.create!(target_account: target_account, | |||||
account: current_account, | |||||
action: type, | |||||
text: text_for_warning) | |||||
# A log entry is only interesting if the warning contains | |||||
# custom text from someone. Otherwise it's just noise. | |||||
log_action(:create, warning) if warning.text.present? | |||||
end | |||||
def process_reports! | |||||
return if report_id.blank? | |||||
authorize(report, :update?) | |||||
if type == 'none' | |||||
log_action(:resolve, report) | |||||
report.resolve!(current_account) | |||||
else | |||||
Report.where(target_account: target_account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id) | |||||
end | |||||
end | |||||
def handle_disable! | |||||
authorize(target_account.user, :disable?) | |||||
log_action(:disable, target_account.user) | |||||
target_account.user&.disable! | |||||
end | |||||
def handle_silence! | |||||
authorize(target_account, :silence?) | |||||
log_action(:silence, target_account) | |||||
target_account.silence! | |||||
end | |||||
def handle_suspend! | |||||
authorize(target_account, :suspend?) | |||||
log_action(:suspend, target_account) | |||||
target_account.suspend! | |||||
queue_suspension_worker! | |||||
end | |||||
def text_for_warning | |||||
[warning_preset&.text, text].compact.join("\n\n") | |||||
end | |||||
def queue_suspension_worker! | |||||
Admin::SuspensionWorker.perform_async(target_account.id) | |||||
end | |||||
def queue_email! | |||||
return unless warnable? | |||||
UserMailer.warning(target_account.user, warning).deliver_later! | |||||
end | |||||
def warnable? | |||||
send_email_notification && target_account.local? | |||||
end | |||||
def warning_preset | |||||
@warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present? | |||||
end | |||||
end |
@ -1,7 +0,0 @@ | |||||
# frozen_string_literal: true | |||||
class Form::AdminSuspensionConfirmation | |||||
include ActiveModel::Model | |||||
attr_accessor :acct, :report_id | |||||
end |
@ -0,0 +1,19 @@ | |||||
# frozen_string_literal: true | |||||
class AccountWarningPresetPolicy < ApplicationPolicy | |||||
def index? | |||||
staff? | |||||
end | |||||
def create? | |||||
staff? | |||||
end | |||||
def update? | |||||
staff? | |||||
end | |||||
def destroy? | |||||
staff? | |||||
end | |||||
end |
@ -0,0 +1,26 @@ | |||||
- content_for :page_title do | |||||
= t('admin.account_actions.title', acct: @account.acct) | |||||
= simple_form_for @account_action, url: admin_account_action_path(@account.id) do |f| | |||||
= f.input :report_id, as: :hidden | |||||
.fields-group | |||||
= f.input :type, collection: Admin::AccountAction.types_for_account(@account), include_blank: false, wrapper: :with_block_label, label_method: ->(type) { I18n.t("simple_form.labels.admin_account_action.types.#{type}")}, hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.acct) | |||||
- if @account.local? | |||||
%hr.spacer/ | |||||
.fields-group | |||||
= f.input :send_email_notification, as: :boolean, wrapper: :with_label | |||||
%hr.spacer/ | |||||
- unless @warning_presets.empty? | |||||
.fields-group | |||||
= f.input :warning_preset_id, collection: @warning_presets, label_method: :text, wrapper: :with_block_label | |||||
.fields-group | |||||
= f.input :text, as: :text, wrapper: :with_block_label, hint: t('simple_form.hints.admin_account_action.text_html', path: admin_warning_presets_path) | |||||
.actions | |||||
= f.button :button, t('admin.account_actions.action'), type: :submit |
@ -0,0 +1,6 @@ | |||||
.speech-bubble.warning | |||||
.speech-bubble__bubble | |||||
= Formatter.instance.linkify(account_warning.text) | |||||
.speech-bubble__owner | |||||
= admin_account_link_to account_warning.account | |||||
%time.formatted{ datetime: account_warning.created_at.iso8601 }= l account_warning.created_at |
@ -1,25 +0,0 @@ | |||||
- content_for :page_title do | |||||
= t('admin.suspensions.title', acct: @account.acct) | |||||
= simple_form_for @suspension, url: admin_account_suspension_path(@account.id), method: :post do |f| | |||||
%p.hint= t('admin.suspensions.warning_html') | |||||
.fields-group | |||||
%ul | |||||
%li.negative-hint | |||||
= number_to_human @account.statuses_count, strip_insignificant_zeros: true | |||||
= t('accounts.posts', count: @account.statuses_count) | |||||
%li.negative-hint | |||||
= number_to_human @account.following_count, strip_insignificant_zeros: true | |||||
= t('accounts.following', count: @account.following_count) | |||||
%li.negative-hint | |||||
= number_to_human @account.followers_count, strip_insignificant_zeros: true | |||||
= t('accounts.followers', count: @account.followers_count) | |||||
%p.hint= t('admin.suspensions.hint_html', value: content_tag(:code, @account.acct)) | |||||
= f.input :acct | |||||
= f.input_field :report_id, as: :hidden | |||||
.actions | |||||
= f.button :button, t('admin.suspensions.proceed'), type: :submit, class: 'negative' |
@ -0,0 +1,11 @@ | |||||
- content_for :page_title do | |||||
= t('admin.warning_presets.edit_preset') | |||||
= simple_form_for @warning_preset, url: admin_warning_preset_path(@warning_preset) do |f| | |||||
= render 'shared/error_messages', object: @warning_preset | |||||
.fields-group | |||||
= f.input :text, wrapper: :with_block_label | |||||
.actions | |||||
= f.button :button, t('generic.save_changes'), type: :submit |
@ -0,0 +1,30 @@ | |||||
- content_for :page_title do | |||||
= t('admin.warning_presets.title') | |||||
- if can? :create, :account_warning_preset | |||||
= simple_form_for @warning_preset, url: admin_warning_presets_path do |f| | |||||
= render 'shared/error_messages', object: @warning_preset | |||||
.fields-group | |||||
= f.input :text, wrapper: :with_block_label | |||||
.actions | |||||
= f.button :button, t('admin.warning_presets.add_new'), type: :submit | |||||
%hr.spacer/ | |||||
- unless @warning_presets.empty? | |||||
.table-wrapper | |||||
%table.table | |||||
%thead | |||||
%tr | |||||
%th= t('simple_form.labels.account_warning_preset.text') | |||||
%th | |||||
%tbody | |||||
- @warning_presets.each do |preset| | |||||
%tr | |||||
%td | |||||
= Formatter.instance.linkify(preset.text) | |||||
%td | |||||
= table_link_to 'pencil', t('admin.warning_presets.edit'), edit_admin_warning_preset_path(preset) | |||||
= table_link_to 'trash', t('admin.warning_presets.delete'), admin_warning_preset_path(preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } |
@ -0,0 +1,63 @@ | |||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.email-body | |||||
.email-container | |||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.content-cell.hero | |||||
.email-row | |||||
.col-6 | |||||
%table.column{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.column-cell.text-center.padded | |||||
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td | |||||
= image_tag full_pack_url('icon_warning.png'), alt: '' | |||||
%h1= t "user_mailer.warning.title.#{@warning.action}" | |||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.email-body | |||||
.email-container | |||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.content-cell.content-start | |||||
.email-row | |||||
.col-6 | |||||
%table.column{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.column-cell.text-center | |||||
- unless @warning.none_action? | |||||
%p= t "user_mailer.warning.explanation.#{@warning.action}" | |||||
- unless @warning.text.blank? | |||||
= Formatter.instance.linkify(@warning.text) | |||||
%table.email-table{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.email-body | |||||
.email-container | |||||
%table.content-section{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.content-cell | |||||
%table.column{ cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.column-cell.button-cell | |||||
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } | |||||
%tbody | |||||
%tr | |||||
%td.button-primary | |||||
= link_to about_more_url do | |||||
%span= t 'user_mailer.warning.review_server_policies' |
@ -0,0 +1,9 @@ | |||||
<%= t "user_mailer.warning.title.#{@warning.action}" %> | |||||
=== | |||||
<% unless @warning.none_action? %> | |||||
<%= t "user_mailer.warning.explanation.#{@warning.action}" %> | |||||
<% end %> | |||||
<%= @warning.text %> |
@ -0,0 +1,12 @@ | |||||
class CreateAccountWarnings < ActiveRecord::Migration[5.2] | |||||
def change | |||||
create_table :account_warnings do |t| | |||||
t.belongs_to :account, foreign_key: { on_delete: :nullify } | |||||
t.belongs_to :target_account, foreign_key: { to_table: 'accounts', on_delete: :cascade } | |||||
t.integer :action, null: false, default: 0 | |||||
t.text :text, null: false, default: '' | |||||
t.timestamps | |||||
end | |||||
end | |||||
end |
@ -0,0 +1,9 @@ | |||||
class CreateAccountWarningPresets < ActiveRecord::Migration[5.2] | |||||
def change | |||||
create_table :account_warning_presets do |t| | |||||
t.text :text, null: false, default: '' | |||||
t.timestamps | |||||
end | |||||
end | |||||
end |
@ -1,33 +0,0 @@ | |||||
require 'rails_helper' | |||||
describe Admin::SilencesController do | |||||
render_views | |||||
before do | |||||
sign_in Fabricate(:user, admin: true), scope: :user | |||||
end | |||||
describe 'POST #create' do | |||||
it 'redirects to admin accounts page' do | |||||
account = Fabricate(:account, silenced: false) | |||||
post :create, params: { account_id: account.id } | |||||
account.reload | |||||
expect(account.silenced?).to eq true | |||||
expect(response).to redirect_to(admin_accounts_path) | |||||
end | |||||
end | |||||
describe 'DELETE #destroy' do | |||||
it 'redirects to admin accounts page' do | |||||
account = Fabricate(:account, silenced: true) | |||||
delete :destroy, params: { account_id: account.id } | |||||
account.reload | |||||
expect(account.silenced?).to eq false | |||||
expect(response).to redirect_to(admin_accounts_path) | |||||
end | |||||
end | |||||
end |
@ -1,39 +0,0 @@ | |||||
require 'rails_helper' | |||||
describe Admin::SuspensionsController do | |||||
render_views | |||||
before do | |||||
sign_in Fabricate(:user, admin: true), scope: :user | |||||
end | |||||
describe 'GET #new' do | |||||
it 'returns 200' do | |||||
get :new, params: { account_id: Fabricate(:account).id, report_id: Fabricate(:report).id } | |||||
expect(response).to have_http_status(200) | |||||
end | |||||
end | |||||
describe 'POST #create' do | |||||
it 'redirects to admin accounts page' do | |||||
account = Fabricate(:account, suspended: false) | |||||
expect(Admin::SuspensionWorker).to receive(:perform_async).with(account.id) | |||||
post :create, params: { account_id: account.id, form_admin_suspension_confirmation: { acct: account.acct } } | |||||
expect(response).to redirect_to(admin_accounts_path) | |||||
end | |||||
end | |||||
describe 'DELETE #destroy' do | |||||
it 'redirects to admin accounts page' do | |||||
account = Fabricate(:account, suspended: true) | |||||
delete :destroy, params: { account_id: account.id } | |||||
account.reload | |||||
expect(account.suspended?).to eq false | |||||
expect(response).to redirect_to(admin_accounts_path) | |||||
end | |||||
end | |||||
end |
@ -0,0 +1,5 @@ | |||||
Fabricator(:account_warning) do | |||||
account nil | |||||
target_account nil | |||||
text "MyText" | |||||
end |
@ -0,0 +1,3 @@ | |||||
Fabricator(:account_warning_preset) do | |||||
text "MyText" | |||||
end |
@ -0,0 +1,5 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe AccountWarningPreset, type: :model do | |||||
pending "add some examples to (or delete) #{__FILE__}" | |||||
end |
@ -0,0 +1,5 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe AccountWarning, type: :model do | |||||
pending "add some examples to (or delete) #{__FILE__}" | |||||
end |
@ -0,0 +1,4 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe Admin::AccountAction, type: :model do | |||||
end |