feat(voice): bridge WhatsApp Cloud Calling to Chatwoot voice pipeline

Wires Meta's WhatsApp Cloud Calling APIs into the voice subsystem so
agents can answer / place calls on WhatsApp Cloud inboxes from the
existing Voice flow. Browser ↔ Meta WebRTC is direct (no media-server
hop); Chatwoot owns the signalling, recording upload, and lifecycle
state on the Call/Message records.

Backend
- API surface: new Api::V1::Accounts::WhatsappCallsController with
  show / accept / reject / terminate / initiate / upload_recording.
  Enforces conversation-level Pundit visibility, blocks initiate when
  the channel isn't embedded-signup voice-enabled, and surfaces the
  Meta 138006 (no-call-permission) flow as a throttled opt-in template
  send under a conversation lock so concurrent retries can't double-send.
- Whatsapp::CallService — accept/reject/terminate state machine with
  call-level locking; wraps Meta API failures (transport or business)
  as Voice::CallErrors::CallFailed so the controller renders 422.
- Whatsapp::IncomingCallService — handles Meta connect/terminate
  webhooks; pins setup:active on outbound answers, materialises a
  missed-call record when terminate arrives before connect, and
  broadcasts voice_call.* events to assignee or account streams.
- Whatsapp::Providers::WhatsappCloudService — adds pre_accept/accept/
  reject/terminate/initiate/permission-request endpoints. Uses
  configurable WHATSAPP_API_VERSION (default v22) since the Calls API
  needs v17+ and OSS phone_id_path is locked at v13.
- Audio recordings: Attachment after_create_commit enqueues
  transcription and rebroadcasts the message so the FE bubble updates
  immediately. Active Storage initializer allows audio MIME types to
  serve inline. Audio transcription drops Whisper temperature to 0 to
  suppress hallucinations on silence.
- Channel::Voice stub backs legacy inbox rows whose channel_type
  survived the DropChannelVoice migration so jbuilder .try chains
  short-circuit instead of raising.

Routes / config
- /api/v1/accounts/:id/whatsapp_calls/* (enterprise-only)
- en.yml error strings under errors.whatsapp.calls.*

The FE consumer of this API ships separately on feat/whatsapp-call-ui.
This commit is contained in:
Tanmay Deep Sharma
2026-05-04 15:38:28 +07:00
parent 555894793e
commit d693d468d8
23 changed files with 431 additions and 9 deletions
+13
View File
@@ -0,0 +1,13 @@
# Allow audio attachments (call recordings, voice notes) to serve inline so the
# in-app <audio> player can stream them. Without this, ActiveStorage's blob model
# forces Content-Disposition: attachment for any MIME outside the default allowlist
# (images + PDF), which makes the browser download instead of play.
Rails.application.config.active_storage.content_types_allowed_inline += %w[
audio/webm
audio/ogg
audio/mpeg
audio/mp4
audio/x-m4a
audio/wav
audio/x-wav
]
+7
View File
@@ -114,6 +114,13 @@ en:
reauthorization:
generic: 'Failed to reauthorize WhatsApp. Please try again.'
not_supported: 'Reauthorization is not supported for this type of WhatsApp channel.'
calls:
not_enabled: 'Calling is not enabled for this inbox'
no_recording: 'No recording file provided'
no_message: 'Call has no associated message'
sdp_offer_required: 'sdp_offer is required'
contact_phone_required: 'Contact phone number is required'
permission_request_failed: 'Failed to send call permission request'
inboxes:
imap:
socket_error: Please check the network connection, IMAP address and try again.
+15
View File
@@ -215,6 +215,21 @@ Rails.application.routes.draw do
end
end
resources :reporting_events, only: [:index] if ChatwootApp.enterprise?
if ChatwootApp.enterprise?
resources :whatsapp_calls, only: [:show] do
member do
post :accept
post :reject
post :terminate
post :upload_recording
end
collection do
post :initiate
end
end
end
resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy]
resources :custom_filters, only: [:index, :show, :create, :update, :destroy]
resources :inboxes, only: [:index, :show, :create, :update, :destroy] do