diff --git a/app/controllers/api/v1/accounts/voice/tokens_controller.rb b/app/controllers/api/v1/accounts/voice/tokens_controller.rb index c806b6fd0..2371fd5d4 100644 --- a/app/controllers/api/v1/accounts/voice/tokens_controller.rb +++ b/app/controllers/api/v1/accounts/voice/tokens_controller.rb @@ -1,136 +1,80 @@ -require 'twilio-ruby' - class Api::V1::Accounts::Voice::TokensController < Api::V1::Accounts::BaseController + before_action :set_voice_inbox + def create - # 1. Find inbox - inbox = Current.account.inboxes.find_by(id: params[:inbox_id]) - - unless inbox - render json: { error: 'Inbox not found' }, status: :not_found - return - end - - # 2. Get Twilio credentials from channel - channel = inbox.channel - config = channel.provider_config_hash || {} - - # Get Twilio credentials from channel config - account_sid = config['account_sid'] - auth_token = config['auth_token'] - api_key_sid = config['api_key_sid'] - api_key_secret = config['api_key_secret'] - phone_number = channel.phone_number - - # 3. Create a unique client identifier - client_identity = "agent-#{current_user.id}-#{Current.account.id}" + render json: build_response + rescue StandardError => e + Rails.logger.error("Voice::TokensController#create: #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}") + render json: { error: 'Failed to generate token', details: e.message }, status: :internal_server_error + end - # 4. Generate Twilio Access Token - begin - # Simple log for debugging - Rails.logger.debug "Generating Twilio token for identity: #{client_identity} using API key: #{api_key_sid}" - - # Create Twilio JWT AccessToken with API key credentials - token = Twilio::JWT::AccessToken.new( - account_sid, - api_key_sid, - api_key_secret, - identity: client_identity, - ttl: 3600 - ) - - # Create Voice grant for the token - voice_grant = Twilio::JWT::AccessToken::VoiceGrant.new - - # 1. Always enable incoming calls - voice_grant.incoming_allow = true - - # 2. CRITICAL: For outgoing WebRTC calls, we need an application SID (TwiML App SID) - # This is required to fix error 31002: "Token does not allow outgoing calls" - outgoing_application_sid = config['outgoing_application_sid'] - - # For WebRTC calls with Twilio Voice SDK, the outgoing_application_sid is mandatory - # Without it, browser-based calling won't work properly - - if outgoing_application_sid.present? - # We have a configured TwiML App SID - use it - voice_grant.outgoing_application_sid = outgoing_application_sid - - # Set additional parameters that will be passed to the TwiML app - # CRITICAL: Include is_agent=true to help identify agent connections on the server side - voice_grant.outgoing_application_params = { - 'account_id' => Current.account.id.to_s, - 'agent_id' => current_user.id.to_s, - 'identity' => client_identity, - 'client_name' => client_identity, - 'accountSid' => account_sid, - 'is_agent' => 'true' # CRITICAL: Identify agent connections - } - - Rails.logger.info("Using configured TwiML App SID: #{outgoing_application_sid}") - else - # Log a clear error message - TwiML App SID is essential for browser-based calling - twiml_url = "#{ENV.fetch('FRONTEND_URL', '')}/api/v1/accounts/#{Current.account.id}/voice/twiml_for_client" - - # Return detailed instructions for setting up TwiML App - setup_instructions = <<~INSTRUCTIONS - No TwiML App SID configured! Browser-based calling requires a Twilio TwiML App. - - To create a TwiML App: - 1. Go to Twilio Console > Voice > TwiML Apps - 2. Create a new TwiML app - 3. Set the Voice Request URL to: #{twiml_url} - 4. Save and copy the new TwiML App SID - 5. Update your voice channel configuration with this SID - INSTRUCTIONS - - Rails.logger.error(setup_instructions) - - # Still try to create a token, but with a warning that it will likely fail - # Use a fake App SID to avoid immediate rejection - voice_grant.outgoing_application_sid = "AP00000000000000000000000000000000" - - # Set parameters that would be used if it were a real App SID - voice_grant.outgoing_application_params = { - 'account_id' => Current.account.id.to_s, - 'agent_id' => current_user.id.to_s, - 'missing_twiml_app' => true # Flag to indicate the problem - } - end - - # Log info for detailed debugging - Rails.logger.info("Voice token created for identity: #{client_identity}") - Rails.logger.info("Outgoing params: #{voice_grant.outgoing_application_params.inspect}") - - # Add the grant to the token - token.add_grant(voice_grant) + private - # Add a warning message if no TwiML App SID is configured - twiml_url = "#{ENV.fetch('FRONTEND_URL', '')}/api/v1/accounts/#{Current.account.id}/voice/twiml_for_client" - warning_message = !outgoing_application_sid.present? ? - "Browser calling requires a Twilio TwiML App SID. Configure one in the voice channel settings." : nil - - # Return token response with additional debugging info - render json: { - token: token.to_jwt, - identity: client_identity, - voice_enabled: true, - account_sid: account_sid, - agent_id: current_user.id, - account_id: Current.account.id, - inbox_id: inbox.id, - phone_number: phone_number, - twiml_endpoint: twiml_url, - has_twiml_app: outgoing_application_sid.present?, - warning: warning_message - } - rescue => e - Rails.logger.error("Error generating token: #{e.message}") - Rails.logger.error("Backtrace: #{e.backtrace[0..5].join("\n")}") - - render json: { - error: 'Failed to generate token', - details: e.message - }, status: :internal_server_error + def build_response + { + token: twilio_token.to_jwt, + identity: client_identity, + voice_enabled: true, + account_sid: twilio_config[:account_sid], + agent_id: Current.user.id, + account_id: Current.account.id, + inbox_id: @voice_inbox.id, + phone_number: twilio_config[:phone_number], + twiml_endpoint: twilio_config[:twiml_url], + has_twiml_app: twilio_config[:outgoing_app_sid].present? + } + end + + def twilio_token + Twilio::JWT::AccessToken.new( + *twilio_credentials, + identity: client_identity, + ttl: 1.hour.to_i + ).tap { |t| t.add_grant(voice_grant) } + end + + def twilio_credentials + twilio_config.values_at(:account_sid, :api_key_sid, :api_key_secret) + end + + def voice_grant + Twilio::JWT::AccessToken::VoiceGrant.new.tap do |grant| + grant.incoming_allow = true + grant.outgoing_application_sid = twilio_config[:outgoing_app_sid] + grant.outgoing_application_params = outgoing_params end end + + def outgoing_params + { + account_id: Current.account.id, + agent_id: Current.user.id, + identity: client_identity, + client_name: client_identity, + accountSid: twilio_config[:account_sid], + is_agent: 'true' + } + end + + def twilio_config + @twilio_config ||= begin + cfg = @voice_inbox.channel.provider_config_hash || {} + { + account_sid: cfg['account_sid'], + api_key_sid: cfg['api_key_sid'], + api_key_secret: cfg['api_key_secret'], + outgoing_app_sid: cfg['outgoing_application_sid'], + phone_number: @voice_inbox.channel.phone_number, + twiml_url: "#{ENV.fetch('FRONTEND_URL', '')}/api/v1/accounts/#{Current.account.id}/voice/twiml_for_client" + }.with_indifferent_access.merge(client_identity:) + end + end + + def client_identity + @client_identity ||= "agent-#{Current.user.id}-account-#{Current.account.id}" + end + + def set_voice_inbox + @voice_inbox = Current.account.inboxes.find(params[:inbox_id]) + end end \ No newline at end of file