FIX: serialize Flags instead of PostActionType (#28362)

### 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
```
This commit is contained in:
Krzysztof Kotlarek
2024-08-14 12:13:46 +10:00
committed by GitHub
parent ed11ee9d05
commit e82e255531
27 changed files with 595 additions and 227 deletions

View File

@@ -37,15 +37,18 @@ module PostGuardian
end
taken = opts[:taken_actions].try(:keys).to_a
post_action_type_view = opts[:post_action_type_view] || PostActionTypeView.new
is_flag =
if (opts[:notify_flag_types] && opts[:additional_message_types])
opts[:notify_flag_types][action_key] || opts[:additional_message_types][action_key]
else
PostActionType.notify_flag_types[action_key] ||
PostActionType.additional_message_types[action_key]
post_action_type_view.notify_flag_types[action_key] ||
post_action_type_view.additional_message_types[action_key]
end
already_taken_this_action = taken.any? && taken.include?(PostActionType.types[action_key])
already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any?
already_taken_this_action =
taken.any? && taken.include?(post_action_type_view.types[action_key])
already_did_flagging =
taken.any? && (taken & post_action_type_view.notify_flag_types.values).any?
result =
if authenticated? && post
@@ -61,7 +64,9 @@ module PostGuardian
# post made by staff, but we don't allow staff flags
return false if is_flag && (!SiteSetting.allow_flagging_staff?) && post&.user&.staff?
return false if is_flag && PostActionType.disabled_flag_types.keys.include?(action_key)
if is_flag && post_action_type_view.disabled_flag_types.keys.include?(action_key)
return false
end
if action_key == :notify_user &&
!@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)
@@ -111,12 +116,13 @@ module PostGuardian
return true if is_admin?
return false unless topic
type_symbol = PostActionType.types[post_action_type_id]
post_action_type_view = PostActionTypeView.new
type_symbol = post_action_type_view.types[post_action_type_id]
return false if type_symbol == :bookmark
return false if type_symbol == :notify_user && !is_moderator?
return can_see_flags?(topic) if PostActionType.is_flag?(type_symbol)
return can_see_flags?(topic) if post_action_type_view.is_flag?(type_symbol)
true
end

View File

@@ -57,7 +57,8 @@ class PostActionCreator
@post = post
@post_action_type_id = post_action_type_id
@post_action_name = PostActionType.types[@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?
@@ -96,7 +97,7 @@ class PostActionCreator
if !post_can_act? || (@queue_for_review && !guardian.is_staff?)
result.forbidden = true
if taken_actions&.keys&.include?(PostActionType.types[@post_action_name])
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"))
@@ -115,7 +116,7 @@ class PostActionCreator
# create meta topic / post if needed
if @message.present? &&
(PostActionType.additional_message_types.keys | %i[spam illegal]).include?(
(@post_action_type_view.additional_message_types.keys | %i[spam illegal]).include?(
@post_action_name,
)
creator = create_message_creator
@@ -170,11 +171,11 @@ class PostActionCreator
private
def flagging_post?
PostActionType.notify_flag_type_ids.include?(@post_action_type_id)
@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 == PostActionType.types[:notify_moderators]
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?
@@ -233,7 +234,8 @@ class PostActionCreator
return if @post.hidden?
return if !@created_by.staff? && @post.user&.staff?
not_auto_action_flag_type = !PostActionType.auto_action_flag_types.include?(@post_action_name)
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
@@ -304,14 +306,14 @@ class PostActionCreator
if post_action
case @post_action_type_id
when *PostActionType.notify_flag_type_ids
when *@post_action_type_view.notify_flag_type_ids
DiscourseEvent.trigger(:flag_created, post_action, self)
when PostActionType.types[:like]
when @post_action_type_view.types[:like]
DiscourseEvent.trigger(:like_created, post_action, self)
end
end
if @post_action_type_id == PostActionType.types[:like]
if @post_action_type_id == @post_action_type_view.types[:like]
GivenDailyLike.increment_for(@created_by.id)
end
@@ -381,7 +383,7 @@ class PostActionCreator
target: @post,
topic: @post.topic,
reviewable_by_moderator: true,
potential_spam: @post_action_type_id == PostActionType.types[:spam],
potential_spam: @post_action_type_id == @post_action_type_view.types[:spam],
payload: {
targets_topic: @targets_topic,
},

View File

@@ -17,6 +17,10 @@ class PostActionDestroyer
new(destroyed_by, post, PostActionType.types[action_key], opts).perform
end
def post_action_type_view
@post_action_type_view ||= PostActionTypeView.new
end
def perform
result = DestroyResult.new
@@ -50,14 +54,14 @@ class PostActionDestroyer
post_action.remove_act!(@destroyed_by)
post_action.post.unhide! if post_action.staff_took_action
if @post_action_type_id == PostActionType.types[:like]
if @post_action_type_id == post_action_type_view.types[:like]
GivenDailyLike.decrement_for(@destroyed_by.id)
end
case @post_action_type_id
when *PostActionType.notify_flag_type_ids
when *post_action_type_view.notify_flag_type_ids
DiscourseEvent.trigger(:flag_destroyed, post_action, self)
when PostActionType.types[:like]
when post_action_type_view.types[:like]
DiscourseEvent.trigger(:like_destroyed, post_action, self)
end
@@ -78,7 +82,7 @@ class PostActionDestroyer
end
def notify_subscribers
name = PostActionType.types[@post_action_type_id]
name = post_action_type_view.types[@post_action_type_id]
if name == :like
@post.publish_change_to_clients!(
:unliked,

View File

@@ -0,0 +1,144 @@
# frozen_string_literal: true
class PostActionTypeView
ATTRIBUTE_NAMES = %i[
id
name
name_key
description
notify_type
auto_action_type
require_message
applies_to
position
enabled
score_type
]
def all_flags
@all_flags ||=
Discourse
.cache
.fetch(PostActionType::POST_ACTION_TYPE_ALL_FLAGS_KEY) do
Flag
.unscoped
.order(:position)
.pluck(ATTRIBUTE_NAMES)
.map { |attributes| ATTRIBUTE_NAMES.zip(attributes).to_h }
end
end
def flag_settings
@flag_settings ||= PostActionType.flag_settings
end
def types
if overridden_by_plugin_or_skipped_db?
return Enum.new(like: PostActionType::LIKE_POST_ACTION_ID).merge!(flag_settings.flag_types)
end
Enum.new(like: PostActionType::LIKE_POST_ACTION_ID).merge(flag_types)
end
def overridden_by_plugin_or_skipped_db?
flag_settings.flag_types.present? || GlobalSetting.skip_db?
end
def auto_action_flag_types
return flag_settings.auto_action_types if overridden_by_plugin_or_skipped_db?
flag_enum(all_flags.select { |flag| flag[:auto_action_type] })
end
def public_types
types.except(*flag_types.keys << :notify_user)
end
def public_type_ids
Discourse
.cache
.fetch(PostActionType::POST_ACTION_TYPE_PUBLIC_TYPE_IDS_KEY) { public_types.values }
end
def flag_types_without_additional_message
return flag_settings.without_additional_message_types if overridden_by_plugin_or_skipped_db?
flag_enum(flags.reject { |flag| flag[:require_message] })
end
def flags
all_flags.reject do |flag|
flag[:score_type] || flag[:id] == PostActionType::LIKE_POST_ACTION_ID
end
end
def flag_types
return flag_settings.flag_types if overridden_by_plugin_or_skipped_db?
flag_enum(flags)
end
def score_types
return flag_settings.flag_types if overridden_by_plugin_or_skipped_db?
flag_enum(all_flags.filter { |flag| flag[:score_type] })
end
# flags resulting in mod notifications
def notify_flag_type_ids
notify_flag_types.values
end
def notify_flag_types
return flag_settings.notify_types if overridden_by_plugin_or_skipped_db?
flag_enum(all_flags.select { |flag| flag[:notify_type] })
end
def topic_flag_types
if overridden_by_plugin_or_skipped_db?
flag_settings.topic_flag_types
else
flag_enum(all_flags.select { |flag| flag[:applies_to].include?("Topic") })
end
end
def disabled_flag_types
flag_enum(all_flags.reject { |flag| flag[:enabled] })
end
def additional_message_types
return flag_settings.additional_message_types if overridden_by_plugin_or_skipped_db?
flag_enum(all_flags.select { |flag| flag[:require_message] })
end
def names
all_flags.reduce({}) do |acc, f|
acc[f[:id]] = f[:name]
acc
end
end
def descriptions
all_flags.reduce({}) do |acc, f|
acc[f[:id]] = f[:description]
acc
end
end
def applies_to
all_flags.reduce({}) do |acc, f|
acc[f[:id]] = f[:applies_to]
acc
end
end
def is_flag?(sym)
flag_types.valid?(sym)
end
private
def flag_enum(scope)
Enum.new(
scope.reduce({}) do |acc, f|
acc[f[:name_key].to_sym] = f[:id]
acc
end,
)
end
end

View File

@@ -348,6 +348,10 @@ class PostDestroyer
Jobs.enqueue(:feature_topic_users, topic_id: @post.topic_id)
end
def post_action_type_view
@post_action_type_view ||= PostActionTypeView.new
end
def trash_public_post_actions
if public_post_actions = PostAction.publics.where(post_id: @post.id)
public_post_actions.each { |pa| permanent? ? pa.destroy! : pa.trash!(@user) }
@@ -357,7 +361,7 @@ class PostDestroyer
@post.custom_fields["deleted_public_actions"] = public_post_actions.ids
@post.save_custom_fields
f = PostActionType.public_types.map { |k, _| ["#{k}_count", 0] }
f = post_action_type_view.public_types.map { |k, _| ["#{k}_count", 0] }
Post.with_deleted.where(id: @post.id).update_all(Hash[*f.flatten])
end
end
@@ -387,7 +391,7 @@ class PostDestroyer
# ReviewableScore#types is a superset of PostActionType#flag_types.
# If the reviewable score type is not on the latter, it means it's not a flag by a user and
# must be an automated flag like `needs_approval`. There's no flag reason for these kind of types.
flag_type = PostActionType.flag_types[rs.reviewable_score_type]
flag_type = post_action_type_view.flag_types[rs.reviewable_score_type]
return unless flag_type
notify_responders = options[:notify_responders]

View File

@@ -598,17 +598,28 @@ class TopicView
ReviewableQueuedPost.pending.where(target_created_by: @user, topic: @topic).order(:created_at)
end
def post_action_type_view
@post_action_type_view ||= PostActionTypeView.new
end
def actions_summary
return @actions_summary unless @actions_summary.nil?
@actions_summary = []
return @actions_summary unless post = posts&.first
PostActionType.topic_flag_types.each do |sym, id|
post_action_type_view.topic_flag_types.each do |sym, id|
@actions_summary << {
id: id,
count: 0,
hidden: false,
can_act: @guardian.post_can_act?(post, sym),
can_act:
@guardian.post_can_act?(
post,
sym,
opts: {
post_action_type_view: post_action_type_view,
},
),
}
end
@@ -623,22 +634,6 @@ class TopicView
@pm_params ||= TopicQuery.new(@user).get_pm_params(topic)
end
def flag_types
@flag_types ||= PostActionType.types
end
def public_flag_types
@public_flag_types ||= PostActionType.public_types
end
def notify_flag_types
@notify_flag_types ||= PostActionType.notify_flag_types
end
def additional_message_types
@additional_message_types ||= PostActionType.additional_message_types
end
def suggested_topics
if @include_suggested
@suggested_topics ||=