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