mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 04:43:37 +08:00
e82e255531
### Why? Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems. ### Solution When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons. At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance. To test backward compatibility you can add this code to any plugin ```ruby replace_flags do |flag_settings| flag_settings.add( 4, :inappropriate, topic_type: true, notify_type: true, auto_action_type: true, ) flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true) end ```
412 lines
12 KiB
Ruby
412 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class PostActionCreator
|
|
class CreateResult < PostActionResult
|
|
attr_accessor :post_action, :reviewable, :reviewable_score
|
|
end
|
|
|
|
# Shortcut methods for easier invocation
|
|
class << self
|
|
def create(
|
|
created_by,
|
|
post,
|
|
action_key,
|
|
message: nil,
|
|
created_at: nil,
|
|
reason: nil,
|
|
silent: false
|
|
)
|
|
new(
|
|
created_by,
|
|
post,
|
|
PostActionType.types[action_key],
|
|
message: message,
|
|
created_at: created_at,
|
|
reason: reason,
|
|
silent: silent,
|
|
).perform
|
|
end
|
|
|
|
%i[like off_topic spam inappropriate].each do |action|
|
|
define_method(action) do |created_by, post, silent = false|
|
|
create(created_by, post, action, silent: silent)
|
|
end
|
|
end
|
|
%i[notify_moderators notify_user illegal].each do |action|
|
|
define_method(action) do |created_by, post, message = nil|
|
|
create(created_by, post, action, message: message)
|
|
end
|
|
end
|
|
end
|
|
|
|
def initialize(
|
|
created_by,
|
|
post,
|
|
post_action_type_id,
|
|
is_warning: false,
|
|
message: nil,
|
|
take_action: false,
|
|
flag_topic: false,
|
|
created_at: nil,
|
|
queue_for_review: false,
|
|
reason: nil,
|
|
silent: false
|
|
)
|
|
@created_by = created_by
|
|
@created_at = created_at || Time.zone.now
|
|
|
|
@post = post
|
|
@post_action_type_id = post_action_type_id
|
|
@post_action_type_view = PostActionTypeView.new
|
|
@post_action_name = @post_action_type_view.types[@post_action_type_id]
|
|
|
|
@is_warning = is_warning
|
|
@take_action = take_action && guardian.is_staff?
|
|
|
|
@message = message
|
|
@flag_topic = flag_topic
|
|
@meta_post = nil
|
|
|
|
@reason = reason
|
|
@queue_for_review = queue_for_review
|
|
|
|
@reason = "queued_by_staff" if reason.nil? && @queue_for_review
|
|
|
|
@silent = silent
|
|
end
|
|
|
|
def post_can_act?
|
|
guardian.post_can_act?(
|
|
@post,
|
|
@post_action_name,
|
|
opts: {
|
|
is_warning: @is_warning,
|
|
taken_actions: taken_actions,
|
|
},
|
|
)
|
|
end
|
|
|
|
def taken_actions
|
|
return @taken_actions if defined?(@taken_actions)
|
|
@taken_actions = PostAction.counts_for([@post].compact, @created_by)[@post&.id]
|
|
end
|
|
|
|
def perform
|
|
result = CreateResult.new
|
|
|
|
if !post_can_act? || (@queue_for_review && !guardian.is_staff?)
|
|
result.forbidden = true
|
|
|
|
if taken_actions&.keys&.include?(@post_action_type_view.types[@post_action_name])
|
|
result.add_error(I18n.t("action_already_performed"))
|
|
else
|
|
result.add_error(I18n.t("invalid_access"))
|
|
end
|
|
return result
|
|
end
|
|
|
|
PostAction.limit_action!(@created_by, @post, @post_action_type_id)
|
|
|
|
reviewable = Reviewable.includes(:reviewable_scores).find_by(target: @post)
|
|
|
|
if reviewable && flagging_post? && cannot_flag_again?(reviewable)
|
|
result.add_error(I18n.t("reviewables.already_handled"))
|
|
return result
|
|
end
|
|
|
|
# create meta topic / post if needed
|
|
if @message.present? &&
|
|
(@post_action_type_view.additional_message_types.keys | %i[spam illegal]).include?(
|
|
@post_action_name,
|
|
)
|
|
creator = create_message_creator
|
|
# We need to check if the creator exists because it's possible `create_message_creator` returns nil
|
|
# in the event that a `post_action_notify_user_handler` evaluated to false, haulting the post creation.
|
|
if creator
|
|
post = creator.create
|
|
if creator.errors.present?
|
|
result.add_errors_from(creator)
|
|
return result
|
|
end
|
|
@meta_post = post
|
|
end
|
|
end
|
|
|
|
begin
|
|
post_action = create_post_action
|
|
|
|
if post_action.blank? || post_action.errors.present?
|
|
result.add_errors_from(post_action)
|
|
else
|
|
create_reviewable(result)
|
|
enforce_rules
|
|
UserActionManager.post_action_created(post_action)
|
|
PostActionNotifier.post_action_created(post_action) if !@silent
|
|
notify_subscribers
|
|
|
|
# agree with other flags
|
|
if @take_action && reviewable = @post.reviewable_flag
|
|
result.reviewable.perform(@created_by, :agree_and_keep)
|
|
post_action.try(:update_counters)
|
|
end
|
|
|
|
result.success = true
|
|
result.post_action = post_action
|
|
end
|
|
rescue ActiveRecord::RecordNotUnique
|
|
# If the user already performed this action, it's probably due to a different browser tab
|
|
# or non-debounced clicking. We can ignore.
|
|
result.success = true
|
|
result.post_action =
|
|
PostAction.find_by(
|
|
user: @created_by,
|
|
post: @post,
|
|
post_action_type_id: @post_action_type_id,
|
|
)
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
private
|
|
|
|
def flagging_post?
|
|
@post_action_type_view.notify_flag_type_ids.include?(@post_action_type_id)
|
|
end
|
|
|
|
def cannot_flag_again?(reviewable)
|
|
return false if @post_action_type_id == @post_action_type_view.types[:notify_moderators]
|
|
flag_type_already_used =
|
|
reviewable.reviewable_scores.any? do |rs|
|
|
rs.reviewable_score_type == @post_action_type_id && !rs.pending?
|
|
end
|
|
not_edited_since_last_review =
|
|
@post.last_version_at.blank? || reviewable.updated_at > @post.last_version_at
|
|
handled_recently =
|
|
reviewable.updated_at > SiteSetting.cooldown_hours_until_reflag.to_i.hours.ago
|
|
|
|
flag_type_already_used && not_edited_since_last_review && handled_recently
|
|
end
|
|
|
|
def notify_subscribers
|
|
if @post_action_name == :like
|
|
@post.publish_change_to_clients! :liked,
|
|
{
|
|
likes_count: @post.like_count + 1,
|
|
user_id: @created_by.id,
|
|
}
|
|
elsif self.class.notify_types.include?(@post_action_name)
|
|
@post.publish_change_to_clients! :acted
|
|
end
|
|
end
|
|
|
|
def self.notify_types
|
|
@notify_types ||= PostActionType.notify_flag_types.keys
|
|
end
|
|
|
|
def enforce_rules
|
|
auto_close_if_threshold_reached
|
|
auto_hide_if_needed
|
|
SpamRule::AutoSilence.new(@post.user, @post).perform
|
|
end
|
|
|
|
def auto_close_if_threshold_reached
|
|
return if topic.nil? || topic.closed?
|
|
return unless topic.auto_close_threshold_reached?
|
|
|
|
# the threshold has been reached, we will close the topic waiting for intervention
|
|
topic.update_status(
|
|
"closed",
|
|
true,
|
|
Discourse.system_user,
|
|
message:
|
|
I18n.t("temporarily_closed_due_to_flags", count: SiteSetting.num_hours_to_close_topic),
|
|
)
|
|
|
|
topic.set_or_create_timer(
|
|
TopicTimer.types[:open],
|
|
SiteSetting.num_hours_to_close_topic,
|
|
by_user: Discourse.system_user,
|
|
)
|
|
end
|
|
|
|
def auto_hide_if_needed
|
|
return if @post.hidden?
|
|
return if !@created_by.staff? && @post.user&.staff?
|
|
|
|
not_auto_action_flag_type =
|
|
!@post_action_type_view.auto_action_flag_types.include?(@post_action_name)
|
|
return if not_auto_action_flag_type && !@queue_for_review
|
|
|
|
if @queue_for_review
|
|
if @post.is_first_post?
|
|
@post.topic.update_status(
|
|
"visible",
|
|
false,
|
|
@created_by,
|
|
{ visibility_reason_id: Topic.visibility_reasons[:op_flag_threshold_reached] },
|
|
)
|
|
end
|
|
|
|
@post.hide!(
|
|
@post_action_type_id,
|
|
Post.hidden_reasons[:flag_threshold_reached],
|
|
custom_message: :queued_by_staff,
|
|
)
|
|
return
|
|
end
|
|
|
|
if trusted_spam_flagger?
|
|
@post.hide!(@post_action_type_id, Post.hidden_reasons[:flagged_by_tl3_user])
|
|
return
|
|
end
|
|
|
|
score = ReviewableFlaggedPost.find_by(target: @post)&.score || 0
|
|
if score >= Reviewable.score_required_to_hide_post || @take_action
|
|
@post.hide!(@post_action_type_id)
|
|
end
|
|
end
|
|
|
|
# Special case: If you have TL3 and the user is TL0, and the flag is spam,
|
|
# hide it immediately.
|
|
def trusted_spam_flagger?
|
|
SiteSetting.high_trust_flaggers_auto_hide_posts && @post_action_name == :spam &&
|
|
@created_by.has_trust_level?(TrustLevel[3]) && @post.user&.trust_level == TrustLevel[0]
|
|
end
|
|
|
|
def create_post_action
|
|
@targets_topic = !!(@post.topic.reload.posts_count != 1 if @flag_topic && @post.topic)
|
|
|
|
where_attrs = {
|
|
post_id: @post.id,
|
|
user_id: @created_by.id,
|
|
post_action_type_id: @post_action_type_id,
|
|
}
|
|
|
|
action_attrs = {
|
|
staff_took_action: @take_action,
|
|
related_post_id: @meta_post&.id,
|
|
targets_topic: @targets_topic,
|
|
created_at: @created_at,
|
|
}
|
|
|
|
# First try to revive a trashed record
|
|
post_action = PostAction.where(where_attrs).with_deleted.where("deleted_at IS NOT NULL").first
|
|
|
|
if post_action
|
|
post_action.recover!
|
|
action_attrs.each { |attr, val| post_action.public_send("#{attr}=", val) }
|
|
post_action.save
|
|
else
|
|
post_action = PostAction.create(where_attrs.merge(action_attrs))
|
|
if post_action && post_action.errors.count == 0
|
|
BadgeGranter.queue_badge_grant(Badge::Trigger::PostAction, post_action: post_action)
|
|
end
|
|
end
|
|
|
|
if post_action
|
|
case @post_action_type_id
|
|
when *@post_action_type_view.notify_flag_type_ids
|
|
DiscourseEvent.trigger(:flag_created, post_action, self)
|
|
when @post_action_type_view.types[:like]
|
|
DiscourseEvent.trigger(:like_created, post_action, self)
|
|
end
|
|
end
|
|
|
|
if @post_action_type_id == @post_action_type_view.types[:like]
|
|
GivenDailyLike.increment_for(@created_by.id)
|
|
end
|
|
|
|
post_action
|
|
rescue ActiveRecord::RecordNotUnique
|
|
# can happen despite being .create
|
|
PostAction.where(where_attrs).first
|
|
end
|
|
|
|
def create_message_creator
|
|
title =
|
|
I18n.t(
|
|
"post_action_types.#{@post_action_name}.email_title",
|
|
title: @post.topic.title,
|
|
locale: SiteSetting.default_locale,
|
|
default: I18n.t("post_action_types.illegal.email_title"),
|
|
)
|
|
|
|
body =
|
|
I18n.t(
|
|
"post_action_types.#{@post_action_name}.email_body",
|
|
message: @message,
|
|
link: "#{Discourse.base_url}#{@post.url}",
|
|
locale: SiteSetting.default_locale,
|
|
default: I18n.t("post_action_types.illegal.email_body"),
|
|
)
|
|
|
|
create_args = {
|
|
archetype: Archetype.private_message,
|
|
is_warning: @is_warning,
|
|
title: title.truncate(SiteSetting.max_topic_title_length, separator: /\s/),
|
|
raw: body,
|
|
}
|
|
|
|
if @post_action_name == :notify_user
|
|
create_args[:subtype] = TopicSubtype.notify_user
|
|
|
|
create_args[:target_usernames] = @post.user.username
|
|
|
|
# Evaluate DiscoursePluginRegistry.post_action_notify_user_handlers.
|
|
# If any return false, return early from this method
|
|
handler_values =
|
|
DiscoursePluginRegistry.post_action_notify_user_handlers.map do |handler|
|
|
handler.call(@created_by, @post, @message)
|
|
end
|
|
return if handler_values.any? { |value| value == false }
|
|
else
|
|
create_args[:subtype] = TopicSubtype.notify_moderators
|
|
create_args[:target_group_names] = [Group[:moderators].name]
|
|
|
|
if SiteSetting.enable_category_group_moderation? &&
|
|
@post.topic&.category&.reviewable_by_group_id?
|
|
create_args[:target_group_names] << @post.topic.category.reviewable_by_group.name
|
|
end
|
|
end
|
|
|
|
PostCreator.new(@created_by, create_args)
|
|
end
|
|
|
|
def create_reviewable(result)
|
|
return unless flagging_post?
|
|
return if @post.user_id.to_i < 0
|
|
|
|
result.reviewable =
|
|
ReviewableFlaggedPost.needs_review!(
|
|
created_by: @created_by,
|
|
target: @post,
|
|
topic: @post.topic,
|
|
reviewable_by_moderator: true,
|
|
potential_spam: @post_action_type_id == @post_action_type_view.types[:spam],
|
|
payload: {
|
|
targets_topic: @targets_topic,
|
|
},
|
|
)
|
|
|
|
result.reviewable_score =
|
|
result.reviewable.add_score(
|
|
@created_by,
|
|
@post_action_type_id,
|
|
created_at: @created_at,
|
|
take_action: @take_action,
|
|
meta_topic_id: @meta_post&.topic_id,
|
|
reason: @reason,
|
|
force_review: trusted_spam_flagger?,
|
|
)
|
|
end
|
|
|
|
def guardian
|
|
@guardian ||= Guardian.new(@created_by)
|
|
end
|
|
|
|
def topic
|
|
@post.topic
|
|
end
|
|
end
|