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
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										40
									
								
								app/controllers/admin/disputes/appeals_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/controllers/admin/disputes/appeals_controller.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										25
									
								
								app/controllers/disputes/appeals_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/controllers/disputes/appeals_controller.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										18
									
								
								app/controllers/disputes/base_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/controllers/disputes/base_controller.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										17
									
								
								app/controllers/disputes/strikes_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/controllers/disputes/strikes_controller.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -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'), | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										20
									
								
								app/helpers/admin/trends/statuses_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/helpers/admin/trends/statuses_helper.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -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; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										49
									
								
								app/models/admin/appeal_filter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/models/admin/appeal_filter.rb
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										58
									
								
								app/models/appeal.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										17
									
								
								app/policies/account_warning_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/policies/account_warning_policy.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										13
									
								
								app/policies/appeal_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/policies/appeal_policy.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										28
									
								
								app/services/appeal_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/services/appeal_service.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										74
									
								
								app/services/approve_appeal_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/services/approve_appeal_service.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -1,7 +0,0 @@ | ||||
| .speech-bubble | ||||
|   .speech-bubble__bubble | ||||
|     = simple_format(h(account_moderation_note.content)) | ||||
|   .speech-bubble__owner | ||||
|     = admin_account_link_to account_moderation_note.account | ||||
|     %time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at | ||||
|     = table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) | ||||
| @@ -1,6 +1,24 @@ | ||||
| .speech-bubble.warning | ||||
|   .speech-bubble__bubble | ||||
|     = Formatter.instance.linkify(account_warning.text) | ||||
|   .speech-bubble__owner | ||||
|     = admin_account_link_to account_warning.account | ||||
|     %time.formatted{ datetime: account_warning.created_at.iso8601 }= l account_warning.created_at | ||||
| = link_to disputes_strike_path(account_warning), class: ['log-entry', account_warning.overruled? && 'log-entry--inactive'] do | ||||
|   .log-entry__header | ||||
|     .log-entry__avatar | ||||
|       = image_tag account_warning.target_account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' | ||||
|     .log-entry__content | ||||
|       .log-entry__title | ||||
|         = t(account_warning.action, scope: 'admin.strikes.actions', name: content_tag(:span, account_warning.account.username, class: 'username'), target: content_tag(:span, account_warning.target_account.acct, class: 'target')).html_safe | ||||
|       .log-entry__timestamp | ||||
|         %time.formatted{ datetime: account_warning.created_at.iso8601 } | ||||
|           = l(account_warning.created_at) | ||||
|  | ||||
|         - if account_warning.report_id.present? | ||||
|           · | ||||
|           = t('admin.reports.title', id: account_warning.report_id) | ||||
|  | ||||
|         - if account_warning.overruled? | ||||
|           · | ||||
|           %span.positive-hint= t('admin.strikes.appeal_approved') | ||||
|         - elsif account_warning.appeal&.pending? | ||||
|           · | ||||
|           %span.warning-hint= t('admin.strikes.appeal_pending') | ||||
|         - elsif account_warning.appeal&.rejected? | ||||
|           · | ||||
|           %span.negative-hint= t('admin.strikes.appeal_rejected') | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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') | ||||
|  | ||||
|   | ||||
							
								
								
									
										21
									
								
								app/views/admin/disputes/appeals/_appeal.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/views/admin/disputes/appeals/_appeal.html.haml
									
									
									
									
									
										Normal 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') | ||||
							
								
								
									
										22
									
								
								app/views/admin/disputes/appeals/index.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/views/admin/disputes/appeals/index.html.haml
									
									
									
									
									
										Normal 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 | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										9
									
								
								app/views/admin_mailer/new_appeal.text.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/admin_mailer/new_appeal.text.erb
									
									
									
									
									
										Normal 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) %> | ||||
							
								
								
									
										20
									
								
								app/views/auth/registrations/_account_warning.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/views/auth/registrations/_account_warning.html.haml
									
									
									
									
									
										Normal 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') | ||||
| @@ -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/ | ||||
|   | ||||
							
								
								
									
										127
									
								
								app/views/disputes/strikes/show.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								app/views/disputes/strikes/show.html.haml
									
									
									
									
									
										Normal 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 | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										59
									
								
								app/views/user_mailer/appeal_approved.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/views/user_mailer/appeal_approved.html.haml
									
									
									
									
									
										Normal 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' | ||||
							
								
								
									
										7
									
								
								app/views/user_mailer/appeal_approved.text.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/views/user_mailer/appeal_approved.text.erb
									
									
									
									
									
										Normal 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 %> | ||||
							
								
								
									
										59
									
								
								app/views/user_mailer/appeal_rejected.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/views/user_mailer/appeal_rejected.html.haml
									
									
									
									
									
										Normal 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' | ||||
							
								
								
									
										7
									
								
								app/views/user_mailer/appeal_rejected.text.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/views/user_mailer/appeal_rejected.text.erb
									
									
									
									
									
										Normal 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 %> | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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" | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| @@ -258,6 +263,7 @@ en: | ||||
|         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 | ||||
| @@ -276,6 +282,7 @@ en: | ||||
|         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}" | ||||
| @@ -307,6 +314,7 @@ en: | ||||
|         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}" | ||||
| @@ -385,6 +393,9 @@ en: | ||||
|       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" | ||||
| @@ -402,6 +413,10 @@ en: | ||||
|       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 | ||||
| @@ -720,6 +735,16 @@ en: | ||||
|       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 | ||||
| @@ -781,6 +806,17 @@ en: | ||||
|       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}) | ||||
| @@ -871,7 +907,6 @@ en: | ||||
|     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. | ||||
| @@ -937,6 +972,32 @@ en: | ||||
|     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: | ||||
| @@ -1501,6 +1562,15 @@ en: | ||||
|     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 | ||||
| @@ -1512,6 +1582,8 @@ en: | ||||
|       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 | ||||
| @@ -1523,7 +1595,6 @@ en: | ||||
|         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 | ||||
|   | ||||
| @@ -27,6 +27,8 @@ en: | ||||
|         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 | ||||
| @@ -119,6 +121,8 @@ en: | ||||
|         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 | ||||
| @@ -197,6 +201,7 @@ en: | ||||
|           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 | ||||
| @@ -204,8 +209,8 @@ en: | ||||
|         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: | ||||
|   | ||||
| @@ -20,7 +20,7 @@ SimpleNavigation::Configuration.run do |navigation| | ||||
|     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 | ||||
| @@ -41,7 +41,7 @@ SimpleNavigation::Configuration.run do |navigation| | ||||
|     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? } | ||||
|   | ||||
| @@ -164,6 +164,12 @@ Rails.application.routes.draw do | ||||
|     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 | ||||
| @@ -324,6 +330,15 @@ Rails.application.routes.draw do | ||||
|         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) | ||||
|   | ||||
| @@ -48,6 +48,7 @@ defaults: &defaults | ||||
|     report: true | ||||
|     pending_account: true | ||||
|     trending_tag: true | ||||
|     appeal: true | ||||
|   interactions: | ||||
|     must_be_follower: false | ||||
|     must_be_following: false | ||||
|   | ||||
							
								
								
									
										14
									
								
								db/migrate/20220124141035_create_appeals.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								db/migrate/20220124141035_create_appeals.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| 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 | ||||
| @@ -0,0 +1,5 @@ | ||||
| class AddOverruledAtToAccountWarnings < ActiveRecord::Migration[6.1] | ||||
|   def change | ||||
|     add_column :account_warnings, :overruled_at, :datetime | ||||
|   end | ||||
| end | ||||
							
								
								
									
										23
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								db/schema.rb
									
									
									
									
									
								
							| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # 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" | ||||
| @@ -135,6 +135,7 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do | ||||
|     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 | ||||
| @@ -243,6 +244,22 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do | ||||
|     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" | ||||
| @@ -1031,6 +1048,10 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do | ||||
|   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 | ||||
|   | ||||
							
								
								
									
										53
									
								
								spec/controllers/admin/disputes/appeals_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								spec/controllers/admin/disputes/appeals_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| 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 | ||||
							
								
								
									
										27
									
								
								spec/controllers/disputes/appeals_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								spec/controllers/disputes/appeals_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| 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 | ||||
							
								
								
									
										30
									
								
								spec/controllers/disputes/strikes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								spec/controllers/disputes/strikes_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| 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 | ||||
| @@ -1,5 +1,6 @@ | ||||
| Fabricator(:account_warning) do | ||||
|   account        nil | ||||
|   target_account nil | ||||
|   text           "MyText" | ||||
|   account | ||||
|   target_account(fabricator: :account) | ||||
|   text { Faker::Lorem.paragraph } | ||||
|   action 'suspend' | ||||
| end | ||||
|   | ||||
							
								
								
									
										5
									
								
								spec/fabricators/appeal_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/fabricators/appeal_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| Fabricator(:appeal) do | ||||
|   strike(fabricator: :account_warning) | ||||
|   account { |attrs| attrs[:strike].target_account } | ||||
|   text { Faker::Lorem.paragraph } | ||||
| end | ||||
| @@ -15,4 +15,9 @@ class AdminMailerPreview < ActionMailer::Preview | ||||
|   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 | ||||
|   | ||||
| @@ -82,6 +82,11 @@ class UserMailerPreview < ActionMailer::Preview | ||||
|     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) | ||||
|   | ||||
							
								
								
									
										5
									
								
								spec/models/appeal_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/appeal_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe Appeal, type: :model do | ||||
|   pending "add some examples to (or delete) #{__FILE__}" | ||||
| end | ||||
		Reference in New Issue
	
	Block a user