Add appeals (#17364)

* Add appeals

* Add ability to reject appeals and ability to browse pending appeals in admin UI

* Add strikes to account page in settings

* Various fixes and improvements

- Add separate notification setting for appeals, separate from reports
- Fix style of links in report/strike header
- Change approving an appeal to not restore statuses (due to federation complexities)
- Change style of successfully appealed strikes on account settings page
- Change account settings page to only show unappealed or recently appealed strikes

* Change appealed_at to overruled_at

* Fix missing method error
master
Eugen Rochko 2022-02-14 21:27:53 +01:00 committed by GitHub
parent 5be705e1e0
commit 564efd0651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1212 additions and 93 deletions

View File

@ -28,7 +28,7 @@ module Admin
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.strikes.custom.latest
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
@domain_block = DomainBlock.rule_for(@account.domain)
end
@ -146,7 +146,7 @@ module Admin
end
def filter_params
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS)
end
def form_account_batch_params

View File

@ -8,6 +8,7 @@ module Admin
@pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count
@pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count
end
private

View File

@ -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

View File

@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
before_action :set_strikes, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
@ -111,8 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_invite
invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
@invite = invite&.valid_for_use? ? invite : nil
@invite = begin
invite = Invite.find_by(code: invite_code) if invite_code.present?
invite if invite&.valid_for_use?
end
end
def determine_layout
@ -123,6 +126,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
@sessions = current_user.session_activations
end
def set_strikes
@strikes = current_account.strikes.active.latest
end
def require_not_suspended!
forbidden if current_account.suspended?
end

View File

@ -0,0 +1,25 @@
# 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
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

View File

@ -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

View File

@ -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

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true
module Admin::AccountModerationNotesHelper
def admin_account_link_to(account)
def admin_account_link_to(account, path: nil)
return if account.nil?
link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
safe_join([
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
content_tag(:span, account.acct, class: 'username'),

View File

@ -33,6 +33,8 @@ module Admin::ActionLogsHelper
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
when 'Instance'
record.domain
when 'Appeal'
link_to record.account.acct, disputes_strike_path(record.strike)
end
end

View File

@ -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

View File

@ -578,12 +578,16 @@ body,
}
.log-entry {
display: block;
line-height: 20px;
padding: 15px;
padding-left: 15px * 2 + 40px;
background: $ui-base-color;
border-bottom: 1px solid darken($ui-base-color, 8%);
position: relative;
text-decoration: none;
color: $darker-text-color;
font-size: 14px;
&:first-child {
border-top-left-radius: 4px;
@ -596,15 +600,12 @@ body,
border-bottom: 0;
}
&:hover {
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 4%);
}
&__header {
color: $darker-text-color;
font-size: 14px;
}
&__avatar {
position: absolute;
left: 15px;
@ -640,6 +641,18 @@ body,
text-decoration: underline;
}
}
&--inactive {
.log-entry__title {
text-decoration: line-through;
}
a,
.username,
.target {
color: $darker-text-color;
}
}
}
a.name-tag,
@ -1175,6 +1188,17 @@ a.sparkline {
font-weight: 600;
padding: 4px 0;
}
a {
color: $ui-highlight-color;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
&--horizontal {
@ -1451,3 +1475,56 @@ a.sparkline {
}
}
}
.strike-card {
padding: 15px;
border-radius: 4px;
background: $ui-base-color;
font-size: 15px;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
color: $primary-text-color;
p {
margin-bottom: 20px;
unicode-bidi: plaintext;
&:last-child {
margin-bottom: 0;
}
}
&__statuses-list {
border-radius: 4px;
border: 1px solid darken($ui-base-color, 8%);
font-size: 13px;
line-height: 18px;
overflow: hidden;
&__item {
padding: 16px;
background: lighten($ui-base-color, 2%);
border-bottom: 1px solid darken($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
&__meta {
color: $darker-text-color;
}
a {
color: inherit;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
}
}

View File

@ -15,6 +15,16 @@ class AdminMailer < ApplicationMailer
end
end
def new_appeal(recipient, appeal)
@appeal = appeal
@me = recipient
@instance = Rails.configuration.x.local_domain
locale_for_account(@me) do
mail to: @me.user_email, subject: I18n.t('admin_mailer.new_appeal.subject', instance: @instance, username: @appeal.account.username)
end
end
def new_pending_account(recipient, user)
@account = user.account
@me = recipient

View File

@ -173,6 +173,26 @@ class UserMailer < Devise::Mailer
end
end
def appeal_approved(user, appeal)
@resource = user
@instance = Rails.configuration.x.local_domain
@appeal = appeal
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_approved.subject', date: l(@appeal.created_at))
end
end
def appeal_rejected(user, appeal)
@resource = user
@instance = Rails.configuration.x.local_domain
@appeal = appeal
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_rejected.subject', date: l(@appeal.created_at))
end
end
def sign_in_token(user, remote_ip, user_agent, timestamp)
@resource = user
@instance = Rails.configuration.x.local_domain

View File

@ -270,6 +270,10 @@ class Account < ApplicationRecord
true
end
def previous_strikes_count
strikes.where(overruled_at: nil).count
end
def keypair
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end

View File

@ -24,6 +24,8 @@ class AccountFilter
scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil)
params.each do |key, value|
next if key.to_s == 'page'
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
end

View File

@ -12,6 +12,7 @@
# updated_at :datetime not null
# report_id :bigint(8)
# status_ids :string is an Array
# overruled_at :datetime
#
class AccountWarning < ApplicationRecord
@ -28,12 +29,17 @@ class AccountWarning < ApplicationRecord
belongs_to :target_account, class_name: 'Account', inverse_of: :strikes
belongs_to :report, optional: true
has_one :appeal, dependent: :destroy
has_one :appeal, dependent: :destroy, inverse_of: :strike
scope :latest, -> { order(id: :desc) }
scope :custom, -> { where.not(text: '') }
scope :active, -> { where(overruled_at: nil).or(where('account_warnings.overruled_at >= ?', 30.days.ago)) }
def statuses
Status.with_discarded.where(id: status_ids || [])
end
def overruled?
overruled_at.present?
end
end

View File

@ -8,6 +8,8 @@ class Admin::ActionLogFilter
).freeze
ACTION_TYPE_MAP = {
approve_appeal: { target_type: 'Appeal', action: 'approve' }.freeze,
reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
confirm_user: { target_type: 'User', action: 'confirm' }.freeze,

View File

@ -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

58
app/models/appeal.rb Normal file
View File

@ -0,0 +1,58 @@
# 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
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 Time.now.utc > (strike.created_at + 20.days)
end
end

View File

@ -265,6 +265,10 @@ class User < ApplicationRecord
settings.notification_emails['pending_account']
end
def allows_appeal_emails?
settings.notification_emails['appeal']
end
def allows_trending_tag_emails?
settings.notification_emails['trending_tag']
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AccountWarningPolicy < ApplicationPolicy
def show?
target? || staff?
end
def appeal?
target?
end
private
def target?
record.target_account_id == current_account&.id
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -246,18 +246,29 @@
%hr.spacer/
- unless @warnings.empty?
= render @warnings
%h3= t 'admin.accounts.previous_strikes'
%p= t('admin.accounts.previous_strikes_description_html', count: @account.previous_strikes_count)
.account-strikes
= render @warnings
%hr.spacer/
= render @moderation_notes
%h3= t 'admin.reports.notes.title'
%p= t 'admin.reports.notes_description_html'
.report-notes
= render partial: 'admin/report_notes/report_note', collection: @moderation_notes
= simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f|
= render 'shared/error_messages', object: @account_moderation_note
= f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
= f.hidden_field :target_account_id
.field-group
= f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
.actions
= f.button :button, t('admin.account_moderation_notes.create'), type: :submit

View File

@ -46,6 +46,9 @@
%span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
= fa_icon 'chevron-right fw'
= link_to admin_disputes_appeals_path(status: 'pending'), class: 'dashboard__quick-access' do
%span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count)
= fa_icon 'chevron-right fw'
.dashboard__item
= react_admin_component :dimension, dimension: 'sources', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.sources')

View File

@ -0,0 +1,21 @@
= link_to disputes_strike_path(appeal.strike), class: ['log-entry', appeal.approved? && 'log-entry--inactive'] do
.log-entry__header
.log-entry__avatar
= image_tag appeal.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
.log-entry__content
.log-entry__title
= t(appeal.strike.action, scope: 'admin.strikes.actions', name: content_tag(:span, appeal.strike.account.username, class: 'username'), target: content_tag(:span, appeal.account.acct, class: 'target')).html_safe
.log-entry__timestamp
%time.formatted{ datetime: appeal.strike.created_at.iso8601 }
= l(appeal.strike.created_at)
- if appeal.strike.report_id.present?
·
= t('admin.reports.title', id: appeal.strike.report_id)
·
- if appeal.approved?
%span.positive-hint= t('admin.strikes.appeal_approved')
- elsif appeal.rejected?
%span.negative-hint= t('admin.strikes.appeal_rejected')
- else
%span.warning-hint= t('admin.strikes.appeal_pending')

View File

@ -0,0 +1,22 @@
- content_for :page_title do
= t('admin.disputes.appeals.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
.filters
.filter-subset
%strong= t('admin.tags.review')
%ul
%li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Appeal.pending.count})"], ' '), status: 'pending'
%li= filter_link_to t('admin.trends.approved'), status: 'approved'
%li= filter_link_to t('admin.trends.rejected'), status: 'rejected'
- if @appeals.empty?
%div.muted-hint.center-text
= t 'admin.disputes.appeals.empty'
- else
.announcements-list
= render partial: 'appeal', collection: @appeals
= paginate @appeals

View File

@ -3,7 +3,7 @@
.report-notes__item__header
%span.username
= link_to display_name(report_note.account), admin_account_path(report_note.account_id)
= link_to report_note.account.username, admin_account_path(report_note.account_id)
%time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
- if report_note.created_at.today?
= t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))

View File

@ -57,7 +57,7 @@
.report-header__details__item__header
%strong= t('admin.accounts.strikes')
.report-header__details__item__content
= @report.target_account.strikes.count
= @report.target_account.previous_strikes_count
.report-header__details
.report-header__details__item

View File

@ -0,0 +1,9 @@
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
<%= raw t('admin_mailer.new_appeal.body', target: @appeal.account.username, action_taken_by: @appeal.strike.account.username, date: l(@appeal.strike.created_at), type: t(@appeal.strike.action, scope: 'admin_mailer.new_appeal.actions')) %>
> <%= raw word_wrap(@appeal.text, break_sequence: "\n> ") %>
<%= raw t('admin_mailer.new_appeal.next_steps') %>
<%= raw t('application_mailer.view')%> <%= disputes_strike_url(@appeal.strike) %>

View File

@ -0,0 +1,20 @@
= link_to disputes_strike_path(account_warning), class: 'log-entry' do
.log-entry__header
.log-entry__avatar
.indicator-icon{ class: account_warning.overruled? ? 'success' : 'failure' }
= fa_icon 'warning'
.log-entry__content
.log-entry__title
= t('disputes.strikes.title', action: t(account_warning.action, scope: 'disputes.strikes.title_actions'), date: l(account_warning.created_at.to_date))
.log-entry__timestamp
%time.formatted{ datetime: account_warning.created_at.iso8601 }= l(account_warning.created_at)
- if account_warning.overruled?
·
%span.positive-hint= t('disputes.strikes.your_appeal_approved')
- elsif account_warning.appeal&.pending?
·
%span.warning-hint= t('disputes.strikes.your_appeal_pending')
- elsif account_warning.appeal&.rejected?
·
%span.negative-hint= t('disputes.strikes.your_appeal_rejected')

View File

@ -1,22 +1,17 @@
- if !@user.confirmed?
.flash-message.warning
= t('auth.status.confirming')
= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
- elsif !@user.approved?
.flash-message.warning
= t('auth.status.pending')
- elsif @user.account.moved_to_account_id.present?
.flash-message.warning
= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
= link_to t('migrations.cancel'), settings_migration_path
%h3= t('auth.status.account_status')
.simple_form
%p.hint
- if @user.account.suspended?
%span.negative-hint= t('user_mailer.warning.explanation.suspend')
- elsif @user.disabled?
%span.negative-hint= t('user_mailer.warning.explanation.disable')
- elsif @user.account.silenced?
%span.warning-hint= t('user_mailer.warning.explanation.silence')
- elsif !@user.confirmed?
%span.warning-hint= t('auth.status.confirming')
= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
- elsif !@user.approved?
%span.warning-hint= t('auth.status.pending')
- elsif @user.account.moved_to_account_id.present?
%span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
= link_to t('migrations.cancel'), settings_migration_path
- else
%span.positive-hint= t('auth.status.functional')
= render partial: 'account_warning', collection: @strikes
%hr.spacer/

View File

@ -0,0 +1,127 @@
- content_for :page_title do
= t('disputes.strikes.title', action: t(@strike.action, scope: 'disputes.strikes.title_actions'), date: l(@strike.created_at.to_date))
- content_for :heading_actions do
- if @appeal.persisted?
= link_to t('admin.accounts.approve'), approve_admin_disputes_appeal_path(@appeal), method: :post, class: 'button' if can?(:approve, @appeal)
= link_to t('admin.accounts.reject'), reject_admin_disputes_appeal_path(@appeal), method: :post, class: 'button button--destructive' if can?(:reject, @appeal)
- if @strike.overruled?
%p.hint
%span.positive-hint
= fa_icon 'check'
= ' '
= t 'disputes.strikes.appeal_approved'
- elsif @appeal.persisted? && @appeal.rejected?
%p.hint
%span.negative-hint
= fa_icon 'times'
= ' '
= t 'disputes.strikes.appeal_rejected'
.report-header
.report-header__card
.strike-card
- unless @strike.none_action?
%p= t "user_mailer.warning.explanation.#{@strike.action}"
- unless @strike.text.blank?
= Formatter.instance.linkify(@strike.text)
- if @strike.report && !@strike.report.other?
%p
%strong= t('user_mailer.warning.reason')
= t("user_mailer.warning.categories.#{@strike.report.category}")
- if @strike.report.violation? && @strike.report.rule_ids.present?
%ul.rules-list
- @strike.report.rules.each do |rule|
%li= rule.text
- if @strike.status_ids.present? && !@strike.status_ids.empty?
%p
%strong= t('user_mailer.warning.statuses')
.strike-card__statuses-list
- status_map = @strike.statuses.includes(:application, :media_attachments).index_by(&:id)
- @strike.status_ids.each do |status_id|
.strike-card__statuses-list__item
- if (status = status_map[status_id.to_i])
.one-liner
= link_to short_account_status_url(@strike.target_account, status_id), class: 'emojify' do
= one_line_preview(status)
- status.media_attachments.each do |media_attachment|
%abbr{ title: media_attachment.description }
= fa_icon 'link'
= media_attachment.file_file_name
.strike-card__statuses-list__item__meta
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
·
= status.application.name
- else
.one-liner= t('disputes.strikes.status', id: status_id)
.strike-card__statuses-list__item__meta
= t('disputes.strikes.status_removed')
.report-header__details
.report-header__details__item
.report-header__details__item__header
%strong= t('disputes.strikes.created_at')
.report-header__details__item__content
%time.formatted{ datetime: @strike.created_at.iso8601, title: l(@strike.created_at) }= l(@strike.created_at)
.report-header__details__item
.report-header__details__item__header
%strong= t('disputes.strikes.recipient')
.report-header__details__item__content
= admin_account_link_to @strike.target_account, path: can?(:show, @strike.target_account) ? admin_account_path(@strike.target_account_id) : ActivityPub::TagManager.instance.url_for(@strike.target_account)
.report-header__details__item
.report-header__details__item__header
%strong= t('disputes.strikes.action_taken')
.report-header__details__item__content
- if @strike.overruled?
%del= t(@strike.action, scope: 'user_mailer.warning.title')
- else
= t(@strike.action, scope: 'user_mailer.warning.title')
- if @strike.report && can?(:show, @strike.report)
.report-header__details__item
.report-header__details__item__header
%strong= t('disputes.strikes.associated_report')
.report-header__details__item__content
= link_to t('admin.reports.report', id: @strike.report.id), admin_report_path(@strike.report)
- if @appeal.persisted?
.report-header__details__item
.report-header__details__item__header
%strong= t('disputes.strikes.appeal_submitted_at')
.report-header__details__item__content
%time.formatted{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }= l(@appeal.created_at)
%hr.spacer/
- if @appeal.persisted?
%h3= t('disputes.strikes.appeal')
.report-notes
.report-notes__item
= image_tag @appeal.account.avatar.url, class: 'report-notes__item__avatar'
.report-notes__item__header
%span.username
= link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account)
%time{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }
- if @appeal.created_at.today?
= t('admin.report_notes.today_at', time: l(@appeal.created_at, format: :time))
- else
= l @appeal.created_at.to_date
.report-notes__item__content
= simple_format(h(@appeal.text))
- elsif can?(:appeal, @strike)
%h3= t('disputes.strikes.appeals.submit')
= simple_form_for(@appeal, url: disputes_strike_appeal_path(@strike)) do |f|
.fields-group
= f.input :text, wrapper: :with_label, input_html: { maxlength: 500 }
.actions
= f.button :button, t('disputes.strikes.appeals.submit'), type: :submit

View File

@ -21,6 +21,7 @@
- if current_user.staff?
= ff.input :report, as: :boolean, wrapper: :with_label
= ff.input :appeal, as: :boolean, wrapper: :with_label
= ff.input :pending_account, as: :boolean, wrapper: :with_label
= ff.input :trending_tag, as: :boolean, wrapper: :with_label

View File

@ -49,7 +49,7 @@
%span.detailed-status__visibility-icon
= visibility_icon status
·
- if status.application && @account.user&.setting_show_application
- if status.application && status.account.user&.setting_show_application
- if status.application.website.blank?
%strong.detailed-status__application= status.application.name
- else

View File

@ -0,0 +1,59 @@
%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{ align: 'center', cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td
= image_tag full_pack_url('media/images/mailer/icon_done.png'), alt: ''
%h1= t 'user_mailer.appeal_approved.title'
%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
%p= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
%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 root_url do
%span= t 'user_mailer.appeal_approved.action'

View File

@ -0,0 +1,7 @@
<%= t 'user_mailer.appeal_approved.title' %>
===
<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
=> <%= root_url %>

View File

@ -0,0 +1,59 @@
%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{ align: 'center', cellspacing: 0, cellpadding: 0 }
%tbody
%tr
%td
= image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: ''
%h1= t 'user_mailer.appeal_rejected.title'
%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
%p= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
%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 root_url do
%span= t 'user_mailer.appeal_approved.action'

View File

@ -0,0 +1,7 @@
<%= t 'user_mailer.appeal_rejected.title' %>
===
<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
=> <%= root_url %>

View File

@ -77,8 +77,8 @@
%tbody
%tr
%td.button-primary
= link_to about_more_url do
%span= t 'user_mailer.warning.review_server_policies'
= link_to disputes_strike_url(@warning) do
%span= t 'user_mailer.warning.appeal'
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
@ -95,4 +95,4 @@
%tbody
%tr
%td.column-cell.text-center
%p= t 'user_mailer.warning.get_in_touch', instance: @instance
%p= t 'user_mailer.warning.appeal_description', instance: @instance

View File

@ -1,25 +1,5 @@
{
"ignored_warnings": [
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "04dbbc249b989db2e0119bbb0f59c9818e12889d2b97c529cdc0b1526002ba4b",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/report.rb",
"line": 113,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")",
"render_path": null,
"location": {
"type": "method",
"class": "Report",
"method": "history"
},
"user_input": "Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)",
"confidence": "High",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
@ -27,7 +7,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/status.rb",
"line": 100,
"line": 104,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
"render_path": null,
@ -107,7 +87,7 @@
"check_name": "PermitAttributes",
"message": "Potentially dangerous key allowed for mass assignment",
"file": "app/controllers/api/v1/admin/reports_controller.rb",
"line": 78,
"line": 90,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit(:resolved, :account_id, :target_account_id)",
"render_path": null,
@ -140,6 +120,36 @@
"confidence": "Medium",
"note": ""
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "afad51718ae373b2f19d2513029fd2afccf58b9148e475934bc6a162ee33c352",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/admin/disputes/appeals/_appeal.html.haml",
"line": 7,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "t((Unresolved Model).new.strike.action, :scope => \"admin.strikes.actions\", :name => content_tag(:span, (Unresolved Model).new.strike.account.username, :class => \"username\"), :target => content_tag(:span, (Unresolved Model).new.account.acct, :class => \"target\"))",
"render_path": [
{
"type": "template",
"name": "admin/disputes/appeals/index",
"line": 16,
"file": "app/views/admin/disputes/appeals/index.html.haml",
"rendered": {
"name": "admin/disputes/appeals/_appeal",
"file": "app/views/admin/disputes/appeals/_appeal.html.haml"
}
}
],
"location": {
"type": "template",
"template": "admin/disputes/appeals/_appeal"
},
"user_input": "(Unresolved Model).new.strike",
"confidence": "Weak",
"note": ""
},
{
"warning_type": "Redirect",
"warning_code": 18,
@ -194,7 +204,7 @@
{
"type": "template",
"name": "admin/trends/links/index",
"line": 37,
"line": 39,
"file": "app/views/admin/trends/links/index.html.haml",
"rendered": {
"name": "admin/trends/links/_preview_card",
@ -213,13 +223,13 @@
{
"warning_type": "Mass Assignment",
"warning_code": 105,
"fingerprint": "e867661b2c9812bc8b75a5df12b28e2a53ab97015de0638b4e732fe442561b28",
"fingerprint": "f9de0ca4b04ae4b51b74d98db14dcbb6dae6809e627b58e711019cf9b4a47866",
"check_name": "PermitAttributes",
"message": "Potentially dangerous key allowed for mass assignment",
"file": "app/controllers/api/v1/reports_controller.rb",
"line": 36,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit(:account_id, :comment, :forward, :status_ids => ([]))",
"code": "params.permit(:account_id, :comment, :category, :forward, :status_ids => ([]), :rule_ids => ([]))",
"render_path": null,
"location": {
"type": "method",
@ -231,6 +241,6 @@
"note": ""
}
],
"updated": "2021-11-14 05:26:09 +0100",
"brakeman_version": "5.1.2"
"updated": "2022-02-13 02:24:12 +0100",
"brakeman_version": "5.2.1"
}

View File

@ -94,7 +94,6 @@ en:
account_moderation_notes:
create: Leave note
created_msg: Moderation note successfully created!
delete: Delete
destroyed_msg: Moderation note successfully destroyed!
accounts:
add_email_domain_block: Block e-mail domain
@ -163,6 +162,11 @@ en:
not_subscribed: Not subscribed
pending: Pending review
perform_full_suspension: Suspend
previous_strikes: Previous strikes
previous_strikes_description_html:
one: This account has <strong>one</strong> strike.
other: This account has <strong>%{count}</strong> strikes.
zero: This account is <strong>in good standing</strong>.
promote: Promote
protocol: Protocol
public: Public
@ -227,6 +231,7 @@ en:
whitelisted: Allowed for federation
action_logs:
action_types:
approve_appeal: Approve Appeal
approve_user: Approve User
assigned_to_self_report: Assign Report
change_email_user: Change E-mail for User