diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 3f416a5b5..bfd786e4f 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -93,6 +93,20 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') } end + def on_whatsapp + params.require(:phone_number) + phone_number = params[:phone_number] + channel = @inbox.channel + + unless channel.respond_to?(:on_whatsapp) + render json: { error: 'Channel does not support whatsapp check' }, status: :unprocessable_entity and return + end + + response = channel.on_whatsapp(phone_number) + + render json: response, status: :ok + end + private def fetch_inbox diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 7995a69b4..f7b5e0eb2 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -122,6 +122,12 @@ class Channel::Whatsapp < ApplicationRecord provider_service.received_messages(conversation.contact.phone_number, messages) end + def on_whatsapp(phone_number) + return unless provider_service.respond_to?(:on_whatsapp) + + provider_service.on_whatsapp(phone_number) + end + delegate :setup_channel_provider, to: :provider_service delegate :send_message, to: :provider_service delegate :send_template, to: :provider_service diff --git a/app/policies/inbox_policy.rb b/app/policies/inbox_policy.rb index 519f7f7f2..696139028 100644 --- a/app/policies/inbox_policy.rb +++ b/app/policies/inbox_policy.rb @@ -61,4 +61,8 @@ class InboxPolicy < ApplicationPolicy def disconnect_channel_provider? @account_user.administrator? end + + def on_whatsapp? + true + end end diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 1d9c68f7f..0023680de 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -189,6 +189,22 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer true end + def on_whatsapp(phone_number) + @phone_number = phone_number + + response = HTTParty.post( + "#{provider_url}/connections/#{whatsapp_channel.phone_number}/on-whatsapp", + headers: api_headers, + body: { + jids: [remote_jid] + }.to_json + ) + + raise ProviderUnavailableError unless process_response(response) + + response.parsed_response&.first || { 'jid' => remote_jid, 'exists' => false, 'lid' => nil } + end + private def provider_url @@ -255,6 +271,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer response.success? end + # FIXME: Make this a function with argument, instead of using instance variable def remote_jid "#{@phone_number.delete('+')}@s.whatsapp.net" end @@ -306,5 +323,6 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer :update_presence, :read_messages, :unread_message, - :received_messages + :received_messages, + :on_whatsapp end diff --git a/config/routes.rb b/config/routes.rb index 6b52d95bc..6ca1322cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -184,6 +184,7 @@ Rails.application.routes.draw do post :setup_channel_provider, on: :member post :disconnect_channel_provider, on: :member delete :avatar, on: :member + post :on_whatsapp, on: :member end resources :inbox_members, only: [:create, :show], param: :inbox_id do collection do diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index f7bb2e699..49a37d7d8 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -1021,4 +1021,71 @@ RSpec.describe 'Inboxes API', type: :request do end end end + + describe 'POST /api/v1/accounts/:account_id/inboxes/:id/on_whatsapp' do + let(:channel) { create(:channel_whatsapp, account: account, provider: 'baileys', validate_provider_config: false) } + let(:inbox) { channel.inbox } + + context 'when unauthenticated' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/on_whatsapp" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated' do + it 'returns unprocessable entity when channel does not support on_whatsapp' do + inbox = create(:inbox, account: account) + + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/on_whatsapp", + headers: admin.create_new_auth_token, + params: { phone_number: '+123456789' }, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq('Channel does not support whatsapp check') + end + + it 'returns unprocessable entity when phone_number is not passed' do + inbox = create(:inbox, account: account) + + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/on_whatsapp", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq('param is missing or the value is empty: phone_number') + end + + it 'calls on_whatsapp when supported and returns provider response' do + service_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, + on_whatsapp: { jid: '123456789@s.whatsapp.net', exists: true, lid: '123@lid' }) + allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new) + .with(whatsapp_channel: channel) + .and_return(service_double) + + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/on_whatsapp", + headers: admin.create_new_auth_token, + params: { phone_number: '+123456789' }, + as: :json + + expect(response).to have_http_status(:ok) + end + + it 'calls on_whatsapp when supported and returns default response on no response from provider' do + service_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, on_whatsapp: nil) + allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new) + .with(whatsapp_channel: channel) + .and_return(service_double) + + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/on_whatsapp", + headers: admin.create_new_auth_token, + params: { phone_number: '+123456789' }, + as: :json + + expect(response).to have_http_status(:ok) + end + end + end end diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index 0c47bf2bd..455783079 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -295,6 +295,32 @@ RSpec.describe Channel::Whatsapp do end end + describe '#on_whatsapp' do + let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) } + let(:conversation) { create(:conversation) } + let(:phone_number) { '+123456789' } + + it 'calls provider service method' do + provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, on_whatsapp: nil) + allow(provider_double).to receive(:on_whatsapp).with(phone_number) + allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new) + .with(whatsapp_channel: channel) + .and_return(provider_double) + + channel.on_whatsapp(phone_number) + + expect(provider_double).to have_received(:on_whatsapp) + end + + it 'does not call method if provider service does not implement it' do + channel.update!(provider: 'whatsapp_cloud') + + expect do + channel.on_whatsapp(phone_number) + end.not_to raise_error + end + end + describe 'callbacks' do describe '#disconnect_channel_provider' do context 'when provider is baileys' do diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index 282cfdc4e..69cd28074 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -642,6 +642,64 @@ describe Whatsapp::Providers::WhatsappBaileysService do end end + describe '#on_whatsapp' do + let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/on-whatsapp" } + let(:phone_number) { '+123456789' } + + context 'when response is successful' do + it 'requests whatsapp check' do + stub_request(:post, request_path) + .with(headers: stub_headers(whatsapp_channel), body: { jids: ["#{phone_number.delete('+')}@s.whatsapp.net"] }.to_json) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: [{ jid: "#{phone_number.delete('+')}@s.whatsapp.net", exists: true, lid: '123@lid' }].to_json + ) + + response = service.on_whatsapp(phone_number) + + expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => true, 'lid' => '123@lid' }) + end + + it 'returns default check response' do + stub_request(:post, request_path) + .with(headers: stub_headers(whatsapp_channel), body: { jids: ["#{phone_number.delete('+')}@s.whatsapp.net"] }.to_json) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: [].to_json + ) + + response = service.on_whatsapp(phone_number) + + expect(response).to eq({ 'jid' => "#{phone_number.delete('+')}@s.whatsapp.net", 'exists' => false, 'lid' => nil }) + end + end + + context 'when response is unsuccessful' do + it 'raises ProviderUnavailableError and logs the error' do + stub_request(:post, request_path) + .with(headers: stub_headers(whatsapp_channel), body: { jids: ["#{phone_number.delete('+')}@s.whatsapp.net"] }.to_json) + .to_return( + status: 400, + body: 'error message', + headers: {} + ) + + 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.on_whatsapp(phone_number) + 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')