## Linear ticket https://linear.app/chatwoot/issue/CW-7187/voice-calls-followup-tasks ## Description Improvements to the WhatsApp voice-calling experience plus a cheaper, more accurate audio-transcription model. - First-time callers now get a real name. An inbound WhatsApp call creates the contact from the caller's WhatsApp profile name instead of the bare phone number. - Clear, consistent call attribution. Call bubbles show a unified "Handled by {agent}" - Cleaner call widget. The dismiss (✕) button is shown only for incoming calls - WhatsApp calling for manual inboxes. voice_calling_supported? now covers any whatsapp_cloud inbox - Transcription: whisper-1 → gpt-4o-mini-transcribe. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## 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
143 lines
5.1 KiB
Ruby
143 lines
5.1 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: channel_whatsapp
|
|
#
|
|
# id :bigint not null, primary key
|
|
# message_templates :jsonb
|
|
# message_templates_last_updated :datetime
|
|
# phone_number :string not null
|
|
# provider :string default("default")
|
|
# provider_config :jsonb
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :integer not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_channel_whatsapp_on_phone_number (phone_number) UNIQUE
|
|
#
|
|
|
|
class Channel::Whatsapp < ApplicationRecord
|
|
include Channelable
|
|
include Reauthorizable
|
|
|
|
self.table_name = 'channel_whatsapp'
|
|
EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze
|
|
|
|
# default at the moment is 360dialog lets change later.
|
|
PROVIDERS = %w[default whatsapp_cloud].freeze
|
|
before_validation :ensure_webhook_verify_token
|
|
|
|
validates :provider, inclusion: { in: PROVIDERS }
|
|
validates :phone_number, presence: true, uniqueness: true
|
|
validate :validate_provider_config
|
|
|
|
after_create :sync_templates
|
|
before_destroy :teardown_webhooks
|
|
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
|
|
|
|
def name
|
|
'Whatsapp'
|
|
end
|
|
|
|
# Mirrors Channel::TwilioSms#voice_enabled? so the call subsystem can duck-type across providers.
|
|
# Meta's Calling API is available to any whatsapp_cloud inbox (embedded-signup or manual keys);
|
|
# only 360dialog (default provider) can't reach the call APIs.
|
|
def voice_enabled?
|
|
voice_calling_supported? &&
|
|
provider_config['calling_enabled'].present? &&
|
|
account.feature_enabled?('channel_voice')
|
|
end
|
|
|
|
# Whether this inbox can do WhatsApp calling at all. Meta's Calling API is
|
|
# reachable by any whatsapp_cloud inbox, so 360dialog inboxes can't be toggled
|
|
# on even though calling_enabled would persist.
|
|
def voice_calling_supported?
|
|
provider == 'whatsapp_cloud'
|
|
end
|
|
|
|
def provider_service
|
|
if provider == 'whatsapp_cloud'
|
|
Whatsapp::Providers::WhatsappCloudService.new(whatsapp_channel: self)
|
|
else
|
|
Whatsapp::Providers::Whatsapp360DialogService.new(whatsapp_channel: self)
|
|
end
|
|
end
|
|
|
|
# Enables voice: turns calling on at Meta (idempotent), subscribes the `calls`
|
|
# webhook field, and sets calling_enabled. Raises on Meta failure.
|
|
# 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!
|
|
raise 'WhatsApp calling requires a whatsapp_cloud inbox' unless voice_calling_supported?
|
|
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)
|
|
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.
|
|
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])
|
|
rescue StandardError => e
|
|
Rails.logger.warn "[WHATSAPP CALL] disable webhook re-subscribe failed: #{e.message}"
|
|
end
|
|
end
|
|
|
|
def mark_message_templates_updated
|
|
# rubocop:disable Rails/SkipsModelValidations
|
|
update_column(:message_templates_last_updated, Time.zone.now)
|
|
# rubocop:enable Rails/SkipsModelValidations
|
|
end
|
|
|
|
delegate :send_message, to: :provider_service
|
|
delegate :send_template, to: :provider_service
|
|
delegate :sync_templates, to: :provider_service
|
|
delegate :media_url, to: :provider_service
|
|
delegate :api_headers, to: :provider_service
|
|
|
|
def setup_webhooks
|
|
perform_webhook_setup
|
|
rescue StandardError => e
|
|
Rails.logger.error "[WHATSAPP] Webhook setup failed: #{e.message}"
|
|
prompt_reauthorization!
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_webhook_verify_token
|
|
provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud'
|
|
end
|
|
|
|
def validate_provider_config
|
|
errors.add(:provider_config, 'Invalid Credentials') unless provider_service.validate_provider_config?
|
|
end
|
|
|
|
def perform_webhook_setup
|
|
webhook_setup_service.perform
|
|
end
|
|
|
|
def webhook_setup_service
|
|
Whatsapp::WebhookSetupService.new(self, provider_config['business_account_id'], provider_config['api_key'])
|
|
end
|
|
|
|
def teardown_webhooks
|
|
Whatsapp::WebhookTeardownService.new(self).perform
|
|
end
|
|
|
|
def should_auto_setup_webhooks?
|
|
# Only auto-setup webhooks for whatsapp_cloud provider with manual setup
|
|
# Embedded signup calls setup_webhooks explicitly in EmbeddedSignupService
|
|
provider == 'whatsapp_cloud' && provider_config['source'] != 'embedded_signup'
|
|
end
|
|
end
|