2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
class PostAction < ActiveRecord :: Base
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
2019-01-04 01:03:01 +08:00
validate :ensure_unique_actions , on : :create
2013-02-06 03:16:51 +08:00
def self . counts_for ( collection , user )
2015-09-28 14:42:05 +08:00
return { } if collection . blank? || ! user
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
2015-01-07 15:20:10 +08:00
def self . lookup_for ( user , topics , post_action_type_id )
return if topics . blank?
2015-04-16 15:29:18 +08:00
# in critical path 2x faster than AR
#
topic_ids = topics . map ( & :id )
2015-01-07 15:20:10 +08:00
map = { }
2018-06-20 15:48:02 +08:00
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 )
2015-04-16 15:29:18 +08:00
. each { | row | ( map [ row . topic_id ] || = [ ] ) << row . post_number }
2015-01-07 15:20:10 +08:00
map
end
2015-06-24 21:19:39 +08:00
def self . count_per_day_for_type ( post_action_type , opts = nil )
opts || = { }
result = unscoped . where ( post_action_type_id : post_action_type )
2015-10-20 04:30:34 +08:00
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 ]
2020-04-22 16:52:50 +08:00
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
2023-09-05 13:47:18 +08:00
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
2015-06-24 21:19:39 +08:00
result . group ( " date(post_actions.created_at) " ) . order ( " date(post_actions.created_at) " ) . count
2014-07-29 01:17:37 +08:00
end
def add_moderator_post_if_needed ( moderator , disposition , delete_post = false )
2015-03-12 02:29:09 +08:00
return if ! SiteSetting . auto_respond_to_flag_actions
2015-03-04 00:52:46 +08:00
return if related_post . nil? || related_post . topic . nil?
2015-03-16 19:02:34 +08:00
return if staff_already_replied? ( related_post . topic )
2019-05-03 06:17:27 +08:00
message_key = + " flags_dispositions. #{ disposition } "
2014-07-29 01:17:37 +08:00
message_key << " _and_deleted " if delete_post
2017-08-09 17:17:54 +08:00
I18n . with_locale ( SiteSetting . default_locale ) do
related_post . topic . add_moderator_post ( moderator , I18n . t ( message_key ) )
end
2021-04-22 18:23:44 +08:00
# archive message for moderators
GroupArchivedMessage . archive! ( Group [ :moderators ] . id , related_post . topic )
2014-07-29 01:17:37 +08:00
end
2015-03-16 19:02:34 +08:00
def staff_already_replied? ( topic )
2015-07-25 04:39:03 +08:00
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 ] ,
2023-01-09 20:20:10 +08:00
)
2015-07-25 04:39:03 +08:00
. exists?
2014-08-18 23:00:14 +08:00
end
2016-03-06 06:51:30 +08:00
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
2017-02-10 22:35:04 +08:00
def self . copy ( original_post , target_post )
cols_to_copy = ( column_names - %w[ id post_id ] ) . join ( " , " )
2018-06-19 14:13:14 +08:00
DB . exec << ~ SQL
2017-02-10 22:35:04 +08:00
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
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
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
def post_action_type_view
@post_action_type_view || = PostActionTypeView . new
end
2013-02-07 23:45:24 +08:00
def is_like?
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
post_action_type_id == post_action_type_view . types [ :like ]
2013-02-06 03:16:51 +08:00
end
def is_flag?
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
! ! post_action_type_view . notify_flag_types [ 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?
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
post_action_type_id == post_action_type_view . types [ :notify_user ] ||
post_action_type_id == post_action_type_view . types [ :notify_moderators ]
2013-04-12 15:55:45 +08:00
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
2022-05-10 08:42:18 +08:00
return unless is_flag? || is_like?
2013-02-06 03:16:51 +08:00
return @rate_limiter if @rate_limiter . present?
2022-05-10 08:42:18 +08:00
%w[ like flag ] . each do | type |
2019-05-07 09:57:55 +08:00
if public_send ( " is_ #{ type } ? " )
2019-05-07 09:00:09 +08:00
limit = SiteSetting . get ( " max_ #{ type } s_per_day " )
2015-04-16 07:44:30 +08:00
2022-02-18 20:44:32 +08:00
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
2015-04-16 07:44:30 +08:00
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 )
2013-02-06 03:16:51 +08:00
return @rate_limiter
end
end
end
2013-02-09 05:55:40 +08:00
2019-01-04 01:03:01 +08:00
def ensure_unique_actions
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
post_action_type_ids =
is_flag? ? post_action_type_view . notify_flag_types . values : post_action_type_id
2019-01-04 01:03:01 +08:00
acted =
PostAction
. where ( user_id : user_id )
2018-12-04 17:48:16 +08:00
. 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?
2013-02-07 07:45:58 +08:00
2019-01-04 01:03:01 +08:00
errors . add ( :post_action_type_id ) if acted
2013-05-11 04:58:23 +08:00
end
2013-08-15 11:44:30 +08:00
def post_action_type_key
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
post_action_type_view . types [ post_action_type_id ]
2013-08-15 11:44:30 +08:00
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 :like
2014-12-06 02:37:43 +08:00
# '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
2017-10-18 01:31:45 +08:00
if ActiveRecord :: Base . connection . column_exists? ( :posts , column )
Post . where ( id : post_id ) . update_all [ " #{ column } = ? " , count ]
end
2013-02-06 03:16:51 +08:00
end
2013-05-28 00:45:10 +08:00
2023-02-13 12:39:45 +08:00
topic_id = Post . with_deleted . where ( id : post_id ) . pick ( :topic_id )
2015-01-08 11:35:56 +08:00
# topic_user
2020-07-08 13:27:42 +08:00
if post_action_type_key == :like
2015-01-08 11:35:56 +08:00
TopicUser . update_post_action_cache (
user_id : user_id ,
topic_id : topic_id ,
post_action_type : post_action_type_key ,
)
2016-12-02 14:03:31 +08:00
end
2013-02-06 03:16:51 +08:00
2022-02-03 13:24:33 +08:00
Topic . find_by ( id : topic_id ) & . update_action_counts if column == " like_count "
2013-06-20 15:42:15 +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
#
2019-04-02 13:17:55 +08:00
# 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)
2019-04-26 16:23:27 +08:00
# index_post_actions_on_user_id (user_id)
2019-04-02 13:17:55 +08:00
# index_post_actions_on_user_id_and_post_action_type_id (user_id,post_action_type_id) WHERE (deleted_at IS NULL)
2013-05-24 10:48:32 +08:00
#