@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
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
@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
--- /dev/null
+# 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
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]
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
@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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
# 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'),
"#{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
--- /dev/null
+# 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
}
.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;
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;
text-decoration: underline;
}
}
+
+ &--inactive {
+ .log-entry__title {
+ text-decoration: line-through;
+ }
+
+ a,
+ .username,
+ .target {
+ color: $darker-text-color;
+ }
+ }
}
a.name-tag,
font-weight: 600;
padding: 4px 0;
}
+
+ a {
+ color: $ui-highlight-color;
+ text-decoration: none;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: underline;
+ }
+ }
}
&--horizontal {
}
}
}
+
+.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;
+ }
+ }
+ }
+ }
+}
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
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
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
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
# updated_at :datetime not null
# report_id :bigint(8)
# status_ids :string is an Array
+# overruled_at :datetime
#
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
).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,
--- /dev/null
+# 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
--- /dev/null
+# 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
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
--- /dev/null
+# 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
--- /dev/null
+# frozen_string_literal: true
+
+class AppealPolicy < ApplicationPolicy
+ def index?
+ staff?
+ end
+
+ def approve?
+ record.pending? && staff?
+ end
+
+ alias reject? approve?
+end
--- /dev/null
+# 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
--- /dev/null
+# 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
+++ /dev/null
-.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)
-.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')
%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'
- = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f|
- = render 'shared/error_messages', object: @account_moderation_note
+ %p= t 'admin.reports.notes_description_html'
- = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
+ .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|
= 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
%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')
--- /dev/null
+= 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')
--- /dev/null
+- 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
.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))
.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
--- /dev/null
+<%= 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) %>
--- /dev/null
+= 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')
+- 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/
--- /dev/null
+- 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
- 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
%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
--- /dev/null
+%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'
--- /dev/null
+<%= 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 %>
--- /dev/null
+%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'
--- /dev/null
+<%= 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 %>
%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
%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
{
"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,
"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,
"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,
"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,
{
"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",
{
"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",
"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"
}
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
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
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
enable_user: Enable User
memorialize_account: Memorialize Account
promote_user: Promote User
+ reject_appeal: Reject Appeal
reject_user: Reject User
remove_avatar_user: Remove Avatar
reopen_report: Reopen Report
update_domain_block: Update Domain Block
update_status: Update Post
actions:
+ approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
approve_user_html: "%{name} approved sign-up from %{target}"
assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
change_email_user_html: "%{name} changed the e-mail address of user %{target}"
enable_user_html: "%{name} enabled login for user %{target}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}"
+ reject_appeal_html: "%{name} rejected moderation decision appeal from %{target}"
reject_user_html: "%{name} rejected sign-up from %{target}"
remove_avatar_user_html: "%{name} removed %{target}'s avatar"
reopen_report_html: "%{name} reopened report %{target}"
media_storage: Media storage
new_users: new users
opened_reports: reports opened
+ pending_appeals_html:
+ one: "<strong>1</strong> pending appeal"
+ other: "<strong>%{count}</strong> pending appeals"
pending_reports_html:
one: "<strong>1</strong> pending report"
other: "<strong>%{count}</strong> pending reports"
top_languages: Top active languages
top_servers: Top active servers
website: Website
+ disputes:
+ appeals:
+ empty: No appeals found.
+ title: Appeals
domain_allows:
add_new: Allow federation with domain
created_msg: Domain has been successfully allowed for federation
no_status_selected: No posts were changed as none were selected
title: Account posts
with_media: With media
+ strikes:
+ actions:
+ delete_statuses: "%{name} deleted %{target}'s posts"
+ disable: "%{name} froze %{target}'s account"
+ none: "%{name} sent a warning to %{target}"
+ sensitive: "%{name} marked %{target}'s account as sensitive"
+ silence: "%{name} limited %{target}'s account"
+ suspend: "%{name} suspended %{target}'s account"
+ appeal_approved: Appealed
+ appeal_pending: Appeal pending
system_checks:
database_schema_check:
message_html: There are pending database migrations. Please run them to ensure the application behaves as expected
empty: You haven't defined any warning presets yet.
title: Manage warning presets
admin_mailer:
+ new_appeal:
+ actions:
+ delete_statuses: to delete their posts
+ disable: to freeze their account
+ none: a warning
+ sensitive: to mark their account as sensitive
+ silence: to limit their account
+ suspend: to suspend their account
+ body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
+ next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
+ subject: "%{username} is appealing a moderation decision on %{instance}"
new_pending_account:
body: The details of the new account are below. You can approve or reject this application.
subject: New account up for review on %{instance} (%{username})
status:
account_status: Account status
confirming: Waiting for e-mail confirmation to be completed.
- functional: Your account is fully operational.
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
too_fast: Form submitted too fast, try again.
directory: Profile directory
explanation: Discover users based on their interests
explore_mastodon: Explore %{title}
+ disputes:
+ strikes:
+ action_taken: Action taken
+ appeal: Appeal
+ appeal_approved: This strike has been successfully appealed and is no longer valid
+ appeal_rejected: The appeal has been rejected
+ appeal_submitted_at: Appeal submitted
+ appealed_msg: Your appeal has been submitted. If it is approved, you will be notified.
+ appeals:
+ submit: Submit appeal
+ associated_report: Associated report
+ created_at: Dated
+ recipient: Addressed to
+ status: 'Post #%{id}'
+ status_removed: Post already removed from system
+ title: "%{action} from %{date}"
+ title_actions:
+ delete_statuses: Post removal
+ disable: Freezing of account
+ none: Warning
+ sensitive: Marking as sensitive of account
+ silence: Limitation of account
+ suspend: Suspension of account
+ your_appeal_approved: Your appeal has been approved
+ your_appeal_pending: You have submitted an appeal
+ your_appeal_rejected: Your appeal has been rejected
domain_validator:
invalid_domain: is not a valid domain name
errors:
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
webauthn: Security keys
user_mailer:
+ appeal_approved:
+ action: Go to your account
+ explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing.
+ subject: Your appeal from %{date} has been approved
+ title: Appeal approved
+ appeal_rejected:
+ explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been rejected.
+ subject: Your appeal from %{date} has been rejected
+ title: Appeal rejected
backup_ready:
explanation: You requested a full backup of your Mastodon account. It's now ready for download!
subject: Your archive is ready for download
subject: Please confirm attempted sign in
title: Sign in attempt
warning:
+ appeal: Submit an appeal
+ appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}.
categories:
spam: Spam
violation: Content violates the following community guidelines
suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed in about 30 days, but we will retain some basic data to prevent you from evading the suspension.
get_in_touch: If you believe this is an error, you can reply to this e-mail to get in touch with the staff of %{instance}.
reason: 'Reason:'
- review_server_policies: Review server policies
statuses: 'Posts that have been found in violation:'
subject:
delete_statuses: Your posts on %{acct} have been removed
scheduled_at: Leave blank to publish the announcement immediately
starts_at: Optional. In case your announcement is bound to a specific time range
text: You can use post syntax. Please be mindful of the space the announcement will take up on the user's screen
+ appeal:
+ text: You can only appeal a strike once
defaults:
autofollow: People who sign up through the invite will automatically follow you
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
scheduled_at: Schedule publication
starts_at: Start of event
text: Announcement
+ appeal:
+ text: Explain why this decision should be reversed
defaults:
autofollow: Invite to follow your account
avatar: Avatar
sign_up_requires_approval: Limit sign-ups
severity: Rule
notification_emails:
+ appeal: Someone appeals a moderator decision
digest: Send digest e-mails
favourite: Someone favourited your post
follow: Someone followed you
mention: Someone mentioned you
pending_account: New account needs review
reblog: Someone boosted your post
- report: A new report is submitted
- trending_tag: A new trend requires approval
+ report: New report is submitted
+ trending_tag: New trend requires review
rule:
text: Rule
tag:
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? }
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
- s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities}
+ s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
end
n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s|
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
- s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts}
+ s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes}
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
resources :login_activities, only: [:index]
end
+ namespace :disputes do
+ resources :strikes, only: [:show] do
+ resource :appeal, only: [:create]
+ end
+ end
+
resources :media, only: [:show] do
get :player
end
end
end
end
+
+ namespace :disputes do
+ resources :appeals, only: [:index] do
+ member do
+ post :approve
+ post :reject
+ end
+ end
+ end
end
get '/admin', to: redirect('/admin/dashboard', status: 302)
report: true
pending_account: true
trending_tag: true
+ appeal: true
interactions:
must_be_follower: false
must_be_following: false
--- /dev/null
+class CreateAppeals < ActiveRecord::Migration[6.1]
+ def change
+ create_table :appeals do |t|
+ t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }
+ t.belongs_to :account_warning, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
+ t.text :text, null: false, default: ''
+ t.datetime :approved_at
+ t.belongs_to :approved_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
+ t.datetime :rejected_at
+ t.belongs_to :rejected_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
+ t.timestamps
+ end
+ end
+end
--- /dev/null
+class AddOverruledAtToAccountWarnings < ActiveRecord::Migration[6.1]
+ def change
+ add_column :account_warnings, :overruled_at, :datetime
+ end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_01_18_183123) do
+ActiveRecord::Schema.define(version: 2022_02_10_153119) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.datetime "updated_at", null: false
t.bigint "report_id"
t.string "status_ids", array: true
+ t.datetime "overruled_at"
t.index ["account_id"], name: "index_account_warnings_on_account_id"
t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id"
end
t.bigint "status_ids", array: true
end
+ create_table "appeals", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.bigint "account_warning_id", null: false
+ t.text "text", default: "", null: false
+ t.datetime "approved_at"
+ t.bigint "approved_by_account_id"
+ t.datetime "rejected_at"
+ t.bigint "rejected_by_account_id"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["account_id"], name: "index_appeals_on_account_id"
+ t.index ["account_warning_id"], name: "index_appeals_on_account_warning_id", unique: true
+ t.index ["approved_by_account_id"], name: "index_appeals_on_approved_by_account_id"
+ t.index ["rejected_by_account_id"], name: "index_appeals_on_rejected_by_account_id"
+ end
+
create_table "backups", force: :cascade do |t|
t.bigint "user_id"
t.string "dump_file_name"
add_foreign_key "announcement_reactions", "accounts", on_delete: :cascade
add_foreign_key "announcement_reactions", "announcements", on_delete: :cascade
add_foreign_key "announcement_reactions", "custom_emojis", on_delete: :cascade
+ add_foreign_key "appeals", "account_warnings", on_delete: :cascade
+ add_foreign_key "appeals", "accounts", column: "approved_by_account_id", on_delete: :nullify
+ add_foreign_key "appeals", "accounts", column: "rejected_by_account_id", on_delete: :nullify
+ add_foreign_key "appeals", "accounts", on_delete: :cascade
add_foreign_key "backups", "users", on_delete: :nullify
add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Admin::Disputes::AppealsController, type: :controller do
+ render_views
+
+ before { sign_in current_user, scope: :user }
+
+ let(:target_account) { Fabricate(:account) }
+ let(:strike) { Fabricate(:account_warning, target_account: target_account, action: :suspend) }
+ let(:appeal) { Fabricate(:appeal, strike: strike, account: target_account) }
+
+ before do
+ target_account.suspend!
+ end
+
+ describe 'POST #approve' do
+ let(:current_user) { Fabricate(:user, admin: true) }
+
+ before do
+ allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil))
+ post :approve, params: { id: appeal.id }
+ end
+
+ it 'unsuspends a suspended account' do
+ expect(target_account.reload.suspended?).to be false
+ end
+
+ it 'redirects back to the strike page' do
+ expect(response).to redirect_to(disputes_strike_path(appeal.strike))
+ end
+
+ it 'notifies target account about approved appeal' do
+ expect(UserMailer).to have_received(:appeal_approved).with(target_account.user, appeal)
+ end
+ end
+
+ describe 'POST #reject' do
+ let(:current_user) { Fabricate(:user, admin: true) }
+
+ before do
+ allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil))
+ post :reject, params: { id: appeal.id }
+ end
+
+ it 'redirects back to the strike page' do
+ expect(response).to redirect_to(disputes_strike_path(appeal.strike))
+ end
+
+ it 'notifies target account about rejected appeal' do
+ expect(UserMailer).to have_received(:appeal_rejected).with(target_account.user, appeal)
+ end
+ end
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Disputes::AppealsController, type: :controller do
+ render_views
+
+ before { sign_in current_user, scope: :user }
+
+ let!(:admin) { Fabricate(:user, admin: true) }
+
+ describe '#create' do
+ let(:current_user) { Fabricate(:user) }
+ let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
+
+ before do
+ allow(AdminMailer).to receive(:new_appeal).and_return(double('email', deliver_later: nil))
+ post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } }
+ end
+
+ it 'notifies staff about new appeal' do
+ expect(AdminMailer).to have_received(:new_appeal).with(admin.account, Appeal.last)
+ end
+
+ it 'redirects back to the strike page' do
+ expect(response).to redirect_to(disputes_strike_path(strike.id))
+ end
+ end
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Disputes::StrikesController, type: :controller do
+ render_views
+
+ before { sign_in current_user, scope: :user }
+
+ describe '#show' do
+ let(:current_user) { Fabricate(:user) }
+ let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
+
+ before do
+ get :show, params: { id: strike.id }
+ end
+
+ context 'when meant for the user' do
+ it 'returns http success' do
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ context 'when meant for a different user' do
+ let(:strike) { Fabricate(:account_warning) }
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(:forbidden)
+ end
+ end
+ end
+end
Fabricator(:account_warning) do
- account nil
- target_account nil
- text "MyText"
+ account
+ target_account(fabricator: :account)
+ text { Faker::Lorem.paragraph }
+ action 'suspend'
end
--- /dev/null
+Fabricator(:appeal) do
+ strike(fabricator: :account_warning)
+ account { |attrs| attrs[:strike].target_account }
+ text { Faker::Lorem.paragraph }
+end
def new_trending_links
AdminMailer.new_trending_links(Account.first, PreviewCard.limit(3))
end
+
+ # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal
+ def new_appeal
+ AdminMailer.new_appeal(Account.first, Appeal.first)
+ end
end
UserMailer.warning(User.first, AccountWarning.last)
end
+ # Preview this email at http://localhost:3000/rails/mailers/user_mailer/appeal_approved
+ def appeal_approved
+ UserMailer.appeal_approved(User.first, Appeal.last)
+ end
+
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token
def sign_in_token
UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc)
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Appeal, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end