chatwoot-develop/app/services/whatsapp/zapi_handlers/received_callback.rb

282 lines
8.3 KiB
Ruby
Executable File

module Whatsapp::ZapiHandlers::ReceivedCallback # rubocop:disable Metrics/ModuleLength
include Whatsapp::ZapiHandlers::Helpers
private
def process_received_callback
@raw_message = processed_params
@message = nil
@contact_inbox = nil
@contact = nil
return unless should_process_message?
return if find_message_by_source_id(raw_message_id) || message_under_process?
cache_message_source_id_in_redis
return handle_edited_message if @raw_message[:isEdit]
with_zapi_contact_lock(@raw_message[:phone]) do
set_contact
unless @contact
Rails.logger.warn "Contact not found for message: #{raw_message_id}"
return
end
set_conversation
handle_create_message
end
ensure
clear_message_source_id_from_redis
end
def should_process_message?
!@raw_message[:isGroup] &&
!@raw_message[:isNewsletter] &&
!@raw_message[:broadcast] &&
!@raw_message[:isStatusReply] &&
!@raw_message.key?(:notification)
end
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
return 'text' if @raw_message.key?(:text)
return 'reaction' if @raw_message.key?(:reaction)
return 'audio' if @raw_message.key?(:audio)
return 'image' if @raw_message.key?(:image)
return 'sticker' if @raw_message.key?(:sticker)
return 'video' if @raw_message.key?(:video)
return 'file' if @raw_message.key?(:document)
return 'contact' if @raw_message.key?(:contact)
'unsupported'
end
def message_content
case message_type
when 'text'
@raw_message.dig(:text, :message)
when 'image'
@raw_message.dig(:image, :caption)
when 'video'
@raw_message.dig(:video, :caption)
when 'file'
@raw_message.dig(:document, :fileName)
when 'reaction'
@raw_message.dig(:reaction, :value)
when 'contact'
@raw_message.dig(:contact, :displayName)
end
end
def contact_name
@raw_message[:senderName] || @raw_message[:chatName] || @raw_message[:phone]
end
def set_contact
push_name = contact_name
source_id = @raw_message[:chatLid].to_s.gsub(/[^\d]/, '')
identifier = @raw_message[:chatLid]
contact_attributes = { name: push_name, identifier: identifier }
unless @raw_message[:phone].ends_with?('@lid')
contact_attributes[:phone_number] = "+#{@raw_message[:phone]}"
update_existing_contact_inbox(@raw_message[:phone], source_id, identifier)
end
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: source_id,
inbox: inbox,
contact_attributes: contact_attributes
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
@contact.update!(name: push_name) if @contact.name == @raw_message[:phone]
update_contact_phone_number
try_update_contact_avatar
end
def update_existing_contact_inbox(phone, source_id, identifier)
# NOTE: This is useful when we create a new contact manually, so we don't have information about contact LID;
# With this, when we receive a message from that contact, we can link it properly.
existing_contact = inbox.account.contacts.find_by(phone_number: "+#{phone}")
return unless existing_contact
existing_contact_inbox = existing_contact.contact_inboxes.find_by(inbox_id: inbox.id)
ActiveRecord::Base.transaction do
existing_contact.update!(identifier: identifier)
existing_contact_inbox&.update!(source_id: source_id)
end
end
def update_contact_phone_number
return if @contact.phone_number.present?
return if @raw_message[:phone].ends_with?('@lid')
@contact.update!(phone_number: "+#{@raw_message[:phone]}")
end
def try_update_contact_avatar
avatar_url = @raw_message[:senderPhoto] || @raw_message[:photo]
return unless avatar_url.present? && avatar_url.start_with?('http')
Avatar::AvatarFromUrlJob.perform_later(@contact, avatar_url)
end
def handle_create_message
if message_type == 'contact'
create_contact_message
else
create_message(attach_media: %w[image sticker file video audio].include?(message_type))
end
end
def create_contact_message
contact_data = @raw_message[:contact]
phones = contact_data[:phones] || []
phones = ['Phone number is not available'] if phones.blank?
phones.each do |phone|
build_message
attach_contact(phone, contact_data)
@message.save!
end
notify_channel_of_received_message
end
def create_message(attach_media: false)
build_message
handle_attach_media if attach_media
@message.save!
notify_channel_of_received_message
end
def build_message
@message = @conversation.messages.build(
content: message_content,
account_id: @inbox.account_id,
inbox_id: @inbox.id,
source_id: raw_message_id,
sender: incoming_message? ? @contact : @inbox.account.account_users.first.user,
sender_type: incoming_message? ? 'Contact' : 'User',
message_type: incoming_message? ? :incoming : :outgoing,
content_attributes: message_content_attributes
)
end
def notify_channel_of_received_message
inbox.channel.received_messages([@message], @conversation) if incoming_message?
end
def message_content_attributes
type = message_type
content_attributes = { external_created_at: @raw_message[:momment] / 1000 }
if type == 'reaction'
content_attributes[:in_reply_to_external_id] = @raw_message.dig(:reaction, :referencedMessage, :messageId)
content_attributes[:is_reaction] = true
elsif type == 'unsupported'
content_attributes[:is_unsupported] = true
end
content_attributes[:in_reply_to_external_id] = @raw_message[:referenceMessageId] if @raw_message[:referenceMessageId].present?
content_attributes
end
def attach_contact(phone, contact_data)
name_parts = contact_data[:displayName]&.split || []
@message.attachments.new(
account_id: @message.account_id,
file_type: :contact,
fallback_title: phone.to_s,
meta: {
firstName: name_parts.first,
lastName: name_parts.drop(1).join(' ')
}.compact_blank
)
end
def handle_attach_media
attachment_file = download_attachment_file
attachment = @message.attachments.build(
account_id: @message.account_id,
file_type: file_content_type.to_s,
file: { io: attachment_file, filename: filename, content_type: message_mimetype }
)
attachment.meta = { is_recorded_audio: true } if @raw_message.dig(:audio, :ptt)
rescue Down::Error => e
@message.update!(is_unsupported: true)
Rails.logger.error "Failed to download attachment for message #{raw_message_id}: #{e.message}"
end
def download_attachment_file
media_url = case message_type
when 'image'
@raw_message.dig(:image, :imageUrl)
when 'sticker'
@raw_message.dig(:sticker, :stickerUrl)
when 'audio'
@raw_message.dig(:audio, :audioUrl)
when 'video'
@raw_message.dig(:video, :videoUrl)
when 'file'
@raw_message.dig(:document, :documentUrl)
end
Down.download(media_url)
end
def filename
case message_type
when 'file'
@raw_message.dig(:document, :fileName)
else
ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present?
"#{file_content_type}_#{raw_message_id}_#{Time.current.strftime('%Y%m%d')}#{ext}"
end
end
def file_content_type
return :image if %w[image sticker].include?(message_type)
return :video if message_type == 'video'
return :audio if message_type == 'audio'
:file
end
def message_mimetype
case message_type
when 'image'
@raw_message.dig(:image, :mimeType)
when 'sticker'
@raw_message.dig(:sticker, :mimeType)
when 'video'
@raw_message.dig(:video, :mimeType)
when 'audio'
@raw_message.dig(:audio, :mimeType)
when 'file'
@raw_message.dig(:document, :mimeType)
end
end
def handle_edited_message
@message = find_message_by_source_id(@raw_message[:messageId])
return unless @message
@message.update!(
content: message_content,
is_edited: true,
previous_content: @message.content
)
end
end