diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php index f352667..ea085b1 100644 --- a/includes/class-twp-webhooks.php +++ b/includes/class-twp-webhooks.php @@ -217,6 +217,27 @@ class TWP_Webhooks { 'callback' => array($this, 'handle_agent_action'), 'permission_callback' => '__return_true' )); + + // Conference status webhook + register_rest_route('twilio-webhook/v1', '/conference-status', array( + 'methods' => 'POST', + 'callback' => array($this, 'handle_conference_status'), + 'permission_callback' => '__return_true' + )); + + // Agent conference join webhook + register_rest_route('twilio-webhook/v1', '/agent-conference-join', array( + 'methods' => 'POST', + 'callback' => array($this, 'handle_agent_conference_join'), + 'permission_callback' => '__return_true' + )); + + // Agent call status webhook (for conference calls) + register_rest_route('twilio-webhook/v1', '/agent-call-status-new', array( + 'methods' => 'POST', + 'callback' => array($this, 'handle_agent_call_status_new'), + 'permission_callback' => '__return_true' + )); // Request callback webhook register_rest_route('twilio-webhook/v1', '/request-callback', array( @@ -2842,4 +2863,129 @@ class TWP_Webhooks { return $this->send_twiml_response($response->asXML()); } + + /** + * Handle conference status events + */ + public function handle_conference_status($request) { + $params = $request->get_params(); + + error_log('TWP Conference Status: ' . print_r($params, true)); + + $status_callback_event = isset($params['StatusCallbackEvent']) ? $params['StatusCallbackEvent'] : ''; + $conference_sid = isset($params['ConferenceSid']) ? $params['ConferenceSid'] : ''; + $friendly_name = isset($params['FriendlyName']) ? $params['FriendlyName'] : ''; + + // Log conference events for debugging + switch ($status_callback_event) { + case 'conference-start': + error_log('TWP Conference: Conference started: ' . $friendly_name); + break; + case 'participant-join': + error_log('TWP Conference: Participant joined: ' . $friendly_name); + break; + case 'participant-leave': + error_log('TWP Conference: Participant left: ' . $friendly_name); + break; + case 'conference-end': + error_log('TWP Conference: Conference ended: ' . $friendly_name); + break; + } + + return new WP_REST_Response('OK', 200); + } + + /** + * Handle agent joining conference with features + */ + public function handle_agent_conference_join($request) { + $params = $request->get_params(); + + error_log('TWP Agent Conference Join: ' . print_r($params, true)); + + $conference_name = isset($_GET['conference_name']) ? $_GET['conference_name'] : ''; + $caller_number = isset($_GET['caller_number']) ? $_GET['caller_number'] : ''; + + $response = new \Twilio\TwiML\VoiceResponse(); + + if (empty($conference_name)) { + $response->say('Conference not found', ['voice' => 'alice']); + $response->hangup(); + return $this->send_twiml_response($response->asXML()); + } + + // Announce the incoming call to the agent + if (!empty($caller_number)) { + $response->say('Incoming call from ' . $this->format_phone_number_for_speech($caller_number), ['voice' => 'alice']); + } else { + $response->say('Incoming call', ['voice' => 'alice']); + } + + // Set up agent conference with features + $dial = $response->dial(); + $conference = $dial->conference($conference_name, [ + 'startConferenceOnEnter' => true, // Start when agent joins + 'endConferenceOnExit' => false, // Don't end when agent leaves (let caller stay) + 'muted' => false, + 'beep' => false, + 'waitUrl' => '', + // Enable DTMF detection for agent features + 'eventCallbackUrl' => home_url('/wp-json/twilio-webhook/v1/conference-events'), + 'record' => false // We'll control recording via DTMF + ]); + + error_log('TWP Agent Conference Join: Joining agent to conference: ' . $conference_name); + + return $this->send_twiml_response($response->asXML()); + } + + /** + * Handle agent call status for conference calls + */ + public function handle_agent_call_status_new($request) { + $params = $request->get_params(); + + error_log('TWP Agent Call Status: ' . print_r($params, true)); + + $call_status = isset($params['CallStatus']) ? $params['CallStatus'] : ''; + $call_sid = isset($params['CallSid']) ? $params['CallSid'] : ''; + + switch ($call_status) { + case 'initiated': + error_log('TWP Agent Call: Call initiated to agent: ' . $call_sid); + break; + case 'ringing': + error_log('TWP Agent Call: Agent phone ringing: ' . $call_sid); + break; + case 'answered': + error_log('TWP Agent Call: Agent answered: ' . $call_sid); + break; + case 'completed': + error_log('TWP Agent Call: Agent call completed: ' . $call_sid); + break; + case 'busy': + case 'no-answer': + case 'failed': + error_log('TWP Agent Call: Agent call failed (' . $call_status . '): ' . $call_sid); + // TODO: Could implement fallback logic here (try next agent, voicemail, etc.) + break; + } + + return new WP_REST_Response('OK', 200); + } + + /** + * Format phone number for speech (adds pauses between digits) + */ + private function format_phone_number_for_speech($number) { + // Remove +1 country code and format for speech + $cleaned = preg_replace('/^\+1/', '', $number); + + if (strlen($cleaned) == 10) { + // Format as (xxx) xxx-xxxx with pauses + return substr($cleaned, 0, 3) . ' ' . substr($cleaned, 3, 3) . ' ' . substr($cleaned, 6, 4); + } + + return $number; + } } \ No newline at end of file diff --git a/includes/class-twp-workflow.php b/includes/class-twp-workflow.php index 3b49725..e381a7b 100644 --- a/includes/class-twp-workflow.php +++ b/includes/class-twp-workflow.php @@ -233,6 +233,9 @@ class TWP_Workflow { case 'Queue': $response->queue((string) $element, $attributes); break; + case 'Conference': + $response->dial()->conference((string) $element, $attributes); + break; case 'Redirect': $response->redirect((string) $element, $attributes); break; @@ -564,56 +567,108 @@ class TWP_Workflow { error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers)); - $dial = $twiml->addChild('Dial'); - $dial->addAttribute('answerOnBridge', 'true'); + // Check if we should use conference mode with agent features + $use_conference_mode = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true; - // Set timeout (default to 30 seconds if not specified) - $timeout = isset($step_data['timeout']) ? $step_data['timeout'] : - (isset($step['timeout']) ? $step['timeout'] : '30'); - $dial->addAttribute('timeout', $timeout); - error_log('TWP Workflow Forward: Using timeout: ' . $timeout); + if ($use_conference_mode) { + // Use conference mode for better call control + error_log('TWP Workflow Forward: Using conference mode for forwarding'); - // Set caller ID to the number that was called (To number from call data) - // This makes the outbound call appear to come from the number the caller dialed - $caller_id = null; - if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) { - $caller_id = $GLOBALS['call_data']['To']; - error_log('TWP Workflow Forward: Using incoming number as caller ID: ' . $caller_id); - } elseif (isset($_POST['To']) && !empty($_POST['To'])) { - $caller_id = $_POST['To']; - error_log('TWP Workflow Forward: Using POST To as caller ID: ' . $caller_id); - } elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) { - $caller_id = $_REQUEST['To']; - error_log('TWP Workflow Forward: Using REQUEST To as caller ID: ' . $caller_id); - } + // Generate a unique conference name using call SID + $call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] : + (isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] : + (isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : uniqid('conf_'))); + $conference_name = 'Forward_' . $call_sid; + + // Put the caller into a conference + $dial = $twiml->addChild('Dial'); + $dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result')); + $dial->addAttribute('method', 'POST'); + + $conference = $dial->addChild('Conference', $conference_name); + $conference->addAttribute('startConferenceOnEnter', 'false'); // Wait for agent + $conference->addAttribute('endConferenceOnExit', 'true'); // End when caller leaves + $conference->addAttribute('waitUrl', 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical'); + $conference->addAttribute('beep', 'false'); + + // Add event callbacks to monitor conference + $conference->addAttribute('statusCallback', home_url('/wp-json/twilio-webhook/v1/conference-status')); + $conference->addAttribute('statusCallbackEvent', 'start join leave end'); + $conference->addAttribute('statusCallbackMethod', 'POST'); + + error_log('TWP Workflow Forward: Placing caller in conference: ' . $conference_name); + + // Immediately call the agent to join the conference + // We'll use the Twilio API to make an outbound call + $twilio = new TWP_Twilio_API(); + + // Set caller ID to the business number + $caller_id = isset($_POST['To']) ? $_POST['To'] : null; + if (!$caller_id && isset($GLOBALS['call_data']['To'])) { + $caller_id = $GLOBALS['call_data']['To']; + } + + // Create TwiML for the agent call that joins them to conference with features + $agent_twiml_url = home_url('/wp-json/twilio-webhook/v1/agent-conference-join?' . http_build_query([ + 'conference_name' => $conference_name, + 'caller_number' => isset($_POST['From']) ? $_POST['From'] : '' + ])); + + try { + error_log('TWP Workflow Forward: Calling agent ' . $forward_numbers[0] . ' to join conference'); + + $agent_call = $twilio->get_client()->calls->create( + $forward_numbers[0], // to + $caller_id, // from (business number) + [ + 'url' => $agent_twiml_url, + 'method' => 'POST', + 'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'), + 'statusCallbackEvent' => ['initiated', 'ringing', 'answered', 'completed'], + 'statusCallbackMethod' => 'POST' + ] + ); + + error_log('TWP Workflow Forward: Agent call created with SID: ' . $agent_call->sid); + + } catch (Exception $e) { + error_log('TWP Workflow Forward: Failed to create agent call: ' . $e->getMessage()); + } - if ($caller_id) { - $dial->addAttribute('callerId', $caller_id); - error_log('TWP Workflow Forward: Set callerId attribute to: ' . $caller_id); } else { - error_log('TWP Workflow Forward: No To number found for caller ID, will use account default'); - } + // Use standard dial forwarding (simpler but less features) + error_log('TWP Workflow Forward: Using standard dial forwarding'); - // Check if we should use bridging with agent features - $use_bridge_features = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true; + $dial = $twiml->addChild('Dial'); + $dial->addAttribute('answerOnBridge', 'true'); - // Add action URL to handle dial result (for tracking and potential fallback) - $action_url = home_url('/wp-json/twilio-webhook/v1/forward-result'); - $dial->addAttribute('action', $action_url); - $dial->addAttribute('method', 'POST'); + // Set timeout (default to 30 seconds if not specified) + $timeout = isset($step_data['timeout']) ? $step_data['timeout'] : + (isset($step['timeout']) ? $step['timeout'] : '30'); + $dial->addAttribute('timeout', $timeout); - // Add all forward numbers with agent features if enabled - foreach ($forward_numbers as $number) { - error_log('TWP Workflow Forward: Adding number to Dial: ' . $number); - $number_element = $dial->addChild('Number', $number); + // Set caller ID to the number that was called + $caller_id = null; + if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) { + $caller_id = $GLOBALS['call_data']['To']; + } elseif (isset($_POST['To']) && !empty($_POST['To'])) { + $caller_id = $_POST['To']; + } elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) { + $caller_id = $_REQUEST['To']; + } - if ($use_bridge_features) { - // Add URL to handle agent-side features (DTMF detection) - $agent_url = home_url('/wp-json/twilio-webhook/v1/agent-features'); - $number_element->addAttribute('url', $agent_url); - $number_element->addAttribute('method', 'POST'); + if ($caller_id) { + $dial->addAttribute('callerId', $caller_id); + } - error_log('TWP Workflow Forward: Added agent features URL for DTMF detection'); + // Add action URL to handle dial result + $dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result')); + $dial->addAttribute('method', 'POST'); + + // Add all forward numbers + foreach ($forward_numbers as $number) { + error_log('TWP Workflow Forward: Adding number to Dial: ' . $number); + $dial->addChild('Number', $number); } }