From 842140549df074f9b4adf421ec7b9cfdfdcc9447 Mon Sep 17 00:00:00 2001 From: "V S K Srinivasa Raman Kaluri (Centific Technologies Inc)" Date: Wed, 30 Apr 2025 15:59:25 -0700 Subject: [PATCH 1/8] feat(call): Add region-based PMA endpoints and call transfer functionality - Add PMA endpoints for Arizona and Texas regions in app settings - Implement dynamic endpoint selection based on config boolean in app settings - Create setRegion endpoint to update PMA endpoint and reinitialize call automation client - Add call transfer capability for ACS participants --- .../CallAutomationService.cs | 45 +++++++++- .../CallAutomationEventsController.cs | 68 +++++++++++++++ .../Controllers/CallController.cs | 86 ++++++++++++++++++- .../Call_Automation_GCCH/Program.cs | 8 +- .../Call_Automation_GCCH/appsettings.json | 4 +- 5 files changed, 204 insertions(+), 7 deletions(-) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs b/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs index fccf45c4..14207dcf 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs @@ -6,16 +6,26 @@ namespace Call_Automation_GCCH.Services { public class CallAutomationService { - private readonly CallAutomationClient _client; - private readonly ILogger _logger; + private CallAutomationClient _client; + private ILogger _logger; private static string? _recordingLocation; private static string _recordingFileFormat = "mp4"; + private string _currentPmaEndpoint = string.Empty; public CallAutomationService(string connectionString, string pmaEndpoint, ILogger logger) { - _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); - // _client = new CallAutomationClient(connectionString: connectionString); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _currentPmaEndpoint = pmaEndpoint; + + if (!string.IsNullOrEmpty(pmaEndpoint)) + { + _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); + } + else + { + _logger.LogWarning("PmaEndpoint is empty. Creating CallAutomationClient without PmaEndpoint parameter."); + _client = new CallAutomationClient(connectionString: connectionString); + } } /// @@ -105,6 +115,33 @@ public CallConnectionProperties GetCallConnectionProperties(string callConnectio } } + /// + /// Updates the CallAutomationClient with new connection settings + /// + /// The ACS connection string + /// The PMA endpoint to use + public void UpdateClient(string connectionString, string pmaEndpoint) + { + _logger = _logger ?? throw new InvalidOperationException("Logger is not initialized"); + _currentPmaEndpoint = pmaEndpoint; + + if (!string.IsNullOrEmpty(pmaEndpoint)) + { + _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); + _logger.LogInformation($"CallAutomationClient recreated with PMA endpoint: {pmaEndpoint}"); + } + else + { + _logger.LogWarning("PmaEndpoint is empty. Creating CallAutomationClient without PmaEndpoint parameter."); + _client = new CallAutomationClient(connectionString: connectionString); + } + } + + public string GetCurrentPmaEndpoint() + { + return _currentPmaEndpoint; + } + //Need Azure Cognitive services for this so in phase 2 //public List GetChoices() //{ diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs index b616a636..eea33974 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs @@ -381,6 +381,74 @@ private void ProcessCallEvent(CallAutomationEventBase parsedEvent) _logger.LogInformation($"Recording State: {recordingStateChanged.State}"); } } + + /// + /// Updates the IsArizona configuration and switches PMA endpoint accordingly + /// + /// Boolean flag to determine which PMA endpoint to use + /// Action result indicating success or error + [HttpPost("setRegion")] + public IActionResult SetRegion([FromBody] RegionConfigRequest request) + { + try + { + _logger.LogInformation($"Changing region configuration. IsArizona: {request.IsArizona}"); + + // Get the current configuration section + var configSection = HttpContext.RequestServices.GetRequiredService().GetSection("CommunicationSettings"); + + // Get the current endpoint being used to determine if an update is needed + string currentEndpoint = _service.GetCurrentPmaEndpoint() ?? string.Empty; + string newEndpoint = request.IsArizona + ? configSection["PmaEndpointArizona"] ?? string.Empty + : configSection["PmaEndpointTexas"] ?? string.Empty; + + // Check if new endpoint is empty + if (string.IsNullOrEmpty(newEndpoint)) + { + _logger.LogWarning($"The {(request.IsArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); + } + + // Only update if the endpoint would actually change + if (currentEndpoint == newEndpoint) + { + if(string.IsNullOrEmpty(currentEndpoint)) + { + return Ok($"Configuration unchanged as the endpoints are empty"); + } + else + { + return Ok($"Configuration unchanged. Already using {(request.IsArizona ? "Arizona" : "Texas")} region."); + } + } + + // Update the IsArizona setting in memory + ((IConfigurationSection)configSection.GetSection("IsArizona")).Value = request.IsArizona.ToString(); + + // Update the client with the new endpoint + var connectionString = configSection["AcsConnectionString"] ?? string.Empty; + if (string.IsNullOrEmpty(connectionString)) + { + _logger.LogError("AcsConnectionString is empty"); + return Problem("AcsConnectionString is empty"); + } + + _service.UpdateClient(connectionString, newEndpoint); + + return Ok($"Region updated successfully to {(request.IsArizona ? "Arizona" : "Texas")}."); + } + catch (Exception ex) + { + _logger.LogError($"Error updating region configuration: {ex.Message}"); + return Problem($"Failed to update region configuration: {ex.Message}"); + } + } + + // Request model for setting the region + public class RegionConfigRequest + { + public bool IsArizona { get; set; } + } } } diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs index b2628d03..8dbf6642 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs @@ -131,7 +131,91 @@ public async Task CreateOutboundCallToAcsAsync(string acsTarget) return Problem($"Failed to create outbound ACS call: {ex.Message}"); } } - + /// + /// Transfers a call to an ACS participant asynchronously + /// + /// The call connection ID + /// The ACS user identifier to transfer to + /// The ACS target identifier + /// Call connection status + [HttpPost("/transferCallToAcsParticipantAsync")] + [Tags("Transfer Call APIs")] + public async Task TransferCallToAcsParticipantAsync(string callConnectionId, string acsTransferTarget, string acsTarget) + { + try + { + _logger.LogInformation($"Starting async call transfer to ACS user: {acsTransferTarget} for call {callConnectionId}"); + + CallConnection callConnection = _service.GetCallConnection(callConnectionId); + var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; + + TransferToParticipantOptions transferToParticipantOptions = new TransferToParticipantOptions(new CommunicationUserIdentifier(acsTransferTarget)) + { + OperationContext = "TransferCallContext", + Transferee = new CommunicationUserIdentifier(acsTarget), + }; + + var transferResult = await callConnection.TransferCallToParticipantAsync(transferToParticipantOptions); + var status = transferResult.GetRawResponse().Status.ToString(); + + _logger.LogInformation($"Call transferred successfully. CallConnectionId: {callConnectionId}, correlation id: {correlationId}, status: {status}"); + + return Ok(new CallConnectionResponse + { + CallConnectionId = callConnectionId, + CorrelationId = correlationId, + Status = status + }); + } + catch (Exception ex) + { + _logger.LogError($"Error transferring call: {ex.Message}. CallConnectionId: {callConnectionId}"); + return Problem($"Failed to transfer call: {ex.Message}"); + } + } + + /// + /// Transfers a call to an ACS participant synchronously + /// + /// The call connection ID + /// The ACS user identifier to transfer to + /// The ACS target identifier + /// Call connection status + [HttpPost("/transferCallToAcsParticipant")] + [Tags("Transfer Call APIs")] + public IActionResult TransferCallToAcsParticipant(string callConnectionId, string acsTransferTarget, string acsTarget) + { + try + { + _logger.LogInformation($"Starting call transfer to ACS user: {acsTransferTarget} for call {callConnectionId}"); + + CallConnection callConnection = _service.GetCallConnection(callConnectionId); + var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; + + TransferToParticipantOptions transferToParticipantOptions = new TransferToParticipantOptions(new CommunicationUserIdentifier(acsTransferTarget)) + { + OperationContext = "TransferCallContext", + Transferee = new CommunicationUserIdentifier(acsTarget), + }; + + var transferResult = callConnection.TransferCallToParticipant(transferToParticipantOptions); + var status = transferResult.GetRawResponse().Status.ToString(); + + _logger.LogInformation($"Call transferred successfully. CallConnectionId: {callConnectionId}, correlation id: {correlationId}, status: {status}"); + + return Ok(new CallConnectionResponse + { + CallConnectionId = callConnectionId, + CorrelationId = correlationId, + Status = status + }); + } + catch (Exception ex) + { + _logger.LogError($"Error transferring call: {ex.Message}. CallConnectionId: {callConnectionId}"); + return Problem($"Failed to transfer call: {ex.Message}"); + } + } /// /// Hangs up a call asynchronously /// diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs index 9b9f128f..7cb7db04 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs @@ -22,7 +22,13 @@ // Add CallAutomationService as a singleton builder.Services.AddSingleton(sp => { string connectionString = commSection["AcsConnectionString"]; - string pmaEndpoint = commSection["PmaEndpoint"]; + bool isArizona = bool.Parse(commSection["IsArizona"] ?? "true"); + string pmaEndpoint = isArizona ? commSection["PmaEndpointArizona"] : commSection["PmaEndpointTexas"]; + + if (string.IsNullOrEmpty(pmaEndpoint)) { + sp.GetRequiredService>().LogWarning($"The {(isArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); + } + return new CallAutomationService(connectionString, pmaEndpoint, sp.GetRequiredService>()); }); diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json b/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json index e7b9186e..c9c4981f 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json +++ b/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json @@ -11,6 +11,8 @@ "AcsConnectionString": "", "AcsPhoneNumber": "", "CallbackUriHost": "", - "PmaEndpoint": "" + "PmaEndpointArizona": "", + "PmaEndpointTexas": "", + "IsArizona": true } } From 890d960470c847deb444a439596889898a585ec2 Mon Sep 17 00:00:00 2001 From: v-vkaluri Date: Wed, 30 Apr 2025 18:25:50 -0700 Subject: [PATCH 2/8] Updated input param of SetRegion to take boolean --- .../CallAutomationEventsController.cs | 129 +++++++++--------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs index eea33974..d82cc14c 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs @@ -166,7 +166,68 @@ public IActionResult HandleCallbacks([FromBody] CloudEvent[] cloudEvents) return Problem($"Error processing callbacks: {ex.Message}"); } } + /// + /// Updates the IsArizona configuration and switches PMA endpoint accordingly + /// + /// Boolean flag to determine which PMA endpoint to use + /// Action result indicating success or error + [HttpPost("/setRegion")] + [Tags("Region Configuration")] + public IActionResult SetRegion(bool isArizona) + { + try + { + _logger.LogInformation($"Changing region configuration. IsArizona: {isArizona}"); + + // Get the current configuration section + var configSection = HttpContext.RequestServices.GetRequiredService().GetSection("CommunicationSettings"); + + // Get the current endpoint being used to determine if an update is needed + string currentEndpoint = _service.GetCurrentPmaEndpoint() ?? string.Empty; + string newEndpoint = isArizona + ? configSection["PmaEndpointArizona"] ?? string.Empty + : configSection["PmaEndpointTexas"] ?? string.Empty; + + // Check if new endpoint is empty + if (string.IsNullOrEmpty(newEndpoint)) + { + _logger.LogWarning($"The {(isArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); + } + + // Only update if the endpoint would actually change + if (currentEndpoint == newEndpoint) + { + if (string.IsNullOrEmpty(currentEndpoint)) + { + return Ok($"Configuration unchanged as the endpoints are empty"); + } + else + { + return Ok($"Configuration unchanged. Already using {(isArizona ? "Arizona" : "Texas")} region."); + } + } + // Update the IsArizona setting in memory + ((IConfigurationSection)configSection.GetSection("IsArizona")).Value = isArizona.ToString(); + + // Update the client with the new endpoint + var connectionString = configSection["AcsConnectionString"] ?? string.Empty; + if (string.IsNullOrEmpty(connectionString)) + { + _logger.LogError("AcsConnectionString is empty"); + return Problem("AcsConnectionString is empty"); + } + + _service.UpdateClient(connectionString, newEndpoint); + + return Ok($"Region updated successfully to {(isArizona ? "Arizona" : "Texas")}."); + } + catch (Exception ex) + { + _logger.LogError($"Error updating region configuration: {ex.Message}"); + return Problem($"Failed to update region configuration: {ex.Message}"); + } + } /// /// Processes individual call automation events /// @@ -381,74 +442,6 @@ private void ProcessCallEvent(CallAutomationEventBase parsedEvent) _logger.LogInformation($"Recording State: {recordingStateChanged.State}"); } } - - /// - /// Updates the IsArizona configuration and switches PMA endpoint accordingly - /// - /// Boolean flag to determine which PMA endpoint to use - /// Action result indicating success or error - [HttpPost("setRegion")] - public IActionResult SetRegion([FromBody] RegionConfigRequest request) - { - try - { - _logger.LogInformation($"Changing region configuration. IsArizona: {request.IsArizona}"); - - // Get the current configuration section - var configSection = HttpContext.RequestServices.GetRequiredService().GetSection("CommunicationSettings"); - - // Get the current endpoint being used to determine if an update is needed - string currentEndpoint = _service.GetCurrentPmaEndpoint() ?? string.Empty; - string newEndpoint = request.IsArizona - ? configSection["PmaEndpointArizona"] ?? string.Empty - : configSection["PmaEndpointTexas"] ?? string.Empty; - - // Check if new endpoint is empty - if (string.IsNullOrEmpty(newEndpoint)) - { - _logger.LogWarning($"The {(request.IsArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); - } - - // Only update if the endpoint would actually change - if (currentEndpoint == newEndpoint) - { - if(string.IsNullOrEmpty(currentEndpoint)) - { - return Ok($"Configuration unchanged as the endpoints are empty"); - } - else - { - return Ok($"Configuration unchanged. Already using {(request.IsArizona ? "Arizona" : "Texas")} region."); - } - } - - // Update the IsArizona setting in memory - ((IConfigurationSection)configSection.GetSection("IsArizona")).Value = request.IsArizona.ToString(); - - // Update the client with the new endpoint - var connectionString = configSection["AcsConnectionString"] ?? string.Empty; - if (string.IsNullOrEmpty(connectionString)) - { - _logger.LogError("AcsConnectionString is empty"); - return Problem("AcsConnectionString is empty"); - } - - _service.UpdateClient(connectionString, newEndpoint); - - return Ok($"Region updated successfully to {(request.IsArizona ? "Arizona" : "Texas")}."); - } - catch (Exception ex) - { - _logger.LogError($"Error updating region configuration: {ex.Message}"); - return Problem($"Failed to update region configuration: {ex.Message}"); - } - } - - // Request model for setting the region - public class RegionConfigRequest - { - public bool IsArizona { get; set; } - } } } From e4fd17752c90840da0c05ffee05074ef578812a3 Mon Sep 17 00:00:00 2001 From: v-vkaluri Date: Tue, 6 May 2025 15:05:21 -0700 Subject: [PATCH 3/8] Added Incoming Call with Media Streaming Options --- .../CallAutomationEventsController.cs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs index d82cc14c..4f0f40fe 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs @@ -442,6 +442,172 @@ private void ProcessCallEvent(CallAutomationEventBase parsedEvent) _logger.LogInformation($"Recording State: {recordingStateChanged.State}"); } } + + #region Incoming Call with Media Streaming + + /// + /// Handles incoming calls with media streaming using query parameters to configure options + /// + /// + /// ## URL Template for Azure Communication Services Configuration + /// + /// ``` + /// https://your-domain.com/api/events/incomingcall?audioChannelMixed=true&audioFormat16k=true&mediaStreaming=true&bidirectionalStreaming=true + /// ``` + /// + /// Simply copy this URL and change the true/false values as needed for your specific configuration. + /// + /// The array of EventGrid events + /// If true, use Mixed audio channel; if false, use Unmixed + /// If true, use 16kHz format; if false, use 24kHz + /// If true, enable media streaming; if false, disable + /// If true, enable bidirectional streaming; if false, disable + /// Action result indicating success or error + [HttpPost("events/incomingcall")] + [Tags("Incoming Call with Media Streaming Options")] + public async Task HandleIncomingCallWithOptions( + [FromBody] EventGridEvent[] eventGridEvents, + [FromQuery] bool audioChannelMixed = true, + [FromQuery] bool audioFormat16k = true, + [FromQuery] bool mediaStreaming = true, + [FromQuery] bool bidirectionalStreaming = true) + { + MediaStreamingAudioChannel audioChannel = audioChannelMixed + ? MediaStreamingAudioChannel.Mixed + : MediaStreamingAudioChannel.Unmixed; + + bool isPcm24kHz = !audioFormat16k; + + return await HandleIncomingCallWithMediaStreaming( + eventGridEvents, + audioChannel, + mediaStreaming, + isPcm24kHz, + bidirectionalStreaming); + } + + /// + /// Generic handler for incoming calls with specific media streaming configurations + /// + private async Task HandleIncomingCallWithMediaStreaming( + EventGridEvent[] eventGridEvents, + MediaStreamingAudioChannel audioChannel, + bool enableMediaStreaming, + bool isPcm24kHz, + bool enableBidirectional) + { + try + { + _logger.LogInformation($"Received {eventGridEvents.Length} event(s) for incoming call with media streaming"); + foreach (var eventGridEvent in eventGridEvents) + { + try + { + _logger.LogInformation($"Processing event: {eventGridEvent.EventType}, Id: {eventGridEvent.Id}"); + + if (eventGridEvent.TryGetSystemEventData(out object eventData)) + { + if (eventData is SubscriptionValidationEventData subscriptionValidationEventData) + { + _logger.LogInformation($"Subscription validation event received with code: {subscriptionValidationEventData.ValidationCode}"); + + var responseData = new SubscriptionValidationResponse + { + ValidationResponse = subscriptionValidationEventData.ValidationCode + }; + return Ok(responseData); + } + if (eventData is AcsIncomingCallEventData incomingCallEventData) + { + try + { + var callerId = incomingCallEventData.FromCommunicationIdentifier.RawId; + _logger.LogInformation($"Incoming call from caller ID: {callerId}, CorrelationId: {incomingCallEventData.CorrelationId}"); + + var callbackUri = new Uri(new Uri(_config.CallbackUriHost), $"/api/callbacks"); + var websocketUri = new Uri(_config.CallbackUriHost.Replace("https", "wss") + "/ws"); + + _logger.LogInformation($"Incoming call with media streaming - correlationId: {incomingCallEventData.CorrelationId}, " + + $"AudioChannel: {audioChannel}, EnableMediaStreaming: {enableMediaStreaming}, " + + $"IsPcm24kHz: {isPcm24kHz}, EnableBidirectional: {enableBidirectional}"); + + MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions( + websocketUri, + MediaStreamingContent.Audio, + audioChannel, + MediaStreamingTransport.Websocket, + enableMediaStreaming) + { + EnableBidirectional = enableBidirectional, + AudioFormat = isPcm24kHz ? AudioFormat.Pcm24KMono : AudioFormat.Pcm16KMono + }; + + var options = new AnswerCallOptions(incomingCallEventData.IncomingCallContext, callbackUri) + { + MediaStreamingOptions = mediaStreamingOptions + // ACS GCCH Phase 2 + // CallIntelligenceOptions = new CallIntelligenceOptions() { CognitiveServicesEndpoint = new Uri(cognitiveServicesEndpoint) } + }; + + _logger.LogInformation($"Answering call with correlationId: {incomingCallEventData.CorrelationId} and media streaming options " + + $"(AudioChannel: {(audioChannel == MediaStreamingAudioChannel.Mixed ? "Mixed" : "Unmixed")}, " + + $"Format: {(isPcm24kHz ? "24kHz" : "16kHz")}, " + + $"Streaming: {(enableMediaStreaming ? "Enabled" : "Disabled")}, " + + $"Bidirectional: {(enableBidirectional ? "Enabled" : "Disabled")})"); + + AnswerCallResult answerCallResult = await _service.GetCallAutomationClient().AnswerCallAsync(options); + var callConnectionMedia = answerCallResult.CallConnection.GetCallMedia(); + + _logger.LogInformation($"Call answered successfully with media streaming. CallConnectionId: {answerCallResult.CallConnection.CallConnectionId}"); + + // Start media streaming if enabled + if (enableMediaStreaming) + { + try + { + await callConnectionMedia.StartMediaStreamingAsync(); + _logger.LogInformation($"Media streaming started for CallConnectionId: {answerCallResult.CallConnection.CallConnectionId}"); + } + catch (Exception streamingEx) + { + _logger.LogError($"Error starting media streaming: {streamingEx.Message}"); + } + } + } + catch (Exception callEx) + { + _logger.LogError($"Error handling incoming call with media streaming: {callEx.Message}"); + } + } + if (eventData is AcsRecordingFileStatusUpdatedEventData statusUpdated) + { + try + { + CallAutomationService.SetRecordingLocation(statusUpdated.RecordingStorageInfo.RecordingChunks[0].ContentLocation); + _logger.LogInformation($"The recording location is: {statusUpdated.RecordingStorageInfo.RecordingChunks[0].ContentLocation}"); + } + catch (Exception recordingEx) + { + _logger.LogError($"Error handling recording status: {recordingEx.Message}"); + } + } + } + } + catch (Exception eventEx) + { + _logger.LogError($"Error processing event: {eventEx.Message}"); + } + } + return Ok(); + } + catch (Exception ex) + { + _logger.LogError($"Error in handling incoming call with media streaming: {ex.Message}"); + return Problem($"Error processing events: {ex.Message}"); + } + } + + #endregion } } From f951fd5670b0c4f02b86c93d05e0246e0908dc13 Mon Sep 17 00:00:00 2001 From: v-vkaluri Date: Tue, 6 May 2025 15:35:04 -0700 Subject: [PATCH 4/8] send audio data to WebSocket client when bidirectional streaming is enabled --- .../Call_Automation_GCCH/Middleware/Helper.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs index 11a54306..febcebf9 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs @@ -45,6 +45,15 @@ public static async Task ProcessRequest(WebSocket webSocket) } LogCollector.Log("***************************************************************************************"); + // Send audio bytes back to client if bidirectional streaming is enabled + if (!audioData.IsSilent && audioData.Data is byte[] audioBytes) + { + await webSocket.SendAsync( + new ArraySegment(audioBytes, 0, audioBytes.Length), + WebSocketMessageType.Binary, + endOfMessage: true, + cancellationToken: CancellationToken.None); + } } if (response is TranscriptionMetadata transcriptionMetadata) @@ -75,12 +84,14 @@ public static async Task ProcessRequest(WebSocket webSocket) LogCollector.Log("***************************************************************************************"); } } - - await webSocket.SendAsync( - new ArraySegment(buffer, 0, receiveResult.Count), - receiveResult.MessageType, - receiveResult.EndOfMessage, - CancellationToken.None); + if (response == null || (response != null && response is not AudioData)) + { + await webSocket.SendAsync( + new ArraySegment(buffer, 0, receiveResult.Count), + receiveResult.MessageType, + receiveResult.EndOfMessage, + CancellationToken.None); + } receiveResult = await webSocket.ReceiveAsync( new ArraySegment(buffer), CancellationToken.None); From 74485eabb7a936db4c5adcba70606b878daf299f Mon Sep 17 00:00:00 2001 From: v-vkaluri Date: Tue, 6 May 2025 17:10:47 -0700 Subject: [PATCH 5/8] Added log before sending the bidirectional logs --- Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs index febcebf9..aaae5587 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs @@ -46,8 +46,9 @@ public static async Task ProcessRequest(WebSocket webSocket) LogCollector.Log("***************************************************************************************"); // Send audio bytes back to client if bidirectional streaming is enabled - if (!audioData.IsSilent && audioData.Data is byte[] audioBytes) + if (audioData.Data is byte[] audioBytes) { + LogCollector.Log("Bidirectional Logs are given below:"); await webSocket.SendAsync( new ArraySegment(audioBytes, 0, audioBytes.Length), WebSocketMessageType.Binary, From 12667cba1d7ff677edd44d807c900873fc76ca68 Mon Sep 17 00:00:00 2001 From: v-vkaluri Date: Tue, 20 May 2025 14:53:19 -0700 Subject: [PATCH 6/8] Updated the PSTN scenarios for all api endpoints --- .../Controllers/CallController.cs | 419 ++-- .../Controllers/MediaController.cs | 1963 ++++++----------- .../Controllers/ParticipantsController.cs | 1176 ++-------- 3 files changed, 1034 insertions(+), 2524 deletions(-) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs index 8dbf6642..46bec54f 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Azure; using Azure.Communication; using Azure.Communication.CallAutomation; using Call_Automation_GCCH.Models; using Call_Automation_GCCH.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,265 +20,307 @@ public class CallController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object + private readonly ConfigurationRequest _config; public CallController( - CallAutomationService service, - ILogger logger, IOptions configOptions) + CallAutomationService service, + ILogger logger, + IOptions configOptions) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); } - /// - /// Creates an outbound call to an ACS user - /// - /// The ACS user identifier to call - /// Call connection information - [HttpPost("/outboundCallToAcs")] + // + // CREATE CALL (ACS or PSTN) + // + + [HttpPost("createCall")] [Tags("Outbound Call APIs")] - public IActionResult CreateOutboundCallToAcs(string acsTarget) - { - try - { - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); - } + public IActionResult CreateCall( + string target, + bool isPstn = false) + => HandleCreateCall(target, isPstn, async: false).Result; - _logger.LogInformation($"Starting outbound call to ACS user: {acsTarget}"); + [HttpPost("createCallAsync")] + [Tags("Outbound Call APIs")] + public Task CreateCallAsync( + string target, + bool isPstn = false) + => HandleCreateCall(target, isPstn, async: true); - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - Uri _eventCallbackUri = callbackUri; - - var callInvite = new CallInvite(new CommunicationUserIdentifier(acsTarget)); - var createCallOptions = new CreateCallOptions(callInvite, callbackUri) - { - // ACS GCCH Phase 2 - // CallIntelligenceOptions = new CallIntelligenceOptions() { CognitiveServicesEndpoint = new Uri(cognitiveServicesEndpoint) }, - }; - _logger.LogInformation("Initiating CreateCall operation"); + // + // TRANSFER CALL + // - CreateCallResult createCallResult = _service.GetCallAutomationClient().CreateCall(createCallOptions); + [HttpPost("transferCall")] + [Tags("Transfer Call APIs")] + public IActionResult TransferCall( + string callConnectionId, + string transferTarget, + string transferee, + bool isPstn = false) + => HandleTransferCall(callConnectionId, transferTarget, transferee, isPstn, async: false).Result; + + [HttpPost("transferCallAsync")] + [Tags("Transfer Call APIs")] + public Task TransferCallAsync( + string callConnectionId, + string transferTarget, + string transferee, + bool isPstn = false) + => HandleTransferCall(callConnectionId, transferTarget, transferee, isPstn, async: true); + + // + // HANG UP + // + + [HttpPost("hangup")] + [Tags("Disconnect call APIs")] + public IActionResult Hangup( + string callConnectionId, + bool isForEveryone) + => HandleHangup(callConnectionId, isForEveryone, async: false).Result; - var connectionId = createCallResult.CallConnectionProperties.CallConnectionId; - var correlationId = createCallResult.CallConnectionProperties.CorrelationId; - var callStatus = createCallResult.CallConnectionProperties.CallConnectionState.ToString(); - - _logger.LogInformation($"Created ACS call with connection id: {connectionId}, correlation id: {correlationId}, status: {callStatus}"); + [HttpPost("hangupAsync")] + [Tags("Disconnect call APIs")] + public Task HangupAsync( + string callConnectionId, + bool isForEveryone) + => HandleHangup(callConnectionId, isForEveryone, async: true); + + // + // GROUP CALL (PSTN or ACS if you like—you could extend to both) + // + [HttpPost("createGroupCallAsync")] + [Tags("Group Call APIs")] + public Task CreateGroupCallAsync( + [FromQuery] string targets) + => HandleGroupCall(targets, async: true); + + [HttpPost("createGroupCall")] + [Tags("Group Call APIs")] + public Task CreateGroupCall( + [FromQuery] string targets) + => HandleGroupCall(targets, async: false); + + // You could add a sync version if you really need it... + + // + // ======== HELPERS ======== + // + + private async Task HandleCreateCall( + string target, + bool isPstn, + bool async) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); - return Ok(new CallConnectionResponse - { - CallConnectionId = connectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - _logger.LogError($"Error creating outbound ACS call: {ex.Message}"); - return Problem($"Failed to create outbound ACS call: {ex.Message}"); - } - } + var idType = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting {(async ? "async " : "")}create {idType} call to {target}"); - /// - /// Creates an outbound call to an ACS user asynchronously - /// - /// The ACS user identifier to call - /// Call connection information - [HttpPost("/outboundCallToAcsAsync")] - [Tags("Outbound Call APIs")] - public async Task CreateOutboundCallToAcsAsync(string acsTarget) - { try { - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); - } - - _logger.LogInformation($"Starting async outbound call to ACS user: {acsTarget}"); - + // Build identifier & invite var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - Uri _eventCallbackUri = callbackUri; + CallInvite invite = isPstn + ? new CallInvite(new PhoneNumberIdentifier(target), + new PhoneNumberIdentifier(_config.AcsPhoneNumber)) + : new CallInvite(new CommunicationUserIdentifier(target)); - _logger.LogInformation($"Created async ACS call with Callback Uri: {callbackUri}"); - var callInvite = new CallInvite(new CommunicationUserIdentifier(acsTarget)); - var createCallOptions = new CreateCallOptions(callInvite, callbackUri) - { - // ACS GCCH Phase 2 - // CallIntelligenceOptions = new CallIntelligenceOptions() { CognitiveServicesEndpoint = new Uri(cognitiveServicesEndpoint) }, - }; - _logger.LogInformation("Initiating CreateCallAsync operation"); + var options = new CreateCallOptions(invite, callbackUri); - CreateCallResult createCallResult = await _service.GetCallAutomationClient().CreateCallAsync(createCallOptions); + // Call SDK + CreateCallResult result = async + ? await _service.GetCallAutomationClient().CreateCallAsync(options) + : _service.GetCallAutomationClient().CreateCall(options); - var connectionId = createCallResult.CallConnectionProperties.CallConnectionId; - var correlationId = createCallResult.CallConnectionProperties.CorrelationId; - var callStatus = createCallResult.CallConnectionProperties.CallConnectionState.ToString(); - - _logger.LogInformation($"Created async ACS call with connection id: {connectionId}, correlation id: {correlationId}, status: {callStatus}"); + var props = result.CallConnectionProperties; + _logger.LogInformation( + $"Created {idType} call: ConnId={props.CallConnectionId}, CorrId={props.CorrelationId}, Status={props.CallConnectionState}"); - return Ok(new CallConnectionResponse - { - CallConnectionId = connectionId, - CorrelationId = correlationId, - Status = callStatus + return Ok(new CallConnectionResponse + { + CallConnectionId = props.CallConnectionId, + CorrelationId = props.CorrelationId, + Status = props.CallConnectionState.ToString() }); } catch (Exception ex) { - _logger.LogError($"Error creating outbound ACS call: {ex.Message}"); - return Problem($"Failed to create outbound ACS call: {ex.Message}"); + _logger.LogError(ex, $"Error creating {idType} call"); + return Problem($"Failed to create {idType} call: {ex.Message}"); } } - /// - /// Transfers a call to an ACS participant asynchronously - /// - /// The call connection ID - /// The ACS user identifier to transfer to - /// The ACS target identifier - /// Call connection status - [HttpPost("/transferCallToAcsParticipantAsync")] - [Tags("Transfer Call APIs")] - public async Task TransferCallToAcsParticipantAsync(string callConnectionId, string acsTransferTarget, string acsTarget) + + private async Task HandleTransferCall( + string callConnectionId, + string transferTarget, + string transferee, + bool isPstn, + bool async) { + if (string.IsNullOrEmpty(callConnectionId)) + return BadRequest("callConnectionId is required"); + + var idType = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting {(async ? "async " : "")}transfer {idType} call: {transferTarget} → {transferee}"); + try { - _logger.LogInformation($"Starting async call transfer to ACS user: {acsTransferTarget} for call {callConnectionId}"); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); + var connection = _service.GetCallConnection(callConnectionId); var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - TransferToParticipantOptions transferToParticipantOptions = new TransferToParticipantOptions(new CommunicationUserIdentifier(acsTransferTarget)) + TransferToParticipantOptions options; + if (isPstn) { - OperationContext = "TransferCallContext", - Transferee = new CommunicationUserIdentifier(acsTarget), - }; + // PSTN → PSTN + options = new TransferToParticipantOptions(new PhoneNumberIdentifier(transferTarget)) + { + OperationContext = "TransferCallContext", + Transferee = new PhoneNumberIdentifier(transferee) + }; + } + else + { + // ACS → ACS + options = new TransferToParticipantOptions(new CommunicationUserIdentifier(transferTarget)) + { + OperationContext = "TransferCallContext", + Transferee = new CommunicationUserIdentifier(transferee) + }; + } - var transferResult = await callConnection.TransferCallToParticipantAsync(transferToParticipantOptions); - var status = transferResult.GetRawResponse().Status.ToString(); + // Call SDK + Response resp = async + ? await connection.TransferCallToParticipantAsync(options) + : connection.TransferCallToParticipant(options); - _logger.LogInformation($"Call transferred successfully. CallConnectionId: {callConnectionId}, correlation id: {correlationId}, status: {status}"); + _logger.LogInformation( + $"Transfer complete. CallConnId={callConnectionId}, CorrId={correlationId}, Status={resp.GetRawResponse().Status}"); return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = correlationId, - Status = status + Status = resp.GetRawResponse().Status.ToString() }); } catch (Exception ex) { - _logger.LogError($"Error transferring call: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Problem($"Failed to transfer call: {ex.Message}"); + _logger.LogError(ex, $"Error transferring {idType} call"); + return Problem($"Failed to transfer {idType} call: {ex.Message}"); } } - /// - /// Transfers a call to an ACS participant synchronously - /// - /// The call connection ID - /// The ACS user identifier to transfer to - /// The ACS target identifier - /// Call connection status - [HttpPost("/transferCallToAcsParticipant")] - [Tags("Transfer Call APIs")] - public IActionResult TransferCallToAcsParticipant(string callConnectionId, string acsTransferTarget, string acsTarget) + private async Task HandleHangup( + string callConnectionId, + bool isForEveryone, + bool async) { + if (string.IsNullOrEmpty(callConnectionId)) + return BadRequest("callConnectionId is required"); + + _logger.LogInformation($"Starting {(async ? "async " : "")}hangup for {callConnectionId}"); + try { - _logger.LogInformation($"Starting call transfer to ACS user: {acsTransferTarget} for call {callConnectionId}"); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); + var connection = _service.GetCallConnection(callConnectionId); var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - TransferToParticipantOptions transferToParticipantOptions = new TransferToParticipantOptions(new CommunicationUserIdentifier(acsTransferTarget)) - { - OperationContext = "TransferCallContext", - Transferee = new CommunicationUserIdentifier(acsTarget), - }; - - var transferResult = callConnection.TransferCallToParticipant(transferToParticipantOptions); - var status = transferResult.GetRawResponse().Status.ToString(); + var resp = async + ? await connection.HangUpAsync(isForEveryone) + : connection.HangUp(isForEveryone); - _logger.LogInformation($"Call transferred successfully. CallConnectionId: {callConnectionId}, correlation id: {correlationId}, status: {status}"); + _logger.LogInformation( + $"Hangup complete. ConnId={callConnectionId}, CorrId={correlationId}, Status={resp.Status}"); return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = correlationId, - Status = status - }); - } - catch (Exception ex) - { - _logger.LogError($"Error transferring call: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Problem($"Failed to transfer call: {ex.Message}"); - } - } - /// - /// Hangs up a call asynchronously - /// - /// The call connection ID - /// Whether to hang up for everyone - /// Call connection status - [HttpPost("/hangupAsync")] - [Tags("Disconnect call APIs")] - public async Task HangupAsync(string callConnectionId, bool isForEveryOne) - { - try - { - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - var disconnectStatus = await callConnection.HangUpAsync(isForEveryOne); - string successMessage = $"Call hung up successfully. CallConnectionId: {callConnectionId}, correlation id: {correlationId}, status: {disconnectStatus.Status.ToString()}"; - _logger.LogInformation(successMessage); - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = disconnectStatus.Status.ToString() + Status = resp.Status.ToString() }); } catch (Exception ex) { - string errorMessage = $"Error hanging up call. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogInformation(errorMessage); + _logger.LogError(ex, "Error hanging up call"); return Problem($"Failed to hang up call: {ex.Message}"); } } - /// - /// Hangs up a call synchronously - /// - /// The call connection ID - /// Whether to hang up for everyone - /// Call connection status - [HttpPost("/hangup")] - [Tags("Disconnect call APIs")] - public IActionResult Hangup(string callConnectionId, bool isForEveryOne) + private async Task HandleGroupCall( + string targets, + bool async) { + if (string.IsNullOrEmpty(targets)) + return BadRequest("Targets parameter is required"); + + var targetList = targets.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .ToList(); + + if (targetList.Count == 0) + return BadRequest("At least one target is required"); + + _logger.LogInformation($"Starting {(async ? "async " : "")}group call to {string.Join(", ", targetList)}"); + try { - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - var disconnectStatus = callConnection.HangUp(isForEveryOne); - string successMessage = $"Call hung up successfully. CallConnectionId: {callConnectionId}, correlation id: {correlationId}, status: {disconnectStatus.Status.ToString()}"; - _logger.LogInformation(successMessage); - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = disconnectStatus.Status.ToString() + // Build identifiers based on format + var idList = new List(); + + foreach (var target in targetList) + { + if (target.StartsWith("8:")) + { + // ACS participant + idList.Add(new CommunicationUserIdentifier(target)); + } + else + { + // PSTN participant + if (!target.StartsWith("+")) + return BadRequest($"PSTN number '{target}' must include country code (e.g., +1 for US)"); + + idList.Add(new PhoneNumberIdentifier(target)); + } + } + + var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + var sourceCallerId = new PhoneNumberIdentifier(_config.AcsPhoneNumber); + + var createGroupOpts = new CreateGroupCallOptions(idList, callbackUri) + { + SourceCallerIdNumber = sourceCallerId, + // ... any media/transcription options you need + }; + + CreateCallResult result; + if (async) + result = await _service.GetCallAutomationClient().CreateGroupCallAsync(createGroupOpts); + else + result = _service.GetCallAutomationClient().CreateGroupCall(createGroupOpts); + + var props = result.CallConnectionProperties; + _logger.LogInformation( + $"Group call created. ConnId={props.CallConnectionId}, CorrId={props.CorrelationId}"); + + return Ok(new CallConnectionResponse + { + CallConnectionId = props.CallConnectionId, + CorrelationId = props.CorrelationId, + Status = props.CallConnectionState.ToString() }); } catch (Exception ex) { - string errorMessage = $"Error hanging up call. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogInformation(errorMessage); - return Problem($"Failed to hang up call: {ex.Message}"); + _logger.LogError(ex, "Error creating group call"); + return Problem($"Failed to create group call: {ex.Message}"); } } } @@ -284,6 +328,7 @@ public IActionResult Hangup(string callConnectionId, bool isForEveryOne) + #region Outbound Call to PSTN /********************************************************************************************** app.MapPost("/outboundCallToPstnAsync", async (string targetPhoneNumber, ILogger logger) => @@ -580,4 +625,4 @@ public IActionResult Hangup(string callConnectionId, bool isForEveryOne) } }).WithTags("Transfer Call APIs"); */ -#endregion +#endregion \ No newline at end of file diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs index 106840e0..0720aeea 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Threading.Channels; +using System.Linq; using System.Threading.Tasks; using Azure; using Azure.Communication; @@ -8,1412 +8,704 @@ using Call_Automation_GCCH.Models; using Call_Automation_GCCH.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Call_Automation_GCCH.Controllers { - [ApiController] - [Route("api/media")] - [Produces("application/json")] - public class MediaController : ControllerBase - { - private readonly CallAutomationService _service; - private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object - - public MediaController( - CallAutomationService service, - ILogger logger, IOptions configOptions) - { - _service = service ?? throw new ArgumentNullException(nameof(service)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); - } - - #region Play Media with File Source - - /// - /// Plays a file to a specific ACS target asynchronously. - /// - /// The call connection ID - /// The ACS user identifier to play to - /// Play operation result - [HttpPost("/playFileSourceAcsTargetAsync")] - [Tags("Play FileSource Media APIs")] - public async Task PlayFileSourceAcsTargetAsync(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + [ApiController] + [Route("api/media")] + [Produces("application/json")] + public class MediaController : ControllerBase + { + private readonly CallAutomationService _service; + private readonly ILogger _logger; + private readonly ConfigurationRequest _config; + + public MediaController( + CallAutomationService service, + ILogger logger, + IOptions configOptions) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); } - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + // ───────────── PLAY FILE SOURCE ──────────────────────────────────────────── + [HttpPost("/playFileSourceToTargetAsync")] + [Tags("Play FileSource Media")] + public Task PlayFileSourceToTargetAsync( + string callConnectionId, + string target) + { + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandlePlayFileSource( + callConnectionId, + new List { identifier }, + playToAll: false, + bargeIn: false, + async: true); } - _logger.LogInformation($"Playing file source to ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - var playTo = new List - { - new CommunicationUserIdentifier(acsTarget) - }; - - var playToOptions = new PlayOptions(fileSource, playTo) - { - OperationContext = "playToContext" - }; - - var playResponse = await callMedia.PlayAsync(playToOptions); - var operationStatus = playResponse.GetRawResponse().ToString(); - - string successMessage = $"File source played successfully to ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error playing file source to ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to play file source: {ex.Message}"); - } - } - - /// - /// Plays a file to a specific ACS target synchronously. - /// - /// The call connection ID - /// The ACS user identifier to play to - /// Play operation result - [HttpPost("/playFileSourceToAcsTarget")] - [Tags("Play FileSource Media APIs")] - public IActionResult PlayFileSourceToAcsTarget(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + [HttpPost("/playFileSourceToTarget")] + [Tags("Play FileSource Media")] + public IActionResult PlayFileSourceToTarget( + string callConnectionId, + string target) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandlePlayFileSource( + callConnectionId, + new List { identifier }, + playToAll: false, + bargeIn: false, + async: false).Result; } - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + [HttpPost("/playFileSourceToAllAsync")] + [Tags("Play FileSource Media")] + public Task PlayFileSourceToAllAsync(string callConnectionId) + => HandlePlayFileSource(callConnectionId, targets: null, playToAll: true, bargeIn: false, async: true); + + [HttpPost("/playFileSourceToAll")] + [Tags("Play FileSource Media")] + public IActionResult PlayFileSourceToAll(string callConnectionId) + => HandlePlayFileSource(callConnectionId, targets: null, playToAll: true, bargeIn: false, async: false).Result; + + [HttpPost("/playFileSourceBargeInAsync")] + [Tags("Play FileSource Media")] + public Task PlayFileSourceBargeInAsync(string callConnectionId) + => HandlePlayFileSource(callConnectionId, targets: null, playToAll: true, bargeIn: true, async: true); + + [HttpPost("/interruptHoldWithPlay")] + [Tags("Play FileSource Media")] + public IActionResult InterruptHoldWithPlay( + string callConnectionId, + string target) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandlePlayFileSource( + callConnectionId, + new List { identifier }, + playToAll: false, + bargeIn: true, + async: false).Result; } - _logger.LogInformation($"Playing file source to ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - var playTo = new List - { - new CommunicationUserIdentifier(acsTarget) - }; - - var playToOptions = new PlayOptions(fileSource, playTo) - { - OperationContext = "playToContext" - }; - - var playResponse = callMedia.Play(playToOptions); - var operationStatus = playResponse.GetRawResponse().ToString(); - - string successMessage = $"File source played successfully to ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error playing file source to ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to play file source: {ex.Message}"); - } - } - - /// - /// Plays a file to all participants asynchronously. - /// - /// The call connection ID - /// Play operation result - [HttpPost("/playFileSourceToAllAsync")] - [Tags("Play FileSource Media APIs")] - public async Task PlayFileSourceToAllAsync(string callConnectionId) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + // ──────────── MEDIA STREAMING: CREATE CALL ───────────────────────────────── + [HttpPost("/createCallWithMediaStreamingAsync")] + [Tags("Media Streaming")] + public Task CreateCallWithMediaStreamingAsync( + string target, + bool enableMediaStreaming = false, + bool isEnableBidirectional = false, + bool isPcm24kMono = false) + { + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); + + return HandleCreateCallWithMediaStreaming( + target, + MediaStreamingAudioChannel.Mixed, + enableMediaStreaming, + isEnableBidirectional, + isPcm24kMono, + async: true); } - _logger.LogInformation($"Playing file source to all participants. CallConnectionId: {callConnectionId}"); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - var playToAllOptions = new PlayToAllOptions(fileSource) - { - OperationContext = "playToAllContext" - }; - - var playResponse = await callMedia.PlayToAllAsync(playToAllOptions); - var operationStatus = playResponse.GetRawResponse().ToString(); - - string successMessage = $"File source played successfully to all participants. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error playing file source to all participants. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to play file source: {ex.Message}"); - } - } - - /// - /// Plays a file to all participants synchronously. - /// - /// The call connection ID - /// Play operation result - [HttpPost("/playFileSourceToAll")] - [Tags("Play FileSource Media APIs")] - public IActionResult PlayFileSourceToAll(string callConnectionId) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + [HttpPost("/createCallWithMediaStreaming")] + [Tags("Media Streaming")] + public IActionResult CreateCallWithMediaStreaming( + string target, + MediaStreamingAudioChannel audioChannel, + bool enableMediaStreaming = false, + bool isEnableBidirectional = false, + bool isPcm24kMono = false) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + return HandleCreateCallWithMediaStreaming( + target, + audioChannel, + enableMediaStreaming, + isEnableBidirectional, + isPcm24kMono, + async: false).Result; } - _logger.LogInformation($"Playing file source to all participants. CallConnectionId: {callConnectionId}"); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - var playToAllOptions = new PlayToAllOptions(fileSource) - { - OperationContext = "playToAllContext" - }; - - var playResponse = callMedia.PlayToAll(playToAllOptions); - var operationStatus = playResponse.GetRawResponse().ToString(); - - string successMessage = $"File source played successfully to all participants. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error playing file source to all participants. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to play file source: {ex.Message}"); - } - } - - /// - /// Plays a file source to all participants with barge-in enabled (interrupting current media). - /// - /// The call connection ID - /// Play operation result - [HttpPost("/playFileSourceBargeInAsync")] - [Tags("Play FileSource Media APIs")] - public async Task PlayFileSourceBargeInAsync(string callConnectionId) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + // ──────────── MEDIA STREAMING: START/STOP ───────────────────────────────── + [HttpPost("/startMediaStreamingAsync")] + [Tags("Media Streaming")] + public Task StartMediaStreamingAsync(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: true, withOptions: false, async: true); + + [HttpPost("/startMediaStreaming")] + [Tags("Media Streaming")] + public IActionResult StartMediaStreaming(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: true, withOptions: false, async: false).Result; + + [HttpPost("/stopMediaStreamingAsync")] + [Tags("Media Streaming")] + public Task StopMediaStreamingAsync(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: false, withOptions: false, async: true); + + [HttpPost("/stopMediaStreaming")] + [Tags("Media Streaming")] + public IActionResult StopMediaStreaming(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: false, withOptions: false, async: false).Result; + + [HttpPost("/startMediaStreamingWithOptionsAsync")] + [Tags("Media Streaming")] + public Task StartMediaStreamingWithOptionsAsync(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: true, withOptions: true, async: true); + + [HttpPost("/startMediaStreamingWithOptions")] + [Tags("Media Streaming")] + public IActionResult StartMediaStreamingWithOptions(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: true, withOptions: true, async: false).Result; + + [HttpPost("/stopMediaStreamingWithOptionsAsync")] + [Tags("Media Streaming")] + public Task StopMediaStreamingWithOptionsAsync(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: false, withOptions: true, async: true); + + [HttpPost("/stopMediaStreamingWithOptions")] + [Tags("Media Streaming")] + public IActionResult StopMediaStreamingWithOptions(string callConnectionId) + => HandleMediaStreaming(callConnectionId, start: false, withOptions: true, async: false).Result; + + // ────────────── CANCEL ALL MEDIA ─────────────────────────────────────────── + [HttpPost("/cancelAllMediaOperationAsync")] + [Tags("Media Operations")] + public Task CancelAllMediaOperationAsync(string callConnectionId) + => HandleCancelAllMediaOperations(callConnectionId, async: true); + + [HttpPost("/cancelAllMediaOperation")] + [Tags("Media Operations")] + public IActionResult CancelAllMediaOperation(string callConnectionId) + => HandleCancelAllMediaOperations(callConnectionId, async: false).Result; + + // ──────────────── RECOGNIZE ──────────────────────────────────────────────── + [HttpPost("/recognizeDTMFAsync")] + [Tags("Recognition")] + public Task RecognizeDTMFAsync(string callConnectionId, string target) + { + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleRecognize(callConnectionId, identifier, RecognizeType.Dtmf, async: true); } - _logger.LogInformation($"Playing file source barge-in. CallConnectionId: {callConnectionId}"); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - var playToAllOptions = new PlayToAllOptions(fileSource) - { - OperationContext = "playBargeInContext", - InterruptCallMediaOperation = true - }; - - var playResponse = await callMedia.PlayToAllAsync(playToAllOptions); - var operationStatus = playResponse.GetRawResponse().ToString(); - - string successMessage = $"File source barge-in played successfully. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error playing file source barge-in. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to play file source: {ex.Message}"); - } - } - #endregion - #region Media Streaming - - [HttpPost("/createCallToAcsWithMediaStreamingAsync")] - [Tags("Media streaming APIs")] - public async Task CreateCallToAcsWithMediaStreamingAsync( - string acsTarget, - bool enableMediaStreaming = false, - bool isEnableBidirectional = false, - bool isPcm24kMono = false) - { - try - { - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; - - _logger.LogInformation($"CreateCallToAcsWithMediaStreamingAsync. websocket url: {websocketUri}, callback uri: {callbackUri}"); - MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions( - new Uri(websocketUri), - MediaStreamingContent.Audio, - MediaStreamingAudioChannel.Mixed, - MediaStreamingTransport.Websocket, - enableMediaStreaming) - { - EnableBidirectional = isEnableBidirectional, - AudioFormat = isPcm24kMono ? AudioFormat.Pcm24KMono : AudioFormat.Pcm16KMono - }; - - var callInvite = new CallInvite(new CommunicationUserIdentifier(acsTarget)); - var createCallOptions = new CreateCallOptions(callInvite, callbackUri) - { - MediaStreamingOptions = mediaStreamingOptions - }; - - CreateCallResult createCallResult = await _service.GetCallAutomationClient().CreateCallAsync(createCallOptions); - string callConnectionId = createCallResult.CallConnectionProperties.CallConnectionId; - string correlationId = createCallResult.CallConnectionProperties.CorrelationId; - // Use call state or some other property as "operation status" - string operationStatus = createCallResult.CallConnectionProperties.CallConnectionState.ToString(); - - string successMessage = $"[createCallToAcsWithMediaStreamingAsync] Created ACS call. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[createCallToAcsWithMediaStreamingAsync] Error creating ACS call: {ex.Message}"; - _logger.LogError(errorMessage); - return Problem($"Failed to create ACS call with media streaming: {ex.Message}"); - } - } - - [HttpPost("/createCallToAcsWithMediaStreaming")] - [Tags("Media streaming APIs")] - public IActionResult CreateCallToAcsWithMediaStreaming( - string acsTarget, - MediaStreamingAudioChannel audioChannel, - bool enableMediaStreaming = false, - bool isEnableBidirectional = false, - bool isPcm24kMono = false) - { - try - { - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; - - MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions( - new Uri(websocketUri), - MediaStreamingContent.Audio, - audioChannel, - MediaStreamingTransport.Websocket, - enableMediaStreaming) - { - EnableBidirectional = isEnableBidirectional, - AudioFormat = isPcm24kMono ? AudioFormat.Pcm24KMono : AudioFormat.Pcm16KMono - }; - - var callInvite = new CallInvite(new CommunicationUserIdentifier(acsTarget)); - var createCallOptions = new CreateCallOptions(callInvite, callbackUri) - { - MediaStreamingOptions = mediaStreamingOptions - }; - - CreateCallResult createCallResult = _service.GetCallAutomationClient().CreateCall(createCallOptions); - string callConnectionId = createCallResult.CallConnectionProperties.CallConnectionId; - string correlationId = createCallResult.CallConnectionProperties.CorrelationId; - string operationStatus = createCallResult.CallConnectionProperties.CallConnectionState.ToString(); - - string successMessage = $"[createCallToAcsWithMediaStreaming] Created ACS call. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[createCallToAcsWithMediaStreaming] Error creating ACS call: {ex.Message}"; - _logger.LogError(errorMessage); - return Problem($"Failed to create ACS call with media streaming: {ex.Message}"); - } - } - - [HttpPost("/startMediaStreamingAsync")] - [Tags("Media streaming APIs")] - public async Task StartMediaStreamingAsync(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - Response response = await callMedia.StartMediaStreamingAsync(); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[startMediaStreamingAsync] Media streaming started. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[startMediaStreamingAsync] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to start media streaming: {ex.Message}"); - } - } - - [HttpPost("/startMediaStreaming")] - [Tags("Media streaming APIs")] - public IActionResult StartMediaStreaming(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - Response response = callMedia.StartMediaStreaming(); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[startMediaStreaming] Media streaming started. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[startMediaStreaming] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to start media streaming: {ex.Message}"); - } - } - - [HttpPost("/stopMediaStreamingAsync")] - [Tags("Media streaming APIs")] - public async Task StopMediaStreamingAsync(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - Response response = await callMedia.StopMediaStreamingAsync(); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[stopMediaStreamingAsync] Media streaming stopped. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[stopMediaStreamingAsync] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to stop media streaming: {ex.Message}"); - } - } - - [HttpPost("/stopMediaStreaming")] - [Tags("Media streaming APIs")] - public IActionResult StopMediaStreaming(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - Response response = callMedia.StopMediaStreaming(); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[stopMediaStreaming] Media streaming stopped. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[stopMediaStreaming] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to stop media streaming: {ex.Message}"); - } - } - - [HttpPost("/startMediaStreamingWithOptionsAsync")] - [Tags("Media streaming APIs")] - public async Task StartMediaStreamingWithOptionsAsync(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - StartMediaStreamingOptions options = new() - { - OperationContext = "StartMediaStreamingContext", - OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") - }; - - Response response = await callMedia.StartMediaStreamingAsync(options); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[startMediaStreamingWithOptionsAsync] Media streaming with options started. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[startMediaStreamingWithOptionsAsync] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to start media streaming with options: {ex.Message}"); - } - } - - [HttpPost("/startMediaStreamingWithOptions")] - [Tags("Media streaming APIs")] - public IActionResult StartMediaStreamingWithOptions(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - StartMediaStreamingOptions options = new() - { - OperationContext = "StartMediaStreamingContext", - OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") - }; - - Response response = callMedia.StartMediaStreaming(options); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[startMediaStreamingWithOptions] Media streaming with options started. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[startMediaStreamingWithOptions] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to start media streaming with options: {ex.Message}"); - } - } - - [HttpPost("/stopMediaStreamingWithOptionsAsync")] - [Tags("Media streaming APIs")] - public async Task StopMediaStreamingWithOptionsAsync(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - StopMediaStreamingOptions options = new() - { - OperationContext = "StopMediaStreamingContext", - OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") - }; - - Response response = await callMedia.StopMediaStreamingAsync(options); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[stopMediaStreamingWithOptionsAsync] Media streaming with options stopped. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[stopMediaStreamingWithOptionsAsync] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to stop media streaming with options: {ex.Message}"); - } - } - - [HttpPost("/stopMediaStreamingWithOptions")] - [Tags("Media streaming APIs")] - public IActionResult StopMediaStreamingWithOptions(string callConnectionId) - { - try - { - var callMedia = _service.GetCallMedia(callConnectionId); - var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; - - StopMediaStreamingOptions options = new() - { - OperationContext = "StopMediaStreamingContext", - OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") - }; - - Response response = callMedia.StopMediaStreaming(options); - string operationStatus = response.Status.ToString(); - - string successMessage = $"[stopMediaStreamingWithOptions] Media streaming with options stopped. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[stopMediaStreamingWithOptions] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to stop media streaming with options: {ex.Message}"); - } - } - - #endregion - - #region Cancel All Media Operations - - [HttpPost("/cancelAllMediaOperationAsync")] - [Tags("Cancel All Media Opertation APIs")] - public async Task CancelAllMediaOperationAsync(string callConnectionId) - { - try - { - // Get the correlationId from the call properties - var callProperties = _service.GetCallConnectionProperties(callConnectionId); - var correlationId = callProperties.CorrelationId; - - // Cancel all media operations - // If CancelAllMediaOperationsAsync returns a Response, capture that: - var response = await _service.GetCallMedia(callConnectionId).CancelAllMediaOperationsAsync(); - string operationStatus = response.GetRawResponse().Status.ToString(); - - string successMessage = $"[cancelAllMediaOperationAsync] All media operations cancelled. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[cancelAllMediaOperationAsync] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to cancel all media operations: {ex.Message}. CallConnectionId: {callConnectionId}"); - } - } - - [HttpPost("/cancelAllMediaOperation")] - [Tags("Cancel All Media Opertation APIs")] - public IActionResult CancelAllMediaOperation(string callConnectionId) - { - try - { - // Get the correlationId from the call properties - var callProperties = _service.GetCallConnectionProperties(callConnectionId); - var correlationId = callProperties.CorrelationId; - - // Cancel all media operations - var response = _service.GetCallMedia(callConnectionId).CancelAllMediaOperations(); - string operationStatus = response.GetRawResponse().Status.ToString(); - - string successMessage = $"[cancelAllMediaOperation] All media operations cancelled. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"[cancelAllMediaOperation] Error: {ex.Message}, CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - return Problem($"Failed to cancel all media operations: {ex.Message}. CallConnectionId: {callConnectionId}"); - } - } - - #endregion - #region Recognization - - /// - /// Recognize DTMF asynchronously. - /// - /// The call connection ID - /// The ACS user identifier - /// status result - [HttpPost("/recognizeDTMFAcsTargetAsync")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Start Recognization APIs")] - public async Task RecognizeDTMFAcsTargetAsync(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + [HttpPost("/recognizeDTMF")] + [Tags("Recognition")] + public IActionResult RecognizeDTMF(string callConnectionId, string target) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleRecognize(callConnectionId, identifier, RecognizeType.Dtmf, async: false).Result; } - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + // ──────────── HOLD / UNHOLD ───────────────────────────────────────────────── + [HttpPost("/holdTargetAsync")] + [Tags("Hold Management")] + public Task HoldTargetAsync(string callConnectionId, string target, bool isPlaySource) + { + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleHold(callConnectionId, identifier, isPlaySource, unhold: false, async: true); } - _logger.LogInformation($"Starting DTMF recognition with ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - //TextSource textSource = new TextSource("Hi, this is recognize test. please provide input thanks!.") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - - var recognizeOptions = - new CallMediaRecognizeDtmfOptions( - targetParticipant: target, maxTonesToCollect: 4) - { - InterruptPrompt = false, - InterToneTimeout = TimeSpan.FromSeconds(5), - OperationContext = "DtmfContext", - InitialSilenceTimeout = TimeSpan.FromSeconds(15), - Prompt = fileSource - }; - - await callMedia.StartRecognizingAsync(recognizeOptions); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"DTMF recognition started successfully with ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error starting DTMF recognition with ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to start DTMF recognition: {ex.Message}"); - } - } - - /// - /// Recognize DTMF. - /// - /// The call connection ID - /// The ACS user identifier - /// status result - [HttpPost("/recognizeDTMFAcsTarget")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Start Recognization APIs")] - public IActionResult RecognizeDTMFAcsTarget(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + [HttpPost("/holdTarget")] + [Tags("Hold Management")] + public IActionResult HoldTarget(string callConnectionId, string target, bool isPlaySource) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleHold(callConnectionId, identifier, isPlaySource, unhold: false, async: false).Result; } - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + [HttpPost("/unholdTargetAsync")] + [Tags("Hold Management")] + public Task UnholdTargetAsync(string callConnectionId, string target) + { + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleHold(callConnectionId, identifier, playSource: false, unhold: true, async: true); } - _logger.LogInformation($"Starting DTMF recognition with ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - //TextSource textSource = new TextSource("Hi, this is recognize test. please provide input thanks!.") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - - var recognizeOptions = - new CallMediaRecognizeDtmfOptions( - targetParticipant: target, maxTonesToCollect: 4) - { - InterruptPrompt = false, - InterToneTimeout = TimeSpan.FromSeconds(5), - OperationContext = "DtmfContext", - InitialSilenceTimeout = TimeSpan.FromSeconds(15), - Prompt = fileSource - }; - - callMedia.StartRecognizing(recognizeOptions); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"DTMF recognition started successfully with ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error starting DTMF recognition with ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to start DTMF recognition: {ex.Message}"); - } - } - - #endregion - #region Hold/Unhold - - /// - /// Hold ACS target asynchronously. - /// - /// The call connection ID - /// The ACS user identifier - /// true or false - /// status result - [HttpPost("/holdAcsTargetAsync")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public async Task HoldAcsTargetAsync(string callConnectionId, string acsTarget, bool isPlaySource) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + [HttpPost("/unholdTarget")] + [Tags("Hold Management")] + public IActionResult UnholdTarget(string callConnectionId, string target) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleHold(callConnectionId, identifier, playSource: false, unhold: true, async: false).Result; } - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + // ────────── INTERRUPT AUDIO AND ANNOUNCE ─────────────────────────────────── + [HttpPost("/interruptAudioAndAnnounceAsync")] + [Tags("Audio Announcements")] + public Task InterruptAudioAndAnnounceAsync(string callConnectionId, string target) + { + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleInterruptAudioAndAnnounce(callConnectionId, identifier, async: true); } - _logger.LogInformation($"Hold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - if (isPlaySource) - { - //TextSource textSource = new TextSource("You are on hold please wait..") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - - HoldOptions holdOptions = new HoldOptions(target) - { - PlaySource = fileSource, - OperationContext = "holdUserContext" - }; - await callMedia.HoldAsync(holdOptions); - } - else - { - HoldOptions holdOptions = new HoldOptions(target) - { - OperationContext = "holdUserContext" - }; - await callMedia.HoldAsync(holdOptions); + [HttpPost("/interruptAudioAndAnnounce")] + [Tags("Audio Announcements")] + public IActionResult InterruptAudioAndAnnounce(string callConnectionId, string target) + { + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); + + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); + + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); + + return HandleInterruptAudioAndAnnounce(callConnectionId, identifier, async: false).Result; } - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"Hold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error holding ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to hold: {ex.Message}"); - } - } - - /// - /// Hold ACS target. - /// - /// The call connection ID - /// The ACS user identifier - /// true or false - /// status result - [HttpPost("/holdAcsTarget")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public IActionResult HoldAcsTarget(string callConnectionId, string acsTarget, bool isPlaySource) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); - } + // ───────────── PRIVATE HANDLERS ────────────────────────────────────────── - if (string.IsNullOrEmpty(acsTarget)) + private async Task HandlePlayFileSource( + string callConnectionId, + List targets, + bool playToAll, + bool bargeIn, + bool async) { - return BadRequest("ACS Target ID is required"); - } - - _logger.LogInformation($"Hold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); + _logger.LogInformation($"Playing file source. CallId={callConnectionId}, PlayToAll={playToAll}, Targets={(targets != null ? string.Join(',', targets.Select(t => t.RawId)) : "All")}, BargeIn={bargeIn}, Async={async}"); + try + { + var callMedia = _service.GetCallMedia(callConnectionId); + var props = _service.GetCallConnectionProperties(callConnectionId); + var fileSource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); - if (isPlaySource) - { - //TextSource textSource = new TextSource("You are on hold please wait..") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - HoldOptions holdOptions = new HoldOptions(target) - { - PlaySource = fileSource, - OperationContext = "holdUserContext" - }; - callMedia.Hold(holdOptions); - } - else - { - HoldOptions holdOptions = new HoldOptions(target) - { - OperationContext = "holdUserContext" - }; - callMedia.Hold(holdOptions); + if (playToAll) + { + var options = new PlayToAllOptions(fileSource) + { + OperationContext = "playToAllContext", + InterruptCallMediaOperation = bargeIn + }; + var response = async ? await callMedia.PlayToAllAsync(options) : callMedia.PlayToAll(options); + var status = response.GetRawResponse().ToString(); + _logger.LogInformation($"Played to all. Status={status}"); + return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = status }); + } + else + { + if (targets == null || targets.Count == 0) + return BadRequest("Target(s) required for play operation."); + + var options = new PlayOptions(fileSource, targets) + { + OperationContext = "playToContext", + InterruptHoldAudio = bargeIn + }; + var response = async ? await callMedia.PlayAsync(options) : callMedia.Play(options); + var status = response.GetRawResponse().ToString(); + _logger.LogInformation($"Played to targets. Status={status}"); + return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = status }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error playing file source."); + return Problem($"Failed to play file source: {ex.Message}"); + } } - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + private async Task HandleCreateCallWithMediaStreaming( + string target, + MediaStreamingAudioChannel audioChannel, + bool enableMediaStreaming, + bool enableBidirectional, + bool pcm24kMono, + bool async) + { + bool isPstn = !target.StartsWith("8:"); + string targetType = isPstn ? "PSTN" : "ACS"; + + _logger.LogInformation($"Creating call with media streaming. Target={target}, Type={targetType}, Channel={audioChannel}, EnableMedia={enableMediaStreaming}, Bidirectional={enableBidirectional}, PCM24kMono={pcm24kMono}, Async={async}"); + try + { + var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; - string successMessage = $"Hold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); + MediaStreamingOptions mediaOpts = new MediaStreamingOptions( + new Uri(websocketUri), + MediaStreamingContent.Audio, + audioChannel, + MediaStreamingTransport.Websocket, + enableMediaStreaming) + { + EnableBidirectional = enableBidirectional, + AudioFormat = pcm24kMono ? AudioFormat.Pcm24KMono : AudioFormat.Pcm16KMono + }; - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error holding ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to hold: {ex.Message}"); - } - } + var invite = isPstn + ? new CallInvite(new PhoneNumberIdentifier(target), new PhoneNumberIdentifier(_config.AcsPhoneNumber)) + : new CallInvite(new CommunicationUserIdentifier(target)); + + var createOpts = new CreateCallOptions(invite, callbackUri) + { + MediaStreamingOptions = mediaOpts + }; - /// - /// Unhold ACS target asynchronously. - /// - /// The call connection ID - /// The ACS user identifier - /// true or false - /// status result - [HttpPost("/unholdAcsTargetAsync")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public async Task UnholdAcsTargetAsync(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); - } + CreateCallResult result = async + ? await _service.GetCallAutomationClient().CreateCallAsync(createOpts) + : _service.GetCallAutomationClient().CreateCall(createOpts); - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + var props = result.CallConnectionProperties; + var status = props.CallConnectionState.ToString(); + _logger.LogInformation($"Call created. CallConnectionId={props.CallConnectionId}, Status={status}"); + return Ok(new { CallConnectionId = props.CallConnectionId, CorrelationId = props.CorrelationId, Status = status }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating call with media streaming"); + return Problem($"Failed to create call with media streaming: {ex.Message}"); + } } - _logger.LogInformation($"Unhold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - UnholdOptions unholdOptions = new UnholdOptions(target) + private async Task HandleMediaStreaming( + string callConnectionId, + bool start, + bool withOptions, + bool async) { - OperationContext = "unholdUserContext" - }; - await callMedia.UnholdAsync(unholdOptions); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"Unhold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error unholding ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to unhold: {ex.Message}"); - } - } + _logger.LogInformation($"{(start ? "Starting" : "Stopping")} media streaming. CallId={callConnectionId}, WithOptions={withOptions}, Async={async}"); + try + { + var callMedia = _service.GetCallMedia(callConnectionId); + var props = _service.GetCallConnectionProperties(callConnectionId); + Response response; - /// - /// Unhold ACS target. - /// - /// The call connection ID - /// The ACS user identifier - /// true or false - /// status result - [HttpPost("/unholdAcsTarget")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public IActionResult UnholdAcsTarget(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); + if (start) + { + if (withOptions) + { + var opts = new StartMediaStreamingOptions + { + OperationContext = "StartMediaStreamingContext", + OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") + }; + response = async ? await callMedia.StartMediaStreamingAsync(opts) : callMedia.StartMediaStreaming(opts); + } + else + { + response = async ? await callMedia.StartMediaStreamingAsync() : callMedia.StartMediaStreaming(); + } + } + else // stop + { + if (withOptions) + { + var opts = new StopMediaStreamingOptions + { + OperationContext = "StopMediaStreamingContext", + OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") + }; + response = async ? await callMedia.StopMediaStreamingAsync(opts) : callMedia.StopMediaStreaming(opts); + } + else + { + response = async ? await callMedia.StopMediaStreamingAsync() : callMedia.StopMediaStreaming(); + } + } + + var status = response.Status.ToString(); + _logger.LogInformation($"Media streaming {(start ? "started" : "stopped")}. Status={status}"); + return Ok(new { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = status }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during media streaming operation"); + return Problem($"Failed media streaming operation: {ex.Message}"); + } } - if (string.IsNullOrEmpty(acsTarget)) + private async Task HandleCancelAllMediaOperations(string callConnectionId, bool async) { - return BadRequest("ACS Target ID is required"); + _logger.LogInformation($"Cancelling all media operations. CallId={callConnectionId}, Async={async}"); + try + { + var props = _service.GetCallConnectionProperties(callConnectionId); + var callMedia = _service.GetCallMedia(callConnectionId); + Response result = async + ? await callMedia.CancelAllMediaOperationsAsync() + : callMedia.CancelAllMediaOperations(); + + // ← Pull status from the raw response + var status = result.GetRawResponse().Status.ToString(); + + _logger.LogInformation($"Cancelled all media operations. Status={status}"); + return Ok(new { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = status }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error cancelling all media operations"); + return Problem($"Failed to cancel all media operations: {ex.Message}"); + } } - _logger.LogInformation($"Unhold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - UnholdOptions unholdOptions = new UnholdOptions(target) - { - OperationContext = "unholdUserContext" - }; - callMedia.Unhold(unholdOptions); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"Unhold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); + private enum RecognizeType { Dtmf, Choice, Speech, SpeechOrDtmf } - return Ok(new CallConnectionResponse + private async Task HandleRecognize( + string callConnectionId, + CommunicationIdentifier target, + RecognizeType type, + bool async) { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error unholding ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to unhold: {ex.Message}"); - } - } - - /// - /// Interrupt audio and announce asynchronously. - /// - /// The call connection ID - /// The ACS user identifier - /// status result - [HttpPost("/interruptAudioAndAnnounceAsync")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public async Task InterruptAudioAndAnnounceAsync(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); - } + _logger.LogInformation($"Starting recognition. CallId={callConnectionId}, Type={type}, Async={async}"); + try + { + var callMedia = _service.GetCallMedia(callConnectionId); + var props = _service.GetCallConnectionProperties(callConnectionId); + var textSource = new TextSource("Please respond.") { VoiceName = "en-US-NancyNeural" }; + var fileSource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + switch (type) + { + case RecognizeType.Dtmf: + var dtmfOpts = new CallMediaRecognizeDtmfOptions(target, maxTonesToCollect: 4) + { + Prompt = fileSource, + InterruptPrompt = false, + InitialSilenceTimeout = TimeSpan.FromSeconds(15), + InterToneTimeout = TimeSpan.FromSeconds(5), + OperationContext = "DtmfContext" + }; + if (async) await callMedia.StartRecognizingAsync(dtmfOpts); else callMedia.StartRecognizing(dtmfOpts); + break; + case RecognizeType.Choice: + var choiceOpts = new CallMediaRecognizeChoiceOptions(target, GetChoices()) + { + Prompt = textSource, + InterruptPrompt = false, + InitialSilenceTimeout = TimeSpan.FromSeconds(10), + OperationContext = "ChoiceContext" + }; + if (async) await callMedia.StartRecognizingAsync(choiceOpts); else callMedia.StartRecognizing(choiceOpts); + break; + case RecognizeType.Speech: + var speechOpts = new CallMediaRecognizeSpeechOptions(target) + { + Prompt = textSource, + InterruptPrompt = false, + InitialSilenceTimeout = TimeSpan.FromSeconds(15), + EndSilenceTimeout = TimeSpan.FromSeconds(15), + OperationContext = "SpeechContext" + }; + if (async) await callMedia.StartRecognizingAsync(speechOpts); else callMedia.StartRecognizing(speechOpts); + break; + case RecognizeType.SpeechOrDtmf: + var bothOpts = new CallMediaRecognizeSpeechOrDtmfOptions(target, 4) + { + Prompt = textSource, + InterruptPrompt = false, + InitialSilenceTimeout = TimeSpan.FromSeconds(15), + EndSilenceTimeout = TimeSpan.FromSeconds(5), + OperationContext = "SpeechOrDTMFContext" + }; + if (async) await callMedia.StartRecognizingAsync(bothOpts); else callMedia.StartRecognizing(bothOpts); + break; + } + + _logger.LogInformation("Recognition started successfully"); + return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = props.CallConnectionState.ToString() }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during recognition"); + return Problem($"Failed to start recognition: {ex.Message}"); + } } - _logger.LogInformation($"Interrupt audio and announce to ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - //TextSource textSource = new TextSource("Hi, This is interrup audio and announcement test") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - - InterruptAudioAndAnnounceOptions interruptAudio = new InterruptAudioAndAnnounceOptions(fileSource, target) - { - OperationContext = "innterruptContext" - }; - - await callMedia.InterruptAudioAndAnnounceAsync(interruptAudio); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"Interrupt audio and announce successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error interrupting audio and announce on ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to interrupt audio and announce: {ex.Message}"); - } - } + private IEnumerable GetChoices() => + new List + { + new RecognitionChoice("yes", new[] { "yes", "yeah" }), + new RecognitionChoice("no", new[] { "no", "nope" }) + }; - /// - /// Interrupt audio and announce. - /// - /// The call connection ID - /// The ACS user identifier - /// status result - [HttpPost("/interruptAudioAndAnnounce")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public IActionResult InterruptAudioAndAnnounce(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) + private async Task HandleHold( + string callConnectionId, + CommunicationIdentifier target, + bool playSource, + bool unhold, + bool async) { - return BadRequest("Call Connection ID is required"); - } + _logger.LogInformation($"{(unhold ? "Unhold" : "Hold")} participant. CallId={callConnectionId}, Target={target.RawId}, PlaySource={playSource}, Async={async}"); + try + { + var callMedia = _service.GetCallMedia(callConnectionId); + var props = _service.GetCallConnectionProperties(callConnectionId); - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + if (unhold) + { + var opts = new UnholdOptions(target) + { + OperationContext = "unholdUserContext" + }; + if (async) await callMedia.UnholdAsync(opts); else callMedia.Unhold(opts); + } + else + { + var opts = new HoldOptions(target) + { + OperationContext = "holdUserContext" + }; + if (playSource) + opts.PlaySource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); + if (async) await callMedia.HoldAsync(opts); else callMedia.Hold(opts); + } + + _logger.LogInformation("Hold/Unhold operation completed"); + return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = props.CallConnectionState.ToString() }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during hold/unhold"); + return Problem($"Failed to {(unhold ? "unhold" : "hold")}: {ex.Message}"); + } } - _logger.LogInformation($"Interrupt audio and announce to ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - //TextSource textSource = new TextSource("Hi, This is interrup audio and announcement test") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - - InterruptAudioAndAnnounceOptions interruptAudio = new InterruptAudioAndAnnounceOptions(fileSource, target) + private async Task HandleInterruptAudioAndAnnounce( + string callConnectionId, + CommunicationIdentifier target, + bool async) { - OperationContext = "innterruptContext" - }; - - callMedia.InterruptAudioAndAnnounce(interruptAudio); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"Interrupt audio and announce successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error interrupting audio and announce on ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to interrupt audio and announce: {ex.Message}"); - } - } + _logger.LogInformation($"Interrupt audio and announce. CallId={callConnectionId}, Target={target.RawId}, Async={async}"); + try + { + var callMedia = _service.GetCallMedia(callConnectionId); + var props = _service.GetCallConnectionProperties(callConnectionId); + var fileSource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); + var opts = new InterruptAudioAndAnnounceOptions(fileSource, target) + { + OperationContext = "interruptContext" + }; - /// - /// Interrupt hold with play. - /// - /// The call connection ID - /// The ACS user identifier - /// status result - [HttpPost("/interruptHoldWithPlay")] - [ProducesResponseType(typeof(CallConnectionResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 400)] - [ProducesResponseType(typeof(ProblemDetails), 500)] - [Tags("Hold Participant APIs")] - public IActionResult InterruptHoldWithPlay(string callConnectionId, string acsTarget) - { - try - { - if (string.IsNullOrEmpty(callConnectionId)) - { - return BadRequest("Call Connection ID is required"); - } + if (async) await callMedia.InterruptAudioAndAnnounceAsync(opts); else callMedia.InterruptAudioAndAnnounce(opts); - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); + _logger.LogInformation("Interrupt audio and announce completed"); + return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, CorrelationId = props.CorrelationId, Status = props.CallConnectionState.ToString() }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during interrupt audio and announce"); + return Problem($"Failed to interrupt audio and announce: {ex.Message}"); + } } - - _logger.LogInformation($"Interrupt hold with play to ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - //TextSource textSource = new TextSource("Hi, This is interrup audio and announcement test") - //{ - // VoiceName = "en-US-NancyNeural" - //}; - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); - List playTo = new List { new CommunicationUserIdentifier(acsTarget) }; - PlayOptions playToOptions = new PlayOptions(fileSource, playTo) - { - OperationContext = "playToContext", - InterruptHoldAudio = true - }; - - callMedia.Play(playToOptions); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - string successMessage = $"Interrupt hold with play successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = callStatus - }); - } - catch (Exception ex) - { - string errorMessage = $"Error interrupting hold and play on ACS target. CallConnectionId: {callConnectionId}. Error: {ex.Message}"; - _logger.LogError(errorMessage); - - return Problem($"Failed to interrupt hold and play: {ex.Message}"); - } } - - #endregion - } } #region Play Media with File Source + + //app.MapPost("/playFileSourceToPstnTargetAsync", async (string callConnectionId, string pstnTarget, ILogger logger) => //{ // try // { // CallMedia callMedia = GetCallMedia(callConnectionId); -// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); +// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); // List playTo = new List { new PhoneNumberIdentifier(pstnTarget) }; // PlayOptions playToOptions = new PlayOptions(fileSource, playTo) // { @@ -1444,7 +736,7 @@ public IActionResult InterruptHoldWithPlay(string callConnectionId, string acsTa // try // { // CallMedia callMedia = GetCallMedia(callConnectionId); -// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); +// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); // List playTo = new List { new PhoneNumberIdentifier(pstnTarget) }; // PlayOptions playToOptions = new PlayOptions(fileSource, playTo) // { @@ -1476,7 +768,7 @@ public IActionResult InterruptHoldWithPlay(string callConnectionId, string acsTa // try // { // CallMedia callMedia = GetCallMedia(callConnectionId); -// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); +// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); // List playTo = new List { new MicrosoftTeamsUserIdentifier(teamsObjectId) }; // PlayOptions playToOptions = new PlayOptions(fileSource, playTo) // { @@ -1507,7 +799,7 @@ public IActionResult InterruptHoldWithPlay(string callConnectionId, string acsTa // try // { // CallMedia callMedia = GetCallMedia(callConnectionId); -// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); +// // FileSource fileSource = new FileSource(new Uri(fileSourceUri)); // List playTo = new List { new MicrosoftTeamsUserIdentifier(teamsObjectId) }; // PlayOptions playToOptions = new PlayOptions(fileSource, playTo) // { @@ -2999,3 +2291,4 @@ public IActionResult InterruptHoldWithPlay(string callConnectionId, string acsTa }).WithTags("Media streaming APIs"); */ #endregion + diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs index 04b988e1..5de97627 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs @@ -1,11 +1,11 @@ using System; using System.Threading.Tasks; +using Azure; using Azure.Communication; using Azure.Communication.CallAutomation; using Call_Automation_GCCH.Models; using Call_Automation_GCCH.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,7 +20,6 @@ public class ParticipantsController : ControllerBase private readonly ILogger _logger; private readonly ConfigurationRequest _config; // final, bound object - public ParticipantsController( CallAutomationService service, ILogger logger, IOptions configOptions) @@ -30,1089 +29,262 @@ public ParticipantsController( _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); } - /// - /// Adds an ACS participant to a call asynchronously - /// - /// Call connection ID - /// ACS participant ID - /// Operation result - [HttpPost("addAcsParticipantAsync")] - [Tags("Add/Remove Participant APIs")] - public async Task AddAcsParticipantAsync(string callConnectionId, string acsParticipant) - { - try - { - _logger.LogInformation($"Starting to add ACS participant async: {acsParticipant} for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - CallInvite callInvite = new CallInvite(new CommunicationUserIdentifier(acsParticipant)); - var addParticipantOptions = new AddParticipantOptions(callInvite) - { - OperationContext = "addAcsUserContext", - InvitationTimeoutInSeconds = 30, - }; - - _logger.LogInformation($"Executing AddParticipantAsync for ACS participant: {acsParticipant} on call {callConnectionId}"); + // ─ Add ─────────────────────────────────────────────────────────────────────── - var result = await callConnection.AddParticipantAsync(addParticipantOptions); - var operationStatus = result.GetRawResponse().ToString(); - var invitationId = result.Value.InvitationId; - - string successMessage = $"Added ACS participant async. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}, InvitationId: {invitationId}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = $"{operationStatus}, InvitationId: {invitationId}" - }); - } - catch (ArgumentNullException ex) - { - _logger.LogInformation($"ACS participant validation error for call {callConnectionId}: {ex.Message}"); - return BadRequest($"Invalid ACS participant: {ex.Message}"); - } - catch (Exception ex) - { - _logger.LogInformation($"Error adding ACS participant for call {callConnectionId}: {ex.Message}"); - return Problem($"Failed to add ACS participant: {ex.Message}"); - } - } - - /// - /// Adds an ACS participant to a call - /// - /// Call connection ID - /// ACS participant ID - /// Operation result - [HttpPost("addAcsParticipant")] + [HttpPost("addParticipant")] [Tags("Add/Remove Participant APIs")] - public IActionResult AddAcsParticipant(string callConnectionId, string acsParticipant) - { - try - { - _logger.LogInformation($"Starting to add ACS participant: {acsParticipant} for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + public IActionResult AddParticipant( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleAddParticipant(callConnectionId, participantId, isPstn, async: false).Result; - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - CallInvite callInvite = new CallInvite(new CommunicationUserIdentifier(acsParticipant)); - var addParticipantOptions = new AddParticipantOptions(callInvite) - { - OperationContext = "addPstnUserContext", - InvitationTimeoutInSeconds = 30, - }; - - _logger.LogInformation($"Executing AddParticipant for ACS participant: {acsParticipant} on call {callConnectionId}"); - - var result = callConnection.AddParticipant(addParticipantOptions); - var operationStatus = result.GetRawResponse().ToString(); - var invitationId = result.Value.InvitationId; - - string successMessage = $"Added ACS participant. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}, InvitationId: {invitationId}"; - _logger.LogInformation(successMessage); + [HttpPost("addParticipantAsync")] + [Tags("Add/Remove Participant APIs")] + public Task AddParticipantAsync( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleAddParticipant(callConnectionId, participantId, isPstn, async: true); - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = $"{operationStatus}, InvitationId: {invitationId}" - }); - } - catch (ArgumentNullException ex) - { - _logger.LogInformation($"ACS participant validation error for call {callConnectionId}: {ex.Message}"); - return BadRequest($"Invalid ACS participant: {ex.Message}"); - } - catch (Exception ex) - { - _logger.LogInformation($"Error adding ACS participant for call {callConnectionId}: {ex.Message}"); - return Problem($"Failed to add ACS participant: {ex.Message}"); - } - } + // ─ Remove ──────────────────────────────────────────────────────────────────── - /// - /// Removes an ACS participant from a call asynchronously - /// - /// Call connection ID - /// ACS target ID to remove - /// Operation status - [HttpPost("removeAcsParticipantAsync")] + [HttpPost("removeParticipant")] [Tags("Add/Remove Participant APIs")] - public async Task RemoveAcsParticipantAsync(string callConnectionId, string acsTarget) - { - try - { - _logger.LogInformation($"Starting to remove ACS participant async: {acsTarget} from call {callConnectionId}"); + public IActionResult RemoveParticipant( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleRemoveParticipant(callConnectionId, participantId, isPstn, async: false).Result; - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + [HttpPost("removeParticipantAsync")] + [Tags("Add/Remove Participant APIs")] + public Task RemoveParticipantAsync( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleRemoveParticipant(callConnectionId, participantId, isPstn, async: true); - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - RemoveParticipantOptions removeParticipantOptions = new RemoveParticipantOptions(new CommunicationUserIdentifier(acsTarget)) - { - OperationContext = "removeAcsParticipantContext" - }; + // ─ Get ─────────────────────────────────────────────────────────────────────── - _logger.LogInformation($"Executing RemoveParticipantAsync for ACS participant: {acsTarget} on call {callConnectionId}"); + [HttpGet("getParticipant")] + [Tags("Get Participant APIs")] + public IActionResult GetParticipant( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleGetParticipant(callConnectionId, participantId, isPstn, async: false).Result; - var response = await callConnection.RemoveParticipantAsync(removeParticipantOptions); - var operationStatus = response.GetRawResponse().ToString(); + [HttpGet("getParticipantAsync")] + [Tags("Get Participant APIs")] + public Task GetParticipantAsync( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleGetParticipant(callConnectionId, participantId, isPstn, async: true); - string successMessage = $"Successfully removed ACS participant async. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); + // ─ Mute ────────────────────────────────────────────────────────────────────── - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (ArgumentNullException ex) - { - _logger.LogInformation($"ACS target validation error for call {callConnectionId}: {ex.Message}"); - return BadRequest($"Invalid ACS target: {ex.Message}"); - } - catch (Exception ex) - { - _logger.LogInformation($"Error removing ACS participant for call {callConnectionId}: {ex.Message}"); - return Problem($"Failed to remove ACS participant: {ex.Message}"); - } - } + [HttpPost("muteParticipant")] + [Tags("Mute Participant APIs")] + public IActionResult MuteParticipant( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleMuteParticipant(callConnectionId, participantId, isPstn, async: false).Result; - /// - /// Removes an ACS participant from a call - /// - /// Call connection ID - /// ACS target ID to remove - /// Operation status - [HttpPost("removeAcsParticipant")] - [Tags("Add/Remove Participant APIs")] - public IActionResult RemoveAcsParticipant(string callConnectionId, string acsTarget) + [HttpPost("muteParticipantAsync")] + [Tags("Mute Participant APIs")] + public Task MuteParticipantAsync( + string callConnectionId, + string participantId, + bool isPstn = false) + => HandleMuteParticipant(callConnectionId, participantId, isPstn, async: true); + + // ─────────────── Shared Handlers ──────────────────────────────────────────── + + private async Task HandleAddParticipant( + string callConnectionId, + string participantId, + bool isPstn, + bool async) { + var opName = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting to add {opName} participant: {participantId} for call {callConnectionId}"); + try { - _logger.LogInformation($"Starting to remove ACS participant: {acsTarget} from call {callConnectionId}"); + var props = _service.GetCallConnectionProperties(callConnectionId); + var connection = _service.GetCallConnection(callConnectionId); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + // build invite + CallInvite invite = isPstn + ? new CallInvite( + new PhoneNumberIdentifier(participantId), + new PhoneNumberIdentifier(_config.AcsPhoneNumber)) + : new CallInvite(new CommunicationUserIdentifier(participantId)); - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - RemoveParticipantOptions removeParticipantOptions = new RemoveParticipantOptions(new CommunicationUserIdentifier(acsTarget)) + var options = new AddParticipantOptions(invite) { - OperationContext = "removeAcsParticipantContext" + OperationContext = isPstn + ? "addPstnUserContext" + : "addAcsUserContext", + InvitationTimeoutInSeconds = 30 }; - _logger.LogInformation($"Executing RemoveParticipant for ACS participant: {acsTarget} on call {callConnectionId}"); - - var response = callConnection.RemoveParticipant(removeParticipantOptions); - var operationStatus = response.GetRawResponse().ToString(); + Response result = async + ? await connection.AddParticipantAsync(options) + : connection.AddParticipant(options); - string successMessage = $"Successfully removed ACS participant. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); + _logger.LogInformation( + $"{opName} participant added: Call={callConnectionId}, CorrId={props.CorrelationId}, " + + $"Status={result.GetRawResponse().Status}, InviteId={result.Value.InvitationId}"); return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus + CorrelationId = props.CorrelationId, + Status = $"{result.GetRawResponse().Status}; InviteId={result.Value.InvitationId}" }); } - catch (ArgumentNullException ex) - { - _logger.LogInformation($"ACS target validation error for call {callConnectionId}: {ex.Message}"); - return BadRequest($"Invalid ACS target: {ex.Message}"); - } catch (Exception ex) { - _logger.LogInformation($"Error removing ACS participant for call {callConnectionId}: {ex.Message}"); - return Problem($"Failed to remove ACS participant: {ex.Message}"); + _logger.LogError(ex, $"Error adding {opName} participant"); + return Problem($"Failed to add participant: {ex.Message}"); } } - /// - /// Cancels adding a participant to a call asynchronously - /// - /// Call connection ID - /// Invitation ID to cancel - /// Operation result - [HttpPost("cancelAddParticipantAsync")] - [Tags("Add/Remove Participant APIs")] - public async Task CancelAddParticipantAsync(string callConnectionId, string invitationId) + private async Task HandleRemoveParticipant( + string callConnectionId, + string participantId, + bool isPstn, + bool async) { + var opName = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting to remove {opName} participant: {participantId} from call {callConnectionId}"); + try { - _logger.LogInformation($"Starting to cancel add participant async with invitation ID: {invitationId} for call {callConnectionId}"); + var props = _service.GetCallConnectionProperties(callConnectionId); + var connection = _service.GetCallConnection(callConnectionId); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + var target = isPstn + ? (CommunicationIdentifier)new PhoneNumberIdentifier(participantId) + : new CommunicationUserIdentifier(participantId); - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - - CancelAddParticipantOperationOptions cancelAddParticipantOperationOptions = new CancelAddParticipantOperationOptions(invitationId) + var options = new RemoveParticipantOptions(target) { - OperationContext = "CancelAddingParticipantContext" + OperationContext = isPstn + ? "removePstnParticipantContext" + : "removeAcsParticipantContext" }; - _logger.LogInformation($"Executing CancelAddParticipantOperationAsync for invitation: {invitationId} on call {callConnectionId}"); - - var result = await callConnection.CancelAddParticipantOperationAsync(cancelAddParticipantOperationOptions); - var operationStatus = result.GetRawResponse().ToString(); - - string successMessage = $"Successfully canceled add participant operation async. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); + Response result = async + ? await connection.RemoveParticipantAsync(options) + : connection.RemoveParticipant(options); - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = operationStatus - }); - } - catch (ArgumentNullException ex) - { - _logger.LogInformation($"Invitation ID validation error for call {callConnectionId}: {ex.Message}"); - return BadRequest($"Invalid invitation ID: {ex.Message}"); - } - catch (Exception ex) - { - _logger.LogInformation($"Error canceling add participant operation for call {callConnectionId}: {ex.Message}"); - return Problem($"Failed to cancel add participant operation: {ex.Message}"); - } - } - - /// - /// Cancels adding a participant to a call - /// - /// Call connection ID - /// Invitation ID to cancel - /// Operation result - [HttpPost("cancelAddParticipant")] - [Tags("Add/Remove Participant APIs")] - public IActionResult CancelAddParticipant(string callConnectionId, string invitationId) - { - try - { - _logger.LogInformation($"Starting to cancel add participant with invitation ID: {invitationId} for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - CancelAddParticipantOperationOptions cancelAddParticipantOperationOptions = new CancelAddParticipantOperationOptions(invitationId) - { - OperationContext = "CancelAddingParticipantContext" - }; + _logger.LogInformation( + $"{opName} participant removed: Call={callConnectionId}, CorrId={props.CorrelationId}, " + + $"Status={result.GetRawResponse().Status}"); - _logger.LogInformation($"Executing CancelAddParticipantOperation for invitation: {invitationId} on call {callConnectionId}"); - - var result = callConnection.CancelAddParticipantOperationAsync(cancelAddParticipantOperationOptions); - var operationInfo = $"Task type: {result.GetType()}"; - - string successMessage = $"Successfully canceled add participant operation. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationInfo: {operationInfo}"; - _logger.LogInformation(successMessage); - return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = "Operation Started" + CorrelationId = props.CorrelationId, + Status = $"{result.GetRawResponse().Status}" }); } - catch (ArgumentNullException ex) - { - _logger.LogInformation($"Invitation ID validation error for call {callConnectionId}: {ex.Message}"); - return BadRequest($"Invalid invitation ID: {ex.Message}"); - } - catch (Exception ex) - { - _logger.LogInformation($"Error canceling add participant operation for call {callConnectionId}: {ex.Message}"); - return Problem($"Failed to cancel add participant operation: {ex.Message}"); - } - } - /// - /// Gets an ACS participant information asynchronously - /// - /// Call connection ID - /// ACS participant ID to retrieve - /// Participant information - [HttpGet("getAcsParticipantAsync/{callConnectionId}/{acsTarget}")] - [Tags("Get Participant APIs")] - public async Task GetAcsParticipantAsync(string callConnectionId, string acsTarget) - { - try - { - _logger.LogInformation($"Starting to get ACS participant: {acsTarget} for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - CallParticipant participant = await callConnection.GetParticipantAsync(new CommunicationUserIdentifier(acsTarget)); - - if (participant != null) - { - string participantInfo = $"Participant: {participant.Identifier.RawId}, IsOnHold: {participant.IsOnHold}"; - _logger.LogInformation($"{participantInfo}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Participant = new - { - Id = participant.Identifier.RawId, - IsOnHold = participant.IsOnHold, - IsMuted = participant.IsMuted, - Type = "ACS User" - } - }); - } - else - { - _logger.LogInformation($"No participant found with target: {acsTarget}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Message = "Participant not found" - }); - } - } catch (Exception ex) { - _logger.LogError($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Problem($"Failed to get participant: {ex.Message}"); + _logger.LogError(ex, $"Error removing {opName} participant"); + return Problem($"Failed to remove participant: {ex.Message}"); } } - /// - /// Gets an ACS participant information synchronously - /// - /// Call connection ID - /// ACS participant ID to retrieve - /// Participant information - [HttpGet("getAcsParticipant/{callConnectionId}/{acsTarget}")] - [Tags("Get Participant APIs")] - public IActionResult GetAcsParticipant(string callConnectionId, string acsTarget) + private async Task HandleGetParticipant( + string callConnectionId, + string participantId, + bool isPstn, + bool async) { - try - { - _logger.LogInformation($"Starting to get ACS participant: {acsTarget} for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + var opName = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting to get {opName} participant: {participantId} for call {callConnectionId}"); - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - CallParticipant participant = callConnection.GetParticipant(new CommunicationUserIdentifier(acsTarget)); - - if (participant != null) - { - string participantInfo = $"Participant: {participant.Identifier.RawId}, IsOnHold: {participant.IsOnHold}"; - _logger.LogInformation($"{participantInfo}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Participant = new - { - Id = participant.Identifier.RawId, - IsOnHold = participant.IsOnHold, - IsMuted = participant.IsMuted, - Type = "ACS User" - } - }); - } - else - { - _logger.LogInformation($"No participant found with target: {acsTarget}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Message = "Participant not found" - }); - } - } - catch (Exception ex) - { - _logger.LogError($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Problem($"Failed to get participant: {ex.Message}"); - } - } - - /// - /// Gets all participants in a call asynchronously - /// - /// Call connection ID - /// List of participants - [HttpGet("getParticipantListAsync/{callConnectionId}")] - [Tags("Get Participant APIs")] - public async Task GetParticipantListAsync(string callConnectionId) - { try { - _logger.LogInformation($"Starting to get participant list for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - var list = await callConnection.GetParticipantsAsync(); - - var participants = list.Value.Select(p => new - { - Id = p.Identifier.RawId, - IsOnHold = p.IsOnHold, - IsMuted = p.IsMuted, - IdentifierType = p.Identifier.GetType().ToString() - }).ToList(); - - int participantCount = participants.Count; - _logger.LogInformation($"Found {participantCount} participants. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - - foreach (var participant in list.Value) - { - _logger.LogInformation("----------------------------------------------------------------------"); - _logger.LogInformation($"Participant: {participant.Identifier.RawId}, IsOnHold: {participant.IsOnHold}, IsMuted: {participant.IsMuted}. CallConnectionId: {callConnectionId}"); - _logger.LogInformation("----------------------------------------------------------------------"); - } + var props = _service.GetCallConnectionProperties(callConnectionId); + var connection = _service.GetCallConnection(callConnectionId); + + CallParticipant participant = async + ? await connection.GetParticipantAsync( + isPstn + ? (CommunicationIdentifier)new PhoneNumberIdentifier(participantId) + : new CommunicationUserIdentifier(participantId)) + : connection.GetParticipant( + isPstn + ? (CommunicationIdentifier)new PhoneNumberIdentifier(participantId) + : new CommunicationUserIdentifier(participantId)); + + if (participant == null) + return NotFound(new { callConnectionId, correlationId = props.CorrelationId, Message = "Not found" }); return Ok(new { CallConnectionId = callConnectionId, - CorrelationId = correlationId, - CallStatus = callStatus, - ParticipantCount = participantCount, - Participants = participants + CorrelationId = props.CorrelationId, + Participant = new + { + RawId = participant.Identifier.RawId, + IsOnHold = participant.IsOnHold, + IsMuted = participant.IsMuted + } }); } catch (Exception ex) { - _logger.LogError($"Error getting participant list: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Problem($"Failed to get participant list: {ex.Message}"); + _logger.LogError(ex, $"Error getting {opName} participant"); + return Problem($"Failed to get participant: {ex.Message}"); } } - /// - /// Gets all participants in a call synchronously - /// - /// Call connection ID - /// List of participants - [HttpGet("getParticipantList/{callConnectionId}")] - [Tags("Get Participant APIs")] - public IActionResult GetParticipantList(string callConnectionId) + private async Task HandleMuteParticipant( + string callConnectionId, + string participantId, + bool isPstn, + bool async) { + var opName = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting to mute {opName} participant: {participantId} for call {callConnectionId}"); + try { - _logger.LogInformation($"Starting to get participant list for call {callConnectionId}"); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - var list = callConnection.GetParticipants(); + var props = _service.GetCallConnectionProperties(callConnectionId); + var connection = _service.GetCallConnection(callConnectionId); - var participants = list.Value.Select(p => new - { - Id = p.Identifier.RawId, - IsOnHold = p.IsOnHold, - IsMuted = p.IsMuted, - IdentifierType = p.Identifier.GetType().ToString() - }).ToList(); + var target = isPstn + ? (CommunicationIdentifier)new PhoneNumberIdentifier(participantId) + : new CommunicationUserIdentifier(participantId); - int participantCount = participants.Count; - _logger.LogInformation($"Found {participantCount} participants. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); + Response result = async + ? await connection.MuteParticipantAsync(target) + : connection.MuteParticipant(target); - foreach (var participant in list.Value) - { - _logger.LogInformation("----------------------------------------------------------------------"); - _logger.LogInformation($"Participant: {participant.Identifier.RawId}, IsOnHold: {participant.IsOnHold}, IsMuted: {participant.IsMuted}. CallConnectionId: {callConnectionId}"); - _logger.LogInformation("----------------------------------------------------------------------"); - } + _logger.LogInformation( + $"{opName} participant muted: Call={callConnectionId}, CorrId={props.CorrelationId}, " + + $"Status={result.GetRawResponse().Status}"); - return Ok(new - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - CallStatus = callStatus, - ParticipantCount = participantCount, - Participants = participants - }); - } - catch (Exception ex) - { - _logger.LogError($"Error getting participant list: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Problem($"Failed to get participant list: {ex.Message}"); - } - } - - [HttpPost("muteAcsParticipantAsync")] - [Tags("Mute Participant APIs")] - public async Task MuteAcsParticipantAsync(string callConnectionId, string acsTarget) - { - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - _logger.LogInformation($"Starting to mute ACS participant async: {acsTarget} for call {callConnectionId}, CorrelationId: {correlationId}"); - - try - { - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - var result = await callConnection.MuteParticipantAsync(target); - - string successMessage = $"Participant muted successfully. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {result.GetRawResponse().Status}"; - _logger.LogInformation(successMessage); - return Ok(new CallConnectionResponse { CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = result.GetRawResponse().Status.ToString() + CorrelationId = props.CorrelationId, + Status = $"{result.GetRawResponse().Status}" }); } catch (Exception ex) { - string errorMessage = $"Error muting participant. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Error: {ex.Message}"; - _logger.LogError(errorMessage); + _logger.LogError(ex, $"Error muting {opName} participant"); return Problem($"Failed to mute participant: {ex.Message}"); } } - - [HttpPost("muteAcsParticipant")] - [Tags("Mute Participant APIs")] - public IActionResult MuteAcsParticipant(string callConnectionId, string acsTarget) - { - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - _logger.LogInformation($"Starting to mute ACS participant: {acsTarget} for call {callConnectionId}, CorrelationId: {correlationId}"); - - try - { - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallConnection callConnection = _service.GetCallConnection(callConnectionId); - var result = callConnection.MuteParticipant(target); - - string successMessage = $"Participant muted successfully. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {result.GetRawResponse().Status}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse - { - CallConnectionId = callConnectionId, - CorrelationId = correlationId, - Status = result.GetRawResponse().Status.ToString() - }); - } - catch (Exception ex) - { - string errorMessage = $"Error muting participant. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Error: {ex.Message}"; - _logger.LogError(errorMessage); - return Problem($"Failed to mute participant: {ex.Message}"); - } - } - } -} - - - -#region Add/Remove Participant to PSTN -/********************************************************************************************** -app.MapPost("/addPstnParticipantAsync", async (string callConnectionId, string pstnParticipant, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to add PSTN participant async: {pstnParticipant} for call {callConnectionId}"); - LogCollector.Log($"Starting to add PSTN participant async: {pstnParticipant} for call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - CallInvite callInvite = new CallInvite(new PhoneNumberIdentifier(pstnParticipant), - new PhoneNumberIdentifier(acsPhoneNumber)); - var addParticipantOptions = new AddParticipantOptions(callInvite) - { - OperationContext = "addPstnUserContext", - InvitationTimeoutInSeconds = 30, - }; - - logger.LogInformation($"Executing AddParticipantAsync for PSTN participant: {pstnParticipant} on call {callConnectionId}"); - LogCollector.Log($"Executing AddParticipantAsync for PSTN participant: {pstnParticipant} on call {callConnectionId}"); - - var result = await callConnection.AddParticipantAsync(addParticipantOptions); - - logger.LogInformation($"Successfully added PSTN participant async: {pstnParticipant} to call {callConnectionId}"); - LogCollector.Log($"Successfully added PSTN participant async: {pstnParticipant} to call {callConnectionId}"); - - return Results.Ok(new { Result = result, CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"PSTN participant validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"PSTN participant validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid PSTN participant: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error adding PSTN participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error adding PSTN participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to add PSTN participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); - -app.MapPost("/addPstnParticipant", (string callConnectionId, string pstnParticipant, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to add PSTN participant: {pstnParticipant} for call {callConnectionId}"); - LogCollector.Log($"Starting to add PSTN participant: {pstnParticipant} for call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - CallInvite callInvite = new CallInvite(new PhoneNumberIdentifier(pstnParticipant), - new PhoneNumberIdentifier(acsPhoneNumber)); - var addParticipantOptions = new AddParticipantOptions(callInvite) - { - OperationContext = "addPstnUserContext", - InvitationTimeoutInSeconds = 30, - }; - - logger.LogInformation($"Executing AddParticipant for PSTN participant: {pstnParticipant} on call {callConnectionId}"); - LogCollector.Log($"Executing AddParticipant for PSTN participant: {pstnParticipant} on call {callConnectionId}"); - - var result = callConnection.AddParticipant(addParticipantOptions); - - logger.LogInformation($"Successfully added PSTN participant: {pstnParticipant} to call {callConnectionId}"); - LogCollector.Log($"Successfully added PSTN participant: {pstnParticipant} to call {callConnectionId}"); - - return Results.Ok(new { Result = result, CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"PSTN participant validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"PSTN participant validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid PSTN participant: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error adding PSTN participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error adding PSTN participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to add PSTN participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); - -app.MapPost("/removePstnParticipantAsync", async (string callConnectionId, string pstnTarget, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to remove PSTN participant async: {pstnTarget} from call {callConnectionId}"); - LogCollector.Log($"Starting to remove PSTN participant async: {pstnTarget} from call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - RemoveParticipantOptions removeParticipantOptions = new RemoveParticipantOptions(new PhoneNumberIdentifier(pstnTarget)) - { - OperationContext = "removePstnParticipantContext" - }; - - logger.LogInformation($"Executing RemoveParticipantAsync for PSTN participant: {pstnTarget} on call {callConnectionId}"); - LogCollector.Log($"Executing RemoveParticipantAsync for PSTN participant: {pstnTarget} on call {callConnectionId}"); - - await callConnection.RemoveParticipantAsync(removeParticipantOptions); - - logger.LogInformation($"Successfully removed PSTN participant async: {pstnTarget} from call {callConnectionId}"); - LogCollector.Log($"Successfully removed PSTN participant async: {pstnTarget} from call {callConnectionId}"); - - return Results.Ok(new { Status = "Success", CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"PSTN target validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"PSTN target validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid PSTN target: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error removing PSTN participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error removing PSTN participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to remove PSTN participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); - -app.MapPost("/removePstnParticipant", (string callConnectionId, string pstnTarget, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to remove PSTN participant: {pstnTarget} from call {callConnectionId}"); - LogCollector.Log($"Starting to remove PSTN participant: {pstnTarget} from call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - RemoveParticipantOptions removeParticipantOptions = new RemoveParticipantOptions(new PhoneNumberIdentifier(pstnTarget)) - { - OperationContext = "removePstnParticipantContext" - }; - - logger.LogInformation($"Executing RemoveParticipant for PSTN participant: {pstnTarget} on call {callConnectionId}"); - LogCollector.Log($"Executing RemoveParticipant for PSTN participant: {pstnTarget} on call {callConnectionId}"); - - callConnection.RemoveParticipant(removeParticipantOptions); - - logger.LogInformation($"Successfully removed PSTN participant: {pstnTarget} from call {callConnectionId}"); - LogCollector.Log($"Successfully removed PSTN participant: {pstnTarget} from call {callConnectionId}"); - - return Results.Ok(new { Status = "Success", CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"PSTN target validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"PSTN target validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid PSTN target: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error removing PSTN participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error removing PSTN participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to remove PSTN participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); -*/ - -#endregion - -#region Add/Remove Teams Participant -/********************************************************************************************** -app.MapPost("/addTeamsParticipantAsync", async (string teamsObjectId, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to add Teams participant async: {teamsObjectId} for call {callConnectionId}"); - LogCollector.Log($"Starting to add Teams participant async: {teamsObjectId} for call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - CallInvite callInvite = new CallInvite(new MicrosoftTeamsUserIdentifier(teamsObjectId)); - var addParticipantOptions = new AddParticipantOptions(callInvite) - { - OperationContext = "addTeamsUserContext", - InvitationTimeoutInSeconds = 30, - }; - - logger.LogInformation($"Executing AddParticipantAsync for Teams participant: {teamsObjectId} on call {callConnection.CallConnectionId}"); - LogCollector.Log($"Executing AddParticipantAsync for Teams participant: {teamsObjectId} on call {callConnectionId}"); - - var result = await callConnection.AddParticipantAsync(addParticipantOptions); - - logger.LogInformation($"Successfully added Teams participant async: {teamsObjectId} to call {callConnectionId}"); - LogCollector.Log($"Successfully added Teams participant async: {teamsObjectId} to call {callConnectionId}"); - - return Results.Ok(new { Result = result, CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"Teams participant validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Teams participant validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid Teams participant: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error adding Teams participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error adding Teams participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to add Teams participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); - -app.MapPost("/addTeamsParticipant", (string callConnectionId, string teamsObjectId, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to add Teams participant: {teamsObjectId} for call {callConnectionId}"); - LogCollector.Log($"Starting to add Teams participant: {teamsObjectId} for call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - CallInvite callInvite = new CallInvite(new MicrosoftTeamsUserIdentifier(teamsObjectId)); - var addParticipantOptions = new AddParticipantOptions(callInvite) - { - OperationContext = "addTeamsUserContext", - InvitationTimeoutInSeconds = 30, - }; - - logger.LogInformation($"Executing AddParticipant for Teams participant: {teamsObjectId} on call {callConnectionId}"); - LogCollector.Log($"Executing AddParticipant for Teams participant: {teamsObjectId} on call {callConnectionId}"); - - var result = callConnection.AddParticipant(addParticipantOptions); - - logger.LogInformation($"Successfully added Teams participant: {teamsObjectId} to call {callConnectionId}"); - LogCollector.Log($"Successfully added Teams participant: {teamsObjectId} to call {callConnectionId}"); - - return Results.Ok(new { Result = result, CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"Teams participant validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Teams participant validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid Teams participant: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error adding Teams participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error adding Teams participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to add Teams participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); - -app.MapPost("/removeTeamsParticipantAsync", async (string callConnectionId, string teamsObjectId, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to remove Teams participant async: {teamsObjectId} from call {callConnectionId}"); - LogCollector.Log($"Starting to remove Teams participant async: {teamsObjectId} from call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - RemoveParticipantOptions removeParticipantOptions = new RemoveParticipantOptions(new MicrosoftTeamsUserIdentifier(teamsObjectId)) - { - OperationContext = "removeTeamsParticipantContext" - }; - - logger.LogInformation($"Executing RemoveParticipantAsync for Teams participant: {teamsObjectId} on call {callConnectionId}"); - LogCollector.Log($"Executing RemoveParticipantAsync for Teams participant: {teamsObjectId} on call {callConnectionId}"); - - await callConnection.RemoveParticipantAsync(removeParticipantOptions); - - logger.LogInformation($"Successfully removed Teams participant async: {teamsObjectId} from call {callConnectionId}"); - LogCollector.Log($"Successfully removed Teams participant async: {teamsObjectId} from call {callConnectionId}"); - - return Results.Ok(new { Status = "Success", CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"Teams object ID validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Teams object ID validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid Teams object ID: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error removing Teams participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error removing Teams participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to remove Teams participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); - -app.MapPost("/removeTeamsParticipant", (string callConnectionId, string teamsObjectId, ILogger logger) => -{ - try - { - logger.LogInformation($"Starting to remove Teams participant: {teamsObjectId} from call {callConnectionId}"); - LogCollector.Log($"Starting to remove Teams participant: {teamsObjectId} from call {callConnectionId}"); - - CallConnection callConnection = GetConnection(callConnectionId); - RemoveParticipantOptions removeParticipantOptions = new RemoveParticipantOptions(new MicrosoftTeamsUserIdentifier(teamsObjectId)) - { - OperationContext = "removeTeamsParticipantContext" - }; - - logger.LogInformation($"Executing RemoveParticipant for Teams participant: {teamsObjectId} on call {callConnectionId}"); - LogCollector.Log($"Executing RemoveParticipant for Teams participant: {teamsObjectId} on call {callConnectionId}"); - - callConnection.RemoveParticipantAsync(removeParticipantOptions); - - logger.LogInformation($"Successfully removed Teams participant: {teamsObjectId} from call {callConnectionId}"); - LogCollector.Log($"Successfully removed Teams participant: {teamsObjectId} from call {callConnectionId}"); - - return Results.Ok(new { Status = "Success", CallConnectionId = callConnectionId }); - } - catch (ArgumentNullException ex) - { - logger.LogInformation($"Teams object ID validation error for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Teams object ID validation error for call {callConnectionId}: {ex.Message}"); - return Results.BadRequest($"Invalid Teams object ID: {ex.Message}"); - } - catch (Exception ex) - { - logger.LogInformation($"Error removing Teams participant for call {callConnectionId}: {ex.Message}"); - LogCollector.Log($"Error removing Teams participant for call {callConnectionId}: {ex.Message}"); - return Results.Problem($"Failed to remove Teams participant: {ex.Message}"); - } -}).WithTags("Add/Remove Participant APIs"); -*/ -#endregion - -#region Get Participant -/* -app.MapPost("/getPstnParticipantAsync", async (string callConnectionId, string pstnTarget, ILogger logger) => -{ - try - { - CallConnection callConnection = GetConnection(callConnectionId); - CallParticipant participant = await callConnection.GetParticipantAsync(new PhoneNumberIdentifier(pstnTarget)); - - if (participant != null) - { - logger.LogInformation($"Participant:-->{participant.Identifier.RawId.ToString()}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"Participant:-->{participant.Identifier.RawId.ToString()}. CallConnectionId: {callConnectionId}"); - logger.LogInformation($"Is Participant on hold:-->{participant.IsOnHold}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"Is Participant on hold:-->{participant.IsOnHold}. CallConnectionId: {callConnectionId}"); - } - else - { - logger.LogInformation($"No participant found with target: {pstnTarget}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"No participant found with target: {pstnTarget}. CallConnectionId: {callConnectionId}"); - } - return Results.Ok(); } - catch (Exception ex) - { - logger.LogError($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Results.Problem($"Failed to get participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - } -}).WithTags("Get Participant APIs"); - -app.MapPost("/getPstnParticipant", (string callConnectionId, string pstnTarget, ILogger logger) => -{ - try - { - CallConnection callConnection = GetConnection(callConnectionId); - CallParticipant participant = callConnection.GetParticipant(new PhoneNumberIdentifier(pstnTarget)); - - if (participant != null) - { - logger.LogInformation($"Participant:-->{participant.Identifier.RawId.ToString()}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"Participant:-->{participant.Identifier.RawId.ToString()}. CallConnectionId: {callConnectionId}"); - logger.LogInformation($"Is Participant on hold:-->{participant.IsOnHold}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"Is Participant on hold:-->{participant.IsOnHold}. CallConnectionId: {callConnectionId}"); - } - else - { - logger.LogInformation($"No participant found with target: {pstnTarget}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"No participant found with target: {pstnTarget}. CallConnectionId: {callConnectionId}"); - } - return Results.Ok(); - } - catch (Exception ex) - { - logger.LogError($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - LogCollector.Log($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - return Results.Problem($"Failed to get participant: {ex.Message}. CallConnectionId: {callConnectionId}"); - } -}).WithTags("Get Participant APIs"); -*/ -#region Get Participants to Teams - -///// -///// Gets a Teams participant information asynchronously -///// -///// Call connection ID -///// Teams user ID to retrieve -///// Participant information -//[HttpGet("getTeamsParticipantAsync/{callConnectionId}/{teamsObjectId}")] -//[Tags("Get Participant APIs")] -//public async Task GetTeamsParticipantAsync(string callConnectionId, string teamsObjectId) -//{ -// try -// { -// _logger.LogInformation($"Starting to get Teams participant: {teamsObjectId} for call {callConnectionId}"); - -// var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; -// var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - -// CallConnection callConnection = _service.GetCallConnection(callConnectionId); -// CallParticipant participant = await callConnection.GetParticipantAsync(new MicrosoftTeamsUserIdentifier(teamsObjectId)); - -// if (participant != null) -// { -// string participantInfo = $"Participant: {participant.Identifier.RawId}, IsOnHold: {participant.IsOnHold}"; -// _logger.LogInformation($"{participantInfo}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - -// return Ok(new -// { -// CallConnectionId = callConnectionId, -// CorrelationId = correlationId, -// Participant = new -// { -// Id = participant.Identifier.RawId, -// IsOnHold = participant.IsOnHold, -// IsMuted = participant.IsMuted, -// Type = "Teams User" -// } -// }); -// } -// else -// { -// _logger.LogInformation($"No participant found with target: {teamsObjectId}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - -// return Ok(new -// { -// CallConnectionId = callConnectionId, -// CorrelationId = correlationId, -// Message = "Participant not found" -// }); -// } -// } -// catch (Exception ex) -// { -// _logger.LogError($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); -// return Problem($"Failed to get participant: {ex.Message}"); -// } -//} - -///// -///// Gets a Teams participant information synchronously -///// -///// Call connection ID -///// Teams user ID to retrieve -///// Participant information -//[HttpGet("getTeamsParticipant/{callConnectionId}/{teamsObjectId}")] -//[Tags("Get Participant APIs")] -//public IActionResult GetTeamsParticipant(string callConnectionId, string teamsObjectId) -//{ -// try -// { -// _logger.LogInformation($"Starting to get Teams participant: {teamsObjectId} for call {callConnectionId}"); - -// var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; -// var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); - -// CallConnection callConnection = _service.GetCallConnection(callConnectionId); -// CallParticipant participant = callConnection.GetParticipant(new MicrosoftTeamsUserIdentifier(teamsObjectId)); - -// if (participant != null) -// { -// string participantInfo = $"Participant: {participant.Identifier.RawId}, IsOnHold: {participant.IsOnHold}"; -// _logger.LogInformation($"{participantInfo}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - -// return Ok(new -// { -// CallConnectionId = callConnectionId, -// CorrelationId = correlationId, -// Participant = new -// { -// Id = participant.Identifier.RawId, -// IsOnHold = participant.IsOnHold, -// IsMuted = participant.IsMuted, -// Type = "Teams User" -// } -// }); -// } -// else -// { -// _logger.LogInformation($"No participant found with target: {teamsObjectId}. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}"); - -// return Ok(new -// { -// CallConnectionId = callConnectionId, -// CorrelationId = correlationId, -// Message = "Participant not found" -// }); -// } -// } -// catch (Exception ex) -// { -// _logger.LogError($"Error getting participant: {ex.Message}. CallConnectionId: {callConnectionId}"); -// return Problem($"Failed to get participant: {ex.Message}"); -// } -//} -#endregion -#endregion \ No newline at end of file +} \ No newline at end of file From 646100169c2b84d873e2449b2182fed0315def6b Mon Sep 17 00:00:00 2001 From: Nikhil Malviya Date: Fri, 23 May 2025 14:36:11 +0530 Subject: [PATCH 7/8] Refactored configuration handling and enhanced swagger - Replaced ConfigurationRequest with a new CommunicationConfiguration model. - Updated controllers to use CommunicationConfigurationService for configuration retrieval. - Introduced SseController to handle server-sent events for incoming call notifications. - Updated Program.cs to configure new services and remove obsolete configurations. - Modified appsettings.json to reflect new configuration structure. - Enhanced Swagger UI with new styles and functionality for configuring communication and incoming call parameters. --- .../CallAutomationService.cs | 327 ++++++++-------- .../CallAutomationEventsController.cs | 129 +++---- .../Controllers/CallController.cs | 14 +- .../Controllers/ConnectController.cs | 30 +- .../Controllers/DTMFController.cs | 5 +- .../Controllers/MediaController.cs | 124 +++--- .../Controllers/ParticipantsController.cs | 8 +- .../Controllers/RecordingsController.cs | 18 +- .../Controllers/SseController.cs | 49 +++ .../Models/CommunicationConfiguration.cs | 19 + .../Models/ConfigurationRequest.cs | 12 - .../Call_Automation_GCCH/Program.cs | 63 ++- .../CommunicationConfigurationService.cs | 46 +++ .../Services/ServerSentEventsService.cs | 27 ++ .../Call_Automation_GCCH/appsettings.json | 21 +- .../wwwroot/swagger-ui/GCCHSwagger.html | 87 ----- .../wwwroot/swagger-ui/gcch-swagger.css | 147 +++++++ .../wwwroot/swagger-ui/gcch-swagger.html | 105 +++++ .../wwwroot/swagger-ui/gcch-swagger.js | 360 ++++++++++++++++++ 19 files changed, 1114 insertions(+), 477 deletions(-) create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/Controllers/SseController.cs create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/Models/CommunicationConfiguration.cs delete mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/Models/ConfigurationRequest.cs create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/Services/CommunicationConfigurationService.cs create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/Services/ServerSentEventsService.cs delete mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/GCCHSwagger.html create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.css create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.html create mode 100644 Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.js diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs b/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs index 14207dcf..893bd55b 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs @@ -4,171 +4,174 @@ namespace Call_Automation_GCCH.Services { - public class CallAutomationService - { - private CallAutomationClient _client; - private ILogger _logger; - private static string? _recordingLocation; - private static string _recordingFileFormat = "mp4"; - private string _currentPmaEndpoint = string.Empty; - - public CallAutomationService(string connectionString, string pmaEndpoint, ILogger logger) + public class CallAutomationService { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _currentPmaEndpoint = pmaEndpoint; - - if (!string.IsNullOrEmpty(pmaEndpoint)) - { - _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); - } - else - { - _logger.LogWarning("PmaEndpoint is empty. Creating CallAutomationClient without PmaEndpoint parameter."); - _client = new CallAutomationClient(connectionString: connectionString); - } - } - - /// - /// Gets the call automation client for direct operations - /// - /// CallAutomationClient instance - public CallAutomationClient GetCallAutomationClient() - { - return _client; - } - - /// - /// Gets the recording location - /// - /// Recording location string - public static string GetRecordingLocation() - { - return _recordingLocation; - } - - /// - /// Sets the recording location - /// - /// The recording location to set - public static void SetRecordingLocation(string location) - { - _recordingLocation = location; - } - - /// - /// Gets the recording file format - /// - /// Recording file format string - public static string GetRecordingFileFormat() - { - return _recordingFileFormat; - } - - /// - /// Sets the recording file format - /// - /// The recording file format to set - public static void SetRecordingFileFormat(string format) - { - _recordingFileFormat = format; - } - - public CallConnection GetCallConnection(string callConnectionId) - { - try - { - return _client.GetCallConnection(callConnectionId); - } - catch (Exception ex) - { - string errorMessage = $"Error in GetCallConnection: {ex.Message}. CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - throw; - } - } + private CallAutomationClient _client; + private readonly ICommunicationConfigurationService _communicationConfigurationService; + private readonly ILogger _logger; + private static string? _recordingLocation; + private static string _recordingFileFormat = "mp4"; + + public CallAutomationService(ICommunicationConfigurationService communicationConfigurationService, ILogger logger) + { + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + var pmaEndpoint = _communicationConfigurationService.communicationConfiguration.PmaEndpoint; + var connectionString = _communicationConfigurationService.communicationConfiguration.AcsConnectionString; + + if (!string.IsNullOrEmpty(pmaEndpoint)) + { + _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); + } + else + { + _logger.LogWarning("PmaEndpoint is empty. Creating CallAutomationClient without PmaEndpoint parameter."); + _client = new CallAutomationClient(connectionString: connectionString); + } + } + + /// + /// Gets the call automation client for direct operations + /// + /// CallAutomationClient instance + public CallAutomationClient GetCallAutomationClient() + { + return _client; + } + + /// + /// Gets the recording location + /// + /// Recording location string + public static string? GetRecordingLocation() + { + return _recordingLocation; + } + + /// + /// Sets the recording location + /// + /// The recording location to set + public static void SetRecordingLocation(string location) + { + _recordingLocation = location; + } + + /// + /// Gets the recording file format + /// + /// Recording file format string + public static string GetRecordingFileFormat() + { + return _recordingFileFormat; + } + + /// + /// Sets the recording file format + /// + /// The recording file format to set + public static void SetRecordingFileFormat(string format) + { + _recordingFileFormat = format; + } + + public CallConnection GetCallConnection(string callConnectionId) + { + try + { + return _client.GetCallConnection(callConnectionId); + } + catch (Exception ex) + { + string errorMessage = $"Error in GetCallConnection: {ex.Message}. CallConnectionId: {callConnectionId}"; + _logger.LogError(errorMessage); + throw; + } + } + + public CallMedia GetCallMedia(string callConnectionId) + { + try + { + return _client.GetCallConnection(callConnectionId).GetCallMedia(); + } + catch (Exception ex) + { + string errorMessage = $"Error in GetCallMedia: {ex.Message}. CallConnectionId: {callConnectionId}"; + _logger.LogError(errorMessage); + throw; // Rethrow so the caller can handle or return an error response + } + } + + public CallConnectionProperties GetCallConnectionProperties(string callConnectionId) + { + try + { + return _client.GetCallConnection(callConnectionId).GetCallConnectionProperties(); + } + catch (Exception ex) + { + string errorMessage = $"Error in GetCallConnectionProperties: {ex.Message}. CallConnectionId: {callConnectionId}"; + _logger.LogError(errorMessage); + throw; + } + } + + /// + /// Updates the CallAutomationClient with new connection settings + /// + /// The ACS connection string + /// The PMA endpoint to use + // public void UpdateClient(string connectionString, string pmaEndpoint) + // { + // _logger = _logger ?? throw new InvalidOperationException("Logger is not initialized"); + // _currentPmaEndpoint = pmaEndpoint; + + // if (!string.IsNullOrEmpty(pmaEndpoint)) + // { + // _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); + // _logger.LogInformation($"CallAutomationClient recreated with PMA endpoint: {pmaEndpoint}"); + // } + // else + // { + // _logger.LogWarning("PmaEndpoint is empty. Creating CallAutomationClient without PmaEndpoint parameter."); + // _client = new CallAutomationClient(connectionString: connectionString); + // } + // } + + // public string GetCurrentPmaEndpoint() + // { + // return _currentPmaEndpoint; + // } + + //Need Azure Cognitive services for this so in phase 2 + //public List GetChoices() + //{ + // return new List { + // new RecognitionChoice("Confirm", new List { + // "Confirm", + // "First", + // "One" + // }) { + // Tone = DtmfTone.One + // }, + // new RecognitionChoice("Cancel", new List { + // "Cancel", + // "Second", + // "Two" + // }) { + // Tone = DtmfTone.Two + // } + //}; + //public List GetChoices() => new List + // { + // // Only DTMF tones, no speech phrases + // new RecognitionChoice("Confirm", new List()) { Tone = DtmfTone.One }, + // new RecognitionChoice("Cancel", new List()) { Tone = DtmfTone.Two } + // }; - public CallMedia GetCallMedia(string callConnectionId) - { - try - { - return _client.GetCallConnection(callConnectionId).GetCallMedia(); - } - catch (Exception ex) - { - string errorMessage = $"Error in GetCallMedia: {ex.Message}. CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - throw; // Rethrow so the caller can handle or return an error response - } - } - - public CallConnectionProperties GetCallConnectionProperties(string callConnectionId) - { - try - { - return _client.GetCallConnection(callConnectionId).GetCallConnectionProperties(); - } - catch (Exception ex) - { - string errorMessage = $"Error in GetCallConnectionProperties: {ex.Message}. CallConnectionId: {callConnectionId}"; - _logger.LogError(errorMessage); - throw; - } } - - /// - /// Updates the CallAutomationClient with new connection settings - /// - /// The ACS connection string - /// The PMA endpoint to use - public void UpdateClient(string connectionString, string pmaEndpoint) - { - _logger = _logger ?? throw new InvalidOperationException("Logger is not initialized"); - _currentPmaEndpoint = pmaEndpoint; - - if (!string.IsNullOrEmpty(pmaEndpoint)) - { - _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); - _logger.LogInformation($"CallAutomationClient recreated with PMA endpoint: {pmaEndpoint}"); - } - else - { - _logger.LogWarning("PmaEndpoint is empty. Creating CallAutomationClient without PmaEndpoint parameter."); - _client = new CallAutomationClient(connectionString: connectionString); - } - } - - public string GetCurrentPmaEndpoint() - { - return _currentPmaEndpoint; - } - - //Need Azure Cognitive services for this so in phase 2 - //public List GetChoices() - //{ - // return new List { - // new RecognitionChoice("Confirm", new List { - // "Confirm", - // "First", - // "One" - // }) { - // Tone = DtmfTone.One - // }, - // new RecognitionChoice("Cancel", new List { - // "Cancel", - // "Second", - // "Two" - // }) { - // Tone = DtmfTone.Two - // } - //}; - //public List GetChoices() => new List - // { - // // Only DTMF tones, no speech phrases - // new RecognitionChoice("Confirm", new List()) { Tone = DtmfTone.One }, - // new RecognitionChoice("Cancel", new List()) { Tone = DtmfTone.Two } - // }; - - } } diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs index 4f0f40fe..e389c706 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs @@ -22,15 +22,15 @@ public class CallAutomationEventsController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object + private readonly ICommunicationConfigurationService _communicationConfigurationService; // final, bound object public CallAutomationEventsController( CallAutomationService service, - ILogger logger, IOptions configOptions) + ILogger logger, ICommunicationConfigurationService communicationConfigurationService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } /// @@ -79,7 +79,7 @@ public async Task HandleEvents([FromBody] EventGridEvent[] eventG var callerId = incomingCallEventData.FromCommunicationIdentifier.RawId; _logger.LogInformation($"Incoming call from caller ID: {callerId}, CorrelationId: {incomingCallEventData.CorrelationId}"); - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), $"/api/callbacks"); + var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), $"/api/callbacks"); _logger.LogInformation($"Incoming call - correlationId: {incomingCallEventData.CorrelationId}, Callback url: {callbackUri}"); var options = new AnswerCallOptions(incomingCallEventData.IncomingCallContext, callbackUri) @@ -173,61 +173,62 @@ public IActionResult HandleCallbacks([FromBody] CloudEvent[] cloudEvents) /// Action result indicating success or error [HttpPost("/setRegion")] [Tags("Region Configuration")] - public IActionResult SetRegion(bool isArizona) - { - try - { - _logger.LogInformation($"Changing region configuration. IsArizona: {isArizona}"); - - // Get the current configuration section - var configSection = HttpContext.RequestServices.GetRequiredService().GetSection("CommunicationSettings"); - - // Get the current endpoint being used to determine if an update is needed - string currentEndpoint = _service.GetCurrentPmaEndpoint() ?? string.Empty; - string newEndpoint = isArizona - ? configSection["PmaEndpointArizona"] ?? string.Empty - : configSection["PmaEndpointTexas"] ?? string.Empty; - - // Check if new endpoint is empty - if (string.IsNullOrEmpty(newEndpoint)) - { - _logger.LogWarning($"The {(isArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); - } + // public IActionResult SetRegion(bool isArizona) + // { + // try + // { + // _logger.LogInformation($"Changing region configuration. IsArizona: {isArizona}"); + + // // Get the current configuration section + // var configSection = HttpContext.RequestServices.GetRequiredService().GetSection("CommunicationSettings"); + + // // Get the current endpoint being used to determine if an update is needed + // string currentEndpoint = _service.GetCurrentPmaEndpoint() ?? string.Empty; + // string newEndpoint = isArizona + // ? configSection["PmaEndpointArizona"] ?? string.Empty + // : configSection["PmaEndpointTexas"] ?? string.Empty; + + // // Check if new endpoint is empty + // if (string.IsNullOrEmpty(newEndpoint)) + // { + // _logger.LogWarning($"The {(isArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); + // } + + // // Only update if the endpoint would actually change + // if (currentEndpoint == newEndpoint) + // { + // if (string.IsNullOrEmpty(currentEndpoint)) + // { + // return Ok($"Configuration unchanged as the endpoints are empty"); + // } + // else + // { + // return Ok($"Configuration unchanged. Already using {(isArizona ? "Arizona" : "Texas")} region."); + // } + // } + + // // Update the IsArizona setting in memory + // ((IConfigurationSection)configSection.GetSection("IsArizona")).Value = isArizona.ToString(); + + // // Update the client with the new endpoint + // var connectionString = configSection["AcsConnectionString"] ?? string.Empty; + // if (string.IsNullOrEmpty(connectionString)) + // { + // _logger.LogError("AcsConnectionString is empty"); + // return Problem("AcsConnectionString is empty"); + // } + + // _service.UpdateClient(connectionString, newEndpoint); + + // return Ok($"Region updated successfully to {(isArizona ? "Arizona" : "Texas")}."); + // } + // catch (Exception ex) + // { + // _logger.LogError($"Error updating region configuration: {ex.Message}"); + // return Problem($"Failed to update region configuration: {ex.Message}"); + // } + // } - // Only update if the endpoint would actually change - if (currentEndpoint == newEndpoint) - { - if (string.IsNullOrEmpty(currentEndpoint)) - { - return Ok($"Configuration unchanged as the endpoints are empty"); - } - else - { - return Ok($"Configuration unchanged. Already using {(isArizona ? "Arizona" : "Texas")} region."); - } - } - - // Update the IsArizona setting in memory - ((IConfigurationSection)configSection.GetSection("IsArizona")).Value = isArizona.ToString(); - - // Update the client with the new endpoint - var connectionString = configSection["AcsConnectionString"] ?? string.Empty; - if (string.IsNullOrEmpty(connectionString)) - { - _logger.LogError("AcsConnectionString is empty"); - return Problem("AcsConnectionString is empty"); - } - - _service.UpdateClient(connectionString, newEndpoint); - - return Ok($"Region updated successfully to {(isArizona ? "Arizona" : "Texas")}."); - } - catch (Exception ex) - { - _logger.LogError($"Error updating region configuration: {ex.Message}"); - return Problem($"Failed to update region configuration: {ex.Message}"); - } - } /// /// Processes individual call automation events /// @@ -472,10 +473,10 @@ public async Task HandleIncomingCallWithOptions( [FromQuery] bool mediaStreaming = true, [FromQuery] bool bidirectionalStreaming = true) { - MediaStreamingAudioChannel audioChannel = audioChannelMixed - ? MediaStreamingAudioChannel.Mixed + MediaStreamingAudioChannel audioChannel = audioChannelMixed + ? MediaStreamingAudioChannel.Mixed : MediaStreamingAudioChannel.Unmixed; - + bool isPcm24kHz = !audioFormat16k; return await HandleIncomingCallWithMediaStreaming( @@ -524,9 +525,9 @@ private async Task HandleIncomingCallWithMediaStreaming( var callerId = incomingCallEventData.FromCommunicationIdentifier.RawId; _logger.LogInformation($"Incoming call from caller ID: {callerId}, CorrelationId: {incomingCallEventData.CorrelationId}"); - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), $"/api/callbacks"); - var websocketUri = new Uri(_config.CallbackUriHost.Replace("https", "wss") + "/ws"); - + var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), $"/api/callbacks"); + var websocketUri = new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"); + _logger.LogInformation($"Incoming call with media streaming - correlationId: {incomingCallEventData.CorrelationId}, " + $"AudioChannel: {audioChannel}, EnableMediaStreaming: {enableMediaStreaming}, " + $"IsPcm24kHz: {isPcm24kHz}, EnableBidirectional: {enableBidirectional}"); diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs index 46bec54f..60f048ef 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs @@ -20,16 +20,16 @@ public class CallController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; + private readonly ICommunicationConfigurationService _communicationConfigurationService; public CallController( CallAutomationService service, ILogger logger, - IOptions configOptions) + ICommunicationConfigurationService communicationConfigurationService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } // @@ -125,10 +125,10 @@ private async Task HandleCreateCall( try { // Build identifier & invite - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); CallInvite invite = isPstn ? new CallInvite(new PhoneNumberIdentifier(target), - new PhoneNumberIdentifier(_config.AcsPhoneNumber)) + new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.AcsPhoneNumber)) : new CallInvite(new CommunicationUserIdentifier(target)); var options = new CreateCallOptions(invite, callbackUri); @@ -291,8 +291,8 @@ private async Task HandleGroupCall( } } - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - var sourceCallerId = new PhoneNumberIdentifier(_config.AcsPhoneNumber); + var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + var sourceCallerId = new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.AcsPhoneNumber); var createGroupOpts = new CreateGroupCallOptions(idList, callbackUri) { diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ConnectController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ConnectController.cs index 6392e980..84e985b8 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ConnectController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ConnectController.cs @@ -18,15 +18,15 @@ public class ConnectController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object + private readonly ICommunicationConfigurationService _communicationConfigurationService; // final, bound object public ConnectController( CallAutomationService service, - ILogger logger, IOptions configOptions) + ILogger logger, ICommunicationConfigurationService communicationConfigurationService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } // Not required at this time @@ -49,8 +49,8 @@ public ConnectController( // _logger.LogInformation($"Starting async room call connection to: {roomId}"); // RoomCallLocator roomCallLocator = new RoomCallLocator(roomId); - // var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - // var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + // var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + // var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; // MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(new Uri(websocketUri), MediaStreamingContent.Audio, // MediaStreamingAudioChannel.Unmixed, MediaStreamingTransport.Websocket, false); // TranscriptionOptions transcriptionOptions = new TranscriptionOptions(new Uri(websocketUri), TranscriptionTransport.Websocket, @@ -104,8 +104,8 @@ public ConnectController( // _logger.LogInformation($"Starting room call connection to: {roomId}"); // RoomCallLocator roomCallLocator = new RoomCallLocator(roomId); - // var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - // var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + // var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + // var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; // MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(new Uri(websocketUri), MediaStreamingContent.Audio, // MediaStreamingAudioChannel.Unmixed, MediaStreamingTransport.Websocket, false); // TranscriptionOptions transcriptionOptions = new TranscriptionOptions(new Uri(websocketUri), TranscriptionTransport.Websocket, @@ -159,8 +159,8 @@ public ConnectController( // _logger.LogInformation($"Starting async group call connection to: {groupId}"); // GroupCallLocator groupCallLocator = new GroupCallLocator(groupId); - // var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - // var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + // var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + // var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; // MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(new Uri(websocketUri), MediaStreamingContent.Audio, // MediaStreamingAudioChannel.Unmixed, MediaStreamingTransport.Websocket, false); // TranscriptionOptions transcriptionOptions = new TranscriptionOptions(new Uri(websocketUri), TranscriptionTransport.Websocket, @@ -219,8 +219,8 @@ public ConnectController( // _logger.LogInformation($"Starting group call connection to: {groupId}"); // GroupCallLocator groupCallLocator = new GroupCallLocator(groupId); - // var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - // var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + // var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + // var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; // MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(new Uri(websocketUri), MediaStreamingContent.Audio, // MediaStreamingAudioChannel.Unmixed, MediaStreamingTransport.Websocket, false); // TranscriptionOptions transcriptionOptions = new TranscriptionOptions(new Uri(websocketUri), TranscriptionTransport.Websocket, @@ -279,8 +279,8 @@ public ConnectController( // _logger.LogInformation($"Starting async one-to-N call connection to: {serverCallId}"); // ServerCallLocator serverCallLocator = new ServerCallLocator(serverCallId); - // var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - // var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + // var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + // var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; // MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(new Uri(websocketUri), MediaStreamingContent.Audio, // MediaStreamingAudioChannel.Unmixed, MediaStreamingTransport.Websocket, false); // TranscriptionOptions transcriptionOptions = new TranscriptionOptions(new Uri(websocketUri), TranscriptionTransport.Websocket, @@ -339,8 +339,8 @@ public ConnectController( // _logger.LogInformation($"Starting one-to-N call connection to: {serverCallId}"); // ServerCallLocator serverCallLocator = new ServerCallLocator(serverCallId); - // var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - // var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + // var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + // var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; // MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(new Uri(websocketUri), MediaStreamingContent.Audio, // MediaStreamingAudioChannel.Unmixed, MediaStreamingTransport.Websocket, false); // TranscriptionOptions transcriptionOptions = new TranscriptionOptions(new Uri(websocketUri), TranscriptionTransport.Websocket, diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/DTMFController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/DTMFController.cs index a6a49770..8e3b9e16 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/DTMFController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/DTMFController.cs @@ -8,7 +8,6 @@ using Call_Automation_GCCH.Models; using Call_Automation_GCCH.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,15 +20,13 @@ public class DTMFController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object public DTMFController( CallAutomationService service, - ILogger logger, IOptions configOptions) + ILogger logger) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); } diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs index 0720aeea..4722e922 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/MediaController.cs @@ -20,16 +20,16 @@ public class MediaController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; + private readonly ICommunicationConfigurationService _communicationConfigurationService; public MediaController( CallAutomationService service, ILogger logger, - IOptions configOptions) + ICommunicationConfigurationService communicationConfigurationService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } // ───────────── PLAY FILE SOURCE ──────────────────────────────────────────── @@ -41,14 +41,14 @@ public Task PlayFileSourceToTargetAsync( { if (string.IsNullOrEmpty(target)) return Task.FromResult(BadRequest("Target is required")); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandlePlayFileSource( callConnectionId, new List { identifier }, @@ -65,14 +65,14 @@ public IActionResult PlayFileSourceToTarget( { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandlePlayFileSource( callConnectionId, new List { identifier }, @@ -104,14 +104,14 @@ public IActionResult InterruptHoldWithPlay( { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandlePlayFileSource( callConnectionId, new List { identifier }, @@ -131,10 +131,10 @@ public Task CreateCallWithMediaStreamingAsync( { if (string.IsNullOrEmpty(target)) return Task.FromResult(BadRequest("Target is required")); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - + return HandleCreateCallWithMediaStreaming( target, MediaStreamingAudioChannel.Mixed, @@ -155,10 +155,10 @@ public IActionResult CreateCallWithMediaStreaming( { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - + return HandleCreateCallWithMediaStreaming( target, audioChannel, @@ -227,14 +227,14 @@ public Task RecognizeDTMFAsync(string callConnectionId, string ta { if (string.IsNullOrEmpty(target)) return Task.FromResult(BadRequest("Target is required")); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleRecognize(callConnectionId, identifier, RecognizeType.Dtmf, async: true); } @@ -244,14 +244,14 @@ public IActionResult RecognizeDTMF(string callConnectionId, string target) { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleRecognize(callConnectionId, identifier, RecognizeType.Dtmf, async: false).Result; } @@ -262,14 +262,14 @@ public Task HoldTargetAsync(string callConnectionId, string targe { if (string.IsNullOrEmpty(target)) return Task.FromResult(BadRequest("Target is required")); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleHold(callConnectionId, identifier, isPlaySource, unhold: false, async: true); } @@ -279,14 +279,14 @@ public IActionResult HoldTarget(string callConnectionId, string target, bool isP { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleHold(callConnectionId, identifier, isPlaySource, unhold: false, async: false).Result; } @@ -296,14 +296,14 @@ public Task UnholdTargetAsync(string callConnectionId, string tar { if (string.IsNullOrEmpty(target)) return Task.FromResult(BadRequest("Target is required")); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleHold(callConnectionId, identifier, playSource: false, unhold: true, async: true); } @@ -313,14 +313,14 @@ public IActionResult UnholdTarget(string callConnectionId, string target) { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleHold(callConnectionId, identifier, playSource: false, unhold: true, async: false).Result; } @@ -331,14 +331,14 @@ public Task InterruptAudioAndAnnounceAsync(string callConnectionI { if (string.IsNullOrEmpty(target)) return Task.FromResult(BadRequest("Target is required")); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleInterruptAudioAndAnnounce(callConnectionId, identifier, async: true); } @@ -348,14 +348,14 @@ public IActionResult InterruptAudioAndAnnounce(string callConnectionId, string t { if (string.IsNullOrEmpty(target)) return BadRequest("Target is required"); - + if (!target.StartsWith("8:") && !target.StartsWith("+")) return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - - CommunicationIdentifier identifier = target.StartsWith("8:") + + CommunicationIdentifier identifier = target.StartsWith("8:") ? new CommunicationUserIdentifier(target) : new PhoneNumberIdentifier(target); - + return HandleInterruptAudioAndAnnounce(callConnectionId, identifier, async: false).Result; } @@ -373,7 +373,7 @@ private async Task HandlePlayFileSource( { var callMedia = _service.GetCallMedia(callConnectionId); var props = _service.GetCallConnectionProperties(callConnectionId); - var fileSource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); + var fileSource = new FileSource(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost + "/audio/prompt.wav")); if (playToAll) { @@ -420,12 +420,12 @@ private async Task HandleCreateCallWithMediaStreaming( { bool isPstn = !target.StartsWith("8:"); string targetType = isPstn ? "PSTN" : "ACS"; - + _logger.LogInformation($"Creating call with media streaming. Target={target}, Type={targetType}, Channel={audioChannel}, EnableMedia={enableMediaStreaming}, Bidirectional={enableBidirectional}, PCM24kMono={pcm24kMono}, Async={async}"); try { - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - var websocketUri = _config.CallbackUriHost.Replace("https", "wss") + "/ws"; + var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; MediaStreamingOptions mediaOpts = new MediaStreamingOptions( new Uri(websocketUri), @@ -439,9 +439,9 @@ private async Task HandleCreateCallWithMediaStreaming( }; var invite = isPstn - ? new CallInvite(new PhoneNumberIdentifier(target), new PhoneNumberIdentifier(_config.AcsPhoneNumber)) + ? new CallInvite(new PhoneNumberIdentifier(target), new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.AcsPhoneNumber)) : new CallInvite(new CommunicationUserIdentifier(target)); - + var createOpts = new CreateCallOptions(invite, callbackUri) { MediaStreamingOptions = mediaOpts @@ -483,7 +483,7 @@ private async Task HandleMediaStreaming( var opts = new StartMediaStreamingOptions { OperationContext = "StartMediaStreamingContext", - OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") + OperationCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks") }; response = async ? await callMedia.StartMediaStreamingAsync(opts) : callMedia.StartMediaStreaming(opts); } @@ -499,7 +499,7 @@ private async Task HandleMediaStreaming( var opts = new StopMediaStreamingOptions { OperationContext = "StopMediaStreamingContext", - OperationCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks") + OperationCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks") }; response = async ? await callMedia.StopMediaStreamingAsync(opts) : callMedia.StopMediaStreaming(opts); } @@ -558,7 +558,7 @@ private async Task HandleRecognize( var callMedia = _service.GetCallMedia(callConnectionId); var props = _service.GetCallConnectionProperties(callConnectionId); var textSource = new TextSource("Please respond.") { VoiceName = "en-US-NancyNeural" }; - var fileSource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); + var fileSource = new FileSource(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost + "/audio/prompt.wav")); switch (type) { @@ -652,7 +652,7 @@ private async Task HandleHold( OperationContext = "holdUserContext" }; if (playSource) - opts.PlaySource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); + opts.PlaySource = new FileSource(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost + "/audio/prompt.wav")); if (async) await callMedia.HoldAsync(opts); else callMedia.Hold(opts); } @@ -676,7 +676,7 @@ private async Task HandleInterruptAudioAndAnnounce( { var callMedia = _service.GetCallMedia(callConnectionId); var props = _service.GetCallConnectionProperties(callConnectionId); - var fileSource = new FileSource(new Uri(_config.CallbackUriHost + "/audio/prompt.wav")); + var fileSource = new FileSource(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost + "/audio/prompt.wav")); var opts = new InterruptAudioAndAnnounceOptions(fileSource, target) { OperationContext = "interruptContext" diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs index 5de97627..da13c29f 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/ParticipantsController.cs @@ -18,15 +18,15 @@ public class ParticipantsController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object + private readonly ICommunicationConfigurationService _communicationConfigurationService; // final, bound object public ParticipantsController( CallAutomationService service, - ILogger logger, IOptions configOptions) + ILogger logger, ICommunicationConfigurationService communicationConfigurationService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } // ─ Add ─────────────────────────────────────────────────────────────────────── @@ -121,7 +121,7 @@ private async Task HandleAddParticipant( CallInvite invite = isPstn ? new CallInvite( new PhoneNumberIdentifier(participantId), - new PhoneNumberIdentifier(_config.AcsPhoneNumber)) + new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.AcsPhoneNumber)) : new CallInvite(new CommunicationUserIdentifier(participantId)); var options = new AddParticipantOptions(invite) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/RecordingsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/RecordingsController.cs index 2c2c4942..3188bc3c 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/RecordingsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/RecordingsController.cs @@ -18,16 +18,16 @@ public class RecordingsController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; + private readonly ICommunicationConfigurationService _communicationConfigurationService; public RecordingsController( CallAutomationService service, ILogger logger, - IOptions configOptions) + ICommunicationConfigurationService communicationConfigurationService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = configOptions.Value ?? throw new ArgumentNullException(nameof(configOptions)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } /// @@ -54,7 +54,7 @@ public async Task StartRecordingWithVideoMp4MixedAsync( recordingOptions.RecordingContent = RecordingContent.AudioVideo; recordingOptions.RecordingFormat = RecordingFormat.Mp4; recordingOptions.RecordingChannel = RecordingChannel.Mixed; - recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); recordingOptions.PauseOnStart = isPauseOnStart; CallAutomationService.SetRecordingFileFormat(RecordingFormat.Mp4.ToString()); @@ -103,7 +103,7 @@ public IActionResult StartRecordingWithVideoMp4Mixed( recordingOptions.RecordingContent = RecordingContent.AudioVideo; recordingOptions.RecordingFormat = RecordingFormat.Mp4; recordingOptions.RecordingChannel = RecordingChannel.Mixed; - recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); recordingOptions.PauseOnStart = isPauseOnStart; var recordingResult = _service.GetCallAutomationClient().GetCallRecording().Start(recordingOptions); @@ -151,7 +151,7 @@ public async Task StartRecordingWithAudioMp3MixedAsync( recordingOptions.RecordingContent = RecordingContent.Audio; recordingOptions.RecordingFormat = RecordingFormat.Mp3; recordingOptions.RecordingChannel = RecordingChannel.Mixed; - recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); recordingOptions.PauseOnStart = isPauseOnStart; var recordingResult = await _service.GetCallAutomationClient().GetCallRecording().StartAsync(recordingOptions); @@ -199,7 +199,7 @@ public IActionResult StartRecordingWithAudioMp3Mixed( recordingOptions.RecordingContent = RecordingContent.Audio; recordingOptions.RecordingFormat = RecordingFormat.Mp3; recordingOptions.RecordingChannel = RecordingChannel.Mixed; - recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); recordingOptions.PauseOnStart = isPauseOnStart; CallAutomationService.SetRecordingFileFormat(RecordingFormat.Mp3.ToString()); @@ -248,7 +248,7 @@ public async Task StartRecordingWithAudioWavUnmixedAsync( recordingOptions.RecordingContent = RecordingContent.Audio; recordingOptions.RecordingFormat = RecordingFormat.Wav; recordingOptions.RecordingChannel = RecordingChannel.Unmixed; - recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); recordingOptions.PauseOnStart = isPauseOnStart; CallAutomationService.SetRecordingFileFormat(RecordingFormat.Wav.ToString()); @@ -297,7 +297,7 @@ public IActionResult StartRecordingWithAudioWavUnmixed( recordingOptions.RecordingContent = RecordingContent.Audio; recordingOptions.RecordingFormat = RecordingFormat.Wav; recordingOptions.RecordingChannel = RecordingChannel.Unmixed; - recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); + recordingOptions.RecordingStateCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); recordingOptions.PauseOnStart = isPauseOnStart; CallAutomationService.SetRecordingFileFormat(RecordingFormat.Wav.ToString()); diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/SseController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/SseController.cs new file mode 100644 index 00000000..8c1a446a --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/SseController.cs @@ -0,0 +1,49 @@ +using System.Text; +using Call_Automation_GCCH.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; + +namespace Call_Automation_GCCH.Controllers +{ + [ApiController] + [Route("sse")] + public class SseController : ControllerBase + { + private readonly IServerSentEventsService _eventService; + + public SseController(IServerSentEventsService eventService) + { + _eventService = eventService; + } + + /// + /// Connect to the server-sent events stream. + /// This method sets the appropriate headers for SSE and listens for messages. + /// + [HttpGet("connect")] + [Tags("Developer")] + public async Task Connect(CancellationToken cancellationToken) + { + Response.Headers.Add(HeaderNames.ContentType, "text/event-stream"); + Response.Headers.Add(HeaderNames.CacheControl, "no-cache"); + Response.Headers.Add(HeaderNames.Connection, "keep-alive"); + + var responseStream = Response.Body; + var reader = _eventService.Reader; + + while (!HttpContext.RequestAborted.IsCancellationRequested) + { + while (reader.TryRead(out var message)) + { + var data = Encoding.UTF8.GetBytes(message); + + await responseStream.WriteAsync(data, cancellationToken); + await responseStream.FlushAsync(cancellationToken); + } + + await Task.Delay(1000); + } + + } + } +} diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Models/CommunicationConfiguration.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Models/CommunicationConfiguration.cs new file mode 100644 index 00000000..275bbced --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Models/CommunicationConfiguration.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Call_Automation_GCCH.Models +{ + public class CommunicationConfiguration + { + [JsonPropertyName("acsConnectionString")] + public string AcsConnectionString { get; set; } + [JsonPropertyName("acsPhoneNumber")] + public string AcsPhoneNumber { get; set; } + [JsonPropertyName("callbackUriHost")] + public string CallbackUriHost { get; set; } + [JsonPropertyName("pmaEndpoint")] + public string PmaEndpoint { get; set; } + // ACS GCCH Phase 2 + // [JsonPropertyName("cognitiveServiceEndpoint")] + // public string CongnitiveServiceEndpoint { get; set; } + } +} diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Models/ConfigurationRequest.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Models/ConfigurationRequest.cs deleted file mode 100644 index 41b05cbc..00000000 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Models/ConfigurationRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Call_Automation_GCCH.Models -{ - public class ConfigurationRequest - { - public string? AcsConnectionString { get; set; } // no default - public string? AcsPhoneNumber { get; set; } - public string? pmaEndpoint { get; set; } - // ACS GCCH Phase 2 - // public string CongnitiveServiceEndpoint { get; set; } - public string? CallbackUriHost { get; set; } - } -} diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs index 7cb7db04..6c7aa0de 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs @@ -1,36 +1,20 @@ -using Azure.Communication; -using Azure.Communication.CallAutomation; -using Azure.Messaging; -using Azure.Messaging.EventGrid; -using Azure.Messaging.EventGrid.SystemEvents; using Call_Automation_GCCH; -using Call_Automation_GCCH.Controllers; using Call_Automation_GCCH.Models; using Call_Automation_GCCH.Services; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; var builder = WebApplication.CreateBuilder(args); -var commSection = builder.Configuration.GetSection("CommunicationSettings"); -// This reads "CommunicationSettings" from appsettings.json -builder.Services.Configure(commSection); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// Add CallAutomationService as a singleton -builder.Services.AddSingleton(sp => { - string connectionString = commSection["AcsConnectionString"]; - bool isArizona = bool.Parse(commSection["IsArizona"] ?? "true"); - string pmaEndpoint = isArizona ? commSection["PmaEndpointArizona"] : commSection["PmaEndpointTexas"]; - - if (string.IsNullOrEmpty(pmaEndpoint)) { - sp.GetRequiredService>().LogWarning($"The {(isArizona ? "PmaEndpointArizona" : "PmaEndpointTexas")} setting is empty"); - } - - return new CallAutomationService(connectionString, pmaEndpoint, sp.GetRequiredService>()); -}); +builder.Services.Configure(builder.Configuration.GetSection(nameof(CommunicationConfiguration))); + +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); builder.Logging.ClearProviders(); builder.Logging.AddProvider(new ConsoleCollectorLoggerProvider()); @@ -46,10 +30,11 @@ // So the UI is served at /swagger c.RoutePrefix = "swagger"; - // Use the custom GCCHSwagger.html from wwwroot/swagger-ui + + // Use the custom gcch-swagger.html from wwwroot/swagger-ui c.IndexStream = () => { - var path = Path.Combine(builder.Environment.WebRootPath, "swagger-ui", "GCCHSwagger.html"); + var path = Path.Combine(builder.Environment.WebRootPath, "swagger-ui", "gcch-swagger.html"); return File.OpenRead(path); }; }); @@ -65,27 +50,27 @@ app.UseWebSockets(); app.Use(async (context, next) => { - // Get the logger instance from the DI container - var logger = context.RequestServices.GetRequiredService>(); + // Get the logger instance from the DI container + var logger = context.RequestServices.GetRequiredService>(); - if (context.Request.Path == "/ws") - { - logger.LogInformation($"Request received. Path: {context.Request.Path}"); - if (context.WebSockets.IsWebSocketRequest) + if (context.Request.Path == "/ws") { - logger.LogInformation("WebSocket request received."); - using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Helper.ProcessRequest(webSocket); + logger.LogInformation($"Request received. Path: {context.Request.Path}"); + if (context.WebSockets.IsWebSocketRequest) + { + logger.LogInformation("WebSocket request received."); + using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Helper.ProcessRequest(webSocket); + } + else + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } } else { - context.Response.StatusCode = StatusCodes.Status400BadRequest; + await next(context); } - } - else - { - await next(context); - } }); // Add custom WebSocket middleware diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Services/CommunicationConfigurationService.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Services/CommunicationConfigurationService.cs new file mode 100644 index 00000000..69282ca6 --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Services/CommunicationConfigurationService.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Text; +using Call_Automation_GCCH.Models; +using Microsoft.Extensions.Options; + +namespace Call_Automation_GCCH.Services +{ + public interface ICommunicationConfigurationService + { + CommunicationConfiguration communicationConfiguration { get; } + } + + public class CommunicationConfigurationService : ICommunicationConfigurationService + { + public CommunicationConfiguration communicationConfiguration { get; } + + public CommunicationConfigurationService(IHttpContextAccessor httpContextAccessor, IOptions defaultCommunicationConfiguration) + { + var context = httpContextAccessor.HttpContext; + if (context != null && context.Request.Headers.TryGetValue("X-Communication-Config", out var headerValue) && !string.IsNullOrWhiteSpace(headerValue)) + { + byte[] jsonBytes = Convert.FromBase64String(headerValue.ToString()); + string jsonString = Encoding.UTF8.GetString(jsonBytes); + var config = JsonSerializer.Deserialize(jsonString); + + if (config != null) + { + communicationConfiguration = new CommunicationConfiguration + { + AcsConnectionString = string.IsNullOrEmpty(config.AcsConnectionString) ? defaultCommunicationConfiguration.Value.AcsConnectionString : config.AcsConnectionString, + AcsPhoneNumber = string.IsNullOrEmpty(config.AcsPhoneNumber) ? defaultCommunicationConfiguration.Value.AcsPhoneNumber : config.AcsPhoneNumber, + CallbackUriHost = string.IsNullOrEmpty(config.CallbackUriHost) ? defaultCommunicationConfiguration.Value.CallbackUriHost : config.CallbackUriHost, + PmaEndpoint = string.IsNullOrEmpty(config.PmaEndpoint) ? defaultCommunicationConfiguration.Value.PmaEndpoint : config.PmaEndpoint, + // ACS GCCH Phase 2 + // CongnitiveServiceEndpoint = string.IsNullOrEmpty(config.CongnitiveServiceEndpoint) ? defaultCommunicationConfiguration.Value.CongnitiveServiceEndpoint: config.CongnitiveServiceEndpoint, + }; + + return; + } + + } + + communicationConfiguration = defaultCommunicationConfiguration.Value; + } + } +} \ No newline at end of file diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Services/ServerSentEventsService.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Services/ServerSentEventsService.cs new file mode 100644 index 00000000..a87b0dbe --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Services/ServerSentEventsService.cs @@ -0,0 +1,27 @@ +using System.Threading.Channels; + +namespace Call_Automation_GCCH.Services +{ + public interface IServerSentEventsService + { + ChannelReader Reader { get; } + void Publish(string payload); + } + + public class ServerSentEventsService : IServerSentEventsService + { + private readonly Channel _channel; + + public ServerSentEventsService() + { + _channel = Channel.CreateUnbounded(); + } + + public ChannelReader Reader => _channel.Reader; + + public void Publish(string payload) + { + _channel.Writer.TryWrite($"data: {payload}\n\n"); + } + } +} diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json b/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json index c9c4981f..f1fba0f0 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json +++ b/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json @@ -1,18 +1,15 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - - "CommunicationSettings": { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CommunicationConfiguration": { "AcsConnectionString": "", "AcsPhoneNumber": "", "CallbackUriHost": "", - "PmaEndpointArizona": "", - "PmaEndpointTexas": "", - "IsArizona": true + "PmaEndpoint": "" } } diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/GCCHSwagger.html b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/GCCHSwagger.html deleted file mode 100644 index 94c5e553..00000000 --- a/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/GCCHSwagger.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - GCCH Swagger UI - - - - - - -
- - - - - - - - diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.css b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.css new file mode 100644 index 00000000..e29a4f80 --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.css @@ -0,0 +1,147 @@ +.swagger-ui .topbar { + background-color: darkgreen; +} + +.swagger-ui .communication-config-title { + display: flex; + align-items: center; + margin-left: 10px; + color: #3b4151; + font-family: sans-serif; +} + +.swagger-ui .communication-config-title h3 { + font-size: 24px; +} + +.swagger-ui .communication-config-title span { + margin-left: 10px; + font-size: 18px; +} + +.swagger-ui .communication-config-container { + margin-bottom: 30px; + padding: 15px; + background-color: #80808018; + border-radius: 5px; + width: 100%; +} + +.swagger-ui .communication-config-parameter-name { + color: #3b4151; + font-family: sans-serif; + font-size: 16px; + font-weight: 400; +} + +.swagger-ui .communication-config-parameter-input { + margin: 10px; + padding: 5px; + width: 100%; +} + +.swagger-ui .input-error { + border: 2px solid red !important; +} + +.inbound-call-config-title { + display: flex; + align-items: center; + justify-content: space-between; + color: #3b4151; + font-family: sans-serif; + font-size: 22px; + padding: 0 16px; +} + +.inbound-call-config-container { + margin-bottom: 20px; + padding: 15px 20px; + background-color: #80808018; + border-radius: 5px; +} + +.inbound-call-config-parameter-name { + color: #3b4151; + font-family: sans-serif; + font-size: 16px; + font-weight: 400; +} + +.inbound-call-config-parameter-option { + margin: 10px; + padding: 5px; + width: 180px; +} + +.inbound-call-config-actions { + text-align: center; +} + +.inbound-call-config-accept-button { + background-color: #49cc90; + color: white; + border: none; + padding: 6px 24px; + border-radius: 4px; + cursor: pointer; + font-weight: 700; +} + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: #fff; + margin: 5% auto; + padding: 20px; + border-radius: 8px; + width: 375px; + height: 440px; + position: relative; + color: #3b4151; + font-family: sans-serif; + font-size: 16px; + font-weight: 400; +} + +.phone { + display: inline-block; + margin-right: 10px; + animation: ring 0.7s infinite linear; +} + +@keyframes ring { + 0% { + transform: rotate(0); + } + + 20% { + transform: rotate(10deg); + } + + 40% { + transform: rotate(-10deg); + } + + 60% { + transform: rotate(7deg); + } + + 80% { + transform: rotate(-7deg); + } + + 100% { + transform: rotate(0); + } +} diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.html b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.html new file mode 100644 index 00000000..76ccbb6e --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.html @@ -0,0 +1,105 @@ + + + + + GCCH Swagger UI + + + + + + + + +
+
+ + + + + + + + + + + + diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.js b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.js new file mode 100644 index 00000000..d9184f0e --- /dev/null +++ b/Call_Automation_GCCH/Call_Automation_GCCH/wwwroot/swagger-ui/gcch-swagger.js @@ -0,0 +1,360 @@ +const getLineBreak = () => document.createElement("br"); + +const getLabel = (textContent) => { + const label = document.createElement("label"); + label.textContent = textContent; + + return label; +}; + +const getOption = (value, textContent) => { + const option = document.createElement("option"); + option.value = value; + option.textContent = textContent; + + return option; +}; + +const getSelect = (id, options, ariaLabel) => { + const select = document.createElement("select"); + + options.forEach((option) => { + select.appendChild(getOption(option.value, option.textContent)); + }); + select.id = id; + select.ariaLabel = ariaLabel; + + return select; +}; + +const getButton = (textContent) => { + const button = document.createElement("button"); + button.textContent = textContent; + + return button; +}; + +const showLibraryVersion = () => { + fetch("/version") + .then((response) => response.json()) + .then((data) => { + const libraryVersion = document.createElement("h4"); + libraryVersion.textContent = `${data.library}: ${data.version}`; + + document.querySelector(".info").appendChild(libraryVersion); + }) + .catch((error) => { + console.error("Error fetching version:", error); + }); +}; + +const isUriValid = (uri) => { + try { + const url = new URL(uri); + + // Check if the URL has a valid scheme (http or https) + if (url.protocol !== "http:" && url.protocol !== "https:") { + return false; + } + + return true; + } catch (_) { + return false; + } +}; + +const communicationConfig = { + acsConnectionString: { + label: "ACS Connection String", + type: "password", + isValid: (acsConnectionString) => { + if (acsConnectionString === "") { + return true; + } + + return /^endpoint=https:\/\/[^;]+;accesskey=[^;]+$/.test( + acsConnectionString + ); + }, + }, + acsPhoneNumber: { + label: "ACS Phone Number", + type: "password", + isValid: (acsPhoneNumber) => { + if (acsPhoneNumber === "") { + return true; + } + + return /^\+\d{10,15}$/.test(acsPhoneNumber); + }, + }, + callbackUriHost: { + label: "Callback URI Host", + type: "password", + isValid: (callbackUriHost) => { + if (callbackUriHost === "") { + return true; + } + + return isUriValid(callbackUriHost); + }, + }, + pmaEndpoint: { + label: "PMA Endpoint", + type: "password", + isValid: (pmaEndpoint) => { + if (pmaEndpoint === "") { + return true; + } + + return isUriValid(pmaEndpoint); + }, + }, +}; + +const showCommunicationConfig = () => { + // Load saved config from local storage + const savedCommunicationConfig = JSON.parse( + localStorage.getItem("communicationConfig") + ) || { + acsConnectionString: "", + acsPhoneNumber: "", + callbackUriHost: "", + pmaEndpoint: "", + }; + + const communicationConfigTitle = document.createElement("div"); + communicationConfigTitle.classList.add("communication-config-title"); + + const communicationLabel = document.createElement("h3"); + communicationLabel.textContent = "Configuration"; + const communicationIcon = document.createElement("span"); + communicationIcon.title = + "Secrets will be stored locally in your browser and sent in HTTP headers over HTTPS. Use only on trusted devices. They may be visible to other users on the same device and accessible to browser extensions. Do not use in production environments."; + communicationIcon.textContent = "ⓘ"; + + communicationConfigTitle.appendChild(communicationLabel); + communicationConfigTitle.appendChild(communicationIcon); + + const communicationConfigContainer = document.createElement("div"); + communicationConfigContainer.classList.add("communication-config-container"); + + Object.keys(communicationConfig).forEach((key) => { + const label = getLabel(communicationConfig[key].label); + label.classList.add("communication-config-parameter-name"); + communicationConfigContainer.appendChild(label); + + communicationConfigContainer.appendChild(getLineBreak()); + + const input = document.createElement("input"); + input.id = key; + input.placeholder = "Default"; + input.type = communicationConfig[key].type; + + input.ariaLabel = communicationConfig[key].label; + + // Set saved value if exists + if (savedCommunicationConfig[key] != null) { + input.value = savedCommunicationConfig[key]; + } + + input.classList.add("communication-config-parameter-input"); + + // Save to local storage on change + input.addEventListener("change", () => { + // Validate the input value + if (communicationConfig[key].isValid(input.value)) { + savedCommunicationConfig[key] = input.value; + localStorage.setItem( + "communicationConfig", + JSON.stringify(savedCommunicationConfig) + ); + + input.classList.remove("input-error"); + } else { + savedCommunicationConfig[key] = ""; + localStorage.setItem( + "communicationConfig", + JSON.stringify(savedCommunicationConfig) + ); + + input.classList.add("input-error"); + } + }); + + communicationConfigContainer.appendChild(input); + + communicationConfigContainer.appendChild(getLineBreak()); + }); + + let elapsed = 0; + const maxWait = 10000; + const appendCommunicationConfig = setInterval(() => { + const topbar = document.querySelector(".information-container"); + if (topbar) { + topbar.appendChild(communicationConfigTitle); + topbar.appendChild(communicationConfigContainer); + + showLibraryVersion(); + + clearInterval(appendCommunicationConfig); + } + + elapsed += 300; + + if (elapsed >= maxWait) { + clearInterval(appendCommunicationConfig); + } + }, 300); +}; + +showCommunicationConfig(); + +const inboundCallConfig = { + audioChannelMixed: { + label: "Audio Channel Mixed", + type: "bool", + }, + audioFormat16k: { + label: "Audio Format 16k", + type: "bool", + }, + mediaStreaming: { + label: "Media Streaming", + type: "bool", + }, + bidirectionalStreaming: { + label: "Bidirectional Streaming", + type: "bool", + }, +}; + +const modal = document.querySelector("#modal"); +const modalContent = document.querySelector("#modal-content"); +window.addEventListener("click", (event) => { + if (event.target === modal) { + // Clear modal content + modalContent.innerHTML = ""; + + modal.style.display = "none"; + } +}); + +const showInboundCallConfig = (eventData) => { + // Load saved config from local storage + const savedInboundCallConfig = JSON.parse( + localStorage.getItem("inboundCallConfig") + ) || { + audioChannelMixed: true, + audioFormat16k: true, + mediaStreaming: true, + bidirectionalStreaming: true, + }; + + const inboundCallConfigTitle = document.createElement("div"); + inboundCallConfigTitle.classList.add("inbound-call-config-title"); + + const inboundCallLabel = document.createElement("h3"); + inboundCallLabel.textContent = "Incoming Call"; + const inboundCallIcon = document.createElement("h3"); + inboundCallIcon.classList.add("phone"); + inboundCallIcon.textContent = "📞"; + + inboundCallConfigTitle.appendChild(inboundCallLabel); + inboundCallConfigTitle.appendChild(inboundCallIcon); + + const inboundCallConfigContainer = document.createElement("div"); + inboundCallConfigContainer.classList.add("inbound-call-config-container"); + + Object.keys(inboundCallConfig).forEach((key) => { + const label = getLabel(inboundCallConfig[key].label); + label.classList.add("inbound-call-config-parameter-name"); + inboundCallConfigContainer.appendChild(label); + + inboundCallConfigContainer.appendChild(getLineBreak()); + + const select = getSelect( + key, + [ + { value: "true", textContent: "true" }, + { value: "false", textContent: "false" }, + ], + inboundCallConfig[key].label + ); + + // Set saved value if exists + if (savedInboundCallConfig[key] != null) { + select.value = savedInboundCallConfig[key].toString(); + } + + select.classList.add("inbound-call-config-parameter-option"); + + // Save to local storage on change + select.addEventListener("change", () => { + // Parse the value to boolean + savedInboundCallConfig[key] = select.value === "true"; + localStorage.setItem( + "inboundCallConfig", + JSON.stringify(savedInboundCallConfig) + ); + }); + + inboundCallConfigContainer.appendChild(select); + + inboundCallConfigContainer.appendChild(getLineBreak()); + }); + + const acceptButton = getButton("ACCEPT"); + acceptButton.classList.add("inbound-call-config-accept-button"); + + acceptButton.addEventListener("click", () => { + const { + audioChannelMixed, + audioFormat16k, + mediaStreaming, + bidirectionalStreaming, + } = savedInboundCallConfig; + + // Call API to accept the call + const acceptCallUrl = `/api/events/incomingcall?audioChannelMixed=${audioChannelMixed}&audioFormat16k=${audioFormat16k}&mediaStreaming=${mediaStreaming}&bidirectionalStreaming=${bidirectionalStreaming}`; + + fetch(acceptCallUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: eventData, + }); + + modal.style.display = "none"; + }); + + const inboundCallConfigActions = document.createElement("div"); + inboundCallConfigActions.classList.add("inbound-call-config-actions"); + + inboundCallConfigActions.appendChild(acceptButton); + + modalContent.appendChild(inboundCallConfigTitle); + modalContent.appendChild(inboundCallConfigContainer); + modalContent.appendChild(inboundCallConfigActions); + + modal.style.display = "block"; +}; + +const evtSource = new EventSource("/sse/connect"); +evtSource.onmessage = (event) => { + const eventData = event.data; + + console.log("Incoming call event received:", JSON.parse(eventData)); + + showInboundCallConfig(eventData); + + setTimeout(() => { + modal.style.display = "none"; + }, 45000); +}; + +evtSource.onerror = (err) => { + console.error("SSE error:", err); + evtSource.close(); +}; From 22d5fb4b5872bdab432a8ef86b7744f14781097f Mon Sep 17 00:00:00 2001 From: Nikhil Malviya Date: Fri, 23 May 2025 15:14:59 +0530 Subject: [PATCH 8/8] Add raiseincomingcall and version endpoint to CallAutomationEventsController --- .../CallAutomationEventsController.cs | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs index e389c706..cd23d3cf 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallAutomationEventsController.cs @@ -1,4 +1,6 @@ using System; +using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; using Azure.Communication; using Azure.Communication.CallAutomation; @@ -22,15 +24,17 @@ public class CallAutomationEventsController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ICommunicationConfigurationService _communicationConfigurationService; // final, bound object + private readonly ICommunicationConfigurationService _communicationConfigurationService; + private readonly IServerSentEventsService _serverSentEventsService; public CallAutomationEventsController( CallAutomationService service, - ILogger logger, ICommunicationConfigurationService communicationConfigurationService) + ILogger logger, ICommunicationConfigurationService communicationConfigurationService, IServerSentEventsService serverSentEventsService) { _service = service ?? throw new ArgumentNullException(nameof(service)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); + _serverSentEventsService = serverSentEventsService ?? throw new ArgumentNullException(nameof(serverSentEventsService)); } /// @@ -38,11 +42,27 @@ public CallAutomationEventsController( /// /// All collected logs [HttpGet("/logs")] + [Tags("Developer")] public IActionResult GetLogs() { return Ok(LogCollector.GetAll()); } + /// + /// Returns the version of the Call Automation library + /// + /// Version information + [HttpGet("/version")] + [Tags("Developer")] + public IActionResult GetCallAutomationLibraryVersion() + { + Assembly assembly = typeof(CallAutomationClient).Assembly; + var version = assembly.GetCustomAttribute(); + + return Ok(new { Library = "Azure.Communication.CallAutomation", Version = version?.InformationalVersion ?? "Unknown" }); + } + + /// /// Handles EventGrid events for incoming calls and recording status updates /// @@ -166,13 +186,14 @@ public IActionResult HandleCallbacks([FromBody] CloudEvent[] cloudEvents) return Problem($"Error processing callbacks: {ex.Message}"); } } + /// /// Updates the IsArizona configuration and switches PMA endpoint accordingly /// /// Boolean flag to determine which PMA endpoint to use /// Action result indicating success or error - [HttpPost("/setRegion")] - [Tags("Region Configuration")] + //[HttpPost("/setRegion")] + //[Tags("Region Configuration")] // public IActionResult SetRegion(bool isArizona) // { // try @@ -446,6 +467,47 @@ private void ProcessCallEvent(CallAutomationEventBase parsedEvent) #region Incoming Call with Media Streaming + /// + /// Raises an incoming call event + /// + [HttpPost("events/raiseincomingcall")] + [Tags("Incoming Call with Media Streaming Options")] + public IActionResult HandleRaiseIncomingCall([FromBody] EventGridEvent[] eventGridEvents) + { + _logger.LogInformation($"Received {eventGridEvents.Length} event(s) in /api/events/raiseincomingcall"); + + foreach (var eventGridEvent in eventGridEvents) + { + try + { + _logger.LogInformation($"Processing event: {eventGridEvent.EventType}, Id: {eventGridEvent.Id}"); + + if (eventGridEvent.TryGetSystemEventData(out object eventData) && eventData is SubscriptionValidationEventData subscriptionValidationEventData) + { + _logger.LogInformation($"Subscription validation event received with code: {subscriptionValidationEventData.ValidationCode}"); + + var responseData = new SubscriptionValidationResponse + { + ValidationResponse = subscriptionValidationEventData.ValidationCode + }; + + return Ok(responseData); + } + } + catch (Exception eventEx) + { + _logger.LogError($"Error processing event: {eventEx.Message}"); + } + + } + + _logger.LogInformation($"Publishing event to server-sent events service"); + + _serverSentEventsService.Publish(JsonSerializer.Serialize(eventGridEvents)); + + return Ok(); + } + /// /// Handles incoming calls with media streaming using query parameters to configure options /// @@ -471,7 +533,8 @@ public async Task HandleIncomingCallWithOptions( [FromQuery] bool audioChannelMixed = true, [FromQuery] bool audioFormat16k = true, [FromQuery] bool mediaStreaming = true, - [FromQuery] bool bidirectionalStreaming = true) + [FromQuery] bool bidirectionalStreaming = true + ) { MediaStreamingAudioChannel audioChannel = audioChannelMixed ? MediaStreamingAudioChannel.Mixed