diff --git a/Call_Automation_GCCH/README.md b/Call_Automation_GCCH/README.md new file mode 100644 index 0000000..564fa6f --- /dev/null +++ b/Call_Automation_GCCH/README.md @@ -0,0 +1,61 @@ +|page_type| languages |products +|---|---------------------------------------|---| +|sample|
Java
|
azureazure-communication-services
| + +# Call Automation - Quick Start Sample + +In this quickstart, we cover how you can use Call Automation SDK to make an outbound call to a phone number and use the newly announced integration with Azure AI services to play dynamic prompts to participants using Text-to-Speech and recognize user voice input through Speech-to-Text to drive business logic in your application. + +# 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..2ab1c33 --- /dev/null +++ b/Call_Automation_GCCH/pom.xml @@ -0,0 +1,200 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.6 + + + + com.communication.callautomation + Call_Automation_GCCH + 1.0-SNAPSHOT + + Call_Automation_GCCH + Call_Automation_GCCH 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 + + + + + com.microsoft.azure + applicationinsights-spring-boot-starter + 2.6.4 + + + junit + junit + 4.13.2 + test + + + com.azure + azure-core + 1.42.0 + + + com.azure + azure-identity + 1.10.4 + + + com.azure + azure-communication-identity + 1.5.0 + + + com.azure + azure-communication-callautomation + 1.5.0 + + + com.azure + azure-messaging-eventgrid + 4.16.0 + + + com.azure + azure-communication-common + 1.4.0 + + + 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 + + + org.json + json + 20210307 + + + + + 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 + + + -parameters + + + + + 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 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.2.5 + + + + repackage + + + + + + + \ No newline at end of file diff --git a/Call_Automation_GCCH/src/main/java/com/communication/CallResponse.java b/Call_Automation_GCCH/src/main/java/com/communication/CallResponse.java new file mode 100644 index 0000000..a562dce --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/CallResponse.java @@ -0,0 +1,57 @@ +package com.communication; + +public class CallResponse { + + private String callConnectionId; + private String correlationId; + private String message; + private String recordingId; + + public CallResponse() { + } + + public CallResponse(String callConnectionId, String correlationId, String message) { + this.callConnectionId = callConnectionId; + this.correlationId = correlationId; + this.message = message; + } + + public CallResponse(String callConnectionId, String correlationId, String message, String recordingId) { + this.callConnectionId = callConnectionId; + this.correlationId = correlationId; + this.message = message; + this.recordingId = recordingId; + } + + public String getCallConnectionId() { + return callConnectionId; + } + + public void setCallConnectionId(String callConnectionId) { + this.callConnectionId = callConnectionId; + } + + public String getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getRecordingId() { + return recordingId; + } + + public void setRecordingId(String recordingId) { + this.recordingId = recordingId; + } +} diff --git a/Call_Automation_GCCH/src/main/java/com/communication/ParticipantListResponse.java b/Call_Automation_GCCH/src/main/java/com/communication/ParticipantListResponse.java new file mode 100644 index 0000000..6b45f2d --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/ParticipantListResponse.java @@ -0,0 +1,105 @@ +package com.communication; + +import java.util.List; + +public class ParticipantListResponse { + + private String callConnectionId; + private String correlationId; + private String message; + private List participants; + private int participantCount; + + public ParticipantListResponse() { + } + + public ParticipantListResponse(String callConnectionId, String correlationId, String message, + List participants) { + this.callConnectionId = callConnectionId; + this.correlationId = correlationId; + this.message = message; + this.participants = participants; + this.participantCount = participants != null ? participants.size() : 0; + } + + public String getCallConnectionId() { + return callConnectionId; + } + + public void setCallConnectionId(String callConnectionId) { + this.callConnectionId = callConnectionId; + } + + public String getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public List getParticipants() { + return participants; + } + + public void setParticipants(List participants) { + this.participants = participants; + this.participantCount = participants != null ? participants.size() : 0; + } + + public int getParticipantCount() { + return participantCount; + } + + public void setParticipantCount(int participantCount) { + this.participantCount = participantCount; + } + + // Inner class for individual participant information + public static class ParticipantInfo { + private String participantId; + private boolean isOnHold; + private boolean isMuted; + + public ParticipantInfo() { + } + + public ParticipantInfo(String participantId, boolean isOnHold, boolean isMuted) { + this.participantId = participantId; + this.isOnHold = isOnHold; + this.isMuted = isMuted; + } + + public String getParticipantId() { + return participantId; + } + + public void setParticipantId(String participantId) { + this.participantId = participantId; + } + + public boolean isOnHold() { + return isOnHold; + } + + public void setOnHold(boolean onHold) { + isOnHold = onHold; + } + + public boolean isMuted() { + return isMuted; + } + + public void setMuted(boolean muted) { + isMuted = muted; + } + } +} \ No newline at end of file diff --git a/Call_Automation_GCCH/src/main/java/com/communication/ParticipantResponse.java b/Call_Automation_GCCH/src/main/java/com/communication/ParticipantResponse.java new file mode 100644 index 0000000..b8a72db --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/ParticipantResponse.java @@ -0,0 +1,72 @@ +package com.communication; + +public class ParticipantResponse { + + private String callConnectionId; + private String correlationId; + private String message; + private String participantId; + private boolean isOnHold; + private boolean isMuted; + + public ParticipantResponse() { + } + + public ParticipantResponse(String callConnectionId, String correlationId, String message, + String participantId, boolean isOnHold, boolean isMuted) { + this.callConnectionId = callConnectionId; + this.correlationId = correlationId; + this.message = message; + this.participantId = participantId; + this.isOnHold = isOnHold; + this.isMuted = isMuted; + } + + public String getCallConnectionId() { + return callConnectionId; + } + + public void setCallConnectionId(String callConnectionId) { + this.callConnectionId = callConnectionId; + } + + public String getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getParticipantId() { + return participantId; + } + + public void setParticipantId(String participantId) { + this.participantId = participantId; + } + + public boolean isOnHold() { + return isOnHold; + } + + public void setOnHold(boolean onHold) { + isOnHold = onHold; + } + + public boolean isMuted() { + return isMuted; + } + + public void setMuted(boolean muted) { + isMuted = muted; + } +} \ No newline at end of file diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/AppConfig.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/AppConfig.java new file mode 100644 index 0000000..3ae8cb6 --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/AppConfig.java @@ -0,0 +1,64 @@ +package com.communication.callautomation; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +import lombok.Getter; + +@ConfigurationProperties(prefix = "acs") +@Getter +public class AppConfig { + private final String connectionString; + private final String callbackUriHost; + private final String acsPhoneNumber; + private final String targetPhoneNumber; + private final String cognitiveServiceEndpoint; + private final String targetTeamsUserId; + + @ConstructorBinding + AppConfig(final String connectionString, + final String callbackUriHost, + final String acsPhoneNumber, + final String targetPhoneNumber, + final String cognitiveServiceEndpoint, + final String targetTeamsUserId) { + this.connectionString = connectionString; + this.callbackUriHost = callbackUriHost; + this.acsPhoneNumber = acsPhoneNumber; + this.targetPhoneNumber = targetPhoneNumber; + this.cognitiveServiceEndpoint = cognitiveServiceEndpoint; + this.targetTeamsUserId = targetTeamsUserId; + } + + public String getCallBackUri() { + return callbackUriHost + "/api/callback"; + } + + public String getCallBackUriForRecordingApis() { + return callbackUriHost + "/api/recordingcallback"; + } + + public String getCallbackUriHost() { + return this.callbackUriHost; + } + + public String getAcsPhoneNumber() { + return this.acsPhoneNumber; + } + + public String getTargetPhoneNumber() { + return this.targetPhoneNumber; + } + + public String getCognitiveServiceEndpoint() { + return this.cognitiveServiceEndpoint; + } + + public String getConnectionString() { + return this.connectionString; + } + + public String getTargetTeamsUserId() { + return this.targetTeamsUserId; + } +} 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..d1ad1c1 --- /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 = AppConfig.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..d1ae29f --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/ProgramSample.java @@ -0,0 +1,1799 @@ +package com.communication.callautomation; + +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.CallMedia; +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.PhoneNumberIdentifier; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; +import com.azure.core.util.Context; +import com.communication.CallResponse; +import com.communication.ParticipantResponse; +import com.communication.ParticipantListResponse; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.bind.annotation.*; +import org.springframework.http.*; + +import java.io.*; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; +import org.springframework.core.io.Resource; +import org.springframework.core.io.FileSystemResource; +import org.json.JSONObject; +import com.azure.messaging.eventgrid.EventGridEvent; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse; +import com.azure.messaging.eventgrid.systemevents.AcsRecordingFileStatusUpdatedEventData; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData; +import com.azure.core.util.BinaryData; +import com.azure.messaging.eventgrid.SystemEventNames; + +@RestController +public class ProgramSample { + + private static final Logger log = LoggerFactory.getLogger(ProgramSample.class); + private CallAutomationClient client; + // private final CallAutomationAsyncClient asyncClient; + + // Configuration state variables + private AppConfig appConfig; + private String acsConnectionString = ""; + private String cognitiveServicesEndpoint = ""; + private String acsPhoneNumber = ""; + private String targetPhoneNumber = ""; + private String targetAcsUserId = ""; + private String callbackUriHost = ""; + private String websocketUriHost = ""; + + private String callConnectionId = ""; + private String recordingId = ""; + private String recordingLocation = ""; + private String recordingFileFormat = ""; + + private String confirmLabel = "Confirm"; + private String cancelLabel = "Cancel"; + + // Event logging configuration + private static final String EVENTS_LOG_FILE = "call_automation_events.txt"; + private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public ProgramSample(final AppConfig appConfig) { + this.appConfig = appConfig; + + // Initialize configuration values from AppConfig + this.acsConnectionString = appConfig.getConnectionString(); + this.callbackUriHost = appConfig.getCallbackUriHost(); + this.acsPhoneNumber = appConfig.getAcsPhoneNumber(); + this.targetPhoneNumber = appConfig.getTargetPhoneNumber(); + this.cognitiveServicesEndpoint = appConfig.getCognitiveServiceEndpoint(); + this.targetAcsUserId = appConfig.getTargetTeamsUserId(); + + // Set websocketUriHost to same as callbackUriHost for now + this.websocketUriHost = appConfig.getCallbackUriHost(); + + client = initClient(); + } + + /** + * Writes event information to a text file + */ + private void writeEventToFile(CallAutomationEventBase event, String additionalInfo) { + try { + Path filePath = Paths.get(EVENTS_LOG_FILE); + String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMATTER); + + StringBuilder eventInfo = new StringBuilder(); + eventInfo.append("==========================================\n"); + eventInfo.append("Timestamp: ").append(timestamp).append("\n"); + eventInfo.append("Event Type: ").append(event.getClass().getSimpleName()).append("\n"); + eventInfo.append("Call Connection ID: ").append(event.getCallConnectionId()).append("\n"); + eventInfo.append("Server Call ID: ").append(event.getServerCallId()).append("\n"); + eventInfo.append("Correlation ID: ").append(event.getCorrelationId()).append("\n"); + + if (additionalInfo != null && !additionalInfo.isEmpty()) { + eventInfo.append("Additional Info: ").append(additionalInfo).append("\n"); + } + + eventInfo.append("==========================================\n\n"); + + // Write to file (append mode) + Files.write(filePath, eventInfo.toString().getBytes(), + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + + } catch (IOException e) { + log.error("Failed to write event to file: {}", e.getMessage()); + } + } + @Tag(name = "02. Call Automation Events", description = "CallAutomation Events") + @PostMapping(path = "/api/callbacks") + public ResponseEntity callbackEvents(@RequestBody final String reqBody) { + try { + List events = CallAutomationEventParser.parseEvents(reqBody); + for (CallAutomationEventBase event : events) { + callConnectionId = event.getCallConnectionId(); + log.info( + "Received call event callConnectionID: {}, serverCallId: {}, CorrelationId: {}, eventType: {}", + callConnectionId, + event.getServerCallId(), + event.getCorrelationId(), + event.getClass().getSimpleName()); + + // Write basic event info to file for all events + String basicEventInfo = String.format("Event: %s", event.getClass().getSimpleName()); + writeEventToFile(event, basicEventInfo); + + if (event instanceof CallConnected) { + log.info("****************************************"); + log.info("CORRELATION ID: {}", event.getCorrelationId()); + log.info("****************************************"); + log.info("CALL CONNECTION ID: {}", event.getCallConnectionId()); + log.info("****************************************"); + var mediaStreamingSubscription = client.getCallConnection(callConnectionId).getCallProperties() + .getMediaStreamingSubscription(); + var transcriptionSubscription = client.getCallConnection(callConnectionId).getCallProperties() + .getTranscriptionSubscription(); + log.info("MediaStreaming State: {}", mediaStreamingSubscription.getState()); + log.info("Transcription State: {}", transcriptionSubscription.getState()); + + // Write to file + String additionalInfo = String.format("MediaStreaming State: %s, Transcription State: %s", + mediaStreamingSubscription.getState(), + transcriptionSubscription.getState()); + writeEventToFile(event, additionalInfo); + } else if (event instanceof MediaStreamingStarted) { + MediaStreamingStarted acsEvent = (MediaStreamingStarted) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("MediaSteaming Status: {}", + acsEvent.getMediaStreamingUpdateResult().getMediaStreamingStatus()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, MediaStreaming Status: %s", + acsEvent.getOperationContext(), + acsEvent.getMediaStreamingUpdateResult().getMediaStreamingStatus()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof MediaStreamingStopped) { + MediaStreamingStopped acsEvent = (MediaStreamingStopped) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("MediaSteaming Status: {}", + acsEvent.getMediaStreamingUpdateResult().getMediaStreamingStatus()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, MediaStreaming Status: %s", + acsEvent.getOperationContext(), + acsEvent.getMediaStreamingUpdateResult().getMediaStreamingStatus()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof MediaStreamingFailed) { + MediaStreamingFailed acsEvent = (MediaStreamingFailed) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("MediaSteaming Status: {}", + acsEvent.getMediaStreamingUpdateResult().getMediaStreamingStatus()); + log.error("Received failed event: {}", acsEvent + .getResultInformation().getMessage()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, MediaStreaming Status: %s, Error: %s", + acsEvent.getOperationContext(), + acsEvent.getMediaStreamingUpdateResult().getMediaStreamingStatus(), + acsEvent.getResultInformation().getMessage()); + writeEventToFile(event, additionalInfo); + } else if (event instanceof TranscriptionStarted) { + TranscriptionStarted acsEvent = (TranscriptionStarted) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("Transcription Status: {}", + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, Transcription Status: %s", + acsEvent.getOperationContext(), + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof TranscriptionUpdated) { + TranscriptionUpdated acsEvent = (TranscriptionUpdated) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("Transcription Status: {}", + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, Transcription Status: %s", + acsEvent.getOperationContext(), + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof TranscriptionStopped) { + TranscriptionStopped acsEvent = (TranscriptionStopped) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("Transcription Status: {}", + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, Transcription Status: %s", + acsEvent.getOperationContext(), + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof TranscriptionFailed) { + TranscriptionFailed acsEvent = (TranscriptionFailed) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("Transcription Status: {}", + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus()); + log.error("Received failed event: {}", acsEvent + .getResultInformation().getMessage()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, Transcription Status: %s, Error: %s", + acsEvent.getOperationContext(), + acsEvent.getTranscriptionUpdateResult().getTranscriptionStatus(), + acsEvent.getResultInformation().getMessage()); + writeEventToFile(event, additionalInfo); + } else { + log.debug("Received unhandled event: {}", event.getClass().getSimpleName()); + // Write unhandled events to file too + writeEventToFile(event, "Unhandled event type"); + } + + if (event instanceof RecognizeCompleted) { + RecognizeCompleted acsEvent = (RecognizeCompleted) event; + RecognizeResult recognizeResult = acsEvent.getRecognizeResult().get(); + String recognitionInfo; + if (recognizeResult instanceof DtmfResult) { + // Take action on collect tones + DtmfResult dtmfResult = (DtmfResult) recognizeResult; + List tones = dtmfResult.getTones(); + log.info("Recognition completed, tones=" + tones + ", context=" + + acsEvent.getOperationContext()); + recognitionInfo = "DTMF Tones: " + tones; + } else if (recognizeResult instanceof ChoiceResult) { + ChoiceResult collectChoiceResult = (ChoiceResult) recognizeResult; + String labelDetected = collectChoiceResult.getLabel(); + String phraseDetected = collectChoiceResult.getRecognizedPhrase(); + recognitionInfo = String.format("Choice - Label: %s, Phrase: %s", labelDetected, phraseDetected); + } else if (recognizeResult instanceof SpeechResult) { + SpeechResult speechResult = (SpeechResult) recognizeResult; + String text = speechResult.getSpeech(); + log.info("Recognition completed, text=" + text + ", context=" + acsEvent.getOperationContext()); + recognitionInfo = "Speech: " + text; + } else { + log.info("Recognition completed, result=" + recognizeResult + ", context=" + + acsEvent.getOperationContext()); + recognitionInfo = "Result: " + recognizeResult; + } + + // Write to file + String additionalInfo = String.format("Operation Context: %s, Recognition Info: %s", + acsEvent.getOperationContext(), recognitionInfo); + writeEventToFile(event, additionalInfo); + } else if (event instanceof RecognizeFailed) { + + RecognizeFailed acsEvent = (RecognizeFailed) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("FailedPlaySourceIndex: {}", + acsEvent.getFailedPlaySourceIndex()); + log.error("Received failed event: {}", acsEvent + .getResultInformation().getMessage()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, FailedPlaySourceIndex: %s, Error: %s", + acsEvent.getOperationContext(), + acsEvent.getFailedPlaySourceIndex(), + acsEvent.getResultInformation().getMessage()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof PlayCompleted) { + PlayCompleted acsEvent = (PlayCompleted) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s", acsEvent.getOperationContext()); + writeEventToFile(event, additionalInfo); + } else if (event instanceof PlayFailed) { + + PlayFailed acsEvent = (PlayFailed) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.info("FailedPlaySourceIndex: {}", + acsEvent.getFailedPlaySourceIndex()); + log.error("Received failed event: {}", acsEvent + .getResultInformation().getMessage()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, FailedPlaySourceIndex: %s, Error: %s", + acsEvent.getOperationContext(), + acsEvent.getFailedPlaySourceIndex(), + acsEvent.getResultInformation().getMessage()); + writeEventToFile(event, additionalInfo); + + } else if (event instanceof RecordingStateChanged) { + RecordingStateChanged acsEvent = (RecordingStateChanged) event; + log.info("Recording State Changed event received: {}", + event.getCallConnectionId()); + log.info("Recording State: {}", acsEvent.getRecordingState()); + + // Write to file + String additionalInfo = String.format("Recording State: %s", acsEvent.getRecordingState()); + writeEventToFile(event, additionalInfo); + } else if (event instanceof CallTransferAccepted) { + CallTransferAccepted acsEvent = (CallTransferAccepted) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s", acsEvent.getOperationContext()); + writeEventToFile(event, additionalInfo); + } else if (event instanceof CallTransferFailed) { + + CallTransferFailed acsEvent = (CallTransferFailed) event; + log.info("Operation Context: {}", acsEvent.getOperationContext()); + log.error("Received failed event: {}", acsEvent + .getResultInformation().getMessage()); + + // Write to file + String additionalInfo = String.format("Operation Context: %s, Error: %s", + acsEvent.getOperationContext(), + acsEvent.getResultInformation().getMessage()); + writeEventToFile(event, additionalInfo); + } + } + return ResponseEntity.ok().body(""); + } catch (Exception e) { + log.error("Error processing callback events: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to process callback events."); + } + } + + private void handleIncomingCall(final BinaryData eventData) { + JSONObject data = new JSONObject(eventData.toString()); + String callbackUri; + AnswerCallOptions options; + String cognitiveServicesUrl; + String websocketUrl; + + try { + callbackUri = callbackUriHost + "/api/callbacks"; + // Replace "https://" with "wss://" for WebSocket protocol + websocketUrl = websocketUriHost; + System.out.println("WebSocket URL: " + websocketUrl); + MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(MediaStreamingAudioChannel.UNMIXED); + mediaStreamingOptions.setTransportUrl(websocketUrl); + mediaStreamingOptions.setStartMediaStreaming(false); + mediaStreamingOptions.setEnableDtmfTones(false); + mediaStreamingOptions.setEnableBidirectional(false); + mediaStreamingOptions.setAudioFormat(AudioFormat.PCM_16K_MONO); + + TranscriptionOptions transcriptionOptions = new TranscriptionOptions("en-ES"); + transcriptionOptions.setTransportUrl(websocketUriHost); + transcriptionOptions.setStartTranscription(false); + + options = new AnswerCallOptions(data.getString("incomingCallContext"), + callbackUri); + options.setMediaStreamingOptions(mediaStreamingOptions); + options.setTranscriptionOptions(transcriptionOptions); + + Response answerCallResponse = client.answerCallWithResponse(options, Context.NONE); + + log.info("Incoming call answered. Cognitive Services Url: {}\nCallbackUri: {}\nCallConnectionId: {}", + cognitiveServicesEndpoint, + callbackUri, + answerCallResponse.getValue().getCallConnectionProperties().getCallConnectionId()); + } catch (Exception e) { + log.error("Error getting recording location info {} {}", + e.getMessage(), + e.getCause()); + } + } + + private ResponseEntity handleSubscriptionValidation(final BinaryData eventData) { + try { + log.info("Received Subscription Validation Event from Incoming Call API endpoint"); + SubscriptionValidationEventData subscriptioneventData = eventData + .toObject(SubscriptionValidationEventData.class); + SubscriptionValidationResponse responseData = new SubscriptionValidationResponse(); + responseData.setValidationResponse(subscriptioneventData.getValidationCode()); + return ResponseEntity.ok().body(responseData); + } catch (Exception e) { + log.error("Error at subscription validation event {} {}", + e.getMessage(), + e.getCause()); + return ResponseEntity.internalServerError().body(null); + } + } + + @Tag(name = "02. Call Automation Events", description = "CallAutomation Events") + @PostMapping(path = "/api/incomingCall") + public ResponseEntity recordinApiEventGridEvents( + @RequestBody final String reqBody) { + List events = EventGridEvent.fromString(reqBody); + for (EventGridEvent eventGridEvent : events) { + if (eventGridEvent.getEventType().equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION)) { + return handleSubscriptionValidation(eventGridEvent.getData()); + } else if (eventGridEvent.getEventType().equals(SystemEventNames.COMMUNICATION_INCOMING_CALL)) { + handleIncomingCall(eventGridEvent.getData()); + } + } + return ResponseEntity.ok().body(null); + } + + @Tag(name = "02. Call Automation Events", description = "CallAutomation Events") + @PostMapping("/api/recordingFileStatus") + public ResponseEntity handleRecordingFileStatus(@RequestBody String reqBody) { + List events = EventGridEvent.fromString(reqBody); + log.info("RECORDING FILE STATUS UPDATED EVENT GRID EVENT RECEIVED."); + for (EventGridEvent eventGridEvent : events) { + if (eventGridEvent.getEventType().equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION)) { + return handleSubscriptionValidation(eventGridEvent.getData()); + } else if (eventGridEvent.getEventType() + .equals(SystemEventNames.COMMUNICATION_RECORDING_FILE_STATUS_UPDATED)) { + log.info("The event received for recording file status update"); + AcsRecordingFileStatusUpdatedEventData recordingFileStatusUpdatedEventData = eventGridEvent.getData() + .toObject(AcsRecordingFileStatusUpdatedEventData.class); + recordingLocation = recordingFileStatusUpdatedEventData.getRecordingStorageInfo().getRecordingChunks() + .get(0).getContentLocation(); + String recordingMetadataLocation = recordingFileStatusUpdatedEventData.getRecordingStorageInfo() + .getRecordingChunks() + .get(0).getMetadataLocation(); + String recordingDeleteLocation = recordingFileStatusUpdatedEventData.getRecordingStorageInfo() + .getRecordingChunks() + .get(0).getDeleteLocation(); + log.info("The recording location is : {}", recordingLocation); + log.info("The recording metadata location is : {}", recordingMetadataLocation); + log.info("The recording delete location is : {}", recordingDeleteLocation); + + } else { + log.debug("Unhandled event."); + } + } + + return ResponseEntity.ok().build(); + } + + // POST: /outboundCallAsync + @Tag(name = "03. Outbound Call APIs", description = "Outbound Call APIs") + @PostMapping("/outboundCallAsync") + public ResponseEntity outboundCallAsync(@RequestParam String target, + @RequestParam boolean isPSTN) { + + try { + String callbackUri = callbackUriHost + "/api/callbacks"; + log.info("Creating async call with callbackUri: {}", callbackUri); + if (isPSTN) { + PhoneNumberIdentifier targetParticipant = new PhoneNumberIdentifier(target); + PhoneNumberIdentifier caller = new PhoneNumberIdentifier(acsPhoneNumber); + CallInvite callInvite = new CallInvite(targetParticipant, caller); + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri); + CallIntelligenceOptions callIntelligenceOptions = new CallIntelligenceOptions(); + createCallOptions.setCallIntelligenceOptions(callIntelligenceOptions); + + log.info("Creating async call to PSTN with target: {}, caller: {}, callbackUri: {}", + targetParticipant.getRawId(), caller.getRawId(), callbackUri); + // Make async call and block to get the result + Response response = client.createCallWithResponse(createCallOptions, Context.NONE); + + if (response != null && response.getValue() != null) { + callConnectionId = response.getValue().getCallConnectionProperties().getCallConnectionId(); + String correlationId = response.getValue().getCallConnectionProperties().getCorrelationId(); + log.info("Created async pstn call with connection id: " + callConnectionId); + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Created async PSTN call successfully"); + return ResponseEntity.ok(callResponse); + } else { + log.error("Failed to create call. Response or value was null."); + CallResponse errorResponse = new CallResponse(null, null, "Failed to create call. Response or value was null."); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } else { + CommunicationUserIdentifier targetParticipant = new CommunicationUserIdentifier(target); + CallInvite callInvite = new CallInvite(targetParticipant); + + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri.toString()); + CallIntelligenceOptions callIntelligenceOptions = new CallIntelligenceOptions(); + createCallOptions.setCallIntelligenceOptions(callIntelligenceOptions); + + Response result = client.createCallWithResponse(createCallOptions, Context.NONE); + callConnectionId = result.getValue().getCallConnectionProperties().getCallConnectionId(); + String correlationId = result.getValue().getCallConnectionProperties().getCorrelationId(); + log.info("Created async call with connection id: " + callConnectionId); + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Created async call successfully"); + return ResponseEntity.ok(callResponse); + } + } catch (Exception e) { + log.error("Error creating call : {}", e.getMessage()); + CallResponse errorResponse = new CallResponse(null, null, "Failed to create call: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "03. Outbound Call APIs", description = "Outbound Call APIs") + @PostMapping("/outboundCall") + public ResponseEntity outboundCallToPstn(@RequestParam String target, + @RequestParam boolean isPSTN) { + try { + if (isPSTN) { + PhoneNumberIdentifier targetParticipant = new PhoneNumberIdentifier(target); + PhoneNumberIdentifier caller = new PhoneNumberIdentifier(acsPhoneNumber); + + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + CallInvite callInvite = new CallInvite(targetParticipant, caller); + + // ✅ Convert URI to String + CreateCallResult result = client.createCall(callInvite, callbackUri.toString()); + callConnectionId = result.getCallConnectionProperties().getCallConnectionId(); + String correlationId = result.getCallConnectionProperties().getCorrelationId(); + log.info("Created call with connection id: " + callConnectionId); + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Created PSTN call successfully"); + return ResponseEntity.ok(callResponse); + } else { + CommunicationUserIdentifier targetParticipant = new CommunicationUserIdentifier(target); + CallInvite callInvite = new CallInvite(targetParticipant); + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + + CreateCallResult result = client.createCall(callInvite, callbackUri.toString()); + callConnectionId = result.getCallConnectionProperties().getCallConnectionId(); + String correlationId = result.getCallConnectionProperties().getCorrelationId(); + log.info("Created call with connection id: " + callConnectionId); + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Created call successfully"); + return ResponseEntity.ok(callResponse); + } + } catch (Exception e) { + log.error("Error creating call : {}", e.getMessage()); + CallResponse errorResponse = new CallResponse(null, null, "Failed to create call: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "04. Disconnect Call APIs", description = "Disconnect call APIs") + @PostMapping("/hangupAsync") + public ResponseEntity hangupAsync(@RequestParam String callConnectionId, @RequestParam boolean isForEveryOne) { + try { + CallConnection callConnection = getConnection(callConnectionId); + CallConnectionProperties properties = callConnection.getCallProperties(); + String correlationId = properties.getCorrelationId(); + + callConnection.hangUpWithResponse(isForEveryOne, Context.NONE); + log.info("Call hangup requested (async) forEveryone={}", isForEveryOne); + + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Call hangup requested (async)"); + return ResponseEntity.ok(callResponse); + } catch (Exception e) { + log.error("Error hanging up call: {}", e.getMessage()); + CallResponse errorResponse = new CallResponse(callConnectionId, null, "Failed to hang up call: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "04. Disconnect Call APIs", description = "Disconnect call APIs") + @PostMapping("/hangup") + public ResponseEntity hangup(@RequestParam String callConnectionId, @RequestParam boolean isForEveryOne) { + try { + CallConnection callConnection = getConnection(callConnectionId); + CallConnectionProperties properties = callConnection.getCallProperties(); + String correlationId = properties.getCorrelationId(); + + callConnection.hangUp(isForEveryOne); + log.info("Call hangup requested (sync) forEveryone={}", isForEveryOne); + + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Call hangup requested"); + return ResponseEntity.ok(callResponse); + } catch (Exception e) { + log.error("Error hanging up call: {}", e.getMessage()); + CallResponse errorResponse = new CallResponse(callConnectionId, null, "Failed to hang up call: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "05. Hold Participant APIs", description = "Hold Participant APIs") + @PostMapping("/holdParticipantAsync") + public ResponseEntity holdParticipantAsync(@RequestParam String callConnectionId, @RequestParam String targetParticipant, + @RequestParam boolean isPSTN, @RequestParam boolean isPlaySource) { + try { + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(targetParticipant); + } else { + target = new CommunicationUserIdentifier(targetParticipant); + } + HoldOptions holdOptions = new HoldOptions(target).setOperationContext("holdUserContext"); + CallMedia callMediaService = getCallMedia(callConnectionId); + + 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)."); + } catch (Exception e) { + log.error("Error holding participant asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to hold participant asynchronously."); + } + } + + @Tag(name = "05. Hold Participant APIs", description = "Hold Participant APIs") + @PostMapping("/holdParticipant") + public ResponseEntity holdParticipant(@RequestParam String callConnectionId, @RequestParam String targetParticipant, + @RequestParam boolean isPSTN, @RequestParam boolean isPlaySource) { + try { + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(targetParticipant); + } else { + target = new CommunicationUserIdentifier(targetParticipant); + } + TextSource textSource = null; + CallMedia callMediaService = getCallMedia(callConnectionId); + + 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."); + } catch (Exception e) { + log.error("Error holding participant: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to hold participant."); + } + } + + @Tag(name = "05. Hold Participant APIs", description = "Hold Participant APIs") + @PostMapping("/unholdParticipantAsync") + public ResponseEntity unholdParticipantAsync(@RequestParam String callConnectionId, @RequestParam String targetParticipant, + @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(targetParticipant); + } else { + target = new CommunicationUserIdentifier(targetParticipant); + } + UnholdOptions unholdOptions = new UnholdOptions(target).setOperationContext("unholdUserContext"); + CallMedia callMediaService = getCallMedia(callConnectionId); + + log.info("Unhold participant asynchronously {}", targetParticipant); + return ResponseEntity.ok("Participant unheld (async)."); + } catch (Exception e) { + log.error("Error unholding participant asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to unhold participant asynchronously."); + } + } + + @Tag(name = "05. Hold Participant APIs", description = "Hold Participant APIs") + @PostMapping("/unholdParticipant") + public ResponseEntity unholdParticipant(@RequestParam String callConnectionId, @RequestParam String targetParticipant, + @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(targetParticipant); + } else { + target = new CommunicationUserIdentifier(targetParticipant); + } + CallMedia callMediaService = getCallMedia(callConnectionId); + + callMediaService.unhold(target); + log.info("Unhold participant synchronously {}", targetParticipant); + return ResponseEntity.ok("Participant unheld."); + } catch (Exception e) { + log.error("Error unholding participant: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to unhold participant."); + } + } + + @Tag(name = "06. Get Participant APIs", description = "Get Participant APIs") + @PostMapping("/getParticipantAsync") + public ResponseEntity getParticipantAsync(@RequestParam String callConnectionId, @RequestParam String targetParticipant, + @RequestParam boolean isPSTN) { + try { + CallConnection callConnection = getConnection(callConnectionId); + CallConnectionProperties properties = callConnection.getCallProperties(); + String correlationId = properties.getCorrelationId(); + + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(targetParticipant); + } else { + target = new CommunicationUserIdentifier(targetParticipant); + } + Response response = callConnection.getParticipantWithResponse( + target, + Context.NONE + ); + + CallParticipant participant = response.getValue(); + + if (participant != null) { + String participantId = participant.getIdentifier().getRawId(); + boolean isOnHold = participant.isOnHold(); + boolean isMuted = participant.isMuted(); + + log.info("Participant: --> {}", participantId); + log.info("Is Participant on hold: --> {}", isOnHold); + log.info("Is Participant muted: --> {}", isMuted); + + ParticipantResponse participantResponse = new ParticipantResponse( + callConnectionId, correlationId, "Participant found successfully", + participantId, isOnHold, isMuted + ); + return ResponseEntity.ok(participantResponse); + } else { + log.warn("No participant found for identifier: {}", targetParticipant); + ParticipantResponse errorResponse = new ParticipantResponse( + callConnectionId, correlationId, "No participant found for identifier: " + targetParticipant, + targetParticipant, false, false + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + } catch (Exception e) { + log.error("Error getting participant asynchronously: {}", e.getMessage()); + ParticipantResponse errorResponse = new ParticipantResponse( + callConnectionId, null, "Failed to get participant asynchronously: " + e.getMessage(), + targetParticipant, false, false + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "06. Get Participant APIs", description = "Get Participant APIs") + @PostMapping("/getParticipant") + public ResponseEntity getParticipant(@RequestParam String callConnectionId, @RequestParam String targetParticipant, + @RequestParam boolean isPSTN) { + try { + CallConnection callConnection = getConnection(callConnectionId); + CallConnectionProperties properties = callConnection.getCallProperties(); + String correlationId = properties.getCorrelationId(); + + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(targetParticipant); + } else { + target = new CommunicationUserIdentifier(targetParticipant); + } + CallParticipant participant = callConnection.getParticipant(target); + + if (participant != null) { + String participantId = participant.getIdentifier().getRawId(); + boolean isOnHold = participant.isOnHold(); + boolean isMuted = participant.isMuted(); + + log.info("Participant: --> {}", participantId); + log.info("Is Participant on hold: --> {}", isOnHold); + log.info("Is Participant muted: --> {}", isMuted); + + ParticipantResponse participantResponse = new ParticipantResponse( + callConnectionId, correlationId, "Participant found successfully", + participantId, isOnHold, isMuted + ); + return ResponseEntity.ok(participantResponse); + } else { + log.warn("No participant found for identifier: {}", targetParticipant); + ParticipantResponse errorResponse = new ParticipantResponse( + callConnectionId, correlationId, "No participant found for identifier: " + targetParticipant, + targetParticipant, false, false + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + } catch (Exception e) { + log.error("Error getting participant: {}", e.getMessage()); + ParticipantResponse errorResponse = new ParticipantResponse( + callConnectionId, null, "Failed to get participant: " + e.getMessage(), + targetParticipant, false, false + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "06. Get Participant APIs", description = "Get Participant APIs") + @PostMapping("/getParticipantListAsync") + public ResponseEntity getParticipantListAsync(@RequestParam String callConnectionId) { + try { + CallConnection callConnection = getConnection(callConnectionId); + CallConnectionProperties properties = callConnection.getCallProperties(); + String correlationId = properties.getCorrelationId(); + + PagedIterable participants = callConnection.listParticipants(Context.NONE); + + if (participants != null) { + List participantList = new ArrayList<>(); + + for (CallParticipant participant : participants) { + String participantId = participant.getIdentifier().getRawId(); + boolean isOnHold = participant.isOnHold(); + boolean isMuted = participant.isMuted(); + + log.info("----------------------------------------------------------------------"); + log.info("Participant: --> {}", participantId); + log.info("Is Participant on hold: --> {}", isOnHold); + log.info("Is Participant muted: --> {}", isMuted); + log.info("----------------------------------------------------------------------"); + + participantList.add(new ParticipantListResponse.ParticipantInfo(participantId, isOnHold, isMuted)); + } + + ParticipantListResponse response = new ParticipantListResponse( + callConnectionId, correlationId, "Participant list retrieved successfully", participantList + ); + return ResponseEntity.ok(response); + } else { + log.warn("No participants returned in the response."); + ParticipantListResponse emptyResponse = new ParticipantListResponse( + callConnectionId, correlationId, "No participants found", new ArrayList<>() + ); + return ResponseEntity.ok(emptyResponse); + } + } catch (Exception e) { + log.error("Error getting participant list: {}", e.getMessage()); + ParticipantListResponse errorResponse = new ParticipantListResponse( + callConnectionId, null, "Failed to get participant list: " + e.getMessage(), new ArrayList<>() + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "06. Get Participant APIs", description = "Get Participant APIs") + @PostMapping("/getParticipantList") + public ResponseEntity getParticipantList(@RequestParam String callConnectionId) { + try { + CallConnection callConnection = getConnection(callConnectionId); + CallConnectionProperties properties = callConnection.getCallProperties(); + String correlationId = properties.getCorrelationId(); + + PagedIterable participants = callConnection.listParticipants(); + + if (participants != null) { + List participantList = new ArrayList<>(); + + for (CallParticipant participant : participants) { + String participantId = participant.getIdentifier().getRawId(); + boolean isOnHold = participant.isOnHold(); + boolean isMuted = participant.isMuted(); + + log.info("----------------------------------------------------------------------"); + log.info("Participant: --> {}", participantId); + log.info("Is Participant on hold: --> {}", isOnHold); + log.info("Is Participant muted: --> {}", isMuted); + log.info("----------------------------------------------------------------------"); + + participantList.add(new ParticipantListResponse.ParticipantInfo(participantId, isOnHold, isMuted)); + } + + ParticipantListResponse response = new ParticipantListResponse( + callConnectionId, correlationId, "Participant list retrieved successfully", participantList + ); + return ResponseEntity.ok(response); + } else { + log.warn("No participants returned in the response."); + ParticipantListResponse emptyResponse = new ParticipantListResponse( + callConnectionId, correlationId, "No participants found", new ArrayList<>() + ); + return ResponseEntity.ok(emptyResponse); + } + } catch (Exception e) { + log.error("Error getting participant list: {}", e.getMessage()); + ParticipantListResponse errorResponse = new ParticipantListResponse( + callConnectionId, null, "Failed to get participant list: " + e.getMessage(), new ArrayList<>() + ); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "07. Mute Participant APIs", description = "Mute Participant APIs") + @PostMapping("/muteParticipantAsync") + public ResponseEntity muteParticipantAsync(@RequestParam String callConnectionId, @RequestParam String targetAcsUserId) { + try { + CommunicationIdentifier target = new CommunicationUserIdentifier(targetAcsUserId); + CallConnection callConnection = getConnection(callConnectionId); + + 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 participant asynchronously: {}", targetAcsUserId); + return ResponseEntity.ok("Muted participant (async)."); + } catch (Exception e) { + log.error("Error muting participant asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to mute participant asynchronously."); + } + } + + @Tag(name = "07. Mute Participant APIs", description = "Mute Participant APIs") + @PostMapping("/muteParticipant") + public ResponseEntity muteParticipant(@RequestParam String callConnectionId, @RequestParam String targetAcsUserId) { + try { + CommunicationIdentifier target = new CommunicationUserIdentifier(targetAcsUserId); + CallConnection callConnection = getConnection(callConnectionId); + + callConnection.muteParticipant(target); // Synchronous mute using options if method is available + log.info("Muted participant synchronously: {}", targetAcsUserId); + return ResponseEntity.ok("Muted participant."); + } catch (Exception e) { + log.error("Error muting participant: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to mute participant."); + } + } + + @Tag(name = "08. Add/Remove Participant APIs", description = "Add/Remove Participant APIs") + @PostMapping("/addParticipantAsync") + public ResponseEntity addParticipantAsync(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + CallConnection callConnectionService = getConnection(callConnectionId); + CommunicationIdentifier target; + CallInvite callInvite; + if (isPSTN) { + PhoneNumberIdentifier phoneTarget = new PhoneNumberIdentifier(participant); + PhoneNumberIdentifier caller = new PhoneNumberIdentifier(acsPhoneNumber); + callInvite = new CallInvite(phoneTarget, caller); + } else { + CommunicationUserIdentifier userTarget = new CommunicationUserIdentifier(participant); + callInvite = new CallInvite(userTarget); + } + AddParticipantOptions options = new AddParticipantOptions(callInvite); + options.setOperationContext("addPstnUserContext"); + options.setInvitationTimeout(Duration.ofSeconds(15)); + Response result = callConnectionService.addParticipantWithResponse(options, Context.NONE); + return ResponseEntity.ok("Invitation Id: " + result.getValue().getInvitationId()); + } + + @Tag(name = "08. Add/Remove Participant APIs", description = "Add/Remove Participant APIs") + @PostMapping("/addParticipant") + public ResponseEntity addParticipant(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + CallConnection callConnectionService = getConnection(callConnectionId); + CommunicationIdentifier target; + CallInvite callInvite; + if (isPSTN) { + PhoneNumberIdentifier phoneTarget = new PhoneNumberIdentifier(participant); + PhoneNumberIdentifier caller = new PhoneNumberIdentifier(acsPhoneNumber); + callInvite = new CallInvite(phoneTarget, caller); + } else { + CommunicationUserIdentifier userTarget = new CommunicationUserIdentifier(participant); + callInvite = new CallInvite(userTarget); + }// Replace with actual ACS number + AddParticipantResult result = callConnectionService.addParticipant(callInvite); + return ResponseEntity.ok("Invitation Id: " + result.getInvitationId()); + } + + @Tag(name = "08. Add/Remove Participant APIs", description = "Add/Remove Participant APIs") + @PostMapping("/removeParticipantAsync") + public ResponseEntity removeParticipantAsync(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + RemoveParticipantOptions options; + if (isPSTN) { + options = new RemoveParticipantOptions(new PhoneNumberIdentifier(participant)); + } else { + options = new RemoveParticipantOptions(new CommunicationUserIdentifier(participant)); + } + options.setOperationContext("removeParticipantContext"); + CallConnection callConnectionService = getConnection(callConnectionId); + callConnectionService.removeParticipantWithResponse(options, Context.NONE); + return ResponseEntity.ok().build(); + } catch (Exception e) { + log.error("Error removing participant asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Tag(name = "08. Add/Remove Participant APIs", description = "Add/Remove Participant APIs") + @PostMapping("/removeParticipant") + public ResponseEntity removeParticipant(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CallConnection callConnectionService = getConnection(callConnectionId); + RemoveParticipantOptions options; + if (isPSTN) { + options = new RemoveParticipantOptions(new PhoneNumberIdentifier(participant)); + } else { + options = new RemoveParticipantOptions(new CommunicationUserIdentifier(participant)); + } + callConnectionService.removeParticipantWithResponse(options, Context.NONE); + return ResponseEntity.ok().build(); + } catch (Exception e) { + log.error("Error removing participant: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to remove participant."); + } + } + + @Tag(name = "09. Transfer Call APIs", description = "APIs for transferring calls to participants") + @PostMapping("/transferCallToParticipantAsync") + public ResponseEntity transferCallToParticipantAsync(@RequestParam String callConnectionId, @RequestParam String transferTarget, @RequestParam String transferee, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if (isPSTN) { + target = new PhoneNumberIdentifier(transferTarget); + TransferCallToParticipantOptions options = new TransferCallToParticipantOptions(target) + .setOperationContext("TransferCallContext") + .setSourceCallerIdNumber(new PhoneNumberIdentifier(transferee)); + client.getCallConnection(callConnectionId) + .transferCallToParticipantWithResponse(options, Context.NONE); + log.info("Call transferred asynchronously to participant: {}", transferTarget); + return ResponseEntity.ok("Transfer call request sent asynchronously."); + } else { + target = new CommunicationUserIdentifier(transferTarget); + TransferCallToParticipantOptions options = new TransferCallToParticipantOptions(target) + .setOperationContext("TransferCallContext"); + client.getCallConnection(callConnectionId) + .transferCallToParticipantWithResponse(options, Context.NONE); + log.info("Call transferred asynchronously to participant: {}", transferTarget); + return ResponseEntity.ok("Transfer call request sent asynchronously."); + } + } catch (Exception e) { + log.error("Error transferring call asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to transfer call asynchronously."); + } + } + + @Tag(name = "09. Transfer Call APIs", description = "APIs for transferring calls to participants") + @PostMapping("/transferCallToParticipant") + public ResponseEntity transferCallToParticipant(@RequestParam String callConnectionId, @RequestParam String transferTarget, @RequestParam String transferee, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier targetParticipant; + if (isPSTN) { + targetParticipant = new PhoneNumberIdentifier(transferTarget); + TransferCallToParticipantOptions options = new TransferCallToParticipantOptions(targetParticipant) + .setOperationContext("TransferCallContext") + .setSourceCallerIdNumber(new PhoneNumberIdentifier(transferee)); + + // Transfer the call + client.getCallConnection(callConnectionId) + .transferCallToParticipantWithResponse(options, Context.NONE); + + + log.info("Call transferred to participant: {}", targetParticipant); + return ResponseEntity.ok("Transfer call request sent."); + } else { + targetParticipant = new CommunicationUserIdentifier(transferTarget); + // Synchronous transfer using CommunicationIdentifier + client.getCallConnection(callConnectionId) + .transferCallToParticipant(targetParticipant); + log.info("Call transferred to participant: {}", transferTarget); + return ResponseEntity.ok("Transfer call request sent."); + } + } catch (Exception e) { + log.error("Error transferring call : {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to transfer call."); + } + } + + @Tag(name = "10. Play Media APIs", description = "Play Media APIs") + @PostMapping("/playFileSourceToTargetAsync") + public ResponseEntity playFileSourceToTargetAsync(@RequestParam String callConnectionId, + @RequestParam String participant, + @RequestParam boolean isPSTN) { + return playFile(callConnectionId, participant, isPSTN, true, false, false); + } + + // 4. - Sync + @Tag(name = "10. Play Media APIs", description = "Play Media APIs") + @PostMapping("/playFileSourceToTarget") + public ResponseEntity playFileSourceToTarget(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + // Use the correct argument list for playFile: (callConnectionId, target, isPSTN, async, isPlayToAll) + return playFile(callConnectionId, participant, isPSTN, false, false, false); + } + + @Tag(name = "10. Play Media APIs", description = "Play Media APIs") + @PostMapping("/playFileSourceToAllAsync") + public ResponseEntity playFileSourceToAllAsync(@RequestParam String callConnectionId) { + return playFile(callConnectionId, null, false, true, false, true); + } + + // 8. All - Sync + @Tag(name = "10. Play Media APIs", description = "Play Media APIs") + @PostMapping("/playFileSourceToAll") + public ResponseEntity playFileSourceToAll(@RequestParam String callConnectionId) { + return playFile(callConnectionId, null, false, false, false, true); + } + + // 9. Barge-In - Async + @Tag(name = "10. Play Media APIs", description = "Play Media APIs") + @PostMapping("/playFileSourceBargeInAsync") + public ResponseEntity playFileSourceBargeInAsync(@RequestParam String callConnectionId) { + return playFile(callConnectionId, null, false, true, true, true); + } + + @Tag(name = "11. Start Recognition APIs", description = "Start Recognition APIs") + @PostMapping("/recognizeDTMFAsync") + public ResponseEntity recognizeDTMFAsync(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + return startDtmfRecognition(callConnectionId, participant, isPSTN, true); + } + + @Tag(name = "11. Start Recognition APIs", description = "Start Recognition APIs") + @PostMapping("/recognizeDTMF") + public ResponseEntity recognizeDTMF(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + return startDtmfRecognition(callConnectionId, participant, isPSTN, false); + } + + // Async Equivalent: /sendDTMFTonesAsync (C#) + @Tag(name = "12. Send or Start DTMF APIs", description = "Send or Start DTMF APIs") + @PostMapping("/sendDTMFTonesAsync") + public ResponseEntity sendDTMFTonesAsync(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if(isPSTN) { + target = new PhoneNumberIdentifier(participant); + } else { + target = new CommunicationUserIdentifier(participant); + } + List tones = Arrays.asList(DtmfTone.ZERO, DtmfTone.ONE); + CallMedia callMediaService = getCallMedia(callConnectionId); + callMediaService.sendDtmfTones(tones, target); // .block() internally + + log.info("Async DTMF tones sent to {}", targetAcsUserId); + return ResponseEntity.ok("DTMF tones sent (async simulation)."); + } catch (Exception e) { + log.error("Error sending DTMF tones to {}: {}", targetAcsUserId, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending DTMF tones"); + } + } + + // Sync Equivalent: /sendDTMFTones (C#) + @Tag(name = "12. Send or Start DTMF APIs", description = "Send or Start DTMF APIs") + @PostMapping("/sendDTMFTones") + public ResponseEntity sendDTMFTones(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if(isPSTN) { + target = new PhoneNumberIdentifier(participant); + } else { + target = new CommunicationUserIdentifier(participant); + } + List tones = Arrays.asList(DtmfTone.ZERO, DtmfTone.ONE); + CallMedia callMediaService = getCallMedia(callConnectionId); + callMediaService.sendDtmfTones(tones, target); + + log.info("DTMF tones sent to {}", targetAcsUserId); + return ResponseEntity.ok("DTMF tones sent."); + } catch (Exception e) { + log.error("Error sending DTMF tones to {}: {}", targetAcsUserId, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending DTMF tones"); + } + } + + // Async Equivalent: /startContinuousDTMFTonesAsync (C#) + @Tag(name = "12. Send or Start DTMF APIs", description = "Send or Start DTMF APIs") + @PostMapping("/startContinuousDTMFTonesAsync") + public ResponseEntity startContinuousDTMFTonesAsync(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if(isPSTN) { + target = new PhoneNumberIdentifier(participant); + } else { + target = new CommunicationUserIdentifier(participant); + } + CallMedia callMediaService = getCallMedia(callConnectionId); + callMediaService.startContinuousDtmfRecognition(target); // .block() internally + + log.info("Async continuous DTMF started for {}", participant); + return ResponseEntity.ok("Started continuous DTMF recognition (async simulation)."); + } catch (Exception e) { + log.error("Error starting continuous DTMF recognition for {}: {}", participant, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error starting continuous DTMF recognition"); + } + } + + // Sync Equivalent: /startContinuousDTMFTones (C#) + @Tag(name = "12. Send or Start DTMF APIs", description = "Send or Start DTMF APIs") + @PostMapping("/startContinuousDTMFTones") + public ResponseEntity startContinuousDTMFTones(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if(isPSTN) { + target = new PhoneNumberIdentifier(participant); + } else { + target = new CommunicationUserIdentifier(participant); + } + CallMedia callMediaService = getCallMedia(callConnectionId); + callMediaService.startContinuousDtmfRecognition(target); + + log.info("Started continuous DTMF for {}", participant); + return ResponseEntity.ok("Started continuous DTMF recognition."); + } catch (Exception e) { + log.error("Error starting continuous DTMF recognition for {}: {}", participant, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error starting continuous DTMF recognition"); + } + } + + // Async Equivalent: /stopContinuousDTMFTonesAsync (C#) + @Tag(name = "12. Send or Start DTMF APIs", description = "Send or Start DTMF APIs") + @PostMapping("/stopContinuousDTMFTonesAsync") + public ResponseEntity stopContinuousDTMFTonesAsync(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if(isPSTN) { + target = new PhoneNumberIdentifier(participant); + } else { + target = new CommunicationUserIdentifier(participant); + } + CallMedia callMediaService = getCallMedia(callConnectionId); + callMediaService.stopContinuousDtmfRecognition(target); // .block() internally + + log.info("Async stop continuous DTMF for {}", participant); + return ResponseEntity.ok("Stopped continuous DTMF recognition (async simulation)."); + } catch (Exception e) { + log.error("Error stopping continuous DTMF recognition for {}: {}", participant, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error stopping continuous DTMF recognition"); + } + } + + // Sync Equivalent: /stopContinuousDTMFTones (C#) + @Tag(name = "12. Send or Start DTMF APIs", description = "Send or Start DTMF APIs") + @PostMapping("/stopContinuousDTMFTones") + public ResponseEntity stopContinuousDTMFTones(@RequestParam String callConnectionId, @RequestParam String participant, @RequestParam boolean isPSTN) { + try { + CommunicationIdentifier target; + if(isPSTN) { + target = new PhoneNumberIdentifier(participant); + } else { + target = new CommunicationUserIdentifier(participant); + } + CallMedia callMediaService = getCallMedia(callConnectionId); + callMediaService.stopContinuousDtmfRecognition(target); + + log.info("Stopped continuous DTMF for {}", participant); + return ResponseEntity.ok("Stopped continuous DTMF recognition."); + } catch (Exception e) { + log.error("Error stopping continuous DTMF recognition for {}: {}", participant, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error stopping continuous DTMF recognition"); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/startRecordingAsync") + public ResponseEntity startRecordingAsync(@RequestParam String callConnectionId, @RequestParam boolean isAudioVideo, + @RequestParam String recordingFormat, @RequestParam boolean isMixed, + @RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + return startRecording(callConnectionId, isAudioVideo, recordingFormat, isMixed, isRecordingWithCallConnectionId, isPauseOnStart, true); + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/startRecording") + public ResponseEntity startRecording(@RequestParam String callConnectionId, @RequestParam boolean isAudioVideo, + @RequestParam String recordingFormat, @RequestParam boolean isMixed, + @RequestParam boolean isRecordingWithCallConnectionId, @RequestParam boolean isPauseOnStart) { + return startRecording(callConnectionId, isAudioVideo, recordingFormat, isMixed, isRecordingWithCallConnectionId, isPauseOnStart, false); + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/pauseRecordingAsync") + public ResponseEntity pauseRecordingAsync(@RequestParam String recordingId) { + try { + client.getCallRecording().pauseWithResponse(recordingId, null); + log.info("Paused recording for {}", recordingId); + CallResponse response = new CallResponse(null, null, "Recording paused successfully", recordingId); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("Error pausing recording for {}: {}", recordingId, e.getMessage()); + CallResponse errorResponse = new CallResponse(null, null, "Error pausing recording: " + e.getMessage(), recordingId); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/pauseRecording") + public ResponseEntity pauseRecording(@RequestParam String recordingId ) { + try { + client.getCallRecording().pause(recordingId); + log.info("Paused recording for {}", recordingId); + CallResponse response = new CallResponse(null, null, "Recording paused successfully", recordingId); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("Error pausing recording for {}: {}", recordingId, e.getMessage()); + CallResponse errorResponse = new CallResponse(null, null, "Error pausing recording: " + e.getMessage(), recordingId); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/resumeRecordingAsync") + public ResponseEntity resumeRecordingAsync(@RequestParam String recordingId) { + try { + client.getCallRecording().resumeWithResponse(recordingId, null); + log.info("Resumed recording for {}", recordingId); + CallResponse response = new CallResponse(null, null, "Recording resumed successfully", recordingId); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("Error resuming recording for {}: {}", recordingId, e.getMessage()); + CallResponse errorResponse = new CallResponse(null, null, "Error resuming recording: " + e.getMessage(), recordingId); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/resumeRecording") + public ResponseEntity resumeRecording(@RequestParam String recordingId) { + try { + client.getCallRecording().resume(recordingId); + log.info("Resumed recording for {}", recordingId); + CallResponse response = new CallResponse(null, null, "Recording resumed successfully", recordingId); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("Error resuming recording for {}: {}", recordingId, e.getMessage()); + CallResponse errorResponse = new CallResponse(null, null, "Error resuming recording: " + e.getMessage(), recordingId); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/stopRecordingAsync") + public ResponseEntity stopRecordingAsync(@RequestParam String recordingId) { + try { + client.getCallRecording().stopWithResponse(recordingId, null); + log.info("Stopped recording for {}", recordingId); + return ResponseEntity.ok("Recording stopped successfully."); + } catch (Exception e) { + log.error("Error stopping recording for {}: {}", recordingId, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error stopping recording"); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @PostMapping("/stopRecording") + public ResponseEntity stopRecording(@RequestParam String recordingId) { + try { + client.getCallRecording().stop(recordingId); + log.info("Stopped recording for {}", recordingId); + return ResponseEntity.ok("Recording stopped successfully."); + } catch (Exception e) { + log.error("Error stopping recording for {}: {}", recordingId, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error stopping recording"); + } + } + + @Tag(name = "13. Recording APIs", description = "Recording APIs") + @GetMapping("/downloadRecording") + public ResponseEntity downloadRecording() { + try { + 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"); + } + return ResponseEntity.ok("Recording downloaded successfully."); + } catch (Exception e) { + log.error("Error while downloading recording", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error downloading recording"); + } + } + + @Tag(name = "14. Cancel All Media Operation APIs", description = "Cancel All Media Operation APIs") + @PostMapping("/cancelAllMediaOperationAsync") + public ResponseEntity cancelAllMediaOperationAsync(@RequestParam String callConnectionId) { + try { + CallMedia callMedia = getCallMedia(callConnectionId); + // 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(); + } + } + + @Tag(name = "14. Cancel All Media Operation APIs", description = "Cancel All Media Operation APIs") + @PostMapping("/cancelAllMediaOperation") + public ResponseEntity cancelAllMediaOperation(@RequestParam String callConnectionId) { + try { + CallMedia callMedia = getCallMedia(callConnectionId); + 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(); + } + } + + @Tag(name = "15. Media Streaming APIs", description = "Media Streaming APIs") + @PostMapping("/createCallWithMediaStreamingAsync") + public ResponseEntity createCallWithMediaStreamingAsync(@RequestParam String targetParticipant, @RequestParam boolean isPSTN) { + try { + CallInvite callInvite; + if(isPSTN) { + PhoneNumberIdentifier target = new PhoneNumberIdentifier(targetParticipant); + callInvite = new CallInvite(target, new PhoneNumberIdentifier(acsPhoneNumber)); + } else { + CommunicationUserIdentifier target = new CommunicationUserIdentifier(targetParticipant); + callInvite = new CallInvite(target); + } + + URI callbackUri = URI.create(callbackUriHost + "/api/callbacks"); + String websocketUri = websocketUriHost.replace("https", "wss") + "/ws"; + + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri.toString()); + CallIntelligenceOptions callIntelligenceOptions = new CallIntelligenceOptions(); + + MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(MediaStreamingAudioChannel.UNMIXED, StreamingTransport.WEBSOCKET); + createCallOptions.setCallIntelligenceOptions(callIntelligenceOptions); + createCallOptions.setMediaStreamingOptions(mediaStreamingOptions); + + Response result = client.createCallWithResponse(createCallOptions, Context.NONE); + callConnectionId = result.getValue().getCallConnectionProperties().getCallConnectionId(); + log.info("Created async call with connection id: " + callConnectionId); + return ResponseEntity.ok("Created async call with connection id: " + callConnectionId); + } catch (Exception e) { + log.error("Error creating call : {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to create call."); + } + } + + @Tag(name = "15. Media Streaming APIs", description = "Media Streaming APIs") + @PostMapping("/startMediaStreamingAsync") + public ResponseEntity startMediaStreamingAsync(@RequestParam String callConnectionId) { + try { + StartMediaStreamingOptions mediaStreamingOptions = new StartMediaStreamingOptions(); + + CallMedia callMedia = getCallMedia(callConnectionId); + callMedia.startMediaStreamingWithResponse(mediaStreamingOptions, Context.NONE); + + log.info("Started media streaming asynchronously for call: {}", callConnectionId); + return ResponseEntity.ok("Media streaming started successfully."); + } catch (Exception e) { + log.error("Error starting media streaming asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to start media streaming."); + } + } + + @Tag(name = "15. Media Streaming APIs", description = "Media Streaming APIs") + @PostMapping("/startMediaStreaming") + public ResponseEntity startMediaStreaming(@RequestParam String callConnectionId) { + try { + CallMedia callMedia = getCallMedia(callConnectionId); + callMedia.startMediaStreaming(); + + log.info("Started media streaming asynchronously for call: {}", callConnectionId); + return ResponseEntity.ok("Media streaming started successfully."); + } catch (Exception e) { + log.error("Error starting media streaming asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to start media streaming."); + } + } + + @Tag(name = "15. Media Streaming APIs", description = "Media Streaming APIs") + @PostMapping("/stopMediaStreamingAsync") + public ResponseEntity stopMediaStreamingAsync(@RequestParam String callConnectionId) { + try { + StopMediaStreamingOptions stopOptions = new StopMediaStreamingOptions(); + CallMedia callMedia = getCallMedia(callConnectionId); + callMedia.stopMediaStreamingWithResponse(stopOptions, Context.NONE); + + log.info("Stopped media streaming asynchronously for call: {}", callConnectionId); + return ResponseEntity.ok("Media streaming stopped successfully."); + } catch (Exception e) { + log.error("Error stopping media streaming asynchronously: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to stop media streaming."); + } + } + + @Tag(name = "15. Media Streaming APIs", description = "Media Streaming APIs") + @PostMapping("/stopMediaStreaming") + public ResponseEntity stopMediaStreaming(@RequestParam String callConnectionId) { + try { + CallMedia callMedia = getCallMedia(callConnectionId); + callMedia.stopMediaStreaming(); + + log.info("Stopped media streaming for call: {}", callConnectionId); + return ResponseEntity.ok("Media streaming stopped successfully."); + } catch (Exception e) { + log.error("Error stopping media streaming: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to stop media streaming."); + } + } + + // 🔄 Shared Method + private CallMedia getCallMedia(String callConnectionId) { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId).getCallMedia(); + } + + private CallConnection getConnection(String callConnectionId) { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId); + } + + private CallConnectionProperties getCallConnectionProperties(String callConnectionId) { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId).getCallProperties(); + } + + private ResponseEntity playFile(String callConnectionId, String target, boolean isPSTN, boolean async, boolean bargeIn, boolean isPlayToAll) { + try { + log.info(callbackUriHost); + PlaySource fileSource = new FileSource().setUrl(callbackUriHost + "/prompt.wav").setPlaySourceCacheId("prompt.wav"); + String context = bargeIn ? "playBargeInContext" : "playContext"; + CallMedia mediaService = getCallMedia(callConnectionId); + + if (isPlayToAll) { + 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; + if(isPSTN){ + playTo = List.of(new PhoneNumberIdentifier(target)); + } else { + playTo = List.of(new CommunicationUserIdentifier(target)); + } + + PlayOptions options = new PlayOptions(fileSource, playTo); + options.setOperationContext(context); + + if (async) { + mediaService.play(fileSource, playTo); + } else { + mediaService.playWithResponse(options, Context.NONE); + } + } + log.info("Successfully played file source to target: {}", target); + return ResponseEntity.ok("Successfully played file source."); + } catch (Exception ex) { + log.error("Error playing file source: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private ResponseEntity startDtmfRecognition(String callConnectionId, String target, boolean isPSTN, boolean async) { + try { + CallMedia callMedia = getCallMedia(callConnectionId); + 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 + + CommunicationIdentifier participant; + if (isPSTN) { + participant = new PhoneNumberIdentifier(target); + } else { + participant = new CommunicationUserIdentifier(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 + } + log.info("Started DTMF recognition for target: {}", target); + return ResponseEntity.ok("Started DTMF recognition."); + } catch (Exception ex) { + log.error("Error starting DTMF recognition: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + private ResponseEntity startRecording(String callConnectionId, boolean isAudioVideo, + String recordingFormat, boolean isMixed, + boolean isRecordingWithCallConnectionId, boolean isPauseOnStart, boolean async) { + try { + CallConnectionProperties properties = getCallConnectionProperties(callConnectionId); + CallLocator locator = new ServerCallLocator(properties.getServerCallId()); + String correlationId = properties.getCorrelationId(); + String eventCallbackUri = callbackUriHost + "/api/callbacks"; + if(isRecordingWithCallConnectionId){ + StartRecordingOptions options = new StartRecordingOptions(callConnectionId); + + options.setRecordingContent(isAudioVideo?RecordingContent.AUDIO_VIDEO:RecordingContent.AUDIO); + if(recordingFormat.equalsIgnoreCase("mp3")){ + options.setRecordingFormat(RecordingFormat.MP3); + } else if(recordingFormat.equalsIgnoreCase("wav")){ + options.setRecordingFormat(RecordingFormat.WAV); + } else { + options.setRecordingFormat(RecordingFormat.MP4); + } + if(isMixed){ + options.setRecordingChannel(RecordingChannel.MIXED); + } else { + options.setRecordingChannel(RecordingChannel.UNMIXED); + } + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = recordingFormat.toLowerCase(); + + if(async){ + Response response = client.getCallRecording() + .startWithResponse(options, Context.NONE); + recordingId = response.getValue().getRecordingId(); + } else { + recordingId = client.getCallRecording().start(options).getRecordingId(); + } + log.info("Recording started. RecordingId: {}", recordingId); + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Recording started successfully", recordingId); + return ResponseEntity.ok(callResponse); + } else { + StartRecordingOptions options = new StartRecordingOptions(locator); + + options.setRecordingContent(isAudioVideo?RecordingContent.AUDIO_VIDEO:RecordingContent.AUDIO); + if(recordingFormat.equalsIgnoreCase("mp3")){ + options.setRecordingFormat(RecordingFormat.MP3); + } else if(recordingFormat.equalsIgnoreCase("wav")){ + options.setRecordingFormat(RecordingFormat.WAV); + } else { + options.setRecordingFormat(RecordingFormat.MP4); + } + if(isMixed){ + options.setRecordingChannel(RecordingChannel.MIXED); + } else { + options.setRecordingChannel(RecordingChannel.UNMIXED); + } + options.setRecordingStateCallbackUrl(eventCallbackUri); + options.setPauseOnStart(isPauseOnStart); + recordingFileFormat = recordingFormat.toLowerCase(); + + if(async){ + Response response = client.getCallRecording() + .startWithResponse(options, Context.NONE); + recordingId = response.getValue().getRecordingId(); + } else { + recordingId = client.getCallRecording().start(options).getRecordingId(); + } + log.info("Recording started. RecordingId: {}", recordingId); + CallResponse callResponse = new CallResponse(callConnectionId, correlationId, "Recording started successfully", recordingId); + return ResponseEntity.ok(callResponse); + } + } catch (Exception e) { + log.error("Error starting recording for {}: {}", recordingId, e.getMessage()); + CallResponse errorResponse = new CallResponse(callConnectionId, null, "Error starting recording: " + e.getMessage(), recordingId); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + } + + /** + * API to download the events log file + */ + @Tag(name = "13. Event Log APIs", description = "APIs for managing event logs") + @GetMapping("/api/download-events-log") + public ResponseEntity downloadEventsLog() { + try { + Path filePath = Paths.get(EVENTS_LOG_FILE); + + if (!Files.exists(filePath)) { + log.warn("Events log file does not exist: {}", EVENTS_LOG_FILE); + return ResponseEntity.notFound().build(); + } + + Resource resource = new FileSystemResource(filePath.toFile()); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + EVENTS_LOG_FILE + "\"") + .header(HttpHeaders.CONTENT_TYPE, "text/plain") + .body(resource); + + } catch (Exception e) { + log.error("Error downloading events log file: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * API to get the current size and info about the events log file + */ + @Tag(name = "13. Event Log APIs", description = "APIs for managing event logs") + @GetMapping("/api/events-log-info") + public ResponseEntity getEventsLogInfo() { + try { + Path filePath = Paths.get(EVENTS_LOG_FILE); + + if (!Files.exists(filePath)) { + return ResponseEntity.ok("Events log file does not exist yet. It will be created when the first event is received."); + } + + long fileSize = Files.size(filePath); + long lineCount = Files.lines(filePath).count(); + String lastModified = Files.getLastModifiedTime(filePath).toString(); + + String info = String.format("Events Log File Info:\n" + + "File: %s\n" + + "Size: %d bytes\n" + + "Lines: %d\n" + + "Last Modified: %s", + EVENTS_LOG_FILE, fileSize, lineCount, lastModified); + + return ResponseEntity.ok(info); + + } catch (Exception e) { + log.error("Error getting events log file info: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error getting file info: " + e.getMessage()); + } + } + + /** + * API to clear the events log file + */ + @Tag(name = "13. Event Log APIs", description = "APIs for managing event logs") + @DeleteMapping("/api/clear-events-log") + public ResponseEntity clearEventsLog() { + try { + Path filePath = Paths.get(EVENTS_LOG_FILE); + + if (Files.exists(filePath)) { + Files.delete(filePath); + log.info("Events log file cleared successfully"); + return ResponseEntity.ok("Events log file cleared successfully"); + } else { + return ResponseEntity.ok("Events log file does not exist"); + } + + } catch (Exception e) { + log.error("Error clearing events log file: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error clearing file: " + e.getMessage()); + } + } + + private CallAutomationClient initClient() { + try { + var client = new CallAutomationClientBuilder() + .connectionString(appConfig.getConnectionString()) + .buildClient(); + log.info("Call Automation Client initialized successfully."); + 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 Client: {} {}", e.getMessage(), e.getCause()); + return null; + } + } +} diff --git a/Call_Automation_GCCH/src/main/java/com/communication/callautomation/config/OpenApiConfig.java b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/config/OpenApiConfig.java new file mode 100644 index 0000000..76f8431 --- /dev/null +++ b/Call_Automation_GCCH/src/main/java/com/communication/callautomation/config/OpenApiConfig.java @@ -0,0 +1,31 @@ +package com.communication.callautomation.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Value("${acs.callbackUriHost}") + private String callbackUriHost; + + @Bean + public OpenAPI customOpenAPI() { + Server server = new Server(); + server.setUrl(callbackUriHost); + server.setDescription("Deployed Server"); + + return new OpenAPI() + .info(new Info() + .title("Call Automation GCCH API") + .description("Call Automation API for GCCH") + .version("1.0")) + .servers(List.of(server)); + } +} \ No newline at end of file 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..45c7940 --- /dev/null +++ b/Call_Automation_GCCH/src/main/resources/application.yml @@ -0,0 +1,48 @@ +spring: + application: + name: CallAutomation_GCCHTestApp + +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + +server: + port: 8080 + servlet: + session: + timeout: 30m + cookie: + same-site: none + secure: false + compression: + enabled: true + http2: + enabled: true + # Enhanced Tomcat configuration for WebSocket stability in deployed environments + tomcat: + connection-timeout: 600000 # 10 minutes + keep-alive-timeout: 600000 # 10 minutes + max-keep-alive-requests: 1000 + threads: + max: 200 + min-spare: 10 + # WebSocket specific configurations + websocket: + max-idle-timeout: 600000 # 10 minutes + # Additional connection properties for deployed environments + max-connections: 8192 + accept-count: 100 + processor-cache: 200 + # Forward headers for proper handling behind load balancers/proxies + forward-headers-strategy: native + use-forward-headers: true + +acs: + connectionstring: "" + callbackUriHost: "" + acsPhoneNumber: "" + targetphonenumber: "" + cognitiveServiceEndpoint: "" + targetTeamsUserId: <(OPTIONAL) YOUR TARGET TEAMS USER ID ex. "ab01bc12-d457-4995-a27b-c405ecfe4870"> diff --git a/Call_Automation_GCCH/static/OutboundCallDesign.png b/Call_Automation_GCCH/static/OutboundCallDesign.png new file mode 100644 index 0000000..c750ab7 Binary files /dev/null and b/Call_Automation_GCCH/static/OutboundCallDesign.png differ diff --git a/Call_Automation_GCCH/static/prompt.wav b/Call_Automation_GCCH/static/prompt.wav new file mode 100644 index 0000000..f7642f7 Binary files /dev/null and b/Call_Automation_GCCH/static/prompt.wav differ