When businesses use WhatsApp Business App (co-existence mode) or Instagram App or TikTok alongside Chatwoot, messages sent from the native apps were not synced properly back to Chatwoot. This left agents with an incomplete conversation history and no visibility into responses sent outside the dashboard. Additionally, if these echo messages did arrive, they appeared as "Sent by: Bot" in the UI since they had no sender, making it confusing for agents. This PR subscribes to WhatsApp `smb_message_echoes` webhook events and routes them through the existing service with an `outgoing_echo` flag, mirroring how Instagram already handles echoes. On the Instagram side, echo messages now also carry the `external_echo` content attribute and `delivered` status. On the frontend, messages with `externalEcho` are distinguished from bot messages showing a "Native app" avatar and an advisory note encouraging agents to reply from Chatwoot to maintain the service window. <img width="1518" height="524" alt="CleanShot 2026-01-29 at 13 37 57@2x" src="https://github.com/user-attachments/assets/5aa0b552-6382-441f-96aa-9a62ca716e4a" /> Fixes https://linear.app/chatwoot/issue/CW-4204/display-messages-not-sent-from-chatwoot-in-case-of-outgoing-echo Fixes https://linear.app/chatwoot/issue/PLA-33/incoming-from-me-messages-from-whatsapp-business-app-are-not-falling
103 lines
3.5 KiB
Ruby
103 lines
3.5 KiB
Ruby
class Webhooks::WhatsappEventsJob < ApplicationJob
|
|
queue_as :low
|
|
|
|
def perform(params = {})
|
|
channel = find_channel_from_whatsapp_business_payload(params)
|
|
|
|
if channel_is_inactive?(channel)
|
|
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
|
return
|
|
end
|
|
|
|
if message_echo_event?(params)
|
|
handle_message_echo(channel, params)
|
|
else
|
|
handle_message_events(channel, params)
|
|
end
|
|
end
|
|
|
|
# Detects if the webhook is an SMB message echo event (message sent from WhatsApp Business app)
|
|
# This is part of WhatsApp coexistence feature where businesses can respond from both
|
|
# Chatwoot and the WhatsApp Business app, with messages synced to Chatwoot.
|
|
#
|
|
# Regular message payload (field: "messages"):
|
|
# {
|
|
# "entry": [{
|
|
# "changes": [{
|
|
# "field": "messages",
|
|
# "value": {
|
|
# "contacts": [{ "wa_id": "919745786257", "profile": { "name": "Customer" } }],
|
|
# "messages": [{ "from": "919745786257", "id": "wamid...", "text": { "body": "Hello" } }]
|
|
# }
|
|
# }]
|
|
# }]
|
|
# }
|
|
#
|
|
# Echo message payload (field: "smb_message_echoes"):
|
|
# {
|
|
# "entry": [{
|
|
# "changes": [{
|
|
# "field": "smb_message_echoes",
|
|
# "value": {
|
|
# "message_echoes": [{ "from": "971545296927", "to": "919745786257", "id": "wamid...", "text": { "body": "Hi" } }]
|
|
# }
|
|
# }]
|
|
# }]
|
|
# }
|
|
#
|
|
# Key differences:
|
|
# - field: "smb_message_echoes" instead of "messages"
|
|
# - message_echoes[] instead of messages[]
|
|
# - "from" is the business number, "to" is the contact (reversed from regular messages)
|
|
# - No "contacts" array in echo payload
|
|
def message_echo_event?(params)
|
|
params.dig(:entry, 0, :changes, 0, :field) == 'smb_message_echoes'
|
|
end
|
|
|
|
def handle_message_echo(channel, params)
|
|
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params, outgoing_echo: true).perform
|
|
end
|
|
|
|
def handle_message_events(channel, params)
|
|
case channel.provider
|
|
when 'whatsapp_cloud'
|
|
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform
|
|
else
|
|
Whatsapp::IncomingMessageService.new(inbox: channel.inbox, params: params).perform
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def channel_is_inactive?(channel)
|
|
return true if channel.blank?
|
|
return true if channel.reauthorization_required?
|
|
return true unless channel.account.active?
|
|
|
|
false
|
|
end
|
|
|
|
def find_channel_by_url_param(params)
|
|
return unless params[:phone_number]
|
|
|
|
Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
|
end
|
|
|
|
def find_channel_from_whatsapp_business_payload(params)
|
|
# for the case where facebook cloud api support multiple numbers for a single app
|
|
# https://github.com/chatwoot/chatwoot/issues/4712#issuecomment-1173838350
|
|
# we will give priority to the phone_number in the payload
|
|
return get_channel_from_wb_payload(params) if params[:object] == 'whatsapp_business_account'
|
|
|
|
find_channel_by_url_param(params)
|
|
end
|
|
|
|
def get_channel_from_wb_payload(wb_params)
|
|
phone_number = "+#{wb_params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}"
|
|
phone_number_id = wb_params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id)
|
|
channel = Channel::Whatsapp.find_by(phone_number: phone_number)
|
|
# validate to ensure the phone number id matches the whatsapp channel
|
|
return channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
|
end
|
|
end
|