discourse/app/models/post_action.rb

403 lines
14 KiB
Ruby
Raw Normal View History

2013-02-06 03:16:51 +08:00
require_dependency 'rate_limiter'
require_dependency 'system_message'
require_dependency 'trashable'
2013-02-06 03:16:51 +08:00
class PostAction < ActiveRecord::Base
2013-05-04 08:52:45 +08:00
class AlreadyActed < StandardError; end
2013-02-07 23:45:24 +08:00
2013-02-06 03:16:51 +08:00
include RateLimiter::OnCreateRecord
include Trashable
2013-02-06 03:16:51 +08:00
belongs_to :post
belongs_to :user
belongs_to :post_action_type
belongs_to :related_post, class_name: 'Post'
2013-02-06 03:16:51 +08:00
rate_limit :post_action_rate_limiter
2013-02-09 05:55:40 +08:00
validate :message_quality
scope :spam_flags, -> { where(post_action_type_id: PostActionType.types[:spam]) }
2013-02-06 03:16:51 +08:00
def self.update_flagged_posts_count
posts_flagged_count = PostAction.joins(post: :topic)
.where('defer = false or defer IS NULL')
.where('post_actions.post_action_type_id' => PostActionType.notify_flag_type_ids,
'posts.deleted_at' => nil,
2013-05-04 08:52:45 +08:00
'topics.deleted_at' => nil)
.count('DISTINCT posts.id')
$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 })
2013-02-06 03:16:51 +08:00
end
def self.flagged_posts_count
$redis.get('posts_flagged_count').to_i
end
def self.counts_for(collection, user)
return {} if collection.blank?
collection_ids = collection.map {|p| p.id}
2013-02-07 23:45:24 +08:00
user_id = user.present? ? user.id : 0
2013-02-06 03:16:51 +08:00
result = PostAction.where(post_id: collection_ids, user_id: user_id)
2013-02-06 03:16:51 +08:00
user_actions = {}
result.each do |r|
user_actions[r.post_id] ||= {}
user_actions[r.post_id][r.post_action_type_id] = r
end
2013-02-07 23:45:24 +08:00
2013-02-06 03:16:51 +08:00
user_actions
2013-02-07 23:45:24 +08:00
end
2013-02-06 03:16:51 +08:00
def self.count_per_day_for_type(sinceDaysAgo = 30, post_action_type)
unscoped.where(post_action_type_id: post_action_type).where('created_at > ?', sinceDaysAgo.days.ago).group('date(created_at)').order('date(created_at)').count
2013-03-18 01:53:00 +08:00
end
2013-02-07 12:15:48 +08:00
def self.clear_flags!(post, moderator_id, action_type_id = nil)
2013-02-06 03:16:51 +08:00
# -1 is the automatic system cleary
2013-02-07 12:15:48 +08:00
actions = if action_type_id
2013-02-07 23:45:24 +08:00
[action_type_id]
2013-02-07 12:15:48 +08:00
else
2013-03-01 20:07:44 +08:00
moderator_id == -1 ? PostActionType.auto_action_flag_types.values : PostActionType.flag_types.values
2013-02-07 12:15:48 +08:00
end
2013-02-06 03:16:51 +08:00
PostAction.update_all({ deleted_at: Time.zone.now, deleted_by: moderator_id }, { post_id: post.id, post_action_type_id: actions })
2013-03-01 20:07:44 +08:00
f = actions.map{|t| ["#{PostActionType.types[t]}_count", 0]}
Post.with_deleted.update_all(Hash[*f.flatten], id: post.id)
2013-02-06 03:16:51 +08:00
update_flagged_posts_count
end
def self.defer_flags!(post, moderator_id)
actions = PostAction.where(
defer: nil,
post_id: post.id,
post_action_type_id:
PostActionType.flag_types.values,
deleted_at: nil
)
actions.each do |a|
a.defer = true
a.defer_by = moderator_id
# so callback is called
a.save
end
update_flagged_posts_count
end
def self.act(user, post, post_action_type_id, opts={})
2013-02-07 23:45:24 +08:00
begin
title, target_usernames, target_group_names, subtype, body = nil
2013-04-15 11:09:52 +08:00
if opts[:message]
[:notify_moderators, :notify_user].each do |k|
if post_action_type_id == PostActionType.types[k]
if k == :notify_moderators
target_group_names = target_moderators
else
target_usernames = post.user.username
end
2013-04-15 11:09:52 +08:00
title = I18n.t("post_action_types.#{k}.email_title",
title: post.topic.title)
body = I18n.t("post_action_types.#{k}.email_body",
message: opts[:message],
link: "#{Discourse.base_url}#{post.url}")
subtype = k == :notify_moderators ? TopicSubtype.notify_moderators : TopicSubtype.notify_user
end
end
end
related_post_id = nil
if target_usernames.present? || target_group_names.present?
related_post_id = PostCreator.new(user,
2013-04-15 11:09:52 +08:00
target_usernames: target_usernames,
target_group_names: target_group_names,
archetype: Archetype.private_message,
subtype: subtype,
title: title,
raw: body
).create.id
end
create( post_id: post.id,
user_id: user.id,
post_action_type_id: post_action_type_id,
message: opts[:message],
staff_took_action: opts[:take_action] || false,
related_post_id: related_post_id )
2013-02-06 03:16:51 +08:00
rescue ActiveRecord::RecordNotUnique
# can happen despite being .create
# since already bookmarked
true
end
end
def self.remove_act(user, post, post_action_type_id)
if action = where(post_id: post.id, user_id: user.id, post_action_type_id: post_action_type_id).first
action.trash!
2013-02-26 00:42:20 +08:00
action.run_callbacks(:save)
2013-02-06 03:16:51 +08:00
end
end
def remove_act!(user)
trash!
run_callbacks(:save)
end
2013-02-07 23:45:24 +08:00
def is_bookmark?
2013-03-01 20:07:44 +08:00
post_action_type_id == PostActionType.types[:bookmark]
2013-02-06 03:16:51 +08:00
end
2013-02-07 23:45:24 +08:00
def is_like?
2013-03-01 20:07:44 +08:00
post_action_type_id == PostActionType.types[:like]
2013-02-06 03:16:51 +08:00
end
def is_flag?
2013-03-01 20:07:44 +08:00
PostActionType.flag_types.values.include?(post_action_type_id)
2013-02-07 23:45:24 +08:00
end
2013-02-06 03:16:51 +08:00
def is_private_message?
post_action_type_id == PostActionType.types[:notify_user] ||
post_action_type_id == PostActionType.types[:notify_moderators]
end
2013-02-06 03:16:51 +08:00
# A custom rate limiter for this model
def post_action_rate_limiter
return unless is_flag? || is_bookmark? || is_like?
2013-02-06 03:16:51 +08:00
return @rate_limiter if @rate_limiter.present?
%w(like flag bookmark).each do |type|
if send("is_#{type}?")
2013-02-07 23:45:24 +08:00
@rate_limiter = RateLimiter.new(user, "create_#{type}:#{Date.today.to_s}", SiteSetting.send("max_#{type}s_per_day"), 1.day.to_i)
2013-02-06 03:16:51 +08:00
return @rate_limiter
end
end
end
2013-02-09 05:55:40 +08:00
def message_quality
return if message.blank?
sentinel = TextSentinel.title_sentinel(message)
2013-04-10 17:00:50 +08:00
errors.add(:message, I18n.t(:is_invalid)) unless sentinel.valid?
2013-02-09 05:55:40 +08:00
end
2013-02-07 23:45:24 +08:00
before_create do
2013-05-04 08:52:45 +08:00
post_action_type_ids = is_flag? ? PostActionType.flag_types.values : post_action_type_id
raise AlreadyActed if PostAction.where(user_id: user_id,
post_id: post_id,
post_action_type_id: post_action_type_ids,
deleted_at: nil)
.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)
flag_counts = exec_sql("SELECT SUM(CASE
WHEN pa.deleted_at IS NULL AND (pa.staff_took_action) THEN :flags_required_to_hide_post
WHEN pa.deleted_at IS NULL AND (NOT pa.staff_took_action) THEN 1
ELSE 0
END) AS new_flags,
SUM(CASE
WHEN pa.deleted_at IS NOT NULL AND (pa.staff_took_action) THEN :flags_required_to_hide_post
WHEN pa.deleted_at IS NOT NULL AND (NOT pa.staff_took_action) THEN 1
ELSE 0
END) AS old_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)",
post_id: post_id,
post_action_types: PostActionType.auto_action_flag_types.values,
flags_required_to_hide_post: SiteSetting.flags_required_to_hide_post).first
[flag_counts['old_flags'].to_i, flag_counts['new_flags'].to_i]
end
2013-02-06 03:16:51 +08:00
after_save do
# Update denormalized counts
2013-03-01 20:07:44 +08:00
post_action_type = PostActionType.types[post_action_type_id]
column = "#{post_action_type.to_s}_count"
2013-02-06 03:16:51 +08:00
delta = deleted_at.nil? ? 1 : -1
# We probably want to refactor this method to something cleaner.
case post_action_type
when :vote
# Voting also changes the sort_order
Post.update_all ["vote_count = vote_count + :delta, sort_order = :max - (vote_count + :delta)",
delta: delta,
max: Topic.max_sort_order], id: post_id
when :like
# `like_score` is weighted higher for staff accounts
Post.update_all ["like_count = like_count + :delta, like_score = like_score + :score_delta",
delta: delta,
score_delta: user.staff? ? delta * SiteSetting.staff_like_weight : delta], id: post_id
2013-02-06 03:16:51 +08:00
else
Post.update_all ["#{column} = #{column} + ?", delta], id: post_id
2013-02-06 03:16:51 +08:00
end
Topic.update_all ["#{column} = #{column} + ?", delta], id: post.topic_id
2013-02-06 03:16:51 +08:00
if PostActionType.notify_flag_type_ids.include?(post_action_type_id)
2013-02-06 03:16:51 +08:00
PostAction.update_flagged_posts_count
end
PostAction.auto_hide_if_needed(post, post_action_type)
SpamRulesEnforcer.enforce!(post.user) if post_action_type == :spam
end
def self.auto_hide_if_needed(post, post_action_type)
return if post.hidden
if PostActionType.auto_action_flag_types.include?(post_action_type) &&
SiteSetting.flags_required_to_hide_post > 0
old_flags, new_flags = PostAction.flag_counts_for(post.id)
2013-02-06 03:16:51 +08:00
if new_flags >= SiteSetting.flags_required_to_hide_post
hide_post!(post, guess_hide_reason(old_flags))
2013-02-06 03:16:51 +08:00
end
end
end
def self.hide_post!(post, reason=nil)
return if post.hidden
unless reason
old_flags,_ = PostAction.flag_counts_for(post.id)
reason = guess_hide_reason(old_flags)
end
Post.update_all(["hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)", reason], id: post.id)
Topic.update_all({ visible: false },
["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)",
topic_id: post.topic_id])
# inform user
if post.user
SystemMessage.create(post.user,
:post_hidden,
url: post.url,
edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts)
end
end
def self.guess_hide_reason(old_flags)
old_flags > 0 ?
Post.hidden_reasons[:flag_threshold_reached_again] :
Post.hidden_reasons[:flag_threshold_reached]
2013-02-06 03:16:51 +08:00
end
def self.flagged_posts_report(filter)
posts = flagged_posts(filter)
return nil if posts.blank?
post_lookup = {}
users = Set.new
posts.each do |p|
users << p["user_id"]
p["excerpt"] = Post.excerpt(p.delete("cooked"))
p[:topic_slug] = Slug.for(p["title"])
post_lookup[p["id"].to_i] = p
end
post_actions = PostAction.includes({:related_post => :topic})
.where(post_action_type_id: PostActionType.notify_flag_type_ids)
.where(post_id: post_lookup.keys)
if filter == 'old'
post_actions = post_actions.with_deleted.where('deleted_at IS NOT NULL OR defer = true')
else
post_actions = post_actions.where('defer IS NULL OR defer = false')
end
post_actions.each do |pa|
post = post_lookup[pa.post_id]
post[:post_actions] ||= []
action = pa.attributes.slice('id', 'user_id', 'post_action_type_id', 'created_at', 'post_id', 'message')
if (pa.related_post && pa.related_post.topic)
action.merge!(topic_id: pa.related_post.topic_id,
slug: pa.related_post.topic.slug,
permalink: pa.related_post.topic.url)
end
post[:post_actions] << action
users << pa.user_id
end
[posts, User.select([:id, :username, :email]).where(id: users.to_a).all]
end
2013-04-15 11:09:52 +08:00
protected
def self.flagged_posts(filter)
sql = SqlBuilder.new "select p.id, t.title, p.cooked, p.user_id, p.topic_id, p.post_number, p.hidden, t.visible topic_visible
from posts p
join topics t on t.id = topic_id
join (
select
post_id,
count(*) as cnt,
max(created_at) max
from post_actions
/*where2*/
group by post_id
) as a on a.post_id = p.id
/*where*/
/*order_by*/
limit 100"
sql.where2 "post_action_type_id in (:flag_types)", flag_types: PostActionType.notify_flag_type_ids
# it may make sense to add a view that shows flags on deleted posts,
# we don't clear the flags on post deletion, just suppress counts
if filter == 'old'
sql.where2 "deleted_at is not null OR defer = true"
sql.order_by "max desc"
else
sql.where "p.deleted_at is null and t.deleted_at is null"
sql.where2 "deleted_at is null and (defer IS NULL OR defer = false)"
sql.order_by "cnt desc, max asc"
end
sql.exec.to_a
end
def self.target_moderators
Group[:moderators].name
end
2013-02-06 03:16:51 +08:00
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 :integer
# message :text
# related_post_id :integer
2013-06-17 08:48:58 +08:00
# staff_took_action :boolean default(FALSE), not null
# defer :boolean
# defer_by :integer
#
# Indexes
#
# idx_unique_actions (user_id,post_action_type_id,post_id,deleted_at) UNIQUE
# index_post_actions_on_post_id (post_id)
#