2013-02-05 14:16:51 -05:00
|
|
|
require_dependency 'pretty_text'
|
|
|
|
require_dependency 'rate_limiter'
|
2013-02-09 10:33:07 -05:00
|
|
|
require_dependency 'post_revisor'
|
2013-03-18 15:12:31 -04:00
|
|
|
require_dependency 'enum'
|
2013-05-30 14:34:44 -04:00
|
|
|
require_dependency 'post_analyzer'
|
2013-06-13 04:18:17 -04:00
|
|
|
require_dependency 'validators/post_validator'
|
2013-10-10 03:45:40 -04:00
|
|
|
require_dependency 'plugin/filter'
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
require 'archetype'
|
|
|
|
require 'digest/sha1'
|
|
|
|
|
|
|
|
class Post < ActiveRecord::Base
|
|
|
|
include RateLimiter::OnCreateRecord
|
2013-05-07 00:39:01 -04:00
|
|
|
include Trashable
|
2017-08-15 11:46:57 -04:00
|
|
|
include Searchable
|
2014-04-28 04:31:51 -04:00
|
|
|
include HasCustomFields
|
2015-02-25 14:53:21 -05:00
|
|
|
include LimitedEdit
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2018-07-25 11:44:09 -04:00
|
|
|
cattr_accessor :plugin_permitted_create_params
|
|
|
|
self.plugin_permitted_create_params = {}
|
2017-08-11 22:10:45 -04:00
|
|
|
|
2014-05-30 00:45:39 -04:00
|
|
|
# increase this number to force a system wide post rebake
|
2017-12-14 18:28:07 -05: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-27 22:30:43 -04:00
|
|
|
|
2013-02-07 10:45:24 -05:00
|
|
|
rate_limit
|
2013-10-09 19:32:03 -04:00
|
|
|
rate_limit :limit_posts_per_day
|
2013-02-19 01:57:14 -05:00
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
belongs_to :user
|
2016-12-02 01:03:31 -05:00
|
|
|
belongs_to :topic
|
2014-07-16 15:04:55 -04:00
|
|
|
|
2013-03-19 19:51:39 -04:00
|
|
|
belongs_to :reply_to_user, class_name: "User"
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
has_many :post_replies
|
|
|
|
has_many :replies, through: :post_replies
|
|
|
|
has_many :post_actions
|
2013-06-13 13:41:45 -04:00
|
|
|
has_many :topic_links
|
2015-12-01 00:52:43 -05:00
|
|
|
has_many :group_mentions, dependent: :destroy
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2013-06-13 17:44:24 -04:00
|
|
|
has_many :post_uploads
|
|
|
|
has_many :uploads, through: :post_uploads
|
2013-06-12 19:43:50 -04:00
|
|
|
|
2015-08-03 00:29:04 -04:00
|
|
|
has_one :post_stat
|
2013-05-22 15:33:33 -04:00
|
|
|
|
2016-04-20 15:29:27 -04:00
|
|
|
has_one :incoming_email
|
|
|
|
|
2013-10-15 10:21:30 -04:00
|
|
|
has_many :post_details
|
|
|
|
|
2013-12-11 21:41:34 -05:00
|
|
|
has_many :post_revisions
|
2018-05-28 19:34:12 -04:00
|
|
|
has_many :revisions, -> { order(:number) }, foreign_key: :post_id, class_name: 'PostRevision'
|
2013-12-11 21:41:34 -05:00
|
|
|
|
2014-07-16 15:04:55 -04:00
|
|
|
has_many :user_actions, foreign_key: :target_post_id
|
|
|
|
|
2019-01-02 09:24:13 -05:00
|
|
|
validates_with ::Validators::PostValidator, unless: :skip_validation
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2016-12-21 21:13:14 -05:00
|
|
|
after_save :index_search
|
2016-12-21 23:03:40 -05:00
|
|
|
after_save :create_user_action
|
2016-12-21 21:13:14 -05:00
|
|
|
|
2013-06-21 11:36:33 -04:00
|
|
|
# We can pass several creating options to a post via attributes
|
2019-01-02 09:24:13 -05:00
|
|
|
attr_accessor :image_sizes, :quoted_post_numbers, :no_bump, :invalidate_oneboxes, :cooking_options, :skip_unique_check, :skip_validation
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2017-11-16 09:45:07 -05:00
|
|
|
LARGE_IMAGES ||= "large_images".freeze
|
|
|
|
BROKEN_IMAGES ||= "broken_images".freeze
|
|
|
|
DOWNLOADED_IMAGES ||= "downloaded_images".freeze
|
|
|
|
|
|
|
|
SHORT_POST_CHARS ||= 1200
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2017-05-11 15:58:43 -04: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-14 14:08:16 -04:00
|
|
|
scope :by_newest, -> { order('created_at DESC, id DESC') }
|
2013-06-09 12:48:44 -04:00
|
|
|
scope :by_post_number, -> { order('post_number ASC') }
|
|
|
|
scope :with_user, -> { includes(:user) }
|
2017-09-14 14:08:16 -04:00
|
|
|
scope :created_since, -> (time_ago) { where('posts.created_at > ?', time_ago) }
|
2013-04-10 08:54:10 -04: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-16 16:56:18 -04:00
|
|
|
scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) }
|
2014-06-26 13:48:07 -04:00
|
|
|
scope :visible, -> { joins(:topic).where('topics.visible = true').where(hidden: false) }
|
2017-09-14 14:08:16 -04:00
|
|
|
scope :secured, -> (guardian) { where('posts.post_type IN (?)', Topic.visible_post_types(guardian&.user)) }
|
2016-05-21 09:17:54 -04:00
|
|
|
scope :for_mailing_list, ->(user, since) {
|
2017-01-13 13:46:33 -05:00
|
|
|
q = created_since(since)
|
|
|
|
.joins(:topic)
|
2017-09-19 03:48:50 -04:00
|
|
|
.where(topic: Topic.for_digest(user, Time.at(0))) # we want all topics with new content, regardless when they were created
|
2017-01-13 13:46:33 -05:00
|
|
|
|
|
|
|
q = q.where.not(post_type: Post.types[:whisper]) unless user.staff?
|
|
|
|
|
|
|
|
q.order('posts.created_at ASC')
|
2016-05-21 09:17:54 -04:00
|
|
|
}
|
2017-10-03 20:47:53 -04:00
|
|
|
scope :raw_match, -> (pattern, type = 'string') {
|
|
|
|
type = type&.downcase
|
|
|
|
|
|
|
|
case type
|
|
|
|
when 'string'
|
|
|
|
where('raw ILIKE ?', "%#{pattern}%")
|
|
|
|
when 'regex'
|
2018-08-23 08:49:00 -04:00
|
|
|
where('raw ~* ?', "(?n)#{pattern}")
|
2017-10-03 20:47:53 -04:00
|
|
|
end
|
|
|
|
}
|
2014-03-07 04:44:04 -05:00
|
|
|
|
2014-01-14 11:15:35 -05:00
|
|
|
delegate :username, to: :user
|
2014-03-07 04:44:04 -05:00
|
|
|
|
2013-03-18 14:59:34 -04:00
|
|
|
def self.hidden_reasons
|
2016-01-08 05:53:52 -05:00
|
|
|
@hidden_reasons ||= Enum.new(flag_threshold_reached: 1,
|
|
|
|
flag_threshold_reached_again: 2,
|
|
|
|
new_user_spam_threshold_reached: 3,
|
2018-07-05 05:07:46 -04:00
|
|
|
flagged_by_tl3_user: 4,
|
2018-10-10 11:50:00 -04:00
|
|
|
email_spam_header_found: 5,
|
|
|
|
flagged_by_tl4_user: 6)
|
2013-03-18 14:59:34 -04:00
|
|
|
end
|
|
|
|
|
2013-03-18 16:03:46 -04:00
|
|
|
def self.types
|
2016-01-08 05:53:52 -05:00
|
|
|
@types ||= Enum.new(regular: 1,
|
|
|
|
moderator_action: 2,
|
|
|
|
small_action: 3,
|
|
|
|
whisper: 4)
|
2013-03-18 16:03:46 -04:00
|
|
|
end
|
|
|
|
|
2013-12-31 14:37:43 -05:00
|
|
|
def self.cook_methods
|
2016-01-08 05:53:52 -05:00
|
|
|
@cook_methods ||= Enum.new(regular: 1,
|
|
|
|
raw_html: 2,
|
|
|
|
email: 3)
|
2013-12-31 14:37:43 -05:00
|
|
|
end
|
|
|
|
|
2013-10-15 10:21:30 -04:00
|
|
|
def self.find_by_detail(key, value)
|
2014-05-06 09:41:59 -04:00
|
|
|
includes(:post_details).find_by(post_details: { key: key, value: value })
|
2013-10-15 10:21:30 -04:00
|
|
|
end
|
|
|
|
|
2016-01-11 11:47:17 -05:00
|
|
|
def whisper?
|
|
|
|
post_type == Post.types[:whisper]
|
|
|
|
end
|
|
|
|
|
2013-10-15 10:21:30 -04:00
|
|
|
def add_detail(key, value, extra = nil)
|
|
|
|
post_details.build(key: key, value: value, extra: extra)
|
|
|
|
end
|
|
|
|
|
2013-10-09 19:32:03 -04:00
|
|
|
def limit_posts_per_day
|
2016-06-20 16:38:15 -04:00
|
|
|
if user && user.new_user_posting_on_first_day? && post_number && post_number > 1
|
2015-02-11 01:45:46 -05:00
|
|
|
RateLimiter.new(user, "first-day-replies-per-day", SiteSetting.max_replies_in_first_day, 1.day.to_i)
|
2013-10-09 19:32:03 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-05 15:27:49 -05: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-10 16:01:23 -04:00
|
|
|
return unless topic
|
|
|
|
|
2018-12-05 15:27:49 -05:00
|
|
|
message = {
|
2015-09-21 18:50:52 -04:00
|
|
|
id: id,
|
|
|
|
post_number: post_number,
|
|
|
|
updated_at: Time.now,
|
2015-10-11 21:45:04 -04:00
|
|
|
user_id: user_id,
|
|
|
|
last_editor_id: last_editor_id,
|
2017-01-20 01:37:22 -05:00
|
|
|
type: type,
|
|
|
|
version: version
|
2018-12-05 15:27:49 -05:00
|
|
|
}.merge(opts)
|
|
|
|
|
|
|
|
publish_message!("/topic/#{topic_id}", message)
|
|
|
|
end
|
|
|
|
|
|
|
|
def publish_message!(channel, message, opts = {})
|
|
|
|
return unless topic
|
2015-09-21 18:50:52 -04:00
|
|
|
|
2015-09-24 20:15:58 -04:00
|
|
|
if Topic.visible_post_types.include?(post_type)
|
2018-03-05 02:38:05 -05:00
|
|
|
if topic.private_message?
|
2018-12-05 19:20:36 -05:00
|
|
|
opts[:user_ids] = User.human_users.where("admin OR moderator").pluck(:id)
|
2018-12-05 15:27:49 -05:00
|
|
|
opts[:user_ids] |= topic.allowed_users.pluck(:id)
|
2017-09-08 17:09:05 -04:00
|
|
|
else
|
2018-12-05 15:27:49 -05:00
|
|
|
opts[:group_ids] = topic.secure_group_ids
|
2017-09-08 17:09:05 -04:00
|
|
|
end
|
2015-09-24 20:15:58 -04:00
|
|
|
else
|
2018-12-05 19:20:36 -05:00
|
|
|
opts[:user_ids] = User.human_users
|
|
|
|
.where("admin OR moderator OR id = ?", user_id)
|
|
|
|
.pluck(:id)
|
2015-09-10 16:01:23 -04:00
|
|
|
end
|
2018-12-05 15:27:49 -05:00
|
|
|
|
|
|
|
MessageBus.publish(channel, message, opts)
|
2014-08-28 23:34:32 -04:00
|
|
|
end
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def trash!(trashed_by = nil)
|
2013-06-13 13:41:45 -04:00
|
|
|
self.topic_links.each(&:destroy)
|
2013-07-09 15:20:18 -04:00
|
|
|
super(trashed_by)
|
2013-06-13 13:41:45 -04:00
|
|
|
end
|
|
|
|
|
2013-05-07 00:39:01 -04:00
|
|
|
def recover!
|
|
|
|
super
|
|
|
|
update_flagged_posts_count
|
2018-10-02 11:25:08 -04:00
|
|
|
recover_public_post_actions
|
2013-06-13 13:41:45 -04:00
|
|
|
TopicLink.extract_from(self)
|
2014-07-15 03:47:24 -04:00
|
|
|
QuotedPost.extract_from(self)
|
2013-10-23 19:05:51 -04:00
|
|
|
if topic && topic.category_id && topic.category
|
2013-10-17 02:44:56 -04:00
|
|
|
topic.category.update_latest
|
|
|
|
end
|
2013-05-07 00:39:01 -04:00
|
|
|
end
|
|
|
|
|
2013-03-22 06:18:48 -04:00
|
|
|
# The key we use in redis to ensure unique posts
|
2013-02-05 14:16:51 -05:00
|
|
|
def unique_post_key
|
2015-02-02 12:44:21 -05:00
|
|
|
"unique-post-#{user_id}:#{raw_hash}"
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-09-09 16:17:31 -04:00
|
|
|
def store_unique_post_key
|
|
|
|
if SiteSetting.unique_posts_mins > 0
|
2014-03-07 02:23:15 -05:00
|
|
|
$redis.setex(unique_post_key, SiteSetting.unique_posts_mins.minutes.to_i, id)
|
2013-09-09 16:17:31 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def matches_recent_post?
|
2014-03-07 02:23:15 -05:00
|
|
|
post_id = $redis.get(unique_post_key)
|
2017-07-27 21:20:09 -04:00
|
|
|
post_id != (nil) && post_id.to_i != (id)
|
2013-09-09 16:17:31 -04:00
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
def raw_hash
|
2013-02-28 13:54:12 -05:00
|
|
|
return if raw.blank?
|
2014-06-15 22:14:06 -04:00
|
|
|
Digest::SHA1.hexdigest(raw)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-02-12 02:43:48 -05:00
|
|
|
def self.white_listed_image_classes
|
2017-09-15 01:02:11 -04:00
|
|
|
@white_listed_image_classes ||= ['avatar', 'favicon', 'thumbnail', 'emoji']
|
2013-02-12 02:43:48 -05:00
|
|
|
end
|
|
|
|
|
2013-05-30 14:34:44 -04:00
|
|
|
def post_analyzer
|
2013-07-22 16:24:47 -04:00
|
|
|
@post_analyzers ||= {}
|
|
|
|
@post_analyzers[raw_hash] ||= PostAnalyzer.new(raw, topic_id)
|
2013-05-30 14:34:44 -04:00
|
|
|
end
|
2013-02-12 02:43:48 -05:00
|
|
|
|
2018-02-08 18:26:56 -05:00
|
|
|
%w{raw_mentions
|
|
|
|
linked_hosts
|
|
|
|
image_count
|
|
|
|
attachment_count
|
|
|
|
link_count
|
|
|
|
raw_links
|
|
|
|
has_oneboxes?}.each do |attr|
|
2013-05-30 14:34:44 -04:00
|
|
|
define_method(attr) do
|
2013-07-22 16:24:47 -04:00
|
|
|
post_analyzer.send(attr)
|
2013-05-30 14:34:44 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-08-12 15:28:54 -04:00
|
|
|
def add_nofollow?
|
2018-09-16 22:02:20 -04:00
|
|
|
return false if user&.staff?
|
2016-08-15 12:57:58 -04:00
|
|
|
user.blank? || SiteSetting.tl3_links_no_follow? || !user.has_trust_level?(TrustLevel[3])
|
2016-08-12 15:28:54 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def omit_nofollow?
|
2016-08-15 12:57:58 -04:00
|
|
|
!add_nofollow?
|
2016-08-12 15:28:54 -04:00
|
|
|
end
|
|
|
|
|
2017-10-17 14:37:51 -04:00
|
|
|
def cook(raw, opts = {})
|
2013-12-31 14:37:43 -05: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-17 14:37:51 -04:00
|
|
|
options = opts.dup
|
|
|
|
options[:cook_method] = cook_method
|
|
|
|
|
|
|
|
post_user = self.user
|
|
|
|
options[:user_id] = post_user.id if post_user
|
2018-09-16 22:02:20 -04:00
|
|
|
options[:omit_nofollow] = true if omit_nofollow?
|
2017-10-17 14:37:51 -04:00
|
|
|
|
2018-09-16 22:02:20 -04:00
|
|
|
cooked = post_analyzer.cook(raw, options)
|
2014-11-23 18:34:29 -05:00
|
|
|
|
|
|
|
new_cooked = Plugin::Filter.apply(:after_post_cook, self, cooked)
|
|
|
|
|
2015-07-29 14:54:33 -04:00
|
|
|
if post_type == Post.types[:regular]
|
|
|
|
if new_cooked != cooked && new_cooked.blank?
|
2017-10-17 14:37:51 -04:00
|
|
|
Rails.logger.debug("Plugin is blanking out post: #{self.url}\nraw: #{raw}")
|
2015-07-29 14:54:33 -04:00
|
|
|
elsif new_cooked.blank?
|
2017-10-17 14:37:51 -04:00
|
|
|
Rails.logger.debug("Blank post detected post: #{self.url}\nraw: #{raw}")
|
2015-07-29 14:54:33 -04:00
|
|
|
end
|
2014-11-23 18:34:29 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
new_cooked
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-04-05 13:59:00 -04: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-08 15:26:06 -05:00
|
|
|
def last_editor
|
|
|
|
self.last_editor_id ? (User.find_by_id(self.last_editor_id) || user) : user
|
|
|
|
end
|
|
|
|
|
2014-02-26 23:43:45 -05:00
|
|
|
def whitelisted_spam_hosts
|
|
|
|
hosts = SiteSetting
|
2017-07-27 21:20:09 -04:00
|
|
|
.white_listed_spam_host_domains
|
|
|
|
.split('|')
|
|
|
|
.map { |h| h.strip }
|
|
|
|
.reject { |h| !h.include?('.') }
|
2014-02-26 23:43:45 -05:00
|
|
|
|
|
|
|
hosts << GlobalSetting.hostname
|
2014-04-28 10:37:28 -04:00
|
|
|
hosts << RailsMultisite::ConnectionManagement.current_hostname
|
2014-02-26 23:43:45 -05:00
|
|
|
|
|
|
|
end
|
|
|
|
|
2013-05-10 16:58:23 -04:00
|
|
|
def total_hosts_usage
|
|
|
|
hosts = linked_hosts.clone
|
2014-02-26 23:43:45 -05: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-10 16:58:23 -04:00
|
|
|
|
2013-05-24 15:20:58 -04:00
|
|
|
TopicLink.where(domain: hosts.keys, user_id: acting_user.id)
|
2017-07-27 21:20:09 -04:00
|
|
|
.group(:domain, :post_id)
|
|
|
|
.count
|
|
|
|
.each_key do |tuple|
|
2013-05-24 15:20:58 -04:00
|
|
|
domain = tuple[0]
|
|
|
|
hosts[domain] = (hosts[domain] || 0) + 1
|
2013-05-10 16:58:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
hosts
|
|
|
|
end
|
|
|
|
|
|
|
|
# Prevent new users from posting the same hosts too many times.
|
|
|
|
def has_host_spam?
|
2018-06-18 20:05:04 -04:00
|
|
|
return false if acting_user.present? && (acting_user.staged? || acting_user.mature_staged? || acting_user.has_trust_level?(TrustLevel[1]))
|
2017-08-10 17:18:57 -04:00
|
|
|
return false if topic&.private_message?
|
2013-05-10 16:58:23 -04:00
|
|
|
|
2016-04-25 17:03:17 -04:00
|
|
|
total_hosts_usage.values.any? { |count| count >= SiteSetting.newuser_spam_host_threshold }
|
2013-05-10 16:58:23 -04:00
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
def archetype
|
2017-09-12 13:04:53 -04:00
|
|
|
topic&.archetype
|
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
|
|
|
def self.regular_order
|
2013-02-07 10:45:24 -05:00
|
|
|
order(:sort_order, :post_number)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.reverse_order
|
2013-02-07 10:45:24 -05:00
|
|
|
order('sort_order desc, post_number desc')
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2018-06-21 00:00:54 -04:00
|
|
|
def self.summary(topic_id)
|
|
|
|
topic_id = topic_id.to_i
|
2015-01-30 01:19:42 -05:00
|
|
|
|
|
|
|
# percent rank has tons of ties
|
2018-06-21 00:00:54 -04:00
|
|
|
where(topic_id: topic_id)
|
2018-06-21 01:26:26 -04:00
|
|
|
.where([
|
|
|
|
"id = ANY(
|
|
|
|
(
|
|
|
|
SELECT posts.id
|
|
|
|
FROM posts
|
|
|
|
WHERE posts.topic_id = #{topic_id.to_i}
|
2018-06-21 02:00:20 -04:00
|
|
|
AND posts.post_number = 1
|
2018-06-21 01:26:26 -04: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 00:00:54 -04:00
|
|
|
)",
|
|
|
|
SiteSetting.summary_percent_filter.to_f / 100.0,
|
|
|
|
SiteSetting.summary_max_results
|
|
|
|
])
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-02-05 20:13:41 -05:00
|
|
|
def update_flagged_posts_count
|
|
|
|
PostAction.update_flagged_posts_count
|
|
|
|
end
|
|
|
|
|
2018-10-02 11:25:08 -04: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-02-28 13:54:12 -05:00
|
|
|
def filter_quotes(parent_post = nil)
|
2013-02-05 14:16:51 -05:00
|
|
|
return cooked if parent_post.blank?
|
|
|
|
|
|
|
|
# We only filter quotes when there is exactly 1
|
|
|
|
return cooked unless (quote_count == 1)
|
|
|
|
|
2013-02-15 20:58:33 -05:00
|
|
|
parent_raw = parent_post.raw.sub(/\[quote.+\/quote\]/m, '')
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2013-03-04 19:42:44 -05:00
|
|
|
if raw[parent_raw] || (parent_raw.size < SHORT_POST_CHARS)
|
2013-02-05 14:16:51 -05:00
|
|
|
return cooked.sub(/\<aside.+\<\/aside\>/m, '')
|
|
|
|
end
|
|
|
|
|
|
|
|
cooked
|
|
|
|
end
|
|
|
|
|
|
|
|
def external_id
|
2013-02-07 10:45:24 -05:00
|
|
|
"#{topic_id}/#{post_number}"
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2014-01-03 12:52:24 -05:00
|
|
|
def reply_to_post
|
|
|
|
return if reply_to_post_number.blank?
|
2014-05-06 09:41:59 -04: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-03 12:52:24 -05:00
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
def reply_notification_target
|
2013-02-28 13:54:12 -05:00
|
|
|
return if reply_to_post_number.blank?
|
2014-05-06 09:41:59 -04: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-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-04-29 23:25:55 -04:00
|
|
|
def self.excerpt(cooked, maxlength = nil, options = {})
|
2013-02-05 14:16:51 -05:00
|
|
|
maxlength ||= SiteSetting.post_excerpt_maxlength
|
2013-04-29 23:25:55 -04:00
|
|
|
PrettyText.excerpt(cooked, maxlength, options)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Strip out most of the markup
|
2013-04-29 23:25:55 -04:00
|
|
|
def excerpt(maxlength = nil, options = {})
|
|
|
|
Post.excerpt(cooked, maxlength, options)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2018-04-17 15:08:13 -04:00
|
|
|
def excerpt_for_topic
|
2018-06-11 05:13:53 -04:00
|
|
|
Post.excerpt(cooked, 220, strip_links: true, strip_images: true)
|
2018-04-17 15:08:13 -04:00
|
|
|
end
|
|
|
|
|
2013-05-25 20:18:04 -04:00
|
|
|
def is_first_post?
|
2015-04-23 13:33:29 -04:00
|
|
|
post_number.blank? ?
|
|
|
|
topic.try(:highest_post_number) == 0 :
|
|
|
|
post_number == 1
|
2013-05-25 20:18:04 -04:00
|
|
|
end
|
|
|
|
|
2016-08-10 13:24:01 -04:00
|
|
|
def is_reply_by_email?
|
|
|
|
via_email && post_number.present? && post_number > 1
|
|
|
|
end
|
|
|
|
|
2013-02-07 10:45:24 -05:00
|
|
|
def is_flagged?
|
2017-10-17 13:31:45 -04:00
|
|
|
post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0
|
2013-02-06 23:15:48 -05:00
|
|
|
end
|
|
|
|
|
2018-07-30 16:45:35 -04:00
|
|
|
def active_flags
|
|
|
|
post_actions.active.where(post_action_type_id: PostActionType.flag_types_without_custom.values)
|
|
|
|
end
|
|
|
|
|
2016-03-30 13:27:34 -04:00
|
|
|
def has_active_flag?
|
2018-07-30 16:45:35 -04:00
|
|
|
active_flags.count != 0
|
2016-03-30 13:27:34 -04:00
|
|
|
end
|
|
|
|
|
2013-02-06 23:15:48 -05:00
|
|
|
def unhide!
|
2015-12-29 16:59:26 -05:00
|
|
|
self.update_attributes(hidden: false)
|
2015-04-23 13:33:29 -04:00
|
|
|
self.topic.update_attributes(visible: true) if is_first_post?
|
2014-08-11 04:48:00 -04:00
|
|
|
save(validate: false)
|
2014-09-22 12:55:13 -04:00
|
|
|
publish_change_to_clients!(:acted)
|
2013-02-06 23:15:48 -05:00
|
|
|
end
|
|
|
|
|
2016-01-12 12:38:49 -05:00
|
|
|
def full_url
|
|
|
|
"#{Discourse.base_url}#{url}"
|
|
|
|
end
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def url(opts = nil)
|
2017-04-24 15:26:06 -04:00
|
|
|
opts ||= {}
|
|
|
|
|
2015-08-11 17:28:36 -04:00
|
|
|
if topic
|
2017-04-24 15:26:06 -04:00
|
|
|
Post.url(topic.slug, topic.id, post_number, opts)
|
2015-08-11 17:28:36 -04:00
|
|
|
else
|
|
|
|
"/404"
|
|
|
|
end
|
2013-04-22 03:45:03 -04:00
|
|
|
end
|
|
|
|
|
2016-06-16 21:27:52 -04:00
|
|
|
def unsubscribe_url(user)
|
|
|
|
"#{Discourse.base_url}/email/unsubscribe/#{UnsubscribeKey.create_key_for(user, self)}"
|
|
|
|
end
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def self.url(slug, topic_id, post_number, opts = nil)
|
2017-04-24 15:26:06 -04:00
|
|
|
opts ||= {}
|
|
|
|
|
|
|
|
result = "/t/"
|
|
|
|
result << "#{slug}/" unless !!opts[:without_slug]
|
|
|
|
|
|
|
|
"#{result}#{topic_id}/#{post_number}"
|
2013-04-22 03:45:03 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.urls(post_ids)
|
2017-07-27 21:20:09 -04:00
|
|
|
ids = post_ids.map { |u| u }
|
2013-04-22 03:45:03 -04:00
|
|
|
if ids.length > 0
|
|
|
|
urls = {}
|
|
|
|
Topic.joins(:posts).where('posts.id' => ids).
|
2017-07-27 21:20:09 -04:00
|
|
|
select(['posts.id as post_id', 'post_number', 'topics.slug', 'topics.title', 'topics.id']).
|
|
|
|
each do |t|
|
2013-04-24 04:05:35 -04:00
|
|
|
urls[t.post_id.to_i] = url(t.slug, t.id, t.post_number)
|
2013-04-22 03:45:03 -04:00
|
|
|
end
|
|
|
|
urls
|
2013-04-24 04:05:35 -04:00
|
|
|
else
|
2013-04-22 03:45:03 -04:00
|
|
|
{}
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def revise(updated_by, changes = {}, opts = {})
|
2014-10-27 17:06:43 -04:00
|
|
|
PostRevisor.new(self).revise!(updated_by, changes, opts)
|
2013-02-09 10:33:07 -05:00
|
|
|
end
|
|
|
|
|
2019-01-16 22:53:09 -05:00
|
|
|
def self.rebake_old(limit, priority: :normal, rate_limiter: true)
|
2019-01-03 17:24:46 -05:00
|
|
|
|
|
|
|
limiter = RateLimiter.new(
|
|
|
|
nil,
|
|
|
|
"global_periodical_rebake_limit",
|
|
|
|
GlobalSetting.max_old_rebakes_per_15_minutes,
|
|
|
|
900,
|
|
|
|
global: true
|
|
|
|
)
|
|
|
|
|
2014-07-17 16:22:46 -04:00
|
|
|
problems = []
|
2014-05-30 00:45:39 -04:00
|
|
|
Post.where('baked_version IS NULL OR baked_version < ?', BAKED_VERSION)
|
2017-12-14 18:28:07 -05:00
|
|
|
.order('id desc')
|
2018-01-04 17:53:46 -05:00
|
|
|
.limit(limit).pluck(:id).each do |id|
|
2014-05-27 22:30:43 -04:00
|
|
|
begin
|
2019-01-03 17:24:46 -05:00
|
|
|
|
|
|
|
break if !limiter.can_perform?
|
|
|
|
|
2018-01-04 17:53:46 -05:00
|
|
|
post = Post.find(id)
|
2019-01-08 16:57:20 -05:00
|
|
|
post.rebake!(priority: priority)
|
2019-01-03 17:24:46 -05:00
|
|
|
|
|
|
|
begin
|
2019-01-16 22:53:09 -05:00
|
|
|
limiter.performed! if rate_limiter
|
2019-01-03 17:24:46 -05:00
|
|
|
rescue RateLimiter::LimitExceeded
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
2014-05-27 22:30:43 -04:00
|
|
|
rescue => e
|
2018-01-04 17:53:46 -05:00
|
|
|
problems << { post: post, ex: e }
|
2017-12-26 20:44:41 -05:00
|
|
|
|
2018-01-04 17:53:46 -05:00
|
|
|
attempts = post.custom_fields["rebake_attempts"].to_i
|
2017-12-26 21:51:16 -05:00
|
|
|
|
2017-12-26 20:44:41 -05:00
|
|
|
if attempts > 3
|
2018-01-04 17:53:46 -05:00
|
|
|
post.update_columns(baked_version: BAKED_VERSION)
|
2018-12-19 11:47:37 -05:00
|
|
|
Discourse.warn_exception(e, message: "Can not rebake post# #{post.id} after 3 attempts, giving up")
|
2017-12-26 20:44:41 -05:00
|
|
|
else
|
2018-01-04 17:53:46 -05:00
|
|
|
post.custom_fields["rebake_attempts"] = attempts + 1
|
|
|
|
post.save_custom_fields
|
2017-12-26 20:44:41 -05:00
|
|
|
end
|
|
|
|
|
2014-05-27 22:30:43 -04:00
|
|
|
end
|
|
|
|
end
|
2014-07-17 16:22:46 -04:00
|
|
|
problems
|
2014-05-27 22:30:43 -04:00
|
|
|
end
|
|
|
|
|
2019-01-08 16:57:20 -05: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-27 22:30:43 -04:00
|
|
|
old_cooked = cooked
|
|
|
|
|
2017-01-31 21:52:15 -05:00
|
|
|
update_columns(cooked: new_cooked, baked_at: Time.new, baked_version: BAKED_VERSION)
|
2014-05-27 22:30:43 -04:00
|
|
|
|
2019-01-08 16:57:20 -05:00
|
|
|
if invalidate_broken_images
|
2018-12-26 12:52:07 -05:00
|
|
|
custom_fields.delete(BROKEN_IMAGES)
|
|
|
|
save_custom_fields
|
|
|
|
end
|
|
|
|
|
2014-05-27 22:30:43 -04:00
|
|
|
# Extracts urls from the body
|
2014-07-15 03:47:24 -04:00
|
|
|
TopicLink.extract_from(self)
|
|
|
|
QuotedPost.extract_from(self)
|
|
|
|
|
2014-05-27 22:30:43 -04:00
|
|
|
# make sure we trigger the post process
|
2019-01-08 16:57:20 -05:00
|
|
|
trigger_post_process(bypass_bump: true, priority: priority)
|
2014-05-27 22:30:43 -04:00
|
|
|
|
2014-09-22 12:55:13 -04:00
|
|
|
publish_change_to_clients!(:rebaked)
|
|
|
|
|
2014-05-27 22:30:43 -04:00
|
|
|
new_cooked != old_cooked
|
|
|
|
end
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def set_owner(new_user, actor, skip_revision = false)
|
2014-10-27 17:06:43 -04:00
|
|
|
return if user_id == new_user.id
|
|
|
|
|
2018-08-20 06:26:19 -04:00
|
|
|
edit_reason = I18n.t('change_owner.post_revision_text', locale: SiteSetting.default_locale)
|
2017-09-14 10:15:07 -04:00
|
|
|
|
|
|
|
revise(
|
|
|
|
actor,
|
|
|
|
{ raw: self.raw, user_id: new_user.id, edit_reason: edit_reason },
|
2018-02-27 09:46:20 -05:00
|
|
|
bypass_bump: true, skip_revision: skip_revision, skip_validations: true
|
2014-10-27 17:06:43 -04:00
|
|
|
)
|
2016-03-16 08:49:27 -04:00
|
|
|
|
|
|
|
if post_number == topic.highest_post_number
|
|
|
|
topic.update_columns(last_post_user_id: new_user.id)
|
|
|
|
end
|
2014-03-27 21:28:14 -04:00
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
before_create do
|
2013-06-09 12:48:44 -04:00
|
|
|
PostCreator.before_create_tasks(self)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2016-03-29 03:50:17 -04: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-05 14:16:51 -05:00
|
|
|
# This calculates the geometric mean of the post timings and stores it along with
|
2013-02-07 10:45:24 -05:00
|
|
|
# each post.
|
2017-07-27 21:20:09 -04:00
|
|
|
def self.calculate_avg_time(min_topic_age = nil)
|
2013-02-11 15:47:28 -05:00
|
|
|
retry_lock_error do
|
2018-06-20 03:48:02 -04:00
|
|
|
builder = DB.build("UPDATE posts
|
2013-02-11 15:47:28 -05:00
|
|
|
SET avg_time = (x.gmean / 1000)
|
|
|
|
FROM (SELECT post_timings.topic_id,
|
|
|
|
post_timings.post_number,
|
2017-03-27 12:45:34 -04:00
|
|
|
round(exp(avg(CASE WHEN msecs > 0 THEN ln(msecs) ELSE 0 END))) AS gmean
|
2013-02-11 15:47:28 -05: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-26 19:45:20 -05:00
|
|
|
/*where*/")
|
|
|
|
|
|
|
|
builder.where("x.topic_id = posts.topic_id
|
2013-09-19 21:34:42 -04:00
|
|
|
AND x.post_number = posts.post_number
|
|
|
|
AND (posts.avg_time <> (x.gmean / 1000)::int OR posts.avg_time IS NULL)")
|
2014-02-26 19:45:20 -05: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-11 15:47:28 -05:00
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-02-07 10:45:24 -05:00
|
|
|
before_save do
|
2013-02-28 13:54:12 -05:00
|
|
|
self.last_editor_id ||= user_id
|
2016-10-24 00:02:38 -04:00
|
|
|
|
2017-08-31 00:06:56 -04:00
|
|
|
if !new_record? && will_save_change_to_raw?
|
2016-10-24 00:02:38 -04:00
|
|
|
self.cooked = cook(raw, topic_id: topic_id)
|
|
|
|
end
|
|
|
|
|
2014-05-27 22:30:43 -04:00
|
|
|
self.baked_at = Time.new
|
2014-05-30 00:45:39 -04:00
|
|
|
self.baked_version = BAKED_VERSION
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-03-18 15:12:31 -04:00
|
|
|
def advance_draft_sequence
|
|
|
|
return if topic.blank? # could be deleted
|
2018-07-11 03:06:49 -04:00
|
|
|
DraftSequence.next!(last_editor_id, topic.draft_key) if last_editor_id
|
2013-03-18 15:12:31 -04:00
|
|
|
end
|
|
|
|
|
2013-07-22 16:39:20 -04:00
|
|
|
# TODO: move to post-analyzer?
|
2013-03-18 15:54:08 -04:00
|
|
|
# Determine what posts are quoted by this post
|
2013-02-05 14:16:51 -05:00
|
|
|
def extract_quoted_post_numbers
|
2013-05-22 15:45:31 -04:00
|
|
|
temp_collector = []
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
# Create relationships for the quotes
|
2013-05-22 15:38:45 -04:00
|
|
|
raw.scan(/\[quote=\"([^"]+)"\]/).each do |quote|
|
|
|
|
args = parse_quote_into_arguments(quote)
|
2013-05-22 15:45:31 -04:00
|
|
|
# If the topic attribute is present, ensure it's the same topic
|
2018-07-10 04:17:28 -04:00
|
|
|
if !(args[:topic].present? && topic_id != args[:topic]) && args[:post] != post_number
|
|
|
|
temp_collector << args[:post]
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2013-02-07 10:45:24 -05:00
|
|
|
|
2013-05-22 15:45:31 -04:00
|
|
|
temp_collector.uniq!
|
|
|
|
self.quoted_post_numbers = temp_collector
|
|
|
|
self.quote_count = temp_collector.size
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-03-18 15:54:08 -04:00
|
|
|
def save_reply_relationships
|
2013-05-23 12:09:06 -04:00
|
|
|
add_to_quoted_post_numbers(reply_to_post_number)
|
|
|
|
return if self.quoted_post_numbers.blank?
|
2013-03-18 15:54:08 -04:00
|
|
|
|
|
|
|
# Create a reply relationship between quoted posts and this new post
|
2013-05-23 12:09:06 -04:00
|
|
|
self.quoted_post_numbers.each do |p|
|
2014-05-06 09:41:59 -04:00
|
|
|
post = Post.find_by(topic_id: topic_id, post_number: p)
|
2013-05-23 12:09:06 -04:00
|
|
|
create_reply_relationship_with(post)
|
2013-03-18 15:54:08 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-03-18 13:55:34 -04:00
|
|
|
# Enqueue post processing for this post
|
2019-01-16 21:24:32 -05:00
|
|
|
def trigger_post_process(bypass_bump: false, priority: :normal, new_post: false)
|
2013-11-21 19:52:26 -05:00
|
|
|
args = {
|
|
|
|
post_id: id,
|
2018-09-05 21:58:01 -04:00
|
|
|
bypass_bump: bypass_bump,
|
2019-01-16 21:24:32 -05:00
|
|
|
new_post: new_post,
|
2013-11-21 19:52:26 -05:00
|
|
|
}
|
2013-02-28 13:54:12 -05:00
|
|
|
args[:image_sizes] = image_sizes if image_sizes.present?
|
|
|
|
args[:invalidate_oneboxes] = true if invalidate_oneboxes.present?
|
2015-09-29 12:51:26 -04:00
|
|
|
args[:cooking_options] = self.cooking_options
|
2019-01-08 16:57:20 -05:00
|
|
|
|
2019-01-16 22:53:09 -05:00
|
|
|
if priority && priority != :normal
|
|
|
|
args[:queue] = priority.to_s
|
2019-01-08 16:57:20 -05:00
|
|
|
end
|
|
|
|
|
2013-02-07 10:45:24 -05:00
|
|
|
Jobs.enqueue(:process_post, args)
|
2015-09-03 23:35:25 -04:00
|
|
|
DiscourseEvent.trigger(:after_trigger_post_process, self)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2013-03-07 11:07:59 -05:00
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def self.public_posts_count_per_day(start_date, end_date, category_id = nil)
|
2015-06-24 09:19:39 -04:00
|
|
|
result = public_posts.where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date)
|
2017-11-02 18:24:43 -04:00
|
|
|
.where(post_type: Post.types[:regular])
|
2015-06-24 09:19:39 -04:00
|
|
|
result = result.where('topics.category_id = ?', category_id) if category_id
|
2018-06-05 03:29:17 -04:00
|
|
|
result
|
|
|
|
.group('date(posts.created_at)')
|
|
|
|
.order('date(posts.created_at)')
|
|
|
|
.count
|
2013-04-03 13:25:52 -04:00
|
|
|
end
|
|
|
|
|
2016-04-21 05:22:41 -04:00
|
|
|
def self.private_messages_count_per_day(start_date, end_date, topic_subtype)
|
2018-06-05 03:29:17 -04: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-07 11:07:59 -05:00
|
|
|
end
|
2013-05-17 12:15:21 -04:00
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def reply_history(max_replies = 100, guardian = nil)
|
2018-06-19 02:13:14 -04: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-26 18:44:42 -04:00
|
|
|
|
|
|
|
# [1,2,3][-10,-1] => nil
|
2017-07-27 21:20:09 -04:00
|
|
|
post_ids = (post_ids[(0 - max_replies)..-1] || post_ids)
|
2014-10-26 18:44:42 -04:00
|
|
|
|
2015-09-24 20:15:58 -04:00
|
|
|
Post.secured(guardian).where(id: post_ids).includes(:user, :topic).order(:id).to_a
|
2013-08-06 17:42:36 -04:00
|
|
|
end
|
|
|
|
|
2017-12-14 18:23:51 -05:00
|
|
|
MAX_REPLY_LEVEL ||= 1000
|
|
|
|
|
2018-04-20 17:05:51 -04:00
|
|
|
def reply_ids(guardian = nil, only_replies_to_single_post: true)
|
2018-06-20 03:48:02 -04:00
|
|
|
builder = DB.build(<<~SQL)
|
2017-12-13 18:43:48 -05:00
|
|
|
WITH RECURSIVE breadcrumb(id, level) AS (
|
|
|
|
SELECT :post_id, 0
|
2017-12-13 16:12:06 -05:00
|
|
|
UNION
|
2017-12-13 18:43:48 -05:00
|
|
|
SELECT reply_id, level + 1
|
2018-04-20 17:05:51 -04: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-13 18:43:48 -05:00
|
|
|
), breadcrumb_with_count AS (
|
2018-04-20 17:05:51 -04: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-13 18:43:48 -05:00
|
|
|
)
|
2018-04-20 17:05:51 -04:00
|
|
|
SELECT id, level
|
|
|
|
FROM breadcrumb_with_count
|
|
|
|
/*where*/
|
|
|
|
ORDER BY id
|
|
|
|
SQL
|
2017-12-13 16:12:06 -05:00
|
|
|
|
2018-04-20 17:05:51 -04: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 03:48:02 -04:00
|
|
|
replies = builder.query_hash(post_id: id, max_reply_level: MAX_REPLY_LEVEL)
|
|
|
|
replies.each { |r| r.symbolize_keys! }
|
2017-12-13 16:12:06 -05:00
|
|
|
|
|
|
|
secured_ids = Post.secured(guardian).where(id: replies.map { |r| r[:id] }).pluck(:id).to_set
|
|
|
|
|
2017-12-13 18:43:48 -05:00
|
|
|
replies.reject { |r| !secured_ids.include?(r[:id]) }
|
2017-12-13 16:12:06 -05:00
|
|
|
end
|
|
|
|
|
2013-12-11 21:41:34 -05:00
|
|
|
def revert_to(number)
|
|
|
|
return if number >= version
|
2014-05-06 09:41:59 -04:00
|
|
|
post_revision = PostRevision.find_by(post_id: id, number: (number + 1))
|
2013-12-11 21:41:34 -05:00
|
|
|
post_revision.modifications.each do |attribute, change|
|
|
|
|
attribute = "version" if attribute == "cached_version"
|
|
|
|
write_attribute(attribute, change[0])
|
|
|
|
end
|
|
|
|
end
|
2013-05-17 12:15:21 -04:00
|
|
|
|
2015-04-24 05:14:10 -04:00
|
|
|
def self.rebake_all_quoted_posts(user_id)
|
|
|
|
return if user_id.blank?
|
|
|
|
|
2018-06-19 02:13:14 -04:00
|
|
|
DB.exec(<<~SQL, user_id)
|
2015-04-24 05:14:10 -04:00
|
|
|
WITH user_quoted_posts AS (
|
|
|
|
SELECT post_id
|
|
|
|
FROM quoted_posts
|
2018-06-19 02:13:14 -04:00
|
|
|
WHERE quoted_post_id IN (SELECT id FROM posts WHERE user_id = ?)
|
2015-04-24 05:14:10 -04: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-26 20:19:49 -05:00
|
|
|
def seen?(user)
|
|
|
|
PostTiming.where(topic_id: topic_id, post_number: post_number, user_id: user.id).exists?
|
|
|
|
end
|
|
|
|
|
2016-12-21 21:13:14 -05:00
|
|
|
def index_search
|
|
|
|
SearchIndexer.index(self)
|
|
|
|
end
|
|
|
|
|
2016-12-21 23:03:40 -05:00
|
|
|
def create_user_action
|
|
|
|
UserActionCreator.log_post(self)
|
|
|
|
end
|
|
|
|
|
2018-01-25 15:38:40 -05:00
|
|
|
def locked?
|
|
|
|
locked_by_id.present?
|
|
|
|
end
|
|
|
|
|
2018-09-05 21:58:01 -04: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-11 21:41:34 -05:00
|
|
|
private
|
2013-05-22 15:45:31 -04:00
|
|
|
|
2013-05-22 15:38:45 -04:00
|
|
|
def parse_quote_into_arguments(quote)
|
|
|
|
return {} unless quote.present?
|
2014-08-17 23:00:02 -04:00
|
|
|
args = HashWithIndifferentAccess.new
|
2013-05-22 15:38:45 -04:00
|
|
|
quote.first.scan(/([a-z]+)\:(\d+)/).each do |arg|
|
2014-08-17 23:00:02 -04:00
|
|
|
args[arg[0]] = arg[1].to_i
|
2013-05-22 15:38:45 -04:00
|
|
|
end
|
|
|
|
args
|
|
|
|
end
|
2013-05-22 15:45:31 -04:00
|
|
|
|
2013-05-23 12:07:45 -04:00
|
|
|
def add_to_quoted_post_numbers(num)
|
|
|
|
return unless num.present?
|
|
|
|
self.quoted_post_numbers ||= []
|
|
|
|
self.quoted_post_numbers << num
|
|
|
|
end
|
2013-05-23 12:08:24 -04:00
|
|
|
|
|
|
|
def create_reply_relationship_with(post)
|
2018-05-16 11:02:43 -04:00
|
|
|
return if post.nil? || self.deleted_at.present?
|
2013-05-23 12:08:24 -04:00
|
|
|
post_reply = post.post_replies.new(reply_id: id)
|
|
|
|
if post_reply.save
|
2015-09-24 20:15:58 -04: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-23 12:08:24 -04:00
|
|
|
end
|
|
|
|
end
|
2013-12-11 21:41:34 -05:00
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2013-05-23 22:48:32 -04:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: posts
|
|
|
|
#
|
|
|
|
# id :integer not null, primary key
|
2013-09-03 17:19:29 -04:00
|
|
|
# user_id :integer
|
2013-05-23 22:48:32 -04:00
|
|
|
# topic_id :integer not null
|
|
|
|
# post_number :integer not null
|
|
|
|
# raw :text not null
|
|
|
|
# cooked :text not null
|
2014-08-27 01:30:17 -04:00
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
2013-05-23 22:48:32 -04: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-16 20:48:58 -04:00
|
|
|
# like_score :integer default(0), not null
|
2013-07-13 21:24:16 -04:00
|
|
|
# deleted_by_id :integer
|
2019-01-11 14:29:56 -05:00
|
|
|
# edit_reason :string
|
2014-02-06 19:07:36 -05:00
|
|
|
# word_count :integer
|
|
|
|
# version :integer default(1), not null
|
|
|
|
# cook_method :integer default(1), not null
|
2014-05-21 19:00:38 -04:00
|
|
|
# wiki :boolean default(FALSE), not null
|
2014-05-29 00:59:14 -04:00
|
|
|
# baked_at :datetime
|
2014-07-03 03:29:44 -04:00
|
|
|
# baked_version :integer
|
|
|
|
# hidden_at :datetime
|
2014-07-14 21:29:44 -04:00
|
|
|
# self_edits :integer default(0), not null
|
2014-07-30 23:14:40 -04:00
|
|
|
# reply_quoted :boolean default(FALSE), not null
|
2014-11-19 22:53:15 -05:00
|
|
|
# via_email :boolean default(FALSE), not null
|
|
|
|
# raw_email :text
|
|
|
|
# public_version :integer default(1), not null
|
2019-01-11 14:29:56 -05:00
|
|
|
# action_code :string
|
2016-10-31 05:41:33 -04:00
|
|
|
# image_url :string
|
2018-02-20 01:28:58 -05:00
|
|
|
# locked_by_id :integer
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2018-07-16 02:18:07 -04: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)
|
|
|
|
# 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-23 22:48:32 -04:00
|
|
|
#
|