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

View File

@ -5,9 +5,8 @@ permissions:
packages: write packages: write
on: on:
push: release:
tags: types: [released]
- '*'
workflow_dispatch: workflow_dispatch:
env: env:
@ -25,10 +24,12 @@ jobs:
runner: ubuntu-22.04-arm runner: ubuntu-22.04-arm
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
env: env:
GIT_REF: ${{ github.head_ref || github.ref_name }} GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.ref }}
- name: Prepare - name: Prepare
run: | run: |
@ -62,7 +63,7 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - 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 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@ -76,7 +77,7 @@ jobs:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} push: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
tags: | tags: |
${{ env.GITHUB_REPO }}:${{ env.SANITIZED_REF }} ${{ env.GITHUB_REPO }}:${{ env.SANITIZED_REF }}
${{ env.GITHUB_REPO }}:latest ${{ env.GITHUB_REPO }}:latest
@ -120,7 +121,7 @@ jobs:
- name: Create manifest list and push - name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests working-directory: ${{ runner.temp }}/digests
env: env:
GIT_REF: ${{ github.head_ref || github.ref_name }} GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: | run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g') SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
docker buildx imagetools create \ docker buildx imagetools create \
@ -130,7 +131,7 @@ jobs:
- name: Inspect image - name: Inspect image
env: env:
GIT_REF: ${{ github.head_ref || github.ref_name }} GIT_REF: ${{ github.event.release.tag_name || github.ref_name }}
run: | run: |
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g') SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
REPO="ghcr.io/${{ github.repository }}" 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:) def toggle_typing_status(typing_status, conversation:)
return unless provider_service.respond_to?(:toggle_typing_status) return unless provider_service.respond_to?(:toggle_typing_status)
recipient_id = conversation.contact.identifier || conversation.contact.phone_number
last_message = conversation.messages.last 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 end
def update_presence(status) 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`. # 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 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 end
def unread_conversation(conversation) def unread_conversation(conversation)
@ -122,7 +129,8 @@ class Channel::Whatsapp < ApplicationRecord
def received_messages(messages, conversation) def received_messages(messages, conversation)
return unless provider_service.respond_to?(:received_messages) 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 end
def on_whatsapp(phone_number) def on_whatsapp(phone_number)

View File

@ -3,14 +3,12 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
private private
def raw_message_id def unwrap_ephemeral_message(msg)
@raw_message[:key][:id] msg.key?(:ephemeralMessage) ? msg.dig(:ephemeralMessage, :message) : msg
end end
def sender_lid def raw_message_id
return @raw_message[:key][:senderLid] if @raw_message[:key].key?(:senderLid) @raw_message[:key][:id]
@raw_message[:key][:remoteJid] if jid_type == 'lid'
end end
def incoming? def incoming?
@ -42,7 +40,7 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end end
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize 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? if msg.key?(:conversation) || msg.dig(:extendedTextMessage, :text).present?
'text' 'text'
elsif msg.key?(:imageMessage) elsif msg.key?(:imageMessage)
@ -72,22 +70,23 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end end
def message_content # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength def message_content # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
msg = unwrap_ephemeral_message(@raw_message[:message])
case message_type case message_type
when 'text' when 'text'
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text) msg[:conversation] || msg.dig(:extendedTextMessage, :text)
when 'image' when 'image'
@raw_message.dig(:message, :imageMessage, :caption) msg.dig(:imageMessage, :caption)
when 'video' when 'video'
@raw_message.dig(:message, :videoMessage, :caption) msg.dig(:videoMessage, :caption)
when 'file' when 'file'
@raw_message.dig(:message, :documentMessage, :caption).presence || msg.dig(:documentMessage, :caption).presence ||
@raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :caption) msg.dig(:documentWithCaptionMessage, :message, :documentMessage, :caption)
when 'reaction' when 'reaction'
@raw_message.dig(:message, :reactionMessage, :text) msg.dig(:reactionMessage, :text)
when 'contact' when 'contact'
# FIXME: Missing specs # FIXME: Missing specs
display_name = @raw_message.dig(:message, :contactMessage, :displayName) display_name = msg.dig(:contactMessage, :displayName)
vcard = @raw_message.dig(:message, :contactMessage, :vcard) vcard = msg.dig(:contactMessage, :vcard)
match_phone_number = vcard&.match(/waid=(\d+)/) match_phone_number = vcard&.match(/waid=(\d+)/)
return display_name unless match_phone_number return display_name unless match_phone_number
@ -97,6 +96,24 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end end
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 def file_content_type
return :image if message_type.in?(%w[image sticker]) return :image if message_type.in?(%w[image sticker])
return :video if message_type.in?(%w[video video_note]) return :video if message_type.in?(%w[video video_note])
@ -106,28 +123,31 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
end end
def message_mimetype def message_mimetype
msg = unwrap_ephemeral_message(@raw_message[:message])
case message_type case message_type
when 'image' when 'image'
@raw_message.dig(:message, :imageMessage, :mimetype) msg.dig(:imageMessage, :mimetype)
when 'sticker' when 'sticker'
@raw_message.dig(:message, :stickerMessage, :mimetype) msg.dig(:stickerMessage, :mimetype)
when 'video' when 'video'
@raw_message.dig(:message, :videoMessage, :mimetype) msg.dig(:videoMessage, :mimetype)
when 'audio' when 'audio'
@raw_message.dig(:message, :audioMessage, :mimetype) msg.dig(:audioMessage, :mimetype)
when 'file' when 'file'
@raw_message.dig(:message, :documentMessage, :mimetype).presence || msg.dig(:documentMessage, :mimetype).presence ||
@raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype) msg.dig(:documentWithCaptionMessage, :message, :documentMessage, :mimetype)
end end
end end
def phone_number_from_jid def extract_from_jid(type:)
reference_field = jid_type == 'lid' ? :senderPn : :remoteJid addressing_mode = @raw_message[:key][:addressingMode]
reference_field = addressing_mode && addressing_mode != type ? :remoteJidAlt : :remoteJid
jid = @raw_message[:key][reference_field] jid = @raw_message[:key][reference_field]
return unless jid return unless jid
# NOTE: jid shape is `<user>_<agent>:<device>@<server>` # 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 jid.split('@').first.split(':').first.split('_').first
end end
@ -136,12 +156,17 @@ module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
name = @raw_message[:verifiedBizName].presence || @raw_message[:pushName] name = @raw_message[:verifiedBizName].presence || @raw_message[:pushName]
return name if name.presence && (self_message? || incoming?) return name if name.presence && (self_message? || incoming?)
phone_number_from_jid extract_from_jid(type: 'pn') || extract_from_jid(type: 'lid')
end end
def self_message? def self_message?
# TODO: Handle denormalized Brazilian phone numbers normalize_phone_number(extract_from_jid(type: 'pn')) == normalize_phone_number(inbox.channel.phone_number.delete('+'))
phone_number_from_jid == inbox.channel.phone_number.delete('+') end
def normalize_phone_number(phone_number)
return unless phone_number
Whatsapp::PhoneNormalizers::BrazilPhoneNormalizer.new.normalize(phone_number)
end end
def ignore_message? 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. # TODO: Current logic will never update the contact avatar if their profile picture changes on WhatsApp.
return if @contact.avatar.attached? 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 ::Avatar::AvatarFromUrlJob.perform_later(@contact, profile_pic_url) if profile_pic_url
end end
def message_under_process? 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) Redis::Alfred.get(key)
end end
def cache_message_source_id_in_redis 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) ::Redis::Alfred.setex(key, true)
end end
def clear_message_source_id_from_redis 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) ::Redis::Alfred.delete(key)
end end
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 Whatsapp::BaileysHandlers::Helpers
include BaileysHelper include BaileysHelper
@ -20,9 +20,9 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
end end
end end
def handle_message # rubocop:disable Metrics/CyclomaticComplexity def handle_message
return unless %w[lid user].include?(jid_type) 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 ignore_message?
return if find_message_by_source_id(raw_message_id) || message_under_process? return if find_message_by_source_id(raw_message_id) || message_under_process?
@ -42,27 +42,51 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
end end
def set_contact 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( contact_inbox = ::ContactInboxWithContactBuilder.new(
# FIXME: update the source_id to use sender LID source_id: source_id,
source_id: phone_number_from_jid,
inbox: inbox, 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 ).perform
@contact_inbox = contact_inbox @contact_inbox = contact_inbox
@contact = contact_inbox.contact @contact = contact_inbox.contact
update_contact_information update_contact_info(phone, source_id, identifier)
end end
def update_contact_information def update_existing_contact_inbox(phone, source_id, identifier)
updates = {} # NOTE: This is useful when we create a new contact manually, so we don't have information about contact LID;
updates[:identifier] = sender_lid if @contact.identifier.blank? # With this, when we receive a message from that contact, we can link it properly.
updates[:phone_number] = "+#{phone_number_from_jid}" if @contact.phone_number.blank? existing_contact_inbox = inbox.contact_inboxes.find_by(source_id: phone)
updates[:name] = contact_name if @contact.name == phone_number_from_jid || @contact.name == sender_lid 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 try_update_contact_avatar
end end
@ -91,10 +115,13 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
def message_content_attributes def message_content_attributes
type = message_type type = message_type
msg = unwrap_ephemeral_message(@raw_message[:message])
content_attributes = { external_created_at: baileys_extract_message_timestamp(@raw_message[:messageTimestamp]) } content_attributes = { external_created_at: baileys_extract_message_timestamp(@raw_message[:messageTimestamp]) }
if type == 'reaction' 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 content_attributes[:is_reaction] = true
elsif reply_to_message_id
content_attributes[:in_reply_to_external_id] = reply_to_message_id
elsif type == 'unsupported' elsif type == 'unsupported'
content_attributes[:is_unsupported] = true content_attributes[:is_unsupported] = true
end end
@ -104,13 +131,14 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
def handle_attach_media def handle_attach_media
attachment_file = download_attachment_file attachment_file = download_attachment_file
msg = unwrap_ephemeral_message(@raw_message[:message])
attachment = @message.attachments.build( attachment = @message.attachments.build(
account_id: @message.account_id, account_id: @message.account_id,
file_type: file_content_type.to_s, file_type: file_content_type.to_s,
file: { io: attachment_file, filename: filename, content_type: message_mimetype } 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 rescue Down::Error => e
@message.update!(is_unsupported: true) @message.update!(is_unsupported: true)
@ -122,7 +150,8 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
end end
def filename 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? return filename if filename.present?
ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.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 true
end end
def send_message(phone_number, message) def send_message(recipient_id, message)
@message = message @message = message
@phone_number = phone_number @recipient_id = recipient_id
if message.content_attributes[:is_reaction] if message.content_attributes[:is_reaction]
@message_content = reaction_message_content @message_content = reaction_message_content
@ -94,8 +94,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
process_response(response) process_response(response)
end end
def toggle_typing_status(typing_status, phone_number:, **) def toggle_typing_status(typing_status, recipient_id:, **)
@phone_number = phone_number @recipient_id = recipient_id
status_map = { status_map = {
Events::Types::CONVERSATION_TYPING_ON => 'composing', Events::Types::CONVERSATION_TYPING_ON => 'composing',
Events::Types::CONVERSATION_RECORDING => 'recording', Events::Types::CONVERSATION_RECORDING => 'recording',
@ -136,8 +136,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true true
end end
def read_messages(messages, phone_number:, **) def read_messages(messages, recipient_id:, **)
@phone_number = phone_number @recipient_id = recipient_id
response = HTTParty.post( response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/read-messages", "#{provider_url}/connections/#{whatsapp_channel.phone_number}/read-messages",
@ -158,8 +158,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true true
end end
def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength def unread_message(recipient_id, message) # rubocop:disable Metrics/MethodLength
@phone_number = phone_number @recipient_id = recipient_id
response = HTTParty.post( response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/chat-modify", "#{provider_url}/connections/#{whatsapp_channel.phone_number}/chat-modify",
@ -185,8 +185,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true true
end end
def received_messages(phone_number, messages) def received_messages(recipient_id, messages)
@phone_number = phone_number @recipient_id = recipient_id
response = HTTParty.post( response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-receipts", "#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-receipts",
@ -220,8 +220,8 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
response.parsed_response response.parsed_response
end end
def on_whatsapp(phone_number) def on_whatsapp(recipient_id)
@phone_number = phone_number @recipient_id = recipient_id
response = HTTParty.post( response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/on-whatsapp", "#{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) 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 end
private private
@ -303,9 +303,10 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
response.success? response.success?
end end
# FIXME: Make this a function with argument, instead of using instance variable
def remote_jid 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 end
def update_external_created_at(response) def update_external_created_at(response)

View File

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

View File

@ -51,14 +51,10 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
end end
def recipient_id 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? # NOTE: `identifier` must be in the WhatsApp LID format
message.conversation.contact.phone_number.gsub(/[^\d]/, '') message.conversation.contact.phone_number&.gsub(/[^\d]/, '') || message.conversation.contact.identifier
else
# NOTE: `identifier` is the WhatsApp LID format. See `Whatsapp::ZapiHandlers::ReceivedCallback#set_contact`
message.conversation.contact.identifier
end
end end
def template_params def template_params

View File

@ -12,17 +12,17 @@ module Whatsapp::ZapiHandlers::Helpers
end end
def cache_message_source_id_in_redis 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) Redis::Alfred.setex(key, true)
end end
def clear_message_source_id_from_redis 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) Redis::Alfred.delete(key)
end end
def message_under_process? 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) Redis::Alfred.get(key)
end end
end end

View File

@ -184,9 +184,10 @@ RSpec.describe Channel::Whatsapp do
let(:conversation) { create(:conversation) } let(:conversation) { create(:conversation) }
it 'calls provider service method' do it 'calls provider service method' do
message = create(:message, conversation: conversation)
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil) provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
allow(provider_double).to receive(:toggle_typing_status) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .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) create(:channel_whatsapp, provider: 'baileys', provider_config: { mark_as_read: true }, validate_provider_config: false, sync_templates: false)
end end
let(:conversation) { create(:conversation) } let(:conversation) { create(:conversation) }
let(:message) { create(:message) } let(:message) { create(:message, conversation: conversation) }
it 'calls provider service method' do it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .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 it 'call method when the provider config mark_as_read is nil' do
channel.update!(provider_config: {}) channel.update!(provider_config: {})
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .and_return(provider_double)
@ -332,7 +333,7 @@ RSpec.describe Channel::Whatsapp do
it 'calls provider service method' do it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, received_messages: nil) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .and_return(provider_double)
@ -415,4 +416,51 @@ RSpec.describe Channel::Whatsapp do
end end
end 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 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(:timestamp) { Time.current.to_i }
let(:raw_message) do 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', pushName: 'John Doe',
messageTimestamp: timestamp, messageTimestamp: timestamp,
message: { conversation: 'Hello from Baileys' } message: { conversation: 'Hello from Baileys' }
@ -360,23 +360,6 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(message.message_type).to eq('outgoing') expect(message.message_type).to eq('outgoing')
end 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 it 'updates the contact name when name is the phone number and message has a pushName' do
create(:contact, account: inbox.account, name: '5511912345678') create(:contact, account: inbox.account, name: '5511912345678')
@ -396,6 +379,23 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(conversation.contact.name).to eq('Verified John') expect(conversation.contact.name).to eq('Verified John')
end 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 it 'creates contact with phone number as name on outgoing message' do
raw_message[:key][:fromMe] = true raw_message[:key][:fromMe] = true
create(:account_user, account: inbox.account) create(:account_user, account: inbox.account)
@ -416,9 +416,23 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(conversation.contact.name).to eq('5511912345678') expect(conversation.contact.name).to eq('5511912345678')
end 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 it 'creates a message on an existing conversation' do
contact = create(:contact, account: inbox.account, name: 'John Doe') 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) existing_conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
described_class.new(inbox: inbox, params: params).perform described_class.new(inbox: inbox, params: params).perform
@ -476,7 +490,7 @@ describe Whatsapp::IncomingMessageBaileysService do
context 'when message type is reaction' do context 'when message type is reaction' do
let!(:message) do let!(:message) do
contact = create(:contact, account: inbox.account, name: '5511912345678') 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) conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
create(:message, inbox: inbox, conversation: conversation, source_id: 'msg_123') create(:message, inbox: inbox, conversation: conversation, source_id: 'msg_123')
end end
@ -485,7 +499,7 @@ describe Whatsapp::IncomingMessageBaileysService do
raw_message[:key][:id] = 'reaction_123' raw_message[:key][:id] = 'reaction_123'
raw_message[:message] = { raw_message[:message] = {
reactionMessage: { reactionMessage: {
key: { remoteJid: '5511912345678@s.whatsapp.net', fromMe: true, id: 'msg_123' }, key: { remoteJid: '12345678@lid', fromMe: true, id: 'msg_123' },
text: '👍' text: '👍'
} }
} }
@ -502,7 +516,7 @@ describe Whatsapp::IncomingMessageBaileysService do
raw_message[:key][:id] = 'reaction_123' raw_message[:key][:id] = 'reaction_123'
raw_message[:message] = { raw_message[:message] = {
reactionMessage: { reactionMessage: {
key: { remoteJid: '5511912345678@s.whatsapp.net', fromMe: true, id: 'msg_123' }, key: { remoteJid: '12345678@lid', fromMe: true, id: 'msg_123' },
text: '' text: ''
} }
} }
@ -522,7 +536,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify', type: 'notify',
messages: [ 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' } }, message: { imageMessage: { caption: 'Hello from Baileys', mimetype: 'image/png' } },
pushName: 'John Doe' pushName: 'John Doe'
} }
@ -574,7 +589,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify', type: 'notify',
messages: [ 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' } }, message: { videoMessage: { caption: 'Hello from Baileys', mimetype: 'video/mp4' } },
pushName: 'John Doe' pushName: 'John Doe'
} }
@ -617,7 +633,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify', type: 'notify',
messages: [ 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 } }, message: { documentMessage: { fileName: filename } },
pushName: 'John Doe' pushName: 'John Doe'
} }
@ -668,7 +685,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify', type: 'notify',
messages: [ 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' } }, message: { audioMessage: { mimetype: 'audio/opus' } },
pushName: 'John Doe' pushName: 'John Doe'
} }
@ -699,7 +717,8 @@ describe Whatsapp::IncomingMessageBaileysService do
type: 'notify', type: 'notify',
messages: [ 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' } }, message: { stickerMessage: { mimetype: 'image/png' } },
pushName: 'John Doe' pushName: 'John Doe'
} }
@ -724,13 +743,13 @@ describe Whatsapp::IncomingMessageBaileysService do
context 'when processing multiple messages' do context 'when processing multiple messages' do
it 'creates separate contacts and conversations for each message' do it 'creates separate contacts and conversations for each message' do
raw_message1 = { 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', pushName: 'John Doe',
messageTimestamp: timestamp, messageTimestamp: timestamp,
message: { conversation: 'Hello from Baileys' } message: { conversation: 'Hello from Baileys' }
} }
raw_message2 = { 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', pushName: 'Jane Doe',
messageTimestamp: timestamp, messageTimestamp: timestamp,
message: { conversation: 'Hello from another user' } message: { conversation: 'Hello from another user' }
@ -764,9 +783,10 @@ describe Whatsapp::IncomingMessageBaileysService do
end end
context 'when jid type is lid' do context 'when jid type is lid' do
it 'processes the message with phone number from lid jid type' do it 'processes the message with phone number from addressingMode pn' do
raw_message[:key][:remoteJid] = '12345678@lid' raw_message[:key][:remoteJid] = '5511912345678@s.whatsapp.net'
raw_message[:key][:senderPn] = '5511912345678@s.whatsapp.net' raw_message[:key][:remoteJidAlt] = '12345678@lid'
raw_message[:key][:addressingMode] = 'pn'
described_class.new(inbox: inbox, params: params).perform described_class.new(inbox: inbox, params: params).perform
@ -778,82 +798,90 @@ describe Whatsapp::IncomingMessageBaileysService do
end end
end end
it 'skips message if senderPn is not present' do it 'skips message if LID cannot be extracted' do
raw_message[:key][:remoteJid] = '12345678@lid' raw_message[:key][:remoteJid] = 'invalid_jid'
raw_message[:key][:remoteJidAlt] = nil
raw_message[:key][:addressingMode] = 'lid'
described_class.new(inbox: inbox, params: params).perform described_class.new(inbox: inbox, params: params).perform
expect(inbox.conversations).to be_empty expect(inbox.conversations).to be_empty
end end
context 'when handling senderLid field' do context 'when handling LID field' do
it 'creates contact with identifier from senderLid' do it 'creates contact with LID as source_id and identifier' do
described_class.new(inbox: inbox, params: params).perform described_class.new(inbox: inbox, params: params).perform
contact = inbox.contacts.last contact = inbox.contacts.last
contact_inbox = inbox.contact_inboxes.last
expect(contact.identifier).to eq('12345678@lid') expect(contact.identifier).to eq('12345678@lid')
expect(contact_inbox.source_id).to eq('12345678')
end 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) contact = create(:contact, account: inbox.account, phone_number: '+5511912345678', identifier: nil)
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678') create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
described_class.new(inbox: inbox, params: params).perform 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.reload.identifier).to eq('12345678@lid')
expect(contact.phone_number).to eq('+5511912345678')
end end
it 'does not update contact identifier if already present' do it 'does not update contact_inbox if source_id is already LID' do
contact = create(:contact, account: inbox.account, phone_number: '+5511912345678', identifier: 'existing@lid') contact = create(:contact, account: inbox.account, phone_number: '+5511912345678', identifier: '12345678@lid')
create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678') contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '12345678')
described_class.new(inbox: inbox, params: params).perform 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
end end
context 'when updating contact information' do context 'when updating contact information' do
it 'updates contact phone number if blank' do it 'updates contact phone number if blank' do
contact = create(:contact, account: inbox.account, phone_number: nil, identifier: '12345678@lid') 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 described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.phone_number).to eq('+5511912345678') expect(contact.reload.phone_number).to eq('+5511912345678')
end 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 it 'updates contact name if it matches phone number' do
contact = create(:contact, account: inbox.account, name: '5511912345678', phone_number: '+5511912345678', identifier: '12345678@lid') 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 described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.name).to eq('John Doe') expect(contact.reload.name).to eq('John Doe')
end 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') 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 described_class.new(inbox: inbox, params: params).perform
expect(contact.reload.name).to eq('John Doe') expect(contact.reload.name).to eq('John Doe')
end 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') 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 described_class.new(inbox: inbox, params: params).perform
@ -958,7 +986,7 @@ describe Whatsapp::IncomingMessageBaileysService do
end end
def format_message_source_key(message_id) 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
def stub_download 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 body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 200, body: '', headers: {}) ).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) expect(result).to be(true)
end end
@ -495,7 +495,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
.to_return(status: 200) .to_return(status: 200)
expect do 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.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end end
end end
@ -605,7 +605,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
) )
.to_return(status: 200) .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 expect(request).to have_been_requested
end end
@ -621,7 +621,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
) )
.to_return(status: 200) .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 expect(request).to have_been_requested
end end
@ -637,7 +637,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
) )
.to_return(status: 200) .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 expect(request).to have_been_requested
end end
@ -664,7 +664,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
allow(Rails.logger).to receive(:error) allow(Rails.logger).to receive(:error)
expect do 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) end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message') expect(Rails.logger).to have_received(:error).with('error message')
@ -730,12 +730,12 @@ describe Whatsapp::Providers::WhatsappBaileysService do
.to_return( .to_return(
status: 200, status: 200,
headers: { 'Content-Type' => 'application/json' }, 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) 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 end
it 'returns default check response' do it 'returns default check response' do
@ -749,7 +749,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
response = service.on_whatsapp(phone_number) 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
end end
@ -916,7 +916,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' }) whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
expect do 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) end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close') 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) .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) expect(result).to be(true)
end end
@ -223,7 +223,7 @@ describe Whatsapp::Providers::WhatsappZapiService do
allow(Rails.logger).to receive(:error) allow(Rails.logger).to receive(:error)
expect do 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.to raise_error(Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError)
end end
end end

View File

@ -397,8 +397,9 @@ describe Whatsapp::SendOnWhatsappService do
end end
it 'calls channel.send_message if channel is not locked on outgoing message' do 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) 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 described_class.new(message: message).perform

View File

@ -1217,6 +1217,6 @@ describe Whatsapp::ZapiHandlers::ReceivedCallback do
private private
def format_message_source_key(message_id) 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
end end