312 lines
10 KiB
Ruby
312 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
##
|
|
# From time to time, site admins may choose to sunset a chat channel and archive
|
|
# the messages within. 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
|
|
|
|
class ArchiveValidationError < StandardError
|
|
attr_reader :errors
|
|
|
|
def initialize(errors: [])
|
|
super
|
|
@errors = errors
|
|
end
|
|
end
|
|
|
|
def self.create_archive_process(chat_channel:, acting_user:, topic_params:)
|
|
return if ChatChannelArchive.exists?(chat_channel: chat_channel)
|
|
|
|
# Only need to validate topic params for a new topic, not an existing one.
|
|
if topic_params[:topic_id].blank?
|
|
valid, errors =
|
|
Chat::ChatChannelArchiveService.validate_topic_params(
|
|
Guardian.new(acting_user),
|
|
topic_params,
|
|
)
|
|
|
|
raise ArchiveValidationError.new(errors: errors) if !valid
|
|
end
|
|
|
|
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
|
|
|
|
def self.validate_topic_params(guardian, topic_params)
|
|
topic_creator =
|
|
TopicCreator.new(
|
|
Discourse.system_user,
|
|
guardian,
|
|
{
|
|
title: topic_params[:topic_title],
|
|
category: topic_params[:category_id],
|
|
tags: topic_params[:tags],
|
|
import_mode: true,
|
|
},
|
|
)
|
|
[topic_creator.valid?, topic_creator.errors.full_messages]
|
|
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
|
|
return if !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_message: err.message)
|
|
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,
|
|
},
|
|
)
|
|
|
|
if topic_creator.valid?
|
|
chat_channel_archive.update!(destination_topic: topic_creator.create)
|
|
else
|
|
Rails.logger.info("Destination topic for #{chat_channel_title} archive was not valid.")
|
|
notify_archiver(
|
|
:failed_no_topic,
|
|
error_message: topic_creator.errors.full_messages.join("\n"),
|
|
)
|
|
end
|
|
end
|
|
|
|
if chat_channel_archive.destination_topic.present?
|
|
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,
|
|
),
|
|
)
|
|
end
|
|
else
|
|
Rails.logger.info("Topic already exists for #{chat_channel_title} archive.")
|
|
end
|
|
|
|
if chat_channel_archive.destination_topic.present?
|
|
update_destination_topic_status
|
|
return true
|
|
end
|
|
|
|
false
|
|
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.new_topic?
|
|
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_message: nil)
|
|
base_translation_params = {
|
|
channel_hashtag_or_name: channel_hashtag_or_name,
|
|
topic_title: chat_channel_archive.destination_topic&.title,
|
|
topic_url: chat_channel_archive.destination_topic&.url,
|
|
topic_validation_errors: result == :failed_no_topic ? error_message : nil,
|
|
}
|
|
|
|
if result == :failed || result == :failed_no_topic
|
|
Discourse.warn_exception(
|
|
error_message,
|
|
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)
|
|
message_translation_key =
|
|
case result
|
|
when :failed
|
|
:chat_channel_archive_failed
|
|
when :failed_no_topic
|
|
:chat_channel_archive_failed_no_topic
|
|
end
|
|
SystemMessage.create_from_system_user(
|
|
chat_channel_archive.archived_by,
|
|
message_translation_key,
|
|
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 != :success ? :failed : :success,
|
|
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
|
|
|
|
def channel_hashtag_or_name
|
|
if chat_channel.slug.present? && SiteSetting.enable_experimental_hashtag_autocomplete
|
|
return "##{chat_channel.slug}::channel"
|
|
end
|
|
chat_channel_title
|
|
end
|
|
end
|