diff --git a/Call_Automation_GCCH/README.md b/Call_Automation_GCCH/README.md new file mode 100644 index 0000000..0452404 --- /dev/null +++ b/Call_Automation_GCCH/README.md @@ -0,0 +1,59 @@ +|page_type| languages |products +|---|---------------------------------------|---| +|sample|
Java
|
azureazure-communication-services
| + +# Call Automation - Quick Start Sample + +# Design + +![design](./static/OutboundCallDesign.png) + +## Prerequisites + +- An Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/?WT.mc_id=A261C142F). +- A deployed Communication Services resource. [Create a Communication Services resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). +- A [phone number](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number) in your Azure Communication Services resource that can make outbound calls. NB: phone numbers are not available in free subscriptions. +- Create Azure AI Multi Service resource. For details, see [Create an Azure AI Multi service](https://learn.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account). +- [Java Development Kit (JDK) Microsoft.OpenJDK.17](https://learn.microsoft.com/en-us/java/openjdk/download) +- [Apache Maven](https://maven.apache.org/download.cgi) +- Create and host a Azure Dev Tunnel. Instructions [here](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started) +- (Optional) A Microsoft Teams user with a phone license that is `voice` enabled. Teams phone license is required to add Teams users to the call. Learn more about Teams licenses [here](https://www.microsoft.com/microsoft-teams/compare-microsoft-teams-bundle-options). Learn about enabling phone system with `voice` [here](https://learn.microsoft.com/microsoftteams/setting-up-your-phone-system). You also need to complete the prerequisite step [Authorization for your Azure Communication Services Resource](https://learn.microsoft.com/azure/communication-services/how-tos/call-automation/teams-interop-call-automation?pivots=programming-language-javascript#step-1-authorization-for-your-azure-communication-services-resource-to-enable-calling-to-microsoft-teams-users) to enable calling to Microsoft Teams users. + +## Before running the sample for the first time + +- Open the application.yml file in the resources folder to configure the following settings + + - `connectionstring`: Azure Communication Service resource's connection string. + - `callerphonenumber`: Phone number associated with the Azure Communication Service resource. + - `targetphonenumber`: Target Phone number. + + Format: "OutboundTarget(Phone Number)". + + For e.g. "+1425XXXAAAA" + - `basecallbackuri`: Base url of the app. For local development use dev tunnel url. + - `cognitiveServiceEndpoint`: Cognitive Service Endpoint. + - `targetTeamsUserId`: (Optional) update field with the Microsoft Teams user Id you would like to add to the call. See [Use Graph API to get Teams user Id](../../../how-tos/call-automation/teams-interop-call-automation.md#step-2-use-the-graph-api-to-get-microsoft-entra-object-id-for-teams-users-and-optionally-check-their-presence). Uncomment the below snippet in ProgramSample.java to enable Teams Interop scenario. + ``` + client.getCallConnection(callConnectionId).addParticipant( + new CallInvite(new MicrosoftTeamsUserIdentifier(appConfig.getTargetTeamsUserId())) + .setSourceDisplayName("Jack (Contoso Tech Support)")); + ``` + +### Setup and host your Azure DevTunnel + +[Azure DevTunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) is an Azure service that enables you to share local web services hosted on the internet. Use the commands below to connect your local development environment to the public internet. This creates a tunnel with a persistent endpoint URL and which allows anonymous access. We will then use this endpoint to notify your application of calling events from the ACS Call Automation service. + +```bash +devtunnel create --allow-anonymous +devtunnel port create -p 8080 +devtunnel host +``` + +### Run the application + +- Navigate to the directory containing the pom.xml file and use the following mvn commands: + - Compile the application: mvn compile + - Build the package: mvn package + - Execute the app: mvn exec:java +- Access the Swagger UI at http://localhost:8080/swagger-ui.html + - Try the GET /outboundCall to run the Sample Application diff --git a/Call_Automation_GCCH/pom.xml b/Call_Automation_GCCH/pom.xml new file mode 100644 index 0000000..a28d028 --- /dev/null +++ b/Call_Automation_GCCH/pom.xml @@ -0,0 +1,177 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.6 + + + + com.communication.callautomation + Call_Automation_GCCH_Test_App + 1.0-SNAPSHOT + + Call_Automation_GCCH_Test_App + CallAutomation Sample application for instructional usage + + + 17 + 17 + UTF-8 + 1.18.26 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + + + junit + junit + 4.13.2 + test + + + com.azure + azure-core + 1.39.0 + + + com.azure + azure-identity + 1.9.0 + + + com.azure + azure-communication-identity + 1.4.5 + + + com.azure + azure-communication-common + + + + + com.azure + azure-communication-callautomation + 1.4.0-alpha.20250408.1 + + + com.azure + azure-messaging-eventgrid + 4.16.0 + + + com.azure + azure-communication-common + 2.0.0-beta.1 + + + org.projectlombok + lombok + provided + ${lombok.version} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.0.0 + + + + + azure-sdk-for-java + https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-java/maven/v1 + + true + + + true + + + + + + + + maven-clean-plugin + 3.2.0 + + + maven-resources-plugin + 3.3.1 + + + maven-compiler-plugin + 3.11.0 + + + maven-surefire-plugin + 3.1.0 + + + maven-jar-plugin + 3.3.0 + + + maven-deploy-plugin + 3.1.1 + + + maven-site-plugin + 3.12.1 + + + maven-project-info-reports-plugin + 3.4.3 + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + + java + + + + + com.communication.callautomation.Main + + + + + + \ No newline at end of file diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ConfigurationRequest.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ConfigurationRequest.java new file mode 100644 index 0000000..d5bfdc9 --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ConfigurationRequest.java @@ -0,0 +1,66 @@ +package com.communication.callautomation; + +import java.util.Map; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@ConfigurationProperties(prefix = "acs") +@Getter +public class ConfigurationRequest { + private String acsConnectionString; + private String cognitiveServiceEndpoint; + private String acsPhoneNumber; + private String callbackUriHost; + private Map botRouting; + private String defaultBotId; + + // Getters and Setters + public String getAcsConnectionString() { + return acsConnectionString; + } + + public void setAcsConnectionString(String acsConnectionString) { + this.acsConnectionString = acsConnectionString; + } + + public String getCognitiveServiceEndpoint() { + return cognitiveServiceEndpoint; + } + + public void setCognitiveServiceEndpoint(String cognitiveServiceEndpoint) { + this.cognitiveServiceEndpoint = cognitiveServiceEndpoint; + } + + public String getAcsPhoneNumber() { + return acsPhoneNumber; + } + + public void setAcsPhoneNumber(String acsPhoneNumber) { + this.acsPhoneNumber = acsPhoneNumber; + } + + public String getCallbackUriHost() { + return callbackUriHost; + } + + public void setCallbackUriHost(String callbackUriHost) { + this.callbackUriHost = callbackUriHost; + } + + public Map getBotRouting() { + return botRouting; + } + + public void setBotRouting(Map botRouting) { + this.botRouting = botRouting; + } + + public String getDefaultBotId() { + return defaultBotId; + } + + public void setDefaultBotId(String defaultBotId) { + this.defaultBotId = defaultBotId; + } +} diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/Main.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/Main.java new file mode 100644 index 0000000..85a3ed8 --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/Main.java @@ -0,0 +1,13 @@ +package com.communication.callautomation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties(value = ConfigurationRequest.class) +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} \ No newline at end of file diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ProgramSample.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ProgramSample.java new file mode 100644 index 0000000..c4287e7 --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ProgramSample.java @@ -0,0 +1,1886 @@ +package com.communication.callautomation; + +import com.azure.communication.callautomation.CallAutomationAsyncClient; +import com.azure.communication.callautomation.CallAutomationClient; +import com.azure.communication.callautomation.CallAutomationClientBuilder; +import com.azure.communication.callautomation.CallAutomationEventParser; +import com.azure.communication.callautomation.CallConnection; +import com.azure.communication.callautomation.CallDialog; +import com.azure.communication.callautomation.CallMedia; +import com.azure.communication.callautomation.implementation.models.PowerVirtualAgentsDialog; +import com.azure.communication.callautomation.models.*; +import com.azure.communication.callautomation.models.events.*; +import com.azure.communication.common.CommunicationIdentifier; +import com.azure.communication.common.CommunicationUserIdentifier; +import com.azure.communication.common.MicrosoftTeamsUserIdentifier; +import com.azure.communication.common.PhoneNumberIdentifier; +import com.azure.communication.identity.implementation.models.CommunicationErrorResponseException; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; +import com.azure.core.models.CloudEvent; +import com.azure.core.util.Context; +import com.azure.messaging.eventgrid.EventGridEvent; +import com.azure.messaging.eventgrid.systemevents.AcsIncomingCallEventData; +import com.azure.messaging.eventgrid.systemevents.AcsRecordingFileStatusUpdatedEventData; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData; + +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONObject; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@RestController +@Slf4j +public class ProgramSample { + + private final ConfigurationRequest appConfig; + private CallAutomationClient client; + private final CallAutomationAsyncClient asyncClient; + // Configuration state variables + private String acsConnectionString = ""; + private String cognitiveServicesEndpoint = ""; + private String acsPhoneNumber = ""; + private String callbackUriHost = ""; + private String fileSourceUri = ""; + + private String callConnectionId = ""; + private String recordingId = ""; + private String recordingLocation = ""; + private String recordingFileFormat = ""; + private URI eventCallbackUri; + + private String callerId = ""; + private String calleeId = ""; + + private Map botRouting = new HashMap<>(); + private String defaultBotId = ""; + private static final String INCOMING_CALL_CONTEXT = "incomingCallContext"; + private ConfigurationRequest configuration = new ConfigurationRequest(); + private String confirmLabel = "Confirm"; + private String cancelLabel = "Cancel"; + public ProgramSample(final ConfigurationRequest appConfig) { + this.appConfig = appConfig; + client = initClient(); + asyncClient = initAsyncClient(); + } + + @PostMapping(path = "/api/callback") + public ResponseEntity callbackEvents(@RequestBody final String reqBody) { + List events = CallAutomationEventParser.parseEvents(reqBody); + for (CallAutomationEventBase event : events) { + String callConnectionId = event.getCallConnectionId(); + log.info( + "Received call event callConnectionID: {}, serverCallId: {}", + callConnectionId, + event.getServerCallId()); + + if (event instanceof CallConnected) { + // handle CallConnected + } else if (event instanceof RecognizeCompleted) { + // handle RecognizeCompleted + } else if (event instanceof RecognizeFailed) { + // handle RecognizeFailed + } else if (event instanceof PlayCompleted || event instanceof PlayFailed) { + // handle PlayCompleted or PlayFailed + } + } + return ResponseEntity.ok().body(""); + } + + @PostMapping("/api/setConfigurations") + public ResponseEntity setConfigurations(@RequestBody ConfigurationRequest configurationRequest) { + // Reset variables + acsConnectionString = ""; + cognitiveServicesEndpoint = ""; + acsPhoneNumber = ""; + callbackUriHost = ""; + fileSourceUri = ""; + + if (configurationRequest != null) { + configuration.setAcsConnectionString( + Optional.ofNullable(configurationRequest.getAcsConnectionString()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("AcsConnectionString is required")) + ); + + configuration.setCognitiveServiceEndpoint( + Optional.ofNullable(configurationRequest.getCognitiveServiceEndpoint()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("CognitiveServiceEndpoint is required")) + ); + + configuration.setAcsPhoneNumber( + Optional.ofNullable(configurationRequest.getAcsPhoneNumber()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("AcsPhoneNumber is required")) + ); + + configuration.setCallbackUriHost( + Optional.ofNullable(configurationRequest.getCallbackUriHost()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("CallbackUriHost is required")) + ); + + configuration.setBotRouting( + Optional.ofNullable(configurationRequest.getBotRouting()) + .orElse(new HashMap<>()) + ); + + configuration.setDefaultBotId( + Optional.ofNullable(configurationRequest.getDefaultBotId()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("DefaultBotId is required")) + ); + } + + // Assign to global variables + acsConnectionString = configuration.getAcsConnectionString(); + cognitiveServicesEndpoint = configuration.getCognitiveServiceEndpoint(); + acsPhoneNumber = configuration.getAcsPhoneNumber(); + callbackUriHost = configuration.getCallbackUriHost(); + botRouting = configuration.getBotRouting(); + defaultBotId = configuration.getDefaultBotId(); + fileSourceUri = "https://sample-videos.com/audio/mp3/crowd-cheering.mp3"; + + client = new CallAutomationClientBuilder() + .connectionString(acsConnectionString) + .buildClient(); + + log.info("Initialized call automation client."); + return ResponseEntity.ok("Configuration set successfully. Initialized call automation client."); + } + @PostMapping("/api/events") + public ResponseEntity handleEvents(@RequestBody EventGridEvent[] eventGridEvents) { + try { + for (EventGridEvent eventGridEvent : eventGridEvents) { + log.info("Recording event received: {}", eventGridEvent.getEventType()); + + // Try to parse system event data + Object eventData = eventGridEvent.getData().toObject(Object.class); + + // SubscriptionValidationEventData + if ("Microsoft.EventGrid.SubscriptionValidationEvent".equals(eventGridEvent.getEventType()) + && eventData instanceof SubscriptionValidationEventData) { + + SubscriptionValidationEventData validationData = + (SubscriptionValidationEventData) eventData; + + Map responseData = Map.of("validationResponse", validationData.getValidationCode()); + return ResponseEntity.ok(responseData); + } + + // AcsIncomingCallEventData + if ("Microsoft.Communication.IncomingCall".equals(eventGridEvent.getEventType()) + && eventData instanceof AcsIncomingCallEventData) { + + AcsIncomingCallEventData incomingCallEventData = + (AcsIncomingCallEventData) eventData; + + callerId = incomingCallEventData.getFromCommunicationIdentifier().getRawId(); + System.out.println("Caller Id--> " + callerId); + + URI callbackUri = new URI(appConfig.getCallbackUriHost() + "/api/callbacks"); + log.info("Incoming call - correlationId: {}, Callback url: {}", + incomingCallEventData.getCorrelationId(), callbackUri); + + eventCallbackUri = callbackUri; + + AnswerCallOptions options = new AnswerCallOptions( + incomingCallEventData.getIncomingCallContext(), + callbackUri.toString() + ); + + // options.setCallIntelligenceOptions( + // new CallIntelligenceOptions() + // .setCognitiveServicesEndpoint(appConfig.getCognitiveServiceEndpoint() + // ) + //); + + // Call client to answer call + AnswerCallResult result = client.answerCallWithResponse(options, Context.NONE).getValue(); + CallMedia callConnectionMedia = result.getCallConnection().getCallMedia(); + } + + // AcsRecordingFileStatusUpdatedEventData + if ("Microsoft.Communication.RecordingFileStatusUpdated".equals(eventGridEvent.getEventType()) + && eventData instanceof AcsRecordingFileStatusUpdatedEventData) { + + AcsRecordingFileStatusUpdatedEventData statusUpdated = + (AcsRecordingFileStatusUpdatedEventData) eventData; + + recordingLocation = statusUpdated + .getRecordingStorageInfo() + .getRecordingChunks() + .get(0) + .getContentLocation(); + + log.info("The recording location is : {}", recordingLocation); + } + } + + return ResponseEntity.ok("Processed"); + + } catch (Exception ex) { + log.error("Error processing events", ex); + return ResponseEntity.status(500).body("Failed to process events"); + } + } + +// POST: /outboundCallToPstnAsync +@PostMapping("/outboundCallToPstnAsync") +public void outboundCallToPstnAsync(@RequestParam String targetPhoneNumber) { + PhoneNumberIdentifier target = new PhoneNumberIdentifier(targetPhoneNumber); + PhoneNumberIdentifier caller = new PhoneNumberIdentifier(acsPhoneNumber); + + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + eventCallbackUri = callbackUri; + + CallInvite callInvite = new CallInvite(target, caller); + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, appConfig.getCallbackUriHost()); + + // Make async call and block to get the result + Response response = asyncClient.createCallWithResponse(createCallOptions).block(); + + if (response != null && response.getValue() != null) { + String connectionId = response.getValue().getCallConnectionProperties().getCallConnectionId(); + log.info("Created async pstn call with connection id: " + connectionId); + } else { + log.error("Failed to create call. Response or value was null."); + } +} + + +@PostMapping("/outboundCallToPstn") +public void outboundCallToPstn(@RequestParam String targetPhoneNumber) { + PhoneNumberIdentifier target = new PhoneNumberIdentifier(targetPhoneNumber); + PhoneNumberIdentifier caller = new PhoneNumberIdentifier(acsPhoneNumber); + + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + eventCallbackUri = callbackUri; + CallInvite callInvite = new CallInvite(target, caller); + + // ✅ Convert URI to String + CreateCallResult result = client.createCall(callInvite, callbackUri.toString()); + String connectionId = result.getCallConnectionProperties().getCallConnectionId(); + log.info("Created call with connection id: " + connectionId); +} + +@PostMapping("/outboundCallToAcsAsync") +public void outboundCallToAcsAsync(@RequestParam String acsTarget) { + CommunicationUserIdentifier target = new CommunicationUserIdentifier(acsTarget); + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + eventCallbackUri = callbackUri; + + CallInvite callInvite = new CallInvite(target); + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri.toString()); + + // CallIntelligenceOptions callIntelligenceOptions = new CallIntelligenceOptions() + // .setCognitiveServicesEndpoint(cognitiveServicesEndpoint); + // createCallOptions.setCallIntelligenceOptions(callIntelligenceOptions); + + Response result = client.createCallWithResponse(createCallOptions, Context.NONE); + String connectionId = result.getValue().getCallConnectionProperties().getCallConnectionId(); + log.info("Created async ACS call with connection id: " + connectionId); +} +@PostMapping("/outboundCallToAcs") +public void outboundCallToAcs(@RequestParam String acsTarget) { + CommunicationUserIdentifier target = new CommunicationUserIdentifier(acsTarget); + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + eventCallbackUri = callbackUri; + + CallInvite callInvite = new CallInvite(target); + CreateCallResult result = client.createCall(callInvite, callbackUri.toString()); + String connectionId = result.getCallConnectionProperties().getCallConnectionId(); + log.info("Created ACS call with connection id: " + connectionId); +} + +@PostMapping("/outboundCallToTeamsAsync") +public void outboundCallToTeamsAsync(@RequestParam String teamsObjectId) { + MicrosoftTeamsUserIdentifier target = new MicrosoftTeamsUserIdentifier(teamsObjectId); + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + eventCallbackUri = callbackUri; + + CallInvite callInvite = new CallInvite(target); + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri.toString()); + + // CallIntelligenceOptions callIntelligenceOptions = new CallIntelligenceOptions() + // .setCognitiveServicesEndpoint(cognitiveServicesEndpoint); + // createCallOptions.setCallIntelligenceOptions(callIntelligenceOptions); + + Response result = client.createCallWithResponse(createCallOptions, Context.NONE); + String connectionId = result.getValue().getCallConnectionProperties().getCallConnectionId(); + log.info("Created async Teams call with connection id: " + connectionId); +} +@PostMapping("/outboundCallToTeams") +public void outboundCallToTeams(@RequestParam String teamsObjectId) { + MicrosoftTeamsUserIdentifier target = new MicrosoftTeamsUserIdentifier(teamsObjectId); + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + eventCallbackUri = callbackUri; + + CallInvite callInvite = new CallInvite(target); + CreateCallResult result = client.createCall(callInvite, callbackUri.toString()); + String connectionId = result.getCallConnectionProperties().getCallConnectionId(); + log.info("Created Teams call with connection id: " + connectionId); +} +@PostMapping("/createGroupCallAsync") + public void createGroupCallAsync(@RequestParam String targetPhoneNumber) { + PhoneNumberIdentifier target = new PhoneNumberIdentifier(targetPhoneNumber); + PhoneNumberIdentifier sourceCallerId = new PhoneNumberIdentifier(appConfig.getAcsPhoneNumber()); + + URI callbackUri = URI.create(appConfig.getCallbackUriHost() + "/api/callbacks"); + eventCallbackUri = callbackUri; + String websocketUri = appConfig.getCallbackUriHost().replace("https", "wss") + "/ws"; + + MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions( + websocketUri, + MediaStreamingTransport.WEBSOCKET, + MediaStreamingContent.AUDIO, + MediaStreamingAudioChannel.UNMIXED, + false + ); + + TranscriptionOptions transcriptionOptions = new TranscriptionOptions( + websocketUri, + TranscriptionTransport.WEBSOCKET, + "en-us", + false + ); + + List targets = List.of(target); + + CreateGroupCallOptions createGroupCallOptions = new CreateGroupCallOptions(targets, callbackUri.toString()) + // .setCallIntelligenceOptions(new CallIntelligenceOptions().setCognitiveServicesEndpoint(appConfig.getCognitiveServiceEndpoint())) + .setSourceCallIdNumber(sourceCallerId) + .setMediaStreamingOptions(mediaStreamingOptions) + .setTranscriptionOptions(transcriptionOptions); + + Response result = client.createGroupCallWithResponse(createGroupCallOptions, Context.NONE); + String connectionId = result.getValue().getCallConnectionProperties().getCallConnectionId(); + log.info("Created async group call with connection id: {}", connectionId); + } + + @PostMapping("/createGroupCall") + public void createGroupCall(@RequestParam String targetPhoneNumber) { + PhoneNumberIdentifier target = new PhoneNumberIdentifier(targetPhoneNumber); + PhoneNumberIdentifier sourceCallerId = new PhoneNumberIdentifier(appConfig.getAcsPhoneNumber()); + + URI callbackUri = URI.create(appConfig.getCallbackUriHost() + "/api/callbacks"); + eventCallbackUri = callbackUri; + + List targets = List.of(target,sourceCallerId); + + CreateCallResult result = client.createGroupCall(targets,callbackUri.toString()); + String connectionId = result.getCallConnectionProperties().getCallConnectionId(); + log.info("Created group call with connection id: {}", connectionId); + } + + @PostMapping("/connectRoomCallAsync") + public ResponseEntity connectRoomCallAsync(@RequestParam String roomId) { + return connectCallAsync(new RoomCallLocator(roomId), "ConnectRoomCallContext"); + } + + @PostMapping("/connectRoomCall") + public ResponseEntity connectRoomCall(@RequestParam String roomId) { + return connectCall(new RoomCallLocator(roomId), "ConnectRoomCallContext"); + } + + @PostMapping("/connectGroupCallAsync") + public ResponseEntity connectGroupCallAsync(@RequestParam String groupId) { + return connectCallAsync(new GroupCallLocator(groupId), "ConnectGroupCallContext"); + } + + @PostMapping("/connectGroupCall") + public ResponseEntity connectGroupCall(@RequestParam String groupId) { + return connectCall(new GroupCallLocator(groupId), "ConnectGroupCallContext"); + } + + @PostMapping("/connectOneToNCallAsync") + public ResponseEntity connectOneToNCallAsync(@RequestParam String serverCallId) { + return connectCallAsync(new ServerCallLocator(serverCallId), "ConnectOneToNCallContext"); + } + + @PostMapping("/connectOneToNCall") + public ResponseEntity connectOneToNCall(@RequestParam String serverCallId) { + return connectCall(new ServerCallLocator(serverCallId), "ConnectOneToNCallContext"); + } + + private ResponseEntity connectCall(CallLocator locator, String context) { + try { + URI callbackUri = new URI(appConfig.getCallbackUriHost() + "/api/callbacks"); + ConnectCallResult result = client.connectCall(locator,callbackUri.toString()); + log.info("Connected sync call with connection ID: {}", result.getCallConnectionProperties().getCallConnectionId()); + return ResponseEntity.ok("Call connected successfully"); + + } catch (Exception e) { + log.error("Error connecting call: {}", e.getMessage()); + return ResponseEntity.status(500).body("Call connection failed"); + } + } + + private ResponseEntity connectCallAsync(CallLocator locator, String context) { + try { + String callbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + String websocketUri = appConfig.getCallbackUriHost().replace("https", "wss") + "/ws"; + + MediaStreamingOptions mediaOptions = new MediaStreamingOptions(websocketUri, MediaStreamingTransport.WEBSOCKET, MediaStreamingContent.AUDIO, + MediaStreamingAudioChannel.UNMIXED, false); + + TranscriptionOptions transcriptionOptions = new TranscriptionOptions(websocketUri, TranscriptionTransport.WEBSOCKET, + "en-us", false); + + ConnectCallOptions options = new ConnectCallOptions(locator, callbackUri) + .setOperationContext(context) + // .setCallIntelligenceOptions(new CallIntelligenceOptions().setCognitiveServicesEndpoint(appConfig.getCognitiveServiceEndpoint())) + .setMediaStreamingOptions(mediaOptions) + .setTranscriptionOptions(transcriptionOptions); + + Mono> response = asyncClient.connectCallWithResponse(options); + response.subscribe(res -> { + if (res.getStatusCode() == 200) { + log.info("Connected async call. Connection ID: {}", res.getValue().getCallConnectionProperties().getCallConnectionId()); + } else { + log.error("Call async connection failed with status code: {}", res.getStatusCode()); + } + }); + return ResponseEntity.ok("Async call request sent"); + + } catch (Exception e) { + log.error("Error connecting async call: {}", e.getMessage()); + return ResponseEntity.status(500).body("Async call connection failed"); + } + } + + // region Add PSTN Participant + @PostMapping("/addPstnParticipantAsync") + public ResponseEntity addPstnParticipantAsync(@RequestParam String pstnParticipant) { + CallConnection callConnectionService = getConnection(); + CallInvite callInvite = new CallInvite( + new PhoneNumberIdentifier(pstnParticipant), + new PhoneNumberIdentifier("acsPhoneNumber")); // Replace with actual ACS number + AddParticipantOptions options = new AddParticipantOptions(callInvite); + options.setOperationContext("addPstnUserContext"); + options.setInvitationTimeout(Duration.ofSeconds(15)); + Object result = callConnectionService.addParticipantWithResponse(options,Context.NONE); + return ResponseEntity.ok(result); + } + + @PostMapping("/addPstnParticipant") + public ResponseEntity addPstnParticipant(@RequestParam String pstnParticipant) { + CallConnection callConnectionService = getConnection(); + CallInvite callInvite = new CallInvite( + new PhoneNumberIdentifier(pstnParticipant), + new PhoneNumberIdentifier(appConfig.getAcsPhoneNumber())).setSourceCallerIdNumber(new PhoneNumberIdentifier(appConfig.getAcsPhoneNumber())); // Replace with actual ACS number + Object result = callConnectionService.addParticipant(callInvite); + return ResponseEntity.ok(result); + } + + // region Add ACS Participant + @PostMapping("/addAcsParticipantAsync") + public ResponseEntity addAcsParticipantAsync(@RequestParam String acsParticipant) { + CallInvite callInvite = new CallInvite(new CommunicationUserIdentifier(acsParticipant)); + AddParticipantOptions options = new AddParticipantOptions(callInvite); + options.setOperationContext("addAcsUserContext"); + options.setInvitationTimeout(Duration.ofSeconds(15)); + + + CallConnection callConnectionService = getConnection(); + Object result = callConnectionService.addParticipantWithResponse(options, Context.NONE); + return ResponseEntity.ok(result); + } + + @PostMapping("/addAcsParticipant") + public ResponseEntity addAcsParticipant(@RequestParam String acsParticipant) { + CallInvite callInvite = new CallInvite(new CommunicationUserIdentifier(acsParticipant)); + CallConnection callConnectionService = getConnection(); + Object result = callConnectionService.addParticipant(callInvite); + return ResponseEntity.ok(result); + } + + // region Add Teams Participant + @PostMapping("/addTeamsParticipantAsync") + public ResponseEntity addTeamsParticipantAsync(@RequestParam String teamsObjectId) { + CallInvite callInvite = new CallInvite(new MicrosoftTeamsUserIdentifier(teamsObjectId)); + AddParticipantOptions options = new AddParticipantOptions(callInvite); + options.setOperationContext("addTeamsUserContext"); + options.setInvitationTimeout(Duration.ofSeconds(15)); + CallConnection callConnectionService = getConnection(); + Object result = callConnectionService.addParticipantWithResponse(options, Context.NONE); + return ResponseEntity.ok(result); + } + + @PostMapping("/addTeamsParticipant") + public ResponseEntity addTeamsParticipant(@RequestParam String teamsObjectId) { + CallInvite callInvite = new CallInvite(new MicrosoftTeamsUserIdentifier(teamsObjectId)); + CallConnection callConnectionService = getConnection(); + Object result = callConnectionService.addParticipant(callInvite); + return ResponseEntity.ok(result); + } + + // region Remove PSTN Participant + @PostMapping("/removePstnParticipantAsync") + public ResponseEntity removePstnParticipantAsync(@RequestParam String pstnTarget) { + RemoveParticipantOptions options = new RemoveParticipantOptions(new PhoneNumberIdentifier(pstnTarget)); + options.setOperationContext("removePstnParticipantContext"); + CallConnection callConnectionService = getConnection(); + callConnectionService.removeParticipantWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/removePstnParticipant") + public ResponseEntity removePstnParticipant(@RequestParam String pstnTarget) { + CallConnection callConnectionService = getConnection(); + callConnectionService.removeParticipant(new PhoneNumberIdentifier(pstnTarget)); + return ResponseEntity.ok().build(); + } + + // region Remove ACS Participant + @PostMapping("/removeAcsParticipantAsync") + public ResponseEntity removeAcsParticipantAsync(@RequestParam String acsTarget) { + RemoveParticipantOptions options = new RemoveParticipantOptions(new CommunicationUserIdentifier(acsTarget)); + options.setOperationContext("removeAcsParticipantContext"); + CallConnection callConnectionService = getConnection(); + callConnectionService.removeParticipantWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/removeAcsParticipant") + public ResponseEntity removeAcsParticipant(@RequestParam String acsTarget) { + CallConnection callConnectionService = getConnection(); + callConnectionService.removeParticipant(new CommunicationUserIdentifier(acsTarget)); + return ResponseEntity.ok().build(); + } + + // region Remove Teams Participant + @PostMapping("/removeTeamsParticipantAsync") + public ResponseEntity removeTeamsParticipantAsync(@RequestParam String teamsObjectId) { + RemoveParticipantOptions options = new RemoveParticipantOptions(new MicrosoftTeamsUserIdentifier(teamsObjectId)); + options.setOperationContext("removeTeamsParticipantContext"); + CallConnection callConnectionService = getConnection(); + callConnectionService.removeParticipantWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/removeTeamsParticipant") + public ResponseEntity removeTeamsParticipant(@RequestParam String teamsObjectId) { + CallConnection callConnectionService = getConnection(); + callConnectionService.removeParticipant(new MicrosoftTeamsUserIdentifier(teamsObjectId)); + return ResponseEntity.ok().build(); + } + + // region Cancel Add Participant + @PostMapping("/cancelAddParticipantAsync") + public ResponseEntity cancelAddParticipantAsync(@RequestParam String invitationId) { + CancelAddParticipantOperationOptions options = new CancelAddParticipantOperationOptions(invitationId); + options.setOperationContext("CancelAddingParticipantContext"); + CallConnection callConnectionService = getConnection(); + Object result = callConnectionService.cancelAddParticipantOperationWithResponse(options,Context.NONE); + return ResponseEntity.ok(result); + } + + @PostMapping("/cancelAddParticipant") + public ResponseEntity cancelAddParticipant(@RequestParam String invitationId) { + CallConnection callConnectionService = getConnection(); + Object result = callConnectionService.cancelAddParticipantOperation(invitationId); + return ResponseEntity.ok(result); + } + private TextSource createTextSource(String message) { + var textSource = new TextSource() + .setText(message) + .setVoiceName("en-US-NancyNeural"); + return textSource; + } + + @PostMapping("/playTextSourceToPstnTargetAsync") + public ResponseEntity playTextSourceToPstnTargetAsync(@RequestParam String pstnTarget) { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + List playTo = Collections.singletonList(new PhoneNumberIdentifier(pstnTarget)); + + PlayOptions options = new PlayOptions(textSource, playTo); + options.setOperationContext("playToContext"); + callMedia.playWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceToPstnTarget") + public ResponseEntity playTextSourceToPstnTarget(@RequestParam String pstnTarget) { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + List playTo = Collections.singletonList(new PhoneNumberIdentifier(pstnTarget)); + callMedia.play(textSource,playTo); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceAcsTargetAsync") + public ResponseEntity playTextSourceAcsTargetAsync(@RequestParam String acsTarget) { + CallMedia callMedia = getCallMedia(); + List playTo = Collections.singletonList(new PhoneNumberIdentifier(acsTarget)); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + PlayOptions options = new PlayOptions(textSource,playTo); + options.setOperationContext("playToContext"); + callMedia.playWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceToAcsTarget") + public ResponseEntity playTextSourceToAcsTarget(@RequestParam String acsTarget) { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + callMedia.play(textSource,Collections.singletonList(new PhoneNumberIdentifier(acsTarget))); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceToTeamsTargetAsync") + public ResponseEntity playTextSourceToTeamsTargetAsync(@RequestParam String teamsObjectId) { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + PlayOptions options = new PlayOptions(textSource, Collections.singletonList(new MicrosoftTeamsUserIdentifier(teamsObjectId))); + options.setOperationContext("playToContext"); + callMedia.playWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceToTeamsTarget") + public ResponseEntity playTextSourceToTeamsTarget(@RequestParam String teamsObjectId) { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + callMedia.play(textSource, Collections.singletonList(new MicrosoftTeamsUserIdentifier(teamsObjectId))); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceToAllAsync") + public ResponseEntity playTextSourceToAllAsync() { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + PlayToAllOptions options = new PlayToAllOptions(textSource); + options.setOperationContext("playToAllContext"); + callMedia.playToAllWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceToAll") + public ResponseEntity playTextSourceToAll() { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is test source played through play source thanks. Goodbye!."); + PlayToAllOptions options = new PlayToAllOptions(textSource); + options.setOperationContext("playToAllContext"); + callMedia.playToAll(textSource); + return ResponseEntity.ok().build(); + } + + @PostMapping("/playTextSourceBargeInAsync") + public ResponseEntity playTextSourceBargeInAsync() { + CallMedia callMedia = getCallMedia(); + TextSource textSource = createTextSource("Hi, this is barge in test played through play source thanks. Goodbye!."); + PlayToAllOptions options = new PlayToAllOptions(textSource); + options.setOperationContext("playToAllContext"); + options.setInterruptCallMediaOperation(true); + callMedia.playToAllWithResponse(options,Context.NONE); + return ResponseEntity.ok().build(); + } + private static final String SSML_STRING = "" + + "Hi, this is %s test played through SSML source. Goodbye!"; + public SsmlSource createSsmlSource(boolean bargeIn) { + String bargeText = bargeIn ? "barge in" : "test"; + SsmlSource ssmlSource = new SsmlSource(); + ssmlSource.setSsmlText(String.format(SSML_STRING, bargeText)); + return ssmlSource; + } + // 1. PSTN - Async + @PostMapping("/playSsmlSourceToPstnTargetAsync") + public ResponseEntity playSsmlSourceToPstnTargetAsync(@RequestParam String pstnTarget) { + return playSsml(pstnTarget, TargetType.PSTN, true, false); + } + + // 2. PSTN - Sync + @PostMapping("/playSsmlSourceToPstnTarget") + public ResponseEntity playSsmlSourceToPstnTarget(@RequestParam String pstnTarget) { + return playSsml(pstnTarget, TargetType.PSTN, false, false); + } + + // 3. ACS - Async + @PostMapping("/playSsmlSourceAcsTargetAsync") + public ResponseEntity playSsmlSourceAcsTargetAsync(@RequestParam String acsTarget) { + return playSsml(acsTarget, TargetType.ACS, true, false); + } + + // 4. ACS - Sync + @PostMapping("/playSsmlSourceToAcsTarget") + public ResponseEntity playSsmlSourceToAcsTarget(@RequestParam String acsTarget) { + return playSsml(acsTarget, TargetType.ACS, false, false); + } + + // 5. Teams - Async + @PostMapping("/playSsmlSourceToTeamsTargetAsync") + public ResponseEntity playSsmlSourceToTeamsTargetAsync(@RequestParam String teamsUserId) { + return playSsml(teamsUserId, TargetType.TEAMS, true, false); + } + + // 6. Teams - Sync + @PostMapping("/playSsmlSourceToTeamsTarget") + public ResponseEntity playSsmlSourceToTeamsTarget(@RequestParam String teamsUserId) { + return playSsml(teamsUserId, TargetType.TEAMS, false, false); + } + + // 7. All - Async + @PostMapping("/playSsmlSourceToAllAsync") + public ResponseEntity playSsmlSourceToAllAsync() { + return playSsml(null, TargetType.ALL, true, false); + } + + // 8. All - Sync + @PostMapping("/playSsmlSourceToAll") + public ResponseEntity playSsmlSourceToAll() { + return playSsml(null, TargetType.ALL, false, false); + } + + // 9. Barge-In - Async + @PostMapping("/playSsmlSourceBargeInAsync") + public ResponseEntity playSsmlSourceBargeInAsync() { + return playSsml(null, TargetType.ALL, true, true); + } + + // 🔄 Shared Method + private ResponseEntity playSsml(String target, TargetType targetType, boolean async, boolean bargeIn) { + SsmlSource ssmlSource = createSsmlSource(bargeIn); + String context = bargeIn ? "bargeInContext" : "testContext"; + CallMedia mediaService = getCallMedia(); + + try { + if (targetType == TargetType.ALL) { + PlayToAllOptions options = new PlayToAllOptions(ssmlSource); + options.setOperationContext(context); + options.setInterruptCallMediaOperation(bargeIn); + + if (async) { + mediaService.playToAll(Collections.singletonList(ssmlSource)); + } else { + mediaService.playToAllWithResponse(options, Context.NONE); + } + } else { + List playTo = switch (targetType) { + case PSTN -> List.of(new PhoneNumberIdentifier(target)); + case ACS -> List.of(new CommunicationUserIdentifier(target)); + case TEAMS -> List.of(new MicrosoftTeamsUserIdentifier(target)); + default -> throw new IllegalArgumentException("Unsupported target type."); + }; + + PlayOptions options = new PlayOptions(ssmlSource, playTo); + options.setOperationContext(context); + options.setInterruptHoldAudio(bargeIn); + + if (async) { + mediaService.play(ssmlSource, playTo); + } else { + mediaService.playWithResponse(options, Context.NONE); + } + } + return ResponseEntity.ok().build(); + } catch (Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + private static final String FILE_SOURCE_URI = "https://yourdomain.com/sample.wav"; // replace with actual URI + + // 1. PSTN - Async + @PostMapping("/playFileSourceToPstnTargetAsync") + public ResponseEntity playFileSourceToPstnTargetAsync(@RequestParam String pstnTarget) { + return playFile(pstnTarget, TargetType.PSTN, true, false); + } + + // 2. PSTN - Sync + @PostMapping("/playFileSourceToPstnTarget") + public ResponseEntity playFileSourceToPstnTarget(@RequestParam String pstnTarget) { + return playFile(pstnTarget, TargetType.PSTN, false, false); + } + + // 3. ACS - Async + @PostMapping("/playFileSourceToAcsTargetAsync") + public ResponseEntity playFileSourceToAcsTargetAsync(@RequestParam String acsTarget) { + return playFile(acsTarget, TargetType.ACS, true, false); + } + + // 4. ACS - Sync + @PostMapping("/playFileSourceToAcsTarget") + public ResponseEntity playFileSourceToAcsTarget(@RequestParam String acsTarget) { + return playFile(acsTarget, TargetType.ACS, false, false); + } + + // 5. Teams - Async + @PostMapping("/playFileSourceToTeamsTargetAsync") + public ResponseEntity playFileSourceToTeamsTargetAsync(@RequestParam String teamsUserId) { + return playFile(teamsUserId, TargetType.TEAMS, true, false); + } + + // 6. Teams - Sync + @PostMapping("/playFileSourceToTeamsTarget") + public ResponseEntity playFileSourceToTeamsTarget(@RequestParam String teamsUserId) { + return playFile(teamsUserId, TargetType.TEAMS, false, false); + } + + // 7. All - Async + @PostMapping("/playFileSourceToAllAsync") + public ResponseEntity playFileSourceToAllAsync() { + return playFile(null, TargetType.ALL, true, false); + } + + // 8. All - Sync + @PostMapping("/playFileSourceToAll") + public ResponseEntity playFileSourceToAll() { + return playFile(null, TargetType.ALL, false, false); + } + + // 9. Barge-In - Async + @PostMapping("/playFileSourceBargeInAsync") + public ResponseEntity playFileSourceBargeInAsync() { + return playFile(null, TargetType.ALL, true, true); + } + + // Shared Logic + private ResponseEntity playFile(String target, TargetType targetType, boolean async, boolean bargeIn) { + FileSource fileSource = new FileSource().setUrl(FILE_SOURCE_URI); + String context = bargeIn ? "playBargeInContext" : "playContext"; + CallMedia mediaService = getCallMedia(); + + try { + if (targetType == TargetType.ALL) { + PlayToAllOptions options = new PlayToAllOptions(fileSource); + options.setOperationContext(context); + options.setInterruptCallMediaOperation(bargeIn); + + if (async) { + mediaService.playToAll(Collections.singletonList(fileSource)); + } else { + mediaService.playToAllWithResponse(options, Context.NONE); + } + } else { + List playTo = switch (targetType) { + case PSTN -> List.of(new PhoneNumberIdentifier(target)); + case ACS -> List.of(new CommunicationUserIdentifier(target)); + case TEAMS -> List.of(new MicrosoftTeamsUserIdentifier(target)); + default -> throw new IllegalArgumentException("Unsupported target type."); + }; + + PlayOptions options = new PlayOptions(fileSource, playTo); + options.setOperationContext(context); + options.setInterruptHoldAudio(bargeIn); + + if (async) { + mediaService.play(fileSource, playTo); + } else { + mediaService.playWithResponse(options, Context.NONE); + } + } + return ResponseEntity.ok().build(); + } catch (Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + @PostMapping("/recognizeDTMFAsync") + public ResponseEntity recognizeDTMFAsync(@RequestParam String pstnTarget) { + return startDtmfRecognition(pstnTarget, true); + } + + @PostMapping("/recognizeDTMF") + public ResponseEntity recognizeDTMF(@RequestParam String pstnTarget) { + return startDtmfRecognition(pstnTarget, false); + } + + @PostMapping("/recognizeSpeechAsync") + public ResponseEntity recognizeSpeechAsync(@RequestParam String pstnTarget) { + return startSpeechRecognition(pstnTarget, true); + } + + @PostMapping("/recognizeSpeech") + public ResponseEntity recognizeSpeech(@RequestParam String pstnTarget) { + return startSpeechRecognition(pstnTarget, false); + } + + @PostMapping("/recognizeSpeechOrDtmfAsync") + public ResponseEntity recognizeSpeechOrDtmfAsync(@RequestParam String pstnTarget) { + return startSpeechOrDtmfRecognition(pstnTarget, true); + } + + @PostMapping("/recognizeSpeechOrDtmf") + public ResponseEntity recognizeSpeechOrDtmf(@RequestParam String pstnTarget) { + return startSpeechOrDtmfRecognition(pstnTarget, false); + } + + @PostMapping("/recognizeChoiceAsync") + public ResponseEntity recognizeChoiceAsync(@RequestParam String pstnTarget) { + return startChoiceRecognition(pstnTarget, true); + } + + @PostMapping("/recognizeChoice") + public ResponseEntity recognizeChoice(@RequestParam String pstnTarget) { + return startChoiceRecognition(pstnTarget, false); + } + + // 🔁 Reusable helper methods below + private ResponseEntity startDtmfRecognition(String target, boolean async) { + try { + CallMedia callMedia = getCallMedia(); + TextSource prompt = new TextSource() + .setText("Hi, this is recognize test. Please provide input. Thanks!") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); // Optional: if enum NEURAL is available + + PhoneNumberIdentifier participant = new PhoneNumberIdentifier(target); + + CallMediaRecognizeDtmfOptions options = new CallMediaRecognizeDtmfOptions(participant, 4) + .setInterruptPrompt(false) + .setInterToneTimeout(Duration.ofSeconds(5)) + .setInitialSilenceTimeout(Duration.ofSeconds(15)) + .setPlayPrompt(prompt) + .setOperationContext("DtmfContext"); + + if (async) { + callMedia.startRecognizingWithResponse(options, Context.NONE); // async version + } else { + callMedia.startRecognizing(options); // sync version + } + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private ResponseEntity startSpeechRecognition(String target, boolean async) { + try { + CallMedia callMedia = getCallMedia(); + TextSource prompt = new TextSource() + .setText("Hi, this is recognize test. Please provide input. Thanks!") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + PhoneNumberIdentifier participant = new PhoneNumberIdentifier(target); + + CallMediaRecognizeSpeechOptions options = new CallMediaRecognizeSpeechOptions(participant, Duration.ofSeconds(15)) + .setInterruptPrompt(false) + .setInitialSilenceTimeout(Duration.ofSeconds(15)) + .setPlayPrompt(prompt) // Fixed method call + .setOperationContext("SpeechContext"); + + if (async) { + callMedia.startRecognizingWithResponse(options, Context.NONE); // async version + } else { + callMedia.startRecognizing(options); // sync version + } + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private ResponseEntity startSpeechOrDtmfRecognition(String target, boolean async) { + try { + CallMedia callMedia = getCallMedia(); + TextSource prompt = new TextSource() + .setText("Hi, this is recognize test. Please provide input. Thanks!") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + PhoneNumberIdentifier participant = new PhoneNumberIdentifier(target); + + var options = new CallMediaRecognizeSpeechOrDtmfOptions(participant, 4, null) + .setInterruptPrompt(false) + .setInitialSilenceTimeout(Duration.ofSeconds(15)) + .setPlayPrompts(prompt) // Fixed method call + .setOperationContext("SpeechContext"); + + if (async) { + callMedia.startRecognizingWithResponse(options, Context.NONE); // async version + } else { + callMedia.startRecognizing(options); // sync version + } + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private ResponseEntity startChoiceRecognition(String target, boolean async) { + try { + CallMedia callMedia = getCallMedia(); + TextSource prompt = new TextSource() + .setText("Hi, this is recognize test. Please provide input. Thanks!") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + PhoneNumberIdentifier participant = new PhoneNumberIdentifier(target); + + CallMediaRecognizeChoiceOptions options = new CallMediaRecognizeChoiceOptions(participant, getChoices()) + .setInterruptPrompt(false) + .setInterruptCallMediaOperation(false) + .setInitialSilenceTimeout(Duration.ofSeconds(10)) + .setPlayPrompt(prompt) + .setOperationContext("ChoiceContext"); + + if (async) { + callMedia.startRecognizingWithResponse(options, Context.NONE); // async version + } else { + callMedia.startRecognizing(options); // sync version + } + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + // Async Equivalent: /sendDTMFTonesAsync (C#) + @PostMapping("/sendDTMFTonesAsync") + public ResponseEntity sendDTMFTonesAsync(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + List tones = Arrays.asList(DtmfTone.ZERO, DtmfTone.ONE); + CallMedia callMediaService = getCallMedia(); + SendDtmfTonesResult result = callMediaService.sendDtmfTones(tones, target); // .block() internally + + log.info("Async DTMF tones sent to {}", pstnTarget); + return ResponseEntity.ok("DTMF tones sent (async simulation)."); + } + + // Sync Equivalent: /sendDTMFTones (C#) + @PostMapping("/sendDTMFTones") + public ResponseEntity sendDTMFTones(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + List tones = Arrays.asList(DtmfTone.ZERO, DtmfTone.ONE); + CallMedia callMediaService = getCallMedia(); + SendDtmfTonesResult result = callMediaService.sendDtmfTones(tones, target); + + log.info("DTMF tones sent to {}", pstnTarget); + return ResponseEntity.ok("DTMF tones sent."); + } + + // Async Equivalent: /startContinuousDTMFTonesAsync (C#) + @PostMapping("/startContinuousDTMFTonesAsync") + public ResponseEntity startContinuousDTMFTonesAsync(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + callMediaService.startContinuousDtmfRecognition(target); // .block() internally + + log.info("Async continuous DTMF started for {}", pstnTarget); + return ResponseEntity.ok("Started continuous DTMF recognition (async simulation)."); + } + + // Sync Equivalent: /startContinuousDTMFTones (C#) + @PostMapping("/startContinuousDTMFTones") + public ResponseEntity startContinuousDTMFTones(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + callMediaService.startContinuousDtmfRecognition(target); + + log.info("Started continuous DTMF for {}", pstnTarget); + return ResponseEntity.ok("Started continuous DTMF recognition."); + } + + // Async Equivalent: /stopContinuousDTMFTonesAsync (C#) + @PostMapping("/stopContinuousDTMFTonesAsync") + public ResponseEntity stopContinuousDTMFTonesAsync(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + callMediaService.stopContinuousDtmfRecognition(target); // .block() internally + + log.info("Async stop continuous DTMF for {}", pstnTarget); + return ResponseEntity.ok("Stopped continuous DTMF recognition (async simulation)."); + } + + // Sync Equivalent: /stopContinuousDTMFTones (C#) + @PostMapping("/stopContinuousDTMFTones") + public ResponseEntity stopContinuousDTMFTones(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + callMediaService.stopContinuousDtmfRecognition(target); + + log.info("Stopped continuous DTMF for {}", pstnTarget); + return ResponseEntity.ok("Stopped continuous DTMF recognition."); + } + + + @PostMapping("/holdParticipantAsync") +public ResponseEntity holdParticipantAsync(@RequestParam String pstnTarget, + @RequestParam boolean isPlaySource) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + HoldOptions holdOptions = new HoldOptions(target).setOperationContext("holdUserContext"); + CallMedia callMediaService = getCallMedia(); + + if (isPlaySource) { + TextSource textSource = new TextSource() + .setText("You are on hold. Please wait...") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + holdOptions.setPlaySource(textSource); + } + + callMediaService.holdWithResponse(holdOptions, Context.NONE); + log.info("Held participant asynchronously with playSource = {}", isPlaySource); + return ResponseEntity.ok("Participant held (async)."); +} + +@PostMapping("/holdParticipant") +public ResponseEntity holdParticipant(@RequestParam String pstnTarget, + @RequestParam boolean isPlaySource) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + TextSource textSource = null; + CallMedia callMediaService = getCallMedia(); + + if (isPlaySource) { + textSource = new TextSource() + .setText("You are on hold. Please wait...") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + } + + callMediaService.hold(target, textSource); + log.info("Held participant synchronously with playSource = {}", isPlaySource); + return ResponseEntity.ok("Participant held."); +} + +@PostMapping("/interruptAudioAndAnnounceAsync") +public ResponseEntity interruptAudioAndAnnounceAsync(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + + TextSource textSource = new TextSource() + .setText("Hi, this is interrupt audio and announcement test.") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + InterruptAudioAndAnnounceOptions options = new InterruptAudioAndAnnounceOptions(textSource, target) + .setOperationContext("interruptContext"); + + callMediaService.interruptAudioAndAnnounceWithResponse(options, Context.NONE); + log.info("InterruptAudioAndAnnounce (async) sent to {}", pstnTarget); + return ResponseEntity.ok("Interrupt audio and announce sent (async)."); +} + +@PostMapping("/interruptAudioAndAnnounce") +public ResponseEntity interruptAudioAndAnnounce(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + + TextSource textSource = new TextSource() + .setText("Hi, this is interrupt audio and announcement test.") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + callMediaService.interruptAudioAndAnnounce(textSource, target); + log.info("InterruptAudioAndAnnounce (sync) sent to {}", pstnTarget); + return ResponseEntity.ok("Interrupt audio and announce sent."); +} + +@PostMapping("/unholdParticipantAsync") +public ResponseEntity unholdParticipantAsync(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + UnholdOptions unholdOptions = new UnholdOptions(target).setOperationContext("unholdUserContext"); + CallMedia callMediaService = getCallMedia(); + + callMediaService.unholdWithResponse(unholdOptions, Context.NONE); + log.info("Unhold participant asynchronously {}", pstnTarget); + return ResponseEntity.ok("Participant unheld (async)."); +} + +@PostMapping("/unholdParticipant") +public ResponseEntity unholdParticipant(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + + callMediaService.unhold(target); + log.info("Unhold participant synchronously {}", pstnTarget); + return ResponseEntity.ok("Participant unheld."); +} + +@PostMapping("/interruptHoldWithPlayAsync") +public ResponseEntity interruptHoldWithPlayAsync(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + + TextSource textSource = new TextSource() + .setText("Hi, this is interrupt hold and play test.") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + List playTo = Collections.singletonList(target); + PlayOptions playOptions = new PlayOptions(textSource, playTo) + .setOperationContext("playToContext") + .setInterruptHoldAudio(true); + + callMediaService.playWithResponse(playOptions, Context.NONE); + log.info("Interrupt hold with play sent (async) to {}", pstnTarget); + return ResponseEntity.ok("Interrupt hold with play sent (async)."); +} + +@PostMapping("/interruptHoldWithPlay") +public ResponseEntity interruptHoldWithPlay(@RequestParam String pstnTarget) { + CommunicationIdentifier target = new PhoneNumberIdentifier(pstnTarget); + CallMedia callMediaService = getCallMedia(); + + TextSource textSource = new TextSource() + .setText("Hi, this is interrupt hold and play test.") + .setVoiceName("en-US-NancyNeural") + .setSourceLocale("en-US") + .setVoiceKind(VoiceKind.MALE); + + List playTo = Collections.singletonList(target); + callMediaService.play(textSource, playTo); + + log.info("Interrupt hold with play sent (sync) to {}", pstnTarget); + return ResponseEntity.ok("Interrupt hold with play sent."); +} +@PostMapping("/hangupAsync") + public ResponseEntity hangupAsync(@RequestParam boolean isForEveryOne) { + CallConnection callConnection = getConnection(); + + callConnection.hangUpWithResponse(isForEveryOne, Context.NONE); + log.info("Call hangup requested (async) forEveryone={}", isForEveryOne); + + return ResponseEntity.ok("Call hangup requested (async)."); + } + + @PostMapping("/hangup") + public ResponseEntity hangup(@RequestParam boolean isForEveryOne) { + CallConnection callConnection = getConnection(); + + callConnection.hangUp(isForEveryOne); + log.info("Call hangup requested (sync) forEveryone={}", isForEveryOne); + + return ResponseEntity.ok("Call hangup requested."); + } + + @PostMapping("/muteAcsParticipantAsync") + public ResponseEntity muteAcsParticipantAsync(@RequestParam String acsTarget) { + CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); + CallConnection callConnection = getConnection(); + + MuteParticipantOptions options = new MuteParticipantOptions(target) + .setOperationContext("muteContext"); + + // Assuming you're calling a method like muteParticipantWithResponse(options, context) + callConnection.muteParticipantWithResponse(options, Context.NONE); + + log.info("Muted ACS participant asynchronously: {}", acsTarget); + return ResponseEntity.ok("Muted ACS participant (async)."); + } + + @PostMapping("/muteAcsParticipant") + public ResponseEntity muteAcsParticipant(@RequestParam String acsTarget) { + CommunicationIdentifier target = new CommunicationUserIdentifier(acsTarget); + CallConnection callConnection = getConnection(); + + callConnection.muteParticipant(target); // Synchronous mute using options if method is available + log.info("Muted ACS participant synchronously: {}", acsTarget); + return ResponseEntity.ok("Muted ACS participant."); + } + + @PostMapping("/transferCallToPstnParticipantAsync") + public ResponseEntity transferCallToPstnParticipantAsync( + @RequestParam String pstnTransferTarget, + @RequestParam String pstnTarget) { + + CallConnection callConnection = getConnection(); + + TransferCallToParticipantOptions options = new TransferCallToParticipantOptions(new PhoneNumberIdentifier(pstnTransferTarget)); + options.setOperationContext("TransferCallContext"); + options.setTransferee(new PhoneNumberIdentifier(pstnTarget)); + + // Async transfer, assuming a method like below exists + ((Mono>) callConnection.transferCallToParticipantWithResponse(options,Context.NONE)) + .subscribe(response -> log.info("Call transferred successfully."), + error -> log.error("Transfer failed", error)); + + return ResponseEntity.ok("Transfer initiated"); + } + + @PostMapping("/transferCallToPstnParticipant") + public ResponseEntity transferCallToPstnParticipant( + @RequestParam String pstnTransferTarget, + @RequestParam String pstnTarget) { + + CallConnection callConnection = getConnection(); + + + // Synchronous transfer + callConnection.transferCallToParticipant(new PhoneNumberIdentifier(pstnTarget)); + + return ResponseEntity.ok("Transfer completed"); + } + @PostMapping("/getPstnParticipantAsync") + public ResponseEntity getPstnParticipantAsync(@RequestParam String pstnTarget) { + CallConnection callConnection = getConnection(); + + Response response = callConnection.getParticipantWithResponse( + new PhoneNumberIdentifier(pstnTarget), + Context.NONE + ); + + CallParticipant participant = response.getValue(); + + if (participant != null) { + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + } + + return ResponseEntity.ok().build(); + } + + + @PostMapping("/getPstnParticipant") + public ResponseEntity getPstnParticipant(@RequestParam String pstnTarget) { + CallConnection callConnection = getConnection(); + CallParticipant participant = callConnection.getParticipant(new PhoneNumberIdentifier(pstnTarget)); + + if (participant != null) { + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + } + return ResponseEntity.ok().build(); + } + + @PostMapping("/getAcsParticipantAsync") + public ResponseEntity getAcsParticipantAsync(@RequestParam String acsTarget) { + CallConnection callConnection = getConnection(); + + Response response = callConnection.getParticipantWithResponse( + new CommunicationUserIdentifier(acsTarget), + Context.NONE + ); + + CallParticipant participant = response.getValue(); + + if (participant != null) { + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + } else { + log.warn("No participant found for ACS identifier: {}", acsTarget); + } + + return ResponseEntity.ok().build(); + } + + + @PostMapping("/getAcsParticipant") + public ResponseEntity getAcsParticipant(@RequestParam String acsTarget) { + CallConnection callConnection = getConnection(); + CallParticipant participant = callConnection.getParticipant(new CommunicationUserIdentifier(acsTarget)); + + if (participant != null) { + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + } + return ResponseEntity.ok().build(); + } + + @PostMapping("/getTeamsParticipantAsync") + public ResponseEntity getTeamsParticipantAsync(@RequestParam String teamsObjectId) { + CallConnection callConnection = getConnection(); + + // Call the method correctly and extract the participant from the response + Response response = callConnection.getParticipantWithResponse( + new MicrosoftTeamsUserIdentifier(teamsObjectId), + Context.NONE + ); + + CallParticipant participant = response.getValue(); + + if (participant != null) { + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + } else { + log.warn("No participant found for Teams Object ID: {}", teamsObjectId); + } + + return ResponseEntity.ok().build(); + } + + @PostMapping("/getTeamsParticipant") + public ResponseEntity getTeamsParticipant(@RequestParam String teamsObjectId) { + CallConnection callConnection = getConnection(); + CallParticipant participant = callConnection.getParticipant(new MicrosoftTeamsUserIdentifier(teamsObjectId)); + + if (participant != null) { + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + } + return ResponseEntity.ok().build(); + } + + @PostMapping("/getParticipantListAsync") +public ResponseEntity getParticipantListAsync() { + CallConnection callConnection = getConnection(); + + PagedIterable participants = callConnection.listParticipants(Context.NONE); + + if (participants != null) { + for (CallParticipant participant : participants) { + log.info("----------------------------------------------------------------------"); + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + log.info("----------------------------------------------------------------------"); + } + } else { + log.warn("No participants returned in the response."); + } + + return ResponseEntity.ok().build(); +} + +@PostMapping("/getParticipantList") +public ResponseEntity getParticipantList() { + CallConnection callConnection = getConnection(); + + PagedIterable participants = callConnection.listParticipants(); + + if (participants != null) { + for (CallParticipant participant : participants) { + log.info("----------------------------------------------------------------------"); + log.info("Participant: --> {}", participant.getIdentifier().getRawId()); + log.info("Is Participant on hold: --> {}", participant.isOnHold()); + log.info("----------------------------------------------------------------------"); + } + } else { + log.warn("No participants returned in the response."); + } + + return ResponseEntity.ok().build(); +} + +@PostMapping("/startRecordingWithVideoMp4MixedAsync") + public void startRecordingWithVideoMp4MixedAsync(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO_VIDEO); + options.setRecordingFormat(RecordingFormat.MP4); + options.setRecordingChannel(RecordingChannel.MIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp4"; + + Response response = client.getCallRecording() + .startWithResponse(options, Context.NONE); + + recordingId = response.getValue().getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/startRecordingWithVideoMp4Mixed") + public void startRecordingWithVideoMp4Mixed(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO_VIDEO); + options.setRecordingFormat(RecordingFormat.MP4); + options.setRecordingChannel(RecordingChannel.MIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp4"; + + recordingId = client.getCallRecording().start(options).getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/startRecordingWithAudioMp3MixedAsync") + public void startRecordingWithAudioMp3MixedAsync(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO); + options.setRecordingFormat(RecordingFormat.MP3); + options.setRecordingChannel(RecordingChannel.MIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp3"; + + Response response = client.getCallRecording() + .startWithResponse(options, Context.NONE); + + recordingId = response.getValue().getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/startRecordingWithAudioMp3Mixed") + public void startRecordingWithAudioMp3Mixed(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO); + options.setRecordingFormat(RecordingFormat.MP3); + options.setRecordingChannel(RecordingChannel.MIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp4"; + + recordingId = client.getCallRecording().start(options).getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/startRecordingWithAudioMp3UnMixedAsync") + public void startRecordingWithAudioMp3UnMixedAsync(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO); + options.setRecordingFormat(RecordingFormat.MP3); + options.setRecordingChannel(RecordingChannel.UNMIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp3"; + + Response response = client.getCallRecording() + .startWithResponse(options, Context.NONE); + + recordingId = response.getValue().getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/startRecordingWithAudioMp3Unmixed") + public void startRecordingWithAudioMp3Unmixed(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO); + options.setRecordingFormat(RecordingFormat.MP3); + options.setRecordingChannel(RecordingChannel.UNMIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp4"; + + recordingId = client.getCallRecording().start(options).getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + @PostMapping("/startRecordingWithAudioWavUnMixedAsync") + public void startRecordingWithAudioWavUnMixedAsync(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO); + options.setRecordingFormat(RecordingFormat.WAV); + options.setRecordingChannel(RecordingChannel.UNMIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp3"; + + Response response = client.getCallRecording() + .startWithResponse(options, Context.NONE); + + recordingId = response.getValue().getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/startRecordingWithAudioWavUnmixed") + public void startRecordingWithAudioWavUnmixed(@RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + CallConnectionProperties properties = getCallConnectionProperties(); + String eventCallbackUri = appConfig.getCallbackUriHost() + "/api/callbacks"; + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + StartRecordingOptions options = isRecordingWithCallConnectionId ? + new StartRecordingOptions(properties.getCallConnectionId()) : new StartRecordingOptions(locator); + + options.setRecordingContent(RecordingContent.AUDIO); + options.setRecordingFormat(RecordingFormat.WAV); + options.setRecordingChannel(RecordingChannel.UNMIXED); + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = "mp4"; + + recordingId = client.getCallRecording().start(options).getRecordingId(); + log.info("Recording started. RecordingId: {}", recordingId); + } + + @PostMapping("/pauseRecordingAsync") + public void pauseRecordingAsync() { + client.getCallRecording().pauseWithResponse(recordingId, null); + } + + @PostMapping("/pauseRecording") + public void pauseRecording() { + client.getCallRecording().pause(recordingId); + } + + @PostMapping("/resumeRecordingAsync") + public void resumeRecordingAsync() { + client.getCallRecording().resumeWithResponse(recordingId, null); + } + + @PostMapping("/resumeRecording") + public void resumeRecording() { + client.getCallRecording().resume(recordingId); + } + + @PostMapping("/stopRecordingAsync") + public void stopRecordingAsync() { + client.getCallRecording().stopWithResponse(recordingId,null); + } + + @PostMapping("/stopRecording") + public void stopRecording() { + client.getCallRecording().stop(recordingId); + } + +@GetMapping("/downloadRecording") +public void downloadRecording() { + if (recordingLocation != null && !recordingLocation.isEmpty()) { + String downloadsPath = System.getProperty("user.home") + "/Downloads"; + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + String fileName = "Recording_" + timestamp + "." + recordingFileFormat; + String filePath = downloadsPath + "/" + fileName; + + try (OutputStream outputStream = new FileOutputStream(filePath)) { + client.getCallRecording().downloadTo(recordingLocation, outputStream); + log.info("Recording downloaded to: {}", filePath); + } catch (IOException e) { + log.error("Error while downloading recording", e); + } + } else { + log.error("Recording is not available"); + } +} + // + @PostMapping("/cancelAllMediaOperationAsync") + public ResponseEntity cancelAllMediaOperationAsync() { + try { + CallMedia callMedia = getCallMedia(); + // Simulate async operation in Java (can use CompletableFuture or similar if truly async) + CompletableFuture.runAsync(() -> { + try { + callMedia.cancelAllMediaOperationsWithResponse(Context.NONE); // If reactive + } catch (Exception e) { + log.error("Failed to cancel media operations asynchronously", e); + } + }); + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + log.error("Error during async cancel", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @PostMapping("/cancelAllMediaOperation") + public ResponseEntity cancelAllMediaOperation() { + try { + CallMedia callMedia = getCallMedia(); + callMedia.cancelAllMediaOperations(); // synchronous method + return ResponseEntity.ok().build(); + } catch (Exception ex) { + log.error("Error during cancel", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + // + + @PostMapping("/startDialogAsync") + public ResponseEntity startDialogAsync() { + try { + Map dialogContext = new HashMap<>(); + + PhoneNumberIdentifier target = new PhoneNumberIdentifier(calleeId); + PhoneNumberIdentifier sourceCallerId = new PhoneNumberIdentifier(callerId); + + String callbackUri = callbackUriHost + "/api/callbacks"; + CallInvite callInvite = new CallInvite(target, sourceCallerId); + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri); + + CreateCallResult createCallResult = client + .createCallWithResponse(createCallOptions, Context.NONE) + .getValue(); + + CallConnection callConnection = getConnection(); // You need to implement this + CallDialog callDialog = callConnection.getCallDialog(); + + log.info("Async call created with ID: {}", createCallResult.getCallConnectionProperties().getCallConnectionId()); + + String botAppId = botRouting.getOrDefault(calleeId, defaultBotId); + + // Create DialogInputType implementation + // PowerVirtualAgentsDialog dialogInputType = new PowerVirtualAgentsDialog() + // .setBotAppId(botAppId) + // .setContext(dialogContext); + + DialogInputType dialogInputType = new DialogInputType(); + // .setBotAppId(botAppId) + // .setContext(dialogContext); + + // FIX: Add a unique dialogId + String dialogId = UUID.randomUUID().toString(); + + StartDialogOptions dialogOptions = new StartDialogOptions(dialogId, dialogInputType, dialogContext); + dialogOptions.setOperationContext("DialogStart"); + + CompletableFuture.runAsync(() -> { + try { + Response response = callDialog.startDialogWithResponse(dialogOptions, Context.NONE); + DialogStateResult result = response.getValue(); + int statusCode = response.getStatusCode(); + + log.info("Dialog started asynchronously. Status code: {}, Result: {}", statusCode, result); + } catch (Exception e) { + log.error("Error starting dialog async", e); + } + }); + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + log.error("Error in /startDialogAsync", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @PostMapping("/startDialog") + public ResponseEntity startDialog() { + try { + Map dialogContext = new HashMap<>(); + + PhoneNumberIdentifier target = new PhoneNumberIdentifier(calleeId); + PhoneNumberIdentifier sourceCallerId = new PhoneNumberIdentifier(callerId); + + String callbackUri = callbackUriHost + "/api/callbacks"; + CallInvite callInvite = new CallInvite(target, sourceCallerId); + + CreateCallResult createCallResult = client.createCall(callInvite, callbackUri); + CallConnection callConnection = getConnection(); // Implement this method + CallDialog callDialog = callConnection.getCallDialog(); + + log.info("Call created with ID: {}", createCallResult.getCallConnectionProperties().getCallConnectionId()); + + String botAppId = botRouting.getOrDefault(calleeId, defaultBotId); + // PowerVirtualAgentsDialog dialogInputType = new PowerVirtualAgentsDialog() + // .setBotAppId(botAppId) + // .setContext(dialogContext); + DialogInputType dialogInputType = new DialogInputType(); + // .setBotAppId(botAppId) + // .setContext(dialogContext); + + String dialogId = UUID.randomUUID().toString(); + StartDialogOptions dialogOptions = new StartDialogOptions(dialogId, dialogInputType, dialogContext); + dialogOptions.setOperationContext("DialogStart"); + + DialogStateResult response = callDialog.startDialog(dialogOptions); + + log.info("Dialog started synchronously. Dialog ID: {}, Dialog State: {}",response.getDialogId()); // assuming getter exists + + return ResponseEntity.ok().build(); + } catch (Exception ex) { + log.error("Error in /startDialog", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private enum TargetType { + PSTN, + ACS, + TEAMS, + ALL + } + + public CallMedia getCallMedia() { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId).getCallMedia(); + } + + public CallConnection getConnection() { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId); + } + + public CallConnectionProperties getCallConnectionProperties() { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId).getCallProperties(); + } + + private List getChoices(){ + var choices = Arrays.asList( + new RecognitionChoice().setLabel(confirmLabel).setPhrases(Arrays.asList("Confirm", "First", "One")).setTone(DtmfTone.ONE), + new RecognitionChoice().setLabel(cancelLabel).setPhrases(Arrays.asList("Cancel", "Second", "Two")).setTone(DtmfTone.TWO) + ); + return choices; + } + + private CallAutomationClient initClient() { + try { + return new CallAutomationClientBuilder() + .connectionString(appConfig.getAcsConnectionString()) + .buildClient(); + } catch (NullPointerException e) { + log.error("Please verify if Application config is properly set up"); + return null; + } catch (Exception e) { + log.error("Error occurred when initializing Call Automation Client: {} {}", e.getMessage(), e.getCause()); + return null; + } + } + + private CallAutomationAsyncClient initAsyncClient() { + CallAutomationAsyncClient client; + try { + client = new CallAutomationClientBuilder() + .connectionString(appConfig.getAcsConnectionString()) + .buildAsyncClient(); + return client; + } catch (NullPointerException e) { + log.error("Please verify if Application config is properly set up"); + return null; + } catch (Exception e) { + log.error("Error occurred when initializing Call Automation Async Client: {} {}", + e.getMessage(), + e.getCause()); + return null; + } + } +} diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/WebSocketConfig.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/WebSocketConfig.java new file mode 100644 index 0000000..20ab61f --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/WebSocketConfig.java @@ -0,0 +1,17 @@ +package com.communication.callautomation; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket // Enables WebSocket support +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(new WebSocketHandler(), "/ws") // Registering WebSocket handler + .setAllowedOrigins("*"); // Allow connections from any origin (adjust as needed) + } +} diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/WebSocketHandler.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/WebSocketHandler.java new file mode 100644 index 0000000..d124650 --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/WebSocketHandler.java @@ -0,0 +1,62 @@ +package com.communication.callautomation; + +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.springframework.web.socket.TextMessage; +import com.azure.communication.callautomation.StreamingData; +import com.azure.communication.callautomation.models.StreamingData; +import com.azure.communication.callautomation.models.TranscriptionData; +import com.azure.communication.callautomation.models.TranscriptionMetadata; +import com.azure.communication.callautomation.models.WordData; + +public class WebSocketHandler extends TextWebSocketHandler { + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String payload = message.getPayload(); + System.out.println("Received message: " + payload); + + // Parse the message into StreamingData (custom data parsing logic) + StreamingData data = StreamingData.parse(payload); + + + // Handle TranscriptionMetadata + if (data instanceof TranscriptionMetadata) { + TranscriptionMetadata transcriptionMetadata = (TranscriptionMetadata) data; + System.out.println("----------------------------------------------------------------"); + System.out.println("TRANSCRIPTION SUBSCRIPTION ID:-->" + transcriptionMetadata.getTranscriptionSubscriptionId()); + System.out.println("LOCALE:-->" + transcriptionMetadata.getLocale()); + System.out.println("CALL CONNECTION ID:-->" + transcriptionMetadata.getCallConnectionId()); + System.out.println("CORRELATION ID:-->" + transcriptionMetadata.getCorrelationId()); + System.out.println("----------------------------------------------------------------"); + } + + // Handle TranscriptionData + if (data instanceof TranscriptionData) { + TranscriptionData transcriptionData = (TranscriptionData) data; + System.out.println("----------------------------------------------------------------"); + System.out.println("TEXT:-->" + transcriptionData.getText()); + System.out.println("FORMAT:-->" + transcriptionData.getFormat()); + System.out.println("CONFIDENCE:-->" + transcriptionData.getConfidence()); + System.out.println("OFFSET:-->" + transcriptionData.getOffset()); + System.out.println("DURATION:-->" + transcriptionData.getDuration()); + + String participant = transcriptionData.getParticipant().getRawId() != null + ? transcriptionData.getParticipant().getRawId() + : ""; + System.out.println("PARTICIPANT:-->" + participant); + System.out.println("RESULT STATUS:-->" + transcriptionData.getResultState()); + + // Print word data (example of transcribed words) + for (WordData word : transcriptionData.getTranscribedWords()) { + System.out.println("TEXT:-->" + word.getText()); + System.out.println("OFFSET:-->" + word.getOffset()); + System.out.println("DURATION:-->" + word.getDuration()); + } + System.out.println("----------------------------------------------------------------"); + } + + // Send an echo response back to the client + session.sendMessage(new TextMessage("Echo: " + payload)); + } +} diff --git a/Call_Automation_GCCH/src/main/resources/application.yml b/Call_Automation_GCCH/src/main/resources/application.yml new file mode 100644 index 0000000..5a99d12 --- /dev/null +++ b/Call_Automation_GCCH/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + application: + name: CallAutomation_OutboundCalling + +server: + port: 8080 + +acs: + connectionstring: + basecallbackuri: + callerphonenumber: + targetphonenumber: + cognitiveServiceEndpoint: + targetTeamsUserId: <(OPTIONAL) YOUR TARGET TEAMS USER ID ex. "ab01bc12-d457-4995-a27b-c405ecfe4870">