Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a5a9c89
feat: add messaging history set handler for WhatsApp and restore cont…
CayoPOliveira Jun 18, 2025
b247261
feat: enhance messaging history processing and message handling in Wh…
CayoPOliveira Jun 19, 2025
d773f1c
feat: include MessagingHistorySet handler in IncomingMessageBaileysSe…
CayoPOliveira Jun 19, 2025
49c1ac9
feat: add options to sync messages and contacts in WhatsApp settings
CayoPOliveira Jun 19, 2025
2a33ae7
feat: implement initial sync for contacts and message history in setu…
CayoPOliveira Jun 19, 2025
83dc898
chore: correct phone number variable usage in create_contact method
CayoPOliveira Jun 25, 2025
a999f8d
refactor: rename methods for avoid impact other baileys handlers
CayoPOliveira Jun 25, 2025
b08d42a
fix: correct argument order in history_handle_attach_media method call
CayoPOliveira Jun 25, 2025
76c2184
fix: improve error logging for attachment download failure
CayoPOliveira Jun 25, 2025
455c69a
fix: correct history_filename method in messaging_history.set
CayoPOliveira Jun 25, 2025
ae06267
chore: remove unused helper includes in messaging_history_set
CayoPOliveira Jun 25, 2025
d00f5e8
fix: variables names in attachment handling methods
CayoPOliveira Jun 25, 2025
48e159c
fix: update fetch_message_history method to include phone_number para…
CayoPOliveira Jun 25, 2025
35e4d2e
feat: add BAILEYS_MESSAGE_HISTORY_COUNT environment variable for mess…
CayoPOliveira Jun 25, 2025
a584148
fix: rename message_content method to history_message_content for cla…
CayoPOliveira Jun 26, 2025
23d88ed
feat: implement fetch_message_history method to retrieve message hist…
CayoPOliveira Jun 26, 2025
7f8275d
fix: simplify setup_channel_provider method by removing unnecessary s…
CayoPOliveira Jun 26, 2025
0ec7602
fix: update process_messaging_history_set to conditionally sync conta…
CayoPOliveira Jun 26, 2025
91ff533
fix: update create_contact method to include inbox parameter in Conta…
CayoPOliveira Jun 26, 2025
345f3cf
fix: rename params name in message creation and attachment creation
CayoPOliveira Jun 26, 2025
ad35f31
fix: reduce default message history count to 5 in fetch_message_histo…
CayoPOliveira Jun 27, 2025
2d01f7f
fix: refactor fetch_message_history methods to streamline parameters …
CayoPOliveira Jun 27, 2025
5e534d2
fix: validate presence of key, message, and messageTimestamp in histo…
CayoPOliveira Jun 27, 2025
16c0c1e
fix: add error handling for contact and message processing in process…
CayoPOliveira Jun 27, 2025
e2b634b
fix: add presence validation for key, message, and messageTimestamp i…
CayoPOliveira Jun 27, 2025
40e7cda
fix: update history_create_message method to use raw_message directly…
CayoPOliveira Jun 27, 2025
b841a1b
fix: add contact attributes to ContactInboxWithContactBuilder in crea…
CayoPOliveira Jun 27, 2025
80c5bcf
fix: update history_message_content_attributes to handle multiple uns…
CayoPOliveira Jun 27, 2025
682d263
fix: remove media attachment handling methods from history_handle_att…
CayoPOliveira Jun 27, 2025
8f03dbc
fix: remove redundant WhatsApp sync labels from inbox management loca…
CayoPOliveira Jun 27, 2025
471b441
fix: update sync label order in inbox management localization file
CayoPOliveira Jun 27, 2025
8745ec0
fix: correct phone number attribute in fetch_message_history method
CayoPOliveira Jun 27, 2025
48a06ad
fix: enhance message flooding prevention and update fetch_message_his…
gabrieljablonski Jun 30, 2025
d86cf1e
chore: update fetch_message_history method to accept oldest_message p…
CayoPOliveira Jun 30, 2025
456ff17
chore: add fetch_message_history to error handling methods in Whatsap…
CayoPOliveira Jun 30, 2025
23a9246
fix: handle blank contact IDs and update name assignment logic in cre…
CayoPOliveira Jun 30, 2025
ffcc625
fix: update history_cache_message_source_id_in_redis to use set with …
CayoPOliveira Jun 30, 2025
6327952
test: create spec in message model for skip message flooding validati…
CayoPOliveira Jun 30, 2025
e93d18a
fix: skip processing of blank contact IDs in messaging history set
CayoPOliveira Jul 1, 2025
8cd5da3
refactor: remove unused process_status method from MessagingHistorySet
CayoPOliveira Jul 1, 2025
1859630
refactor: simplify process_messaging_history_set method by removing u…
CayoPOliveira Jul 1, 2025
0d47da5
refactor: simplify process_messaging_history_set and create_contact v…
CayoPOliveira Jul 1, 2025
5f15fa4
fix: exclude unsupported message types from ignore processing list in…
CayoPOliveira Jul 1, 2025
05a6569
refactor: update conversation creation to use history_conversation_pa…
CayoPOliveira Jul 1, 2025
b1cf9c8
test: add specs for messaging-history.set event handling
CayoPOliveira Jul 1, 2025
e38f893
refactor: streamline process_messaging_history_set by consolidating c…
CayoPOliveira Jul 1, 2025
1537b1d
refactor: enhance history_handle_message and history_message_valid me…
CayoPOliveira Jul 1, 2025
8cc5b26
test: add specs for sync_full_history and sync_contacts in setup_chan…
CayoPOliveira Jul 1, 2025
dde9ed4
chore: update fetch_message_history to ensure request body is properl…
CayoPOliveira Jul 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@
"MARK_AS_READ": {
"LABEL": "Send read receipts"
},
"SYNC_FULL_HISTORY": {
"ONLY_CONTACTS_LABEL": "Sync only contacts",
"LABEL": "Sync messages and contacts"
},
"ADVANCED_OPTIONS": "Advanced options",
"BAILEYS": {
"SUBTITLE": "Click below to setup the WhatsApp channel using Baileys.",
Expand Down Expand Up @@ -564,7 +568,14 @@
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings",
"WHATSAPP_MARK_AS_READ_TITLE": "Read receipts",
"WHATSAPP_MARK_AS_READ_SUBHEADER": "If turned off, when a message is viewed in Chatwoot, a read receipt will not be sent to the sender. Your messages will still be able to receive read receipts from the sender.",
"WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts"
"WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts",

"WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_TITLE": "Sync messages and contacts",
"WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_SUBHEADER": "It will restore your messages and contacts from the WhatsApp.",
"WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_LABEL": "Sync messages and contacts",
"WHATSAPP_FETCH_CONTACTS_TITLE": "Sync contacts",
"WHATSAPP_FETCH_CONTACTS_SUBHEADER": "It will restore your contacts from the WhatsApp.",
"WHATSAPP_FETCH_CONTACTS_LABEL": "Sync contacts"
},
"HELP_CENTER": {
"LABEL": "Help Center",
Expand Down
12 changes: 11 additions & 1 deletion app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@
"MARK_AS_READ": {
"LABEL": "Enviar confirmações de leitura"
},
"SYNC_FULL_HISTORY": {
"ONLY_CONTACTS_LABEL": "Sincronizar apenas contatos",
"LABEL": "Sincronizar mensagens e contatos"
},
"ADVANCED_OPTIONS": "Opções avançadas",
"BAILEYS": {
"SUBTITLE": "Clique abaixo para configurar o canal do WhatsApp usando o Baileys.",
Expand Down Expand Up @@ -564,7 +568,13 @@
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat",
"WHATSAPP_MARK_AS_READ_TITLE": "Confirmações de leitura",
"WHATSAPP_MARK_AS_READ_SUBHEADER": "Se essa opção estiver desativada, ao visualizar uma mensagem pelo Chatwoot, não será enviada uma confirmação de leitura para o remetente. As suas mensagens ainda poderão receber confirmações de leitura.",
"WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura"
"WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura",
"WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_TITLE": "Sincronizar mensagens e contatos",
"WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_SUBHEADER": "Isso irá restaurar suas mensagens e contatos do WhatsApp.",
"WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_LABEL": "Sincronizar mensagens e contatos",
"WHATSAPP_FETCH_CONTACTS_TITLE": "Sincronizar contatos",
"WHATSAPP_FETCH_CONTACTS_SUBHEADER": "Isso irá restaurar seus contatos do WhatsApp.",
"WHATSAPP_FETCH_CONTACTS_LABEL": "Sincronizar contatos"
},
"HELP_CENTER": {
"LABEL": "Centro de Ajuda",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default {
providerUrl: '',
showAdvancedOptions: false,
markAsRead: true,
syncContacts: false,
syncFullHistory: false,
};
},
computed: {
Expand All @@ -53,6 +55,8 @@ export default {
try {
const providerConfig = {
mark_as_read: this.markAsRead,
sync_contacts: this.syncContacts,
sync_full_history: this.syncFullHistory,
};

if (this.apiKey || this.providerUrl) {
Expand Down Expand Up @@ -177,6 +181,34 @@ export default {
</div>
</label>
</div>

<div
v-if="!syncFullHistory"
class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]"
>
<label>
<div class="flex mb-2 items-center">
<span class="mr-2 text-sm">
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.SYNC_FULL_HISTORY.ONLY_CONTACTS_LABEL'
)
}}
</span>
<Switch id="syncContacts" v-model="syncContacts" />
</div>
</label>
</div>
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
<label>
<div class="flex mb-2 items-center">
<span class="mr-2 text-sm">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.SYNC_FULL_HISTORY.LABEL') }}
</span>
<Switch id="syncFullHistory" v-model="syncFullHistory" />
</div>
</label>
</div>
</template>

<div class="w-full">
Expand Down
238 changes: 238 additions & 0 deletions app/services/whatsapp/baileys_handlers/messaging_history_set.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength
# include Whatsapp::BaileysHandlers::Helpers
# include BaileysHelper

private

def process_messaging_history_set
contacts = params.dig(:data, :contacts) || []
contacts.each do |contact|
create_contact(contact) if jid_user?(contact[:id])
end

messages = params.dig(:data, :messages) || []
messages.each do |message|
handle_message(message)
end
end

# TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here
def jid_user?(jid)
server = jid.split('@').last
server == 's.whatsapp.net' || server == 'c.us'
end

# TODO: Refactor this method in helpers to receive the jid as an argument and remove it from here
def phone_number_from_jid(jid)
jid.split('@').first.split(':').first.split('_').first
end

def create_contact(contact)
phone_number_from_jid = phone_number_from_jid(contact[:id])
name = contact[:verifiedName].presence || contact[:name].presence || phone_number_from_jid
::ContactInboxWithContactBuilder.new(
# FIXME: update the source_id to complete jid in future
source_id: phone_number_from_jid,
inbox: inbox,
contact_attributes: { name: name, phone_number: "+#{phone_number_from_jid}" }
).perform
end

def handle_message(raw_message)
jid = raw_message[:key][:remoteJid]
return unless jid_user?(jid)

id = raw_message[:key][:id]
return if message_type(raw_message[:message]).in?(%w[protocol context unsupported])
return if find_message_by_source_id(id) || message_under_process?(id)

cache_message_source_id_in_redis(id)
contact_inbox = find_contact_inbox(jid)

unless contact_inbox.contact
clear_message_source_id_from_redis(id)

Rails.logger.warn "Contact not found for message: #{id}"
return
end

create_message(raw_message, contact_inbox)
clear_message_source_id_from_redis(id)
end

# TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here
def message_type(message_content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
if message_content.key?(:conversation) || message_content.dig(:extendedTextMessage, :text).present?
'text'
elsif message_content.key?(:imageMessage)
'image'
elsif message_content.key?(:audioMessage)
'audio'
elsif message_content.key?(:videoMessage)
'video'
elsif message_content.key?(:documentMessage) || message_content.key?(:documentWithCaptionMessage)
'file'
elsif message_content.key?(:stickerMessage)
'sticker'
elsif message_content.key?(:reactionMessage)
'reaction'
elsif message_content.key?(:editedMessage)
'edited'
elsif message_content.key?(:protocolMessage)
'protocol'
elsif message_content.key?(:messageContextInfo)
'context'
else
'unsupported'
end
end

def find_message_by_source_id(source_id)
return unless source_id

Message.find_by(source_id: source_id).presence
end

def find_contact_inbox(jid)
phone_number = phone_number_from_jid(jid)
::ContactInboxWithContactBuilder.new(
# FIXME: update the source_id to complete jid in future
source_id: phone_number
).perform
end

# TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here
def message_under_process?(source_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id)
Redis::Alfred.get(key)
end

# TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here
def cache_message_source_id_in_redis(source_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id)
::Redis::Alfred.setex(key, true)
end

# TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here
def clear_message_source_id_from_redis(source_id)
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id)
::Redis::Alfred.delete(key)
end

def create_message(raw_message, contact_inbox) # rubocop:disable Metrics/AbcSize
conversation = get_conversation(contact_inbox)
inbox = contact_inbox.inbox
message = conversation.messages.build(
content: message_content(raw_message[:message]),
account_id: inbox.account_id,
inbox_id: inbox.id,
source_id: raw_message[:key][:id],
sender: incoming?(raw_message) ? contact_inbox.contact : inbox.account.account_users.first.user,
sender_type: incoming?(raw_message) ? 'Contact' : 'User',
message_type: incoming?(raw_message) ? :incoming : :outgoing,
content_attributes: message_content_attributes(raw_message),
status: process_status(raw_message[:status]) || 'sent'
)

handle_attach_media(conversation, message, raw_message) if message_type(raw_message[:message]).in?(%w[image file video audio sticker])

message.save!
end

# NOTE: See reference in app/services/whatsapp/incoming_message_base_service.rb:97
def get_conversation(contact_inbox)
return contact_inbox.conversations.last if contact_inbox.inbox.lock_to_single_conversation

# NOTE: if lock to single conversation is disabled, create a new conversation if previous conversation is resolved
return contact_inbox.conversations.where.not(status: :resolved).last.presence ||
::Conversation.create!(conversation_params(contact_inbox))
end

def conversation_params(contact_inbox)
{
account_id: contact_inbox.inbox.account_id,
inbox_id: contact_inbox.inbox.id,
contact_id: contact_inbox.contact.id,
contact_inbox_id: contact_inbox.id
}
end

# TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here
def incoming?(raw_message)
!raw_message[:key][:fromMe]
end

# TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here
def message_content(raw_message)
raw_message.dig(:message, :conversation) ||
raw_message.dig(:message, :extendedTextMessage, :text) ||
raw_message.dig(:message, :imageMessage, :caption) ||
raw_message.dig(:message, :videoMessage, :caption) ||
raw_message.dig(:message, :documentMessage, :caption).presence ||
raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :caption) ||
raw_message.dig(:message, :reactionMessage, :text)
end

def message_content_attributes(raw_message)
{
external_created_at: baileys_extract_message_timestamp(raw_message[:messageTimestamp]),
is_unsupported: message_type(raw_message[:message]) == 'unsupported' ? true : nil
}.compact
end

def process_status(status)
{
'PENDING' => 'sent',
'DELIVERY_ACK' => 'delivered',
'READ' => 'read'
}[status]
end

def handle_attach_media(message, conversation, raw_message)
attachment_file = download_attachment_file(conversation, raw_message)
message_type = message_type(raw_message[:message])
file_type = file_content_type(message_type).to_s
message_mimetype = message_mimetype(message_type, raw_message)
attachment = message.attachments.build(
account_id: message.account_id,
file_type: file_type,
file: { io: attachment_file, filename: filename(raw_message, message_mimetype, file_type), content_type: message_mimetype }
)
attachment.meta = { is_recorded_audio: true } if raw_message.dig(:message, :audioMessage, :ptt)
rescue Down::Error => e
message.update!(is_unsupported: true)

Rails.logger.error "Failed to download attachment for message #{raw_message_id}: #{e.message}"
end

def download_attachment_file(conversation, raw_message)
Down.download(conversation.inbox.channel.media_url(raw_message.dig(:key, :id)), headers: conversation.inbox.channel.api_headers)
end

def file_content_type(message_type)
return :image if message_type.in?(%w[image sticker])
return :video if message_type.in?(%w[video video_note])
return :audio if message_type == 'audio'

:file
end

def filename(raw_message, message_mimetype, file_content_type)
filename = raw_message.dig[:message][:documentMessage][:fileName]
return filename if filename.present?

ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present?
"#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}"
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix syntax error in dig method call.

The dig method is called incorrectly with bracket notation instead of parentheses.

 def filename(raw_message, message_mimetype, file_content_type)
-  filename = raw_message.dig[:message][:documentMessage][:fileName]
+  filename = raw_message.dig(:message, :documentMessage, :fileName)
   return filename if filename.present?
 
   ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present?
   "#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}"
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def filename(raw_message, message_mimetype, file_content_type)
filename = raw_message.dig[:message][:documentMessage][:fileName]
return filename if filename.present?
ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present?
"#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}"
end
def filename(raw_message, message_mimetype, file_content_type)
filename = raw_message.dig(:message, :documentMessage, :fileName)
return filename if filename.present?
ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present?
"#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}"
end
🤖 Prompt for AI Agents
In app/services/whatsapp/baileys_handlers/messaging_history_set.rb around lines
220 to 226, the dig method is incorrectly called using bracket notation instead
of parentheses. Replace the square brackets after dig with parentheses to
correctly access nested hash keys, changing
raw_message.dig[:message][:documentMessage][:fileName] to
raw_message.dig(:message, :documentMessage, :fileName).


def message_mimetype(message_type, raw_message)
{
'image' => raw_message.dig(:message, :imageMessage, :mimetype),
'sticker' => raw_message.dig(:message, :stickerMessage, :mimetype),
'video' => raw_message.dig(:message, :videoMessage, :mimetype),
'audio' => raw_message.dig(:message, :audioMessage, :mimetype),
'file' => raw_message.dig(:message, :documentMessage, :mimetype).presence ||
raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype)
}[message_type]
end
end
1 change: 1 addition & 0 deletions app/services/whatsapp/incoming_message_baileys_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
include Whatsapp::BaileysHandlers::ConnectionUpdate
include Whatsapp::BaileysHandlers::MessagesUpsert
include Whatsapp::BaileysHandlers::MessagesUpdate
include Whatsapp::BaileysHandlers::MessagingHistorySet

class InvalidWebhookVerifyToken < StandardError; end

Expand Down
Loading