Files
leadchat/app/jobs/webhooks/tiktok_events_job.rb
Muhsin Keloth b686d14044 feat: Handle external echo messages from native apps (#13371)
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
2026-02-02 15:52:53 +05:30

70 lines
1.9 KiB
Ruby

# https://business-api.tiktok.com/portal/docs?id=1832190670631937
class Webhooks::TiktokEventsJob < MutexApplicationJob
queue_as :default
retry_on LockAcquisitionError, wait: 2.seconds, attempts: 8
SUPPORTED_EVENTS = [:im_send_msg, :im_receive_msg, :im_mark_read_msg].freeze
def perform(event)
@event = event.with_indifferent_access
return if channel_is_inactive?
key = format(::Redis::Alfred::TIKTOK_MESSAGE_MUTEX, business_id: business_id, conversation_id: conversation_id)
with_lock(key, 10.seconds) do
process_event
end
end
private
def channel_is_inactive?
return true if channel.blank?
return true unless channel.account.active?
false
end
def process_event
return if event_name.blank? || channel.blank?
send(event_name)
end
def event_name
@event_name ||= SUPPORTED_EVENTS.include?(@event[:event].to_sym) ? @event[:event] : nil
end
def business_id
@business_id ||= @event[:user_openid]
end
def content
@content ||= JSON.parse(@event[:content]).deep_symbolize_keys
end
def conversation_id
@conversation_id ||= content[:conversation_id]
end
def channel
@channel ||= Channel::Tiktok.find_by(business_id: business_id)
end
# Receive real-time notifications if you send a message to a user.
def im_send_msg
# This can be either an echo message or a message sent directly via tiktok application
::Tiktok::MessageService.new(channel: channel, content: content, outgoing_echo: true).perform
end
# Receive real-time notifications if a user outside the European Economic Area (EEA), Switzerland, or the UK sends a message to you.
def im_receive_msg
::Tiktok::MessageService.new(channel: channel, content: content).perform
end
# Receive real-time notifications when a Personal Account user marks all messages in a session as read.
def im_mark_read_msg
::Tiktok::ReadStatusService.new(channel: channel, content: content).perform
end
end