2013-02-05 14:16:51 -05:00
require_dependency 'rate_limiter'
require_dependency 'system_message'
class PostAction < ActiveRecord :: Base
2013-05-03 20:52:45 -04:00
class AlreadyActed < StandardError ; end
2013-02-07 10:45:24 -05:00
2013-02-05 14:16:51 -05:00
include RateLimiter :: OnCreateRecord
2013-05-07 00:39:01 -04:00
include Trashable
2013-02-05 14:16:51 -05:00
belongs_to :post
belongs_to :user
belongs_to :post_action_type
2013-05-29 16:49:34 -04:00
belongs_to :related_post , class_name : 'Post'
2014-02-05 17:54:16 -05:00
belongs_to :target_user , class_name : 'User'
2013-02-05 14:16:51 -05:00
rate_limit :post_action_rate_limiter
2013-05-31 11:41:40 -04:00
scope :spam_flags , - > { where ( post_action_type_id : PostActionType . types [ :spam ] ) }
2014-07-28 13:17:37 -04:00
scope :flags , - > { where ( post_action_type_id : PostActionType . notify_flag_type_ids ) }
scope :publics , - > { where ( post_action_type_id : PostActionType . public_type_ids ) }
2014-07-30 17:35:42 -04:00
scope :active , - > { where ( disagreed_at : nil , defered_at : nil , agreed_at : nil , deleted_at : nil ) }
2013-05-31 11:41:40 -04:00
2013-08-14 23:44:30 -04:00
after_save :update_counters
after_save :enforce_rules
2014-03-30 21:34:01 -04:00
after_commit :notify_subscribers
2013-08-14 23:44:30 -04:00
2014-07-28 13:17:37 -04:00
def disposed_by_id
2014-07-30 17:35:42 -04:00
disagreed_by_id || agreed_by_id || defered_by_id
2014-07-28 13:17:37 -04:00
end
def disposed_at
2014-07-30 17:35:42 -04:00
disagreed_at || agreed_at || defered_at
2014-07-28 13:17:37 -04:00
end
def disposition
2014-07-30 17:35:42 -04:00
return :disagreed if disagreed_at
2014-07-28 13:17:37 -04:00
return :agreed if agreed_at
return :defered if defered_at
nil
end
2013-02-05 14:16:51 -05:00
def self . update_flagged_posts_count
2014-07-28 13:17:37 -04:00
posts_flagged_count = PostAction . active
. flags
. joins ( post : :topic )
. where ( 'posts.deleted_at' = > nil )
. where ( 'topics.deleted_at' = > nil )
2013-05-03 20:52:45 -04:00
. count ( 'DISTINCT posts.id' )
2013-02-08 15:54:28 -05:00
$redis . set ( 'posts_flagged_count' , posts_flagged_count )
2013-05-17 15:11:37 -04:00
user_ids = User . staff . pluck ( :id )
2013-05-02 01:15:17 -04:00
MessageBus . publish ( '/flagged_counts' , { total : posts_flagged_count } , { user_ids : user_ids } )
2013-02-05 14:16:51 -05:00
end
def self . flagged_posts_count
$redis . get ( 'posts_flagged_count' ) . to_i
end
def self . counts_for ( collection , user )
2014-07-28 13:17:37 -04:00
return { } if collection . blank?
2013-02-05 14:16:51 -05:00
2014-07-28 13:17:37 -04:00
collection_ids = collection . map ( & :id )
2014-07-30 17:35:42 -04:00
user_id = user . try ( :id ) || 0
2013-02-05 14:16:51 -05:00
2014-07-28 13:17:37 -04:00
post_actions = PostAction . where ( post_id : collection_ids , user_id : user_id )
2014-06-04 11:41:11 -04:00
2013-02-05 14:16:51 -05:00
user_actions = { }
2014-07-28 13:17:37 -04:00
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
2013-02-05 14:16:51 -05:00
end
2013-02-07 10:45:24 -05:00
2013-02-05 14:16:51 -05:00
user_actions
2013-02-07 10:45:24 -05:00
end
2013-02-05 14:16:51 -05:00
2014-07-28 13:17:37 -04:00
def self . count_per_day_for_type ( post_action_type , since_days_ago = 30 )
unscoped . where ( post_action_type_id : post_action_type )
. where ( 'created_at > ?' , since_days_ago . days . ago )
. group ( 'date(created_at)' )
. order ( 'date(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 . flag_types . values )
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 )
end
update_flagged_posts_count
2013-03-17 13:53:00 -04:00
end
2014-07-28 13:17:37 -04:00
def self . clear_flags! ( post , moderator )
2013-02-05 14:16:51 -05:00
# -1 is the automatic system cleary
2014-07-28 13:17:37 -04:00
action_type_ids = moderator . id == - 1 ?
PostActionType . auto_action_flag_types . values :
PostActionType . flag_types . values
actions = PostAction . where ( post_id : post . id )
. where ( post_action_type_id : action_type_ids )
actions . each do | action |
2014-07-30 17:35:42 -04:00
action . disagreed_at = Time . zone . now
action . disagreed_by_id = moderator . id
2014-07-28 13:17:37 -04:00
# so callback is called
action . save
action . add_moderator_post_if_needed ( moderator , :disagreed )
2013-02-06 23:15:48 -05:00
end
2013-02-05 14:16:51 -05:00
2014-07-28 13:17:37 -04:00
# reset all cached counters
f = action_type_ids . map { | t | [ " #{ PostActionType . types [ t ] } _count " , 0 ] }
Post . with_deleted . where ( id : post . id ) . update_all ( Hash [ * f . flatten ] )
2013-02-05 14:16:51 -05:00
update_flagged_posts_count
end
2014-07-28 13:17:37 -04:00
def self . defer_flags! ( post , moderator , delete_post = false )
actions = PostAction . active
. where ( post_id : post . id )
. where ( post_action_type_id : PostActionType . flag_types . values )
2013-06-20 03:42:15 -04:00
2014-07-28 13:17:37 -04:00
actions . each do | action |
action . defered_at = Time . zone . now
action . defered_by_id = moderator . id
2013-06-20 03:42:15 -04:00
# so callback is called
2014-07-28 13:17:37 -04:00
action . save
action . add_moderator_post_if_needed ( moderator , :defered , delete_post )
2013-06-20 03:42:15 -04:00
end
update_flagged_posts_count
end
2014-07-28 13:17:37 -04:00
def add_moderator_post_if_needed ( moderator , disposition , delete_post = false )
return unless related_post
message_key = " flags_dispositions. #{ disposition } "
message_key << " _and_deleted " if delete_post
related_post . topic . add_moderator_post ( moderator , I18n . t ( message_key ) )
end
2013-08-19 07:14:26 -04:00
def self . create_message_for_post_action ( user , post , post_action_type_id , opts )
post_action_type = PostActionType . types [ post_action_type_id ]
2013-04-12 03:55:45 -04:00
2013-08-19 07:14:26 -04:00
return unless opts [ :message ] && [ :notify_moderators , :notify_user ] . include? ( post_action_type )
2014-05-13 11:44:23 -04:00
title = I18n . t ( " post_action_types. #{ post_action_type } .email_title " , title : post . topic . title )
body = I18n . t ( " post_action_types. #{ post_action_type } .email_body " , message : opts [ :message ] , link : " #{ Discourse . base_url } #{ post . url } " )
opts = {
archetype : Archetype . private_message ,
title : title ,
raw : body
}
if post_action_type == :notify_moderators
opts [ :subtype ] = TopicSubtype . notify_moderators
opts [ :target_group_names ] = " moderators "
2014-05-12 15:26:36 -04:00
else
2014-05-13 11:44:23 -04:00
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 reciepients, we should think through
# a cleaner technique, a PM with myself is valid for flagging
'x'
end
2014-05-12 15:26:36 -04:00
end
2013-05-31 17:38:28 -04:00
2014-05-13 11:44:23 -04:00
PostCreator . new ( user , opts ) . create . id
2013-02-05 14:16:51 -05:00
end
2013-08-19 07:14:26 -04:00
def self . act ( user , post , post_action_type_id , opts = { } )
2014-07-28 13:17:37 -04:00
related_post_id = create_message_for_post_action ( user , post , post_action_type_id , opts )
staff_took_action = opts [ :take_action ] || false
2013-08-19 07:14:26 -04:00
2014-07-28 13:17:37 -04:00
targets_topic = if opts [ :flag_topic ] && post . topic
2014-02-18 15:18:31 -05:00
post . topic . reload
post . topic . posts_count != 1
end
2014-07-18 16:14:47 -04:00
where_attrs = {
post_id : post . id ,
user_id : user . id ,
post_action_type_id : post_action_type_id
}
action_attributes = {
2014-07-28 13:17:37 -04:00
staff_took_action : staff_took_action ,
2014-07-18 16:14:47 -04:00
related_post_id : related_post_id ,
targets_topic : ! ! targets_topic
}
# First try to revive a trashed record
row_count = PostAction . where ( where_attrs )
2014-07-28 13:17:37 -04:00
. with_deleted
. where ( " deleted_at IS NOT NULL " )
. update_all ( action_attributes . merge ( deleted_at : nil ) )
2014-07-18 16:14:47 -04:00
if row_count == 0
post_action = create ( where_attrs . merge ( action_attributes ) )
2014-07-22 21:42:24 -04:00
if post_action && post_action . errors . count == 0
BadgeGranter . queue_badge_grant ( Badge :: Trigger :: PostAction , post_action : post_action )
2014-07-18 16:14:47 -04:00
end
else
post_action = PostAction . where ( where_attrs ) . first
2014-06-17 02:29:49 -04:00
end
2014-07-28 13:17:37 -04:00
# agree with other flags
PostAction . agree_flags! ( post , user ) if staff_took_action
# update counters
post_action . try ( :update_counters )
2014-06-17 02:29:49 -04:00
post_action
2013-08-19 07:14:26 -04:00
rescue ActiveRecord :: RecordNotUnique
# can happen despite being .create
# since already bookmarked
true
end
2013-02-05 14:16:51 -05:00
def self . remove_act ( user , post , post_action_type_id )
2014-06-04 11:41:11 -04:00
finder = PostAction . where ( post_id : post . id , user_id : user . id , post_action_type_id : post_action_type_id )
finder = finder . with_deleted if user . try ( :staff? )
if action = finder . first
2013-08-14 23:44:30 -04:00
action . remove_act! ( user )
2013-02-05 14:16:51 -05:00
end
end
2013-05-12 21:48:01 -04:00
def remove_act! ( user )
2013-07-09 15:20:18 -04:00
trash! ( user )
2014-03-30 21:34:01 -04:00
# NOTE: save is called to ensure all callbacks are called
# trash will not trigger callbacks, and triggering after_commit
# is not trivial
save
2013-05-12 21:48:01 -04:00
end
2013-02-07 10:45:24 -05:00
def is_bookmark?
2013-03-01 07:07:44 -05:00
post_action_type_id == PostActionType . types [ :bookmark ]
2013-02-05 14:16:51 -05:00
end
2013-02-07 10:45:24 -05:00
def is_like?
2013-03-01 07:07:44 -05:00
post_action_type_id == PostActionType . types [ :like ]
2013-02-05 14:16:51 -05:00
end
def is_flag?
2013-03-01 07:07:44 -05:00
PostActionType . flag_types . values . include? ( post_action_type_id )
2013-02-07 10:45:24 -05:00
end
2013-02-05 14:16:51 -05:00
2013-04-12 03:55:45 -04:00
def is_private_message?
post_action_type_id == PostActionType . types [ :notify_user ] ||
post_action_type_id == PostActionType . types [ :notify_moderators ]
end
2013-05-10 16:58:23 -04:00
2013-02-05 14:16:51 -05:00
# A custom rate limiter for this model
def post_action_rate_limiter
2013-02-28 13:54:12 -05:00
return unless is_flag? || is_bookmark? || is_like?
2013-02-05 14:16:51 -05:00
return @rate_limiter if @rate_limiter . present?
%w( like flag bookmark ) . each do | type |
if send ( " is_ #{ type } ? " )
2013-02-07 10:45:24 -05: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-05 14:16:51 -05:00
return @rate_limiter
end
end
end
2013-02-08 16:55:40 -05:00
2013-02-07 10:45:24 -05:00
before_create do
2013-05-03 20:52:45 -04:00
post_action_type_ids = is_flag? ? PostActionType . flag_types . values : post_action_type_id
2014-07-28 13:17:37 -04:00
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 )
2014-07-30 17:35:42 -04:00
. where ( disagreed_at : nil )
2014-07-28 13:17:37 -04:00
. where ( targets_topic : targets_topic )
2013-05-03 20:52:45 -04:00
. exists?
2013-02-06 18:45:58 -05:00
end
2013-05-10 16:58:23 -04:00
# 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
2014-07-30 17:35:42 -04:00
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
2013-05-10 16:58:23 -04:00
ELSE 0
END ) AS new_flags ,
SUM ( CASE
2014-07-30 17:35:42 -04:00
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
2013-05-10 16:58:23 -04:00
ELSE 0
END ) AS old_flags
FROM post_actions AS pa
INNER JOIN users AS u ON u . id = pa . user_id
2014-07-30 17:35:42 -04:00
WHERE pa . post_id = :post_id
AND pa . post_action_type_id IN ( :post_action_types )
AND pa . deleted_at IS NULL " ,
2013-05-10 16:58:23 -04:00
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-08-14 23:44:30 -04:00
def post_action_type_key
PostActionType . types [ post_action_type_id ]
end
def update_counters
2013-02-05 14:16:51 -05:00
# Update denormalized counts
2013-08-14 23:44:30 -04:00
column = " #{ post_action_type_key . to_s } _count "
2014-07-28 13:17:37 -04:00
count = PostAction . where ( post_id : post_id )
. where ( post_action_type_id : post_action_type_id )
. count
2013-02-05 14:16:51 -05:00
2013-05-27 12:45:10 -04:00
# We probably want to refactor this method to something cleaner.
2013-08-14 23:44:30 -04:00
case post_action_type_key
2013-05-27 12:45:10 -04:00
when :vote
# Voting also changes the sort_order
2014-07-28 13:17:37 -04:00
Post . where ( id : post_id ) . update_all [ " vote_count = :count, sort_order = :max - :count " , count : count , max : Topic . max_sort_order ]
2013-05-27 12:45:10 -04:00
when :like
# `like_score` is weighted higher for staff accounts
2014-07-28 13:17:37 -04:00
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 ]
2013-02-05 14:16:51 -05:00
else
2014-07-28 13:17:37 -04:00
Post . where ( id : post_id ) . update_all [ " #{ column } = ? " , count ]
2013-02-05 14:16:51 -05:00
end
2013-05-27 12:45:10 -04:00
2014-07-28 13:17:37 -04:00
topic_id = Post . with_deleted . where ( id : post_id ) . pluck ( :topic_id ) . first
2014-07-28 16:08:31 -04:00
topic_count = Post . where ( topic_id : topic_id ) . sum ( column )
Topic . where ( id : topic_id ) . update_all [ " #{ column } = ? " , topic_count ]
2013-02-05 14:16:51 -05:00
2013-05-29 16:49:34 -04:00
if PostActionType . notify_flag_type_ids . include? ( post_action_type_id )
2013-02-05 14:16:51 -05:00
PostAction . update_flagged_posts_count
end
2013-08-14 23:44:30 -04:00
end
2013-06-20 03:42:15 -04:00
2013-08-14 23:44:30 -04:00
def enforce_rules
2014-06-04 11:41:11 -04:00
post = Post . with_deleted . where ( id : post_id ) . first
2013-08-14 23:44:30 -04:00
PostAction . auto_hide_if_needed ( post , post_action_type_key )
SpamRulesEnforcer . enforce! ( post . user ) if post_action_type_key == :spam
2013-06-20 03:42:15 -04:00
end
2014-03-23 22:22:03 -04:00
def notify_subscribers
if ( is_like? || is_flag? ) && post
MessageBus . publish ( " /topic/ #{ post . topic_id } " , {
id : post . id ,
post_number : post . post_number ,
type : " acted "
} ,
group_ids : post . topic . secure_group_ids
)
end
end
2013-06-20 03:42:15 -04:00
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
2013-05-10 16:58:23 -04:00
old_flags , new_flags = PostAction . flag_counts_for ( post . id )
2013-02-05 14:16:51 -05:00
if new_flags > = SiteSetting . flags_required_to_hide_post
2014-04-30 10:58:01 -04:00
hide_post! ( post , post_action_type , guess_hide_reason ( old_flags ) )
2013-02-05 14:16:51 -05:00
end
end
2013-06-20 03:42:15 -04:00
end
2013-05-31 11:41:40 -04:00
2014-04-30 10:58:01 -04:00
def self . hide_post! ( post , post_action_type , reason = nil )
2013-06-20 03:42:15 -04:00
return if post . hidden
unless reason
old_flags , _ = PostAction . flag_counts_for ( post . id )
reason = guess_hide_reason ( old_flags )
end
2014-06-20 15:03:02 -04:00
Post . where ( id : post . id ) . update_all ( [ " hidden = true, hidden_at = CURRENT_TIMESTAMP, hidden_reason_id = COALESCE(hidden_reason_id, ?) " , reason ] )
2014-07-28 13:17:37 -04:00
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 )
2013-06-20 03:42:15 -04:00
# inform user
if post . user
2014-04-30 10:58:01 -04:00
options = {
url : post . url ,
edit_delay : SiteSetting . cooldown_minutes_after_hiding_posts ,
flag_reason : I18n . t ( " flag_reasons. #{ post_action_type } " ) ,
}
SystemMessage . create ( post . user , :post_hidden , options )
2013-06-20 03:42:15 -04:00
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-05 14:16:51 -05:00
end
2013-04-12 03:55:45 -04:00
2014-04-30 10:58:01 -04:00
def self . post_action_type_for_post ( post_id )
2014-07-28 13:17:37 -04:00
post_action = PostAction . find_by ( defered_at : nil , post_id : post_id , post_action_type_id : PostActionType . flag_types . values , deleted_at : nil )
2014-04-30 10:58:01 -04:00
PostActionType . types [ post_action . post_action_type_id ]
end
2013-04-14 23:09:52 -04:00
protected
2013-04-12 03:55:45 -04:00
2013-05-09 03:37:34 -04:00
def self . target_moderators
Group [ :moderators ] . name
2013-04-12 03:55:45 -04:00
end
2013-02-05 14:16:51 -05:00
end
2013-05-23 22:48:32 -04:00
# == 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
2014-07-30 23:14:40 -04:00
# created_at :datetime
# updated_at :datetime
2013-07-09 15:20:18 -04:00
# deleted_by_id :integer
2013-05-23 22:48:32 -04:00
# related_post_id :integer
2013-06-16 20:48:58 -04:00
# staff_took_action :boolean default(FALSE), not null
2014-07-30 23:14:40 -04:00
# defered_by_id :integer
2014-02-05 17:54:16 -05:00
# targets_topic :boolean default(FALSE)
2014-07-28 13:17:37 -04:00
# agreed_at :datetime
# agreed_by_id :integer
2014-07-30 23:14:40 -04:00
# defered_at :datetime
# disagreed_at :datetime
# disagreed_by_id :integer
2013-05-23 22:48:32 -04:00
#
# Indexes
#
2014-07-30 23:14:40 -04:00
# idx_unique_actions (user_id,post_action_type_id,post_id,deleted_at,targets_topic) UNIQUE
2013-05-23 22:48:32 -04:00
# index_post_actions_on_post_id (post_id)
#