feat(whatsapp): delete messages on baileys/zapi providers (#194)
* feat(baileys): implement message deletion functionality * feat(zapi): add message deletion functionality and corresponding tests * feat(whatsapp): update message deletion logic for provider compatibility * feat(whatsapp): enhance message deletion logic to handle missing phone numbers
This commit is contained in:
parent
ec8366aabd
commit
77c90a69ca
@ -27,6 +27,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true })
|
||||
message.attachments.destroy_all
|
||||
end
|
||||
delete_message_on_channel
|
||||
end
|
||||
|
||||
def retry
|
||||
@ -76,6 +77,15 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
message.translations.present? && message.translations[permitted_params[:target_language]].present?
|
||||
end
|
||||
|
||||
def delete_message_on_channel
|
||||
return unless @conversation.inbox.channel.respond_to?(:delete_message)
|
||||
return if message.source_id.blank?
|
||||
|
||||
@conversation.inbox.channel.delete_message(message, conversation: @conversation)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to delete message on channel: #{e.message}"
|
||||
end
|
||||
|
||||
# API inbox check
|
||||
def ensure_api_inbox
|
||||
# Only API inboxes can update messages
|
||||
|
||||
@ -139,6 +139,19 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
provider_service.on_whatsapp(phone_number)
|
||||
end
|
||||
|
||||
def delete_message(message, conversation:)
|
||||
return unless provider_service.respond_to?(:delete_message)
|
||||
|
||||
recipient_id = if provider == 'zapi'
|
||||
conversation.contact.phone_number.presence || conversation.contact.identifier
|
||||
else
|
||||
conversation.contact.identifier || conversation.contact.phone_number
|
||||
end
|
||||
return if recipient_id.blank?
|
||||
|
||||
provider_service.delete_message(recipient_id, message)
|
||||
end
|
||||
|
||||
delegate :setup_channel_provider, to: :provider_service
|
||||
delegate :send_message, to: :provider_service
|
||||
delegate :send_template, to: :provider_service
|
||||
|
||||
@ -241,6 +241,27 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
||||
response.parsed_response&.first || { 'jid' => remote_jid, 'exists' => false }
|
||||
end
|
||||
|
||||
def delete_message(recipient_id, message)
|
||||
@recipient_id = recipient_id
|
||||
|
||||
response = HTTParty.delete(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/messages",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
jid: remote_jid,
|
||||
key: {
|
||||
id: message.source_id,
|
||||
remoteJid: remote_jid,
|
||||
fromMe: message.message_type == 'outgoing'
|
||||
}
|
||||
}.to_json
|
||||
)
|
||||
|
||||
raise ProviderUnavailableError unless process_response(response)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_url
|
||||
@ -362,5 +383,6 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
||||
:read_messages,
|
||||
:unread_message,
|
||||
:received_messages,
|
||||
:on_whatsapp
|
||||
:on_whatsapp,
|
||||
:delete_message
|
||||
end
|
||||
|
||||
@ -116,6 +116,26 @@ class Whatsapp::Providers::WhatsappZapiService < Whatsapp::Providers::BaseServic
|
||||
response.parsed_response || { 'exists' => false, 'phone' => nil, 'lid' => nil }
|
||||
end
|
||||
|
||||
def delete_message(recipient_id, message)
|
||||
return false if recipient_id.blank?
|
||||
|
||||
phone = recipient_id.delete('+')
|
||||
|
||||
response = HTTParty.delete(
|
||||
"#{api_instance_path_with_token}/messages",
|
||||
headers: api_headers,
|
||||
query: {
|
||||
messageId: message.source_id,
|
||||
phone: phone,
|
||||
owner: message.message_type == 'outgoing'
|
||||
}
|
||||
)
|
||||
|
||||
raise ProviderUnavailableError unless process_response(response)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_instance_path
|
||||
|
||||
@ -275,6 +275,88 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel supports delete_message' do
|
||||
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'baileys', account: account, validate_provider_config: false) }
|
||||
let(:whatsapp_inbox) { whatsapp_channel.inbox }
|
||||
let(:contact) { create(:contact, account: account, identifier: '+551187654321', phone_number: '+551187654321') }
|
||||
let(:contact_inbox) { create(:contact_inbox, inbox: whatsapp_inbox, contact: contact) }
|
||||
let(:whatsapp_conversation) { create(:conversation, inbox: whatsapp_inbox, account: account, contact: contact, contact_inbox: contact_inbox) }
|
||||
let(:message_with_source) do
|
||||
create(:message, account: account, conversation: whatsapp_conversation, inbox: whatsapp_inbox, source_id: 'msg_123', message_type: :outgoing)
|
||||
end
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:delete_request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/messages" }
|
||||
|
||||
before do
|
||||
create(:inbox_member, inbox: whatsapp_inbox, user: agent)
|
||||
end
|
||||
|
||||
it 'calls delete_message on the channel' do
|
||||
delete_stub = stub_request(:delete, delete_request_path)
|
||||
.with(
|
||||
headers: { 'Content-Type' => 'application/json', 'x-api-key' => whatsapp_channel.provider_config['api_key'] },
|
||||
body: hash_including(jid: "#{contact.identifier.delete('+')}@s.whatsapp.net")
|
||||
)
|
||||
.to_return(status: 200, body: '{}')
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{whatsapp_conversation.display_id}/messages/#{message_with_source.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(message_with_source.reload.deleted).to be true
|
||||
expect(delete_stub).to have_been_requested
|
||||
end
|
||||
|
||||
it 'does not fail when channel delete_message raises an error' do
|
||||
stub_request(:delete, delete_request_path)
|
||||
.to_return(status: 400, body: 'Provider error')
|
||||
|
||||
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
|
||||
.to_return(status: 200)
|
||||
|
||||
allow(Rails.logger).to receive(:error)
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{whatsapp_conversation.display_id}/messages/#{message_with_source.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(message_with_source.reload.deleted).to be true
|
||||
end
|
||||
|
||||
it 'skips channel deletion when message has no source_id' do
|
||||
message_without_source = create(:message, account: account, conversation: whatsapp_conversation, inbox: whatsapp_inbox, source_id: nil)
|
||||
delete_stub = stub_request(:delete, delete_request_path).to_return(status: 200, body: '{}')
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{whatsapp_conversation.display_id}/messages/#{message_without_source.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(message_without_source.reload.deleted).to be true
|
||||
expect(delete_stub).not_to have_been_requested
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel does not support delete_message' do
|
||||
let(:message_with_source) { create(:message, account: account, conversation: conversation, source_id: 'msg_123') }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, inbox: conversation.inbox, user: agent)
|
||||
end
|
||||
|
||||
it 'skips channel deletion' do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{message_with_source.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(message_with_source.reload.deleted).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id/retry' do
|
||||
|
||||
@ -378,6 +378,58 @@ RSpec.describe Channel::Whatsapp do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_message' do
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) }
|
||||
let(:contact) { create(:contact, identifier: '+551187654321') }
|
||||
let(:contact_inbox) { create(:contact_inbox, inbox: channel.inbox, contact: contact) }
|
||||
let(:conversation) { create(:conversation, inbox: channel.inbox, contact: contact, contact_inbox: contact_inbox) }
|
||||
let(:message) { create(:message, conversation: conversation, inbox: channel.inbox, source_id: 'msg_123', message_type: :outgoing) }
|
||||
|
||||
it 'calls provider service delete_message method for baileys' do
|
||||
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, delete_message: true)
|
||||
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
|
||||
.with(whatsapp_channel: channel)
|
||||
.and_return(provider_double)
|
||||
|
||||
channel.delete_message(message, conversation: conversation)
|
||||
|
||||
expect(provider_double).to have_received(:delete_message).with(contact.identifier, message)
|
||||
end
|
||||
|
||||
it 'calls provider service delete_message method for zapi with phone_number' do
|
||||
contact.update!(phone_number: '+551199999999')
|
||||
channel.update!(provider: 'zapi')
|
||||
provider_double = instance_double(Whatsapp::Providers::WhatsappZapiService, delete_message: true)
|
||||
allow(Whatsapp::Providers::WhatsappZapiService).to receive(:new)
|
||||
.with(whatsapp_channel: channel)
|
||||
.and_return(provider_double)
|
||||
|
||||
channel.delete_message(message, conversation: conversation)
|
||||
|
||||
expect(provider_double).to have_received(:delete_message).with(contact.phone_number, message)
|
||||
end
|
||||
|
||||
it 'calls provider service delete_message method for zapi falling back to identifier when phone_number is blank' do
|
||||
channel.update!(provider: 'zapi')
|
||||
provider_double = instance_double(Whatsapp::Providers::WhatsappZapiService, delete_message: true)
|
||||
allow(Whatsapp::Providers::WhatsappZapiService).to receive(:new)
|
||||
.with(whatsapp_channel: channel)
|
||||
.and_return(provider_double)
|
||||
|
||||
channel.delete_message(message, conversation: conversation)
|
||||
|
||||
expect(provider_double).to have_received(:delete_message).with(contact.identifier, message)
|
||||
end
|
||||
|
||||
it 'does not call method if provider service does not implement it' do
|
||||
channel.update!(provider: 'whatsapp_cloud')
|
||||
|
||||
expect do
|
||||
channel.delete_message(message, conversation: conversation)
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
describe '#disconnect_channel_provider' do
|
||||
context 'when provider implements the method' do
|
||||
|
||||
@ -804,6 +804,75 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_message' do
|
||||
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/messages" }
|
||||
let(:outgoing_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_456', message_type: :outgoing) }
|
||||
let(:incoming_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_789', message_type: :incoming) }
|
||||
|
||||
context 'when deleting an outgoing message' do
|
||||
it 'sends delete request with fromMe true' do
|
||||
stub_request(:delete, request_path)
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
jid: test_send_jid,
|
||||
key: {
|
||||
id: outgoing_message.source_id,
|
||||
remoteJid: test_send_jid,
|
||||
fromMe: true
|
||||
}
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200, body: '{}')
|
||||
|
||||
result = service.delete_message(test_send_phone_number, outgoing_message)
|
||||
|
||||
expect(result).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deleting an incoming message' do
|
||||
it 'sends delete request with fromMe false' do
|
||||
stub_request(:delete, request_path)
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
jid: test_send_jid,
|
||||
key: {
|
||||
id: incoming_message.source_id,
|
||||
remoteJid: test_send_jid,
|
||||
fromMe: false
|
||||
}
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200, body: '{}')
|
||||
|
||||
result = service.delete_message(test_send_phone_number, incoming_message)
|
||||
|
||||
expect(result).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is unsuccessful' do
|
||||
it 'raises ProviderUnavailableError and logs the error' do
|
||||
stub_request(:delete, request_path)
|
||||
.with(headers: stub_headers(whatsapp_channel))
|
||||
.to_return(status: 400, body: 'error message')
|
||||
|
||||
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
|
||||
.to_return(status: 200)
|
||||
|
||||
allow(Rails.logger).to receive(:error)
|
||||
|
||||
expect do
|
||||
service.delete_message(test_send_phone_number, outgoing_message)
|
||||
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
|
||||
|
||||
expect(Rails.logger).to have_received(:error).with('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_URL is set' do
|
||||
it 'uses the base url from the environment variable' do
|
||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
|
||||
|
||||
@ -288,6 +288,60 @@ describe Whatsapp::Providers::WhatsappZapiService do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_message' do
|
||||
let(:outgoing_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_456', message_type: :outgoing) }
|
||||
let(:incoming_message) { create(:message, inbox: whatsapp_channel.inbox, source_id: 'msg_789', message_type: :incoming) }
|
||||
|
||||
context 'when deleting an outgoing message' do
|
||||
it 'sends delete request with owner true' do
|
||||
stub_request(:delete, "#{api_instance_path_with_token}/messages")
|
||||
.with(
|
||||
headers: stub_headers,
|
||||
query: { messageId: outgoing_message.source_id, phone: test_send_phone_number, owner: 'true' }
|
||||
)
|
||||
.to_return(status: 204, body: '{}')
|
||||
|
||||
result = service.delete_message("+#{test_send_phone_number}", outgoing_message)
|
||||
|
||||
expect(result).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deleting an incoming message' do
|
||||
it 'sends delete request with owner false' do
|
||||
stub_request(:delete, "#{api_instance_path_with_token}/messages")
|
||||
.with(
|
||||
headers: stub_headers,
|
||||
query: { messageId: incoming_message.source_id, phone: test_send_phone_number, owner: 'false' }
|
||||
)
|
||||
.to_return(status: 204, body: '{}')
|
||||
|
||||
result = service.delete_message("+#{test_send_phone_number}", incoming_message)
|
||||
|
||||
expect(result).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is unsuccessful' do
|
||||
it 'raises ProviderUnavailableError and logs the error' do
|
||||
stub_request(:delete, "#{api_instance_path_with_token}/messages")
|
||||
.with(
|
||||
headers: stub_headers,
|
||||
query: { messageId: outgoing_message.source_id, phone: test_send_phone_number, owner: 'true' }
|
||||
)
|
||||
.to_return(status: 400, body: 'error message')
|
||||
|
||||
allow(Rails.logger).to receive(:error)
|
||||
|
||||
expect do
|
||||
service.delete_message("+#{test_send_phone_number}", outgoing_message)
|
||||
end.to raise_error(Whatsapp::Providers::WhatsappZapiService::ProviderUnavailableError)
|
||||
|
||||
expect(Rails.logger).to have_received(:error).with('error message')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_message' do
|
||||
let(:request_path) { "#{api_instance_path_with_token}/send-text" }
|
||||
let(:result_body) { { 'messageId' => 'msg_123' } }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user