From 72a4c91e5f7479963b23cc2893e94a588546f38c Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 4 Mar 2026 14:24:11 +0100 Subject: [PATCH] Refactor Bird API client to extract reusable `executeCallAndHangupWhenAccepted` method, update `callGateAndHangupWhenAccepted` and test outbound call logic to use shared implementation, and enhance timeout handling. --- services/nginx/app/classes/bird.php | 52 +++++++++++++------ .../app/routes/birdVoiceWebhooksRoute.php | 12 +++-- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/services/nginx/app/classes/bird.php b/services/nginx/app/classes/bird.php index b5f19845..b1cfdf21 100644 --- a/services/nginx/app/classes/bird.php +++ b/services/nginx/app/classes/bird.php @@ -382,6 +382,15 @@ class bird } public function createOutboundTestCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options = []): array + { + return $this->executeCallAndHangupWhenAccepted($workspaceId, $channelId, $options, 'BIRD_TEST_OUTBOUND_CALL'); + } + + /** + * Internal method to place a call, wait for it to be accepted/ongoing, and then hang up. + * Used by both test outbound calls and actual gate calls. + */ + private function executeCallAndHangupWhenAccepted(string $workspaceId, string $channelId, array $options, string $logPrefix): array { $pollIntervalSeconds = max(1, (int)($options['pollIntervalSeconds'] ?? 2)); $maxPollSeconds = max(5, (int)($options['maxPollSeconds'] ?? 30)); @@ -396,15 +405,20 @@ class bird } $payload['to'] = $targetNumber; + // Ensure Bird terminates an unanswered call after we've stopped polling for it. + if (!isset($payload['timeout'])) { + $payload['timeout'] = $maxPollSeconds; + } + $this->logBirdAction( - 'BIRD_TEST_OUTBOUND_CALL_START', + $logPrefix . '_START', 'workspace=' . $workspaceId . ' channel=' . $channelId . ' to=' . $targetNumber ); $createResponse = $this->createVoiceCall($workspaceId, $channelId, $payload); $callId = $this->extractId($createResponse); if ($callId === null) { - $this->logBirdAction('BIRD_TEST_OUTBOUND_CALL_NO_ID', 'Call accepted but no call ID returned', 0); + $this->logBirdAction($logPrefix . '_NO_ID', 'Call accepted but no call ID returned', 0); return [ 'to' => $targetNumber, 'to_e164' => $targetNumber, @@ -422,7 +436,7 @@ class bird $status = $this->extractStatus($current); $this->logBirdAction( - 'BIRD_TEST_OUTBOUND_CALL_POLL', + $logPrefix . '_POLL', 'call=' . $callId . ' attempt=' . $attempt . '/' . $maxAttempts . ' status=' . ($status ?? 'unknown') ); @@ -435,7 +449,7 @@ class bird } } $hangupResponse = $this->hangupVoiceCall($workspaceId, $channelId, $callId, $hangupPayload); - $this->logBirdAction('BIRD_TEST_OUTBOUND_CALL_HANGUP_SENT', 'call=' . $callId . ' status=' . $status); + $this->logBirdAction($logPrefix . '_HANGUP_SENT', 'call=' . $callId . ' status=' . $status); return [ 'to' => $targetNumber, 'to_e164' => $targetNumber, @@ -455,7 +469,7 @@ class bird $lastStatus = $this->extractStatus($lastCall); $this->logBirdAction( - 'BIRD_TEST_OUTBOUND_CALL_TIMEOUT', + $logPrefix . '_TIMEOUT', 'call=' . $callId . ' last_status=' . ($lastStatus ?? 'unknown'), 0 ); @@ -598,17 +612,23 @@ class bird public function callGateAndHangupWhenAccepted(int $countryCode, int $phone, int $timeout) { - $this->logBirdAction('callGateAndHangupWhenAccepted', "Calling gate with country code $countryCode and phone $phone"); - $this->logBirdAction('callGateAndHangupWhenAccepted', "Timeout set to $timeout seconds"); - $voiceResponse = $this->createVoiceCall( - $this->config->workplaceId->getVariableValue(), - $this->config->channelId->getVariableValue(), - // Call the gate with the provided phone number, and a short timeout to minimize potential disturbance. - [ - 'to' => '+' . $countryCode . $phone, - 'timeout' => max(5, min(60, (int)$timeout)), - ] - ); + $ws = $this->config->workplaceId->getVariableValue(); + $ch = $this->config->channelId->getVariableValue(); + + $options = [ + 'to' => '+' . $countryCode . $phone, + 'maxPollSeconds' => (int)$timeout, + ]; + + $result = $this->executeCallAndHangupWhenAccepted($ws, $ch, $options, 'BIRD_GATE_CALL'); + + if (!($result['hangup_sent'] ?? false)) { + $msg = $result['message'] ?? 'Failed to call gate and hangup when accepted'; + if ($result['timed_out_waiting_for_accepted'] ?? false) { + $msg = 'Timed out waiting for gate to accept call'; + } + throw new Exception("Failed to call gate (+{$countryCode}{$phone}): " . $msg); + } } } diff --git a/services/nginx/app/routes/birdVoiceWebhooksRoute.php b/services/nginx/app/routes/birdVoiceWebhooksRoute.php index fafb778e..570a22d8 100644 --- a/services/nginx/app/routes/birdVoiceWebhooksRoute.php +++ b/services/nginx/app/routes/birdVoiceWebhooksRoute.php @@ -205,13 +205,17 @@ class birdVoiceWebhooksRoute return false; } [$countryCode, $phone] = $normalized; - // Use bird voice call to call the phone number, and then hang up after 5 seconds to trigger the gate + + // Use the configured threshold or default to 10 seconds. + $timeout = (int)($config['call_duration_threshold'] ?? 10); + + // Use bird voice call to call the phone number, and then hang up after it is accepted to trigger the gate. $client = new bird(); try { $client->callGateAndHangupWhenAccepted( - $countryCode, - $phone, - 5, + (int)$countryCode, + (int)$phone, + $timeout, ); return true; } catch (\Throwable $e) {