discourse/app/models/post_action.rb

287 lines
9.4 KiB
Ruby

# frozen_string_literal: true
class PostAction < ActiveRecord::Base
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
validate :ensure_unique_actions, on: :create
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 { |row| (map[row.topic_id] ||= []) << row.post_number }
map
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]
if opts[:category_id]
if opts[:include_subcategories]
result =
result.joins(post: :topic).where(
"topics.category_id IN (?)",
Category.subcategory_ids(opts[:category_id]),
)
else
result = result.joins(post: :topic).where("topics.category_id = ?", opts[:category_id])
end
end
if opts[:group_ids]
result =
result
.joins("INNER JOIN users ON users.id = post_actions.user_id")
.joins("INNER JOIN group_users ON group_users.user_id = users.id")
.where("group_users.group_id IN (?)", opts[:group_ids])
end
result.group("date(post_actions.created_at)").order("date(post_actions.created_at)").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
# archive message for moderators
GroupArchivedMessage.archive!(Group[:moderators].id, related_post.topic)
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.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.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)
Discourse.deprecate(
"PostAction.remove_act is deprecated. Use `PostActionDestroyer` instead.",
output_in_test: true,
drop_from: "2.9.0",
)
PostActionDestroyer.new(user, post, post_action_type_id).perform
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_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_like?
return @rate_limiter if @rate_limiter.present?
%w[like flag].each do |type|
if public_send("is_#{type}?")
limit = SiteSetting.get("max_#{type}s_per_day")
if (is_flag? || is_like?) && user && user.trust_level >= 2
multiplier =
SiteSetting.get("tl#{user.trust_level}_additional_#{type}s_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
def ensure_unique_actions
post_action_type_ids = is_flag? ? PostActionType.notify_flag_types.values : post_action_type_id
acted =
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?
errors.add(:post_action_type_id) if acted
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).pick(:topic_id)
# topic_user
if post_action_type_key == :like
TopicUser.update_post_action_cache(
user_id: user_id,
topic_id: topic_id,
post_action_type: post_action_type_key,
)
end
Topic.find_by(id: topic_id)&.update_action_counts if column == "like_count"
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_action_type_id_and_disagreed_at (post_action_type_id,disagreed_at) WHERE (disagreed_at IS NULL)
# index_post_actions_on_post_id (post_id)
# index_post_actions_on_user_id (user_id)
# index_post_actions_on_user_id_and_post_action_type_id (user_id,post_action_type_id) WHERE (deleted_at IS NULL)
#