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
|
# when you have a temperory id in your frontend and want it echoed back via action cable
|
||||||
attr_accessor :echo_id
|
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 message_type: { incoming: 0, outgoing: 1, activity: 2, template: 3 }
|
||||||
enum content_type: {
|
enum content_type: {
|
||||||
text: 0,
|
text: 0,
|
||||||
@ -232,6 +235,7 @@ class Message < ApplicationRecord
|
|||||||
# Added this to cover the validation specs in messages
|
# Added this to cover the validation specs in messages
|
||||||
# We can revisit and see if we can remove this later
|
# We can revisit and see if we can remove this later
|
||||||
return if conversation.blank?
|
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.
|
# 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
|
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'])
|
expect(conv_new_message.errors[:base]).to eq(['Too many messages'])
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user