Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@

- [ ] Report buttons not working
- [ ] Text not getting localized across several pages
- [ ] Automerge renovates
167 changes: 167 additions & 0 deletions src/app/Http/Controllers/Api/V1/WebRtcController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Services\SettingsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Twilio\Jwt\AccessToken;
use Twilio\Jwt\Grants\VoiceGrant;

/**
* @OA\Tag(
* name="WebRTC",
* description="WebRTC calling endpoints for browser-based voice calls"
* )
*/
class WebRtcController extends Controller
{
protected SettingsService $settings;

public function __construct(SettingsService $settings)
{
$this->settings = $settings;
}

/**
* Generate a Twilio Access Token for WebRTC calling
*
* @OA\Get(
* path="/api/v1/webrtc/token",
* tags={"WebRTC"},
* summary="Generate a Twilio access token for WebRTC calling",
* description="Returns a JWT token that can be used to initialize a Twilio Voice SDK client for browser-based calling",
* @OA\Response(
* response=200,
* description="Token generated successfully",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="token", type="string", description="JWT access token for Twilio Voice SDK"),
* @OA\Property(property="identity", type="string", description="Unique identity for this caller"),
* @OA\Property(property="expires_in", type="integer", description="Token validity in seconds", example=3600)
* )
* ),
* @OA\Response(
* response=403,
* description="WebRTC calling is not enabled",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="error", type="string", example="WebRTC calling is not enabled")
* )
* ),
* @OA\Response(
* response=500,
* description="WebRTC is not properly configured or token generation failed",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="error", type="string", example="WebRTC is not properly configured")
* )
* )
* )
*
* @param Request $request
* @return JsonResponse
*/
public function token(Request $request): JsonResponse
{
// Check if WebRTC is enabled
if (!$this->settings->has('webrtc_enabled') || !$this->settings->get('webrtc_enabled')) {
return response()->json([
'error' => 'WebRTC calling is not enabled'
], 403);
}

// Validate required Twilio settings
$accountSid = $this->settings->get('twilio_account_sid');
$apiKey = $this->settings->get('twilio_api_key');
$apiSecret = $this->settings->get('twilio_api_secret');
$twimlAppSid = $this->settings->get('twilio_twiml_app_sid');

if (empty($accountSid) || empty($apiKey) || empty($apiSecret) || empty($twimlAppSid)) {
Log::error('WebRTC token generation failed: Missing Twilio configuration');
return response()->json([
'error' => 'WebRTC is not properly configured'
], 500);
}

try {
// Generate a unique identity for this caller
$identity = 'webrtc_' . uniqid();

// Create access token
$token = new AccessToken(
$accountSid,
$apiKey,
$apiSecret,
3600, // Token valid for 1 hour
$identity
);

// Create Voice grant
$voiceGrant = new VoiceGrant();
$voiceGrant->setOutgoingApplicationSid($twimlAppSid);

// Add grant to token
$token->addGrant($voiceGrant);

return response()->json([
'token' => $token->toJWT(),
'identity' => $identity,
'expires_in' => 3600
]);
} catch (\Exception $e) {
Log::error('WebRTC token generation failed: ' . $e->getMessage());
return response()->json([
'error' => 'Failed to generate access token'
], 500);
}
}

/**
* Get WebRTC widget configuration
*
* @OA\Get(
* path="/api/v1/webrtc/config",
* tags={"WebRTC"},
* summary="Get WebRTC widget configuration",
* description="Returns configuration settings for the WebRTC dial widget",
* @OA\Response(
* response=200,
* description="Configuration retrieved successfully",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="enabled", type="boolean", description="Whether WebRTC is enabled", example=true),
* @OA\Property(property="title", type="string", description="Helpline title", example="Helpline"),
* @OA\Property(property="language", type="string", description="Default language", example="en-US")
* )
* ),
* @OA\Response(
* response=403,
* description="WebRTC calling is not enabled",
* @OA\JsonContent(
* type="object",
* @OA\Property(property="error", type="string", example="WebRTC calling is not enabled")
* )
* )
* )
*
* @param Request $request
* @return JsonResponse
*/
public function config(Request $request): JsonResponse
{
if (!$this->settings->has('webrtc_enabled') || !$this->settings->get('webrtc_enabled')) {
return response()->json([
'error' => 'WebRTC calling is not enabled'
], 403);
}

return response()->json([
'enabled' => true,
'title' => $this->settings->get('title') ?: 'Helpline',
'language' => $this->settings->get('language') ?: 'en-US',
]);
}
}
31 changes: 18 additions & 13 deletions src/app/Http/Controllers/CallFlowController.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,25 @@ public function __construct(

public function index(Request $request)
{
if ($request->has('CallSid')) {
// Check for misconfigured phone numbers, but skip for WebRTC calls
// WebRTC calls have no phoneNumberSid since they originate from a browser client
$isWebRtcCall = str_starts_with($request->get('From', ''), 'client:');
if ($request->has('CallSid') && !$isWebRtcCall) {
$phoneNumberSid = $this->twilio->client()->calls($request->get('CallSid'))->fetch()->phoneNumberSid;
$incomingPhoneNumber = $this->twilio->client()->incomingPhoneNumbers($phoneNumberSid)->fetch();

Log::debug(sprintf(
"Alert debugging:: phoneNumberSid:%s, incomingPhoneNumber:%s, statusCallback:%s",
$phoneNumberSid,
$incomingPhoneNumber,
$incomingPhoneNumber->statusCallback
));

if ($incomingPhoneNumber->statusCallback == null
|| !str_contains($incomingPhoneNumber->statusCallback, "status.php")) {
$this->call->createMisconfiguredPhoneNumberAlert($incomingPhoneNumber->phoneNumber);
if ($phoneNumberSid) {
$incomingPhoneNumber = $this->twilio->client()->incomingPhoneNumbers($phoneNumberSid)->fetch();

Log::debug(sprintf(
"Alert debugging:: phoneNumberSid:%s, incomingPhoneNumber:%s, statusCallback:%s",
$phoneNumberSid,
$incomingPhoneNumber,
$incomingPhoneNumber->statusCallback
));

if ($incomingPhoneNumber->statusCallback == null
|| !str_contains($incomingPhoneNumber->statusCallback, "status.php")) {
$this->call->createMisconfiguredPhoneNumberAlert($incomingPhoneNumber->phoneNumber);
}
}
}

Expand Down
143 changes: 143 additions & 0 deletions src/app/Http/Controllers/WebRtcCallController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace App\Http\Controllers;

use App\Constants\EventId;
use App\Services\CallService;
use App\Services\ConfigService;
use App\Services\SettingsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Twilio\TwiML\VoiceResponse;

class WebRtcCallController extends Controller
{
protected SettingsService $settings;
protected CallService $call;
protected ConfigService $config;

public function __construct(
SettingsService $settings,
CallService $call,
ConfigService $config
) {
$this->settings = $settings;
$this->call = $call;
$this->config = $config;
}

/**
* Handle incoming WebRTC call from browser
* This is the TwiML Application webhook endpoint
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function handleCall(Request $request)
{
Log::info('WebRTC call received', [
'from' => $request->input('From'),
'callSid' => $request->input('CallSid'),
'serviceBodyId' => $request->input('serviceBodyId'),
'searchType' => $request->input('searchType'),
'location' => $request->input('location'),
]);

$twiml = new VoiceResponse();

// Check if WebRTC is enabled
if (!$this->settings->has('webrtc_enabled') || !$this->settings->get('webrtc_enabled')) {
$twiml->say('WebRTC calling is not enabled. Goodbye.')
->setVoice($this->settings->voice())
->setLanguage($this->settings->get('language'));
$twiml->hangup();
return response($twiml)->header('Content-Type', 'text/xml; charset=utf-8');
}

// Get parameters from the browser
$serviceBodyId = $request->input('serviceBodyId');
$searchType = $request->input('searchType', 'helpline');
$location = $request->input('location');

// Build redirect URL based on search type
$baseUrl = url('/');
$params = [];

// Mark this as a WebRTC call
$params['webrtc'] = '1';

// If service body is specified, use it
if ($serviceBodyId) {
$params['override_service_body_id'] = $serviceBodyId;
}

// Handle different search types
switch ($searchType) {
case 'helpline':
// Direct to helpline search with location if provided
if ($location) {
// Store location in session and redirect to address lookup
session()->put('webrtc_location', $location);
$redirectUrl = $baseUrl . '/address-lookup?' . http_build_query($params);
$twiml->redirect($redirectUrl);
} else {
// Go to normal IVR flow
$redirectUrl = $baseUrl . '/?' . http_build_query($params);
$twiml->redirect($redirectUrl);
}
break;

case 'meeting':
// Direct to meeting search
if ($location) {
session()->put('webrtc_location', $location);
$params['SearchType'] = '2'; // Meeting search
$redirectUrl = $baseUrl . '/address-lookup?' . http_build_query($params);
$twiml->redirect($redirectUrl);
} else {
$params['SearchType'] = '2';
$redirectUrl = $baseUrl . '/?' . http_build_query($params);
$twiml->redirect($redirectUrl);
}
break;

case 'jft':
// Direct to Just For Today
$redirectUrl = $baseUrl . '/fetch-jft?' . http_build_query($params);
$twiml->redirect($redirectUrl);
break;

case 'spad':
// Direct to SPAD
$redirectUrl = $baseUrl . '/fetch-spad?' . http_build_query($params);
$twiml->redirect($redirectUrl);
break;

default:
// Default: go to main IVR
$redirectUrl = $baseUrl . '/?' . http_build_query($params);
$twiml->redirect($redirectUrl);
break;
}

return response($twiml)->header('Content-Type', 'text/xml; charset=utf-8');
}

/**
* Handle WebRTC call status callbacks
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function statusCallback(Request $request)
{
Log::info('WebRTC call status callback', [
'callSid' => $request->input('CallSid'),
'callStatus' => $request->input('CallStatus'),
]);

// Return empty TwiML response
$twiml = new VoiceResponse();
return response($twiml)->header('Content-Type', 'text/xml; charset=utf-8');
}
}
2 changes: 2 additions & 0 deletions src/app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Http\Middleware\SmsBlackhole;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateTwilioSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
Expand Down Expand Up @@ -99,5 +100,6 @@ class Kernel extends HttpKernel
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
'twilio.signature' => ValidateTwilioSignature::class,
];
}
5 changes: 4 additions & 1 deletion src/app/Http/Middleware/TrustProxies.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* Set to '*' to trust all proxies (needed for ngrok, load balancers, etc.)
* This is safe because we validate Twilio signatures on webhook endpoints.
*
* @var array|string|null
*/
protected $proxies;
protected $proxies = '*';

/**
* The headers that should be used to detect proxies.
Expand Down
Loading
Loading