module Whatsapp::BaileysHandlers::MessagesUpsert include Whatsapp::BaileysHandlers::Helpers include BaileysHelper private def process_messages_upsert messages = processed_params[:data][:messages] messages.each do |message| @message = nil @contact_inbox = nil @contact = nil @raw_message = message next handle_message if incoming? # NOTE: Shared lock with Whatsapp::SendOnWhatsappService # Avoids race conditions when sending messages. with_baileys_channel_lock_on_outgoing_message(inbox.channel.id) { handle_message } end end def handle_message return unless %w[lid user].include?(jid_type) return unless extract_from_jid(type: 'lid') return if ignore_message? return if find_message_by_source_id(raw_message_id) || message_under_process? cache_message_source_id_in_redis set_contact unless @contact clear_message_source_id_from_redis Rails.logger.warn "Contact not found for message: #{raw_message_id}" return end set_conversation handle_create_message clear_message_source_id_from_redis end def set_contact phone = extract_from_jid(type: 'pn') source_id = extract_from_jid(type: 'lid') identifier = "#{source_id}@lid" update_existing_contact_inbox(phone, source_id, identifier) if phone contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: source_id, inbox: inbox, contact_attributes: { name: contact_name, phone_number: ("+#{phone}" if phone), identifier: identifier } ).perform @contact_inbox = contact_inbox @contact = contact_inbox.contact update_contact_info(phone, source_id, identifier) 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 = inbox.contact_inboxes.find_by(source_id: phone) return unless existing_contact_inbox return if inbox.contact_inboxes.exists?(source_id: source_id) existing_contact = existing_contact_inbox.contact conflicting_identifier = inbox.account.contacts.find_by(identifier: identifier) conflicting_phone = inbox.account.contacts.find_by(phone_number: "+#{phone}") return if conflicting_identifier && conflicting_identifier.id != existing_contact.id return if conflicting_phone && conflicting_phone.id != existing_contact.id ActiveRecord::Base.transaction do existing_contact_inbox.update!(source_id: source_id) existing_contact.update!(identifier: identifier, phone_number: "+#{phone}") end end def update_contact_info(phone, source_id, identifier) update_params = {} update_params[:phone_number] = "+#{phone}" if phone update_params[:identifier] = identifier update_params[:name] = contact_name if @contact.name.in?([phone, source_id, identifier]) @contact.update!(update_params) if update_params.present? try_update_contact_avatar end def handle_create_message create_message(attach_media: %w[image file video audio sticker].include?(message_type)) end def create_message(attach_media: false) @message = @conversation.messages.build( content: message_content, account_id: @inbox.account_id, inbox_id: @inbox.id, source_id: raw_message_id, sender: incoming? ? @contact : @inbox.account.account_users.first.user, sender_type: incoming? ? 'Contact' : 'User', message_type: incoming? ? :incoming : :outgoing, content_attributes: message_content_attributes ) handle_attach_media if attach_media @message.save! inbox.channel.received_messages([@message], @conversation) if incoming? end def message_content_attributes type = message_type msg = unwrap_ephemeral_message(@raw_message[:message]) content_attributes = { external_created_at: baileys_extract_message_timestamp(@raw_message[:messageTimestamp]) } if type == 'reaction' content_attributes[:in_reply_to_external_id] = msg.dig(:reactionMessage, :key, :id) content_attributes[:is_reaction] = true elsif reply_to_message_id content_attributes[:in_reply_to_external_id] = reply_to_message_id elsif type == 'unsupported' content_attributes[:is_unsupported] = true end content_attributes end def handle_attach_media attachment_file = download_attachment_file msg = unwrap_ephemeral_message(@raw_message[:message]) 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 msg.dig(:audioMessage, :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 Down.download(@conversation.inbox.channel.media_url(@raw_message.dig(:key, :id)), headers: @conversation.inbox.channel.api_headers) end def filename msg = unwrap_ephemeral_message(@raw_message[:message]) filename = msg.dig(:documentMessage, :fileName) || msg.dig(:documentWithCaptionMessage, :message, :documentMessage, :fileName) return filename if filename.present? 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