2013-02-06 03:16:51 +08:00
require_dependency 'rate_limiter'
require_dependency 'system_message'
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
2013-05-07 12:39:01 +08:00
include Trashable
2013-02-06 03:16:51 +08:00
belongs_to :post
belongs_to :user
belongs_to :post_action_type
2013-05-30 04:49:34 +08:00
belongs_to :related_post , class_name : 'Post'
2014-02-06 06:54:16 +08:00
belongs_to :target_user , class_name : 'User'
2013-02-06 03:16:51 +08:00
rate_limit :post_action_rate_limiter
2013-05-31 23:41:40 +08:00
scope :spam_flags , - > { where ( post_action_type_id : PostActionType . types [ :spam ] ) }
2014-07-29 01:17:37 +08: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-08-11 17:56:54 +08:00
scope :active , - > { where ( disagreed_at : nil , deferred_at : nil , agreed_at : nil , deleted_at : nil ) }
2013-05-31 23:41:40 +08:00
2013-08-15 11:44:30 +08:00
after_save :update_counters
after_save :enforce_rules
2014-03-31 09:34:01 +08:00
after_commit :notify_subscribers
2013-08-15 11:44:30 +08:00
2014-07-29 01:17:37 +08:00
def disposed_by_id
2014-08-11 17:56:54 +08:00
disagreed_by_id || agreed_by_id || deferred_by_id
2014-07-29 01:17:37 +08:00
end
def disposed_at
2014-08-11 17:56:54 +08:00
disagreed_at || agreed_at || deferred_at
2014-07-29 01:17:37 +08:00
end
def disposition
2014-07-31 05:35:42 +08:00
return :disagreed if disagreed_at
2014-07-29 01:17:37 +08:00
return :agreed if agreed_at
2014-08-11 17:56:54 +08:00
return :deferred if deferred_at
2014-07-29 01:17:37 +08:00
nil
end
2013-02-06 03:16:51 +08:00
def self . update_flagged_posts_count
2014-07-29 01:17:37 +08:00
posts_flagged_count = PostAction . active
. flags
. joins ( post : :topic )
. where ( 'posts.deleted_at' = > nil )
. where ( 'topics.deleted_at' = > nil )
2013-05-04 08:52:45 +08:00
. count ( 'DISTINCT posts.id' )
2013-02-09 04:54:28 +08:00
$redis . set ( 'posts_flagged_count' , posts_flagged_count )
2013-05-18 03:11:37 +08:00
user_ids = User . staff . pluck ( :id )
2013-05-02 13:15:17 +08:00
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 )
2014-07-29 01:17:37 +08:00
return { } if collection . blank?
2013-02-06 03:16:51 +08:00
2014-07-29 01:17:37 +08:00
collection_ids = collection . map ( & :id )
2014-07-31 05:35:42 +08:00
user_id = user . try ( :id ) || 0
2013-02-06 03:16:51 +08:00
2014-07-29 01:17:37 +08:00
post_actions = PostAction . where ( post_id : collection_ids , user_id : user_id )
2014-06-04 23:41:11 +08:00
2013-02-06 03:16:51 +08:00
user_actions = { }
2014-07-29 01:17:37 +08: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-06 03:16:51 +08:00
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
2014-08-04 23:29:01 +08:00
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
2014-07-29 01:17:37 +08: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-18 01:53:00 +08:00
end
2014-07-29 01:17:37 +08:00
def self . clear_flags! ( post , moderator )
2013-02-06 03:16:51 +08:00
# -1 is the automatic system cleary
2014-07-29 01:17:37 +08: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-31 05:35:42 +08:00
action . disagreed_at = Time . zone . now
action . disagreed_by_id = moderator . id
2014-07-29 01:17:37 +08:00
# so callback is called
action . save
action . add_moderator_post_if_needed ( moderator , :disagreed )
2013-02-07 12:15:48 +08:00
end
2013-02-06 03:16:51 +08:00
2014-07-29 01:17:37 +08: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-06 03:16:51 +08:00
update_flagged_posts_count
end
2014-07-29 01:17:37 +08: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 15:42:15 +08:00
2014-07-29 01:17:37 +08:00
actions . each do | action |
2014-08-11 17:56:54 +08:00
action . deferred_at = Time . zone . now
action . deferred_by_id = moderator . id
2013-06-20 15:42:15 +08:00
# so callback is called
2014-07-29 01:17:37 +08:00
action . save
2014-08-11 17:56:54 +08:00
action . add_moderator_post_if_needed ( moderator , :deferred , delete_post )
2013-06-20 15:42:15 +08:00
end
update_flagged_posts_count
end
2014-07-29 01:17:37 +08:00
def add_moderator_post_if_needed ( moderator , disposition , delete_post = false )
2014-08-18 23:00:14 +08:00
return if related_post . nil?
return if moderator_already_replied? ( related_post . topic , moderator )
2014-07-29 01:17:37 +08:00
message_key = " flags_dispositions. #{ disposition } "
message_key << " _and_deleted " if delete_post
related_post . topic . add_moderator_post ( moderator , I18n . t ( message_key ) )
end
2014-08-18 23:00:14 +08:00
def moderator_already_replied? ( topic , moderator )
2014-10-28 05:06:43 +08:00
topic . posts . where ( " user_id = :user_id OR post_type = :post_type " , user_id : moderator . id , post_type : Post . types [ :moderator_action ] ) . exists?
2014-08-18 23:00:14 +08:00
end
2013-08-19 19:14:26 +08: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 15:55:45 +08:00
2013-08-19 19:14:26 +08:00
return unless opts [ :message ] && [ :notify_moderators , :notify_user ] . include? ( post_action_type )
2014-05-13 23:44:23 +08: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-13 03:26:36 +08:00
else
2014-05-13 23:44:23 +08:00
opts [ :subtype ] = TopicSubtype . notify_user
opts [ :target_usernames ] = if post_action_type == :notify_user
2014-08-21 02:14:19 +08:00
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
2014-05-13 03:26:36 +08:00
end
2013-06-01 05:38:28 +08:00
2014-05-13 23:44:23 +08:00
PostCreator . new ( user , opts ) . create . id
2013-02-06 03:16:51 +08:00
end
2014-08-21 02:14:19 +08:00
def self . act ( user , post , post_action_type_id , opts = { } )
2014-07-29 01:17:37 +08: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 19:14:26 +08:00
2014-07-29 01:17:37 +08:00
targets_topic = if opts [ :flag_topic ] && post . topic
2014-08-21 02:14:19 +08:00
post . topic . reload
post . topic . posts_count != 1
end
2014-02-19 04:18:31 +08:00
2014-07-19 04:14:47 +08:00
where_attrs = {
post_id : post . id ,
user_id : user . id ,
post_action_type_id : post_action_type_id
}
action_attributes = {
2014-07-29 01:17:37 +08:00
staff_took_action : staff_took_action ,
2014-07-19 04:14:47 +08: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-29 01:17:37 +08:00
. with_deleted
. where ( " deleted_at IS NOT NULL " )
. update_all ( action_attributes . merge ( deleted_at : nil ) )
2014-07-19 04:14:47 +08:00
if row_count == 0
post_action = create ( where_attrs . merge ( action_attributes ) )
2014-07-23 09:42:24 +08:00
if post_action && post_action . errors . count == 0
BadgeGranter . queue_badge_grant ( Badge :: Trigger :: PostAction , post_action : post_action )
2014-07-19 04:14:47 +08:00
end
else
post_action = PostAction . where ( where_attrs ) . first
2014-09-03 05:37:19 +08:00
# after_commit is not called on an `update_all` so do the notify ourselves
post_action . notify_subscribers
2014-06-17 14:29:49 +08:00
end
2014-07-29 01:17:37 +08:00
# agree with other flags
PostAction . agree_flags! ( post , user ) if staff_took_action
# update counters
post_action . try ( :update_counters )
2014-06-17 14:29:49 +08:00
post_action
2013-08-19 19:14:26 +08:00
rescue ActiveRecord :: RecordNotUnique
# can happen despite being .create
# since already bookmarked
2014-08-18 10:03:46 +08:00
PostAction . where ( where_attrs ) . first
2013-08-19 19:14:26 +08:00
end
2013-02-06 03:16:51 +08:00
def self . remove_act ( user , post , post_action_type_id )
2014-06-04 23:41:11 +08:00
finder = PostAction . where ( post_id : post . id , user_id : user . id , post_action_type_id : post_action_type_id )
2014-08-19 22:14:17 +08:00
finder = finder . with_deleted . includes ( :post ) if user . try ( :staff? )
2014-06-04 23:41:11 +08:00
if action = finder . first
2013-08-15 11:44:30 +08:00
action . remove_act! ( user )
2014-08-19 22:14:17 +08:00
action . post . unhide! if action . staff_took_action
2013-02-06 03:16:51 +08:00
end
end
2013-05-13 09:48:01 +08:00
def remove_act! ( user )
2013-07-10 03:20:18 +08:00
trash! ( user )
2014-03-31 09:34:01 +08: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-13 09:48:01 +08:00
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
2013-04-12 15:55:45 +08:00
def is_private_message?
post_action_type_id == PostActionType . types [ :notify_user ] ||
post_action_type_id == PostActionType . types [ :notify_moderators ]
end
2013-05-11 04:58:23 +08:00
2013-02-06 03:16:51 +08:00
# A custom rate limiter for this model
def post_action_rate_limiter
2013-03-01 02:54:12 +08:00
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 } ? " )
2014-08-15 02:20:52 +08:00
@rate_limiter = RateLimiter . new ( user , " create_ #{ type } : #{ Date . today } " , 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
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
2014-07-29 01:17:37 +08: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-31 05:35:42 +08:00
. where ( disagreed_at : nil )
2014-07-29 01:17:37 +08:00
. where ( targets_topic : targets_topic )
2013-05-04 08:52:45 +08:00
. exists?
2013-02-07 07:45:58 +08:00
end
2013-05-11 04:58:23 +08: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-31 05:35:42 +08: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-11 04:58:23 +08:00
ELSE 0
END ) AS new_flags ,
SUM ( CASE
2014-07-31 05:35:42 +08: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-11 04:58:23 +08: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-31 05:35:42 +08: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-11 04:58:23 +08: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-15 11:44:30 +08:00
def post_action_type_key
PostActionType . types [ post_action_type_id ]
end
def update_counters
2013-02-06 03:16:51 +08:00
# Update denormalized counts
2014-08-15 02:20:52 +08:00
column = " #{ post_action_type_key } _count "
2014-07-29 01:17:37 +08:00
count = PostAction . where ( post_id : post_id )
. where ( post_action_type_id : post_action_type_id )
. count
2013-02-06 03:16:51 +08:00
2013-05-28 00:45:10 +08:00
# We probably want to refactor this method to something cleaner.
2013-08-15 11:44:30 +08:00
case post_action_type_key
2013-05-28 00:45:10 +08:00
when :vote
# Voting also changes the sort_order
2014-07-29 01:17:37 +08:00
Post . where ( id : post_id ) . update_all [ " vote_count = :count, sort_order = :max - :count " , count : count , max : Topic . max_sort_order ]
2013-05-28 00:45:10 +08:00
when :like
# `like_score` is weighted higher for staff accounts
2014-07-29 01:17:37 +08: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-06 03:16:51 +08:00
else
2014-07-29 01:17:37 +08:00
Post . where ( id : post_id ) . update_all [ " #{ column } = ? " , count ]
2013-02-06 03:16:51 +08:00
end
2013-05-28 00:45:10 +08:00
2014-07-29 01:17:37 +08:00
topic_id = Post . with_deleted . where ( id : post_id ) . pluck ( :topic_id ) . first
2014-07-29 04:08:31 +08:00
topic_count = Post . where ( topic_id : topic_id ) . sum ( column )
Topic . where ( id : topic_id ) . update_all [ " #{ column } = ? " , topic_count ]
2013-02-06 03:16:51 +08:00
2013-05-30 04:49:34 +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
2013-08-15 11:44:30 +08:00
end
2013-06-20 15:42:15 +08:00
2013-08-15 11:44:30 +08:00
def enforce_rules
2014-06-04 23:41:11 +08:00
post = Post . with_deleted . where ( id : post_id ) . first
2014-10-02 00:53:17 +08:00
PostAction . auto_hide_if_needed ( user , post , post_action_type_key )
2013-08-15 11:44:30 +08:00
SpamRulesEnforcer . enforce! ( post . user ) if post_action_type_key == :spam
2013-06-20 15:42:15 +08:00
end
2014-03-24 10:22:03 +08:00
def notify_subscribers
if ( is_like? || is_flag? ) && post
2014-08-29 11:34:32 +08:00
post . publish_change_to_clients! :acted
2014-03-24 10:22:03 +08:00
end
end
2014-10-02 00:53:17 +08:00
def self . auto_hide_if_needed ( acting_user , post , post_action_type )
2013-06-20 15:42:15 +08:00
return if post . hidden
2014-10-02 00:53:17 +08:00
if post_action_type == :spam &&
acting_user . 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 ) &&
SiteSetting . flags_required_to_hide_post > 0
2013-06-20 15:42:15 +08:00
2013-05-11 04:58:23 +08:00
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
2014-04-30 22:58:01 +08:00
hide_post! ( post , post_action_type , guess_hide_reason ( old_flags ) )
2013-02-06 03:16:51 +08:00
end
end
2013-06-20 15:42:15 +08:00
end
2013-05-31 23:41:40 +08:00
2014-04-30 22:58:01 +08:00
def self . hide_post! ( post , post_action_type , reason = nil )
2013-06-20 15:42:15 +08:00
return if post . hidden
unless reason
old_flags , _ = PostAction . flag_counts_for ( post . id )
reason = guess_hide_reason ( old_flags )
end
2014-09-26 01:51:00 +08:00
Post . where ( id : post . id ) . update_all ( [ " hidden = true, hidden_at = ?, hidden_reason_id = COALESCE(hidden_reason_id, ?) " , Time . now , reason ] )
2014-08-19 22:14:17 +08: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 15:42:15 +08:00
# inform user
if post . user
2014-04-30 22:58:01 +08: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 15:42:15 +08: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-06 03:16:51 +08:00
end
2013-04-12 15:55:45 +08:00
2014-04-30 22:58:01 +08:00
def self . post_action_type_for_post ( post_id )
2014-08-11 17:56:54 +08:00
post_action = PostAction . find_by ( deferred_at : nil , post_id : post_id , post_action_type_id : PostActionType . flag_types . values , deleted_at : nil )
2014-04-30 22:58:01 +08:00
PostActionType . types [ post_action . post_action_type_id ]
end
2013-05-09 15:37:34 +08:00
def self . target_moderators
Group [ :moderators ] . name
2013-04-12 15:55:45 +08:00
end
2013-02-06 03:16:51 +08:00
end
2013-05-24 10:48:32 +08: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-08-27 13:19:25 +08:00
# created_at :datetime not null
# updated_at :datetime not null
2013-07-10 03:20:18 +08:00
# deleted_by_id :integer
2013-05-24 10:48:32 +08:00
# related_post_id :integer
2013-06-17 08:48:58 +08:00
# staff_took_action :boolean default(FALSE), not null
2014-08-23 01:01:44 +08:00
# deferred_by_id :integer
# targets_topic :boolean default(FALSE), not null
2014-07-29 01:17:37 +08:00
# agreed_at :datetime
# agreed_by_id :integer
2014-08-23 01:01:44 +08:00
# deferred_at :datetime
2014-07-31 11:14:40 +08:00
# disagreed_at :datetime
# disagreed_by_id :integer
2013-05-24 10:48:32 +08:00
#
# Indexes
#
2014-08-23 01:01:44 +08:00
# idx_unique_actions (user_id,post_action_type_id,post_id,targets_topic) UNIQUE
2013-05-24 10:48:32 +08:00
# index_post_actions_on_post_id (post_id)
#