feat: rake task to copy conversations from one inbox to another (#84)
* feat: implement inbox message cloning task * feat: update contact inbox cloning to use builder pattern for unique token generation * fix: skip validations during message cloning to improve performance and error handling * feat: enhance inbox message cloning with detailed contact attributes and improved tagging * feat: improve inbox message cloning with enhanced error handling and attribute duplication * feat: skip message flooding check * chore: improved transaction handling * test: add specs for skip message flooding --------- Co-authored-by: gabrieljablonski <gabriel.g.jablonski@gmail.com>
This commit is contained in:
parent
2cf1795a3b
commit
93d9992cda
@ -80,6 +80,9 @@ class Message < ApplicationRecord
|
||||
# when you have a temperory id in your frontend and want it echoed back via action cable
|
||||
attr_accessor :echo_id
|
||||
|
||||
# NOTE: Allow skipping message flooding validation for bulk operations like imports/cloning
|
||||
attr_accessor :skip_message_flooding_validation
|
||||
|
||||
enum message_type: { incoming: 0, outgoing: 1, activity: 2, template: 3 }
|
||||
enum content_type: {
|
||||
text: 0,
|
||||
@ -232,6 +235,7 @@ class Message < ApplicationRecord
|
||||
# Added this to cover the validation specs in messages
|
||||
# We can revisit and see if we can remove this later
|
||||
return if conversation.blank?
|
||||
return if skip_message_flooding_validation
|
||||
|
||||
# there are cases where automations can result in message loops, we need to prevent such cases.
|
||||
if conversation.messages.where('created_at >= ?', 1.minute.ago).count >= Limits.conversation_message_per_minute_limit
|
||||
|
||||
163
lib/tasks/clone_inbox.rake
Normal file
163
lib/tasks/clone_inbox.rake
Normal file
@ -0,0 +1,163 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :inbox do # rubocop:disable Metrics/BlockLength
|
||||
desc 'Clone all messages from a source inbox to a destination inbox'
|
||||
task :clone_messages, %i[source_inbox_id destination_inbox_id] => :environment do |_task, args| # rubocop:disable Metrics/BlockLength
|
||||
source_inbox_id = args[:source_inbox_id]
|
||||
destination_inbox_id = args[:destination_inbox_id]
|
||||
|
||||
if source_inbox_id.blank? || destination_inbox_id.blank?
|
||||
puts 'Usage: rails inbox:clone_messages[<source_inbox_id>,<destination_inbox_id>]'
|
||||
next
|
||||
end
|
||||
|
||||
if source_inbox_id == destination_inbox_id
|
||||
puts "ERROR: Source and destination inbox IDs are the same (ID: #{source_inbox_id}). Please provide different IDs."
|
||||
next
|
||||
end
|
||||
|
||||
source_inbox = Inbox.find(source_inbox_id)
|
||||
destination_inbox = Inbox.find(destination_inbox_id)
|
||||
|
||||
if source_inbox.account_id != destination_inbox.account_id
|
||||
puts "ERROR: Source and destination inboxes are in different accounts (Source Account ID: #{source_inbox.account_id}, " \
|
||||
"Destination Account ID: #{destination_inbox.account_id})"
|
||||
next
|
||||
end
|
||||
|
||||
puts "Cloning messages from '#{source_inbox.name}' (ID: #{source_inbox.id}) to '#{destination_inbox.name}' (ID: #{destination_inbox.id})..."
|
||||
|
||||
old_to_new_contact_inbox = {}
|
||||
old_to_new_conversation = {}
|
||||
cloned_messages_count = 0
|
||||
failed_messages_count = 0
|
||||
|
||||
puts 'Cloning contacts and contact inboxes...'
|
||||
ActiveRecord::Base.transaction do
|
||||
source_inbox.contact_inboxes.includes(:contact).find_each do |contact_inbox|
|
||||
existing_contact_inbox = destination_inbox.contact_inboxes.find_by(source_id: contact_inbox.source_id)
|
||||
if existing_contact_inbox
|
||||
old_to_new_contact_inbox[contact_inbox.id] = existing_contact_inbox.id
|
||||
next
|
||||
end
|
||||
|
||||
contact_attrs = contact_inbox.contact.attributes.except('id', 'created_at', 'updated_at', 'account_id')
|
||||
|
||||
new_contact_inbox = ContactInboxWithContactBuilder.new(
|
||||
source_id: contact_inbox.source_id,
|
||||
inbox: destination_inbox,
|
||||
contact_attributes: contact_attrs
|
||||
).perform
|
||||
new_contact = new_contact_inbox.contact
|
||||
|
||||
if contact_inbox.contact.respond_to?(:label_list) && contact_inbox.contact.label_list.present?
|
||||
new_contact.label_list = contact_inbox.contact.label_list
|
||||
end
|
||||
|
||||
new_contact.custom_attributes = contact_inbox.contact.custom_attributes.dup if contact_inbox.contact.custom_attributes.present?
|
||||
|
||||
new_contact.save!
|
||||
old_to_new_contact_inbox[contact_inbox.id] = new_contact_inbox.id
|
||||
end
|
||||
end
|
||||
puts "Cloned #{old_to_new_contact_inbox.count} contact inboxes."
|
||||
|
||||
puts 'Cloning conversations...'
|
||||
ActiveRecord::Base.transaction do
|
||||
source_inbox.conversations.includes(:conversation_participants, :csat_survey_response).find_each do |conversation|
|
||||
target_contact_inbox_id = old_to_new_contact_inbox[conversation.contact_inbox_id]
|
||||
next unless target_contact_inbox_id
|
||||
|
||||
existing_conversation = destination_inbox.conversations.find_by(
|
||||
contact_inbox_id: target_contact_inbox_id,
|
||||
created_at: conversation.created_at
|
||||
)
|
||||
|
||||
if existing_conversation
|
||||
old_to_new_conversation[conversation.id] = existing_conversation.id
|
||||
next
|
||||
end
|
||||
|
||||
new_conversation_attrs = conversation.attributes.except('id', 'display_id', 'updated_at', 'uuid')
|
||||
new_conversation_attrs['inbox_id'] = destination_inbox.id
|
||||
new_conversation_attrs['contact_inbox_id'] = target_contact_inbox_id
|
||||
|
||||
new_conversation = destination_inbox.conversations.create!(new_conversation_attrs)
|
||||
old_to_new_conversation[conversation.id] = new_conversation.id
|
||||
|
||||
if conversation.label_list.present?
|
||||
new_conversation.label_list = conversation.label_list
|
||||
new_conversation.save!
|
||||
end
|
||||
|
||||
new_conversation.update!(custom_attributes: conversation.custom_attributes.dup) if conversation.custom_attributes.present?
|
||||
|
||||
conversation.conversation_participants.each do |conversation_participant|
|
||||
participant_attrs = conversation_participant.attributes.except('id', 'created_at', 'updated_at')
|
||||
participant_attrs['conversation_id'] = new_conversation.id
|
||||
new_conversation.conversation_participants.create!(participant_attrs)
|
||||
end
|
||||
|
||||
if (csat_response = conversation.csat_survey_response)
|
||||
new_conversation.create_csat_survey_response!(csat_response.attributes.except('id', 'conversation_id', 'updated_at'))
|
||||
end
|
||||
end
|
||||
end
|
||||
puts "Cloned #{old_to_new_conversation.count} conversations."
|
||||
|
||||
puts 'Cloning messages...'
|
||||
source_inbox.messages.includes(:attachments).find_in_batches(batch_size: 1000) do |message_batch| # rubocop:disable Metrics/BlockLength
|
||||
ActiveRecord::Base.transaction do # rubocop:disable Metrics/BlockLength
|
||||
message_batch.each do |original_message| # rubocop:disable Metrics/BlockLength
|
||||
new_conv_id = old_to_new_conversation[original_message.conversation_id]
|
||||
next unless new_conv_id
|
||||
|
||||
existing_message = if original_message.source_id.present?
|
||||
Message.find_by(
|
||||
conversation_id: new_conv_id,
|
||||
source_id: original_message.source_id
|
||||
)
|
||||
else
|
||||
Message.find_by(
|
||||
conversation_id: new_conv_id,
|
||||
created_at: original_message.created_at,
|
||||
content: original_message.content
|
||||
)
|
||||
end
|
||||
|
||||
if existing_message
|
||||
cloned_messages_count += 1
|
||||
next
|
||||
end
|
||||
|
||||
message_attrs = original_message.attributes.except('id', 'updated_at')
|
||||
message_attrs['conversation_id'] = new_conv_id
|
||||
message_attrs['inbox_id'] = destination_inbox.id
|
||||
|
||||
new_message = destination_inbox.messages.new(message_attrs)
|
||||
|
||||
begin
|
||||
new_message.skip_message_flooding_validation = true
|
||||
new_message.save!
|
||||
cloned_messages_count += 1
|
||||
original_message.attachments.each do |attachment|
|
||||
new_message.attachments.create!(
|
||||
file_type: attachment.file_type,
|
||||
account_id: destination_inbox.account_id,
|
||||
file: attachment.file.blob
|
||||
)
|
||||
end
|
||||
rescue StandardError => e
|
||||
failed_messages_count += 1
|
||||
puts "Failed to clone message ID: #{original_message.id}. Errors: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
print "Progress: Cloned: #{cloned_messages_count} | Failed: #{failed_messages_count}\r"
|
||||
end
|
||||
|
||||
puts "\nCloning complete."
|
||||
puts "Successfully cloned #{cloned_messages_count} messages."
|
||||
puts "Failed to clone #{failed_messages_count} messages."
|
||||
end
|
||||
end
|
||||
@ -38,6 +38,58 @@ RSpec.describe Message do
|
||||
expect(conv_new_message.errors[:base]).to eq(['Too many messages'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_message_flooding_validation is set' do
|
||||
it 'skips message flooding validation when set to true' do
|
||||
with_modified_env 'CONVERSATION_MESSAGE_PER_MINUTE_LIMIT': '2' do
|
||||
conversation = message.conversation
|
||||
create(:message, conversation: conversation)
|
||||
conv_new_message = build(:message, conversation: message.conversation)
|
||||
|
||||
expect(conv_new_message.valid?).to be false
|
||||
expect(conv_new_message.errors[:base]).to eq(['Too many messages'])
|
||||
|
||||
conv_new_message.skip_message_flooding_validation = true
|
||||
conv_new_message.valid?
|
||||
expect(conv_new_message.errors[:base]).to be_empty
|
||||
expect(conv_new_message.valid?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
it 'still validates other attributes when message flooding is skipped' do
|
||||
message_without_required_fields = build(:message)
|
||||
message_without_required_fields.account_id = nil
|
||||
message_without_required_fields.inbox_id = nil
|
||||
message_without_required_fields.skip_message_flooding_validation = true
|
||||
|
||||
expect(message_without_required_fields.valid?).to be false
|
||||
expect(message_without_required_fields.errors[:account_id]).to include("can't be blank")
|
||||
expect(message_without_required_fields.errors[:inbox_id]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'allows bulk message creation when skip_message_flooding_validation is true' do
|
||||
with_modified_env 'CONVERSATION_MESSAGE_PER_MINUTE_LIMIT': '2' do
|
||||
conversation = message.conversation
|
||||
|
||||
messages_to_create = 5
|
||||
created_messages = []
|
||||
|
||||
messages_to_create.times do |i|
|
||||
new_message = build(:message,
|
||||
conversation: conversation,
|
||||
content: "Bulk message #{i + 1}")
|
||||
new_message.skip_message_flooding_validation = true
|
||||
|
||||
expect(new_message.valid?).to be true
|
||||
new_message.save!
|
||||
created_messages << new_message
|
||||
end
|
||||
|
||||
expect(created_messages.count).to eq(messages_to_create)
|
||||
expect(conversation.messages.count).to eq(messages_to_create + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user