Fix #4669: Track moderation report status.

* Add ability to mark moderation reports as 'handled' or 'rejected'.
* Automatically mark reports as handled when the comment or forum post
  is deleted.
* Send a dmail to the reporter when their report is handled.
* Don't show the report notice on comments or forum posts when all
  reports against it have been handled or rejected.
* Add a fix script to mark all existing reports for deleted comments,
  forum posts, or dmails as handled.
This commit is contained in:
evazion
2022-01-20 17:41:05 -06:00
parent 98aee048f2
commit c8d27c2719
20 changed files with 160 additions and 16 deletions

View File

@@ -42,6 +42,6 @@ class CommentComponent < ApplicationComponent
end
def reported?
policy(ModerationReport).can_see_moderation_reports? && comment.moderation_reports.present?
policy(ModerationReport).can_see_moderation_reports? && comment.pending_moderation_reports.present?
end
end

View File

@@ -85,7 +85,7 @@
<% if reported? %>
<li class="moderation-report-notice">
Reported (<%= link_to pluralize(comment.moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "Comment", model_id: comment.id }) %>)
Reported (<%= link_to pluralize(comment.pending_moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "Comment", model_id: comment.id, status: "pending" }) %>)
</li>
<% end %>

View File

@@ -12,7 +12,7 @@ class CommentSectionComponent < ApplicationComponent
@comments = @post.comments.order(id: :asc)
@comments = @comments.includes(:creator)
@comments = @comments.includes(:votes) if !current_user.is_anonymous?
@comments = @comments.includes(:moderation_reports) if policy(ModerationReport).can_see_moderation_reports?
@comments = @comments.includes(:pending_moderation_reports) if policy(ModerationReport).can_see_moderation_reports?
@comments = @comments.last(limit) if limit.present?
@dtext_data = DText.preprocess(@comments.map(&:body))

View File

@@ -1,9 +1,9 @@
# frozen_string_literal: true
class ForumPostComponent < ApplicationComponent
attr_reader :forum_post, :original_forum_post_id, :dtext_data, :moderation_reports, :current_user
attr_reader :forum_post, :original_forum_post_id, :dtext_data, :current_user
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :policy, :data_attributes_for, to: :helpers
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :data_attributes_for, to: :helpers
with_collection_parameter :forum_post
@@ -12,7 +12,7 @@ class ForumPostComponent < ApplicationComponent
original_forum_post_id = forum_topic.original_post&.id
forum_posts = forum_posts.includes(:creator, :bulk_update_request)
forum_posts = forum_posts.includes(:moderation_reports) if Pundit.policy!(current_user, ModerationReport).show?
forum_posts = forum_posts.includes(:pending_moderation_reports) if Pundit.policy(current_user, ModerationReport).can_see_moderation_reports?
super(forum_posts, dtext_data: dtext_data, original_forum_post_id: original_forum_post_id, current_user: current_user)
end
@@ -29,7 +29,7 @@ class ForumPostComponent < ApplicationComponent
policy(forum_post).show_deleted?
end
def has_moderation_reports?
policy(ModerationReport).can_see_moderation_reports? && forum_post.moderation_reports.present?
def reported?
policy(ModerationReport).can_see_moderation_reports? && forum_post.pending_moderation_reports.present?
end
end

View File

@@ -1,4 +1,4 @@
<%= tag.article class: "forum-post message", id: "forum_post_#{forum_post.id}", data: { "is-reported": has_moderation_reports?, **data_attributes_for(forum_post, "", forum_post.html_data_attributes) } do %>
<%= tag.article class: "forum-post message", id: "forum_post_#{forum_post.id}", data: { "is-reported": reported?, **data_attributes_for(forum_post, "", forum_post.html_data_attributes) } do %>
<div class="author">
<div class="author-name">
<%= link_to_user forum_post.creator %>
@@ -24,8 +24,10 @@
<% end %>
<% end %>
<% if has_moderation_reports? %>
<li class="moderation-report-notice">Reported (<%= link_to pluralize(forum_post.moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "ForumPost", model_id: forum_post.id }) %>)</li>
<% if reported? %>
<li class="moderation-report-notice">
Reported (<%= link_to pluralize(forum_post.pending_moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "ForumPost", model_id: forum_post.id, status: "pending" }) %>)
</li>
<% end %>
<%= render PopupMenuComponent.new do |menu| %>

View File

@@ -29,4 +29,12 @@ class ModerationReportsController < ApplicationController
flash.now[:notice] = @moderation_report.valid? ? "Report submitted" : @moderation_report.errors.full_messages.join("; ")
respond_with(@moderation_report)
end
def update
@moderation_report = authorize ModerationReport.find(params[:id])
@moderation_report.update(permitted_attributes(@moderation_report))
flash.now[:notice] = @moderation_report.valid? ? "Report updated" : @moderation_report.errors.full_messages.join("; ")
respond_with(@moderation_report)
end
end

View File

@@ -6,11 +6,13 @@ class Comment < ApplicationRecord
belongs_to_updater
has_many :moderation_reports, as: :model, dependent: :destroy
has_many :pending_moderation_reports, -> { pending }, as: :model, class_name: "ModerationReport"
has_many :votes, class_name: "CommentVote", dependent: :destroy
validates :body, presence: true, length: { maximum: 15_000 }, if: :body_changed?
before_create :autoreport_spam
before_save :handle_reports_on_deletion
after_create :update_last_commented_at_on_create
after_update(:if => ->(rec) {(!rec.is_deleted? || !rec.saved_change_to_is_deleted?) && CurrentUser.id != rec.creator_id}) do |rec|
ModAction.log("comment ##{rec.id} updated by #{CurrentUser.user.name}", :comment_update)
@@ -78,6 +80,13 @@ class Comment < ApplicationRecord
end
end
def handle_reports_on_deletion
return unless Pundit.policy!(updater, ModerationReport).update?
return unless moderation_reports.pending.present? && is_deleted_change == [false, true]
moderation_reports.pending.update!(status: :handled)
end
def quoted_response
DText.quote(body, creator.name)
end

View File

@@ -8,6 +8,7 @@ class ForumPost < ApplicationRecord
belongs_to :topic, class_name: "ForumTopic", inverse_of: :forum_posts
has_many :moderation_reports, as: :model
has_many :pending_moderation_reports, -> { pending }, as: :model, class_name: "ModerationReport"
has_many :votes, class_name: "ForumPostVote"
has_one :tag_alias
has_one :tag_implication
@@ -16,6 +17,7 @@ class ForumPost < ApplicationRecord
validates :body, presence: true, length: { maximum: 200_000 }, if: :body_changed?
before_create :autoreport_spam
before_save :handle_reports_on_deletion
after_create :update_topic_updated_at_on_create
after_update :update_topic_updated_at_on_update_for_original_posts
after_destroy :update_topic_updated_at_on_destroy
@@ -157,6 +159,12 @@ class ForumPost < ApplicationRecord
end
end
def handle_reports_on_deletion
return unless moderation_reports.pending.present? && is_deleted_change == [false, true]
moderation_reports.pending.update!(status: :handled)
end
def async_send_discord_notification
DiscordNotificationJob.perform_later(forum_post: self)
end

View File

@@ -12,12 +12,19 @@ class ModerationReport < ApplicationRecord
after_create :create_forum_post!
after_create :autoban_reported_user
after_save :notify_reporter
scope :dmail, -> { where(model_type: "Dmail") }
scope :comment, -> { where(model_type: "Comment") }
scope :forum_post, -> { where(model_type: "ForumPost") }
scope :recent, -> { where("moderation_reports.created_at >= ?", 1.week.ago) }
enum status: {
pending: 0,
rejected: 1,
handled: 2,
}
def self.model_types
MODEL_TYPES
end
@@ -73,6 +80,15 @@ class ModerationReport < ApplicationRecord
end
end
def notify_reporter
return if creator == User.system
return unless handled? && status_before_last_save != :handled
Dmail.create_automated(to: creator, title: "Thank you for reporting #{model.dtext_shortlink}", body: <<~EOS)
Thank you for reporting #{model.dtext_shortlink}. Action has been taken against the user.
EOS
end
def reported_user
case model
when Comment, ForumPost
@@ -85,7 +101,7 @@ class ModerationReport < ApplicationRecord
end
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model)
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params)

View File

@@ -13,11 +13,19 @@ class ModerationReportPolicy < ApplicationPolicy
unbanned? && policy(record.model).reportable?
end
def update?
user.is_moderator?
end
def can_see_moderation_reports?
user.is_moderator?
end
def permitted_attributes
def permitted_attributes_for_create
[:model_type, :model_id, :reason]
end
def permitted_attributes_for_update
[:status]
end
end

View File

@@ -7,6 +7,7 @@
<%= f.input :reason_matches, label: "Reason", input_html: { value: params[:search][:reason_matches] } %>
<%= f.input :model_id, label: "ID", input_html: { value: params[:search][:model_id] } %>
<%= f.input :model_type, label: "Type", collection: ModerationReport.model_types, include_blank: true, selected: params[:search][:model_type] %>
<%= f.input :status, label: "Status", collection: ModerationReport.statuses.keys, include_blank: true, selected: params[:search][:status] %>
<%= f.submit "Search" %>
<% end %>
@@ -26,6 +27,10 @@
</span>
<% end %>
<% t.column "Status" do |report| %>
<%= link_to report.status.capitalize, moderation_reports_path(search: { status: report.status }) %>
<% end %>
<% t.column "Reported user" do |report| %>
<%= link_to_user report.reported_user %>
<% end %>
@@ -35,6 +40,18 @@
<%= link_to "»", moderation_reports_path(search: { creator_name: report.creator.name }) %>
<div><%= time_ago_in_words_tagged(report.created_at) %></div>
<% end %>
<% t.column column: "control" do |report| %>
<%= render PopupMenuComponent.new do |menu| %>
<% menu.item do %>
<%= link_to "Mark as handled", moderation_report_path(report.id), method: :put, remote: true, "data-params": "moderation_report[status]=handled" %>
<% end %>
<% menu.item do %>
<%= link_to "Reject", moderation_report_path(report.id), method: :put, remote: true, "data-params": "moderation_report[status]=rejected" %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= numbered_paginator(@moderation_reports) %>

View File

@@ -0,0 +1 @@
location.reload();