feat(baileys): upgrade to v7 (#143)

* feat(baileys): upgrade to v7 (#139)

* feat(baileys): v7

* chore(zapi): match baileys changes

* test: fix send on whatsapp spec

* chore: simplify logic and minor fixes

* fix: ensure contact avatar updates when profile picture changes

* chore: add beta release workflow

* feat: enhance contact update logic to handle conflicts in inbox updates (#140)

* feat: update GitHub Actions workflow to trigger on release events and adjust GIT_REF handling

* fix: prevent unique constraint error when LID contact inbox already exists (#141)

* fix: prevent unique constraint error when LID contact inbox already exists

* feat(baileys): handle ephemeral messages
This commit is contained in:
Gabriel Jablonski 2025-11-13 20:42:08 -03:00 committed by GitHub
parent 17149f3dca
commit c041090675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 766 additions and 169 deletions

View File

@ -5,9 +5,8 @@ permissions:
packages: write
on:
push:
tags:
- '*'
release:
types: [released]
workflow_dispatch:
env:
@ -25,10 +24,12 @@ jobs:
runner: ubuntu-22.04-arm
runs-on: ${{ matrix.runner }}
env:
GIT_REF: ${{ github.head_ref || github.ref_name || github.sha }}
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}
- name: Prepare
run: |
@ -61,7 +62,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
registry: ghcr.io
@ -75,7 +76,7 @@ jobs:
context: .
file: docker/Dockerfile
platforms: ${{ matrix.platform }}
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
push: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
tags: |
${{ env.GITHUB_REPO }}:${{ env.SANITIZED_REF }}-ee
${{ env.GITHUB_REPO }}:latest-ee
@ -119,7 +120,7 @@ jobs:
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
GIT_REF: ${{ github.head_ref || github.ref_name || github.sha }}
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
docker buildx imagetools create \
@ -129,7 +130,7 @@ jobs:
- name: Inspect image
env:
GIT_REF: ${{ github.head_ref || github.ref_name || github.sha }}
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
REPO="ghcr.io/${{ github.repository }}"

View File

@ -5,9 +5,8 @@ permissions:
packages: write
on:
push:
tags:
- '*'
release:
types: [released]
workflow_dispatch:
env:
@ -25,10 +24,12 @@ jobs:
runner: ubuntu-22.04-arm
runs-on: ${{ matrix.runner }}
env:
GIT_REF: ${{ github.head_ref || github.ref_name }}
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}
- name: Prepare
run: |
@ -62,7 +63,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
registry: ghcr.io
@ -76,7 +77,7 @@ jobs:
context: .
file: docker/Dockerfile
platforms: ${{ matrix.platform }}
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
push: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
tags: |
${{ env.GITHUB_REPO }}:${{ env.SANITIZED_REF }}
${{ env.GITHUB_REPO }}:latest
@ -120,7 +121,7 @@ jobs:
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
GIT_REF: ${{ github.head_ref || github.ref_name }}
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
docker buildx imagetools create \
@ -130,7 +131,7 @@ jobs:
- name: Inspect image
env:
GIT_REF: ${{ github.head_ref || github.ref_name }}
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
REPO="ghcr.io/${{ github.repository }}"

View File

@ -0,0 +1,139 @@
name: Publish Chatwoot beta docker images to GitHub
permissions:
contents: read
packages: write
on:
release:
types: [prereleased]
workflow_dispatch:
env:
GITHUB_REPO: ghcr.io/${{ github.repository }}
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-22.04-arm
runs-on: ${{ matrix.runner }}
env:
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Update version in app.yml
run: |
if [[ "$GIT_REF" =~ ^v(.+)$ ]]; then
VERSION="${BASH_REMATCH[1]}"
echo "Updating version to: $VERSION"
sed -i "s/version: '.*'/version: '$VERSION'/" config/app.yml
else
echo "No version tag found, keeping existing version"
fi
- name: Set Docker Tags
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
echo "SANITIZED_REF=${SANITIZED_REF}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to GitHub Container Registry
id: build-ghcr
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
platforms: ${{ matrix.platform }}
push: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
tags: |
${{ env.GITHUB_REPO }}:${{ env.SANITIZED_REF }}
${{ env.GITHUB_REPO }}:beta
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build-ghcr.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
docker buildx imagetools create \
-t ghcr.io/${{ github.repository }}:${SANITIZED_REF} \
-t ghcr.io/${{ github.repository }}:beta \
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
- name: Inspect image
env:
GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
REPO="ghcr.io/${{ github.repository }}"
docker buildx imagetools inspect ${REPO}:${SANITIZED_REF}
docker buildx imagetools inspect ${REPO}:beta

View File

@ -86,8 +86,9 @@ class Channel::Whatsapp < ApplicationRecord
def toggle_typing_status(typing_status, conversation:)
return unless provider_service.respond_to?(:toggle_typing_status)
recipient_id = conversation.contact.identifier || conversation.contact.phone_number
last_message = conversation.messages.last
provider_service.toggle_typing_status(typing_status, last_message: last_message, phone_number: conversation.contact.phone_number)
provider_service.toggle_typing_status(typing_status, last_message: last_message, recipient_id: recipient_id)
end
def update_presence(status)
@ -101,7 +102,13 @@ class Channel::Whatsapp < ApplicationRecord
# NOTE: This is the default behavior, so `mark_as_read` being `nil` is the same as `true`.
return if provider_config&.dig('mark_as_read') == false
provider_service.read_messages(messages, phone_number: conversation.contact.phone_number)
recipient_id = if provider == 'zapi'
conversation.contact.phone_number
else
conversation.contact.identifier || conversation.contact.phone_number
end
provider_service.read_messages(messages, recipient_id: recipient_id)
end
def unread_conversation(conversation)
@ -122,7 +129,8 @@ class Channel::Whatsapp < ApplicationRecord
def received_messages(messages, conversation)
return unless provider_service.respond_to?(:received_messages)
provider_service.received_messages(conversation.contact.phone_number, messages)
recipient_id = conversation.contact.identifier || conversation.contact.phone_number
provider_service.received_messages(recipient_id, messages)
end
def on_whatsapp(phone_number)

View File

@ -3,14 +3,12 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
private
def raw_message_id
@raw_message[:key][:id]
def unwrap_ephemeral_message(msg)
msg.key?(:ephemeralMessage) ? msg.dig(:ephemeralMessage, :message) : msg
end
def sender_lid
return @raw_message[:key][:senderLid] if @raw_message[:key].key?(:senderLid)
@raw_message[:key][:remoteJid] if jid_type == 'lid'
def raw_message_id
@raw_message[:key][:id]
end
def incoming?
@ -42,7 +40,7 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize
msg = @raw_message[:message]
msg = unwrap_ephemeral_message(@raw_message[:message])
if msg.key?(:conversation) || msg.dig(:extendedTextMessage, :text).present?
'text'
elsif msg.key?(:imageMessage)
@ -72,22 +70,23 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end
def message_content # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
msg = unwrap_ephemeral_message(@raw_message[:message])
case message_type
when 'text'
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
msg[:conversation] || msg.dig(:extendedTextMessage, :text)
when 'image'
@raw_message.dig(:message, :imageMessage, :caption)
msg.dig(:imageMessage, :caption)
when 'video'
@raw_message.dig(:message, :videoMessage, :caption)
msg.dig(:videoMessage, :caption)
when 'file'
@raw_message.dig(:message, :documentMessage, :caption).presence ||
@raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :caption)
msg.dig(:documentMessage, :caption).presence ||
msg.dig(:documentWithCaptionMessage, :message, :documentMessage, :caption)
when 'reaction'
@raw_message.dig(:message, :reactionMessage, :text)
msg.dig(:reactionMessage, :text)
when 'contact'
# FIXME: Missing specs
display_name = @raw_message.dig(:message, :contactMessage, :displayName)
vcard = @raw_message.dig(:message, :contactMessage, :vcard)
display_name = msg.dig(:contactMessage, :displayName)
vcard = msg.dig(:contactMessage, :vcard)
match_phone_number = vcard&.match(/waid=(\d+)/)
return display_name unless match_phone_number
@ -97,6 +96,24 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end
end
def reply_to_message_id # rubocop:disable Metrics/CyclomaticComplexity
msg = unwrap_ephemeral_message(@raw_message[:message])
message_key = case message_type
when 'text' then :extendedTextMessage
when 'image' then :imageMessage
when 'sticker' then :stickerMessage
when 'audio' then :audioMessage
when 'video' then :videoMessage
when 'contact' then :contactMessage
when 'file'
context_info = msg.dig(:documentMessage, :contextInfo).presence ||
msg.dig(:documentWithCaptionMessage, :message, :documentMessage, :contextInfo)
return context_info&.dig(:stanzaId)
end
msg.dig(message_key, :contextInfo, :stanzaId) if message_key
end
def file_content_type
return :image if message_type.in?(%w[image sticker])
return :video if message_type.in?(%w[video video_note])
@ -106,28 +123,31 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end
def message_mimetype
msg = unwrap_ephemeral_message(@raw_message[:message])
case message_type
when 'image'
@raw_message.dig(:message, :imageMessage, :mimetype)
msg.dig(:imageMessage, :mimetype)
when 'sticker'
@raw_message.dig(:message, :stickerMessage, :mimetype)
msg.dig(:stickerMessage, :mimetype)
when 'video'
@raw_message.dig(:message, :videoMessage, :mimetype)
msg.dig(:videoMessage, :mimetype)
when 'audio'
@raw_message.dig(:message, :audioMessage, :mimetype)
msg.dig(:audioMessage, :mimetype)
when 'file'
@raw_message.dig(:message, :documentMessage, :mimetype).presence ||
@raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype)
msg.dig(:documentMessage, :mimetype).presence ||
msg.dig(:documentWithCaptionMessage, :message, :documentMessage, :mimetype)
end
end
def phone_number_from_jid
reference_field = jid_type == 'lid' ? :senderPn : :remoteJid
def extract_from_jid(type:)
addressing_mode = @raw_message[:key][:addressingMode]
reference_field = addressing_mode && addressing_mode != type ? :remoteJidAlt : :remoteJid
jid = @raw_message[:key][reference_field]
return unless jid
# NOTE: jid shape is `<user>_<agent>:<device>@<server>`
# https://github.com/WhiskeySockets/Baileys/blob/v6.7.16/src/WABinary/jid-utils.ts#L19
# https://github.com/WhiskeySockets/Baileys/blob/v7.0.0-rc.6/src/WABinary/jid-utils.ts#L52
jid.split('@').first.split(':').first.split('_').first
end
@ -136,12 +156,17 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
name = @raw_message[:verifiedBizName].presence || @raw_message[:pushName]
return name if name.presence && (self_message? || incoming?)
phone_number_from_jid
extract_from_jid(type: 'pn') || extract_from_jid(type: 'lid')
end
def self_message?
# TODO: Handle denormalized Brazilian phone numbers
phone_number_from_jid == inbox.channel.phone_number.delete('+')
normalize_phone_number(extract_from_jid(type: 'pn')) == normalize_phone_number(inbox.channel.phone_number.delete('+'))
end
def normalize_phone_number(phone_number)
return unless phone_number
Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer.new.normalize(phone_number)
end
def ignore_message?
@ -162,22 +187,23 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
# TODO: Current logic will never update the contact avatar if their profile picture changes on WhatsApp.
return if @contact.avatar.attached?
profile_pic_url = fetch_profile_picture_url(phone_number_from_jid)
phone = extract_from_jid(type: 'pn')
profile_pic_url = fetch_profile_picture_url(phone) if phone
::Avatar::AvatarFromUrlJob.perform_later(@contact, profile_pic_url) if profile_pic_url
end
def message_under_process?
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{raw_message_id}")
Redis::Alfred.get(key)
end
def cache_message_source_id_in_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{raw_message_id}")
::Redis::Alfred.setex(key, true)
end
def clear_message_source_id_from_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{raw_message_id}")
::Redis::Alfred.delete(key)
end
end

View File

@ -1,4 +1,4 @@
module Whatsapp::BaileysHandlers::MessagesUpsert
module Whatsapp::BaileysHandlers::MessagesUpsert # rubocop:disable Metrics/ModuleLength
include Whatsapp::BaileysHandlers::Helpers
include BaileysHelper
@ -20,9 +20,9 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
end
end
def handle_message # rubocop:disable Metrics/CyclomaticComplexity
def handle_message
return unless %w[lid user].include?(jid_type)
return if jid_type == 'lid' && !phone_number_from_jid
return unless extract_from_jid(type: 'lid')
return if ignore_message?
return if find_message_by_source_id(raw_message_id) || message_under_process?
@ -42,27 +42,51 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
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(
# FIXME: update the source_id to use sender LID
source_id: phone_number_from_jid,
source_id: source_id,
inbox: inbox,
contact_attributes: { name: contact_name, phone_number: "+#{phone_number_from_jid}", identifier: sender_lid }
contact_attributes: { name: contact_name, phone_number: ("+#{phone}" if phone), identifier: identifier }
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
update_contact_information
update_contact_info(phone, source_id, identifier)
end
def update_contact_information
updates = {}
updates[:identifier] = sender_lid if @contact.identifier.blank?
updates[:phone_number] = "+#{phone_number_from_jid}" if @contact.phone_number.blank?
updates[:name] = contact_name if @contact.name == phone_number_from_jid || @contact.name == sender_lid
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)
@contact.update!(updates) if updates.present?
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
@ -91,10 +115,13 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
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] = @raw_message.dig(:message, :reactionMessage, :key, :id)
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
@ -104,13 +131,14 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
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 @raw_message.dig(:message, :audioMessage, :ptt)
attachment.meta = { is_recorded_audio: true } if msg.dig(:audioMessage, :ptt)
rescue Down::Error => e
@message.update!(is_unsupported: true)
@ -122,7 +150,8 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
end
def filename
filename = @raw_message.dig(:message, :documentMessage, :fileName)
msg = unwrap_ephemeral_message(@raw_message[:message])
filename = msg.dig(:documentMessage, :fileName)
return filename if filename.present?
ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present?

View File

@ -55,9 +55,9 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true
end
def send_message(phone_number, message)
def send_message(recipient_id, message)
@message = message
@phone_number = phone_number
@recipient_id = recipient_id
if message.content_attributes[:is_reaction]
@message_content = reaction_message_content
@ -94,8 +94,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
process_response(response)
end
def toggle_typing_status(typing_status, phone_number:, **)
@phone_number = phone_number
def toggle_typing_status(typing_status, recipient_id:, **)
@recipient_id = recipient_id
status_map = {
Events::Types::CONVERSATION_TYPING_ON => 'composing',
Events::Types::CONVERSATION_RECORDING => 'recording',
@ -136,8 +136,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true
end
def read_messages(messages, phone_number:, **)
@phone_number = phone_number
def read_messages(messages, recipient_id:, **)
@recipient_id = recipient_id
response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/read-messages",
@ -158,8 +158,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true
end
def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength
@phone_number = phone_number
def unread_message(recipient_id, message) # rubocop:disable Metrics/MethodLength
@recipient_id = recipient_id
response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/chat-modify",
@ -185,8 +185,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true
end
def received_messages(phone_number, messages)
@phone_number = phone_number
def received_messages(recipient_id, messages)
@recipient_id = recipient_id
response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-receipts",
@ -220,8 +220,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
response.parsed_response
end
def on_whatsapp(phone_number)
@phone_number = phone_number
def on_whatsapp(recipient_id)
@recipient_id = recipient_id
response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/on-whatsapp",
@ -233,7 +233,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
raise ProviderUnavailableError unless process_response(response)
response.parsed_response&.first || { 'jid' => remote_jid, 'exists' => false, 'lid' => nil }
response.parsed_response&.first || { 'jid' => remote_jid, 'exists' => false }
end
private
@ -303,9 +303,10 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
response.success?
end
# FIXME: Make this a function with argument, instead of using instance variable
def remote_jid
"#{@phone_number.delete('+')}@s.whatsapp.net"
return @recipient_id if @recipient_id.ends_with?('@lid')
"#{@recipient_id.delete('+')}@s.whatsapp.net"
end
def update_external_created_at(response)

View File

@ -80,7 +80,7 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic
response.parsed_response['value']
end
def read_messages(messages, phone_number:, **)
def read_messages(messages, recipient_id:, **)
# NOTE: Z-API will handle marking previous messages as read.
last_message = messages.last
@ -88,7 +88,7 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic
"#{api_instance_path_with_token}/read-message",
headers: api_headers,
body: {
phone: phone_number.delete('+'),
phone: recipient_id.delete('+'),
messageId: last_message.source_id
}.to_json
)

View File

@ -51,14 +51,10 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
end
def recipient_id
return message.conversation.contact_inbox.source_id unless channel.provider == 'zapi'
return message.conversation.contact_inbox.source_id unless %w[baileys zapi].include?(channel.provider)
if message.conversation.contact.phone_number.present?
message.conversation.contact.phone_number.gsub(/[^\d]/, '')
else
# NOTE: `identifier` is the WhatsApp LID format. See `Whatsapp::ZapiHandlers::ReceivedCallback#set_contact`
message.conversation.contact.identifier
end
# NOTE: `identifier` must be in the WhatsApp LID format
message.conversation.contact.phone_number&.gsub(/[^\d]/, '') || message.conversation.contact.identifier
end
def template_params

View File

@ -12,17 +12,17 @@ module Whatsapp::ZapiHandlers::Helpers
end
def cache_message_source_id_in_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{raw_message_id}")
Redis::Alfred.setex(key, true)
end
def clear_message_source_id_from_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{raw_message_id}")
Redis::Alfred.delete(key)
end
def message_under_process?
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: raw_message_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{raw_message_id}")
Redis::Alfred.get(key)
end
end

View File

@ -184,9 +184,10 @@ RSpec.describe Channel::Whatsapp do
let(:conversation) { create(:conversation) }
it 'calls provider service method' do
message = create(:message, conversation: conversation)
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
allow(provider_double).to receive(:toggle_typing_status)
.with(Events::Types::CONVERSATION_TYPING_ON, phone_number: conversation.contact.phone_number)
.with(Events::Types::CONVERSATION_TYPING_ON, recipient_id: conversation.contact.identifier, last_message: message)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -242,11 +243,11 @@ RSpec.describe Channel::Whatsapp do
create(:channel_whatsapp, provider: 'baileys', provider_config: { mark_as_read: true }, validate_provider_config: false, sync_templates: false)
end
let(:conversation) { create(:conversation) }
let(:message) { create(:message) }
let(:message) { create(:message, conversation: conversation) }
it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil)
allow(provider_double).to receive(:read_messages).with([message], phone_number: conversation.contact.phone_number)
allow(provider_double).to receive(:read_messages).with([message], recipient_id: conversation.contact.identifier)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -259,7 +260,7 @@ RSpec.describe Channel::Whatsapp do
it 'call method when the provider config mark_as_read is nil' do
channel.update!(provider_config: {})
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil)
allow(provider_double).to receive(:read_messages).with([message], phone_number: conversation.contact.phone_number)
allow(provider_double).to receive(:read_messages).with([message], recipient_id: conversation.contact.identifier)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -332,7 +333,7 @@ RSpec.describe Channel::Whatsapp do
it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, received_messages: nil)
allow(provider_double).to receive(:received_messages).with(conversation.contact.phone_number, messages)
allow(provider_double).to receive(:received_messages).with(conversation.contact.identifier, messages)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -415,4 +416,51 @@ RSpec.describe Channel::Whatsapp do
end
end
end
describe '#provider_connection_data' do
let(:channel) do
create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false,
provider_connection: {
'connection' => 'open',
'qr_data_url' => 'data:image/png;base64,test',
'error' => 'some_error'
})
end
context 'when user is an administrator' do
it 'includes qr_data_url and error in the response' do
account_user = create(:account_user, account: channel.account, role: :administrator)
allow(Current).to receive(:account_user).and_return(account_user)
data = channel.provider_connection_data
expect(data).to eq({
connection: 'open',
qr_data_url: 'data:image/png;base64,test',
error: 'some_error'
})
end
end
context 'when user is not an administrator' do
it 'excludes qr_data_url and error from the response' do
account_user = create(:account_user, account: channel.account, role: :agent)
allow(Current).to receive(:account_user).and_return(account_user)
data = channel.provider_connection_data
expect(data).to eq({ connection: 'open' })
end
end
context 'when Current.account_user is nil' do
it 'excludes qr_data_url and error from the response' do
allow(Current).to receive(:account_user).and_return(nil)
data = channel.provider_connection_data
expect(data).to eq({ connection: 'open' })
end
end
end
end

View File

@ -0,0 +1,319 @@
require 'rails_helper'
describe Whatsapp::BaileysHandlers::MessagesUpsert do
let(:webhook_verify_token) { 'valid_token' }
let!(:whatsapp_channel) do
create(:channel_whatsapp,
provider: 'baileys',
provider_config: { webhook_verify_token: webhook_verify_token },
validate_provider_config: false,
received_messages: false)
end
let(:inbox) { whatsapp_channel.inbox }
let(:timestamp) { Time.current.to_i }
before do
stub_request(:get, /profile-picture-url/)
.to_return(
status: 200,
body: { data: { profilePictureUrl: nil } }.to_json
)
end
describe '#update_existing_contact_inbox' do
context 'when updating contact inbox with LID information' do
let(:phone) { '5511912345678' }
let(:lid) { '12345678' }
let(:source_id) { lid }
let(:identifier) { "#{lid}@lid" }
context 'when there is no conflict' do
it 'updates existing contact_inbox source_id from phone to LID' do
contact = create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: nil)
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: phone)
raw_message = {
key: { id: 'msg_123', remoteJid: "#{lid}@lid", remoteJidAlt: "#{phone}@s.whatsapp.net", fromMe: false, addressingMode: 'lid' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello' }
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.not_to raise_error
expect(contact_inbox.reload.source_id).to eq(source_id)
expect(contact.reload.identifier).to eq(identifier)
expect(contact.phone_number).to eq("+#{phone}")
end
end
context 'when identifier is already taken by a different contact (race condition)' do
it 'does not raise validation error and skips the update' do
original_contact = create(:contact, account: inbox.account, phone_number: nil, identifier: nil, name: 'Original Contact')
original_contact_inbox = create(:contact_inbox, inbox: inbox, contact: original_contact, source_id: phone)
conflicting_contact = create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: identifier,
name: 'Conflicting Contact')
create(:contact_inbox, inbox: inbox, contact: conflicting_contact, source_id: source_id)
raw_message = {
key: { id: 'msg_123', remoteJid: "#{lid}@lid", remoteJidAlt: "#{phone}@s.whatsapp.net", fromMe: false, addressingMode: 'lid' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello' }
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.not_to raise_error
expect(original_contact_inbox.reload.source_id).to eq(phone)
expect(original_contact.reload.identifier).to be_nil
message = inbox.messages.last
expect(message).to be_present
expect(message.sender).to eq(conflicting_contact)
expect(message.conversation.contact).to eq(conflicting_contact)
end
end
context 'when phone number is already taken by a different contact (race condition)' do
it 'does not raise validation error and skips the update' do
original_contact = create(:contact, account: inbox.account, phone_number: nil, identifier: nil)
create(:contact_inbox, inbox: inbox, contact: original_contact, source_id: phone)
different_lid = '87654321'
different_identifier = "#{different_lid}@lid"
conflicting_contact = create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: different_identifier)
create(:contact_inbox, inbox: inbox, contact: conflicting_contact, source_id: different_lid)
raw_message = {
key: { id: 'msg_123', remoteJid: "#{lid}@lid", remoteJidAlt: "#{phone}@s.whatsapp.net", fromMe: false, addressingMode: 'lid' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello' }
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.not_to raise_error
expect(original_contact.reload.phone_number).to be_nil
expect(original_contact.identifier).to be_nil
end
end
context 'when LID contact_inbox already exists (race condition)' do
it 'does not raise unique constraint error and skips the update' do
original_contact = create(:contact, account: inbox.account, phone_number: nil, identifier: nil)
create(:contact_inbox, inbox: inbox, contact: original_contact, source_id: phone)
lid_contact = create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: identifier)
create(:contact_inbox, inbox: inbox, contact: lid_contact, source_id: source_id)
raw_message = {
key: { id: 'msg_123', remoteJid: "#{lid}@lid", remoteJidAlt: "#{phone}@s.whatsapp.net", fromMe: false, addressingMode: 'lid' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello' }
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.not_to raise_error
message = inbox.messages.last
expect(message).to be_present
expect(message.sender).to eq(lid_contact)
end
end
context 'when updating the same contact (no conflict)' do
it 'successfully updates the contact' do
contact = create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: nil)
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: phone)
raw_message = {
key: { id: 'msg_123', remoteJid: "#{lid}@lid", remoteJidAlt: "#{phone}@s.whatsapp.net", fromMe: false, addressingMode: 'lid' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello' }
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.not_to raise_error
expect(contact_inbox.reload.source_id).to eq(source_id)
expect(contact.reload.identifier).to eq(identifier)
expect(contact.phone_number).to eq("+#{phone}")
end
end
end
end
describe 'ephemeral message handling' do
let(:phone) { '5511912345678' }
let(:lid) { '12345678' }
context 'when receiving an ephemeral text message' do
it 'correctly unwraps and processes the message' do
raw_message = {
key: { id: 'msg_ephemeral_123', remoteJid: "#{phone}@s.whatsapp.net", remoteJidAlt: "#{lid}@lid", fromMe: false,
addressingMode: 'pn' },
pushName: 'Gabriel',
messageTimestamp: timestamp,
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2
},
ephemeralMessage: {
message: {
extendedTextMessage: {
text: 'This is a disappearing message',
contextInfo: {
expiration: 604_800,
disappearingMode: { initiator: 0 }
}
}
}
}
}
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.to change(inbox.messages, :count).by(1)
message = inbox.messages.last
expect(message.content).to eq('This is a disappearing message')
expect(message.message_type).to eq('incoming')
expect(message.is_unsupported).to be_falsey
end
end
context 'when receiving an ephemeral image message' do
it 'correctly unwraps and processes the message with media' do
raw_message = {
key: { id: 'msg_ephemeral_image_123', remoteJid: "#{phone}@s.whatsapp.net", remoteJidAlt: "#{lid}@lid", fromMe: false,
addressingMode: 'pn' },
pushName: 'Gabriel',
messageTimestamp: timestamp,
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2
},
ephemeralMessage: {
message: {
imageMessage: {
caption: 'Check this out',
mimetype: 'image/jpeg',
url: 'https://example.com/image.jpg'
}
}
}
}
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
stub_request(:get, whatsapp_channel.media_url('msg_ephemeral_image_123'))
.to_return(status: 200, body: 'fake image data')
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.to change(inbox.messages, :count).by(1)
message = inbox.messages.last
expect(message.content).to eq('Check this out')
expect(message.message_type).to eq('incoming')
expect(message.is_unsupported).to be_falsey
expect(message.attachments.count).to eq(1)
end
end
context 'when receiving an ephemeral reaction message' do
it 'correctly unwraps and processes the reaction' do
# First create the original message
contact = create(:contact, account: inbox.account, phone_number: "+#{phone}", identifier: "#{lid}@lid")
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: lid)
conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
original_message = create(:message, inbox: inbox, conversation: conversation, source_id: 'original_msg_id')
raw_message = {
key: { id: 'msg_ephemeral_reaction_123', remoteJid: "#{phone}@s.whatsapp.net", remoteJidAlt: "#{lid}@lid", fromMe: false,
addressingMode: 'pn' },
pushName: 'Gabriel',
messageTimestamp: timestamp,
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2
},
ephemeralMessage: {
message: {
reactionMessage: {
text: '👍',
key: { id: 'original_msg_id' }
}
}
}
}
}
params = {
webhookVerifyToken: webhook_verify_token,
event: 'messages.upsert',
data: { type: 'notify', messages: [raw_message] }
}
expect do
Whatsapp::IncomingMessageBaileysService.new(inbox: inbox, params: params).perform
end.to change(conversation.messages, :count).by(1)
reaction = conversation.messages.last
expect(reaction.content).to eq('👍')
expect(reaction.message_type).to eq('incoming')
expect(reaction.content_attributes['is_reaction']).to be_truthy
expect(reaction.in_reply_to).to eq(original_message.id)
expect(reaction.in_reply_to_external_id).to eq(original_message.source_id)
end
end
end
end

View File

@ -171,7 +171,7 @@ describe Whatsapp::IncomingMessageBaileysService do
let(:timestamp) { Time.current.to_i }
let(:raw_message) do
{
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false, senderLid: '12345678@lid' },
key: { id: 'msg_123', remoteJid: '12345678@lid', remoteJidAlt: '5511912345678@s.whatsapp.net', fromMe: false, addressingMode: 'lid' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello from Baileys' }
@ -360,23 +360,6 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(message.message_type).to eq('outgoing')
end
it 'creates an outgoing self message' do
self_jid = "#{whatsapp_channel.phone_number.delete('+')}@s.whatsapp.net"
raw_message[:key][:remoteJid] = self_jid
raw_message[:key][:fromMe] = true
create(:account_user, account: inbox.account)
described_class.new(inbox: inbox, params: params).perform
conversation = inbox.conversations.last
message = conversation.messages.last
expect(message).to be_present
expect(message.content).to eq('Hello from Baileys')
expect(conversation.contact.name).to eq('John Doe')
expect(message.message_type).to eq('outgoing')
end
it 'updates the contact name when name is the phone number and message has a pushName' do
create(:contact, account: inbox.account, name: '5511912345678')
@ -396,6 +379,23 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(conversation.contact.name).to eq('Verified John')
end
it 'creates contact with LID only when phone number is not present' do
raw_message[:key][:remoteJid] = '87654321@lid'
raw_message[:key][:remoteJidAlt] = nil
described_class.new(inbox: inbox, params: params).perform
conversation = inbox.conversations.last
contact = conversation.contact
contact_inbox = conversation.contact_inbox
expect(contact).to be_present
expect(contact.phone_number).to be_nil
expect(contact.identifier).to eq('87654321@lid')
expect(contact.name).to eq('John Doe')
expect(contact_inbox.source_id).to eq('87654321')
end
it 'creates contact with phone number as name on outgoing message' do
raw_message[:key][:fromMe] = true
create(:account_user, account: inbox.account)
@ -416,9 +416,23 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(conversation.contact.name).to eq('5511912345678')
end
it 'migrates existing phone-based contact inbox to LID-based when receiving message with LID' do
contact = create(:contact, account: inbox.account, name: 'Existing Contact', phone_number: '+5511912345678')
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
described_class.new(inbox: inbox, params: params).perform
contact_inbox.reload
contact.reload
expect(contact_inbox.source_id).to eq('12345678')
expect(contact.identifier).to eq('12345678@lid')
expect(contact.phone_number).to eq('+5511912345678')
end
it 'creates a message on an existing conversation' do
contact = create(:contact, account: inbox.account, name: 'John Doe')
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
existing_conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
described_class.new(inbox: inbox, params: params).perform
@ -476,7 +490,7 @@ describe Whatsapp::IncomingMessageBaileysService do
context 'when message type is reaction' do
let!(:message) do
contact = create(:contact, account: inbox.account, name: '5511912345678')
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
create(:message, inbox: inbox, conversation: conversation, source_id: 'msg_123')
end
@ -485,7 +499,7 @@ describe Whatsapp::IncomingMessageBaileysService do
raw_message[:key][:id] = 'reaction_123'
raw_message[:message] = {
reactionMessage: {
key: { remoteJid: '5511912345678@s.whatsapp.net', fromMe: true, id: 'msg_123' },
key: { remoteJid: '12345678@lid', fromMe: true, id: 'msg_123' },
text: '👍'
}
}
@ -502,7 +516,7 @@ describe Whatsapp::IncomingMessageBaileysService do
raw_message[:key][:id] = 'reaction_123'
raw_message[:message] = {
reactionMessage: {
key: { remoteJid: '5511912345678@s.whatsapp.net', fromMe: true, id: 'msg_123' },
key: { remoteJid: '12345678@lid', fromMe: true, id: 'msg_123' },
text: ''
}
}
@ -522,7 +536,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify',
messages: [
{
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
key: { id: 'msg_123', remoteJid: '12345678@lid', remoteJidAlt: '5511912345678@s.whatsapp.net', fromMe: false,
addressingMode: 'lid' },
message: { imageMessage: { caption: 'Hello from Baileys', mimetype: 'image/png' } },
pushName: 'John Doe'
}
@ -574,7 +589,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify',
messages: [
{
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
key: { id: 'msg_123', remoteJid: '12345678@lid', remoteJidAlt: '5511912345678@s.whatsapp.net', fromMe: false,
addressingMode: 'lid' },
message: { videoMessage: { caption: 'Hello from Baileys', mimetype: 'video/mp4' } },
pushName: 'John Doe'
}
@ -617,7 +633,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify',
messages: [
{
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
key: { id: 'msg_123', remoteJid: '12345678@lid', remoteJidAlt: '5511912345678@s.whatsapp.net', fromMe: false,
addressingMode: 'lid' },
message: { documentMessage: { fileName: filename } },
pushName: 'John Doe'
}
@ -668,7 +685,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify',
messages: [
{
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
key: { id: 'msg_123', remoteJid: '12345678@lid', remoteJidAlt: '5511912345678@s.whatsapp.net', fromMe: false,
addressingMode: 'lid' },
message: { audioMessage: { mimetype: 'audio/opus' } },
pushName: 'John Doe'
}
@ -699,7 +717,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify',
messages: [
{
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
key: { id: 'msg_123', remoteJid: '12345678@lid', remoteJidAlt: '5511912345678@s.whatsapp.net', fromMe: false,
addressingMode: 'lid' },
message: { stickerMessage: { mimetype: 'image/png' } },
pushName: 'John Doe'
}
@ -724,13 +743,13 @@ describe Whatsapp::IncomingMessageBaileysService do
context 'when processing multiple messages' do
it 'creates separate contacts and conversations for each message' do
raw_message1 = {
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', remoteJidAlt: '12345678@lid', fromMe: false, addressingMode: 'pn' },
pushName: 'John Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello from Baileys' }
}
raw_message2 = {
key: { id: 'msg_124', remoteJid: '5511987654321@s.whatsapp.net', fromMe: false },
key: { id: 'msg_124', remoteJid: '5511987654321@s.whatsapp.net', remoteJidAlt: '87654321@lid', fromMe: false, addressingMode: 'pn' },
pushName: 'Jane Doe',
messageTimestamp: timestamp,
message: { conversation: 'Hello from another user' }
@ -764,9 +783,10 @@ describe Whatsapp::IncomingMessageBaileysService do
end
context 'when jid type is lid' do
it 'processes the message with phone number from lid jid type' do
raw_message[:key][:remoteJid] = '12345678@lid'
raw_message[:key][:senderPn] = '5511912345678@s.whatsapp.net'
it 'processes the message with phone number from addressingMode pn' do
raw_message[:key][:remoteJid] = '5511912345678@s.whatsapp.net'
raw_message[:key][:remoteJidAlt] = '12345678@lid'
raw_message[:key][:addressingMode] = 'pn'
described_class.new(inbox: inbox, params: params).perform
@ -778,82 +798,90 @@ describe Whatsapp::IncomingMessageBaileysService do
end
end
it 'skips message if senderPn is not present' do
raw_message[:key][:remoteJid] = '12345678@lid'
it 'skips message if LID cannot be extracted' do
raw_message[:key][:remoteJid] = 'invalid_jid'
raw_message[:key][:remoteJidAlt] = nil
raw_message[:key][:addressingMode] = 'lid'
described_class.new(inbox: inbox, params: params).perform
expect(inbox.conversations).to be_empty
end
context 'when handling senderLid field' do
it 'creates contact with identifier from senderLid' do
context 'when handling LID field' do
it 'creates contact with LID as source_id and identifier' do
described_class.new(inbox: inbox, params: params).perform
contact = inbox.contacts.last
contact_inbox = inbox.contact_inboxes.last
expect(contact.identifier).to eq('12345678@lid')
expect(contact_inbox.source_id).to eq('12345678')
end
it 'updates contact identifier for existing contacts without identifier' do
it 'updates existing contact_inbox from phone to LID source_id' do
contact = create(:contact, account: inbox.account, phone_number: '+5511912345678', identifier: nil)
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
described_class.new(inbox: inbox, params: params).perform
contact_inbox = inbox.contact_inboxes.find_by(contact: contact)
expect(contact_inbox.source_id).to eq('12345678')
expect(contact.reload.identifier).to eq('12345678@lid')
expect(contact.phone_number).to eq('+5511912345678')
end
it 'does not update contact identifier if already present' do
contact = create(:contact, account: inbox.account, phone_number: '+5511912345678', identifier: 'existing@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
it 'does not update contact_inbox if source_id is already LID' do
contact = create(:contact, account: inbox.account, phone_number: '+5511912345678', identifier: '12345678@lid')
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.identifier).to eq('existing@lid')
expect(contact_inbox.reload.source_id).to eq('12345678')
expect(contact.reload.identifier).to eq('12345678@lid')
end
end
context 'when updating contact information' do
it 'updates contact phone number if blank' do
contact = create(:contact, account: inbox.account, phone_number: nil, identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.phone_number).to eq('+5511912345678')
end
it 'does not update contact phone number if already present' do
contact = create(:contact, account: inbox.account, phone_number: '+9999999999', identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.phone_number).to eq('+9999999999')
end
it 'updates contact name if it matches phone number' do
contact = create(:contact, account: inbox.account, name: '5511912345678', phone_number: '+5511912345678', identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.name).to eq('John Doe')
end
it 'updates contact name if it matches sender LID' do
it 'updates contact name if it matches LID source_id' do
contact = create(:contact, account: inbox.account, name: '12345678', phone_number: '+5511912345678', identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.name).to eq('John Doe')
end
it 'updates contact name if it matches identifier' do
contact = create(:contact, account: inbox.account, name: '12345678@lid', phone_number: '+5511912345678', identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.name).to eq('John Doe')
end
it 'does not update contact name if it is different from phone number and sender LID' do
it 'does not update contact name if it is different from phone number, source_id, and identifier' do
contact = create(:contact, account: inbox.account, name: 'Existing Name', phone_number: '+5511912345678', identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform
@ -958,7 +986,7 @@ describe Whatsapp::IncomingMessageBaileysService do
end
def format_message_source_key(message_id)
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{message_id}")
end
def stub_download

View File

@ -477,7 +477,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 200, body: '', headers: {})
result = service.read_messages([message], phone_number: test_send_phone_number)
result = service.read_messages([message], recipient_id: test_send_phone_number)
expect(result).to be(true)
end
@ -495,7 +495,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
.to_return(status: 200)
expect do
service.read_messages([message], phone_number: test_send_phone_number)
service.read_messages([message], recipient_id: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end
end
@ -605,7 +605,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
)
.to_return(status: 200)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, phone_number: test_send_phone_number)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, recipient_id: test_send_phone_number)
expect(request).to have_been_requested
end
@ -621,7 +621,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
)
.to_return(status: 200)
service.toggle_typing_status(Events::Types::CONVERSATION_RECORDING, phone_number: test_send_phone_number)
service.toggle_typing_status(Events::Types::CONVERSATION_RECORDING, recipient_id: test_send_phone_number)
expect(request).to have_been_requested
end
@ -637,7 +637,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
)
.to_return(status: 200)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_OFF, phone_number: test_send_phone_number)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_OFF, recipient_id: test_send_phone_number)
expect(request).to have_been_requested
end
@ -664,7 +664,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
allow(Rails.logger).to receive(:error)
expect do
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, phone_number: test_send_phone_number)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, recipient_id: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
@ -730,12 +730,12 @@ describe Whatsapp::Providers::WhatsappBaileysService do
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: [{ jid: "#{phone_number.delete('+')}@s.whatsapp.net", exists: true, lid: '123@lid' }].to_json
body: [{ jid: "#{phone_number.delete('+')}@s.whatsapp.net", exists: true }].to_json
)
response = service.on_whatsapp(phone_number)
expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => true, 'lid' => '123@lid' })
expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => true })
end
it 'returns default check response' do
@ -749,7 +749,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
response = service.on_whatsapp(phone_number)
expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => false, 'lid' => nil })
expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => false })
end
end
@ -916,7 +916,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
expect do
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, phone_number: test_send_phone_number)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, recipient_id: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')

View File

@ -208,7 +208,7 @@ describe Whatsapp::Providers::WhatsappZapiService do
)
.to_return(status: 200)
result = service.read_messages(messages, phone_number: "+#{test_send_phone_number}")
result = service.read_messages(messages, recipient_id: "+#{test_send_phone_number}")
expect(result).to be(true)
end
@ -223,7 +223,7 @@ describe Whatsapp::Providers::WhatsappZapiService do
allow(Rails.logger).to receive(:error)
expect do
service.read_messages(messages, phone_number: "+#{test_send_phone_number}")
service.read_messages(messages, recipient_id: "+#{test_send_phone_number}")
end.to raise_error(Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError)
end
end

View File

@ -397,8 +397,9 @@ describe Whatsapp::SendOnWhatsappService do
end
it 'calls channel.send_message if channel is not locked on outgoing message' do
conversation.contact.update!(phone_number: '+123456789')
message = create(:message, message_type: :outgoing, content: 'test', conversation: conversation)
allow(whatsapp_channel).to receive(:send_message).with(conversation.contact_inbox.source_id, message).and_return('123456789')
allow(whatsapp_channel).to receive(:send_message).with('123456789', message).and_return('123456789')
described_class.new(message: message).perform

View File

@ -1217,6 +1217,6 @@ describe Whatsapp::ZapiHandlers::ReceivedCallback do
private
def format_message_source_key(message_id)
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: "#{inbox.id}_#{message_id}")
end
end