From d693d468d8a3f0f9ff53f1c6df2bd28811600d51 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Mon, 4 May 2026 15:38:28 +0700 Subject: [PATCH] feat(voice): bridge WhatsApp Cloud Calling to Chatwoot voice pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/models/attachment.rb | 12 ++ app/models/channel/whatsapp.rb | 6 +- app/services/base/send_on_channel_service.rb | 3 +- app/views/api/v1/models/_inbox.json.jbuilder | 3 + config/initializers/active_storage.rb | 13 ++ config/locales/en.yml | 7 + config/routes.rb | 15 ++ .../v1/accounts/whatsapp_calls_controller.rb | 162 ++++++++++++++++++ enterprise/app/models/call.rb | 2 - enterprise/app/models/channel/voice.rb | 8 + .../models/enterprise/concerns/attachment.rb | 17 ++ .../app/models/enterprise/conversation.rb | 15 ++ .../providers/whatsapp_cloud_service.rb | 17 +- .../messages/audio_transcription_service.rb | 4 +- .../app/services/whatsapp/call_service.rb | 102 +++++++++++ .../whatsapp/incoming_call_service.rb | 17 +- .../whatsapp_calls/accept.json.jbuilder | 1 + .../whatsapp_calls/initiate.json.jbuilder | 5 + .../whatsapp_calls/reject.json.jbuilder | 2 + .../whatsapp_calls/show.json.jbuilder | 1 + .../whatsapp_calls/terminate.json.jbuilder | 2 + .../upload_recording.json.jbuilder | 2 + .../v1/models/_whatsapp_call.json.jbuilder | 24 +++ 23 files changed, 431 insertions(+), 9 deletions(-) create mode 100644 config/initializers/active_storage.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb create mode 100644 enterprise/app/models/channel/voice.rb create mode 100644 enterprise/app/services/whatsapp/call_service.rb create mode 100644 enterprise/app/views/api/v1/accounts/whatsapp_calls/accept.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/whatsapp_calls/initiate.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/whatsapp_calls/reject.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/whatsapp_calls/show.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/whatsapp_calls/terminate.json.jbuilder create mode 100644 enterprise/app/views/api/v1/accounts/whatsapp_calls/upload_recording.json.jbuilder create mode 100644 enterprise/app/views/api/v1/models/_whatsapp_call.json.jbuilder diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 769e134be..8c87ff433 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -104,11 +104,23 @@ class Attachment < ApplicationRecord audio_file_data = base_data.merge(file_metadata) audio_file_data.merge( { + # ActiveStorage's redirect endpoint defaults to Content-Disposition: attachment, + # which makes