mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 22:53:56 +08:00
25a226279a
The #pluck_first freedom patch, first introduced by @danielwaterworth has served us well, and is used widely throughout both core and plugins. It seems to have been a common enough use case that Rails 6 introduced it's own method #pick with the exact same implementation. This allows us to retire the freedom patch and switch over to the built-in ActiveRecord method. There is no replacement for #pluck_first!, but a quick search shows we are using this in a very limited capacity, and in some cases incorrectly (by assuming a nil return rather than an exception), which can quite easily be replaced with #pick plus some extra handling.
291 lines
9.5 KiB
Ruby
291 lines
9.5 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
|
|
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.act(created_by, post, post_action_type_id, opts = {})
|
|
Discourse.deprecate(
|
|
"PostAction.act is deprecated. Use `PostActionCreator` instead.",
|
|
output_in_test: true,
|
|
drop_from: "2.9.0",
|
|
)
|
|
|
|
result =
|
|
PostActionCreator.new(created_by, post, post_action_type_id, message: opts[:message]).perform
|
|
|
|
result.success? ? result.post_action : nil
|
|
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)
|
|
#
|