249 lines
8.4 KiB
Ruby
249 lines
8.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
##
|
|
# From time to time, site admins may choose to sunset a chat channel and archive
|
|
# the messages within. The main use case for this is a topic-based channel, but
|
|
# it can be used for category channels just fine. It cannot be used for DM channels
|
|
# in its current iteration.
|
|
#
|
|
# To archive a channel, we mark it read_only first to prevent any further message
|
|
# additions or changes, and create a record to track whether the archive topic
|
|
# will be new or existing. When we archive the channel, messages are copied into
|
|
# posts in batches using the [chat] BBCode to quote the messages. The messages are
|
|
# deleted once the batch has its post made. The execute action of this class is
|
|
# idempotent, so if we fail halfway through the archive process it can be run again.
|
|
#
|
|
# Once all of the messages have been copied then we mark the channel as archived.
|
|
class Chat::ChatChannelArchiveService
|
|
ARCHIVED_MESSAGES_PER_POST = 100
|
|
|
|
def self.begin_archive_process(chat_channel:, acting_user:, topic_params:)
|
|
return if ChatChannelArchive.exists?(chat_channel: chat_channel)
|
|
|
|
ChatChannelArchive.transaction do
|
|
chat_channel.read_only!(acting_user)
|
|
|
|
archive =
|
|
ChatChannelArchive.create!(
|
|
chat_channel: chat_channel,
|
|
archived_by: acting_user,
|
|
total_messages: chat_channel.chat_messages.count,
|
|
destination_topic_id: topic_params[:topic_id],
|
|
destination_topic_title: topic_params[:topic_title],
|
|
destination_category_id: topic_params[:category_id],
|
|
destination_tags: topic_params[:tags],
|
|
)
|
|
Jobs.enqueue(:chat_channel_archive, chat_channel_archive_id: archive.id)
|
|
|
|
archive
|
|
end
|
|
end
|
|
|
|
def self.retry_archive_process(chat_channel:)
|
|
return if !chat_channel.chat_channel_archive&.failed?
|
|
Jobs.enqueue(
|
|
:chat_channel_archive,
|
|
chat_channel_archive_id: chat_channel.chat_channel_archive.id,
|
|
)
|
|
chat_channel.chat_channel_archive
|
|
end
|
|
|
|
attr_reader :chat_channel_archive, :chat_channel, :chat_channel_title
|
|
|
|
def initialize(chat_channel_archive)
|
|
@chat_channel_archive = chat_channel_archive
|
|
@chat_channel = chat_channel_archive.chat_channel
|
|
@chat_channel_title = chat_channel.title(chat_channel_archive.archived_by)
|
|
end
|
|
|
|
def execute
|
|
chat_channel_archive.update(archive_error: nil)
|
|
|
|
begin
|
|
ensure_destination_topic_exists!
|
|
|
|
Rails.logger.info(
|
|
"Creating posts from message batches for #{chat_channel_title} archive, #{chat_channel_archive.total_messages} messages to archive (#{chat_channel_archive.total_messages / ARCHIVED_MESSAGES_PER_POST} posts).",
|
|
)
|
|
|
|
# a batch should be idempotent, either the post is created and the
|
|
# messages are deleted or we roll back the whole thing.
|
|
#
|
|
# at some point we may want to reconsider disabling post validations,
|
|
# and add in things like dynamic resizing of the number of messages per
|
|
# post based on post length, but that can be done later
|
|
#
|
|
# another future improvement is to send a MessageBus message for each
|
|
# completed batch, so the UI can receive updates and show a progress
|
|
# bar or something similar
|
|
chat_channel
|
|
.chat_messages
|
|
.find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |chat_messages|
|
|
create_post(
|
|
ChatTranscriptService.new(
|
|
chat_channel,
|
|
chat_channel_archive.archived_by,
|
|
messages_or_ids: chat_messages,
|
|
opts: {
|
|
no_link: true,
|
|
include_reactions: true,
|
|
},
|
|
).generate_markdown,
|
|
) { delete_message_batch(chat_messages.map(&:id)) }
|
|
end
|
|
|
|
kick_all_users
|
|
complete_archive
|
|
rescue => err
|
|
notify_archiver(:failed, error: err)
|
|
raise err
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def create_post(raw)
|
|
pc = nil
|
|
Post.transaction do
|
|
pc =
|
|
PostCreator.new(
|
|
Discourse.system_user,
|
|
raw: raw,
|
|
# we must skip these because the posts are created in a big transaction,
|
|
# we do them all at the end instead
|
|
skip_jobs: true,
|
|
# we do not want to be sending out notifications etc. from this
|
|
# automatic background process
|
|
import_mode: true,
|
|
# don't want to be stopped by watched word or post length validations
|
|
skip_validations: true,
|
|
topic_id: chat_channel_archive.destination_topic_id,
|
|
)
|
|
|
|
pc.create
|
|
|
|
# so we can also delete chat messages in the same transaction
|
|
yield if block_given?
|
|
end
|
|
pc.enqueue_jobs
|
|
end
|
|
|
|
def ensure_destination_topic_exists!
|
|
if !chat_channel_archive.destination_topic.present?
|
|
Rails.logger.info("Creating topic for #{chat_channel_title} archive.")
|
|
Topic.transaction do
|
|
topic_creator =
|
|
TopicCreator.new(
|
|
Discourse.system_user,
|
|
Guardian.new(chat_channel_archive.archived_by),
|
|
{
|
|
title: chat_channel_archive.destination_topic_title,
|
|
category: chat_channel_archive.destination_category_id,
|
|
tags: chat_channel_archive.destination_tags,
|
|
import_mode: true,
|
|
},
|
|
)
|
|
|
|
chat_channel_archive.update!(destination_topic: topic_creator.create)
|
|
end
|
|
|
|
Rails.logger.info("Creating first post for #{chat_channel_title} archive.")
|
|
create_post(
|
|
I18n.t(
|
|
"chat.channel.archive.first_post_raw",
|
|
channel_name: chat_channel_title,
|
|
channel_url: chat_channel.url,
|
|
),
|
|
)
|
|
else
|
|
Rails.logger.info("Topic already exists for #{chat_channel_title} archive.")
|
|
end
|
|
|
|
update_destination_topic_status
|
|
end
|
|
|
|
def update_destination_topic_status
|
|
# we only want to do this when the destination topic is new, not an
|
|
# existing topic, because we don't want to update the status unexpectedly
|
|
# on an existing topic
|
|
if chat_channel_archive.destination_topic_title.present?
|
|
if SiteSetting.chat_archive_destination_topic_status == "archived"
|
|
chat_channel_archive.destination_topic.update!(archived: true)
|
|
elsif SiteSetting.chat_archive_destination_topic_status == "closed"
|
|
chat_channel_archive.destination_topic.update!(closed: true)
|
|
end
|
|
end
|
|
end
|
|
|
|
def delete_message_batch(message_ids)
|
|
ChatMessage.transaction do
|
|
ChatMessage.where(id: message_ids).update_all(
|
|
deleted_at: DateTime.now,
|
|
deleted_by_id: chat_channel_archive.archived_by.id,
|
|
)
|
|
|
|
chat_channel_archive.update!(
|
|
archived_messages: chat_channel_archive.archived_messages + message_ids.length,
|
|
)
|
|
end
|
|
|
|
Rails.logger.info(
|
|
"Archived #{chat_channel_archive.archived_messages} messages for #{chat_channel_title} archive.",
|
|
)
|
|
end
|
|
|
|
def complete_archive
|
|
Rails.logger.info("Creating posts completed for #{chat_channel_title} archive.")
|
|
chat_channel.archived!(chat_channel_archive.archived_by)
|
|
notify_archiver(:success)
|
|
end
|
|
|
|
def notify_archiver(result, error: nil)
|
|
base_translation_params = {
|
|
channel_name: chat_channel_title,
|
|
topic_title: chat_channel_archive.destination_topic.title,
|
|
topic_url: chat_channel_archive.destination_topic.url,
|
|
}
|
|
|
|
if result == :failed
|
|
Discourse.warn_exception(
|
|
error,
|
|
message: "Error when archiving chat channel #{chat_channel_title}.",
|
|
env: {
|
|
chat_channel_id: chat_channel.id,
|
|
chat_channel_name: chat_channel_title,
|
|
},
|
|
)
|
|
error_translation_params =
|
|
base_translation_params.merge(
|
|
channel_url: chat_channel.url,
|
|
messages_archived: chat_channel_archive.archived_messages,
|
|
)
|
|
chat_channel_archive.update(archive_error: error.message)
|
|
SystemMessage.create_from_system_user(
|
|
chat_channel_archive.archived_by,
|
|
:chat_channel_archive_failed,
|
|
error_translation_params,
|
|
)
|
|
else
|
|
SystemMessage.create_from_system_user(
|
|
chat_channel_archive.archived_by,
|
|
:chat_channel_archive_complete,
|
|
base_translation_params,
|
|
)
|
|
end
|
|
|
|
ChatPublisher.publish_archive_status(
|
|
chat_channel,
|
|
archive_status: result,
|
|
archived_messages: chat_channel_archive.archived_messages,
|
|
archive_topic_id: chat_channel_archive.destination_topic_id,
|
|
total_messages: chat_channel_archive.total_messages,
|
|
)
|
|
end
|
|
|
|
def kick_all_users
|
|
Chat::ChatChannelMembershipManager.new(chat_channel).unfollow_all_users
|
|
end
|
|
end
|