mirror of
https://github.com/discourse/discourse.git
synced 2025-01-02 08:34:04 +08:00
41584ab40c
Currently in services, we don’t make a distinction between input parameters, options and dependencies. This can lead to user input modifying the service behavior, whereas it was not the developer intention. This patch addresses the issue by changing how data is provided to services: - `params` is now used to hold all data coming from outside (typically user input from a controller) and a contract will take its values from `params`. - `options` is a new key to provide options to a service. This typically allows changing a service behavior at runtime. It is, of course, totally optional. - `dependencies` is actually anything else provided to the service (like `guardian`) and available directly from the context object. The `service_params` helper in controllers has been updated to reflect those changes, so most of the existing services didn’t need specific changes. The options block has the same DSL as contracts, as it’s also based on `ActiveModel`. There aren’t any validations, though. Here’s an example: ```ruby options do attribute :allow_changing_hidden, :boolean, default: false end ``` And here’s an example of how to call a service with the new keys: ```ruby MyService.call(params: { key1: value1, … }, options: { my_option: true }, guardian:, …) ```
296 lines
9.9 KiB
Ruby
296 lines
9.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
##
|
|
# Used to move chat messages from a chat channel to some other
|
|
# location.
|
|
#
|
|
# Channel -> Channel:
|
|
# -------------------
|
|
#
|
|
# Messages are sometimes misplaced and must be moved to another channel. For
|
|
# now we only support moving messages between public channels, handling the
|
|
# permissions and membership around moving things in and out of DMs is a little
|
|
# much for V1.
|
|
#
|
|
# The original messages will be deleted, and then similar to PostMover in core,
|
|
# all of the references associated to a chat message (e.g. reactions, bookmarks,
|
|
# notifications, revisions, mentions, uploads) will be updated to the new
|
|
# message IDs via a moved_chat_messages temporary table.
|
|
#
|
|
# Reply chains are a little complex. No reply chains are preserved when moving
|
|
# messages into a new channel. Remaining messages that referenced moved ones
|
|
# have their in_reply_to_id cleared so the data makes sense.
|
|
#
|
|
# The service supports moving threads. If any of the selected messages is the
|
|
# original message of a thread, the entire thread with all its replies will be
|
|
# moved to the destination channel. Moving individual messages out of a thread
|
|
# is still disabled.
|
|
|
|
module Chat
|
|
class MessageMover
|
|
class NoMessagesFound < StandardError
|
|
end
|
|
|
|
class InvalidChannel < StandardError
|
|
end
|
|
|
|
def initialize(acting_user:, source_channel:, message_ids:)
|
|
@source_channel = source_channel
|
|
@acting_user = acting_user
|
|
@source_message_ids = message_ids
|
|
@source_messages = find_messages(@source_message_ids, source_channel)
|
|
@ordered_source_message_ids = @source_messages.map(&:id)
|
|
@source_thread_ids = @source_messages.pluck(:thread_id).uniq.compact
|
|
end
|
|
|
|
def move_to_channel(destination_channel)
|
|
if !@source_channel.public_channel? || !destination_channel.public_channel?
|
|
raise InvalidChannel.new(I18n.t("chat.errors.message_move_invalid_channel"))
|
|
end
|
|
|
|
if @ordered_source_message_ids.empty?
|
|
raise NoMessagesFound.new(I18n.t("chat.errors.message_move_no_messages_found"))
|
|
end
|
|
|
|
moved_messages = nil
|
|
|
|
Chat::Message.transaction do
|
|
create_temp_table_for_messages
|
|
create_temp_table_for_threads
|
|
moved_thread_ids = create_destination_threads_in_channel(destination_channel)
|
|
moved_messages =
|
|
find_messages(
|
|
create_destination_messages_in_channel(destination_channel, moved_thread_ids),
|
|
destination_channel,
|
|
)
|
|
bulk_insert_movement_metadata_for_messages
|
|
update_message_references
|
|
delete_source_messages
|
|
update_reply_references
|
|
update_tracking_state
|
|
update_thread_references(moved_thread_ids)
|
|
delete_source_threads
|
|
end
|
|
|
|
add_moved_placeholder(destination_channel, moved_messages.first)
|
|
moved_messages
|
|
end
|
|
|
|
private
|
|
|
|
def find_messages(message_ids, channel)
|
|
Chat::Message
|
|
.includes(thread: %i[original_message original_message_user])
|
|
.where(chat_channel_id: channel.id)
|
|
.where(
|
|
"id IN (:message_ids) OR thread_id IN (SELECT thread_id FROM chat_messages WHERE id IN (:message_ids))",
|
|
message_ids: message_ids,
|
|
)
|
|
.order("created_at ASC, id ASC")
|
|
end
|
|
|
|
def create_temp_table_for_messages
|
|
DB.exec("DROP TABLE IF EXISTS moved_chat_messages") if Rails.env.test?
|
|
|
|
DB.exec <<~SQL
|
|
CREATE TEMPORARY TABLE moved_chat_messages (
|
|
old_chat_message_id BIGINT,
|
|
new_chat_message_id BIGINT
|
|
) ON COMMIT DROP;
|
|
|
|
CREATE INDEX moved_chat_messages_old_chat_message_id ON moved_chat_messages(old_chat_message_id);
|
|
SQL
|
|
end
|
|
|
|
def create_temp_table_for_threads
|
|
DB.exec("DROP TABLE IF EXISTS moved_chat_threads") if Rails.env.test?
|
|
|
|
DB.exec <<~SQL
|
|
CREATE TEMPORARY TABLE moved_chat_threads (
|
|
old_thread_id INTEGER,
|
|
new_thread_id INTEGER
|
|
) ON COMMIT DROP;
|
|
|
|
CREATE INDEX moved_chat_threads_old_thread_id ON moved_chat_threads(old_thread_id);
|
|
SQL
|
|
end
|
|
|
|
def bulk_insert_movement_metadata_for_messages
|
|
values_sql = @movement_metadata.map { |mm| "(#{mm[:old_id]}, #{mm[:new_id]})" }.join(",\n")
|
|
DB.exec(
|
|
"INSERT INTO moved_chat_messages(old_chat_message_id, new_chat_message_id) VALUES #{values_sql}",
|
|
)
|
|
end
|
|
|
|
def create_destination_threads_in_channel(destination_channel)
|
|
moved_thread_ids =
|
|
@source_thread_ids.each_with_object({}) do |old_thread_id, hash|
|
|
old_thread = Chat::Thread.find(old_thread_id)
|
|
new_thread =
|
|
Chat::Thread.create!(
|
|
channel_id: destination_channel.id,
|
|
original_message_user_id: old_thread.original_message_user_id,
|
|
original_message_id: old_thread.original_message_id, # Placeholder, will be updated later
|
|
replies_count: old_thread.replies_count,
|
|
status: old_thread.status,
|
|
title: old_thread.title,
|
|
)
|
|
hash[old_thread_id] = new_thread.id
|
|
end
|
|
|
|
moved_thread_ids
|
|
end
|
|
|
|
##
|
|
# We purposefully omit in_reply_to_id when creating the messages in the
|
|
# new channel, because it could be pointing to a message that has not
|
|
# been moved.
|
|
def create_destination_messages_in_channel(destination_channel, moved_thread_ids)
|
|
insert_messages = <<-SQL
|
|
INSERT INTO chat_messages (
|
|
chat_channel_id, user_id, last_editor_id, message, cooked, cooked_version, thread_id, created_at, updated_at
|
|
)
|
|
SELECT :destination_channel_id, user_id, last_editor_id, message, cooked, cooked_version, :new_thread_id, CLOCK_TIMESTAMP(), CLOCK_TIMESTAMP()
|
|
FROM chat_messages
|
|
WHERE id = :source_message_id
|
|
RETURNING id
|
|
SQL
|
|
|
|
moved_message_ids =
|
|
@source_messages.map do |source_message|
|
|
new_thread_id = moved_thread_ids[source_message.thread_id]
|
|
|
|
new_message_id =
|
|
DB.query_single(
|
|
insert_messages,
|
|
{
|
|
destination_channel_id: destination_channel.id,
|
|
new_thread_id: new_thread_id,
|
|
source_message_id: source_message.id,
|
|
},
|
|
).first
|
|
|
|
new_message_id
|
|
end
|
|
@movement_metadata =
|
|
moved_message_ids.map.with_index do |chat_message_id, idx|
|
|
{ old_id: @ordered_source_message_ids[idx], new_id: chat_message_id }
|
|
end
|
|
moved_message_ids
|
|
end
|
|
|
|
def update_message_references
|
|
DB.exec(<<~SQL)
|
|
UPDATE chat_message_reactions cmr
|
|
SET chat_message_id = mm.new_chat_message_id
|
|
FROM moved_chat_messages mm
|
|
WHERE cmr.chat_message_id = mm.old_chat_message_id
|
|
SQL
|
|
|
|
DB.exec(<<~SQL, target_type: Chat::Message.polymorphic_name)
|
|
UPDATE upload_references uref
|
|
SET target_id = mm.new_chat_message_id
|
|
FROM moved_chat_messages mm
|
|
WHERE uref.target_id = mm.old_chat_message_id AND uref.target_type = :target_type
|
|
SQL
|
|
|
|
DB.exec(<<~SQL)
|
|
UPDATE chat_mentions cment
|
|
SET chat_message_id = mm.new_chat_message_id
|
|
FROM moved_chat_messages mm
|
|
WHERE cment.chat_message_id = mm.old_chat_message_id
|
|
SQL
|
|
|
|
DB.exec(<<~SQL)
|
|
UPDATE chat_message_revisions crev
|
|
SET chat_message_id = mm.new_chat_message_id
|
|
FROM moved_chat_messages mm
|
|
WHERE crev.chat_message_id = mm.old_chat_message_id
|
|
SQL
|
|
|
|
DB.exec(<<~SQL)
|
|
UPDATE chat_webhook_events cweb
|
|
SET chat_message_id = mm.new_chat_message_id
|
|
FROM moved_chat_messages mm
|
|
WHERE cweb.chat_message_id = mm.old_chat_message_id
|
|
SQL
|
|
end
|
|
|
|
def delete_source_messages
|
|
# We do this so @source_messages is not nulled out, which is the
|
|
# case when using update_all here.
|
|
DB.exec(
|
|
<<~SQL,
|
|
UPDATE chat_messages
|
|
SET deleted_at = NOW(), deleted_by_id = :deleted_by_id
|
|
WHERE id IN (:source_message_ids)
|
|
OR thread_id IN (:source_thread_ids)
|
|
SQL
|
|
source_message_ids: @source_message_ids,
|
|
deleted_by_id: @acting_user.id,
|
|
source_thread_ids: @source_thread_ids,
|
|
)
|
|
Chat::Publisher.publish_bulk_delete!(@source_channel, @source_message_ids)
|
|
end
|
|
|
|
def add_moved_placeholder(destination_channel, first_moved_message)
|
|
@source_channel.add(Discourse.system_user)
|
|
Chat::CreateMessage.call(
|
|
guardian: Discourse.system_user.guardian,
|
|
params: {
|
|
chat_channel_id: @source_channel.id,
|
|
message:
|
|
I18n.t(
|
|
"chat.channel.messages_moved",
|
|
count: @source_message_ids.length,
|
|
acting_username: @acting_user.username,
|
|
channel_name: destination_channel.title(@acting_user),
|
|
first_moved_message_url: first_moved_message.url,
|
|
),
|
|
},
|
|
)
|
|
end
|
|
|
|
def update_reply_references
|
|
DB.exec(<<~SQL, deleted_reply_to_ids: @source_message_ids)
|
|
UPDATE chat_messages
|
|
SET in_reply_to_id = NULL
|
|
WHERE in_reply_to_id IN (:deleted_reply_to_ids)
|
|
SQL
|
|
end
|
|
|
|
def update_thread_references(moved_thread_ids)
|
|
Chat::Thread.transaction do
|
|
moved_thread_ids.each do |old_thread_id, new_thread_id|
|
|
thread = Chat::Thread.find(new_thread_id)
|
|
|
|
new_original_message_id, new_last_message_id =
|
|
DB.query_single(<<-SQL, new_thread_id: new_thread_id)
|
|
SELECT MIN(id), MAX(id)
|
|
FROM chat_messages
|
|
WHERE thread_id = :new_thread_id
|
|
SQL
|
|
|
|
thread.update!(
|
|
original_message_id: new_original_message_id,
|
|
last_message_id: new_last_message_id,
|
|
)
|
|
|
|
thread.set_replies_count_cache(thread.replies_count)
|
|
end
|
|
end
|
|
end
|
|
|
|
def delete_source_threads
|
|
@source_thread_ids.each do |thread_id|
|
|
thread = Chat::Thread.find_by(id: thread_id)
|
|
thread.destroy if thread.present?
|
|
end
|
|
end
|
|
|
|
def update_tracking_state
|
|
::Chat::Action::ResetUserLastReadChannelMessage.call(@source_message_ids, @source_channel.id)
|
|
end
|
|
end
|
|
end
|