This PR delivers the first slice of the voice channel: inbound call handling. When a customer calls a configured voice number, Chatwoot now creates a new conversation and shows a dedicated call bubble in the UI. As the call progresses (ringing, answered, completed), its status updates in real time in both the conversation list and the call bubble, so agents can instantly see what’s happening. This focuses on the inbound flow and is part of breaking the larger voice feature into smaller, functional, and testable units; further enhancements will follow in subsequent PRs. references: #11602 , #11481 ## Testing - Configure a Voice inbox in Chatwoot with your Twilio number. - Place a call to that number. - Verify a new conversation appears in the Voice inbox for the call. - Open it and confirm a dedicated voice call message bubble is shown. - Watch status update live (ringing/answered); hang up and see it change to completed in both the bubble and conversation list. - to test missed call status, make sure to hangup the call before the please wait while we connect you to an agent message plays ## Screens <img width="400" alt="Screenshot 2025-09-03 at 3 11 25 PM" src="https://github.com/user-attachments/assets/d6a1d2ff-2ded-47b7-9144-a9d898beb380" /> <img width="700" alt="Screenshot 2025-09-03 at 3 11 33 PM" src="https://github.com/user-attachments/assets/c25e6a1e-a885-47f7-b3d7-c3e15eef18c7" /> <img width="700" alt="Screenshot 2025-09-03 at 3 11 57 PM" src="https://github.com/user-attachments/assets/29e7366d-b1d4-4add-a062-4646d2bff435" /> <img width="442" height="255" alt="Screenshot 2025-09-04 at 11 55 01 PM" src="https://github.com/user-attachments/assets/703126f6-a448-49d9-9c02-daf3092cc7f9" /> --------- Co-authored-by: Muhsin <muhsinkeramam@gmail.com>
100 lines
2.8 KiB
Ruby
100 lines
2.8 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: channel_voice
|
|
#
|
|
# id :bigint not null, primary key
|
|
# additional_attributes :jsonb
|
|
# phone_number :string not null
|
|
# provider :string default("twilio"), not null
|
|
# provider_config :jsonb not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :integer not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_channel_voice_on_account_id (account_id)
|
|
# index_channel_voice_on_phone_number (phone_number) UNIQUE
|
|
#
|
|
class Channel::Voice < ApplicationRecord
|
|
include Channelable
|
|
|
|
self.table_name = 'channel_voice'
|
|
|
|
validates :phone_number, presence: true, uniqueness: true
|
|
validates :provider, presence: true
|
|
validates :provider_config, presence: true
|
|
|
|
# Validate phone number format (E.164 format)
|
|
validates :phone_number, format: { with: /\A\+[1-9]\d{1,14}\z/ }
|
|
|
|
# Provider-specific configs stored in JSON
|
|
validate :validate_provider_config
|
|
before_validation :provision_twilio_on_create, on: :create, if: :twilio?
|
|
|
|
EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze
|
|
|
|
def name
|
|
"Voice (#{phone_number})"
|
|
end
|
|
|
|
def messaging_window_enabled?
|
|
false
|
|
end
|
|
|
|
# Public URLs used to configure Twilio webhooks
|
|
def voice_call_webhook_url
|
|
digits = phone_number.delete_prefix('+')
|
|
"#{ENV.fetch('FRONTEND_URL', nil)}/twilio/voice/call/#{digits}"
|
|
end
|
|
|
|
def voice_status_webhook_url
|
|
digits = phone_number.delete_prefix('+')
|
|
"#{ENV.fetch('FRONTEND_URL', nil)}/twilio/voice/status/#{digits}"
|
|
end
|
|
|
|
private
|
|
|
|
def twilio?
|
|
provider == 'twilio'
|
|
end
|
|
|
|
def validate_provider_config
|
|
return if provider_config.blank?
|
|
|
|
case provider
|
|
when 'twilio'
|
|
validate_twilio_config
|
|
end
|
|
end
|
|
|
|
def validate_twilio_config
|
|
config = provider_config.with_indifferent_access
|
|
# Require credentials and provisioned TwiML App SID
|
|
required_keys = %w[account_sid auth_token api_key_sid api_key_secret twiml_app_sid]
|
|
required_keys.each do |key|
|
|
errors.add(:provider_config, "#{key} is required for Twilio provider") if config[key].blank?
|
|
end
|
|
end
|
|
|
|
def provision_twilio_on_create
|
|
service = ::Twilio::VoiceWebhookSetupService.new(channel: self)
|
|
app_sid = service.perform
|
|
return if app_sid.blank?
|
|
|
|
cfg = provider_config.with_indifferent_access
|
|
cfg[:twiml_app_sid] = app_sid
|
|
self.provider_config = cfg
|
|
rescue StandardError => e
|
|
error_details = {
|
|
error_class: e.class.to_s,
|
|
message: e.message,
|
|
phone_number: phone_number,
|
|
account_id: account_id,
|
|
backtrace: e.backtrace&.first(5)
|
|
}
|
|
Rails.logger.error("TWILIO_VOICE_SETUP_ON_CREATE_ERROR: #{error_details}")
|
|
errors.add(:base, "Twilio setup failed: #{e.message}")
|
|
end
|
|
end
|