discourse/lib/post_action_creator.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

301 lines
8.5 KiB
Ruby

# frozen_string_literal: true
require_dependency 'post_action_result'
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)
new(created_by, post, PostActionType.types[action_key], message: message, created_at: created_at).perform
end
[:like, :off_topic, :spam, :inappropriate, :bookmark].each do |action|
define_method(action) do |created_by, post|
create(created_by, post, action)
end
end
[:notify_moderators, :notify_user].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
)
@created_by = created_by
@created_at = created_at || Time.zone.now
@post = post
@post_action_type_id = post_action_type_id
@post_action_name = PostActionType.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
end
def perform
result = CreateResult.new
unless guardian.post_can_act?(
@post,
@post_action_name,
opts: {
is_warning: @is_warning,
taken_actions: PostAction.counts_for([@post].compact, @created_by)[@post&.id]
}
)
result.forbidden = true
result.add_error(I18n.t("invalid_access"))
return result
end
PostAction.limit_action!(@created_by, @post, @post_action_type_id)
# create meta topic / post if needed
if @message.present? && [:notify_moderators, :notify_user, :spam].include?(@post_action_name)
creator = create_message_creator
post = creator.create
if creator.errors.present?
result.add_errors_from(creator)
return result
end
@meta_post = post
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)
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 proably 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 notify_subscribers
if self.class.notify_types.include?(@post_action_name)
@post.publish_change_to_clients! :acted
end
end
def self.notify_types
@notify_types ||= ([:like] + 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?
if @post_action_name == :spam &&
@created_by.has_trust_level?(TrustLevel[3]) &&
@post.user&.trust_level == TrustLevel[0]
@post.hide!(@post_action_type_id, Post.hidden_reasons[:flagged_by_tl3_user])
elsif PostActionType.auto_action_flag_types.include?(@post_action_name)
if @created_by.has_trust_level?(TrustLevel[4]) &&
!@created_by.staff? &&
@post.user&.trust_level != TrustLevel[4]
@post.hide!(@post_action_type_id, Post.hidden_reasons[:flagged_by_tl4_user])
elsif SiteSetting.score_required_to_hide_post > 0
score = ReviewableFlaggedPost.find_by(target: @post)&.score || 0
if score >= SiteSetting.score_required_to_hide_post
@post.hide!(@post_action_type_id)
end
end
end
end
def create_post_action
@targets_topic = !!(
if @flag_topic && @post.topic
@post.topic.reload.posts_count != 1
end
)
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
PostActionNotifier.post_action_created(post_action)
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 && PostActionType.notify_flag_type_ids.include?(@post_action_type_id)
DiscourseEvent.trigger(:flag_created, post_action)
end
GivenDailyLike.increment_for(@created_by.id) if @post_action_type_id == PostActionType.types[:like]
post_action
rescue ActiveRecord::RecordNotUnique
# can happen despite being .create
# since already bookmarked
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
)
body = I18n.t(
"post_action_types.#{@post_action_name}.email_body",
message: @message,
link: "#{Discourse.base_url}#{@post.url}",
locale: SiteSetting.default_locale
)
create_args = {
archetype: Archetype.private_message,
is_warning: @is_warning,
title: title.truncate(SiteSetting.max_topic_title_length, separator: /\s/),
raw: body
}
if [:notify_moderators, :spam].include?(@post_action_name)
create_args[:subtype] = TopicSubtype.notify_moderators
create_args[:target_group_names] = Group[:moderators].name
else
create_args[:subtype] = TopicSubtype.notify_user
create_args[:target_usernames] =
if @post_action_name == :notify_user
@post.user.username
elsif @post_action_name != :notify_moderators
# this is a hack to allow a PM with no recipients, we should think through
# a cleaner technique, a PM with myself is valid for flagging
'x'
end
end
PostCreator.new(@created_by, create_args)
end
def create_reviewable(result)
return unless PostActionType.notify_flag_type_ids.include?(@post_action_type_id)
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 == PostActionType.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,
)
end
def guardian
@guardian ||= Guardian.new(@created_by)
end
def topic
@post.topic
end
end