diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs b/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs index fccf45c4..893bd55b 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/CallAutomationService.cs @@ -4,134 +4,174 @@ namespace Call_Automation_GCCH.Services { - public class CallAutomationService - { - private readonly CallAutomationClient _client; - private readonly ILogger _logger; - private static string? _recordingLocation; - private static string _recordingFileFormat = "mp4"; - - public CallAutomationService(string connectionString, string pmaEndpoint, ILogger logger) + public class CallAutomationService { - _client = new CallAutomationClient(pmaEndpoint: new Uri(pmaEndpoint), connectionString: connectionString); - // _client = new CallAutomationClient(connectionString: connectionString); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// 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 - } - } + 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 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; - } } - - //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 b616a636..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 ConfigurationRequest _config; // final, bound object + private readonly ICommunicationConfigurationService _communicationConfigurationService; + private readonly IServerSentEventsService _serverSentEventsService; public CallAutomationEventsController( CallAutomationService service, - ILogger logger, IOptions configOptions) + ILogger logger, ICommunicationConfigurationService communicationConfigurationService, IServerSentEventsService serverSentEventsService) { _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)); + _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 /// @@ -79,7 +99,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) @@ -167,6 +187,69 @@ public IActionResult HandleCallbacks([FromBody] CloudEvent[] cloudEvents) } } + /// + /// 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,6 +464,214 @@ private void ProcessCallEvent(CallAutomationEventBase parsedEvent) _logger.LogInformation($"Recording State: {recordingStateChanged.State}"); } } + + #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 + /// + /// + /// ## 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(_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}"); + + 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 } } diff --git a/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Controllers/CallController.cs index b2628d03..60f048ef 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,181 +20,307 @@ public class CallController : ControllerBase { private readonly CallAutomationService _service; private readonly ILogger _logger; - private readonly ConfigurationRequest _config; // final, bound object + private readonly ICommunicationConfigurationService _communicationConfigurationService; public CallController( - CallAutomationService service, - ILogger logger, IOptions configOptions) + CallAutomationService service, + 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)); } - /// - /// 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 CreateCall( + string target, + bool isPstn = false) + => HandleCreateCall(target, isPstn, async: false).Result; + + [HttpPost("createCallAsync")] [Tags("Outbound Call APIs")] - public IActionResult CreateOutboundCallToAcs(string acsTarget) + public Task CreateCallAsync( + string target, + bool isPstn = false) + => HandleCreateCall(target, isPstn, async: true); + + // + // TRANSFER CALL + // + + [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; + + [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"); + + var idType = isPstn ? "PSTN" : "ACS"; + _logger.LogInformation($"Starting {(async ? "async " : "")}create {idType} call to {target}"); + try { - if (string.IsNullOrEmpty(acsTarget)) - { - return BadRequest("ACS Target ID is required"); - } + // Build identifier & invite + var callbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + CallInvite invite = isPstn + ? new CallInvite(new PhoneNumberIdentifier(target), + new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.AcsPhoneNumber)) + : new CallInvite(new CommunicationUserIdentifier(target)); - _logger.LogInformation($"Starting outbound call to ACS user: {acsTarget}"); + var options = new CreateCallOptions(invite, callbackUri); - 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"); + // Call SDK + CreateCallResult result = async + ? await _service.GetCallAutomationClient().CreateCallAsync(options) + : _service.GetCallAutomationClient().CreateCall(options); - CreateCallResult createCallResult = _service.GetCallAutomationClient().CreateCall(createCallOptions); + var props = result.CallConnectionProperties; + _logger.LogInformation( + $"Created {idType} call: ConnId={props.CallConnectionId}, CorrId={props.CorrelationId}, Status={props.CallConnectionState}"); - 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}"); - - 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}"); } } - /// - /// 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) + 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 { - if (string.IsNullOrEmpty(acsTarget)) + var connection = _service.GetCallConnection(callConnectionId); + var correlationId = _service.GetCallConnectionProperties(callConnectionId).CorrelationId; + + TransferToParticipantOptions options; + if (isPstn) { - return BadRequest("ACS Target ID is required"); + // PSTN → PSTN + options = new TransferToParticipantOptions(new PhoneNumberIdentifier(transferTarget)) + { + OperationContext = "TransferCallContext", + Transferee = new PhoneNumberIdentifier(transferee) + }; } - - _logger.LogInformation($"Starting async outbound call to ACS user: {acsTarget}"); - - var callbackUri = new Uri(new Uri(_config.CallbackUriHost), "/api/callbacks"); - Uri _eventCallbackUri = callbackUri; - - _logger.LogInformation($"Created async ACS call with Callback Uri: {callbackUri}"); - var callInvite = new CallInvite(new CommunicationUserIdentifier(acsTarget)); - var createCallOptions = new CreateCallOptions(callInvite, callbackUri) + else { - // ACS GCCH Phase 2 - // CallIntelligenceOptions = new CallIntelligenceOptions() { CognitiveServicesEndpoint = new Uri(cognitiveServicesEndpoint) }, - }; - _logger.LogInformation("Initiating CreateCallAsync operation"); + // ACS → ACS + options = new TransferToParticipantOptions(new CommunicationUserIdentifier(transferTarget)) + { + OperationContext = "TransferCallContext", + Transferee = new CommunicationUserIdentifier(transferee) + }; + } - CreateCallResult createCallResult = await _service.GetCallAutomationClient().CreateCallAsync(createCallOptions); + // Call SDK + Response resp = async + ? await connection.TransferCallToParticipantAsync(options) + : connection.TransferCallToParticipant(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}"); + _logger.LogInformation( + $"Transfer complete. CallConnId={callConnectionId}, CorrId={correlationId}, Status={resp.GetRawResponse().Status}"); - return Ok(new CallConnectionResponse - { - CallConnectionId = connectionId, + return Ok(new CallConnectionResponse + { + CallConnectionId = callConnectionId, CorrelationId = correlationId, - Status = callStatus + Status = resp.GetRawResponse().Status.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 transferring {idType} call"); + return Problem($"Failed to transfer {idType} 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) + + 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 { + var connection = _service.GetCallConnection(callConnectionId); 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 - { + + var resp = async + ? await connection.HangUpAsync(isForEveryone) + : connection.HangUp(isForEveryone); + + _logger.LogInformation( + $"Hangup complete. ConnId={callConnectionId}, CorrId={correlationId}, Status={resp.Status}"); + + 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(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + var sourceCallerId = new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.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}"); } } } @@ -200,6 +328,7 @@ public IActionResult Hangup(string callConnectionId, bool isForEveryOne) + #region Outbound Call to PSTN /********************************************************************************************** app.MapPost("/outboundCallToPstnAsync", async (string targetPhoneNumber, ILogger logger) => @@ -496,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/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 106840e0..4722e922 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 ICommunicationConfigurationService _communicationConfigurationService; + + public MediaController( + CallAutomationService service, + ILogger logger, + ICommunicationConfigurationService communicationConfigurationService) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _communicationConfigurationService = communicationConfigurationService ?? throw new ArgumentNullException(nameof(communicationConfigurationService)); } - 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) + [HttpPost("/playFileSourceToTarget")] + [Tags("Play FileSource Media")] + public IActionResult PlayFileSourceToTarget( + string callConnectionId, + string target) { - OperationContext = "playToContext" - }; - - var playResponse = await callMedia.PlayAsync(playToOptions); - var operationStatus = playResponse.GetRawResponse().ToString(); + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); - string successMessage = $"File source played successfully to ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}, OperationStatus: {operationStatus}"; - _logger.LogInformation(successMessage); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - 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}"); - } - } + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - /// - /// 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"); + 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 + [HttpPost("/recognizeDTMF")] + [Tags("Recognition")] + public IActionResult RecognizeDTMF(string callConnectionId, string target) { - 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"; + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); - _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 - }; + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - 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 - }; + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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 + return HandleRecognize(callConnectionId, identifier, RecognizeType.Dtmf, async: false).Result; + } - [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 + // ──────────── HOLD / UNHOLD ───────────────────────────────────────────────── + [HttpPost("/holdTargetAsync")] + [Tags("Hold Management")] + public Task HoldTargetAsync(string callConnectionId, string target, bool isPlaySource) { - 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; + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); - // Cancel all media operations - var response = _service.GetCallMedia(callConnectionId).CancelAllMediaOperations(); - string operationStatus = response.GetRawResponse().Status.ToString(); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - string successMessage = $"[cancelAllMediaOperation] All media operations cancelled. " + - $"CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, Status: {operationStatus}"; - _logger.LogInformation(successMessage); + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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"); + return HandleHold(callConnectionId, identifier, isPlaySource, unhold: false, async: true); } - if (string.IsNullOrEmpty(acsTarget)) + [HttpPost("/holdTarget")] + [Tags("Hold Management")] + public IActionResult HoldTarget(string callConnectionId, string target, bool isPlaySource) { - return BadRequest("ACS Target ID is required"); - } + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); - _logger.LogInformation($"Starting DTMF recognition with ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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"); + return HandleHold(callConnectionId, identifier, isPlaySource, unhold: false, async: false).Result; } - if (string.IsNullOrEmpty(acsTarget)) + [HttpPost("/unholdTargetAsync")] + [Tags("Hold Management")] + public Task UnholdTargetAsync(string callConnectionId, string target) { - return BadRequest("ACS Target ID is required"); - } - - _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" - //}; + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); - var _fileSourceUri = _config.CallbackUriHost + "/audio/prompt.wav"; - FileSource fileSource = new FileSource(new Uri(_fileSourceUri)); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - 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); + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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"); + return HandleHold(callConnectionId, identifier, playSource: false, unhold: true, async: true); } - if (string.IsNullOrEmpty(acsTarget)) + [HttpPost("/unholdTarget")] + [Tags("Hold Management")] + public IActionResult UnholdTarget(string callConnectionId, string target) { - return BadRequest("ACS Target ID is required"); - } + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); - _logger.LogInformation($"Hold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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); + return HandleHold(callConnectionId, identifier, playSource: false, unhold: true, async: false).Result; } - else + + // ────────── INTERRUPT AUDIO AND ANNOUNCE ─────────────────────────────────── + [HttpPost("/interruptAudioAndAnnounceAsync")] + [Tags("Audio Announcements")] + public Task InterruptAudioAndAnnounceAsync(string callConnectionId, string target) { - HoldOptions holdOptions = new HoldOptions(target) - { - OperationContext = "holdUserContext" - }; - await callMedia.HoldAsync(holdOptions); - } + if (string.IsNullOrEmpty(target)) + return Task.FromResult(BadRequest("Target is required")); - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return Task.FromResult(BadRequest("PSTN number must include country code (e.g., +1 for US)")); - string successMessage = $"Hold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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"); + return HandleInterruptAudioAndAnnounce(callConnectionId, identifier, async: true); } - if (string.IsNullOrEmpty(acsTarget)) + [HttpPost("/interruptAudioAndAnnounce")] + [Tags("Audio Announcements")] + public IActionResult InterruptAudioAndAnnounce(string callConnectionId, string target) { - return BadRequest("ACS Target ID is required"); - } - - _logger.LogInformation($"Hold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); + if (string.IsNullOrEmpty(target)) + return BadRequest("Target is required"); - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); + if (!target.StartsWith("8:") && !target.StartsWith("+")) + return BadRequest("PSTN number must include country code (e.g., +1 for US)"); - CallMedia callMedia = _service.GetCallMedia(callConnectionId); + CommunicationIdentifier identifier = target.StartsWith("8:") + ? new CommunicationUserIdentifier(target) + : new PhoneNumberIdentifier(target); - 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); + return HandleInterruptAudioAndAnnounce(callConnectionId, identifier, async: false).Result; } - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + // ───────────── PRIVATE HANDLERS ────────────────────────────────────────── - string successMessage = $"Hold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); - - return Ok(new CallConnectionResponse + private async Task HandlePlayFileSource( + string callConnectionId, + List targets, + bool playToAll, + bool bargeIn, + bool async) { - 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}"); - } - } + _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(_communicationConfigurationService.communicationConfiguration.CallbackUriHost + "/audio/prompt.wav")); - /// - /// 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"); + 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}"); + } } - if (string.IsNullOrEmpty(acsTarget)) + private async Task HandleCreateCallWithMediaStreaming( + string target, + MediaStreamingAudioChannel audioChannel, + bool enableMediaStreaming, + bool enableBidirectional, + bool pcm24kMono, + bool async) { - return BadRequest("ACS Target ID is required"); - } - - _logger.LogInformation($"Unhold ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); + bool isPstn = !target.StartsWith("8:"); + string targetType = isPstn ? "PSTN" : "ACS"; - CallMedia callMedia = _service.GetCallMedia(callConnectionId); - - UnholdOptions unholdOptions = new UnholdOptions(target) - { - OperationContext = "unholdUserContext" - }; - await callMedia.UnholdAsync(unholdOptions); + _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(_communicationConfigurationService.communicationConfiguration.CallbackUriHost), "/api/callbacks"); + var websocketUri = _communicationConfigurationService.communicationConfiguration.CallbackUriHost.Replace("https", "wss") + "/ws"; - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + MediaStreamingOptions mediaOpts = new MediaStreamingOptions( + new Uri(websocketUri), + MediaStreamingContent.Audio, + audioChannel, + MediaStreamingTransport.Websocket, + enableMediaStreaming) + { + EnableBidirectional = enableBidirectional, + AudioFormat = pcm24kMono ? AudioFormat.Pcm24KMono : AudioFormat.Pcm16KMono + }; - string successMessage = $"Unhold successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); + var invite = isPstn + ? new CallInvite(new PhoneNumberIdentifier(target), new PhoneNumberIdentifier(_communicationConfigurationService.communicationConfiguration.AcsPhoneNumber)) + : new CallInvite(new CommunicationUserIdentifier(target)); - 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}"); - } - } + var createOpts = new CreateCallOptions(invite, callbackUri) + { + MediaStreamingOptions = mediaOpts + }; - /// - /// 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"); - } + 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" - }; - 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); - - 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; - /// - /// 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"); + if (start) + { + if (withOptions) + { + var opts = new StartMediaStreamingOptions + { + OperationContext = "StartMediaStreamingContext", + OperationCallbackUri = new Uri(new Uri(_communicationConfigurationService.communicationConfiguration.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(_communicationConfigurationService.communicationConfiguration.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($"Interrupt audio and announce to ACS target. CallConnectionId: {callConnectionId}, Target: {acsTarget}"); - - CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); - - CallMedia callMedia = _service.GetCallMedia(callConnectionId); + private enum RecognizeType { Dtmf, Choice, Speech, SpeechOrDtmf } - //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 HandleRecognize( + string callConnectionId, + CommunicationIdentifier target, + RecognizeType type, + bool async) { - 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}"); - } - } - - /// - /// 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)) - { - 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(_communicationConfigurationService.communicationConfiguration.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" - }; - - 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}"); - } - } + private IEnumerable GetChoices() => + new List + { + new RecognitionChoice("yes", new[] { "yes", "yeah" }), + new RecognitionChoice("no", new[] { "no", "nope" }) + }; - /// - /// 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)) + 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(_communicationConfigurationService.communicationConfiguration.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 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) + private async Task HandleInterruptAudioAndAnnounce( + string callConnectionId, + CommunicationIdentifier target, + bool async) { - OperationContext = "playToContext", - InterruptHoldAudio = true - }; - - callMedia.Play(playToOptions); - - var correlationId = (_service.GetCallConnectionProperties(callConnectionId)).CorrelationId; - var callStatus = (_service.GetCallConnectionProperties(callConnectionId)).CallConnectionState.ToString(); + _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(_communicationConfigurationService.communicationConfiguration.CallbackUriHost + "/audio/prompt.wav")); + var opts = new InterruptAudioAndAnnounceOptions(fileSource, target) + { + OperationContext = "interruptContext" + }; - string successMessage = $"Interrupt hold with play successfully on ACS target. CallConnectionId: {callConnectionId}, CorrelationId: {correlationId}, CallStatus: {callStatus}"; - _logger.LogInformation(successMessage); + if (async) await callMedia.InterruptAudioAndAnnounceAsync(opts); else callMedia.InterruptAudioAndAnnounce(opts); - 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}"); - } + _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}"); + } + } } - - #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..da13c29f 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; @@ -18,1101 +18,273 @@ 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)); } - /// - /// 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(_communicationConfigurationService.communicationConfiguration.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 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/Middleware/Helper.cs b/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs index 11a54306..aaae5587 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Middleware/Helper.cs @@ -45,6 +45,16 @@ public static async Task ProcessRequest(WebSocket webSocket) } LogCollector.Log("***************************************************************************************"); + // Send audio bytes back to client if bidirectional streaming is enabled + if (audioData.Data is byte[] audioBytes) + { + LogCollector.Log("Bidirectional Logs are given below:"); + await webSocket.SendAsync( + new ArraySegment(audioBytes, 0, audioBytes.Length), + WebSocketMessageType.Binary, + endOfMessage: true, + cancellationToken: CancellationToken.None); + } } if (response is TranscriptionMetadata transcriptionMetadata) @@ -75,12 +85,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); 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 9b9f128f..6c7aa0de 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs +++ b/Call_Automation_GCCH/Call_Automation_GCCH/Program.cs @@ -1,30 +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"]; - string pmaEndpoint = commSection["PmaEndpoint"]; - 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()); @@ -40,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); }; }); @@ -59,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 e7b9186e..f1fba0f0 100644 --- a/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json +++ b/Call_Automation_GCCH/Call_Automation_GCCH/appsettings.json @@ -1,13 +1,12 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - - "CommunicationSettings": { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CommunicationConfiguration": { "AcsConnectionString": "", "AcsPhoneNumber": "", "CallbackUriHost": "", 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(); +};