# frozen_string_literal: true class TopicCreator attr_reader :user, :guardian, :opts include HasErrors def self.create(user, guardian, opts) self.new(user, guardian, opts).create end def initialize(user, guardian, opts) @user = user @guardian = guardian @opts = opts @added_users = [] end def valid? topic = Topic.new(setup_topic_params) # validate? will clear the error hash # so we fire the validation event after # this allows us to add errors valid = topic.valid? validate_visibility(topic) category = find_category if category.present? && guardian.can_tag?(topic) tags = @opts[:tags].presence || [] # adds topic.errors DiscourseTagging.validate_category_tags(guardian, topic, category, tags) end DiscourseEvent.trigger(:after_validate_topic, topic, self) valid &&= topic.errors.empty? add_errors_from(topic) unless valid valid end def create topic = Topic.new(setup_topic_params) validate_visibility!(topic) setup_tags(topic) if fields = @opts[:custom_fields] topic.custom_fields = fields end DiscourseEvent.trigger(:before_create_topic, topic, self) setup_auto_close_time(topic) process_private_message(topic) save_topic(topic) create_warning(topic) watch_topic(topic) create_shared_draft(topic) UserActionManager.topic_created(topic) topic end private def validate_visibility(topic) if !@opts[:skip_validations] && !topic.visible && !guardian.can_create_unlisted_topic?(topic) topic.errors.add(:base, :unable_to_unlist) end end def validate_visibility!(topic) validate_visibility(topic) rollback_from_errors!(topic) if topic.errors.full_messages.present? end def create_shared_draft(topic) return if @opts[:shared_draft].blank? || @opts[:shared_draft] == "false" category_id = @opts[:category].blank? ? SiteSetting.shared_drafts_category.to_i : @opts[:category] SharedDraft.create(topic_id: topic.id, category_id: category_id) end def create_warning(topic) return unless @opts[:is_warning] # We can only attach warnings to PMs rollback_with!(topic, :warning_requires_pm) unless topic.private_message? # Don't create it if there is more than one user rollback_with!(topic, :too_many_users) if @added_users.size != 1 # Create a warning record UserWarning.create(topic: topic, user: @added_users.first, created_by: @user) end def watch_topic(topic) topic.notifier.watch_topic!(topic.user_id) unless @opts[:auto_track] == false topic.reload.topic_allowed_users.each do |tau| next if tau.user_id == -1 || tau.user_id == topic.user_id topic.notifier.watch!(tau.user_id) end topic.reload.topic_allowed_groups.each do |topic_allowed_group| topic_allowed_group.group.set_message_default_notification_levels!(topic) end end def setup_topic_params @opts[:visible] = true if @opts[:visible].nil? topic_params = { title: @opts[:title], user_id: @user.id, last_post_user_id: @user.id, visible: @opts[:visible], } %i[subtype archetype import_mode advance_draft].each do |key| topic_params[key] = @opts[key] if @opts[key].present? end if topic_params[:import_mode] && @opts[:views].to_i > 0 topic_params[:views] = @opts[:views].to_i end if topic_params[:import_mode] && @opts[:participant_count].to_i > 0 topic_params[:participant_count] = @opts[:participant_count].to_i end # Automatically give it a moderator warning subtype if specified topic_params[:subtype] = TopicSubtype.moderator_warning if @opts[:is_warning] category = find_category unless (@opts[:skip_validations] || @opts[:archetype] == Archetype.private_message) @guardian.ensure_can_create!(Topic, category) end raise Discourse::InvalidParameters.new(:category) if @opts[:category].present? && category.nil? topic_params[:category_id] = category.id if category.present? topic_params[:created_at] = convert_time(@opts[:created_at]) if @opts[:created_at].present? topic_params[:pinned_at] = convert_time(@opts[:pinned_at]) if @opts[:pinned_at].present? topic_params[:pinned_globally] = @opts[:pinned_globally] if @opts[:pinned_globally].present? topic_params[:external_id] = @opts[:external_id] if @opts[:external_id].present? topic_params[:featured_link] = @opts[:featured_link] topic_params end def convert_time(timestamp) if timestamp.is_a?(Time) timestamp else Time.zone.parse(timestamp.to_s) end end def find_category @category ||= begin # PM can't have a category if @opts[:archetype].present? && @opts[:archetype] == Archetype.private_message @opts.delete(:category) end return Category.find(SiteSetting.shared_drafts_category) if @opts[:shared_draft] if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /\A\d+\z/) Category.find_by(id: @opts[:category]) end end end def setup_tags(topic) if @opts[:tags].present? valid_tags = DiscourseTagging.tag_topic_by_names(topic, @guardian, @opts[:tags]) unless valid_tags topic.errors.add(:base, :unable_to_tag) rollback_from_errors!(topic) end end watched_words = WordWatcher.words_for_action(:tag) if watched_words.present? word_watcher = WordWatcher.new("#{@opts[:title]} #{@opts[:raw]}") word_watcher_tags = topic.tags.map(&:name) watched_words.each do |word, opts| if word_watcher.word_matches?(word, case_sensitive: opts[:case_sensitive]) word_watcher_tags += opts[:replacement].split(",") end end DiscourseTagging.tag_topic_by_names(topic, Discourse.system_user.guardian, word_watcher_tags) end end def setup_auto_close_time(topic) return unless @opts[:auto_close_time].present? return unless @guardian.can_moderate?(topic) topic.set_auto_close(@opts[:auto_close_time], by_user: @user) end def process_private_message(topic) return unless @opts[:archetype] == Archetype.private_message topic.subtype = TopicSubtype.user_to_user unless topic.subtype unless @opts[:target_usernames].present? || @opts[:target_emails].present? || @opts[:target_group_names].present? rollback_with!(topic, :no_user_selected) end if @opts[:target_emails].present? && !@guardian.can_send_private_messages_to_email? rollback_with!(topic, :send_to_email_disabled) end add_users(topic, @opts[:target_usernames]) add_emails(topic, @opts[:target_emails]) add_groups(topic, @opts[:target_group_names]) topic.topic_allowed_users.build(user_id: @user.id) if !@added_users.include?(user) end def save_topic(topic) topic.disable_rate_limits! if @opts[:skip_validations] rollback_from_errors!(topic) unless topic.save(validate: !@opts[:skip_validations]) end def add_users(topic, usernames) return unless usernames names = usernames.split(",").flatten.map(&:downcase) len = 0 User .includes(:user_option) .where("username_lower in (?)", names) .find_each do |user| check_can_send_permission!(topic, user) @added_users << user topic.topic_allowed_users.build(user_id: user.id) len += 1 end rollback_with!(topic, :target_user_not_found) unless len == names.length end def add_emails(topic, emails) return unless emails begin emails = emails.split(",").flatten len = 0 emails.each do |email| display_name = email.split("@").first if user = find_or_create_user(email, display_name) if !@added_users.include?(user) @added_users << user topic.topic_allowed_users.build(user_id: user.id) end len += 1 end end ensure rollback_with!(topic, :target_user_not_found) unless len == emails.length end end def add_groups(topic, groups) return unless groups names = groups.split(",").flatten.map(&:downcase) len = 0 Group .where("lower(name) in (?)", names) .each do |group| check_can_send_permission!(topic, group) topic.topic_allowed_groups.build(group_id: group.id) len += 1 group.update_columns(has_messages: true) unless group.has_messages end rollback_with!(topic, :target_group_not_found) unless len == names.length end def check_can_send_permission!(topic, obj) unless @opts[:skip_validations] || @guardian.can_send_private_message?( obj, notify_moderators: topic&.subtype == TopicSubtype.notify_moderators, ) rollback_with!(topic, :cant_send_pm) end end def find_or_create_user(email, display_name) user = User.find_by_email(email) if !user && SiteSetting.enable_staged_users username = UserNameSuggester.sanitize_username(display_name) if display_name.present? user = User.create!( email: email, username: UserNameSuggester.suggest(username.presence || email), name: display_name.presence || User.suggest_name(email), staged: true, ) end user end end