discourse/plugins/chat/lib/chat_channel_archive_servic...

248 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,
)
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