2013-02-06 03:16:51 +08:00
require_dependency 'pretty_text'
require_dependency 'rate_limiter'
2013-02-09 23:33:07 +08:00
require_dependency 'post_revisor'
2013-03-19 03:12:31 +08:00
require_dependency 'enum'
2013-05-31 02:34:44 +08:00
require_dependency 'post_analyzer'
2013-06-13 16:18:17 +08:00
require_dependency 'validators/post_validator'
2013-10-10 15:45:40 +08:00
require_dependency 'plugin/filter'
2013-02-06 03:16:51 +08:00
require 'archetype'
require 'digest/sha1'
class Post < ActiveRecord :: Base
include RateLimiter :: OnCreateRecord
2013-05-07 12:39:01 +08:00
include Trashable
2017-08-15 23:46:57 +08:00
include Searchable
2014-04-28 16:31:51 +08:00
include HasCustomFields
2015-02-26 03:53:21 +08:00
include LimitedEdit
2013-02-06 03:16:51 +08:00
2018-07-25 23:44:09 +08:00
cattr_accessor :plugin_permitted_create_params
self . plugin_permitted_create_params = { }
2017-08-12 10:10:45 +08:00
2014-05-30 12:45:39 +08:00
# increase this number to force a system wide post rebake
2019-04-09 13:54:14 +08:00
# Recreate `index_for_rebake_old` when the number is increased
2017-12-15 07:28:07 +08:00
# Version 1, was the initial version
# Version 2 15-12-2017, introduces CommonMark and a huge number of onebox fixes
BAKED_VERSION = 2
2014-05-28 10:30:43 +08:00
2013-02-07 23:45:24 +08:00
rate_limit
2013-10-10 07:32:03 +08:00
rate_limit :limit_posts_per_day
2013-02-19 14:57:14 +08:00
2013-02-06 03:16:51 +08:00
belongs_to :user
2016-12-02 14:03:31 +08:00
belongs_to :topic
2014-07-17 03:04:55 +08:00
2013-03-20 07:51:39 +08:00
belongs_to :reply_to_user , class_name : " User "
2013-02-06 03:16:51 +08:00
has_many :post_replies
has_many :replies , through : :post_replies
has_many :post_actions
2013-06-14 01:41:45 +08:00
has_many :topic_links
2015-12-01 13:52:43 +08:00
has_many :group_mentions , dependent : :destroy
2013-02-06 03:16:51 +08:00
2013-06-14 05:44:24 +08:00
has_many :post_uploads
has_many :uploads , through : :post_uploads
2013-06-13 07:43:50 +08:00
2015-08-03 12:29:04 +08:00
has_one :post_stat
2013-05-23 03:33:33 +08:00
2016-04-21 03:29:27 +08:00
has_one :incoming_email
2013-10-15 22:21:30 +08:00
has_many :post_details
2013-12-12 10:41:34 +08:00
has_many :post_revisions
2018-05-29 07:34:12 +08:00
has_many :revisions , - > { order ( :number ) } , foreign_key : :post_id , class_name : 'PostRevision'
2013-12-12 10:41:34 +08:00
2014-07-17 03:04:55 +08:00
has_many :user_actions , foreign_key : :target_post_id
2019-01-02 22:24:13 +08:00
validates_with :: Validators :: PostValidator , unless : :skip_validation
2013-02-06 03:16:51 +08:00
2016-12-22 10:13:14 +08:00
after_save :index_search
2013-06-21 23:36:33 +08:00
# We can pass several creating options to a post via attributes
2019-01-02 22:24:13 +08:00
attr_accessor :image_sizes , :quoted_post_numbers , :no_bump , :invalidate_oneboxes , :cooking_options , :skip_unique_check , :skip_validation
2013-02-06 03:16:51 +08:00
2019-04-10 21:00:59 +08:00
LARGE_IMAGES || = " large_images " . freeze
BROKEN_IMAGES || = " broken_images " . freeze
DOWNLOADED_IMAGES || = " downloaded_images " . freeze
MISSING_UPLOADS || = " missing uploads " . freeze
2017-11-16 22:45:07 +08:00
SHORT_POST_CHARS || = 1200
2013-02-06 03:16:51 +08:00
2019-04-10 20:39:35 +08:00
register_custom_field_type ( MISSING_UPLOADS , :json )
2017-05-12 03:58:43 +08:00
scope :private_posts_for_user , - > ( user ) {
where ( " posts.topic_id IN (SELECT topic_id
FROM topic_allowed_users
WHERE user_id = :user_id
UNION ALL
SELECT tg . topic_id
FROM topic_allowed_groups tg
JOIN group_users gu ON gu . user_id = :user_id AND
gu . group_id = tg . group_id ) " ,
user_id : user . id )
}
2017-09-15 02:08:16 +08:00
scope :by_newest , - > { order ( 'created_at DESC, id DESC' ) }
2013-06-10 00:48:44 +08:00
scope :by_post_number , - > { order ( 'post_number ASC' ) }
scope :with_user , - > { includes ( :user ) }
2017-09-15 02:08:16 +08:00
scope :created_since , - > ( time_ago ) { where ( 'posts.created_at > ?' , time_ago ) }
2013-04-10 20:54:10 +08:00
scope :public_posts , - > { joins ( :topic ) . where ( 'topics.archetype <> ?' , Archetype . private_message ) }
scope :private_posts , - > { joins ( :topic ) . where ( 'topics.archetype = ?' , Archetype . private_message ) }
2013-04-17 04:56:18 +08:00
scope :with_topic_subtype , - > ( subtype ) { joins ( :topic ) . where ( 'topics.subtype = ?' , subtype ) }
2014-06-27 01:48:07 +08:00
scope :visible , - > { joins ( :topic ) . where ( 'topics.visible = true' ) . where ( hidden : false ) }
2017-09-15 02:08:16 +08:00
scope :secured , - > ( guardian ) { where ( 'posts.post_type IN (?)' , Topic . visible_post_types ( guardian & . user ) ) }
2019-04-06 07:55:24 +08:00
2016-05-21 21:17:54 +08:00
scope :for_mailing_list , - > ( user , since ) {
2017-01-14 02:46:33 +08:00
q = created_since ( since )
2019-04-06 07:55:24 +08:00
. joins ( " INNER JOIN ( #{ Topic . for_digest ( user , Time . at ( 0 ) ) . select ( :id ) . to_sql } ) AS digest_topics ON digest_topics.id = posts.topic_id " ) # we want all topics with new content, regardless when they were created
. order ( 'posts.created_at ASC' )
2017-01-14 02:46:33 +08:00
q = q . where . not ( post_type : Post . types [ :whisper ] ) unless user . staff?
2019-04-06 07:55:24 +08:00
q
2016-05-21 21:17:54 +08:00
}
2019-04-06 07:55:24 +08:00
2017-10-04 08:47:53 +08:00
scope :raw_match , - > ( pattern , type = 'string' ) {
type = type & . downcase
case type
when 'string'
where ( 'raw ILIKE ?' , " % #{ pattern } % " )
when 'regex'
2018-08-23 20:49:00 +08:00
where ( 'raw ~* ?' , " (?n) #{ pattern } " )
2017-10-04 08:47:53 +08:00
end
}
2014-03-07 17:44:04 +08:00
2019-04-10 16:22:35 +08:00
scope :have_uploads , - > {
where (
2019-04-15 04:24:55 +08:00
" (posts.cooked LIKE '%<a %' OR posts.cooked LIKE '%<img %') AND (posts.cooked LIKE ? OR posts.cooked LIKE '%/original/%' OR posts.cooked LIKE '%/optimized/%') " ,
2019-04-10 16:22:35 +08:00
" %/uploads/ #{ RailsMultisite :: ConnectionManagement . current_db } /% "
)
}
2014-01-15 00:15:35 +08:00
delegate :username , to : :user
2014-03-07 17:44:04 +08:00
2013-03-19 02:59:34 +08:00
def self . hidden_reasons
2016-01-08 18:53:52 +08:00
@hidden_reasons || = Enum . new ( flag_threshold_reached : 1 ,
flag_threshold_reached_again : 2 ,
new_user_spam_threshold_reached : 3 ,
2018-07-05 17:07:46 +08:00
flagged_by_tl3_user : 4 ,
2018-10-10 23:50:00 +08:00
email_spam_header_found : 5 ,
flagged_by_tl4_user : 6 )
2013-03-19 02:59:34 +08:00
end
2013-03-19 04:03:46 +08:00
def self . types
2016-01-08 18:53:52 +08:00
@types || = Enum . new ( regular : 1 ,
moderator_action : 2 ,
small_action : 3 ,
whisper : 4 )
2013-03-19 04:03:46 +08:00
end
2014-01-01 03:37:43 +08:00
def self . cook_methods
2016-01-08 18:53:52 +08:00
@cook_methods || = Enum . new ( regular : 1 ,
raw_html : 2 ,
email : 3 )
2014-01-01 03:37:43 +08:00
end
2019-04-19 22:53:58 +08:00
def self . notices
@notices || = Enum . new ( custom : " custom " ,
new_user : " new_user " ,
returning_user : " returning_user " )
end
2013-10-15 22:21:30 +08:00
def self . find_by_detail ( key , value )
2014-05-06 21:41:59 +08:00
includes ( :post_details ) . find_by ( post_details : { key : key , value : value } )
2013-10-15 22:21:30 +08:00
end
2016-01-12 00:47:17 +08:00
def whisper?
post_type == Post . types [ :whisper ]
end
2013-10-15 22:21:30 +08:00
def add_detail ( key , value , extra = nil )
post_details . build ( key : key , value : value , extra : extra )
end
2013-10-10 07:32:03 +08:00
def limit_posts_per_day
2016-06-21 04:38:15 +08:00
if user && user . new_user_posting_on_first_day? && post_number && post_number > 1
2015-02-11 14:45:46 +08:00
RateLimiter . new ( user , " first-day-replies-per-day " , SiteSetting . max_replies_in_first_day , 1 . day . to_i )
2013-10-10 07:32:03 +08:00
end
end
2018-12-06 04:27:49 +08:00
def publish_change_to_clients! ( type , opts = { } )
# special failsafe for posts missing topics consistency checks should fix,
# but message is safe to skip
2015-09-11 04:01:23 +08:00
return unless topic
2018-12-06 04:27:49 +08:00
message = {
2015-09-22 06:50:52 +08:00
id : id ,
post_number : post_number ,
updated_at : Time . now ,
2015-10-12 09:45:04 +08:00
user_id : user_id ,
last_editor_id : last_editor_id ,
2017-01-20 14:37:22 +08:00
type : type ,
version : version
2018-12-06 04:27:49 +08:00
} . merge ( opts )
publish_message! ( " /topic/ #{ topic_id } " , message )
end
def publish_message! ( channel , message , opts = { } )
return unless topic
2015-09-22 06:50:52 +08:00
2015-09-25 08:15:58 +08:00
if Topic . visible_post_types . include? ( post_type )
2018-03-05 15:38:05 +08:00
if topic . private_message?
2018-12-06 08:20:36 +08:00
opts [ :user_ids ] = User . human_users . where ( " admin OR moderator " ) . pluck ( :id )
2018-12-06 04:27:49 +08:00
opts [ :user_ids ] |= topic . allowed_users . pluck ( :id )
2017-09-09 05:09:05 +08:00
else
2018-12-06 04:27:49 +08:00
opts [ :group_ids ] = topic . secure_group_ids
2017-09-09 05:09:05 +08:00
end
2015-09-25 08:15:58 +08:00
else
2018-12-06 08:20:36 +08:00
opts [ :user_ids ] = User . human_users
. where ( " admin OR moderator OR id = ? " , user_id )
. pluck ( :id )
2015-09-11 04:01:23 +08:00
end
2018-12-06 04:27:49 +08:00
MessageBus . publish ( channel , message , opts )
2014-08-29 11:34:32 +08:00
end
2017-07-28 09:20:09 +08:00
def trash! ( trashed_by = nil )
2013-06-14 01:41:45 +08:00
self . topic_links . each ( & :destroy )
2019-03-11 17:19:58 +08:00
self . delete_post_notices
2013-07-10 03:20:18 +08:00
super ( trashed_by )
2013-06-14 01:41:45 +08:00
end
2013-05-07 12:39:01 +08:00
def recover!
super
2018-10-02 23:25:08 +08:00
recover_public_post_actions
2013-06-14 01:41:45 +08:00
TopicLink . extract_from ( self )
2014-07-15 15:47:24 +08:00
QuotedPost . extract_from ( self )
2013-10-24 07:05:51 +08:00
if topic && topic . category_id && topic . category
2013-10-17 14:44:56 +08:00
topic . category . update_latest
end
2013-05-07 12:39:01 +08:00
end
2013-03-22 18:18:48 +08:00
# The key we use in redis to ensure unique posts
2013-02-06 03:16:51 +08:00
def unique_post_key
2015-02-03 01:44:21 +08:00
" unique-post- #{ user_id } : #{ raw_hash } "
2013-02-06 03:16:51 +08:00
end
2013-09-10 04:17:31 +08:00
def store_unique_post_key
if SiteSetting . unique_posts_mins > 0
2014-03-07 15:23:15 +08:00
$redis . setex ( unique_post_key , SiteSetting . unique_posts_mins . minutes . to_i , id )
2013-09-10 04:17:31 +08:00
end
end
def matches_recent_post?
2019-03-14 08:15:09 +08:00
post_id = $redis . get ( unique_post_key )
post_id != ( nil ) && post_id . to_i != ( id )
2013-09-10 04:17:31 +08:00
end
2013-02-06 03:16:51 +08:00
def raw_hash
2013-03-01 02:54:12 +08:00
return if raw . blank?
2014-06-16 10:14:06 +08:00
Digest :: SHA1 . hexdigest ( raw )
2013-02-06 03:16:51 +08:00
end
2013-02-12 15:43:48 +08:00
def self . white_listed_image_classes
2017-09-15 13:02:11 +08:00
@white_listed_image_classes || = [ 'avatar' , 'favicon' , 'thumbnail' , 'emoji' ]
2013-02-12 15:43:48 +08:00
end
2013-05-31 02:34:44 +08:00
def post_analyzer
2013-07-23 04:24:47 +08:00
@post_analyzers || = { }
@post_analyzers [ raw_hash ] || = PostAnalyzer . new ( raw , topic_id )
2013-05-31 02:34:44 +08:00
end
2013-02-12 15:43:48 +08:00
2018-02-09 07:26:56 +08:00
%w{ raw_mentions
linked_hosts
image_count
attachment_count
link_count
raw_links
has_oneboxes? } . each do | attr |
2013-05-31 02:34:44 +08:00
define_method ( attr ) do
2013-07-23 04:24:47 +08:00
post_analyzer . send ( attr )
2013-05-31 02:34:44 +08:00
end
end
2016-08-13 03:28:54 +08:00
def add_nofollow?
2018-09-17 10:02:20 +08:00
return false if user & . staff?
2016-08-16 00:57:58 +08:00
user . blank? || SiteSetting . tl3_links_no_follow? || ! user . has_trust_level? ( TrustLevel [ 3 ] )
2016-08-13 03:28:54 +08:00
end
def omit_nofollow?
2016-08-16 00:57:58 +08:00
! add_nofollow?
2016-08-13 03:28:54 +08:00
end
2017-10-18 02:37:51 +08:00
def cook ( raw , opts = { } )
2014-01-01 03:37:43 +08:00
# For some posts, for example those imported via RSS, we support raw HTML. In that
# case we can skip the rendering pipeline.
return raw if cook_method == Post . cook_methods [ :raw_html ]
2017-10-18 02:37:51 +08:00
options = opts . dup
options [ :cook_method ] = cook_method
post_user = self . user
options [ :user_id ] = post_user . id if post_user
2018-09-17 10:02:20 +08:00
options [ :omit_nofollow ] = true if omit_nofollow?
2017-10-18 02:37:51 +08:00
2018-09-17 10:02:20 +08:00
cooked = post_analyzer . cook ( raw , options )
2014-11-24 07:34:29 +08:00
new_cooked = Plugin :: Filter . apply ( :after_post_cook , self , cooked )
2015-07-30 02:54:33 +08:00
if post_type == Post . types [ :regular ]
if new_cooked != cooked && new_cooked . blank?
2017-10-18 02:37:51 +08:00
Rails . logger . debug ( " Plugin is blanking out post: #{ self . url } \n raw: #{ raw } " )
2015-07-30 02:54:33 +08:00
elsif new_cooked . blank?
2017-10-18 02:37:51 +08:00
Rails . logger . debug ( " Blank post detected post: #{ self . url } \n raw: #{ raw } " )
2015-07-30 02:54:33 +08:00
end
2014-11-24 07:34:29 +08:00
end
new_cooked
2013-02-06 03:16:51 +08:00
end
2013-04-06 01:59:00 +08:00
# Sometimes the post is being edited by someone else, for example, a mod.
# If that's the case, they should not be bound by the original poster's
# restrictions, for example on not posting images.
def acting_user
@acting_user || user
end
def acting_user = ( pu )
@acting_user = pu
end
2016-03-09 04:26:06 +08:00
def last_editor
self . last_editor_id ? ( User . find_by_id ( self . last_editor_id ) || user ) : user
end
2014-02-27 12:43:45 +08:00
def whitelisted_spam_hosts
hosts = SiteSetting
2017-07-28 09:20:09 +08:00
. white_listed_spam_host_domains
. split ( '|' )
. map { | h | h . strip }
. reject { | h | ! h . include? ( '.' ) }
2014-02-27 12:43:45 +08:00
hosts << GlobalSetting . hostname
2014-04-28 22:37:28 +08:00
hosts << RailsMultisite :: ConnectionManagement . current_hostname
2014-02-27 12:43:45 +08:00
end
2013-05-11 04:58:23 +08:00
def total_hosts_usage
hosts = linked_hosts . clone
2014-02-27 12:43:45 +08:00
whitelisted = whitelisted_spam_hosts
hosts . reject! do | h |
whitelisted . any? do | w |
h . end_with? ( w )
end
end
return hosts if hosts . length == 0
2013-05-11 04:58:23 +08:00
2013-05-25 03:20:58 +08:00
TopicLink . where ( domain : hosts . keys , user_id : acting_user . id )
2017-07-28 09:20:09 +08:00
. group ( :domain , :post_id )
. count
. each_key do | tuple |
2013-05-25 03:20:58 +08:00
domain = tuple [ 0 ]
hosts [ domain ] = ( hosts [ domain ] || 0 ) + 1
2013-05-11 04:58:23 +08:00
end
hosts
end
# Prevent new users from posting the same hosts too many times.
def has_host_spam?
2018-06-19 08:05:04 +08:00
return false if acting_user . present? && ( acting_user . staged? || acting_user . mature_staged? || acting_user . has_trust_level? ( TrustLevel [ 1 ] ) )
2017-08-11 05:18:57 +08:00
return false if topic & . private_message?
2013-05-11 04:58:23 +08:00
2016-04-26 05:03:17 +08:00
total_hosts_usage . values . any? { | count | count > = SiteSetting . newuser_spam_host_threshold }
2013-05-11 04:58:23 +08:00
end
2013-02-06 03:16:51 +08:00
def archetype
2017-09-13 01:04:53 +08:00
topic & . archetype
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
def self . regular_order
2013-02-07 23:45:24 +08:00
order ( :sort_order , :post_number )
2013-02-06 03:16:51 +08:00
end
def self . reverse_order
2013-02-07 23:45:24 +08:00
order ( 'sort_order desc, post_number desc' )
2013-02-06 03:16:51 +08:00
end
2018-06-21 12:00:54 +08:00
def self . summary ( topic_id )
topic_id = topic_id . to_i
2015-01-30 14:19:42 +08:00
# percent rank has tons of ties
2018-06-21 12:00:54 +08:00
where ( topic_id : topic_id )
2018-06-21 13:26:26 +08:00
. where ( [
" id = ANY(
(
SELECT posts . id
FROM posts
WHERE posts . topic_id = #{topic_id.to_i}
2018-06-21 14:00:20 +08:00
AND posts . post_number = 1
2018-06-21 13:26:26 +08:00
) UNION
(
SELECT p1 . id
FROM posts p1
WHERE p1 . percent_rank < = ?
AND p1 . topic_id = #{topic_id.to_i}
ORDER BY p1 . percent_rank
LIMIT ?
)
2018-06-21 12:00:54 +08:00
) " ,
SiteSetting . summary_percent_filter . to_f / 100 . 0 ,
SiteSetting . summary_max_results
] )
2013-02-06 03:16:51 +08:00
end
2019-03-08 16:48:35 +08:00
def delete_post_notices
2019-04-19 22:53:58 +08:00
self . custom_fields . delete ( " notice_type " )
self . custom_fields . delete ( " notice_args " )
2019-03-11 17:19:58 +08:00
self . save_custom_fields
2019-03-08 16:48:35 +08:00
end
2018-10-02 23:25:08 +08:00
def recover_public_post_actions
PostAction . publics
. with_deleted
. where ( post_id : self . id , id : self . custom_fields [ " deleted_public_actions " ] )
. find_each do | post_action |
post_action . recover!
post_action . save!
end
self . custom_fields . delete ( " deleted_public_actions " )
self . save_custom_fields
end
2013-03-01 02:54:12 +08:00
def filter_quotes ( parent_post = nil )
2013-02-06 03:16:51 +08:00
return cooked if parent_post . blank?
# We only filter quotes when there is exactly 1
return cooked unless ( quote_count == 1 )
2013-02-16 09:58:33 +08:00
parent_raw = parent_post . raw . sub ( / \ [quote.+ \/ quote \ ] /m , '' )
2013-02-06 03:16:51 +08:00
2013-03-05 08:42:44 +08:00
if raw [ parent_raw ] || ( parent_raw . size < SHORT_POST_CHARS )
2013-02-06 03:16:51 +08:00
return cooked . sub ( / \ <aside.+ \ < \/ aside \ > /m , '' )
end
cooked
end
def external_id
2013-02-07 23:45:24 +08:00
" #{ topic_id } / #{ post_number } "
2013-02-06 03:16:51 +08:00
end
2014-01-04 01:52:24 +08:00
def reply_to_post
return if reply_to_post_number . blank?
2014-05-06 21:41:59 +08:00
@reply_to_post || = Post . find_by ( " topic_id = :topic_id AND post_number = :post_number " , topic_id : topic_id , post_number : reply_to_post_number )
2014-01-04 01:52:24 +08:00
end
2013-02-06 03:16:51 +08:00
def reply_notification_target
2013-03-01 02:54:12 +08:00
return if reply_to_post_number . blank?
2014-05-06 21:41:59 +08:00
Post . find_by ( " topic_id = :topic_id AND post_number = :post_number AND user_id <> :user_id " , topic_id : topic_id , post_number : reply_to_post_number , user_id : user_id ) . try ( :user )
2013-02-06 03:16:51 +08:00
end
2013-04-30 11:25:55 +08:00
def self . excerpt ( cooked , maxlength = nil , options = { } )
2013-02-06 03:16:51 +08:00
maxlength || = SiteSetting . post_excerpt_maxlength
2013-04-30 11:25:55 +08:00
PrettyText . excerpt ( cooked , maxlength , options )
2013-02-06 03:16:51 +08:00
end
# Strip out most of the markup
2013-04-30 11:25:55 +08:00
def excerpt ( maxlength = nil , options = { } )
Post . excerpt ( cooked , maxlength , options )
2013-02-06 03:16:51 +08:00
end
2018-04-18 03:08:13 +08:00
def excerpt_for_topic
2018-06-11 17:13:53 +08:00
Post . excerpt ( cooked , 220 , strip_links : true , strip_images : true )
2018-04-18 03:08:13 +08:00
end
2013-05-26 08:18:04 +08:00
def is_first_post?
2015-04-24 01:33:29 +08:00
post_number . blank? ?
topic . try ( :highest_post_number ) == 0 :
post_number == 1
2013-05-26 08:18:04 +08:00
end
2016-08-11 01:24:01 +08:00
def is_reply_by_email?
via_email && post_number . present? && post_number > 1
end
2013-02-07 23:45:24 +08:00
def is_flagged?
2017-10-18 01:31:45 +08:00
post_actions . where ( post_action_type_id : PostActionType . flag_types_without_custom . values , deleted_at : nil ) . count != 0
2013-02-07 12:15:48 +08:00
end
2018-07-31 04:45:35 +08:00
def active_flags
post_actions . active . where ( post_action_type_id : PostActionType . flag_types_without_custom . values )
end
2019-01-04 01:03:01 +08:00
def reviewable_flag
ReviewableFlaggedPost . pending . find_by ( target : self )
end
def hide! ( post_action_type_id , reason = nil )
return if hidden?
reason || = hidden_at ?
Post . hidden_reasons [ :flag_threshold_reached_again ] :
Post . hidden_reasons [ :flag_threshold_reached ]
hiding_again = hidden_at . present?
self . hidden = true
self . hidden_at = Time . zone . now
self . hidden_reason_id = reason
save!
Topic . where (
" id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden) " ,
topic_id : topic_id
) . update_all ( visible : false )
# inform user
if user . present?
options = {
url : url ,
edit_delay : SiteSetting . cooldown_minutes_after_hiding_posts ,
flag_reason : I18n . t (
" flag_reasons. #{ PostActionType . types [ post_action_type_id ] } " ,
locale : SiteSetting . default_locale ,
base_path : Discourse . base_path
)
}
Jobs . enqueue_in (
5 . seconds ,
:send_system_message ,
user_id : user . id ,
message_type : hiding_again ? :post_hidden_again : :post_hidden ,
message_options : options
)
end
2016-03-31 01:27:34 +08:00
end
2013-02-07 12:15:48 +08:00
def unhide!
2015-12-30 05:59:26 +08:00
self . update_attributes ( hidden : false )
2015-04-24 01:33:29 +08:00
self . topic . update_attributes ( visible : true ) if is_first_post?
2014-08-11 16:48:00 +08:00
save ( validate : false )
2014-09-23 00:55:13 +08:00
publish_change_to_clients! ( :acted )
2013-02-07 12:15:48 +08:00
end
2016-01-13 01:38:49 +08:00
def full_url
" #{ Discourse . base_url } #{ url } "
end
2017-07-28 09:20:09 +08:00
def url ( opts = nil )
2017-04-25 03:26:06 +08:00
opts || = { }
2015-08-12 05:28:36 +08:00
if topic
2017-04-25 03:26:06 +08:00
Post . url ( topic . slug , topic . id , post_number , opts )
2015-08-12 05:28:36 +08:00
else
" /404 "
end
2013-04-22 15:45:03 +08:00
end
2016-06-17 09:27:52 +08:00
def unsubscribe_url ( user )
" #{ Discourse . base_url } /email/unsubscribe/ #{ UnsubscribeKey . create_key_for ( user , self ) } "
end
2017-07-28 09:20:09 +08:00
def self . url ( slug , topic_id , post_number , opts = nil )
2017-04-25 03:26:06 +08:00
opts || = { }
result = " /t/ "
result << " #{ slug } / " unless ! ! opts [ :without_slug ]
" #{ result } #{ topic_id } / #{ post_number } "
2013-04-22 15:45:03 +08:00
end
def self . urls ( post_ids )
2017-07-28 09:20:09 +08:00
ids = post_ids . map { | u | u }
2013-04-22 15:45:03 +08:00
if ids . length > 0
urls = { }
Topic . joins ( :posts ) . where ( 'posts.id' = > ids ) .
2017-07-28 09:20:09 +08:00
select ( [ 'posts.id as post_id' , 'post_number' , 'topics.slug' , 'topics.title' , 'topics.id' ] ) .
each do | t |
2013-04-24 16:05:35 +08:00
urls [ t . post_id . to_i ] = url ( t . slug , t . id , t . post_number )
2013-04-22 15:45:03 +08:00
end
urls
2013-04-24 16:05:35 +08:00
else
2013-04-22 15:45:03 +08:00
{ }
end
2013-02-06 03:16:51 +08:00
end
2017-07-28 09:20:09 +08:00
def revise ( updated_by , changes = { } , opts = { } )
2014-10-28 05:06:43 +08:00
PostRevisor . new ( self ) . revise! ( updated_by , changes , opts )
2013-02-09 23:33:07 +08:00
end
2019-01-17 11:53:09 +08:00
def self . rebake_old ( limit , priority : :normal , rate_limiter : true )
2019-01-04 06:24:46 +08:00
limiter = RateLimiter . new (
nil ,
" global_periodical_rebake_limit " ,
GlobalSetting . max_old_rebakes_per_15_minutes ,
900 ,
global : true
)
2014-07-18 04:22:46 +08:00
problems = [ ]
2014-05-30 12:45:39 +08:00
Post . where ( 'baked_version IS NULL OR baked_version < ?' , BAKED_VERSION )
2017-12-15 07:28:07 +08:00
. order ( 'id desc' )
2018-01-05 06:53:46 +08:00
. limit ( limit ) . pluck ( :id ) . each do | id |
2014-05-28 10:30:43 +08:00
begin
2019-01-04 06:24:46 +08:00
break if ! limiter . can_perform?
2018-01-05 06:53:46 +08:00
post = Post . find ( id )
2019-01-09 05:57:20 +08:00
post . rebake! ( priority : priority )
2019-01-04 06:24:46 +08:00
begin
2019-01-17 11:53:09 +08:00
limiter . performed! if rate_limiter
2019-01-04 06:24:46 +08:00
rescue RateLimiter :: LimitExceeded
break
end
2014-05-28 10:30:43 +08:00
rescue = > e
2018-01-05 06:53:46 +08:00
problems << { post : post , ex : e }
2017-12-27 09:44:41 +08:00
2018-01-05 06:53:46 +08:00
attempts = post . custom_fields [ " rebake_attempts " ] . to_i
2017-12-27 10:51:16 +08:00
2017-12-27 09:44:41 +08:00
if attempts > 3
2018-01-05 06:53:46 +08:00
post . update_columns ( baked_version : BAKED_VERSION )
2018-12-20 00:47:37 +08:00
Discourse . warn_exception ( e , message : " Can not rebake post # #{ post . id } after 3 attempts, giving up " )
2017-12-27 09:44:41 +08:00
else
2018-01-05 06:53:46 +08:00
post . custom_fields [ " rebake_attempts " ] = attempts + 1
post . save_custom_fields
2017-12-27 09:44:41 +08:00
end
2014-05-28 10:30:43 +08:00
end
end
2014-07-18 04:22:46 +08:00
problems
2014-05-28 10:30:43 +08:00
end
2019-01-09 05:57:20 +08:00
def rebake! ( invalidate_broken_images : false , invalidate_oneboxes : false , priority : nil )
new_cooked = cook ( raw , topic_id : topic_id , invalidate_oneboxes : invalidate_oneboxes )
2014-05-28 10:30:43 +08:00
old_cooked = cooked
2019-04-01 16:29:00 +08:00
update_columns (
2019-04-01 10:14:29 +08:00
cooked : new_cooked ,
baked_at : Time . zone . now ,
baked_version : BAKED_VERSION
)
2014-05-28 10:30:43 +08:00
2019-01-09 05:57:20 +08:00
if invalidate_broken_images
2018-12-27 01:52:07 +08:00
custom_fields . delete ( BROKEN_IMAGES )
save_custom_fields
end
2014-05-28 10:30:43 +08:00
# Extracts urls from the body
2014-07-15 15:47:24 +08:00
TopicLink . extract_from ( self )
QuotedPost . extract_from ( self )
2014-05-28 10:30:43 +08:00
# make sure we trigger the post process
2019-01-09 05:57:20 +08:00
trigger_post_process ( bypass_bump : true , priority : priority )
2014-05-28 10:30:43 +08:00
2014-09-23 00:55:13 +08:00
publish_change_to_clients! ( :rebaked )
2014-05-28 10:30:43 +08:00
new_cooked != old_cooked
end
2017-07-28 09:20:09 +08:00
def set_owner ( new_user , actor , skip_revision = false )
2014-10-28 05:06:43 +08:00
return if user_id == new_user . id
2018-08-20 18:26:19 +08:00
edit_reason = I18n . t ( 'change_owner.post_revision_text' , locale : SiteSetting . default_locale )
2017-09-14 22:15:07 +08:00
revise (
actor ,
{ raw : self . raw , user_id : new_user . id , edit_reason : edit_reason } ,
2018-02-27 22:46:20 +08:00
bypass_bump : true , skip_revision : skip_revision , skip_validations : true
2014-10-28 05:06:43 +08:00
)
2016-03-16 20:49:27 +08:00
if post_number == topic . highest_post_number
topic . update_columns ( last_post_user_id : new_user . id )
end
2014-03-28 09:28:14 +08:00
end
2013-02-06 03:16:51 +08:00
before_create do
2013-06-10 00:48:44 +08:00
PostCreator . before_create_tasks ( self )
2013-02-06 03:16:51 +08:00
end
2016-03-29 15:50:17 +08:00
def self . estimate_posts_per_day
val = $redis . get ( " estimated_posts_per_day " )
return val . to_i if val
posts_per_day = Topic . listable_topics . secured . joins ( :posts ) . merge ( Post . created_since ( 30 . days . ago ) ) . count / 30
$redis . setex ( " estimated_posts_per_day " , 1 . day . to_i , posts_per_day . to_s )
posts_per_day
end
2013-02-06 03:16:51 +08:00
# This calculates the geometric mean of the post timings and stores it along with
2013-02-07 23:45:24 +08:00
# each post.
2017-07-28 09:20:09 +08:00
def self . calculate_avg_time ( min_topic_age = nil )
2013-02-12 04:47:28 +08:00
retry_lock_error do
2018-06-20 15:48:02 +08:00
builder = DB . build ( " UPDATE posts
2013-02-12 04:47:28 +08:00
SET avg_time = ( x . gmean / 1000 )
FROM ( SELECT post_timings . topic_id ,
post_timings . post_number ,
2017-03-28 00:45:34 +08:00
round ( exp ( avg ( CASE WHEN msecs > 0 THEN ln ( msecs ) ELSE 0 END ) ) ) AS gmean
2013-02-12 04:47:28 +08:00
FROM post_timings
INNER JOIN posts AS p2
ON p2 . post_number = post_timings . post_number
AND p2 . topic_id = post_timings . topic_id
AND p2 . user_id < > post_timings . user_id
GROUP BY post_timings . topic_id , post_timings . post_number ) AS x
2014-02-27 08:45:20 +08:00
/ *where* / " )
builder . where ( " x.topic_id = posts.topic_id
2013-09-20 09:34:42 +08:00
AND x . post_number = posts . post_number
AND ( posts . avg_time < > ( x . gmean / 1000 ) :: int OR posts . avg_time IS NULL ) " )
2014-02-27 08:45:20 +08:00
if min_topic_age
builder . where ( " posts.topic_id IN (SELECT id FROM topics where bumped_at > :bumped_at) " ,
bumped_at : min_topic_age )
end
builder . exec
2013-02-12 04:47:28 +08:00
end
2013-02-06 03:16:51 +08:00
end
2013-02-07 23:45:24 +08:00
before_save do
2013-03-01 02:54:12 +08:00
self . last_editor_id || = user_id
2016-10-24 12:02:38 +08:00
2017-08-31 12:06:56 +08:00
if ! new_record? && will_save_change_to_raw?
2016-10-24 12:02:38 +08:00
self . cooked = cook ( raw , topic_id : topic_id )
end
2014-05-28 10:30:43 +08:00
self . baked_at = Time . new
2014-05-30 12:45:39 +08:00
self . baked_version = BAKED_VERSION
2013-02-06 03:16:51 +08:00
end
2013-03-19 03:12:31 +08:00
def advance_draft_sequence
return if topic . blank? # could be deleted
2018-07-11 15:06:49 +08:00
DraftSequence . next! ( last_editor_id , topic . draft_key ) if last_editor_id
2013-03-19 03:12:31 +08:00
end
2013-07-23 04:39:20 +08:00
# TODO: move to post-analyzer?
2013-03-19 03:54:08 +08:00
# Determine what posts are quoted by this post
2013-02-06 03:16:51 +08:00
def extract_quoted_post_numbers
2013-05-23 03:45:31 +08:00
temp_collector = [ ]
2013-02-06 03:16:51 +08:00
# Create relationships for the quotes
2013-05-23 03:38:45 +08:00
raw . scan ( / \ [quote= \ "([^"]+)" \ ] / ) . each do | quote |
args = parse_quote_into_arguments ( quote )
2013-05-23 03:45:31 +08:00
# If the topic attribute is present, ensure it's the same topic
2018-07-10 16:17:28 +08:00
if ! ( args [ :topic ] . present? && topic_id != args [ :topic ] ) && args [ :post ] != post_number
temp_collector << args [ :post ]
end
2013-02-06 03:16:51 +08:00
end
2013-02-07 23:45:24 +08:00
2013-05-23 03:45:31 +08:00
temp_collector . uniq!
self . quoted_post_numbers = temp_collector
self . quote_count = temp_collector . size
2013-02-06 03:16:51 +08:00
end
2013-03-19 03:54:08 +08:00
def save_reply_relationships
2013-05-24 00:09:06 +08:00
add_to_quoted_post_numbers ( reply_to_post_number )
return if self . quoted_post_numbers . blank?
2013-03-19 03:54:08 +08:00
# Create a reply relationship between quoted posts and this new post
2013-05-24 00:09:06 +08:00
self . quoted_post_numbers . each do | p |
2014-05-06 21:41:59 +08:00
post = Post . find_by ( topic_id : topic_id , post_number : p )
2013-05-24 00:09:06 +08:00
create_reply_relationship_with ( post )
2013-03-19 03:54:08 +08:00
end
end
2013-03-19 01:55:34 +08:00
# Enqueue post processing for this post
2019-01-17 10:24:32 +08:00
def trigger_post_process ( bypass_bump : false , priority : :normal , new_post : false )
2013-11-22 08:52:26 +08:00
args = {
post_id : id ,
2018-09-06 09:58:01 +08:00
bypass_bump : bypass_bump ,
2019-01-17 10:24:32 +08:00
new_post : new_post ,
2013-11-22 08:52:26 +08:00
}
2013-03-01 02:54:12 +08:00
args [ :image_sizes ] = image_sizes if image_sizes . present?
args [ :invalidate_oneboxes ] = true if invalidate_oneboxes . present?
2015-09-30 00:51:26 +08:00
args [ :cooking_options ] = self . cooking_options
2019-01-09 05:57:20 +08:00
2019-01-17 11:53:09 +08:00
if priority && priority != :normal
args [ :queue ] = priority . to_s
2019-01-09 05:57:20 +08:00
end
2013-02-07 23:45:24 +08:00
Jobs . enqueue ( :process_post , args )
2015-09-04 11:35:25 +08:00
DiscourseEvent . trigger ( :after_trigger_post_process , self )
2013-02-06 03:16:51 +08:00
end
2013-03-08 00:07:59 +08:00
2017-07-28 09:20:09 +08:00
def self . public_posts_count_per_day ( start_date , end_date , category_id = nil )
2015-06-24 21:19:39 +08:00
result = public_posts . where ( 'posts.created_at >= ? AND posts.created_at <= ?' , start_date , end_date )
2017-11-03 06:24:43 +08:00
. where ( post_type : Post . types [ :regular ] )
2015-06-24 21:19:39 +08:00
result = result . where ( 'topics.category_id = ?' , category_id ) if category_id
2018-06-05 15:29:17 +08:00
result
. group ( 'date(posts.created_at)' )
. order ( 'date(posts.created_at)' )
. count
2013-04-04 01:25:52 +08:00
end
2016-04-21 17:22:41 +08:00
def self . private_messages_count_per_day ( start_date , end_date , topic_subtype )
2018-06-05 15:29:17 +08:00
private_posts . with_topic_subtype ( topic_subtype )
. where ( 'posts.created_at >= ? AND posts.created_at <= ?' , start_date , end_date )
. group ( 'date(posts.created_at)' )
. order ( 'date(posts.created_at)' )
. count
2013-03-08 00:07:59 +08:00
end
2013-05-18 00:15:21 +08:00
2017-07-28 09:20:09 +08:00
def reply_history ( max_replies = 100 , guardian = nil )
2018-06-19 14:13:14 +08:00
post_ids = DB . query_single ( << ~ SQL , post_id : id , topic_id : topic_id )
WITH RECURSIVE breadcrumb ( id , reply_to_post_number ) AS (
SELECT p . id , p . reply_to_post_number FROM posts AS p
WHERE p . id = :post_id
UNION
SELECT p . id , p . reply_to_post_number FROM posts AS p , breadcrumb
WHERE breadcrumb . reply_to_post_number = p . post_number
AND p . topic_id = :topic_id
)
SELECT id from breadcrumb
WHERE id < > :post_id
ORDER by id
SQL
2014-10-27 06:44:42 +08:00
# [1,2,3][-10,-1] => nil
2017-07-28 09:20:09 +08:00
post_ids = ( post_ids [ ( 0 - max_replies ) .. - 1 ] || post_ids )
2014-10-27 06:44:42 +08:00
2015-09-25 08:15:58 +08:00
Post . secured ( guardian ) . where ( id : post_ids ) . includes ( :user , :topic ) . order ( :id ) . to_a
2013-08-07 05:42:36 +08:00
end
2017-12-15 07:23:51 +08:00
MAX_REPLY_LEVEL || = 1000
2018-04-21 05:05:51 +08:00
def reply_ids ( guardian = nil , only_replies_to_single_post : true )
2018-06-20 15:48:02 +08:00
builder = DB . build ( << ~ SQL )
2017-12-14 07:43:48 +08:00
WITH RECURSIVE breadcrumb ( id , level ) AS (
SELECT :post_id , 0
2017-12-14 05:12:06 +08:00
UNION
2017-12-14 07:43:48 +08:00
SELECT reply_id , level + 1
2018-04-21 05:05:51 +08:00
FROM post_replies AS r
JOIN breadcrumb AS b ON ( r . post_id = b . id )
WHERE r . post_id < > r . reply_id
AND b . level < :max_reply_level
2017-12-14 07:43:48 +08:00
) , breadcrumb_with_count AS (
2018-04-21 05:05:51 +08:00
SELECT
id ,
level ,
COUNT ( * ) AS count
FROM post_replies AS r
JOIN breadcrumb AS b ON ( r . reply_id = b . id )
WHERE r . reply_id < > r . post_id
GROUP BY id , level
2017-12-14 07:43:48 +08:00
)
2018-04-21 05:05:51 +08:00
SELECT id , level
FROM breadcrumb_with_count
/ *where* /
ORDER BY id
SQL
2017-12-14 05:12:06 +08:00
2018-04-21 05:05:51 +08:00
builder . where ( " level > 0 " )
# ignore posts that aren't replies to exactly one post
# for example it skips a post when it contains 2 quotes (which are replies) from different posts
builder . where ( " count = 1 " ) if only_replies_to_single_post
2018-06-20 15:48:02 +08:00
replies = builder . query_hash ( post_id : id , max_reply_level : MAX_REPLY_LEVEL )
replies . each { | r | r . symbolize_keys! }
2017-12-14 05:12:06 +08:00
secured_ids = Post . secured ( guardian ) . where ( id : replies . map { | r | r [ :id ] } ) . pluck ( :id ) . to_set
2017-12-14 07:43:48 +08:00
replies . reject { | r | ! secured_ids . include? ( r [ :id ] ) }
2017-12-14 05:12:06 +08:00
end
2013-12-12 10:41:34 +08:00
def revert_to ( number )
return if number > = version
2014-05-06 21:41:59 +08:00
post_revision = PostRevision . find_by ( post_id : id , number : ( number + 1 ) )
2013-12-12 10:41:34 +08:00
post_revision . modifications . each do | attribute , change |
attribute = " version " if attribute == " cached_version "
write_attribute ( attribute , change [ 0 ] )
end
end
2013-05-18 00:15:21 +08:00
2015-04-24 17:14:10 +08:00
def self . rebake_all_quoted_posts ( user_id )
return if user_id . blank?
2018-06-19 14:13:14 +08:00
DB . exec ( << ~ SQL , user_id )
2015-04-24 17:14:10 +08:00
WITH user_quoted_posts AS (
SELECT post_id
FROM quoted_posts
2018-06-19 14:13:14 +08:00
WHERE quoted_post_id IN ( SELECT id FROM posts WHERE user_id = ?)
2015-04-24 17:14:10 +08:00
)
UPDATE posts
SET baked_version = NULL
WHERE baked_version IS NOT NULL
AND id IN ( SELECT post_id FROM user_quoted_posts )
SQL
end
2016-01-27 09:19:49 +08:00
def seen? ( user )
PostTiming . where ( topic_id : topic_id , post_number : post_number , user_id : user . id ) . exists?
end
2016-12-22 10:13:14 +08:00
def index_search
SearchIndexer . index ( self )
end
2018-01-26 04:38:40 +08:00
def locked?
locked_by_id . present?
end
2018-09-06 09:58:01 +08:00
def link_post_uploads ( fragments : nil )
upload_ids = [ ]
fragments || = Nokogiri :: HTML :: fragment ( self . cooked )
fragments . css ( " a/@href " , " img/@src " ) . each do | media |
if upload = Upload . get_from_url ( media . value )
upload_ids << upload . id
end
end
upload_ids |= Upload . where ( id : downloaded_images . values ) . pluck ( :id )
values = upload_ids . map! { | upload_id | " ( #{ self . id } , #{ upload_id } ) " } . join ( " , " )
PostUpload . transaction do
PostUpload . where ( post_id : self . id ) . delete_all
if values . size > 0
DB . exec ( " INSERT INTO post_uploads (post_id, upload_id) VALUES #{ values } " )
end
end
end
def downloaded_images
JSON . parse ( self . custom_fields [ Post :: DOWNLOADED_IMAGES ] . presence || " {} " )
rescue JSON :: ParserError
{ }
end
2013-12-12 10:41:34 +08:00
private
2013-05-23 03:45:31 +08:00
2013-05-23 03:38:45 +08:00
def parse_quote_into_arguments ( quote )
return { } unless quote . present?
2014-08-18 11:00:02 +08:00
args = HashWithIndifferentAccess . new
2013-05-23 03:38:45 +08:00
quote . first . scan ( / ([a-z]+) \ :( \ d+) / ) . each do | arg |
2014-08-18 11:00:02 +08:00
args [ arg [ 0 ] ] = arg [ 1 ] . to_i
2013-05-23 03:38:45 +08:00
end
args
end
2013-05-23 03:45:31 +08:00
2013-05-24 00:07:45 +08:00
def add_to_quoted_post_numbers ( num )
return unless num . present?
self . quoted_post_numbers || = [ ]
self . quoted_post_numbers << num
end
2013-05-24 00:08:24 +08:00
def create_reply_relationship_with ( post )
2018-05-16 23:02:43 +08:00
return if post . nil? || self . deleted_at . present?
2013-05-24 00:08:24 +08:00
post_reply = post . post_replies . new ( reply_id : id )
if post_reply . save
2015-09-25 08:15:58 +08:00
if Topic . visible_post_types . include? ( self . post_type )
Post . where ( id : post . id ) . update_all [ 'reply_count = reply_count + 1' ]
end
2013-05-24 00:08:24 +08:00
end
end
2013-12-12 10:41:34 +08:00
2013-02-06 03:16:51 +08:00
end
2013-05-24 10:48:32 +08:00
# == Schema Information
#
# Table name: posts
#
# id :integer not null, primary key
2013-09-04 05:19:29 +08:00
# user_id :integer
2013-05-24 10:48:32 +08:00
# topic_id :integer not null
# post_number :integer not null
# raw :text not null
# cooked :text not null
2014-08-27 13:30:17 +08:00
# created_at :datetime not null
# updated_at :datetime not null
2013-05-24 10:48:32 +08:00
# reply_to_post_number :integer
# reply_count :integer default(0), not null
# quote_count :integer default(0), not null
# deleted_at :datetime
# off_topic_count :integer default(0), not null
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# bookmark_count :integer default(0), not null
# avg_time :integer
# score :float
# reads :integer default(0), not null
# post_type :integer default(1), not null
# sort_order :integer
# last_editor_id :integer
# hidden :boolean default(FALSE), not null
# hidden_reason_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# illegal_count :integer default(0), not null
# inappropriate_count :integer default(0), not null
# last_version_at :datetime not null
# user_deleted :boolean default(FALSE), not null
# reply_to_user_id :integer
# percent_rank :float default(1.0)
# notify_user_count :integer default(0), not null
2013-06-17 08:48:58 +08:00
# like_score :integer default(0), not null
2013-07-14 09:24:16 +08:00
# deleted_by_id :integer
2019-01-12 03:29:56 +08:00
# edit_reason :string
2014-02-07 08:07:36 +08:00
# word_count :integer
# version :integer default(1), not null
# cook_method :integer default(1), not null
2014-05-22 07:00:38 +08:00
# wiki :boolean default(FALSE), not null
2014-05-29 12:59:14 +08:00
# baked_at :datetime
2014-07-03 15:29:44 +08:00
# baked_version :integer
# hidden_at :datetime
2014-07-15 09:29:44 +08:00
# self_edits :integer default(0), not null
2014-07-31 11:14:40 +08:00
# reply_quoted :boolean default(FALSE), not null
2014-11-20 11:53:15 +08:00
# via_email :boolean default(FALSE), not null
# raw_email :text
# public_version :integer default(1), not null
2019-01-12 03:29:56 +08:00
# action_code :string
2016-10-31 17:41:33 +08:00
# image_url :string
2018-02-20 14:28:58 +08:00
# locked_by_id :integer
2013-05-24 10:48:32 +08:00
#
# Indexes
#
2018-07-16 14:18:07 +08:00
# idx_posts_created_at_topic_id (created_at,topic_id) WHERE (deleted_at IS NULL)
# idx_posts_deleted_posts (topic_id,post_number) WHERE (deleted_at IS NOT NULL)
# idx_posts_user_id_deleted_at (user_id) WHERE (deleted_at IS NULL)
2019-04-11 12:37:11 +08:00
# index_for_rebake_old (id) WHERE (((baked_version IS NULL) OR (baked_version < 2)) AND (deleted_at IS NULL))
2019-04-09 13:27:22 +08:00
# index_posts_on_id_and_baked_version (id DESC,baked_version) WHERE (deleted_at IS NULL)
2018-07-16 14:18:07 +08:00
# index_posts_on_reply_to_post_number (reply_to_post_number)
# index_posts_on_topic_id_and_percent_rank (topic_id,percent_rank)
# index_posts_on_topic_id_and_post_number (topic_id,post_number) UNIQUE
# index_posts_on_topic_id_and_sort_order (topic_id,sort_order)
# index_posts_on_user_id_and_created_at (user_id,created_at)
2013-05-24 10:48:32 +08:00
#