2013-02-05 14:16:51 -05:00
# Responsible for creating posts and topics
#
require_dependency 'rate_limiter'
2013-06-04 20:13:01 +02:00
require_dependency 'topic_creator'
2014-02-17 01:57:37 -05:00
require_dependency 'post_jobs_enqueuer'
2014-07-30 14:04:27 +10:00
require_dependency 'distributed_mutex'
2015-03-26 16:57:50 -04:00
require_dependency 'has_errors'
2013-02-05 14:16:51 -05:00
class PostCreator
2015-03-26 16:57:50 -04:00
include HasErrors
2013-02-05 14:16:51 -05:00
2015-03-26 16:57:50 -04:00
attr_reader :opts
2013-02-05 14:16:51 -05:00
# Acceptable options:
#
# raw - raw text of post
2013-02-25 19:42:20 +03:00
# image_sizes - We can pass a list of the sizes of images in the post as a shortcut.
2013-03-18 13:55:34 -04:00
# invalidate_oneboxes - Whether to force invalidation of oneboxes in this post
2013-05-13 14:06:16 -04:00
# acting_user - The user performing the action might be different than the user
# who is the post "author." For example when copying posts to a new
# topic.
2013-05-20 16:44:06 +10:00
# created_at - Post creation time (optional)
2013-07-22 11:40:39 +10:00
# auto_track - Automatically track this topic if needed (default true)
2014-05-18 11:33:34 +10:00
# custom_fields - Custom fields to be added to the post, Hash (default nil)
2015-01-05 07:52:33 -08:00
# post_type - Whether this is a regular post or moderator post.
# no_bump - Do not cause this post to bump the topic.
# cooking_options - Options for rendering the text
# cook_method - Method of cooking the post.
# :regular - Pass through Markdown parser and strip bad HTML
# :raw_html - Perform no processing
2015-06-05 11:46:21 -04:00
# :raw_email - Imported from an email
2015-01-05 07:52:33 -08:00
# via_email - Mark this post as arriving via email
# raw_email - Full text of arriving email (to store)
2013-02-05 14:16:51 -05:00
#
# When replying to a topic:
# topic_id - topic we're replying to
# reply_to_post_number - post number we're replying to
#
# When creating a topic:
# title - New topic title
# archetype - Topic archetype
2014-09-08 11:11:56 -04:00
# is_warning - Is the topic a warning?
2013-02-05 14:16:51 -05:00
# category - Category to assign to topic
# target_usernames - comma delimited list of usernames for membership (private message)
2013-05-09 17:37:34 +10:00
# target_group_names - comma delimited list of groups for membership (private message)
2013-02-05 14:16:51 -05:00
# meta_data - Topic meta data hash
2015-01-19 15:00:55 +01:00
# created_at - Topic creation time (optional)
# pinned_at - Topic pinned time (optional)
2015-03-13 21:24:11 +01:00
# pinned_globally - Is the topic pinned globally (optional)
2013-06-21 11:36:33 -04:00
#
2013-02-05 14:16:51 -05:00
def initialize ( user , opts )
2013-04-05 15:29:46 +11:00
# TODO: we should reload user in case it is tainted, should take in a user_id as opposed to user
# If we don't do this we introduce a rather risky dependency
2013-02-25 19:42:20 +03:00
@user = user
2013-06-21 11:36:33 -04:00
@opts = opts || { }
2013-05-10 16:58:23 -04:00
@spam = false
2013-02-05 14:16:51 -05:00
end
2013-05-10 16:58:23 -04:00
# True if the post was considered spam
def spam?
@spam
end
2014-07-31 13:15:16 +10:00
def skip_validations?
@opts [ :skip_validations ]
end
2013-02-05 14:16:51 -05:00
def guardian
@guardian || = Guardian . new ( @user )
end
2015-03-26 16:57:50 -04:00
def valid?
2013-06-04 20:13:01 +02:00
@topic = nil
@post = nil
2013-02-05 14:16:51 -05:00
2014-07-31 13:15:16 +10:00
if @user . suspended? && ! skip_validations?
2015-03-26 16:57:50 -04:00
errors [ :base ] << I18n . t ( :user_is_suspended )
return false
2014-07-31 13:15:16 +10:00
end
2015-03-26 16:57:50 -04:00
if new_topic?
topic_creator = TopicCreator . new ( @user , guardian , @opts )
return false unless skip_validations? || validate_child ( topic_creator )
else
@topic = Topic . find_by ( id : @opts [ :topic_id ] )
if ( @topic . blank? || ! guardian . can_create? ( Post , @topic ) )
errors [ :base ] << I18n . t ( :topic_not_found )
return false
end
2013-02-05 14:16:51 -05:00
end
2015-03-26 16:57:50 -04:00
setup_post
return true if skip_validations?
if @post . has_host_spam?
@spam = true
errors [ :base ] << I18n . t ( :spamming_host )
return false
end
DiscourseEvent . trigger :before_create_post , @post
DiscourseEvent . trigger :validate_post , @post
post_validator = Validators :: PostValidator . new ( skip_topic : true )
post_validator . validate ( @post )
valid = @post . errors . blank?
add_errors_from ( @post ) unless valid
valid
end
def create
if valid?
transaction do
create_topic
save_post
extract_links
store_unique_post_key
track_topic
update_topic_stats
update_topic_auto_close
update_user_counts
create_embedded_topic
ensure_in_allowed_users if guardian . is_staff?
@post . advance_draft_sequence
@post . save_reply_relationships
end
end
if @post && errors . blank?
2014-07-30 14:04:27 +10:00
publish
2014-03-18 15:22:39 +11:00
track_latest_on_category
enqueue_jobs
2014-07-23 11:42:24 +10:00
BadgeGranter . queue_badge_grant ( Badge :: Trigger :: PostRevision , post : @post )
2015-03-31 12:58:56 -04:00
trigger_after_events ( @post )
2014-03-18 15:22:39 +11:00
end
2014-02-10 20:29:31 +01:00
2014-07-30 14:24:20 +10:00
if @post || @spam
2014-07-30 14:04:27 +10:00
handle_spam unless @opts [ :import_mode ]
end
2013-06-04 20:13:01 +02:00
@post
2013-02-05 14:16:51 -05:00
end
def self . create ( user , opts )
2013-02-25 19:42:20 +03:00
PostCreator . new ( user , opts ) . create
2013-02-05 14:16:51 -05:00
end
2013-06-09 18:48:44 +02:00
def self . before_create_tasks ( post )
2014-02-10 20:29:31 +01:00
set_reply_user_id ( post )
2013-06-09 18:48:44 +02:00
2013-12-10 13:47:07 -05:00
post . word_count = post . raw . scan ( / \ w+ / ) . size
2013-06-09 18:48:44 +02:00
post . post_number || = Topic . next_post_number ( post . topic_id , post . reply_to_post_number . present? )
2013-06-21 11:36:33 -04:00
cooking_options = post . cooking_options || { }
cooking_options [ :topic_id ] = post . topic_id
post . cooked || = post . cook ( post . raw , cooking_options )
2013-06-09 18:48:44 +02:00
post . sort_order = post . post_number
post . last_version_at || = Time . now
end
2014-02-10 20:29:31 +01:00
def self . set_reply_user_id ( post )
return unless post . reply_to_post_number . present?
2014-05-06 14:41:59 +01:00
post . reply_to_user_id || = Post . select ( :user_id ) . find_by ( topic_id : post . topic_id , post_number : post . reply_to_post_number ) . try ( :user_id )
2014-02-10 20:29:31 +01:00
end
2013-06-09 18:48:44 +02:00
2013-05-02 15:15:17 +10:00
protected
2015-03-31 12:58:56 -04:00
def trigger_after_events ( post )
DiscourseEvent . trigger ( :topic_created , post . topic , @opts , @user ) unless @opts [ :topic_id ]
DiscourseEvent . trigger ( :post_created , post , @opts , @user )
end
2014-07-30 14:04:27 +10:00
def transaction ( & blk )
Post . transaction do
if new_topic?
blk . call
else
# we need to ensure post_number is monotonically increasing with no gaps
# so we serialize creation to avoid needing rollbacks
DistributedMutex . synchronize ( " topic_id_ #{ @opts [ :topic_id ] } " , & blk )
end
end
end
2014-04-03 14:42:26 -04:00
# You can supply an `embed_url` for a post to set up the embedded relationship.
# This is used by the wp-discourse plugin to associate a remote post with a
# discourse post.
def create_embedded_topic
return unless @opts [ :embed_url ] . present?
2015-06-15 12:08:55 -04:00
embed = TopicEmbed . new ( topic_id : @post . topic_id , post_id : @post . id , embed_url : @opts [ :embed_url ] )
rollback_from_errors! ( embed ) unless embed . save
2014-04-03 14:42:26 -04:00
end
2014-02-10 20:29:31 +01:00
def handle_spam
if @spam
GroupMessage . create ( Group [ :moderators ] . name ,
:spam_post_blocked ,
{ user : @user ,
limit_once_per : 24 . hours ,
message_params : { domains : @post . linked_hosts . keys . join ( ', ' ) } } )
2015-03-26 16:57:50 -04:00
elsif @post && errors . blank? && ! skip_validations?
2014-02-10 20:29:31 +01:00
SpamRulesEnforcer . enforce! ( @post )
2013-10-17 17:44:56 +11:00
end
end
2014-02-10 20:29:31 +01:00
def track_latest_on_category
return unless @post && @post . errors . count == 0 && @topic && @topic . category_id
Category . where ( id : @topic . category_id ) . update_all ( latest_post_id : @post . id )
2015-04-23 19:33:29 +02:00
Category . where ( id : @topic . category_id ) . update_all ( latest_topic_id : @topic . id ) if @post . is_first_post?
2014-02-10 20:29:31 +01:00
end
2013-09-06 14:07:23 +10:00
def ensure_in_allowed_users
return unless @topic . private_message?
unless @topic . topic_allowed_users . where ( user_id : @user . id ) . exists?
@topic . topic_allowed_users . create! ( user_id : @user . id )
end
end
2013-06-04 20:13:01 +02:00
private
2015-03-26 16:57:50 -04:00
def create_topic
return if @topic
begin
2013-06-04 20:13:01 +02:00
topic_creator = TopicCreator . new ( @user , guardian , @opts )
2015-03-26 16:57:50 -04:00
@topic = topic_creator . create
rescue ActiveRecord :: Rollback
add_errors_from ( topic_creator )
return
2013-05-02 15:15:17 +10:00
end
2015-03-26 16:57:50 -04:00
@post . topic_id = @topic . id
@post . topic = @topic
2013-05-02 15:15:17 +10:00
end
2013-07-22 15:06:53 +10:00
def update_topic_stats
# Update attributes on the topic - featured users and last posted.
attrs = { last_posted_at : @post . created_at , last_post_user_id : @post . user_id }
attrs [ :bumped_at ] = @post . created_at unless @post . no_bump
2013-12-10 13:47:07 -05:00
attrs [ :word_count ] = ( @topic . word_count || 0 ) + @post . word_count
2014-03-18 13:40:40 -04:00
attrs [ :excerpt ] = @post . excerpt ( 220 , strip_links : true ) if new_topic?
2013-07-22 15:06:53 +10:00
@topic . update_attributes ( attrs )
end
2014-10-10 18:21:44 +02:00
def update_topic_auto_close
if @topic . auto_close_based_on_last_post && @topic . auto_close_hours
@topic . set_auto_close ( @topic . auto_close_hours ) . save
end
end
2013-06-04 20:13:01 +02:00
def setup_post
2014-09-02 01:18:06 +02:00
@opts [ :raw ] = TextCleaner . normalize_whitespaces ( @opts [ :raw ] ) . gsub ( / \ s+ \ z / , " " )
2014-08-12 00:01:58 +02:00
2015-03-26 16:57:50 -04:00
post = Post . new ( raw : @opts [ :raw ] ,
topic_id : @topic . try ( :id ) ,
user : @user ,
reply_to_post_number : @opts [ :reply_to_post_number ] )
2013-06-04 20:13:01 +02:00
2013-06-21 11:36:33 -04:00
# Attributes we pass through to the post instance if present
2014-10-16 11:38:02 +05:30
[ :post_type , :no_bump , :cooking_options , :image_sizes , :acting_user , :invalidate_oneboxes , :cook_method , :via_email , :raw_email ] . each do | a |
2013-06-21 11:36:33 -04:00
post . send ( " #{ a } = " , @opts [ a ] ) if @opts [ a ] . present?
end
2013-06-04 20:13:01 +02:00
post . extract_quoted_post_numbers
post . created_at = Time . zone . parse ( @opts [ :created_at ] . to_s ) if @opts [ :created_at ] . present?
2014-02-10 20:29:31 +01:00
2014-05-18 11:33:34 +10:00
if fields = @opts [ :custom_fields ]
post . custom_fields = fields
end
2013-06-04 20:13:01 +02:00
@post = post
end
def save_post
2015-04-21 13:16:05 -04:00
@post . disable_rate_limits! if skip_validations?
2015-03-26 16:57:50 -04:00
saved = @post . save ( validate : ! skip_validations? )
rollback_from_errors! ( @post ) unless saved
2013-05-02 15:15:17 +10:00
end
2013-06-04 20:13:01 +02:00
def store_unique_post_key
2013-09-09 16:17:31 -04:00
@post . store_unique_post_key
2013-06-04 20:13:01 +02:00
end
2014-07-28 19:17:37 +02:00
def update_user_counts
@user . create_user_stat if @user . user_stat . nil?
2014-02-10 20:29:31 +01:00
2014-07-28 19:17:37 +02:00
if @user . user_stat . first_post_created_at . nil?
@user . user_stat . first_post_created_at = @post . created_at
end
@user . user_stat . post_count += 1
2015-04-23 19:33:29 +02:00
@user . user_stat . topic_count += 1 if @post . is_first_post?
2013-06-04 20:13:01 +02:00
# We don't count replies to your own topics
2014-07-03 14:43:24 -04:00
if ! @opts [ :import_mode ] && @user . id != @topic . user_id
2013-10-04 13:28:49 +10:00
@user . user_stat . update_topic_reply_count
2013-06-04 20:13:01 +02:00
end
2014-07-28 19:17:37 +02:00
@user . user_stat . save!
2013-06-04 20:13:01 +02:00
@user . last_posted_at = @post . created_at
@user . save!
end
def publish
2014-07-03 14:43:24 -04:00
return if @opts [ :import_mode ]
2014-02-10 20:29:31 +01:00
return unless @post . post_number > 1
2014-08-28 20:34:32 -07:00
@post . publish_change_to_clients! :created
2013-06-04 20:13:01 +02:00
end
def extract_links
TopicLink . extract_from ( @post )
2014-07-15 17:47:24 +10:00
QuotedPost . extract_from ( @post )
2013-06-04 20:13:01 +02:00
end
def track_topic
2014-02-10 20:29:31 +01:00
return if @opts [ :auto_track ] == false
2014-08-28 22:07:40 -07:00
TopicUser . change ( @post . user_id ,
@topic . id ,
2014-02-10 20:29:31 +01:00
posted : true ,
last_read_post_number : @post . post_number ,
2014-10-31 09:40:35 +11:00
highest_seen_post_number : @post . post_number )
2014-06-04 11:41:42 +10:00
# assume it took us 5 seconds of reading time to make a post
PostTiming . record_timing ( topic_id : @post . topic_id ,
user_id : @post . user_id ,
post_number : @post . post_number ,
msecs : 5000 )
TopicUser . auto_track ( @user . id , @topic . id , TopicUser . notification_reasons [ :created_post ] )
2013-06-04 20:13:01 +02:00
end
def enqueue_jobs
2014-02-10 20:29:31 +01:00
return unless @post && ! @post . errors . present?
2014-07-03 14:43:24 -04:00
PostJobsEnqueuer . new ( @post , @topic , new_topic? , { import_mode : @opts [ :import_mode ] } ) . enqueue_jobs
2014-02-17 01:57:37 -05:00
end
2014-02-10 20:29:31 +01:00
2014-02-17 01:57:37 -05:00
def new_topic?
@opts [ :topic_id ] . blank?
2013-06-04 20:13:01 +02:00
end
2014-02-17 01:57:37 -05:00
2013-02-05 14:16:51 -05:00
end