0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/app/models/post_mover.rb
Régis Hanol 3e012af58e
FIX: Scope duplicate topic title check to what the user can see (#41871)
Previously, the duplicate topic title check compared new titles against
every topic on the site regardless of visibility: users were blocked by
titles in categories they couldn't see or unlisted topics they couldn't
find, the bare "Title has already been used" error gave no way to locate
the conflict (and doubled as an existence oracle for hidden titles), and
the behavior was controlled by two entangled boolean settings.

This change scopes the check to the destination category plus whatever
the acting user can actually see, links the conflicting topic in the
error — safe by construction, since being blocked now implies being able
to see it:

> This title has already been used by [another topic]().

It also consolidates the two booleans into a single
`duplicate_topic_titles` enum (`disallowed` /
`allowed_across_categories` / `allowed`), with existing values migrated
and the old names kept as hidden deprecated aliases that admin search
still resolves.

Reported in
https://meta.discourse.org/t/title-has-already-been-used-in-a-secure-category/123047

Note for self-hosters: env-provided settings can't be migrated —
`DISCOURSE_ALLOW_DUPLICATE_TOPIC_TITLES=true` configs need to switch to
`DISCOURSE_DUPLICATE_TOPIC_TITLES=allowed`.
2026-07-24 15:35:35 +02:00

849 lines
29 KiB
Ruby
Vendored

# frozen_string_literal: true
class PostMover
attr_reader :original_topic, :destination_topic, :user, :post_ids
def self.move_types
@move_types ||= Enum.new(:new_topic, :existing_topic)
end
# options:
# freeze_original: :boolean - if true, the original topic will be frozen but not deleted and posts will be "copied" to topic
def initialize(original_topic, user, post_ids, move_to_pm: false, options: {})
@original_topic = original_topic
@original_topic_title = original_topic.title
@user = user
@post_ids = post_ids
# For now we store a copy of post_ids. If `freeze_original` is present, we will have new post_ids.
# When we create the new posts, we will pluck out post_ids out of this and replace with updated ids.
@post_ids_after_move = post_ids
@move_to_pm = move_to_pm
@options = options
end
def to_topic(id, participants: nil, chronological_order: false)
@move_type = PostMover.move_types[:existing_topic]
@creating_new_topic = false
@chronological_order = chronological_order
topic = Topic.find_by_id(id)
if topic.archetype != @original_topic.archetype &&
[@original_topic.archetype, topic.archetype].include?(Archetype.private_message)
raise Discourse::InvalidParameters
end
Topic.transaction { move_posts_to topic }
add_allowed_users(participants) if participants.present? && @move_to_pm
enqueue_jobs(topic)
topic
end
def to_new_topic(title, category_id = nil, tag_ids: nil, tags: nil)
@move_type = PostMover.move_types[:new_topic]
@creating_new_topic = true
post = Post.find_by(id: post_ids.first)
raise Discourse::InvalidParameters unless post
archetype = @move_to_pm ? Archetype.private_message : Archetype.default
topic =
Topic.transaction do
new_topic =
Topic.create!(
user: post.user,
acting_user: user,
title: title,
category_id: category_id,
created_at: post.created_at,
archetype: archetype,
)
if tag_ids.present?
DiscourseTagging.tag_topic_by_ids(new_topic, Guardian.new(user), tag_ids)
elsif tags.present?
DiscourseTagging.tag_topic_by_names(new_topic, Guardian.new(user), tags)
end
move_posts_to new_topic
watch_new_topic
update_topic_excerpt new_topic
new_topic
end
enqueue_jobs(topic)
topic
end
private
def update_topic_excerpt(topic)
topic.update_excerpt(topic.first_post.excerpt_for_topic)
end
def move_posts_to(topic)
Guardian.new(user).ensure_can_see! topic
@destination_topic = topic
# Serialize concurrent moves/merges on the same source topic. Without this
# lock, two transactions can each read `ordered_posts.last.post_number` at
# the same value, both compute the same `@first_post_number_moved`, and
# then collide on the `(topic_id, post_number)` unique index when each
# tries to insert the "split_topic" moderator post. The lock is released
# on commit/rollback of the surrounding `Topic.transaction`.
@original_topic.lock!
ensure_acting_user_is_allowed_in_destination
@full_move = (posts_preventing_close.pluck(:id) - posts.map(&:id)).empty?
@first_post_number_moved =
posts.first.is_first_post? ? posts[1]&.post_number : posts.first.post_number
if @options[:freeze_original]
# in this case we need to add the moderator post after the last copied post
if @full_move
# Use max_post_number (which includes soft-deleted posts) so the number
# we force via `add_moderator_post` matches what `Topic.next_post_number`
# will atomically assign via raw SQL. Otherwise a deleted post at the
# tail leaves a tombstone in `index_posts_on_topic_id_and_post_number`
# that collides with our `update!(post_number: ...)`.
@first_post_number_moved = @original_topic.max_post_number + 1
else
# Same reason: shift all posts above `posts.last.post_number`, including
# soft-deleted ones, so the slot at `posts.last.post_number + 1` is
# guaranteed free in the unique index.
from_posts =
@original_topic
.posts
.with_deleted
.where("post_number > ?", posts.last.post_number)
.order(:post_number)
shift_post_numbers(from_posts)
@first_post_number_moved = posts.last.post_number + 1
end
end
create_temp_table
move_each_post
handle_moved_references
create_moderator_post_in_original_topic
update_statistics
update_user_actions
update_last_post_stats
update_upload_security_status
update_bookmarks
update_reviewables
close_topic_and_schedule_deletion if @full_move
destination_topic.reload
DiscourseEvent.trigger(
:posts_moved,
destination_topic_id: destination_topic.id,
original_topic_id: original_topic.id,
)
destination_topic
end
def ensure_acting_user_is_allowed_in_destination
return if !@move_to_pm
return if destination_topic.archetype != Archetype.private_message
return if user.id.blank? || user.bot?
return if destination_topic.topic_allowed_users.exists?(user_id: user.id)
destination_topic.topic_allowed_users.create!(user_id: user.id)
destination_topic.notifier.watch!(user.id)
end
def create_temp_table
DB.exec("DROP TABLE IF EXISTS temp_moved_posts") if Rails.env.test?
DB.exec <<~SQL
CREATE TEMPORARY TABLE temp_moved_posts (
old_topic_id INTEGER,
old_post_id INTEGER,
old_post_number INTEGER,
new_topic_id INTEGER,
new_topic_title VARCHAR,
new_post_id INTEGER,
new_post_number INTEGER
) ON COMMIT DROP;
CREATE INDEX moved_posts_old_post_number ON temp_moved_posts(old_post_number);
CREATE INDEX moved_posts_old_post_id ON temp_moved_posts(old_post_id);
SQL
end
def handle_moved_references
move_incoming_emails
move_notifications
update_reply_counts
update_quotes
move_first_post_replies
delete_post_replies
copy_shifted_post_timings_to_temp
delete_invalid_post_timings
copy_shifted_post_timings_from_temp
move_post_timings
copy_first_post_timings
copy_topic_users
end
def move_each_post
if @chronological_order
move_each_post_chronological
else
move_each_post_sequential
end
end
def move_each_post_sequential
max_post_number = destination_topic.max_post_number + 1
@post_creator = nil
@move_map = {}
@reply_count = {}
posts.each_with_index do |post, offset|
@move_map[post.post_number] = offset + max_post_number
if post.reply_to_post_number.present?
@reply_count[post.reply_to_post_number] = (@reply_count[post.reply_to_post_number] || 0) + 1
end
end
posts.each do |post|
metadata = movement_metadata(post, new_post_number: @move_map[post.post_number])
new_post = post.is_first_post? ? create_first_post(post) : move(post)
store_movement(metadata, new_post)
if @move_to_pm && !destination_topic.topic_allowed_users.exists?(user_id: post.user_id)
destination_topic.topic_allowed_users.create!(user_id: post.user_id)
end
end
end
def move_each_post_chronological
destination_posts = destination_topic.ordered_posts.with_deleted
# drops posts from destination_topic until it finds one that was created after posts.first
min_created_at = posts.first.created_at
moved_posts = destination_posts.drop_while { |post| post.created_at <= min_created_at }
# if no post in destination_topic was created after posts.first it's equal to sequential
if moved_posts.empty?
initial_post_number = destination_topic.max_post_number + 1
else
initial_post_number = moved_posts.first.post_number
end
last_index = 0
posts.each do |post|
while last_index < moved_posts.length && moved_posts[last_index].created_at <= post.created_at
last_index += 1
end
moved_posts.insert(last_index, post)
end
@post_creator = nil
@move_map = {}
@shift_map = {}
@reply_count = {}
next_post_number = initial_post_number
moved_posts.each do |post|
if post.topic_id == destination_topic.id
# avoid shifting to a lower post number
next_post_number = post.post_number if post.post_number > next_post_number
@shift_map[post.post_number] = next_post_number
else
@move_map[post.post_number] = next_post_number
if post.reply_to_post_number.present?
@reply_count[post.reply_to_post_number] = (@reply_count[post.reply_to_post_number] || 0) +
1
end
end
next_post_number += 1
end
moved_posts.reverse_each do |post|
if post.topic_id == destination_topic.id
metadata = movement_metadata(post, new_post_number: @shift_map[post.post_number])
new_post = move_same_topic(post)
else
metadata = movement_metadata(post, new_post_number: @move_map[post.post_number])
new_post = post.is_first_post? ? create_first_post(post) : move(post)
if @move_to_pm && !destination_topic.topic_allowed_users.exists?(user_id: post.user_id)
destination_topic.topic_allowed_users.create!(user_id: post.user_id)
end
end
store_movement(metadata, new_post)
end
# change topic owner if there's a new first post
destination_topic.update_column(:user_id, posts.first.user_id) if initial_post_number == 1
end
def create_first_post(post)
@post_creator =
PostCreator.new(
post.user,
raw: post.raw,
topic_id: destination_topic.id,
acting_user: user,
cook_method: post.cook_method,
via_email: post.via_email,
raw_email: post.raw_email,
skip_validations: true,
created_at: post.created_at,
guardian: Guardian.new(user),
skip_jobs: true,
no_bump: true,
)
new_post = @post_creator.create!
move_email_logs(post, new_post)
PostAction.copy(post, new_post)
PostRevision.copy(post, new_post)
attrs_to_update = {
reply_count: @reply_count[1] || 0,
version: post.version,
public_version: post.public_version,
}
if new_post.post_number != @move_map[post.post_number]
attrs_to_update[:post_number] = @move_map[post.post_number]
attrs_to_update[:sort_order] = @move_map[post.post_number]
end
new_post.update_columns(attrs_to_update)
new_post.custom_fields = post.custom_fields
new_post.save_custom_fields
@post_ids_after_move =
@post_ids_after_move.map { |post_id| post_id == post.id ? new_post.id : post_id }
DiscourseEvent.trigger(:first_post_moved, new_post, post)
DiscourseEvent.trigger(:post_moved, new_post, original_topic.id, post)
# we don't want to keep the old topic's OP bookmarked when we are
# moving it into a new topic
Bookmark.where(bookmarkable: post).update_all(bookmarkable_id: new_post.id)
new_post
end
def move(post)
update = {
reply_count: @reply_count[post.post_number] || 0,
post_number: @move_map[post.post_number],
reply_to_post_number: @move_map[post.reply_to_post_number],
topic_id: destination_topic.id,
sort_order: @move_map[post.post_number],
baked_version: nil,
}
update[:reply_to_user_id] = nil unless @move_map[post.reply_to_post_number]
moved_post =
if @options[:freeze_original]
post.dup
else
post
end
moved_post.attributes = update
moved_post.disable_rate_limits! if @options[:freeze_original]
moved_post.save(validate: false)
if moved_post.id != post.id
@post_ids_after_move =
@post_ids_after_move.map { |post_id| post_id == post.id ? moved_post.id : post_id }
end
DiscourseEvent.trigger(:post_moved, moved_post, original_topic.id, post)
# Move any links from the post to the new topic
moved_post.topic_links.update_all(topic_id: destination_topic.id)
moved_post
end
def move_same_topic(post)
update = {
post_number: @shift_map[post.post_number],
sort_order: @shift_map[post.post_number],
baked_version: nil,
}
if @shift_map[post.reply_to_post_number]
update[:reply_to_post_number] = @shift_map[post.reply_to_post_number]
end
post.attributes = update
post.save(validate: false)
post
end
def movement_metadata(post, new_post_number: nil)
{
old_topic_id: post.topic_id,
old_post_id: post.id,
old_post_number: post.post_number,
post_user_id: post.user_id,
new_topic_id: destination_topic.id,
new_post_number: new_post_number,
new_topic_title: destination_topic.title,
}
end
def store_movement(metadata, new_post)
metadata[:new_post_id] = new_post.id
metadata[:now] = Time.zone.now
metadata[:created_new_topic] = @creating_new_topic
metadata[:old_topic_title] = @original_topic_title
metadata[:user_id] = @user.id
metadata[:full_move] = @full_move
DB.exec(<<~SQL, metadata)
INSERT INTO temp_moved_posts(old_topic_id, old_post_id, old_post_number, new_topic_id, new_topic_title, new_post_id, new_post_number)
VALUES (:old_topic_id, :old_post_id, :old_post_number, :new_topic_id, :new_topic_title, :new_post_id, :new_post_number)
SQL
DB.exec(<<~SQL, metadata)
INSERT INTO moved_posts(old_topic_id, old_topic_title, old_post_id, old_post_number, post_user_id, user_id, full_move, new_topic_id, new_topic_title, new_post_id, new_post_number, created_new_topic, created_at, updated_at)
VALUES (:old_topic_id, :old_topic_title, :old_post_id, :old_post_number, :post_user_id, :user_id, :full_move, :new_topic_id, :new_topic_title, :new_post_id, :new_post_number, :created_new_topic, :now, :now)
SQL
end
def shift_post_numbers(from_posts)
from_posts.reverse_each { |post| post.update_columns(post_number: post.post_number + 1) }
end
def move_incoming_emails
DB.exec <<~SQL
UPDATE incoming_emails ie
SET topic_id = mp.new_topic_id,
post_id = mp.new_post_id
FROM temp_moved_posts mp
WHERE ie.topic_id = mp.old_topic_id AND ie.post_id = mp.old_post_id
AND mp.old_topic_id <> mp.new_topic_id
SQL
end
def move_email_logs(old_post, new_post)
EmailLog.where(post_id: old_post.id).update_all(post_id: new_post.id)
end
def move_notifications
DB.exec <<~SQL
UPDATE notifications n
SET topic_id = mp.new_topic_id,
post_number = mp.new_post_number,
data = (data :: JSONB ||
jsonb_strip_nulls(
jsonb_build_object(
'topic_title', CASE WHEN data :: JSONB ->> 'topic_title' IS NULL
THEN NULL
ELSE mp.new_topic_title END
)
)) :: JSON
FROM temp_moved_posts mp
WHERE n.topic_id = mp.old_topic_id AND n.post_number = mp.old_post_number
AND n.notification_type <> #{Notification.types[:watching_first_post]}
SQL
end
def update_reply_counts
DB.exec <<~SQL
UPDATE posts p
SET reply_count = GREATEST(0, reply_count - x.moved_reply_count)
FROM (
SELECT r.post_id, mp.new_topic_id, COUNT(1) AS moved_reply_count
FROM temp_moved_posts mp
JOIN post_replies r ON (mp.old_post_id = r.reply_post_id)
GROUP BY r.post_id, mp.new_topic_id
) x
WHERE x.post_id = p.id AND x.new_topic_id <> p.topic_id
SQL
end
def update_quotes
DB.exec <<~SQL
UPDATE posts p
SET raw = REPLACE(p.raw,
', post:' || mp.old_post_number || ', topic:' || mp.old_topic_id,
', post:' || mp.new_post_number || ', topic:' || mp.new_topic_id),
baked_version = NULL
FROM temp_moved_posts mp, quoted_posts qp
WHERE p.id = qp.post_id AND mp.old_post_id = qp.quoted_post_id
SQL
end
def move_first_post_replies
DB.exec <<~SQL
UPDATE post_replies pr
SET post_id = mp.new_post_id
FROM temp_moved_posts mp
WHERE mp.old_post_id <> mp.new_post_id AND pr.post_id = mp.old_post_id AND
EXISTS (SELECT 1 FROM temp_moved_posts mr WHERE mr.new_post_id = pr.reply_post_id)
SQL
end
def delete_post_replies
DB.exec <<~SQL
DELETE FROM post_replies pr USING temp_moved_posts mp
WHERE (SELECT topic_id FROM posts WHERE id = pr.post_id) <>
(SELECT topic_id FROM posts WHERE id = pr.reply_post_id)
AND (pr.reply_post_id = mp.old_post_id OR pr.post_id = mp.old_post_id)
SQL
end
def copy_shifted_post_timings_to_temp
DB.exec("DROP TABLE IF EXISTS temp_post_timings") if Rails.env.test?
# copy post_timings for shifted posts to a temp table using the new_post_number
# they'll be copied back after delete_invalid_post_timings makes room for them
DB.exec <<~SQL
CREATE TEMPORARY TABLE temp_post_timings ON COMMIT DROP
AS (
SELECT pt.topic_id, mp.new_post_number as post_number, pt.user_id, pt.msecs
FROM post_timings pt
JOIN temp_moved_posts mp
ON mp.old_topic_id = pt.topic_id
AND mp.old_post_number = pt.post_number
AND mp.old_topic_id = mp.new_topic_id
)
SQL
end
def copy_shifted_post_timings_from_temp
DB.exec <<~SQL
INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
SELECT DISTINCT ON (topic_id, post_number, user_id) topic_id, user_id, post_number, msecs
FROM temp_post_timings
ORDER BY topic_id, post_number, user_id, msecs DESC
ON CONFLICT (topic_id, post_number, user_id) DO UPDATE
SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
SQL
end
def copy_first_post_timings
DB.exec(<<~SQL, post_ids: @post_ids_after_move)
INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
SELECT mp.new_topic_id, pt.user_id, mp.new_post_number, pt.msecs
FROM post_timings pt
JOIN temp_moved_posts mp ON (pt.topic_id = mp.old_topic_id AND pt.post_number = mp.old_post_number)
WHERE mp.old_post_id <> mp.new_post_id
AND mp.new_post_id IN (:post_ids)
ON CONFLICT (topic_id, post_number, user_id) DO UPDATE
SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
SQL
end
def delete_invalid_post_timings
DB.exec <<~SQL
DELETE
FROM post_timings pt
USING temp_moved_posts mp
WHERE pt.topic_id = mp.new_topic_id
AND pt.post_number = mp.new_post_number
SQL
end
def move_post_timings
DB.exec(<<~SQL, post_ids: @post_ids_after_move)
UPDATE post_timings pt
SET topic_id = mp.new_topic_id,
post_number = mp.new_post_number
FROM temp_moved_posts mp
WHERE pt.topic_id = mp.old_topic_id
AND pt.post_number = mp.old_post_number
AND mp.old_post_id = mp.new_post_id
AND mp.old_topic_id <> mp.new_topic_id
AND mp.new_post_id IN (:post_ids)
SQL
end
def copy_topic_users
params = {
old_topic_id: original_topic.id,
new_topic_id: destination_topic.id,
old_highest_post_number: destination_topic.highest_post_number,
old_highest_staff_post_number: destination_topic.highest_staff_post_number,
}
DB.exec(<<~SQL, params)
INSERT INTO topic_users(user_id, topic_id, posted, last_read_post_number,
last_emailed_post_number, first_visited_at, last_visited_at, notification_level,
notifications_changed_at, notifications_reason_id)
SELECT tu.user_id,
:new_topic_id AS topic_id,
EXISTS(
SELECT 1
FROM posts p
WHERE p.topic_id = :new_topic_id
AND p.user_id = tu.user_id
LIMIT 1
) AS posted,
(
SELECT MAX(lr.new_post_number)
FROM temp_moved_posts lr
WHERE lr.old_topic_id = tu.topic_id
AND lr.old_post_number <= tu.last_read_post_number
AND lr.old_topic_id <> lr.new_topic_id
) AS last_read_post_number,
(
SELECT MAX(le.new_post_number)
FROM temp_moved_posts le
WHERE le.old_topic_id = tu.topic_id
AND le.old_post_number <= tu.last_emailed_post_number
AND le.old_topic_id <> le.new_topic_id
) AS last_emailed_post_number,
GREATEST(tu.first_visited_at, t.created_at) AS first_visited_at,
GREATEST(tu.last_visited_at, t.created_at) AS last_visited_at,
tu.notification_level,
tu.notifications_changed_at,
tu.notifications_reason_id
FROM topic_users tu
JOIN topics t ON (t.id = :new_topic_id)
WHERE tu.topic_id = :old_topic_id
AND GREATEST(
tu.last_read_post_number,
tu.last_emailed_post_number
) >= (SELECT MIN(mp.old_post_number) FROM temp_moved_posts mp WHERE mp.old_topic_id <> mp.new_topic_id)
ON CONFLICT (topic_id, user_id) DO UPDATE
SET posted = excluded.posted,
last_read_post_number = CASE
WHEN topic_users.last_read_post_number = :old_highest_staff_post_number OR (
:old_highest_post_number < :old_highest_staff_post_number
AND topic_users.last_read_post_number = :old_highest_post_number
AND NOT EXISTS(SELECT 1
FROM users u
WHERE u.id = topic_users.user_id
AND (admin OR moderator))
) THEN
GREATEST(topic_users.last_read_post_number,
excluded.last_read_post_number)
ELSE topic_users.last_read_post_number END,
last_emailed_post_number = CASE
WHEN topic_users.last_emailed_post_number = :old_highest_staff_post_number OR (
:old_highest_post_number < :old_highest_staff_post_number
AND topic_users.last_emailed_post_number = :old_highest_post_number
AND NOT EXISTS(SELECT 1
FROM users u
WHERE u.id = topic_users.user_id
AND (admin OR moderator))
) THEN
GREATEST(topic_users.last_emailed_post_number,
excluded.last_emailed_post_number)
ELSE topic_users.last_emailed_post_number END,
first_visited_at = LEAST(topic_users.first_visited_at, excluded.first_visited_at),
last_visited_at = GREATEST(topic_users.last_visited_at, excluded.last_visited_at)
SQL
end
def update_statistics
destination_topic.update_statistics!
original_topic.update_statistics!
TopicUser.update_post_action_cache(
topic_id: [original_topic.id, destination_topic.id],
post_id: @post_ids,
)
end
def update_user_actions
UserAction.synchronize_target_topic_ids(posts.map(&:id))
end
def create_moderator_post_in_original_topic
move_type_str = PostMover.move_types[@move_type].to_s
move_type_str.sub!("topic", "message") if @move_to_pm
topic_link =
if posts.first.is_first_post?
"[#{destination_topic.title}](#{destination_topic.relative_url})"
else
"[#{destination_topic.title}](#{posts.first.relative_url})"
end
post_type = @move_to_pm ? Post.types[:whisper] : Post.types[:small_action]
continue =
DiscoursePluginRegistry.apply_modifier(
:post_mover_create_moderator_post,
true,
user: user,
post_type: post_type,
post_ids: @post_ids_after_move,
full_move: @full_move,
original_topic: original_topic,
destination_topic: destination_topic,
first_post_number_moved: @first_post_number_moved,
)
return if !continue
message =
I18n.with_locale(SiteSetting.default_locale) do
I18n.t(
"move_posts.#{move_type_str}_moderator_post",
count: posts.length,
topic_link: topic_link,
)
end
original_topic.add_moderator_post(
user,
message,
post_type: post_type,
action_code: "split_topic",
post_number: @first_post_number_moved,
)
end
def posts
@posts ||=
Post
.where(topic: @original_topic, id: post_ids)
.where.not(post_type: Post.types[:small_action])
.where.not(raw: "")
.order(:created_at)
.tap { |posts| raise Discourse::InvalidParameters.new(:post_ids) if posts.empty? }
end
def posts_preventing_close
@original_topic
.posts
.where(post_type: [Post.types[:regular], Post.types[:whisper]])
.where.not(raw: "")
.where("action_code IS DISTINCT FROM 'split_topic'")
end
def update_last_post_stats
post = destination_topic.ordered_posts.where.not(post_type: Post.types[:whisper]).last
if post && @post_ids_after_move.include?(post.id)
attrs = {}
attrs[:last_posted_at] = post.created_at
attrs[:last_post_user_id] = post.user_id
attrs[:bumped_at] = Time.now
attrs[:updated_at] = Time.now
destination_topic.update_columns(attrs)
end
end
def update_upload_security_status
DB.after_commit { Jobs.enqueue(:update_topic_upload_security, topic_id: @destination_topic.id) }
end
def update_bookmarks
DB.after_commit do
Jobs.enqueue(:sync_topic_user_bookmarked, topic_id: @original_topic.id)
Jobs.enqueue(:sync_topic_user_bookmarked, topic_id: @destination_topic.id)
end
end
def update_reviewables
Reviewable.where(target_type: "Post", target_id: @post_ids).update_all(
topic_id: @destination_topic.id,
category_id: @destination_topic.category_id,
)
end
def watch_new_topic
if @destination_topic.archetype == Archetype.private_message
if @original_topic.archetype == Archetype.private_message
notification_levels =
TopicUser
.where(topic_id: @original_topic.id, user_id: posts.pluck(:user_id))
.pluck(:user_id, :notification_level)
.to_h
else
notification_levels =
posts
.pluck(:user_id)
.uniq
.map { |user_id| [user_id, TopicUser.notification_levels[:watching]] }
.to_h
end
else
notification_levels = [[@destination_topic.user_id, TopicUser.notification_levels[:watching]]]
end
notification_levels.each do |user_id, notification_level|
TopicUser.change(
user_id,
@destination_topic.id,
notification_level: notification_level,
notifications_reason_id:
TopicUser.notification_reasons[
destination_topic.user_id == user_id ? :created_topic : :created_post
],
)
end
end
def add_allowed_users(usernames)
return if usernames.blank?
names = usernames.split(",").flatten
User
.where(username: names)
.find_each do |user|
unless destination_topic.topic_allowed_users.where(user_id: user.id).exists?
destination_topic.topic_allowed_users.build(user_id: user.id)
end
end
destination_topic.save!
end
def enqueue_jobs(topic)
enqueue_post_creator_jobs =
DiscoursePluginRegistry.apply_modifier(
:post_mover_enqueue_post_creator_jobs,
@post_creator.present?,
)
@post_creator.enqueue_jobs if enqueue_post_creator_jobs
Jobs.enqueue(:notify_moved_posts, post_ids: @post_ids_after_move, moved_by_id: user.id)
Jobs.enqueue(:delete_inaccessible_notifications, topic_id: topic.id)
end
def close_topic_and_schedule_deletion
@original_topic.update_status("closed", true, @user)
return if @options[:freeze_original] # we only close the topic when freezing it
days_to_deleting = SiteSetting.delete_merged_stub_topics_after_days
if days_to_deleting == 0
is_allowed_to_delete_after_merge =
DiscoursePluginRegistry.apply_modifier(
:is_allowed_to_delete_after_merge,
Guardian.new(@user).can_delete?(@original_topic),
@original_topic,
@user,
)
if is_allowed_to_delete_after_merge
first_post = @original_topic.ordered_posts.first
PostDestroyer.new(
@user,
first_post,
context: I18n.t("topic_statuses.auto_deleted_by_merge"),
).destroy
@original_topic.trash!(Discourse.system_user)
end
elsif days_to_deleting > 0
@original_topic.set_or_create_timer(
TopicTimer.types[:delete],
days_to_deleting * 24,
by_user: @user,
)
end
end
end