Browse Source
Conflicts: - `db/schema.rb`: Conflict due to glitch-soc adding the `content_type` column on status edits and thus having a different schema version number. Solved by taking upstream's schema version number, as it is higher than glitch-soc's.main
81 changed files with 1461 additions and 381 deletions
@ -0,0 +1,40 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class Admin::Disputes::AppealsController < Admin::BaseController |
|||
before_action :set_appeal, except: :index |
|||
|
|||
def index |
|||
authorize :appeal, :index? |
|||
|
|||
@appeals = filtered_appeals.page(params[:page]) |
|||
end |
|||
|
|||
def approve |
|||
authorize @appeal, :approve? |
|||
log_action :approve, @appeal |
|||
ApproveAppealService.new.call(@appeal, current_account) |
|||
redirect_to disputes_strike_path(@appeal.strike) |
|||
end |
|||
|
|||
def reject |
|||
authorize @appeal, :approve? |
|||
log_action :reject, @appeal |
|||
@appeal.reject!(current_account) |
|||
UserMailer.appeal_rejected(@appeal.account.user, @appeal) |
|||
redirect_to disputes_strike_path(@appeal.strike) |
|||
end |
|||
|
|||
private |
|||
|
|||
def filtered_appeals |
|||
Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account) |
|||
end |
|||
|
|||
def filter_params |
|||
params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS) |
|||
end |
|||
|
|||
def set_appeal |
|||
@appeal = Appeal.find(params[:id]) |
|||
end |
|||
end |
@ -0,0 +1,26 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class Disputes::AppealsController < Disputes::BaseController |
|||
before_action :set_strike |
|||
|
|||
def create |
|||
authorize @strike, :appeal? |
|||
|
|||
@appeal = AppealService.new.call(@strike, appeal_params[:text]) |
|||
|
|||
redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') |
|||
rescue ActiveRecord::RecordInvalid => e |
|||
@appeal = e.record |
|||
render template: 'disputes/strikes/show' |
|||
end |
|||
|
|||
private |
|||
|
|||
def set_strike |
|||
@strike = current_account.strikes.find(params[:strike_id]) |
|||
end |
|||
|
|||
def appeal_params |
|||
params.require(:appeal).permit(:text) |
|||
end |
|||
end |
@ -0,0 +1,18 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class Disputes::BaseController < ApplicationController |
|||
include Authorization |
|||
|
|||
layout 'admin' |
|||
|
|||
skip_before_action :require_functional! |
|||
|
|||
before_action :set_body_classes |
|||
before_action :authenticate_user! |
|||
|
|||
private |
|||
|
|||
def set_body_classes |
|||
@body_classes = 'admin' |
|||
end |
|||
end |
@ -0,0 +1,17 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class Disputes::StrikesController < Disputes::BaseController |
|||
before_action :set_strike |
|||
|
|||
def show |
|||
authorize @strike, :show? |
|||
|
|||
@appeal = @strike.appeal || @strike.build_appeal |
|||
end |
|||
|
|||
private |
|||
|
|||
def set_strike |
|||
@strike = AccountWarning.find(params[:id]) |
|||
end |
|||
end |
@ -0,0 +1,20 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
module Admin::Trends::StatusesHelper |
|||
def one_line_preview(status) |
|||
text = begin |
|||
if status.local? |
|||
status.text.split("\n").first |
|||
else |
|||
Nokogiri::HTML(status.text).css('html > body > *').first&.text |
|||
end |
|||
end |
|||
|
|||
return '' if text.blank? |
|||
|
|||
html = Formatter.instance.send(:encode, text) |
|||
html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?) |
|||
|
|||
html.html_safe # rubocop:disable Rails/OutputSafety |
|||
end |
|||
end |
@ -0,0 +1,49 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class Admin::AppealFilter |
|||
KEYS = %i( |
|||
status |
|||
).freeze |
|||
|
|||
attr_reader :params |
|||
|
|||
def initialize(params) |
|||
@params = params |
|||
end |
|||
|
|||
def results |
|||
scope = Appeal.order(id: :desc) |
|||
|
|||
params.each do |key, value| |
|||
next if %w(page).include?(key.to_s) |
|||
|
|||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present? |
|||
end |
|||
|
|||
scope |
|||
end |
|||
|
|||
private |
|||
|
|||
def scope_for(key, value) |
|||
case key.to_s |
|||
when 'status' |
|||
status_scope(value) |
|||
else |
|||
raise "Unknown filter: #{key}" |
|||
end |
|||
end |
|||
|
|||
def status_scope(value) |
|||
case value |
|||
when 'approved' |
|||
Appeal.approved |
|||
when 'rejected' |
|||
Appeal.rejected |
|||
when 'pending' |
|||
Appeal.pending |
|||
else |
|||
raise "Unknown status: #{value}" |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,60 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
# == Schema Information |
|||
# |
|||
# Table name: appeals |
|||
# |
|||
# id :bigint(8) not null, primary key |
|||
# account_id :bigint(8) not null |
|||
# account_warning_id :bigint(8) not null |
|||
# text :text default(""), not null |
|||
# approved_at :datetime |
|||
# approved_by_account_id :bigint(8) |
|||
# rejected_at :datetime |
|||
# rejected_by_account_id :bigint(8) |
|||
# created_at :datetime not null |
|||
# updated_at :datetime not null |
|||
# |
|||
class Appeal < ApplicationRecord |
|||
MAX_STRIKE_AGE = 20.days |
|||
|
|||
belongs_to :account |
|||
belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id' |
|||
belongs_to :approved_by_account, class_name: 'Account', optional: true |
|||
belongs_to :rejected_by_account, class_name: 'Account', optional: true |
|||
|
|||
validates :text, presence: true, length: { maximum: 2_000 } |
|||
validates :account_warning_id, uniqueness: true |
|||
|
|||
validate :validate_time_frame, on: :create |
|||
|
|||
scope :approved, -> { where.not(approved_at: nil) } |
|||
scope :rejected, -> { where.not(rejected_at: nil) } |
|||
scope :pending, -> { where(approved_at: nil, rejected_at: nil) } |
|||
|
|||
def pending? |
|||
!approved? && !rejected? |
|||
end |
|||
|
|||
def approved? |
|||
approved_at.present? |
|||
end |
|||
|
|||
def rejected? |
|||
rejected_at.present? |
|||
end |
|||
|
|||
def approve!(current_account) |
|||
update!(approved_at: Time.now.utc, approved_by_account: current_account) |
|||
end |
|||
|
|||
def reject!(current_account) |
|||
update!(rejected_at: Time.now.utc, rejected_by_account: current_account) |
|||
end |
|||
|
|||
private |
|||
|
|||
def validate_time_frame |
|||
errors.add(:base, I18n.t('strikes.errors.too_late')) if strike.created_at < MAX_STRIKE_AGE.ago |
|||
end |
|||
end |
@ -0,0 +1,17 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class AccountWarningPolicy < ApplicationPolicy |
|||
def show? |
|||
target? || staff? |
|||
end |
|||
|
|||
def appeal? |
|||
target? && record.created_at >= Appeal::MAX_STRIKE_AGE.ago |
|||
end |
|||
|
|||
private |
|||
|
|||
def target? |
|||
record.target_account_id == current_account&.id |
|||
end |
|||
end |
@ -0,0 +1,13 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class AppealPolicy < ApplicationPolicy |
|||
def index? |
|||
staff? |
|||
end |
|||
|
|||
def approve? |
|||
record.pending? && staff? |
|||
end |
|||
|
|||
alias reject? approve? |
|||
end |
@ -0,0 +1,28 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class AppealService < BaseService |
|||
def call(strike, text) |
|||
@strike = strike |
|||
@text = text |
|||
|
|||
create_appeal! |
|||
notify_staff! |
|||
|
|||
@appeal |
|||
end |
|||
|
|||
private |
|||
|
|||
def create_appeal! |
|||
@appeal = @strike.create_appeal!( |
|||
text: @text, |
|||
account: @strike.target_account |
|||
) |
|||
end |
|||
|
|||
def notify_staff! |
|||
User.staff.includes(:account).each do |u| |
|||
AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails? |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,74 @@ |
|||
# frozen_string_literal: true |
|||
|
|||
class ApproveAppealService < BaseService |
|||
def call(appeal, current_account) |
|||
@appeal = appeal |
|||
@strike = appeal.strike |
|||
@current_account = current_account |
|||
|
|||
ApplicationRecord.transaction do |
|||
undo_strike_action! |
|||
mark_strike_as_appealed! |
|||
end |
|||
|
|||
queue_workers! |
|||
notify_target_account! |
|||
end |
|||
|
|||
private |
|||
|
|||
def target_account |
|||
@strike.target_account |
|||
end |
|||
|
|||
def undo_strike_action! |
|||
case @strike.action |
|||
when 'disable' |
|||
undo_disable! |
|||
when 'delete_statuses' |
|||
undo_delete_statuses! |
|||
when 'sensitive' |
|||
undo_sensitive! |
|||
when 'silence' |
|||
undo_silence! |
|||
when 'suspend' |
|||
undo_suspend! |
|||
end |
|||
end |
|||
|
|||
def mark_strike_as_appealed! |
|||
@appeal.approve!(@current_account) |
|||
@strike.touch(:overruled_at) |
|||
end |
|||
|
|||
def undo_disable! |
|||
target_account.user.enable! |
|||
end |
|||
|
|||
def undo_delete_statuses! |
|||
# Cannot be undone |
|||
end |
|||
|
|||
def undo_sensitive! |
|||
target_account.unsensitize! |
|||
end |
|||
|
|||
def undo_silence! |
|||
target_account.unsilence! |
|||
end |
|||
|
|||
def undo_suspend! |
|||
target_account.unsuspend! |
|||
end |
|||
|
|||
def queue_workers! |
|||
case @strike.action |
|||
when 'suspend' |
|||
Admin::UnsuspensionWorker.perform_async(target_account.id) |
|||
end |
|||
end |
|||
|
|||
def notify_target_account! |
|||
UserMailer.appeal_approved(target_account.user, @appeal).deliver_later |
|||
end |
|||
end |
@ -1,7 +0,0 @@ |
|||
.speech-bubble |
|||
.speech-bubble__bubble |
|||
= simple_format(h(account_moderation_note.content)) |
|||
.speech-bubble__owner |
|||
= admin_account_link_to account_moderation_note.account |
|||
%time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at |
|||
= table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) |
@ -1,6 +1,24 @@ |
|||
.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 |
|||
= link_to disputes_strike_path(account_warning), class: ['log-entry', account_warning.overruled? && 'log-entry--inactive'] do |
|||
.log-entry__header |
|||
.log-entry__avatar |
|||
= image_tag account_warning.target_account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' |
|||
.log-entry__content |
|||
.log-entry__title |
|||
= t(account_warning.action, scope: 'admin.strikes.actions', name: content_tag(:span, account_warning.account.username, class: 'username'), target: content_tag(:span, account_warning.target_account.acct, class: 'target')).html_safe |
|||
.log-entry__timestamp |
|||
%time.formatted{ datetime: account_warning.created_at.iso8601 } |
|||
= l(account_warning.created_at) |
|||
|
|||
- if account_warning.report_id.present? |
|||
· |
|||
= t('admin.reports.title', id: account_warning.report_id) |
|||
|
|||
- if account_warning.overruled? |
|||
· |
|||
%span.positive-hint= t('admin.strikes.appeal_approved') |
|||
- elsif account_warning.appeal&.pending? |
|||
· |
|||
%span.warning-hint= t('admin.strikes.appeal_pending') |
|||
- elsif account_warning.appeal&.rejected? |
|||
· |
|||
%span.negative-hint= t('admin.strikes.appeal_rejected') |
|||
|