fix: only subscribe calls webhook field when voice calling enabled (#14718)

## Description

Solves issue https://github.com/chatwoot/chatwoot/issues/14690

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How has this been tested?

- UI flows

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Tanmay Deep Sharma
2026-06-12 12:25:25 +05:30
committed by GitHub
co-authored by Muhsin Keloth
parent c041fde3a2
commit e055cead35
5 changed files with 40 additions and 31 deletions
+9 -7
View File
@@ -69,8 +69,10 @@ class Channel::Whatsapp < ApplicationRecord
end
end
# Enables voice: turns calling on at Meta (idempotent), subscribes the `calls`
# webhook field, and sets calling_enabled. Raises on Meta failure.
# Enables voice: turns calling on at Meta (idempotent), then re-registers webhooks
# with the in-memory calling_enabled flag so the `calls` field is subscribed. The
# flag is persisted only after registration succeeds, so a webhook failure can't
# leave the inbox reporting voice_enabled? while the WABA isn't subscribed to calls.
# Saved with validate: false to skip validate_provider_config's remote credential
# re-check, which could spuriously fail and desync the flag from Meta.
def enable_voice_calling!
@@ -78,21 +80,21 @@ class Channel::Whatsapp < ApplicationRecord
raise 'WhatsApp calling requires the channel_voice feature' unless account.feature_enabled?('channel_voice')
provider_service.update_calling_status('ENABLED')
webhook_setup_service.register_callback
self.provider_config = provider_config.merge('calling_enabled' => true)
webhook_setup_service.register_callback
save!(validate: false)
end
# Disables voice: unsets calling_enabled (gates the call subsystem) and drops
# `calls` from the webhook subscription (best-effort, so a Meta outage can't
# trap admins). Leaves Meta's WABA calling.status untouched.
# Disables voice: unsets calling_enabled (gates the call subsystem) and re-registers
# webhooks, which drops `calls` from the subscription (best-effort, so a Meta outage
# can't trap admins). Leaves Meta's WABA calling.status untouched.
def disable_voice_calling!
raise 'WhatsApp calling requires a whatsapp_cloud inbox' unless voice_calling_supported?
self.provider_config = provider_config.merge('calling_enabled' => false)
save!(validate: false)
begin
webhook_setup_service.register_callback(subscribed_fields: %w[messages smb_message_echoes])
webhook_setup_service.register_callback
rescue StandardError => e
Rails.logger.warn "[WHATSAPP CALL] disable webhook re-subscribe failed: #{e.message}"
end
+1 -1
View File
@@ -60,7 +60,7 @@ class Whatsapp::FacebookApiClient
data['code_verification_status'] == 'VERIFIED'
end
WEBHOOK_DEFAULT_FIELDS = %w[messages smb_message_echoes calls].freeze
WEBHOOK_DEFAULT_FIELDS = %w[messages smb_message_echoes].freeze
def subscribe_waba_webhook(waba_id, callback_url, verify_token, subscribed_fields: WEBHOOK_DEFAULT_FIELDS)
# Step 1: Subscribe app to WABA first (required before override)
+11 -9
View File
@@ -17,9 +17,9 @@ class Whatsapp::WebhookSetupService
setup_webhook
end
def register_callback(subscribed_fields: nil)
def register_callback
validate_parameters!
setup_webhook(subscribed_fields: subscribed_fields)
setup_webhook
end
private
@@ -55,21 +55,23 @@ class Whatsapp::WebhookSetupService
@channel.save!
end
def setup_webhook(subscribed_fields: nil)
def setup_webhook
callback_url = build_callback_url
verify_token = @channel.provider_config['webhook_verify_token']
args = [@waba_id, callback_url, verify_token]
if subscribed_fields
@api_client.subscribe_waba_webhook(*args, subscribed_fields: subscribed_fields)
else
@api_client.subscribe_waba_webhook(*args)
end
@api_client.subscribe_waba_webhook(@waba_id, callback_url, verify_token, subscribed_fields: subscribed_fields)
rescue StandardError => e
Rails.logger.error("[WHATSAPP] Webhook setup failed: #{e.message}")
raise "Webhook setup failed: #{e.message}"
end
# Subscribe to `calls` only when voice calling is enabled on the inbox
def subscribed_fields
fields = %w[messages smb_message_echoes]
fields << 'calls' if @channel.provider_config['calling_enabled']
fields
end
def build_callback_url
frontend_url = ENV.fetch('FRONTEND_URL', nil)
phone_number = @channel.phone_number
@@ -177,7 +177,7 @@ describe Whatsapp::FacebookApiClient do
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes calls] }.to_json
subscribed_fields: %w[messages smb_message_echoes] }.to_json
)
.to_return(
status: 200,
@@ -224,7 +224,7 @@ describe Whatsapp::FacebookApiClient do
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes calls] }.to_json
subscribed_fields: %w[messages smb_message_echoes] }.to_json
)
.to_return(status: 400, body: { error: 'Webhook callback override failed' }.to_json)
end
@@ -43,7 +43,7 @@ describe Whatsapp::WebhookSetupService do
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
@@ -51,7 +51,8 @@ describe Whatsapp::WebhookSetupService do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
service.perform
end
end
@@ -65,14 +66,15 @@ describe Whatsapp::WebhookSetupService do
throughput: { level: 'APPLICABLE' }
})
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
end
it 'does NOT register phone, but sets up webhook' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).not_to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
service.perform
end
end
@@ -88,7 +90,7 @@ describe Whatsapp::WebhookSetupService do
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
@@ -96,7 +98,8 @@ describe Whatsapp::WebhookSetupService do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
service.perform
end
end
@@ -112,7 +115,7 @@ describe Whatsapp::WebhookSetupService do
allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456)
allow(api_client).to receive(:register_phone_number).with('123456789', 223_456)
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
allow(channel).to receive(:save!)
end
@@ -120,7 +123,8 @@ describe Whatsapp::WebhookSetupService do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:register_phone_number).with('123456789', 223_456)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token')
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token', subscribed_fields: %w[messages
smb_message_echoes])
service.perform
end
end
@@ -279,14 +283,15 @@ describe Whatsapp::WebhookSetupService do
throughput: { level: 'APPLICABLE' }
})
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'existing_verify_token').and_return({ 'success' => true })
.with(waba_id, anything, 'existing_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
end
it 'successfully reauthorizes with new access token' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).not_to receive(:register_phone_number)
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'existing_verify_token')
.with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'existing_verify_token',
subscribed_fields: %w[messages smb_message_echoes])
service_reauth.perform
end
end
@@ -294,7 +299,7 @@ describe Whatsapp::WebhookSetupService do
it 'uses the existing webhook verify token during reauthorization' do
with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do
expect(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'existing_verify_token')
.with(waba_id, anything, 'existing_verify_token', subscribed_fields: %w[messages smb_message_echoes])
service_reauth.perform
end
end
@@ -308,7 +313,7 @@ describe Whatsapp::WebhookSetupService do
throughput: { level: 'APPLICABLE' }
})
allow(api_client).to receive(:subscribe_waba_webhook)
.with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true })
.with(waba_id, anything, 'test_verify_token', subscribed_fields: %w[messages smb_message_echoes]).and_return({ 'success' => true })
end
it 'completes successfully without errors' do