feat: add message update processing in IncomingMessageBaileysService (#20)
* feat: add message update processing in IncomingMessageBaileysService * feat: implement message update handling in IncomingMessageBaileysService * feat: add MessageNotFoundError for handling invalid update messages in IncomingMessageBaileysService * chore: specs for message.update events and bug fixes * chore: refactor message update handling in IncomingMessageBaileysService * chore: nit remove redundant comment * chore: enhance logging for unsupported message update statuses in IncomingMessageBaileysService and disabled metrics * chore: message status update logic with transition checks * chore: update status mapping for PENDING to sent in status_mapper * chore: update status_mapper comments and fix case statement for status codes * fix: logging for unsupported message updates in update_message_content method * test: add specs for unsupported status transitions in messages.update event * refactor: ensure message status is reloaded before assertion in messages.update event spec * refactor: status variable in status_mapper method * refactor: rename status_transition_allowed method to status_transition_allowed? * refactor: streamline message creation in specs by using let! for setup * feat: process webhook whatsapp await response (#21) * feat: enhance WhatsApp webhook processing and error responses handling * chore: correct spelling of 'WhatsApp' in webhook controller specs * refactor: rename webhook processing method and improve error handling * chore: update error handling in WhatsApp controller specs for specific exceptions * refactor: remove handling for StandardError in WhatsApp controller specs * refactor: simplify perform_whatsapp_events_job method * chore: update response status from not_found to bad_request for invalid message * refactor: update expectations for job processing in WhatsApp controller specs
This commit is contained in:
parent
22c8ea8265
commit
25670564eb
@ -8,11 +8,26 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
return
|
||||
end
|
||||
|
||||
perform_whatsapp_events_job
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def perform_whatsapp_events_job
|
||||
perform_sync if params[:awaitResponse].present?
|
||||
return if performed?
|
||||
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
def perform_sync
|
||||
Webhooks::WhatsappEventsJob.perform_now(params.to_unsafe_hash)
|
||||
rescue Whatsapp::IncomingMessageBaileysService::InvalidWebhookVerifyToken
|
||||
head :unauthorized
|
||||
rescue Whatsapp::IncomingMessageBaileysService::MessageNotFoundError
|
||||
head :bad_request
|
||||
end
|
||||
|
||||
def valid_token?(token)
|
||||
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService
|
||||
class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService # rubocop:disable Metrics/ClassLength
|
||||
class InvalidWebhookVerifyToken < StandardError; end
|
||||
class MessageNotFoundError < StandardError; end
|
||||
|
||||
def perform
|
||||
raise InvalidWebhookVerifyToken if processed_params[:webhookVerifyToken] != inbox.channel.provider_config['webhook_verify_token']
|
||||
@ -171,4 +172,75 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
::Redis::Alfred.delete(key)
|
||||
end
|
||||
|
||||
def process_messages_update
|
||||
updates = processed_params[:data]
|
||||
updates.each do |update|
|
||||
@message = nil
|
||||
@raw_message = update
|
||||
handle_update
|
||||
end
|
||||
end
|
||||
|
||||
def handle_update
|
||||
raise MessageNotFoundError unless valid_update_message?
|
||||
|
||||
update_status if @raw_message.dig(:update, :status).present?
|
||||
update_message_content if @raw_message.dig(:update, :message).present?
|
||||
end
|
||||
|
||||
def valid_update_message?
|
||||
@message = find_message_by_source_id(message_id)
|
||||
@message.present?
|
||||
end
|
||||
|
||||
def update_status
|
||||
status = status_mapper
|
||||
@message.update!(status: status) if status.present? && status_transition_allowed?(status)
|
||||
end
|
||||
|
||||
def status_mapper
|
||||
# NOTE: Baileys status codes vs. Chatwoot support:
|
||||
# - (0) ERROR → (3) failed
|
||||
# - (1) PENDING → (0) sent
|
||||
# - (2) SERVER_ACK → (0) sent
|
||||
# - (3) DELIVERY_ACK → (1) delivered
|
||||
# - (4) READ → (2) read
|
||||
# - (5) PLAYED → (unsupported: PLAYED)
|
||||
# For details: https://github.com/WhiskeySockets/Baileys/blob/v6.7.16/WAProto/index.d.ts#L36694
|
||||
status = @raw_message.dig(:update, :status)
|
||||
case status
|
||||
when 0
|
||||
'failed'
|
||||
when 1, 2
|
||||
'sent'
|
||||
when 3
|
||||
'delivered'
|
||||
when 4
|
||||
'read'
|
||||
when 5
|
||||
Rails.logger.warn 'Baileys unsupported message update status: PLAYED(5)'
|
||||
else
|
||||
Rails.logger.warn "Baileys unsupported message update status: #{status}"
|
||||
end
|
||||
end
|
||||
|
||||
def status_transition_allowed?(new_status)
|
||||
return false if @message.status == 'read'
|
||||
return false if @message.status == 'delivered' && new_status == 'sent'
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def update_message_content
|
||||
message = @raw_message.dig(:update, :message, :editedMessage, :message)
|
||||
if message.blank?
|
||||
Rails.logger.warn 'No valid message content found in the update event'
|
||||
return
|
||||
end
|
||||
|
||||
content = message[:conversation] || message.dig(:extendedTextMessage, :text)
|
||||
|
||||
@message.update!(content: content) if content.present?
|
||||
end
|
||||
end
|
||||
|
||||
@ -23,11 +23,13 @@ RSpec.describe 'Webhooks::WhatsappController', type: :request do
|
||||
end
|
||||
|
||||
describe 'POST /webhooks/whatsapp/{:phone_number}' do
|
||||
it 'call the whatsapp events job with the params' do
|
||||
it 'calls the whatsapp events job asynchronously with perform_later when awaitResponse is not present' do
|
||||
allow(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||
expect(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||
|
||||
post '/webhooks/whatsapp/123221321', params: { content: 'hello' }
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
expect(Webhooks::WhatsappEventsJob).to have_received(:perform_later)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
context 'when phone number is in inactive list' do
|
||||
@ -37,9 +39,10 @@ RSpec.describe 'Webhooks::WhatsappController', type: :request do
|
||||
|
||||
it 'returns service unavailable for inactive phone number in URL params' do
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
expect(Rails.logger).to receive(:warn).with('Rejected webhook for inactive WhatsApp number: +1234567890')
|
||||
|
||||
post '/webhooks/whatsapp/+1234567890', params: { content: 'hello' }
|
||||
|
||||
expect(Rails.logger).to have_received(:warn).with('Rejected webhook for inactive WhatsApp number: +1234567890')
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Inactive WhatsApp number')
|
||||
end
|
||||
@ -52,10 +55,38 @@ RSpec.describe 'Webhooks::WhatsappController', type: :request do
|
||||
|
||||
it 'processes the webhook normally' do
|
||||
allow(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||
expect(Webhooks::WhatsappEventsJob).to receive(:perform_later)
|
||||
|
||||
post '/webhooks/whatsapp/+1234567890', params: { content: 'hello' }
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
expect(Webhooks::WhatsappEventsJob).to have_received(:perform_later)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when awaitResponse param is present' do
|
||||
it 'calls the whatsapp events job synchronously' do
|
||||
allow(Webhooks::WhatsappEventsJob).to receive(:perform_now)
|
||||
|
||||
post '/webhooks/whatsapp/123221321', params: { content: 'hello', awaitResponse: true }
|
||||
|
||||
expect(Webhooks::WhatsappEventsJob).to have_received(:perform_now)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'returns 401 when InvalidWebhookVerifyToken is raised' do
|
||||
allow(Webhooks::WhatsappEventsJob).to receive(:perform_now).and_raise(Whatsapp::IncomingMessageBaileysService::InvalidWebhookVerifyToken)
|
||||
|
||||
post '/webhooks/whatsapp/123221321', params: { content: 'hello', awaitResponse: true }
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns 400 when MessageNotFoundError is raised' do
|
||||
allow(Webhooks::WhatsappEventsJob).to receive(:perform_now).and_raise(Whatsapp::IncomingMessageBaileysService::MessageNotFoundError)
|
||||
|
||||
post '/webhooks/whatsapp/123221321', params: { content: 'hello', awaitResponse: true }
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -320,6 +320,119 @@ describe Whatsapp::IncomingMessageBaileysService do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing messages.update event' do
|
||||
context 'when message is not found' do
|
||||
let(:message_id) { 'msg_123' }
|
||||
let(:update_payload) do
|
||||
{
|
||||
key: { id: message_id },
|
||||
update: {
|
||||
status: 2
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'raises MessageNotFoundError' do
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.update',
|
||||
data: [update_payload]
|
||||
}
|
||||
|
||||
expect do
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
end.to raise_error(Whatsapp::IncomingMessageBaileysService::MessageNotFoundError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is found' do
|
||||
let(:message_id) { 'msg_123' }
|
||||
let!(:message) { create(:message, source_id: message_id, status: 'sent') }
|
||||
|
||||
it 'updates the message status' do
|
||||
update_payload = { key: { id: message_id }, update: { status: 3 } }
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.update',
|
||||
data: [update_payload]
|
||||
}
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(message.reload.status).to eq('delivered')
|
||||
end
|
||||
|
||||
it 'updates the message content' do
|
||||
update_payload = {
|
||||
key: { id: message_id },
|
||||
update: {
|
||||
message: { editedMessage: { message: { conversation: 'New message content' } } }
|
||||
}
|
||||
}
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.update',
|
||||
data: [update_payload]
|
||||
}
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(message.reload.content).to eq('New message content')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status transition is not allowed (message already read)' do
|
||||
let(:message_id) { 'msg_123' }
|
||||
let!(:message) { create(:message, source_id: message_id, status: 'read') }
|
||||
|
||||
it 'does not update the status' do
|
||||
update_payload = { key: { id: message_id }, update: { status: 3 } }
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.update',
|
||||
data: [update_payload]
|
||||
}
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(message.reload.status).to eq('read')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update unsupported status' do
|
||||
let(:message_id) { 'msg_123' }
|
||||
let!(:message) { create(:message, source_id: message_id) } # rubocop:disable RSpec/LetSetup
|
||||
|
||||
it 'logs warning for unsupported played status' do
|
||||
update_payload = { key: { id: message_id }, update: { status: 5 } }
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.update',
|
||||
data: [update_payload]
|
||||
}
|
||||
allow(Rails.logger).to receive(:warn).with('Baileys unsupported message update status: PLAYED(5)')
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(Rails.logger).to have_received(:warn)
|
||||
end
|
||||
|
||||
it 'logs warning for unsupported status' do
|
||||
update_payload = { key: { id: message_id }, update: { status: 6 } }
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.update',
|
||||
data: [update_payload]
|
||||
}
|
||||
allow(Rails.logger).to receive(:warn).with('Baileys unsupported message update status: 6')
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(Rails.logger).to have_received(:warn)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def format_message_source_key(message_id)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user