0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 18:09:26 +08:00
discourse/lib/new_post_manager.rb
Penar Musaraj 6b2f4579ba
DEV: auto-silence only if a post is queued (#41435)
A new user could previously get in a permanent silenced state by simply
uploading 2 or more media items in their first post by uploading quickly
and not adding any content. Our code would silence them (via fast typing
detection) and not queue the reviewable because the post would be
rejected, from `newuser_max_embedded_media`.

This fix ensures that the silence only happens if the post is queued for
review.
2026-07-03 14:06:55 -04:00

327 lines
9.7 KiB
Ruby
Vendored

# frozen_string_literal: true
# Determines what actions should be taken with new posts.
#
# The default action is to create the post, but this can be extended
# with `NewPostManager.add_handler` to take other approaches depending
# on the user or input.
class NewPostManager
attr_reader :user, :args
FAST_TYPING_THRESHOLD_MAP = { disabled: 0, low: 1000, standard: 3000, high: 5000 }
def self.sorted_handlers
@sorted_handlers ||= clear_handlers!
end
def self.handlers
sorted_handlers.map { |h| h[:proc] }
end
def self.plugin_payload_attributes
@payload_attributes ||= []
end
def self.add_plugin_payload_attribute(attribute)
plugin_payload_attributes << attribute
end
def self.clear_handlers!
@sorted_handlers = []
end
def self.add_handler(priority = 0, &block)
sorted_handlers << { priority: priority, proc: block }
@sorted_handlers.sort_by! { |h| -h[:priority] }
end
def self.is_first_post?(manager)
user = manager.user
args = manager.args
!!(args[:first_post_checks] && user.post_count == 0 && user.topic_count == 0)
end
def self.is_fast_typer?(manager)
args = manager.args
is_first_post?(manager) &&
args[:typing_duration_msecs].to_i <
FAST_TYPING_THRESHOLD_MAP[SiteSetting.fast_typing_threshold.to_sym] &&
SiteSetting.auto_silence_fast_typers_on_first_post &&
manager.user.trust_level <= SiteSetting.auto_silence_fast_typers_max_trust_level
end
def self.auto_silence?(manager)
is_first_post?(manager) &&
WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").should_silence?
end
def self.matches_auto_silence_regex?(manager)
args = manager.args
pattern = SiteSetting.auto_silence_first_post_regex
return false if pattern.blank?
return false unless is_first_post?(manager)
begin
regex = Regexp.new(pattern, Regexp::IGNORECASE)
rescue => e
Rails.logger.warn "Invalid regex in auto_silence_first_post_regex #{e}"
return false
end
"#{args[:title]} #{args[:raw]}" =~ regex
end
def self.exempt_user?(user)
user.staff?
end
def self.post_needs_approval?(manager)
user = manager.user
return :email_auth_res_enqueue if manager.args[:email_auth_res_action] == :enqueue
return :skip if exempt_user?(user)
return :email_spam if manager.args[:email_spam]
if user.trust_level <= TrustLevel.levels[:basic] &&
(user.post_count + user.topic_count) < SiteSetting.approve_post_count
return :post_count
end
if !user.staged? && !user.in_any_groups?(SiteSetting.approve_unless_allowed_groups_map)
return :group
end
if manager.args[:title].present? && !user.staged? &&
!user.in_any_groups?(SiteSetting.approve_new_topics_unless_allowed_groups_map)
return :new_topics_unless_allowed_groups
end
if WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval?
return :watched_word
end
return :fast_typer if is_fast_typer?(manager)
return :auto_silence_regex if auto_silence?(manager) || matches_auto_silence_regex?(manager)
return :staged if SiteSetting.approve_unless_staged? && user.staged?
return :category if post_needs_approval_in_its_category?(manager)
if manager.args[:image_sizes].present? &&
!user.in_any_groups?(SiteSetting.skip_review_media_groups_map)
return :contains_media
end
:skip
end
def self.post_needs_approval_in_its_category?(manager)
guardian = manager.user.guardian
if manager.args[:topic_id].present?
topic = Topic.find_by(id: manager.args[:topic_id])
return false unless topic&.category
guardian.reply_posting_review_required?(topic.category) && !guardian.can_review_topic?(topic)
elsif manager.args[:category].present?
category = Category.find(manager.args[:category])
guardian.topic_posting_review_required?(category) &&
!guardian.is_category_group_moderator?(category)
else
false
end
end
def self.default_handler(manager)
reason = post_needs_approval?(manager)
return if reason == :skip
validator = PostValidator.new
post = Post.new(raw: manager.args[:raw])
post.user = manager.user
validator.validate(post)
if post.errors[:raw].present?
result = NewPostResult.new(:created_post, false)
result.errors.add(:base, post.errors[:raw])
return result
elsif manager.args[:topic_id]
topic = Topic.unscoped.where(id: manager.args[:topic_id]).first
unless manager.user.guardian.can_create_post_on_topic?(topic)
result = NewPostResult.new(:created_post, false)
result.errors.add(:base, I18n.t(:topic_not_found))
return result
end
elsif manager.args[:category]
category = Category.find_by(id: manager.args[:category])
unless manager.user.guardian.can_create_topic_on_category?(category)
result = NewPostResult.new(:created_post, false)
result.errors.add(:base, I18n.t("js.errors.reasons.forbidden"))
return result
end
end
result = manager.enqueue(reason)
if result.success? || (reason == :email_spam && is_first_post?(manager))
I18n.with_locale(SiteSetting.default_locale) do
if is_fast_typer?(manager)
UserSilencer.auto_silence(
manager.user,
Discourse.system_user,
keep_posts: true,
reason: I18n.t("user.new_user_typed_too_fast"),
)
elsif auto_silence?(manager) || matches_auto_silence_regex?(manager)
UserSilencer.auto_silence(
manager.user,
Discourse.system_user,
keep_posts: true,
reason: I18n.t("user.content_matches_auto_silence_regex"),
)
elsif reason == :email_spam && is_first_post?(manager)
UserSilencer.auto_silence(
manager.user,
Discourse.system_user,
keep_posts: true,
reason: I18n.t("user.email_in_spam_header"),
)
end
end
end
result
end
def self.queue_enabled?
SiteSetting.approve_post_count > 0 ||
!SiteSetting.approve_unless_allowed_groups_map.include?(Group::AUTO_GROUPS[:trust_level_0]) ||
!SiteSetting.approve_new_topics_unless_allowed_groups_map.include?(
Group::AUTO_GROUPS[:trust_level_0],
) || SiteSetting.approve_unless_staged ||
WordWatcher.words_for_action_exist?(:require_approval) || handlers.size > 1
end
def initialize(user, args)
@user = user
@args = args.delete_if { |_, v| v.nil? }
end
def perform
if !self.class.exempt_user?(@user) &&
matches = WordWatcher.new("#{@args[:title]} #{@args[:raw]}").should_block?.presence
result = NewPostResult.new(:created_post, false)
if matches.size == 1
key = "contains_blocked_word"
translation_args = { word: CGI.escapeHTML(matches[0]) }
else
key = "contains_blocked_words"
translation_args = { words: CGI.escapeHTML(matches.join(", ")) }
end
result.errors.add(:base, I18n.t(key, translation_args))
return result
end
# Perform handlers until one returns a result
NewPostManager.handlers.any? do |handler|
result = handler.call(self)
return result if result
end
# We never queue private messages
if @args[:archetype] == Archetype.private_message ||
(
args[:topic_id] &&
Topic.where(id: args[:topic_id], archetype: Archetype.private_message).exists?
)
return perform_create_post
end
NewPostManager.default_handler(self) || perform_create_post
end
# Enqueue this post
def enqueue(reason = nil, creator_opts: {})
result = NewPostResult.new(:enqueued)
payload = { raw: @args[:raw], tags: @args[:tags] }
%w[typing_duration_msecs composer_open_duration_msecs reply_to_post_number].each do |a|
payload[a] = @args[a].to_i if @args[a]
end
self.class.plugin_payload_attributes.each { |a| payload[a] = @args[a] if @args[a].present? }
payload[:via_email] = true if !!@args[:via_email]
payload[:raw_email] = @args[:raw_email] if @args[:raw_email].present?
reviewable =
ReviewableQueuedPost.new(
created_by: Discourse.system_user,
payload: payload,
topic_id: @args[:topic_id],
reviewable_by_moderator: true,
target_created_by: @user,
)
reviewable.payload["title"] = @args[:title] if @args[:title].present?
reviewable.category_id = args[:category] if args[:category].present?
reviewable.created_new!
create_options = reviewable.create_options.merge(creator_opts)
creator =
(
if @args[:topic_id]
PostCreator.new(@user, create_options)
else
TopicCreator.new(@user, Guardian.new(@user), create_options)
end
)
errors = Set.new
creator.valid?
creator.errors.full_messages.each { |msg| errors << msg }
errors = creator.errors.full_messages.uniq
if errors.blank?
if reviewable.save
reviewable.add_score(
Discourse.system_user,
ReviewableScore.types[:needs_approval],
reason: reason,
force_review: true,
)
else
reviewable.errors.full_messages.each { |msg| errors << msg }
end
end
result.reviewable = reviewable
result.reason = reason if reason
result.check_errors(errors)
result.pending_count = ReviewableQueuedPost.where(target_created_by: @user).pending.count
result
end
def perform_create_post
result = NewPostResult.new(:create_post)
creator = PostCreator.new(@user, @args)
post = creator.create
result.check_errors_from(creator)
if result.success?
result.post = post
else
@user.flag_linked_posts_as_spam if creator.spam?
end
result
end
end