Files
leadchat/app/services/whatsapp/incoming_message_base_service.rb
Pranav Raj S b7c9f779ad fix: Avoid processing reactions, ephemeral, request_welcome or unsupported messages (#8780)
Currently, we do not support reactions, ephemeral messages, or the request_welcome event for the WhatsApp channel. However, if this is the first event we receive in Chatwoot (i.e., there is no previous conversation or contact in Chatwoot), it will create a contact and a conversation without any messages. This confuses our customer, as it may appear that Chatwoot has missed some messages. There are multiple cases where this might be the first event we receive in Chatwoot. One quick example is when the user has sent an outbound campaign from another tool and their customers reacted to the message.

Another event like this is request_welcome event. WhatsApp has a concept for welcome messages. You can send an outbound message even though the user has not send a message. You can receive notifications through a webhook whenever a WhatsApp user initiates a chat with you for the first time. (Read the Welcome message section: https://developers.facebook.com/docs/whatsapp/cloud-api/phone-numbers/conversational-components/ ). Although this can help the business send a pro-active message to the user, we don't have it scoped in our feature set. For now, I'm ignoring this event.

Fixes https://linear.app/chatwoot/issue/CW-3018/whatsapp-handle-request-welcome-case-properly
Fixes https://linear.app/chatwoot/issue/CW-3017/whatsapp-handle-reactions-properly
2024-01-25 11:40:18 +04:00

168 lines
5.3 KiB
Ruby

# Mostly modeled after the intial implementation of the service based on 360 Dialog
# https://docs.360dialog.com/whatsapp-api/whatsapp-api/media
# https://developers.facebook.com/docs/whatsapp/api/media/
class Whatsapp::IncomingMessageBaseService
include ::Whatsapp::IncomingMessageServiceHelpers
pattr_initialize [:inbox!, :params!]
def perform
processed_params
if processed_params.try(:[], :statuses).present?
process_statuses
elsif processed_params.try(:[], :messages).present?
process_messages
end
end
private
def process_messages
# We don't support reactions & ephemeral message now, we need to skip processing the message
# if the webhook event is a reaction or an ephermal message or an unsupported message.
return if unprocessable_message_type?(message_type)
# Multiple webhook event can be received against the same message due to misconfigurations in the Meta
# business manager account. While we have not found the core reason yet, the following line ensure that
# there are no duplicate messages created.
return if find_message_by_source_id(@processed_params[:messages].first[:id]) || message_under_process?
cache_message_source_id_in_redis
set_contact
return unless @contact
set_conversation
create_messages
clear_message_source_id_from_redis
end
def process_statuses
return unless find_message_by_source_id(@processed_params[:statuses].first[:id])
update_message_with_status(@message, @processed_params[:statuses].first)
rescue ArgumentError => e
Rails.logger.error "Error while processing whatsapp status update #{e.message}"
end
def update_message_with_status(message, status)
message.status = status[:status]
if status[:status] == 'failed' && status[:errors].present?
error = status[:errors]&.first
message.external_error = "#{error[:code]}: #{error[:title]}"
end
message.save!
end
def create_messages
message = @processed_params[:messages].first
log_error(message) && return if error_webhook_event?(message)
process_in_reply_to(message)
message_type == 'contacts' ? create_contact_messages(message) : create_regular_message(message)
end
def create_contact_messages(message)
message['contacts'].each do |contact|
create_message(contact)
attach_contact(contact)
@message.save!
end
end
def create_regular_message(message)
create_message(message)
attach_files
attach_location if message_type == 'location'
@message.save!
end
def set_contact
contact_params = @processed_params[:contacts]&.first
return if contact_params.blank?
waid = processed_waid(contact_params[:wa_id])
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: waid,
inbox: inbox,
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
def set_conversation
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
@conversation = if @inbox.lock_to_single_conversation
@contact_inbox.conversations.last
else
@contact_inbox.conversations
.where.not(status: :resolved).last
end
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
def attach_files
return if %w[text button interactive location contacts].include?(message_type)
attachment_payload = @processed_params[:messages].first[message_type.to_sym]
@message.content ||= attachment_payload[:caption]
attachment_file = download_attachment_file(attachment_payload)
return if attachment_file.blank?
@message.attachments.new(
account_id: @message.account_id,
file_type: file_content_type(message_type),
file: {
io: attachment_file,
filename: attachment_file.original_filename,
content_type: attachment_file.content_type
}
)
end
def attach_location
location = @processed_params[:messages].first['location']
location_name = location['name'] ? "#{location['name']}, #{location['address']}" : ''
@message.attachments.new(
account_id: @message.account_id,
file_type: file_content_type(message_type),
coordinates_lat: location['latitude'],
coordinates_long: location['longitude'],
fallback_title: location_name,
external_url: location['url']
)
end
def create_message(message)
@message = @conversation.messages.build(
content: message_content(message),
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: message[:id].to_s,
in_reply_to_external_id: @in_reply_to_external_id
)
end
def attach_contact(contact)
phones = contact[:phones]
phones = [{ phone: 'Phone number is not available' }] if phones.blank?
phones.each do |phone|
@message.attachments.new(
account_id: @message.account_id,
file_type: file_content_type(message_type),
fallback_title: phone[:phone].to_s
)
end
end
end