mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 00:33:44 +08:00
bc3efab816
Previously it would unhide their post but leave them silenced. This fix also cleans up some of the helper classes to make it easier to pass extra data to the silencing code (for example, a link to the post that caused the user to be silenced.) This patch also refactors the auto_silence specs to avoid using stubs.
723 lines
24 KiB
Ruby
723 lines
24 KiB
Ruby
require_dependency 'rate_limiter'
|
|
require_dependency 'system_message'
|
|
|
|
class PostAction < ActiveRecord::Base
|
|
class AlreadyActed < StandardError; end
|
|
class FailedToCreatePost < StandardError; end
|
|
|
|
include RateLimiter::OnCreateRecord
|
|
include Trashable
|
|
|
|
belongs_to :post
|
|
belongs_to :user
|
|
belongs_to :post_action_type
|
|
belongs_to :related_post, class_name: 'Post'
|
|
belongs_to :target_user, class_name: 'User'
|
|
|
|
rate_limit :post_action_rate_limiter
|
|
|
|
scope :spam_flags, -> { where(post_action_type_id: PostActionType.types[:spam]) }
|
|
scope :flags, -> { where(post_action_type_id: PostActionType.notify_flag_type_ids) }
|
|
scope :publics, -> { where(post_action_type_id: PostActionType.public_type_ids) }
|
|
scope :active, -> { where(disagreed_at: nil, deferred_at: nil, agreed_at: nil, deleted_at: nil) }
|
|
|
|
after_save :update_counters
|
|
after_save :enforce_rules
|
|
after_save :create_user_action
|
|
after_save :update_notifications
|
|
after_create :create_notifications
|
|
after_commit :notify_subscribers
|
|
|
|
def disposed_by_id
|
|
disagreed_by_id || agreed_by_id || deferred_by_id
|
|
end
|
|
|
|
def disposed_at
|
|
disagreed_at || agreed_at || deferred_at
|
|
end
|
|
|
|
def disposition
|
|
return :disagreed if disagreed_at
|
|
return :agreed if agreed_at
|
|
return :deferred if deferred_at
|
|
nil
|
|
end
|
|
|
|
def self.flag_count_by_date(start_date, end_date, category_id = nil)
|
|
result = where('post_actions.created_at >= ? AND post_actions.created_at <= ?', start_date, end_date)
|
|
result = result.where(post_action_type_id: PostActionType.flag_types_without_custom.values)
|
|
result = result.joins(post: :topic).where("topics.category_id = ?", category_id) if category_id
|
|
result.group('date(post_actions.created_at)')
|
|
.order('date(post_actions.created_at)')
|
|
.count
|
|
end
|
|
|
|
# Forums can choose to apply a minimum number of flags required before it shows up in
|
|
# the admin interface. One exception is posts hidden by tl3/tl4 - we want those to
|
|
# show up even if the minimum visibility is not met.
|
|
def self.apply_minimum_visibility(relation)
|
|
return relation unless SiteSetting.min_flags_staff_visibility > 1
|
|
|
|
params = {
|
|
min_flags: SiteSetting.min_flags_staff_visibility,
|
|
hidden_reasons: Post.hidden_reasons.only(:flagged_by_tl3_user, :flagged_by_tl4_user).values
|
|
}
|
|
|
|
relation.having(<<~SQL, params)
|
|
(COUNT(*) >= :min_flags) OR
|
|
(SUM(CASE
|
|
WHEN posts.hidden_reason_id IN (:hidden_reasons) THEN 1
|
|
ELSE 0
|
|
END) > 0)
|
|
SQL
|
|
end
|
|
|
|
def self.update_flagged_posts_count
|
|
flagged_relation = PostAction.active
|
|
.flags
|
|
.joins(post: :topic)
|
|
.where('posts.deleted_at' => nil)
|
|
.where('topics.deleted_at' => nil)
|
|
.where('posts.user_id > 0')
|
|
.group("posts.id")
|
|
|
|
flagged_relation = apply_minimum_visibility(flagged_relation)
|
|
|
|
posts_flagged_count = flagged_relation
|
|
.pluck("posts.id")
|
|
.count
|
|
|
|
$redis.set('posts_flagged_count', posts_flagged_count)
|
|
user_ids = User.staff.pluck(:id)
|
|
MessageBus.publish('/flagged_counts', { total: posts_flagged_count }, user_ids: user_ids)
|
|
end
|
|
|
|
def self.flagged_posts_count
|
|
$redis.get('posts_flagged_count').to_i
|
|
end
|
|
|
|
def self.counts_for(collection, user)
|
|
return {} if collection.blank? || !user
|
|
|
|
collection_ids = collection.map(&:id)
|
|
user_id = user.try(:id) || 0
|
|
|
|
post_actions = PostAction.where(post_id: collection_ids, user_id: user_id)
|
|
|
|
user_actions = {}
|
|
post_actions.each do |post_action|
|
|
user_actions[post_action.post_id] ||= {}
|
|
user_actions[post_action.post_id][post_action.post_action_type_id] = post_action
|
|
end
|
|
|
|
user_actions
|
|
end
|
|
|
|
def self.lookup_for(user, topics, post_action_type_id)
|
|
return if topics.blank?
|
|
# in critical path 2x faster than AR
|
|
#
|
|
topic_ids = topics.map(&:id)
|
|
map = {}
|
|
|
|
builder = DB.build <<~SQL
|
|
SELECT p.topic_id, p.post_number
|
|
FROM post_actions pa
|
|
JOIN posts p ON pa.post_id = p.id
|
|
WHERE p.deleted_at IS NULL AND pa.deleted_at IS NULL AND
|
|
pa.post_action_type_id = :post_action_type_id AND
|
|
pa.user_id = :user_id AND
|
|
p.topic_id IN (:topic_ids)
|
|
ORDER BY p.topic_id, p.post_number
|
|
SQL
|
|
|
|
builder.query(user_id: user.id, post_action_type_id: post_action_type_id, topic_ids: topic_ids).each do |row|
|
|
(map[row.topic_id] ||= []) << row.post_number
|
|
end
|
|
|
|
map
|
|
end
|
|
|
|
def self.active_flags_counts_for(collection)
|
|
return {} if collection.blank?
|
|
|
|
collection_ids = collection.map(&:id)
|
|
|
|
post_actions = PostAction.active.flags.where(post_id: collection_ids)
|
|
|
|
user_actions = {}
|
|
post_actions.each do |post_action|
|
|
user_actions[post_action.post_id] ||= {}
|
|
user_actions[post_action.post_id][post_action.post_action_type_id] ||= []
|
|
user_actions[post_action.post_id][post_action.post_action_type_id] << post_action
|
|
end
|
|
|
|
user_actions
|
|
end
|
|
|
|
def self.count_per_day_for_type(post_action_type, opts = nil)
|
|
opts ||= {}
|
|
result = unscoped.where(post_action_type_id: post_action_type)
|
|
result = result.where('post_actions.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago)
|
|
result = result.where('post_actions.created_at <= ?', opts[:end_date]) if opts[:end_date]
|
|
result = result.joins(post: :topic).merge(Topic.in_category_and_subcategories(opts[:category_id])) if opts[:category_id]
|
|
result.group('date(post_actions.created_at)')
|
|
.order('date(post_actions.created_at)')
|
|
.count
|
|
end
|
|
|
|
def self.agree_flags!(post, moderator, delete_post = false)
|
|
actions = PostAction.active
|
|
.where(post_id: post.id)
|
|
.where(post_action_type_id: PostActionType.notify_flag_types.values)
|
|
|
|
trigger_spam = false
|
|
actions.each do |action|
|
|
action.agreed_at = Time.zone.now
|
|
action.agreed_by_id = moderator.id
|
|
# so callback is called
|
|
action.save
|
|
action.add_moderator_post_if_needed(moderator, :agreed, delete_post)
|
|
trigger_spam = true if action.post_action_type_id == PostActionType.types[:spam]
|
|
end
|
|
|
|
# Update the flags_agreed user stat
|
|
UserStat.where(user_id: actions.map(&:user_id)).update_all("flags_agreed = flags_agreed + 1")
|
|
|
|
DiscourseEvent.trigger(:confirmed_spam_post, post) if trigger_spam
|
|
|
|
if actions.first.present?
|
|
DiscourseEvent.trigger(:flag_reviewed, post)
|
|
DiscourseEvent.trigger(:flag_agreed, actions.first)
|
|
end
|
|
|
|
update_flagged_posts_count
|
|
end
|
|
|
|
def self.clear_flags!(post, moderator)
|
|
# -1 is the automatic system cleary
|
|
action_type_ids =
|
|
if moderator.id == Discourse::SYSTEM_USER_ID
|
|
PostActionType.auto_action_flag_types.values
|
|
else
|
|
PostActionType.notify_flag_type_ids
|
|
end
|
|
|
|
actions = PostAction.active.where(post_id: post.id).where(post_action_type_id: action_type_ids)
|
|
|
|
actions.each do |action|
|
|
action.disagreed_at = Time.zone.now
|
|
action.disagreed_by_id = moderator.id
|
|
# so callback is called
|
|
action.save
|
|
action.add_moderator_post_if_needed(moderator, :disagreed)
|
|
end
|
|
|
|
# Update the flags_disagreed user stat
|
|
UserStat.where(user_id: actions.map(&:user_id)).update_all("flags_disagreed = flags_disagreed + 1")
|
|
|
|
# reset all cached counters
|
|
cached = {}
|
|
action_type_ids.each do |atid|
|
|
column = "#{PostActionType.types[atid]}_count"
|
|
cached[column] = 0 if ActiveRecord::Base.connection.column_exists?(:posts, column)
|
|
end
|
|
|
|
Post.with_deleted.where(id: post.id).update_all(cached)
|
|
|
|
if actions.first.present?
|
|
DiscourseEvent.trigger(:flag_reviewed, post)
|
|
DiscourseEvent.trigger(:flag_disagreed, actions.first)
|
|
end
|
|
|
|
update_flagged_posts_count
|
|
|
|
undo_hide_and_silence(post)
|
|
end
|
|
|
|
def self.defer_flags!(post, moderator, delete_post = false)
|
|
actions = PostAction.active
|
|
.where(post_id: post.id)
|
|
.where(post_action_type_id: PostActionType.notify_flag_type_ids)
|
|
|
|
actions.each do |action|
|
|
action.deferred_at = Time.zone.now
|
|
action.deferred_by_id = moderator.id
|
|
# so callback is called
|
|
action.save
|
|
action.add_moderator_post_if_needed(moderator, :deferred, delete_post)
|
|
end
|
|
|
|
if actions.first.present?
|
|
DiscourseEvent.trigger(:flag_reviewed, post)
|
|
DiscourseEvent.trigger(:flag_deferred, actions.first)
|
|
end
|
|
|
|
update_flagged_posts_count
|
|
end
|
|
|
|
def add_moderator_post_if_needed(moderator, disposition, delete_post = false)
|
|
return if !SiteSetting.auto_respond_to_flag_actions
|
|
return if related_post.nil? || related_post.topic.nil?
|
|
return if staff_already_replied?(related_post.topic)
|
|
message_key = "flags_dispositions.#{disposition}"
|
|
message_key << "_and_deleted" if delete_post
|
|
|
|
I18n.with_locale(SiteSetting.default_locale) do
|
|
related_post.topic.add_moderator_post(moderator, I18n.t(message_key))
|
|
end
|
|
end
|
|
|
|
def staff_already_replied?(topic)
|
|
topic.posts.where("user_id IN (SELECT id FROM users WHERE moderator OR admin) OR (post_type != :regular_post_type)", regular_post_type: Post.types[:regular]).exists?
|
|
end
|
|
|
|
def self.create_message_for_post_action(user, post, post_action_type_id, opts)
|
|
post_action_type = PostActionType.types[post_action_type_id]
|
|
|
|
return unless opts[:message] && [:notify_moderators, :notify_user, :spam].include?(post_action_type)
|
|
|
|
title = I18n.t("post_action_types.#{post_action_type}.email_title", title: post.topic.title, locale: SiteSetting.default_locale)
|
|
body = I18n.t("post_action_types.#{post_action_type}.email_body", message: opts[:message], link: "#{Discourse.base_url}#{post.url}", locale: SiteSetting.default_locale)
|
|
warning = opts[:is_warning] if opts[:is_warning].present?
|
|
title = title.truncate(SiteSetting.max_topic_title_length, separator: /\s/)
|
|
|
|
opts = {
|
|
archetype: Archetype.private_message,
|
|
is_warning: warning,
|
|
title: title,
|
|
raw: body
|
|
}
|
|
|
|
if [:notify_moderators, :spam].include?(post_action_type)
|
|
opts[:subtype] = TopicSubtype.notify_moderators
|
|
opts[:target_group_names] = target_moderators
|
|
else
|
|
opts[:subtype] = TopicSubtype.notify_user
|
|
|
|
opts[:target_usernames] =
|
|
if post_action_type == :notify_user
|
|
post.user.username
|
|
elsif post_action_type != :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(user, opts).create!&.id
|
|
end
|
|
|
|
def self.limit_action!(user, post, post_action_type_id)
|
|
RateLimiter.new(user, "post_action-#{post.id}_#{post_action_type_id}", 4, 1.minute).performed!
|
|
end
|
|
|
|
def self.act(user, post, post_action_type_id, opts = {})
|
|
limit_action!(user, post, post_action_type_id)
|
|
|
|
begin
|
|
related_post_id = create_message_for_post_action(user, post, post_action_type_id, opts)
|
|
rescue ActiveRecord::RecordNotSaved => e
|
|
raise FailedToCreatePost.new(e.message)
|
|
end
|
|
|
|
staff_took_action = opts[:take_action] || false
|
|
|
|
targets_topic =
|
|
if opts[:flag_topic] && post.topic
|
|
post.topic.reload.posts_count != 1
|
|
end
|
|
|
|
where_attrs = {
|
|
post_id: post.id,
|
|
user_id: user.id,
|
|
post_action_type_id: post_action_type_id
|
|
}
|
|
|
|
action_attrs = {
|
|
staff_took_action: staff_took_action,
|
|
related_post_id: related_post_id,
|
|
targets_topic: !!targets_topic
|
|
}
|
|
|
|
# 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.send("#{attr}=", val) }
|
|
post_action.save
|
|
PostActionNotifier.post_action_created(post_action)
|
|
else
|
|
post_action = 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(user.id) if post_action_type_id == PostActionType.types[:like]
|
|
|
|
# agree with other flags
|
|
if staff_took_action
|
|
PostAction.agree_flags!(post, user)
|
|
post_action.try(:update_counters)
|
|
end
|
|
|
|
post_action
|
|
rescue ActiveRecord::RecordNotUnique
|
|
# can happen despite being .create
|
|
# since already bookmarked
|
|
PostAction.where(where_attrs).first
|
|
end
|
|
|
|
def self.undo_hide_and_silence(post)
|
|
return unless post.hidden?
|
|
|
|
post.unhide!
|
|
UserSilencer.unsilence(post.user) if UserSilencer.was_silenced_for?(post)
|
|
end
|
|
|
|
def self.copy(original_post, target_post)
|
|
cols_to_copy = (column_names - %w{id post_id}).join(', ')
|
|
|
|
DB.exec <<~SQL
|
|
INSERT INTO post_actions(post_id, #{cols_to_copy})
|
|
SELECT #{target_post.id}, #{cols_to_copy}
|
|
FROM post_actions
|
|
WHERE post_id = #{original_post.id}
|
|
SQL
|
|
|
|
target_post.post_actions.each { |post_action| post_action.update_counters }
|
|
end
|
|
|
|
def self.remove_act(user, post, post_action_type_id)
|
|
|
|
limit_action!(user, post, post_action_type_id)
|
|
|
|
finder = PostAction.where(post_id: post.id, user_id: user.id, post_action_type_id: post_action_type_id)
|
|
finder = finder.with_deleted.includes(:post) if user.try(:staff?)
|
|
if action = finder.first
|
|
action.remove_act!(user)
|
|
action.post.unhide! if action.staff_took_action
|
|
GivenDailyLike.decrement_for(user.id) if post_action_type_id == PostActionType.types[:like]
|
|
end
|
|
end
|
|
|
|
def remove_act!(user)
|
|
trash!(user)
|
|
# NOTE: save is called to ensure all callbacks are called
|
|
# trash will not trigger callbacks, and triggering after_commit
|
|
# is not trivial
|
|
save
|
|
end
|
|
|
|
def is_bookmark?
|
|
post_action_type_id == PostActionType.types[:bookmark]
|
|
end
|
|
|
|
def is_like?
|
|
post_action_type_id == PostActionType.types[:like]
|
|
end
|
|
|
|
def is_flag?
|
|
!!PostActionType.notify_flag_types[post_action_type_id]
|
|
end
|
|
|
|
def is_private_message?
|
|
post_action_type_id == PostActionType.types[:notify_user] ||
|
|
post_action_type_id == PostActionType.types[:notify_moderators]
|
|
end
|
|
|
|
# A custom rate limiter for this model
|
|
def post_action_rate_limiter
|
|
return unless is_flag? || is_bookmark? || is_like?
|
|
|
|
return @rate_limiter if @rate_limiter.present?
|
|
|
|
%w(like flag bookmark).each do |type|
|
|
if send("is_#{type}?")
|
|
limit = SiteSetting.send("max_#{type}s_per_day")
|
|
|
|
if is_like? && user && user.trust_level >= 2
|
|
multiplier = SiteSetting.send("tl#{user.trust_level}_additional_likes_per_day_multiplier").to_f
|
|
multiplier = 1.0 if multiplier < 1.0
|
|
|
|
limit = (limit * multiplier).to_i
|
|
end
|
|
|
|
@rate_limiter = RateLimiter.new(user, "create_#{type}", limit, 1.day.to_i)
|
|
return @rate_limiter
|
|
end
|
|
end
|
|
end
|
|
|
|
before_create do
|
|
post_action_type_ids = is_flag? ? PostActionType.notify_flag_types.values : post_action_type_id
|
|
raise AlreadyActed if PostAction.where(user_id: user_id)
|
|
.where(post_id: post_id)
|
|
.where(post_action_type_id: post_action_type_ids)
|
|
.where(deleted_at: nil)
|
|
.where(disagreed_at: nil)
|
|
.where(targets_topic: targets_topic)
|
|
.exists?
|
|
end
|
|
|
|
# Returns the flag counts for a post, taking into account that some users
|
|
# can weigh flags differently.
|
|
def self.flag_counts_for(post_id)
|
|
params = {
|
|
post_id: post_id,
|
|
post_action_types: PostActionType.auto_action_flag_types.values,
|
|
flags_required_to_hide_post: SiteSetting.flags_required_to_hide_post
|
|
}
|
|
|
|
DB.query_single(<<~SQL, params)
|
|
SELECT COALESCE(SUM(CASE
|
|
WHEN pa.disagreed_at IS NOT NULL AND pa.staff_took_action THEN :flags_required_to_hide_post
|
|
WHEN pa.disagreed_at IS NOT NULL AND NOT pa.staff_took_action THEN 1
|
|
ELSE 0
|
|
END),0) AS old_flags,
|
|
COALESCE(SUM(CASE
|
|
WHEN pa.disagreed_at IS NULL AND pa.staff_took_action THEN :flags_required_to_hide_post
|
|
WHEN pa.disagreed_at IS NULL AND NOT pa.staff_took_action THEN 1
|
|
ELSE 0
|
|
END), 0) AS new_flags
|
|
FROM post_actions AS pa
|
|
INNER JOIN users AS u ON u.id = pa.user_id
|
|
WHERE pa.post_id = :post_id
|
|
AND pa.post_action_type_id in (:post_action_types)
|
|
AND pa.deleted_at IS NULL
|
|
SQL
|
|
end
|
|
|
|
def post_action_type_key
|
|
PostActionType.types[post_action_type_id]
|
|
end
|
|
|
|
def update_counters
|
|
# Update denormalized counts
|
|
column = "#{post_action_type_key}_count"
|
|
count = PostAction.where(post_id: post_id)
|
|
.where(post_action_type_id: post_action_type_id)
|
|
.count
|
|
|
|
# We probably want to refactor this method to something cleaner.
|
|
case post_action_type_key
|
|
when :like
|
|
# 'like_score' is weighted higher for staff accounts
|
|
score = PostAction.joins(:user)
|
|
.where(post_id: post_id)
|
|
.sum("CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END")
|
|
Post.where(id: post_id).update_all ["like_count = :count, like_score = :score", count: count, score: score]
|
|
else
|
|
if ActiveRecord::Base.connection.column_exists?(:posts, column)
|
|
Post.where(id: post_id).update_all ["#{column} = ?", count]
|
|
end
|
|
end
|
|
|
|
topic_id = Post.with_deleted.where(id: post_id).pluck(:topic_id).first
|
|
|
|
# topic_user
|
|
if [:like, :bookmark].include? post_action_type_key
|
|
TopicUser.update_post_action_cache(user_id: user_id,
|
|
topic_id: topic_id,
|
|
post_action_type: post_action_type_key)
|
|
end
|
|
|
|
if column == "like_count"
|
|
topic_count = Post.where(topic_id: topic_id).sum(column)
|
|
Topic.where(id: topic_id).update_all ["#{column} = ?", topic_count]
|
|
end
|
|
|
|
if PostActionType.notify_flag_type_ids.include?(post_action_type_id)
|
|
PostAction.update_flagged_posts_count
|
|
end
|
|
|
|
end
|
|
|
|
def enforce_rules
|
|
post = Post.with_deleted.where(id: post_id).first
|
|
PostAction.auto_close_if_threshold_reached(post.topic)
|
|
PostAction.auto_hide_if_needed(user, post, post_action_type_key)
|
|
SpamRule::AutoSilence.new(post.user, post).perform
|
|
end
|
|
|
|
def create_user_action
|
|
if is_bookmark? || is_like?
|
|
UserActionCreator.log_post_action(self)
|
|
end
|
|
end
|
|
|
|
def update_notifications
|
|
if self.deleted_at.present?
|
|
PostActionNotifier.post_action_deleted(self)
|
|
end
|
|
end
|
|
|
|
def create_notifications
|
|
PostActionNotifier.post_action_created(self)
|
|
end
|
|
|
|
def notify_subscribers
|
|
if (is_like? || is_flag?) && post
|
|
post.publish_change_to_clients! :acted
|
|
end
|
|
end
|
|
|
|
MAXIMUM_FLAGS_PER_POST = 3
|
|
|
|
def self.auto_close_threshold_reached?(topic)
|
|
return if topic.user&.staff?
|
|
flags = PostAction.active
|
|
.flags
|
|
.joins(:post)
|
|
.where("posts.topic_id = ?", topic.id)
|
|
.where("post_actions.user_id > 0")
|
|
.group("post_actions.user_id")
|
|
.pluck("post_actions.user_id, COUNT(post_id)")
|
|
|
|
# we need a minimum number of unique flaggers
|
|
return if flags.count < SiteSetting.num_flaggers_to_close_topic
|
|
# we need a minimum number of flags
|
|
return if flags.sum { |f| f[1] } < SiteSetting.num_flags_to_close_topic
|
|
|
|
true
|
|
end
|
|
|
|
def self.auto_close_if_threshold_reached(topic)
|
|
return if topic.nil? || topic.closed?
|
|
return unless auto_close_threshold_reached?(topic)
|
|
|
|
# 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 self.auto_hide_if_needed(acting_user, post, post_action_type)
|
|
return if post.hidden?
|
|
return if (!acting_user.staff?) && post.user&.staff?
|
|
|
|
if post_action_type == :spam &&
|
|
acting_user.has_trust_level?(TrustLevel[3]) &&
|
|
post.user&.trust_level == TrustLevel[0]
|
|
|
|
hide_post!(post, post_action_type, Post.hidden_reasons[:flagged_by_tl3_user])
|
|
|
|
elsif PostActionType.auto_action_flag_types.include?(post_action_type)
|
|
|
|
if acting_user.has_trust_level?(TrustLevel[4]) &&
|
|
!acting_user.staff? &&
|
|
post.user&.trust_level != TrustLevel[4]
|
|
|
|
hide_post!(post, post_action_type, Post.hidden_reasons[:flagged_by_tl4_user])
|
|
elsif SiteSetting.flags_required_to_hide_post > 0
|
|
|
|
_old_flags, new_flags = PostAction.flag_counts_for(post.id)
|
|
|
|
if new_flags >= SiteSetting.flags_required_to_hide_post
|
|
hide_post!(post, post_action_type, guess_hide_reason(post))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.hide_post!(post, post_action_type, reason = nil)
|
|
return if post.hidden
|
|
|
|
unless reason
|
|
reason = guess_hide_reason(post)
|
|
end
|
|
|
|
hiding_again = post.hidden_at.present?
|
|
|
|
post.hidden = true
|
|
post.hidden_at = Time.zone.now
|
|
post.hidden_reason_id = reason
|
|
post.save
|
|
|
|
Topic.where("id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", topic_id: post.topic_id).update_all(visible: false)
|
|
|
|
# inform user
|
|
if post.user
|
|
options = {
|
|
url: post.url,
|
|
edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts,
|
|
flag_reason: I18n.t(
|
|
"flag_reasons.#{post_action_type}",
|
|
locale: SiteSetting.default_locale,
|
|
base_path: Discourse.base_path
|
|
)
|
|
}
|
|
|
|
Jobs.enqueue_in(5.seconds, :send_system_message,
|
|
user_id: post.user.id,
|
|
message_type: hiding_again ? :post_hidden_again : :post_hidden,
|
|
message_options: options)
|
|
end
|
|
update_flagged_posts_count
|
|
end
|
|
|
|
def self.guess_hide_reason(post)
|
|
post.hidden_at ?
|
|
Post.hidden_reasons[:flag_threshold_reached_again] :
|
|
Post.hidden_reasons[:flag_threshold_reached]
|
|
end
|
|
|
|
def self.post_action_type_for_post(post_id)
|
|
post_action = PostAction.find_by(deferred_at: nil, post_id: post_id, post_action_type_id: PostActionType.notify_flag_types.values, deleted_at: nil)
|
|
PostActionType.types[post_action.post_action_type_id] if post_action
|
|
end
|
|
|
|
def self.target_moderators
|
|
Group[:moderators].name
|
|
end
|
|
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: post_actions
|
|
#
|
|
# id :integer not null, primary key
|
|
# post_id :integer not null
|
|
# user_id :integer not null
|
|
# post_action_type_id :integer not null
|
|
# deleted_at :datetime
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# deleted_by_id :integer
|
|
# related_post_id :integer
|
|
# staff_took_action :boolean default(FALSE), not null
|
|
# deferred_by_id :integer
|
|
# targets_topic :boolean default(FALSE), not null
|
|
# agreed_at :datetime
|
|
# agreed_by_id :integer
|
|
# deferred_at :datetime
|
|
# disagreed_at :datetime
|
|
# disagreed_by_id :integer
|
|
#
|
|
# Indexes
|
|
#
|
|
# idx_unique_actions (user_id,post_action_type_id,post_id,targets_topic) UNIQUE WHERE ((deleted_at IS NULL) AND (disagreed_at IS NULL) AND (deferred_at IS NULL))
|
|
# idx_unique_flags (user_id,post_id,targets_topic) UNIQUE WHERE ((deleted_at IS NULL) AND (disagreed_at IS NULL) AND (deferred_at IS NULL) AND (post_action_type_id = ANY (ARRAY[3, 4, 7, 8])))
|
|
# index_post_actions_on_post_id (post_id)
|
|
# index_post_actions_on_user_id_and_post_action_type_id (user_id,post_action_type_id) WHERE (deleted_at IS NULL)
|
|
#
|