Voice calling is now a capability on the existing TwilioSms rather than a separate Voice model. A single Twilio phone number handles both SMS and voice calls through one inbox. Fixes https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms **What changed** - Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms - Added voice_enabled, twiml_app_sid, api_key_secret columns to channel_twilio_sms table - Dropped channel_voice table (no production data) - All voice logic lives in Enterprise layer via prepend_mod_with('Channel::TwilioSms') - Added Voice settings tab on Twilio SMS inbox settings to enable/disable voice - Validates Twilio number voice capability before provisioning - Teardown service cleans up TwiML app and credentials when voice is disabled - Frontend voice detection uses isVoiceCallEnabled() / getVoiceCallProvider() helpers — extensible to future providers - Gated by channel_voice feature flag **How to test** 1. Enable feature flag: Account.find(<id>).enable_features('channel_voice') 2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials → verify incoming/outgoing calls and SMS work 3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox → Voice tab → toggle on → provide API key credentials → verify calls work 4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted, credentials cleared, SMS still works 5. Re-enable voice: Toggle on again → must provide api_key_secret again → new TwiML app provisioned --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
84 lines
2.4 KiB
Ruby
84 lines
2.4 KiB
Ruby
class Twilio::VoiceWebhookSetupService
|
|
include Rails.application.routes.url_helpers
|
|
|
|
pattr_initialize [:channel!]
|
|
|
|
HTTP_METHOD = 'POST'.freeze
|
|
|
|
# Returns created TwiML App SID on success.
|
|
def perform
|
|
validate_token_credentials!
|
|
|
|
app_sid = create_twiml_app!
|
|
configure_number_webhooks!
|
|
app_sid
|
|
end
|
|
|
|
private
|
|
|
|
def validate_token_credentials!
|
|
channel.client.incoming_phone_numbers.list(limit: 1)
|
|
rescue StandardError => e
|
|
log_twilio_error('AUTH_VALIDATION_TOKEN', e)
|
|
raise
|
|
end
|
|
|
|
def create_twiml_app!
|
|
friendly_name = "Chatwoot Voice #{channel.phone_number}"
|
|
app = channel.client.applications.create(
|
|
friendly_name: friendly_name,
|
|
voice_url: channel.voice_call_webhook_url,
|
|
voice_method: HTTP_METHOD
|
|
)
|
|
app.sid
|
|
rescue StandardError => e
|
|
log_twilio_error('TWIML_APP_CREATE', e)
|
|
raise
|
|
end
|
|
|
|
def configure_number_webhooks!
|
|
numbers = channel.client.incoming_phone_numbers.list(phone_number: channel.phone_number)
|
|
if numbers.empty?
|
|
Rails.logger.warn "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
|
|
return
|
|
end
|
|
|
|
channel.client
|
|
.incoming_phone_numbers(numbers.first.sid)
|
|
.update(
|
|
voice_url: channel.voice_call_webhook_url,
|
|
voice_method: HTTP_METHOD,
|
|
status_callback: channel.voice_status_webhook_url,
|
|
status_callback_method: HTTP_METHOD
|
|
)
|
|
rescue StandardError => e
|
|
log_twilio_error('NUMBER_WEBHOOKS_UPDATE', e)
|
|
raise
|
|
end
|
|
|
|
def log_twilio_error(context, error)
|
|
details = build_error_details(context, error)
|
|
add_twilio_specific_details(details, error)
|
|
|
|
backtrace = error.backtrace.is_a?(Array) ? error.backtrace.first(5) : []
|
|
Rails.logger.error("TWILIO_VOICE_SETUP_ERROR: #{details} backtrace=#{backtrace}")
|
|
end
|
|
|
|
def build_error_details(context, error)
|
|
{
|
|
context: context,
|
|
phone_number: channel.phone_number,
|
|
account_sid: channel.account_sid,
|
|
error_class: error.class.to_s,
|
|
message: error.message
|
|
}
|
|
end
|
|
|
|
def add_twilio_specific_details(details, error)
|
|
details[:status_code] = error.status_code if error.respond_to?(:status_code)
|
|
details[:twilio_code] = error.code if error.respond_to?(:code)
|
|
details[:more_info] = error.more_info if error.respond_to?(:more_info)
|
|
details[:details] = error.details if error.respond_to?(:details)
|
|
end
|
|
end
|