From 84468d3e868dafab8d7ffe9eb74eb8c4c17e62fa Mon Sep 17 00:00:00 2001 From: nageshy Date: Mon, 28 Jul 2025 14:53:53 +0530 Subject: [PATCH 1/8] Initial Update --- CallAutomation_LobbyCall_Sample/README.md | 37 ++ CallAutomation_LobbyCall_Sample/pom.xml | 195 ++++++++ .../callautomation/ConfigurationRequest.java | 41 ++ .../callautomation/CorsConfig.java | 29 ++ .../callautomation/LobbyWebSocketHandler.java | 49 ++ .../communication/callautomation/Main.java | 13 + .../callautomation/ProgramSample.java | 466 ++++++++++++++++++ .../callautomation/WebSocketConfig.java | 40 ++ .../callautomation/WebSocketHandler.java | 63 +++ .../src/main/resources/application.yml | 21 + 10 files changed, 954 insertions(+) create mode 100644 CallAutomation_LobbyCall_Sample/README.md create mode 100644 CallAutomation_LobbyCall_Sample/pom.xml create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/Main.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/resources/application.yml diff --git a/CallAutomation_LobbyCall_Sample/README.md b/CallAutomation_LobbyCall_Sample/README.md new file mode 100644 index 0000000..142fc4f --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/README.md @@ -0,0 +1,37 @@ +|page_type| languages |products +|---|---------------------------------------|---| +|sample|
Java
|
azureazure-communication-services
| + +# Call Automation - Lobby Call Sample + +In this sample, we cover how you can use Call Automation SDK Lobby Call Sample. + +### 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 and POST methods to run the Sample Application + +### Configuring settings + +In the swagger app, provide these values for the setConfiguration endpoint to configure settings + +1. `acsConnectionString`: Azure Communication Service resource's connection string. +2. `cognitiveServiceEndpoint`: Cognitive service endpoint. +3. `callbackUriHost`: Base url of the app. (For local development replace the dev tunnel url) +4. `pmaEndpoint` : PMA service endpoint. +5. `acsGeneratedId`: Communication Identifier generated through the ACS. +6. `webSocketToken`: Web Socket Token Key. \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/pom.xml b/CallAutomation_LobbyCall_Sample/pom.xml new file mode 100644 index 0000000..318ea02 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/pom.xml @@ -0,0 +1,195 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.6 + + + + com.communication.callautomation + CallAutomation_LobbyCallSample + 1.0-SNAPSHOT + + CallAutomation_LobbyCallSample + CallAutomation Sample application for instructional usage + + + 17 + 17 + UTF-8 + 1.18.26 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + + + 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.6.0-beta.1 + + + com.azure + azure-messaging-eventgrid + 4.16.0 + + + com.azure + azure-communication-common + 1.5.0-beta.1 + + + org.projectlombok + lombok + provided + ${lombok.version} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.0.0 + + + org.json + json + 20231013 + + + + + azure-sdk-for-java + https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-java/maven/v1 + + true + + + true + + + + + + + + maven-clean-plugin + 3.2.0 + + + maven-resources-plugin + 3.3.1 + + + maven-compiler-plugin + 3.11.0 + + + maven-surefire-plugin + 3.1.0 + + + maven-jar-plugin + 3.3.0 + + + maven-deploy-plugin + 3.1.1 + + + maven-site-plugin + 3.12.1 + + + maven-project-info-reports-plugin + 3.4.3 + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + + java + + + + + com.communication.callautomation.Main + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.2.5 + + + + repackage + + + + + + + \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java new file mode 100644 index 0000000..c9dedaf --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java @@ -0,0 +1,41 @@ +package com.communication.callautomation; + +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@ConfigurationProperties(prefix = "acs") +@Getter +public class ConfigurationRequest { + private String acsConnectionString; + private String cognitiveServiceEndpoint; + private String callbackUriHost; + private String pmaEndpoint; + private String acsGeneratedId; + private String webSocketToken; + + // Getters and Setters + public void setAcsConnectionString(String acsConnectionString) { + this.acsConnectionString = acsConnectionString; + } + + public void setCognitiveServiceEndpoint(String cognitiveServiceEndpoint) { + this.cognitiveServiceEndpoint = cognitiveServiceEndpoint; + } + + public void setCallbackUriHost(String callbackUriHost) { + this.callbackUriHost = callbackUriHost; + } + + public void setPmaEndpoint(String pmaEndpoint) { + this.pmaEndpoint = pmaEndpoint; + } + + public void setAcsGeneratedId(String acsGeneratedId) { + this.acsGeneratedId = acsGeneratedId; + } + + public void setWebSocketToken(String webSocketToken) { + this.webSocketToken = webSocketToken; + } +} diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java new file mode 100644 index 0000000..fa91989 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java @@ -0,0 +1,29 @@ +package com.communication.callautomation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(@NonNull CorsRegistry registry) { + registry.addMapping("/**") // Allow all endpoints + // .allowedOrigins("http://localhost:8080", "https://localhost:8080", "http://8kvlj5f1.inc1.devtunnels.ms:8080", "https://8kvlj5f1.inc1.devtunnels.ms:8080") // Allow all origins + // .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow all HTTP methods + // .allowedHeaders("*") // Allow all headers + // .allowCredentials(true); // Enable credentials for security + .allowedOrigins("*") // Allow all origins + .allowedMethods("*") // Allow all HTTP methods + .allowedHeaders("*") // Allow all headers + .allowCredentials(false); // Disable credentials for security + } + }; + } +} \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java new file mode 100644 index 0000000..5b20c5e --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java @@ -0,0 +1,49 @@ +package com.communication.callautomation; + +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component +public class LobbyWebSocketHandler extends TextWebSocketHandler { + + private static final Logger log = LoggerFactory.getLogger(LobbyWebSocketHandler.class); + + // Reference to ProgramSample for handling messages + private ProgramSample programSample; + + public void setProgramSample(ProgramSample programSample) { + this.programSample = programSample; + } + + @Override + public void afterConnectionEstablished(@NonNull WebSocketSession session) { + log.info("WebSocket connection established: " + session.getId()); + } + + @Override + protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) throws Exception { + log.info("Received WebSocket message: " + message.getPayload()); + + // Delegate to ProgramSample if available + if (programSample != null) { + programSample.handleWebSocketMessage(session, message); + } else { + log.warn("ProgramSample not available, cannot process message"); + } + } + + @Override + public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable exception) throws Exception { + log.error("WebSocket transport error: " + exception.getMessage()); + } + + @Override + public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull org.springframework.web.socket.CloseStatus status) throws Exception { + log.info("WebSocket connection closed: " + session.getId()); + } +} diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/Main.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/Main.java new file mode 100644 index 0000000..85a3ed8 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/Main.java @@ -0,0 +1,13 @@ +package com.communication.callautomation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties(value = ConfigurationRequest.class) +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java new file mode 100644 index 0000000..d0cf99a --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java @@ -0,0 +1,466 @@ +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.messaging.eventgrid.EventGridEvent; +import com.azure.messaging.eventgrid.SystemEventNames; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse; +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.util.BinaryData; +import com.azure.core.util.Context; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.json.JSONObject; + +import org.springframework.web.bind.annotation.*; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.http.*; +import org.springframework.beans.factory.annotation.Autowired; +import jakarta.annotation.PostConstruct; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.Comparator; + +@RestController +public class ProgramSample { + + private static final Logger log = LoggerFactory.getLogger(ProgramSample.class); + private CallAutomationClient acsClient; + + @Autowired + private WebSocketConfig webSocketConfig; + + @Autowired + private LobbyWebSocketHandler lobbyWebSocketHandler; + + // Configuration state variables + private ConfigurationRequest configuration = new ConfigurationRequest(); + private String acsConnectionString = ""; + private String cognitiveServiceEndpoint = ""; + private String callbackUriHost = ""; + private String pmaEndpoint; + private String acsGeneratedId = ""; + private String webSocketToken = ""; + + private String lobbyCallConnectionId = ""; + private String targetCallConnectionId = ""; + private String lobbyCallerId = ""; + + @PostConstruct + public void init() { + // Set up the connection between ProgramSample and LobbyWebSocketHandler + if (lobbyWebSocketHandler != null) { + lobbyWebSocketHandler.setProgramSample(this); + } + } + + // Getter method for webSocketToken + public String getWebSocketToken() { + return webSocketToken; + } + + // Method to handle WebSocket messages (called by LobbyWebSocketHandler) + public void handleWebSocketMessage(WebSocketSession session, TextMessage message) { + String jsResponse = message.getPayload(); + log.info("Received from JS: " + jsResponse); + + if ("yes".equalsIgnoreCase(jsResponse.trim())) { + log.info("TODO: Move Participant"); + try { + log.info( + "\n~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~\n" + + "Move Participant operation started..\n" + + "Source Caller Id: " + lobbyCallerId + "\n" + + "Source Connection Id: " + lobbyCallConnectionId + "\n" + + "Target Connection Id: " + targetCallConnectionId + "\n" + ); + + CallConnection targetConnection = acsClient.getCallConnection(targetCallConnectionId); + // CallConnection sourceConnection = client.getCallConnection(lobbyConnectionId.get()); + + CommunicationIdentifier participantToMove; + if (lobbyCallerId.startsWith("+")) { + participantToMove = new PhoneNumberIdentifier(lobbyCallerId); + } else { + participantToMove = new CommunicationUserIdentifier(lobbyCallerId); + } + + MoveParticipantsOptions options = new MoveParticipantsOptions( + java.util.Collections.singletonList(participantToMove), + lobbyCallConnectionId + ); + targetConnection.moveParticipants(options); + // If no exception is thrown, the operation is considered successful + log.info("Move Participants operation completed successfully."); + } catch (Exception ex) { + log.info("Error in manual move participants operation: " + ex.getMessage()); + } + } + } + + @Tag(name = "STEP 00. Call Automation Events", description = "Configure Event Grid webhook to point to /api/lobbyCallEventHandler endpoint") + @PostMapping(path = "/api/lobbyCallEventHandler") + public ResponseEntity lobbyCallEventHandler(@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); + } + + 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(null); + } catch (Exception e) { + log.error("Error at subscription validation event {} {}", + e.getMessage(), + e.getCause()); + return ResponseEntity.internalServerError().body(null); + } + } + + private void handleIncomingCall(final BinaryData eventData) { + JSONObject data = new JSONObject(eventData.toString()); + String callbackUri = URI.create(callbackUriHost + "/api/callbacks").toString(); + + String fromCallerId = data.getJSONObject("from").getString("rawId"); + String toCallerId = data.getJSONObject("to").getString("rawId"); + log.info("Incoming call from: {}, to: {}", fromCallerId, toCallerId); + String incomingCallContext = data.getString("incomingCallContext"); + log.info("Incoming Call Context: " + incomingCallContext); + + // Lobby Call: Answer + if (toCallerId.contains(acsGeneratedId)) { + StringBuilder msgLog = new StringBuilder(); + try { + AnswerCallOptions options = new AnswerCallOptions(incomingCallContext, callbackUri); + options.setOperationContext("LobbyCall"); + CallIntelligenceOptions intelligenceOptions = new CallIntelligenceOptions(); + intelligenceOptions.setCognitiveServicesEndpoint(cognitiveServiceEndpoint); + options.setCallIntelligenceOptions(intelligenceOptions); + + AnswerCallResult answerCallResult = acsClient.answerCallWithResponse(options, Context.NONE).getValue(); + lobbyCallConnectionId = answerCallResult.getCallConnection().getCallProperties().getCallConnectionId(); + + msgLog.append("User Call(Inbound) Answered by Call Automation.\n") + .append("From Caller Raw Id: ").append(fromCallerId).append("\n") + .append("To Caller Raw Id: ").append(toCallerId).append("\n") + .append("Lobby Call Connection Id: ").append(lobbyCallConnectionId).append("\n") + .append("Correlation Id: ").append(answerCallResult.getCallConnection().getCallProperties().getCorrelationId()).append("\n") + .append("Lobby Call answered successfully.\n"); + } catch (Exception ex) { + msgLog.append("Error answering call: ").append(ex.getMessage()).append("\n"); + } + log.info(msgLog.toString()); + } + } + + @Tag(name = "STEP 00. Call Automation Events", description = "Call Back Events") + @PostMapping(path = "/api/callbacks") + public ResponseEntity callbackEvents(@RequestBody final String reqBody) { + StringBuilder msgLog = new StringBuilder(); + try { + List events = CallAutomationEventParser.parseEvents(reqBody); + for (CallAutomationEventBase event : events) { + String callConnectionId = event.getCallConnectionId(); + log.info( + "Received call event callConnectionID: {}, serverCallId: {}, CorrelationId: {}, eventType: {}", + callConnectionId, + event.getServerCallId(), + event.getCorrelationId(), + event.getClass().getSimpleName()); + + // Parse the event using the Azure SDK's parser if available + // For illustration, we use eventType string matching + if (event instanceof CallConnected) { + String operationContext = ((CallConnected) event).getOperationContext(); + String correlationId = ((CallConnected) event).getCorrelationId(); + + log.info("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ "); + log.info("Received callConnected.CallConnectionId : " + callConnectionId); + + if ("LobbyCall".equals(operationContext)) { + msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + .append("Received call event : ").append(event.getClass()).append("\n") + .append("Lobby Call Connection Id: ").append(callConnectionId).append("\n") + .append("Correlation Id: ").append(correlationId).append("\n"); + + // Record lobby caller id and connection id + CallConnection lobbyCallConnection = acsClient.getCallConnection(callConnectionId); + CallConnectionProperties callConnectionProperties = lobbyCallConnection.getCallProperties(); + lobbyCallerId = callConnectionProperties.getSource().getRawId(); + lobbyCallConnectionId = callConnectionProperties.getCallConnectionId(); + log.info("Lobby Caller Id: " + lobbyCallerId); + log.info("Lobby Connection Id: " + lobbyCallConnectionId); + + // Play lobby waiting message + CallMedia callMedia = lobbyCallConnection.getCallMedia(); + TextSource textSource = new TextSource().setText("You are currently in a lobby call, we will notify the admin that you are waiting."); + textSource.setVoiceName("en-US-NancyNeural"); + CommunicationUserIdentifier playTo = new CommunicationUserIdentifier(lobbyCallerId); + callMedia.play(textSource, List.of(playTo)); + } + } else if (event instanceof PlayCompleted) { + msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + .append("Received event: ").append(event.getClass()).append("\n"); + + // Notify Target Call user via websocket + // In Java/Spring, you would use a WebSocket messaging template or similar + // For now, just log the message + String confirmMessageToTargetCall = "Target Call user has been notified that the lobby call is connected."; + msgLog.append("Target Call notified with message: ").append(confirmMessageToTargetCall).append("\n"); + log.info("Target Call notified with message: " + confirmMessageToTargetCall); + return ResponseEntity.ok("Target Call notified with message: " + confirmMessageToTargetCall); + // } else if (event instanceof MoveParticipantsSucceeded) { + // String correlationId = event.getCorrelationId(); + // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + // .append("Received event: ").append(event.getClass()).append("\n") + // .append("Call Connection Id: ").append(callConnectionId).append("\n") + // .append("Correlation Id: ").append(correlationId).append("\n"); + } else if (event instanceof CallDisconnected) { + msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + .append("Received event: ").append(event.getClass()).append("\n") + .append("Call Connection Id: ").append(callConnectionId).append("\n"); + } + } + } catch (Exception ex) { + msgLog.append("Error processing event: ").append(ex.getMessage()).append("\n"); + } + log.info(msgLog.toString()); + return ResponseEntity.ok(msgLog.toString()); + } + + @Tag(name = "STEP 01. Set Configuration", description = "Assign the global variables for the Call Automation sample") + @PostMapping("/api/setConfiguration") + public ResponseEntity setConfiguration(@RequestBody ConfigurationRequest configurationRequest) { + try { + // Reset variables + acsConnectionString = ""; + cognitiveServiceEndpoint = ""; + callbackUriHost = ""; + pmaEndpoint = ""; + acsGeneratedId = ""; + webSocketToken = ""; + + lobbyCallConnectionId = ""; + lobbyCallerId = ""; + + if (configurationRequest != null) { + configuration.setAcsConnectionString( + Optional.ofNullable(configurationRequest.getAcsConnectionString()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("AcsConnectionString is required")) + ); + configuration.setCognitiveServiceEndpoint( + Optional.ofNullable(configurationRequest.getCognitiveServiceEndpoint()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("CognitiveServiceEndpoint is required")) + ); + configuration.setPmaEndpoint( + Optional.ofNullable(configurationRequest.getPmaEndpoint()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("PmaEndpoint is required")) + ); + configuration.setCallbackUriHost( + Optional.ofNullable(configurationRequest.getCallbackUriHost()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("CallbackUriHost is required")) + ); + configuration.setAcsGeneratedId( + Optional.ofNullable(configurationRequest.getAcsGeneratedId()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("AcsGeneratedId is required")) + ); + configuration.setWebSocketToken( + Optional.ofNullable(configurationRequest.getWebSocketToken()) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException("WebSocketToken is required")) + ); + } + + // Assign to global variables + acsConnectionString = configuration.getAcsConnectionString(); + cognitiveServiceEndpoint = configuration.getCognitiveServiceEndpoint(); + callbackUriHost = configuration.getCallbackUriHost(); + pmaEndpoint = configuration.getPmaEndpoint(); + acsGeneratedId = configuration.getAcsGeneratedId(); + webSocketToken = configuration.getWebSocketToken(); + + acsClient = initClient(pmaEndpoint, acsConnectionString); + + // Update WebSocket configuration with the new token + if (webSocketConfig != null) { + webSocketConfig.updateWebSocketEndpoint(webSocketToken); + log.info("WebSocket endpoint updated with token: /ws/{}", webSocketToken); + } + + log.info("Initialized call automation client."); + return ResponseEntity.ok("Configuration set successfully. Initialized call automation client. WebSocket endpoint: /ws/" + webSocketToken); + } catch (Exception e) { + log.error("Error configuring: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to configure call automation client."); + } + } + + @Tag(name = "STEP 02. Target Call To ACSUser", description = "Make a call to ACS User by using this endpoint") + @PostMapping(path = "/targetCallToAcsUser") + public ResponseEntity createTargetCall(@RequestParam String acsTarget) { + StringBuilder msgLog = new StringBuilder(); + msgLog.append("\n~~~~~~~~~~~~ /TargetCall(Create) ~~~~~~~~~~~~\n"); + + try { + URI callbackUri = new URI(callbackUriHost + "/api/callbacks"); + CommunicationUserIdentifier targetUser = new CommunicationUserIdentifier(acsTarget); + CallInvite callInvite = new CallInvite(targetUser); + CreateCallOptions createCallOptions = new CreateCallOptions(callInvite, callbackUri.toString()); + CallIntelligenceOptions intelligenceOptions = new CallIntelligenceOptions(); + intelligenceOptions.setCognitiveServicesEndpoint(cognitiveServiceEndpoint); + createCallOptions.setCallIntelligenceOptions(intelligenceOptions); + + CreateCallResult createCallResult = acsClient.createCall(callInvite, callbackUri.toString()); + targetCallConnectionId = createCallResult.getCallConnectionProperties().getCallConnectionId(); + + msgLog.append("TargetCall:\n") + .append("-----------\n") + .append("From: Call Automation\n") + .append("To: ").append(acsTarget).append("\n") + .append("Target Call Connection Id: ").append(targetCallConnectionId).append("\n") + .append("Correlation Id: ") + .append(createCallResult.getCallConnectionProperties().getCorrelationId()).append("\n"); + + log.info(msgLog.toString()); + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); + } catch (Exception ex) { + msgLog.append("Error creating call: ").append(ex.getMessage()).append("\n"); + log.error(msgLog.toString()); + return ResponseEntity.status(500).contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); + } + } + + @Tag(name = "STEP 03. Get Call Participants", description = "Get call participants for the lobby call connection by using this endpoint") + @GetMapping(path = "/getParticipants") + public ResponseEntity getParticipants() { + StringBuilder msgLog = new StringBuilder(); + msgLog.append("\n~~~~~~~~~~~~ /GetParticipants/").append(lobbyCallConnectionId).append(" ~~~~~~~~~~~~\n"); + + try { + CallConnection callConnection = acsClient.getCallConnection(lobbyCallConnectionId); + PagedIterable participantsResult = callConnection.listParticipants(); + List participants = participantsResult.stream().collect(Collectors.toList()); + + // Map and sort participants: phone numbers first, then ACS users + List participantInfo = participants.stream() + .map(p -> { + CommunicationIdentifier id = p.getIdentifier(); + String type = id.getClass().getSimpleName(); + String rawId = id.getRawId(); + String phoneNumber = (id instanceof PhoneNumberIdentifier) ? ((PhoneNumberIdentifier) id).getPhoneNumber() : null; + String acsUserId = (id instanceof CommunicationUserIdentifier) ? ((CommunicationUserIdentifier) id).getId() : null; + + if (acsUserId == null || acsUserId.isBlank()) { + return String.format("%s - RawId: %s, Phone: %s", type, rawId, phoneNumber); + } else { + return String.format("%s - RawId: %s", type, acsUserId); + } + }) + .sorted(Comparator.comparing(info -> info.contains("Phone:") ? "" : "z")) // phone numbers first + .collect(Collectors.toList()); + + if (participantInfo.isEmpty()) { + return ResponseEntity.status(404).body( + String.format("{\"Message\":\"No participants found for the specified call connection.\",\"CallConnectionId\":\"%s\"}", lobbyCallConnectionId) + ); + } else { + msgLog.append("\nNo of Participants: ").append(participantInfo.size()) + .append("\nParticipants: \n-------------\n"); + for (int i = 0; i < participantInfo.size(); i++) { + msgLog.append(i + 1).append(". ").append(participantInfo.get(i)).append("\n"); + } + log.info(msgLog.toString()); + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); + } + } catch (Exception ex) { + System.err.println("Error getting participants for call " + lobbyCallConnectionId + ": " + ex.getMessage()); + return ResponseEntity.badRequest().body( + String.format("{\"Error\":\"%s\",\"CallConnectionId\":\"%s\"}", ex.getMessage(), lobbyCallConnectionId) + ); + } + } + + @Tag(name = "STEP 04. Terminate Calls", description = "Terminate all Calls created so far by using this endpoint") + @GetMapping("/terminateCalls") + public ResponseEntity terminateCalls() { + try { + CallConnection callConnection = getCallConnection(acsClient, lobbyCallConnectionId); + callConnection.hangUpWithResponse(true, Context.NONE); + } catch (Exception e) { + log.warn("Could not hang up lobby call: {}", e.getMessage()); + } + try { + CallConnection callConnection = getCallConnection(acsClient, targetCallConnectionId); + callConnection.hangUpWithResponse(true, Context.NONE); + } catch (Exception e) { + log.warn("Could not hang up target call: {}", e.getMessage()); + } + lobbyCallConnectionId = ""; + targetCallConnectionId = ""; + lobbyCallerId = ""; + return ResponseEntity.ok("Terminated all calls"); + } + + // 🔄 Shared Methods + private CallConnection getCallConnection(CallAutomationClient client, String callConnectionId) { + if (callConnectionId == null || callConnectionId.isEmpty()) { + throw new IllegalArgumentException("Call connection id is empty"); + } + return client.getCallConnection(callConnectionId); + } + + private CallAutomationClient initClient(String endpoint, String connectionString) { + try { + if (connectionString == null || connectionString.trim().isEmpty()) { + log.error("ACS Connection String is null or empty"); + return null; + } + + log.info("Initializing Call Automation Client with connection string length: {}", connectionString.length()); + + var client = new CallAutomationClientBuilder() + .endpoint(endpoint) + .connectionString(connectionString) + .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/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java new file mode 100644 index 0000000..5e00231 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java @@ -0,0 +1,40 @@ +package com.communication.callautomation; + +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.beans.factory.annotation.Autowired; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Autowired + private LobbyWebSocketHandler lobbyWebSocketHandler; + + private WebSocketHandlerRegistry currentRegistry; + private String currentSocketToken = "default"; + + @Override + public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) { + this.currentRegistry = registry; + // Initial registration with default token + registry.addHandler(lobbyWebSocketHandler, "/ws/" + currentSocketToken).setAllowedOrigins("*"); + } + + public void updateWebSocketEndpoint(String newToken) { + if (newToken != null && !newToken.isEmpty() && !newToken.equals(currentSocketToken)) { + this.currentSocketToken = newToken; + // Re-register with new token + if (currentRegistry != null) { + currentRegistry.addHandler(lobbyWebSocketHandler, "/ws/" + newToken).setAllowedOrigins("*"); + } + } + } + + public String getCurrentSocketToken() { + return currentSocketToken; + } +} \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java new file mode 100644 index 0000000..154a556 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java @@ -0,0 +1,63 @@ +package com.communication.callautomation; + +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.springframework.lang.NonNull; +import org.springframework.web.socket.TextMessage; +//import com.azure.communication.callautomation.StreamingData; +import com.azure.communication.callautomation.models.StreamingData; +import com.azure.communication.callautomation.models.TranscriptionData; +import com.azure.communication.callautomation.models.TranscriptionMetadata; +import com.azure.communication.callautomation.models.WordData; + +public class WebSocketHandler extends TextWebSocketHandler { + + @Override + protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) throws Exception { + String payload = message.getPayload(); + System.out.println("Received message: " + payload); + + // Parse the message into StreamingData (custom data parsing logic) + StreamingData data = StreamingData.parse(payload); + + + // Handle TranscriptionMetadata + if (data instanceof TranscriptionMetadata) { + TranscriptionMetadata transcriptionMetadata = (TranscriptionMetadata) data; + System.out.println("----------------------------------------------------------------"); + System.out.println("TRANSCRIPTION SUBSCRIPTION ID:-->" + transcriptionMetadata.getTranscriptionSubscriptionId()); + System.out.println("LOCALE:-->" + transcriptionMetadata.getLocale()); + System.out.println("CALL CONNECTION ID:-->" + transcriptionMetadata.getCallConnectionId()); + System.out.println("CORRELATION ID:-->" + transcriptionMetadata.getCorrelationId()); + System.out.println("----------------------------------------------------------------"); + } + + // Handle TranscriptionData + if (data instanceof TranscriptionData) { + TranscriptionData transcriptionData = (TranscriptionData) data; + System.out.println("----------------------------------------------------------------"); + System.out.println("TEXT:-->" + transcriptionData.getText()); + System.out.println("FORMAT:-->" + transcriptionData.getFormat()); + System.out.println("CONFIDENCE:-->" + transcriptionData.getConfidence()); + System.out.println("OFFSET:-->" + transcriptionData.getOffset()); + System.out.println("DURATION:-->" + transcriptionData.getDuration()); + + String participant = transcriptionData.getParticipant().getRawId() != null + ? transcriptionData.getParticipant().getRawId() + : ""; + System.out.println("PARTICIPANT:-->" + participant); + System.out.println("RESULT STATUS:-->" + transcriptionData.getResultState()); + + // Print word data (example of transcribed words) + for (WordData word : transcriptionData.getTranscribedWords()) { + System.out.println("TEXT:-->" + word.getText()); + System.out.println("OFFSET:-->" + word.getOffset()); + System.out.println("DURATION:-->" + word.getDuration()); + } + System.out.println("----------------------------------------------------------------"); + } + + // Send an echo response back to the client + session.sendMessage(new TextMessage("Echo: " + payload)); + } +} diff --git a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml new file mode 100644 index 0000000..91bdfa8 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + application: + name: CallAutomation_LobbyCallSample + +springdoc: + swagger-ui: + tagsSorter: alpha + +server: + port: 8080 + +applicationinsights: + connection-string: "" + +acs: + acsConnectionString: "" + cognitiveServiceEndpoint: "" + callbackUriHost: "" + pmaEndpoint: "" + acsGeneratedId: "" + webSocketToken: "" \ No newline at end of file From 473f8e8dc8df3ebb6c60f09d2e00e1abc74c7761 Mon Sep 17 00:00:00 2001 From: nageshy Date: Mon, 28 Jul 2025 15:09:31 +0530 Subject: [PATCH 2/8] Updated code --- .../callautomation/CorsConfig.java | 4 -- .../callautomation/ProgramSample.java | 2 +- .../callautomation/WebSocketHandler.java | 63 ------------------- 3 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java index fa91989..82a2fb1 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/CorsConfig.java @@ -15,10 +15,6 @@ public WebMvcConfigurer corsConfigurer() { @Override public void addCorsMappings(@NonNull CorsRegistry registry) { registry.addMapping("/**") // Allow all endpoints - // .allowedOrigins("http://localhost:8080", "https://localhost:8080", "http://8kvlj5f1.inc1.devtunnels.ms:8080", "https://8kvlj5f1.inc1.devtunnels.ms:8080") // Allow all origins - // .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow all HTTP methods - // .allowedHeaders("*") // Allow all headers - // .allowCredentials(true); // Enable credentials for security .allowedOrigins("*") // Allow all origins .allowedMethods("*") // Allow all HTTP methods .allowedHeaders("*") // Allow all headers diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java index d0cf99a..0395372 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java @@ -404,7 +404,7 @@ public ResponseEntity getParticipants() { return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); } } catch (Exception ex) { - System.err.println("Error getting participants for call " + lobbyCallConnectionId + ": " + ex.getMessage()); + log.error("Error getting participants for call " + lobbyCallConnectionId + ": " + ex.getMessage()); return ResponseEntity.badRequest().body( String.format("{\"Error\":\"%s\",\"CallConnectionId\":\"%s\"}", ex.getMessage(), lobbyCallConnectionId) ); diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java deleted file mode 100644 index 154a556..0000000 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.communication.callautomation; - -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; -import org.springframework.lang.NonNull; -import org.springframework.web.socket.TextMessage; -//import com.azure.communication.callautomation.StreamingData; -import com.azure.communication.callautomation.models.StreamingData; -import com.azure.communication.callautomation.models.TranscriptionData; -import com.azure.communication.callautomation.models.TranscriptionMetadata; -import com.azure.communication.callautomation.models.WordData; - -public class WebSocketHandler extends TextWebSocketHandler { - - @Override - protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) throws Exception { - String payload = message.getPayload(); - System.out.println("Received message: " + payload); - - // Parse the message into StreamingData (custom data parsing logic) - StreamingData data = StreamingData.parse(payload); - - - // Handle TranscriptionMetadata - if (data instanceof TranscriptionMetadata) { - TranscriptionMetadata transcriptionMetadata = (TranscriptionMetadata) data; - System.out.println("----------------------------------------------------------------"); - System.out.println("TRANSCRIPTION SUBSCRIPTION ID:-->" + transcriptionMetadata.getTranscriptionSubscriptionId()); - System.out.println("LOCALE:-->" + transcriptionMetadata.getLocale()); - System.out.println("CALL CONNECTION ID:-->" + transcriptionMetadata.getCallConnectionId()); - System.out.println("CORRELATION ID:-->" + transcriptionMetadata.getCorrelationId()); - System.out.println("----------------------------------------------------------------"); - } - - // Handle TranscriptionData - if (data instanceof TranscriptionData) { - TranscriptionData transcriptionData = (TranscriptionData) data; - System.out.println("----------------------------------------------------------------"); - System.out.println("TEXT:-->" + transcriptionData.getText()); - System.out.println("FORMAT:-->" + transcriptionData.getFormat()); - System.out.println("CONFIDENCE:-->" + transcriptionData.getConfidence()); - System.out.println("OFFSET:-->" + transcriptionData.getOffset()); - System.out.println("DURATION:-->" + transcriptionData.getDuration()); - - String participant = transcriptionData.getParticipant().getRawId() != null - ? transcriptionData.getParticipant().getRawId() - : ""; - System.out.println("PARTICIPANT:-->" + participant); - System.out.println("RESULT STATUS:-->" + transcriptionData.getResultState()); - - // Print word data (example of transcribed words) - for (WordData word : transcriptionData.getTranscribedWords()) { - System.out.println("TEXT:-->" + word.getText()); - System.out.println("OFFSET:-->" + word.getOffset()); - System.out.println("DURATION:-->" + word.getDuration()); - } - System.out.println("----------------------------------------------------------------"); - } - - // Send an echo response back to the client - session.sendMessage(new TextMessage("Echo: " + payload)); - } -} From 0b030a070e68cae3e0875b2e8a713eb878fbae03 Mon Sep 17 00:00:00 2001 From: nageshy Date: Tue, 29 Jul 2025 12:32:30 +0530 Subject: [PATCH 3/8] Updated code --- .../callautomation/HttpsConfig.java | 27 +++ .../callautomation/OpenApiConfig.java | 35 ++++ .../callautomation/ProgramSample.java | 180 +++++++++++++++--- .../src/main/resources/application.yml | 13 +- .../src/main/resources/keystore.p12 | Bin 0 -> 2760 bytes 5 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java create mode 100644 CallAutomation_LobbyCall_Sample/src/main/resources/keystore.p12 diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java new file mode 100644 index 0000000..1333df7 --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java @@ -0,0 +1,27 @@ +package com.communication.callautomation; + +import org.apache.catalina.connector.Connector; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HttpsConfig { + + @Value("${server.http.port:8080}") + private int httpPort; + + @Bean + public WebServerFactoryCustomizer servletContainer() { + return server -> { + // Add HTTP connector for dual protocol support + Connector httpConnector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); + httpConnector.setScheme("http"); + httpConnector.setPort(httpPort); + httpConnector.setSecure(false); + server.addAdditionalTomcatConnectors(httpConnector); + }; + } +} \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java new file mode 100644 index 0000000..4d465ee --- /dev/null +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java @@ -0,0 +1,35 @@ +package com.communication.callautomation; + +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + Server httpsServer = new Server(); + httpsServer.setUrl("https://localhost:8443"); + httpsServer.setDescription("HTTPS Server (Primary)"); + + Server httpServer = new Server(); + httpServer.setUrl("http://localhost:8080"); + httpServer.setDescription("HTTP Server (Alternative)"); + + return new OpenAPI() + .info(new Info() + .title("Call Automation Lobby Call Sample API") + .description("Azure Communication Services Call Automation API for managing lobby calls.\n\n" + + "**Server Selection:**\n" + + "- HTTPS Server (Primary): https://localhost:8443 - Secure connection\n" + + "- HTTP Server (Alternative): http://localhost:8080 - Non-secure connection\n\n" + + "**Note:** Both servers are now available. HTTPS uses a self-signed certificate.") + .version("1.0.0")) + .servers(List.of(httpsServer, httpServer)); + } +} diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java index 0395372..6088af5 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java @@ -114,8 +114,9 @@ public void handleWebSocketMessage(WebSocketSession session, TextMessage message } @Tag(name = "STEP 00. Call Automation Events", description = "Configure Event Grid webhook to point to /api/lobbyCallEventHandler endpoint") - @PostMapping(path = "/api/lobbyCallEventHandler") + @PostMapping("/api/lobbyCallEventHandler") public ResponseEntity lobbyCallEventHandler(@RequestBody final String reqBody) { + log.info("Lobby call event received"); List events = EventGridEvent.fromString(reqBody); for (EventGridEvent eventGridEvent : events) { if (eventGridEvent.getEventType().equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION)) { @@ -144,6 +145,7 @@ private ResponseEntity handleSubscriptionValidation(final BinaryData eve } private void handleIncomingCall(final BinaryData eventData) { + log.info("Incoming call received"); JSONObject data = new JSONObject(eventData.toString()); String callbackUri = URI.create(callbackUriHost + "/api/callbacks").toString(); @@ -179,9 +181,84 @@ private void handleIncomingCall(final BinaryData eventData) { } } + // @Tag(name = "STEP 00. Call Automation Events", description = "Call Back Events") + // @PostMapping("/api/callbacks") + // public ResponseEntity callbackEvents(@RequestBody final String reqBody) { + // StringBuilder msgLog = new StringBuilder(); + // try { + // List events = CallAutomationEventParser.parseEvents(reqBody); + // for (CallAutomationEventBase event : events) { + // String callConnectionId = event.getCallConnectionId(); + // log.info( + // "Received call event callConnectionID: {}, serverCallId: {}, CorrelationId: {}, eventType: {}", + // callConnectionId, + // event.getServerCallId(), + // event.getCorrelationId(), + // event.getClass().getSimpleName()); + + // // Parse the event using the Azure SDK's parser if available + // // For illustration, we use eventType string matching + // if (event instanceof CallConnected) { + // String operationContext = ((CallConnected) event).getOperationContext(); + // String correlationId = ((CallConnected) event).getCorrelationId(); + + // log.info("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ "); + // log.info("Received callConnected.CallConnectionId : " + callConnectionId); + + // if ("LobbyCall".equals(operationContext)) { + // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + // .append("Received call event : ").append(event.getClass()).append("\n") + // .append("Lobby Call Connection Id: ").append(callConnectionId).append("\n") + // .append("Correlation Id: ").append(correlationId).append("\n"); + + // // Record lobby caller id and connection id + // CallConnection lobbyCallConnection = acsClient.getCallConnection(callConnectionId); + // CallConnectionProperties callConnectionProperties = lobbyCallConnection.getCallProperties(); + // lobbyCallerId = callConnectionProperties.getSource().getRawId(); + // lobbyCallConnectionId = callConnectionProperties.getCallConnectionId(); + // log.info("Lobby Caller Id: " + lobbyCallerId); + // log.info("Lobby Connection Id: " + lobbyCallConnectionId); + + // // Play lobby waiting message + // CallMedia callMedia = lobbyCallConnection.getCallMedia(); + // TextSource textSource = new TextSource().setText("You are currently in a lobby call, we will notify the admin that you are waiting."); + // textSource.setVoiceName("en-US-NancyNeural"); + // CommunicationUserIdentifier playTo = new CommunicationUserIdentifier(lobbyCallerId); + // callMedia.play(textSource, List.of(playTo)); + // } + // } else if (event instanceof PlayCompleted) { + // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + // .append("Received event: ").append(event.getClass()).append("\n"); + + // // Notify Target Call user via websocket + // // In Java/Spring, you would use a WebSocket messaging template or similar + // // For now, just log the message + // String confirmMessageToTargetCall = "Target Call user has been notified that the lobby call is connected."; + // msgLog.append("Target Call notified with message: ").append(confirmMessageToTargetCall).append("\n"); + // log.info("Target Call notified with message: " + confirmMessageToTargetCall); + // return ResponseEntity.ok("Target Call notified with message: " + confirmMessageToTargetCall); + // // } else if (event instanceof MoveParticipantsSucceeded) { + // // String correlationId = event.getCorrelationId(); + // // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + // // .append("Received event: ").append(event.getClass()).append("\n") + // // .append("Call Connection Id: ").append(callConnectionId).append("\n") + // // .append("Correlation Id: ").append(correlationId).append("\n"); + // } else if (event instanceof CallDisconnected) { + // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + // .append("Received event: ").append(event.getClass()).append("\n") + // .append("Call Connection Id: ").append(callConnectionId).append("\n"); + // } + // } + // } catch (Exception ex) { + // msgLog.append("Error processing event: ").append(ex.getMessage()).append("\n"); + // } + // log.info(msgLog.toString()); + // return ResponseEntity.ok(msgLog.toString()); + // } + @Tag(name = "STEP 00. Call Automation Events", description = "Call Back Events") - @PostMapping(path = "/api/callbacks") - public ResponseEntity callbackEvents(@RequestBody final String reqBody) { + @PostMapping("/api/callbacks") + public ResponseEntity callbackEventsOld(@RequestBody final String reqBody) { StringBuilder msgLog = new StringBuilder(); try { List events = CallAutomationEventParser.parseEvents(reqBody); @@ -202,6 +279,7 @@ public ResponseEntity callbackEvents(@RequestBody final String reqBody) log.info("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ "); log.info("Received callConnected.CallConnectionId : " + callConnectionId); + log.info("Operation Context : " + operationContext); if ("LobbyCall".equals(operationContext)) { msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") @@ -222,36 +300,70 @@ public ResponseEntity callbackEvents(@RequestBody final String reqBody) TextSource textSource = new TextSource().setText("You are currently in a lobby call, we will notify the admin that you are waiting."); textSource.setVoiceName("en-US-NancyNeural"); CommunicationUserIdentifier playTo = new CommunicationUserIdentifier(lobbyCallerId); + PlayOptions playToOptions = new PlayOptions(textSource, List.of(playTo)); + playToOptions.setOperationContext("playToContext"); callMedia.play(textSource, List.of(playTo)); } } else if (event instanceof PlayCompleted) { msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") .append("Received event: ").append(event.getClass()).append("\n"); - // Notify Target Call user via websocket - // In Java/Spring, you would use a WebSocket messaging template or similar - // For now, just log the message - String confirmMessageToTargetCall = "Target Call user has been notified that the lobby call is connected."; - msgLog.append("Target Call notified with message: ").append(confirmMessageToTargetCall).append("\n"); - log.info("Target Call notified with message: " + confirmMessageToTargetCall); - return ResponseEntity.ok("Target Call notified with message: " + confirmMessageToTargetCall); - // } else if (event instanceof MoveParticipantsSucceeded) { - // String correlationId = event.getCorrelationId(); - // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") - // .append("Received event: ").append(event.getClass()).append("\n") - // .append("Call Connection Id: ").append(callConnectionId).append("\n") - // .append("Correlation Id: ").append(correlationId).append("\n"); + // Move Participant logic + try { + msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~\n") + .append("Move Participant operation started..\n") + .append("Source Caller Id: ").append(lobbyCallerId).append("\n") + .append("Source Connection Id: ").append(lobbyCallConnectionId).append("\n") + .append("Target Connection Id: ").append(targetCallConnectionId).append("\n"); + + CallConnection targetConnection = acsClient.getCallConnection(targetCallConnectionId); + // CallConnection sourceConnection = client.getCallConnection(lobbyConnectionId); + + CommunicationIdentifier participantToMove; + if (lobbyCallerId.startsWith("+")) { + participantToMove = new PhoneNumberIdentifier(lobbyCallerId); + } else if (lobbyCallerId.startsWith("8:acs:")) { + participantToMove = new CommunicationUserIdentifier(lobbyCallerId); + } else { + return ResponseEntity.badRequest().body("Invalid participant format. Use phone number (+1234567890) or ACS user ID (8:acs:...)"); + } + + MoveParticipantsOptions options = new MoveParticipantsOptions( + java.util.Collections.singletonList(participantToMove), + lobbyCallConnectionId + ); + targetConnection.moveParticipants(options); + // If no exception is thrown, the operation is considered successful + msgLog.append("\nMove Participants operation completed successfully.\n"); + + log.info(msgLog.toString()); + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); + } catch (Exception ex) { + log.info("Error in manual move participants operation: " + ex.getMessage()); + return ResponseEntity.badRequest().body( + String.format("{\"Success\":false,\"Error\":\"%s\",\"Message\":\"Move participants operation failed.\"}", ex.getMessage()) + ); + } + } else if (event instanceof MoveParticipantSucceeded) { + String correlationId = event.getCorrelationId(); + msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + .append("Received event: ").append(event.getClass()).append("\n") + .append("Call Connection Id: ").append(callConnectionId).append("\n") + .append("Correlation Id: ").append(correlationId).append("\n"); } else if (event instanceof CallDisconnected) { msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") .append("Received event: ").append(event.getClass()).append("\n") - .append("Call Connection Id: ").append(callConnectionId).append("\n"); + .append("Call Connection Id: ").append(((CallDisconnected) event).getCallConnectionId()).append("\n"); } } } catch (Exception ex) { msgLog.append("Error processing event: ").append(ex.getMessage()).append("\n"); } - log.info(msgLog.toString()); - return ResponseEntity.ok(msgLog.toString()); + + if (msgLog.length() > 0) { + log.info(msgLog.toString()); + } + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); } @Tag(name = "STEP 01. Set Configuration", description = "Assign the global variables for the Call Automation sample") @@ -319,15 +431,39 @@ public ResponseEntity setConfiguration(@RequestBody ConfigurationRequest } log.info("Initialized call automation client."); - return ResponseEntity.ok("Configuration set successfully. Initialized call automation client. WebSocket endpoint: /ws/" + webSocketToken); + return ResponseEntity.ok("Configuration set successfully. Initialized call automation client. WebSocket endpoint: ws://localhost:8080/ws/" + webSocketToken); } catch (Exception e) { log.error("Error configuring: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to configure call automation client."); } } + @Tag(name = "STEP 01. Get WebSocket Info", description = "Get current WebSocket endpoint information") + @GetMapping("/api/getWebSocketInfo") + public ResponseEntity getWebSocketInfo() { + try { + String currentToken = webSocketConfig != null ? webSocketConfig.getCurrentSocketToken() : "not-configured"; + String wsEndpoint = "/ws/" + currentToken; + String response = String.format( + "{\n" + + " \"webSocketToken\": \"%s\",\n" + + " \"webSocketEndpoint\": \"%s\",\n" + + " \"httpWebSocketUrl\": \"ws://localhost:8080%s\",\n" + + " \"httpsWebSocketUrl\": \"wss://localhost:8443%s\",\n" + + " \"httpUrl\": \"http://localhost:8080\",\n" + + " \"httpsUrl\": \"https://localhost:8443\"\n" + + "}", + currentToken, wsEndpoint, wsEndpoint, wsEndpoint + ); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); + } catch (Exception e) { + log.error("Error getting WebSocket info: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to get WebSocket information."); + } + } + @Tag(name = "STEP 02. Target Call To ACSUser", description = "Make a call to ACS User by using this endpoint") - @PostMapping(path = "/targetCallToAcsUser") + @PostMapping("/targetCallToAcsUser") public ResponseEntity createTargetCall(@RequestParam String acsTarget) { StringBuilder msgLog = new StringBuilder(); msgLog.append("\n~~~~~~~~~~~~ /TargetCall(Create) ~~~~~~~~~~~~\n"); @@ -362,7 +498,7 @@ public ResponseEntity createTargetCall(@RequestParam String acsTarget) { } @Tag(name = "STEP 03. Get Call Participants", description = "Get call participants for the lobby call connection by using this endpoint") - @GetMapping(path = "/getParticipants") + @GetMapping("/getParticipants") public ResponseEntity getParticipants() { StringBuilder msgLog = new StringBuilder(); msgLog.append("\n~~~~~~~~~~~~ /GetParticipants/").append(lobbyCallConnectionId).append(" ~~~~~~~~~~~~\n"); diff --git a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml index 91bdfa8..80a35a1 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml +++ b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml @@ -5,9 +5,20 @@ spring: springdoc: swagger-ui: tagsSorter: alpha + operationsSorter: alpha server: - port: 8080 + port: 8443 # HTTPS primary port + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: password + key-store-type: PKCS12 + key-alias: callautomation + enabled-protocols: TLSv1.2,TLSv1.3 + # HTTP port for additional connector + http: + port: 8080 applicationinsights: connection-string: "" diff --git a/CallAutomation_LobbyCall_Sample/src/main/resources/keystore.p12 b/CallAutomation_LobbyCall_Sample/src/main/resources/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..e324cc604d826e10827f922ddaa65c189b7ff981 GIT binary patch literal 2760 zcma)8c{tPy7oHh27)mrOzUTX%?(_Zieb0H$bKdj5=bS&^=Kur_1_aCk5I6!^;fh3a;x+=z z2F@XHct8mpmyT#>fB(fA0Ⓢ z|Fm3y5>((=fPUQZ<<<#GCcAAz2#JB_ox?&Pr+6S>DS(TW{ofZMa2N;>XN7wb&B4Ab zP_QDDXV1b6041&g111X~3%HmylmHzHD@wb|g6%LXQMkqLV#xONYc zcfaw6amMtQ;jo`?^=%G3?epBGtLAh<(`{a-u?n2@AlANnuzgVcK-WpEAh``W_tq1( zI^&D-kt`9iN2@a4brp<0Z$QwGQMs*i+ha$mVIO)emMk_@%)R%`{Va-0E8mL*j!-({ zZmu-tch^VQm-|2Mw|+$lf2sS9%tV^DLZs|Q6pFaxMMfaRJck(kTA;dK;v*hTJJdl2 ztdP$I?elG_+e$0H{-OpgpsL4NNmX>N(7u9iwfMMdo!l|AaWLbeJ3>BK%6p3FfEjOX zQ#uUJLwqGGr_e7H46Toe3}nA3Zse=j-K;3MNm8}oo?clhqn?t9Sl3EU$nf@rb2lCO zmz`_c@{!l4yC2H)W2^@X5ryu;3uOhbMdjebGz4m9{G7hCBEYVI= z1;a5qZHrz3ftVcB%ecrUm0a$$5NJmK@UvdwJW5bDFr?|JaHl4~?b5o8;m>fQlE6I?&ol|ZBJnsrem zSi-@ZA+xEGo>(hMMnzG*yiWyaP5DNm;qc3@vpNtO{bvmsBMObV}S-fLh*>W)C{njnc87_3sF!qzTyu>WP?<;rqmU;PM zl!E5`9MQ&U%jMFJm32Feg3!%+&AIWagQvUj)P!@vhlq*bPe6O(`&a4~I62Ki)B6fs ztPHh*D$Ir|`8SbG{9Dnk&sGBhRoK;{Ov#D6w}k@e zPl1>lp=t9U@??YB{?JVIF&PcSbr9REV!Y3%9B-9Kl}?I;_w}R)7e~%nSAaW(b+$h5 z%A(V`{e#G348E@h2DtNVS8;2Dsc{G3WeM6=PSU@YtD#D~6DAY9vok zqiHWdHMr_RZ*)oN9)l;+lS%ZDQn3hCy&bXN@|28U&N^GSot0Ss_Lhg;scdU7TH(dl zZtQJeW|n~{ZGYLdkkZR5=)iM2`km%}eWS2%bjz=F zL)nhV2WI%q8T^lB&N%IwgB0G}XYTxGZU(2%DFL&rRJju_g;6@g7Z)6_jNF;(-7AH6 zpW#)=7_QjJxR18ciOs;CLSz;h<&*fU1)B*bBc;Kx%emxE(EP!&uHKj#GxFUDwL67l zEBixa6K6Co2yF)We{35|UPMSfN_WX7J#lFsFUpD~KtWO%=5xq*S#gg`_SJ3pF|{ZR$koA; zgg+VMLubAyo{T-CNGB#Cg&pFG;)L}zc~?Sh$xXKgDc2GX?mnNjoi3B?A*$;po*b5W zRq~?0(&_%!MMy`Q zq!dHp(uwc@{l^i^<=RQR1NEk5O3>D{TY-Im6w89O;P{Ach0g2C0OfPZXv&4zt@TZ| zJ)Xb=$F|d2qSgl@K6chgea9P$s$m6*uPq&7TJ`viwWL1nDcr;T zyt-<%@PqZqq87D%|Drc}aTbdTPxJJ9%szw`|Am!;w4ccYU7aqAuE`VeO9QW|scY7V zp)5f=p3$9kPuwa$-j>@|*iGqGY!MG2)A(gR{T0{Zb(71%%XkKbp0}dvDm^hP3FT{n zp+5TADiKP!-Y|a{D8Fcfl2)(3$^5`FhE5mto$g1Y@r&`Vw>8yNTFLk)-@2Y`ri9N} zRDJvsU7^A_!=1^IkJX*Kbsn3wTI; znvefp+HVNyYe1V`mLNlc&#o+GNqFysPm|jLd?#ms>zDmjSmYdI<#=^?h;Kgos_j7(Ib~`N7Nzr z&+sM`t*X&4yNH!M2rt2FsVBw-)my&K^`C>hW!?K3WSW)jf;mjr@Qh`uj%Nn>Dabx} z)7&ar^?0T~ekm*~YfR-i&|0Jy&CZ>w^{RL=UL(F7Uq>-v ln#WnQc1V5?RA6Q90Dnh*cL`_fIc4IKI$4=Ivhptr`Ws@N>%ag2 literal 0 HcmV?d00001 From ac7be2720da92fca325b083816207268c7f74c5c Mon Sep 17 00:00:00 2001 From: nageshy Date: Tue, 29 Jul 2025 14:57:32 +0530 Subject: [PATCH 4/8] Updated code --- .../callautomation/HttpsConfig.java | 27 --------- .../callautomation/OpenApiConfig.java | 57 +++++++++++++++---- .../callautomation/ProgramSample.java | 37 +++--------- .../src/main/resources/application.yml | 32 +++++++---- 4 files changed, 75 insertions(+), 78 deletions(-) delete mode 100644 CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java deleted file mode 100644 index 1333df7..0000000 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/HttpsConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.communication.callautomation; - -import org.apache.catalina.connector.Connector; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.server.WebServerFactoryCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class HttpsConfig { - - @Value("${server.http.port:8080}") - private int httpPort; - - @Bean - public WebServerFactoryCustomizer servletContainer() { - return server -> { - // Add HTTP connector for dual protocol support - Connector httpConnector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - httpConnector.setScheme("http"); - httpConnector.setPort(httpPort); - httpConnector.setSecure(false); - server.addAdditionalTomcatConnectors(httpConnector); - }; - } -} \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java index 4d465ee..6f53aa7 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/OpenApiConfig.java @@ -3,33 +3,66 @@ 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 org.springframework.util.StringUtils; import java.util.List; +import java.util.ArrayList; @Configuration public class OpenApiConfig { + @Value("${devtunnel.url:}") + private String devTunnelUrl; + + @Value("${devtunnel.enabled:false}") + private boolean devTunnelEnabled; + + @Value("${server.port:8443}") + private int serverPort; + @Bean public OpenAPI customOpenAPI() { - Server httpsServer = new Server(); - httpsServer.setUrl("https://localhost:8443"); - httpsServer.setDescription("HTTPS Server (Primary)"); + List servers = new ArrayList<>(); + + // Local server + Server localServer = new Server(); + localServer.setUrl("http://localhost:" + serverPort); + localServer.setDescription("Local Server (HTTP on port " + serverPort + ")"); + servers.add(localServer); + + // Dev Tunnel server (only if configured) + if (devTunnelEnabled && StringUtils.hasText(devTunnelUrl)) { + Server devTunnelServer = new Server(); + devTunnelServer.setUrl(devTunnelUrl); + devTunnelServer.setDescription("Dev Tunnel Server (HTTPS via tunnel)"); + servers.add(devTunnelServer); + } - Server httpServer = new Server(); - httpServer.setUrl("http://localhost:8080"); - httpServer.setDescription("HTTP Server (Alternative)"); + String description = buildDescription(); return new OpenAPI() .info(new Info() .title("Call Automation Lobby Call Sample API") - .description("Azure Communication Services Call Automation API for managing lobby calls.\n\n" + - "**Server Selection:**\n" + - "- HTTPS Server (Primary): https://localhost:8443 - Secure connection\n" + - "- HTTP Server (Alternative): http://localhost:8080 - Non-secure connection\n\n" + - "**Note:** Both servers are now available. HTTPS uses a self-signed certificate.") + .description(description) .version("1.0.0")) - .servers(List.of(httpsServer, httpServer)); + .servers(servers); + } + + private String buildDescription() { + StringBuilder desc = new StringBuilder(); + desc.append("Azure Communication Services Call Automation API for managing lobby calls.\n"); + desc.append("**🚀 Server Access Information:**\n"); + desc.append("- **Local**: [http://localhost:").append(serverPort).append("/swagger-ui/index.html](http://localhost:").append(serverPort).append("/swagger-ui/index.html)\n"); + + if (devTunnelEnabled && StringUtils.hasText(devTunnelUrl)) { + desc.append("- **Dev Tunnel**: [").append(devTunnelUrl).append("/swagger-ui/index.html](").append(devTunnelUrl).append("/swagger-ui/index.html)\n"); + } else { + desc.append("- **Dev Tunnel**: Not configured (set DEVTUNNEL_URL environment variable)\n"); + } + + return desc.toString(); } } diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java index 6088af5..e88627f 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java @@ -431,37 +431,16 @@ public ResponseEntity setConfiguration(@RequestBody ConfigurationRequest } log.info("Initialized call automation client."); - return ResponseEntity.ok("Configuration set successfully. Initialized call automation client. WebSocket endpoint: ws://localhost:8080/ws/" + webSocketToken); + return ResponseEntity.ok("Configuration set successfully. Initialized call automation client.\n" + + "WebSocket endpoints available:\n" + + "- HTTP: ws://localhost:8080/ws/" + webSocketToken + "\n" + + "- HTTPS: wss://localhost:8443/ws/" + webSocketToken); } catch (Exception e) { log.error("Error configuring: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to configure call automation client."); } } - @Tag(name = "STEP 01. Get WebSocket Info", description = "Get current WebSocket endpoint information") - @GetMapping("/api/getWebSocketInfo") - public ResponseEntity getWebSocketInfo() { - try { - String currentToken = webSocketConfig != null ? webSocketConfig.getCurrentSocketToken() : "not-configured"; - String wsEndpoint = "/ws/" + currentToken; - String response = String.format( - "{\n" + - " \"webSocketToken\": \"%s\",\n" + - " \"webSocketEndpoint\": \"%s\",\n" + - " \"httpWebSocketUrl\": \"ws://localhost:8080%s\",\n" + - " \"httpsWebSocketUrl\": \"wss://localhost:8443%s\",\n" + - " \"httpUrl\": \"http://localhost:8080\",\n" + - " \"httpsUrl\": \"https://localhost:8443\"\n" + - "}", - currentToken, wsEndpoint, wsEndpoint, wsEndpoint - ); - return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); - } catch (Exception e) { - log.error("Error getting WebSocket info: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to get WebSocket information."); - } - } - @Tag(name = "STEP 02. Target Call To ACSUser", description = "Make a call to ACS User by using this endpoint") @PostMapping("/targetCallToAcsUser") public ResponseEntity createTargetCall(@RequestParam String acsTarget) { @@ -501,10 +480,10 @@ public ResponseEntity createTargetCall(@RequestParam String acsTarget) { @GetMapping("/getParticipants") public ResponseEntity getParticipants() { StringBuilder msgLog = new StringBuilder(); - msgLog.append("\n~~~~~~~~~~~~ /GetParticipants/").append(lobbyCallConnectionId).append(" ~~~~~~~~~~~~\n"); + msgLog.append("\n~~~~~~~~~~~~ /GetParticipants/").append(targetCallConnectionId).append(" ~~~~~~~~~~~~\n"); try { - CallConnection callConnection = acsClient.getCallConnection(lobbyCallConnectionId); + CallConnection callConnection = acsClient.getCallConnection(targetCallConnectionId); PagedIterable participantsResult = callConnection.listParticipants(); List participants = participantsResult.stream().collect(Collectors.toList()); @@ -540,9 +519,9 @@ public ResponseEntity getParticipants() { return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); } } catch (Exception ex) { - log.error("Error getting participants for call " + lobbyCallConnectionId + ": " + ex.getMessage()); + log.error("Error getting participants for call " + targetCallConnectionId + ": " + ex.getMessage()); return ResponseEntity.badRequest().body( - String.format("{\"Error\":\"%s\",\"CallConnectionId\":\"%s\"}", ex.getMessage(), lobbyCallConnectionId) + String.format("{\"Error\":\"%s\",\"CallConnectionId\":\"%s\"}", ex.getMessage(), targetCallConnectionId) ); } } diff --git a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml index 80a35a1..a3c613c 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml +++ b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml @@ -6,19 +6,31 @@ springdoc: swagger-ui: tagsSorter: alpha operationsSorter: alpha + tryItOutEnabled: true + supportedSubmitMethods: ["get", "post", "put", "delete", "patch"] + showCommonExtensions: false + showExtensions: false + api-docs: + enabled: true + show-actuator: false server: - port: 8443 # HTTPS primary port + port: 8443 # Primary port - accepts both HTTP and HTTPS ssl: - enabled: true - key-store: classpath:keystore.p12 - key-store-password: password - key-store-type: PKCS12 - key-alias: callautomation - enabled-protocols: TLSv1.2,TLSv1.3 - # HTTP port for additional connector - http: - port: 8080 + enabled: false # Disable SSL to allow HTTP requests from dev tunnels + # Forward headers for reverse proxy support (dev tunnels) + forward-headers-strategy: framework + # Tomcat configuration for reverse proxy and dev tunnels + tomcat: + remoteip: + remote-ip-header: x-forwarded-for + protocol-header: x-forwarded-proto + protocol-header-https-value: https + +# Dev tunnel configuration +devtunnel: + url: https://2zm3fjdh-8443.inc1.devtunnels.ms # Set via environment variable or leave empty for local only + enabled: true # Set to true when using dev tunnels applicationinsights: connection-string: "" From 59e437c4ccc57d00de03f4d323ed6169d596b01c Mon Sep 17 00:00:00 2001 From: Nikhil Malviya Date: Wed, 6 Aug 2025 21:37:32 +0530 Subject: [PATCH 5/8] Updated WebSocketConfig for single endpoint and refactored ProgramSample --- .../callautomation/ConfigurationRequest.java | 16 +- .../callautomation/LobbyWebSocketHandler.java | 8 +- .../callautomation/ProgramSample.java | 270 +++++------------- .../callautomation/WebSocketConfig.java | 20 +- .../src/main/resources/application.yml | 2 +- 5 files changed, 84 insertions(+), 232 deletions(-) diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java index c9dedaf..4814912 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ConfigurationRequest.java @@ -10,9 +10,7 @@ public class ConfigurationRequest { private String acsConnectionString; private String cognitiveServiceEndpoint; private String callbackUriHost; - private String pmaEndpoint; - private String acsGeneratedId; - private String webSocketToken; + private String acsGeneratedIdForTargetCallSender; // Getters and Setters public void setAcsConnectionString(String acsConnectionString) { @@ -27,15 +25,7 @@ public void setCallbackUriHost(String callbackUriHost) { this.callbackUriHost = callbackUriHost; } - public void setPmaEndpoint(String pmaEndpoint) { - this.pmaEndpoint = pmaEndpoint; - } - - public void setAcsGeneratedId(String acsGeneratedId) { - this.acsGeneratedId = acsGeneratedId; - } - - public void setWebSocketToken(String webSocketToken) { - this.webSocketToken = webSocketToken; + public void setAcsGeneratedIdForTargetCallSender(String acsGeneratedIdForTargetCallSender) { + this.acsGeneratedIdForTargetCallSender = acsGeneratedIdForTargetCallSender; } } diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java index 5b20c5e..db1f25b 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/LobbyWebSocketHandler.java @@ -5,12 +5,16 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.ArrayList; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component public class LobbyWebSocketHandler extends TextWebSocketHandler { - + private final List sessions = new ArrayList<>(); private static final Logger log = LoggerFactory.getLogger(LobbyWebSocketHandler.class); // Reference to ProgramSample for handling messages @@ -22,6 +26,7 @@ public void setProgramSample(ProgramSample programSample) { @Override public void afterConnectionEstablished(@NonNull WebSocketSession session) { + sessions.add(session); log.info("WebSocket connection established: " + session.getId()); } @@ -44,6 +49,7 @@ public void handleTransportError(@NonNull WebSocketSession session, @NonNull Thr @Override public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull org.springframework.web.socket.CloseStatus status) throws Exception { + sessions.remove(session); log.info("WebSocket connection closed: " + session.getId()); } } diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java index e88627f..7b76f43 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/ProgramSample.java @@ -39,11 +39,8 @@ @RestController public class ProgramSample { - private static final Logger log = LoggerFactory.getLogger(ProgramSample.class); private CallAutomationClient acsClient; - - @Autowired - private WebSocketConfig webSocketConfig; + private static final Logger log = LoggerFactory.getLogger(ProgramSample.class); @Autowired private LobbyWebSocketHandler lobbyWebSocketHandler; @@ -53,9 +50,12 @@ public class ProgramSample { private String acsConnectionString = ""; private String cognitiveServiceEndpoint = ""; private String callbackUriHost = ""; - private String pmaEndpoint; - private String acsGeneratedId = ""; - private String webSocketToken = ""; + private String acsGeneratedIdForTargetCallSender = ""; + private String acsGeneratedIdForTargetCallReceiver = ""; + private String acsGeneratedIdForLobbyCallReceiver = ""; + + private String textToPlayToLobbyUser = "You are currently in a lobby call, we will notify the admin that you are waiting."; + private String confirmMessageToTargetCall = "A user is waiting in lobby, do you want to add the lobby user to your call?"; private String lobbyCallConnectionId = ""; private String targetCallConnectionId = ""; @@ -69,11 +69,6 @@ public void init() { } } - // Getter method for webSocketToken - public String getWebSocketToken() { - return webSocketToken; - } - // Method to handle WebSocket messages (called by LobbyWebSocketHandler) public void handleWebSocketMessage(WebSocketSession session, TextMessage message) { String jsResponse = message.getPayload(); @@ -96,15 +91,14 @@ public void handleWebSocketMessage(WebSocketSession session, TextMessage message CommunicationIdentifier participantToMove; if (lobbyCallerId.startsWith("+")) { participantToMove = new PhoneNumberIdentifier(lobbyCallerId); - } else { + } else if (lobbyCallerId.startsWith("8:acs")) { participantToMove = new CommunicationUserIdentifier(lobbyCallerId); + } else { + log.error("Invalid participant identifier"); + return; } - MoveParticipantsOptions options = new MoveParticipantsOptions( - java.util.Collections.singletonList(participantToMove), - lobbyCallConnectionId - ); - targetConnection.moveParticipants(options); + targetConnection.moveParticipants(java.util.Collections.singletonList(participantToMove), lobbyCallConnectionId); // If no exception is thrown, the operation is considered successful log.info("Move Participants operation completed successfully."); } catch (Exception ex) { @@ -113,38 +107,25 @@ public void handleWebSocketMessage(WebSocketSession session, TextMessage message } } - @Tag(name = "STEP 00. Call Automation Events", description = "Configure Event Grid webhook to point to /api/lobbyCallEventHandler endpoint") - @PostMapping("/api/lobbyCallEventHandler") - public ResponseEntity lobbyCallEventHandler(@RequestBody final String reqBody) { - log.info("Lobby call event received"); - 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); - } - - private ResponseEntity handleSubscriptionValidation(final BinaryData eventData) { + 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(null); + + return ResponseEntity.ok().body(responseData); } catch (Exception e) { log.error("Error at subscription validation event {} {}", e.getMessage(), e.getCause()); - return ResponseEntity.internalServerError().body(null); + + return ResponseEntity.internalServerError().build(); } } - - private void handleIncomingCall(final BinaryData eventData) { + + private ResponseEntity handleIncomingCall(final BinaryData eventData) { log.info("Incoming call received"); JSONObject data = new JSONObject(eventData.toString()); String callbackUri = URI.create(callbackUriHost + "/api/callbacks").toString(); @@ -156,7 +137,7 @@ private void handleIncomingCall(final BinaryData eventData) { log.info("Incoming Call Context: " + incomingCallContext); // Lobby Call: Answer - if (toCallerId.contains(acsGeneratedId)) { + if (toCallerId.contains(acsGeneratedIdForTargetCallSender)) { StringBuilder msgLog = new StringBuilder(); try { AnswerCallOptions options = new AnswerCallOptions(incomingCallContext, callbackUri); @@ -174,91 +155,41 @@ private void handleIncomingCall(final BinaryData eventData) { .append("Lobby Call Connection Id: ").append(lobbyCallConnectionId).append("\n") .append("Correlation Id: ").append(answerCallResult.getCallConnection().getCallProperties().getCorrelationId()).append("\n") .append("Lobby Call answered successfully.\n"); + log.info(msgLog.toString()); + + return ResponseEntity.ok().build(); } catch (Exception ex) { msgLog.append("Error answering call: ").append(ex.getMessage()).append("\n"); + log.info(msgLog.toString()); + + return ResponseEntity.internalServerError().body(msgLog.toString()); } - log.info(msgLog.toString()); } + + return ResponseEntity.ok().build(); } - // @Tag(name = "STEP 00. Call Automation Events", description = "Call Back Events") - // @PostMapping("/api/callbacks") - // public ResponseEntity callbackEvents(@RequestBody final String reqBody) { - // StringBuilder msgLog = new StringBuilder(); - // try { - // List events = CallAutomationEventParser.parseEvents(reqBody); - // for (CallAutomationEventBase event : events) { - // String callConnectionId = event.getCallConnectionId(); - // log.info( - // "Received call event callConnectionID: {}, serverCallId: {}, CorrelationId: {}, eventType: {}", - // callConnectionId, - // event.getServerCallId(), - // event.getCorrelationId(), - // event.getClass().getSimpleName()); - - // // Parse the event using the Azure SDK's parser if available - // // For illustration, we use eventType string matching - // if (event instanceof CallConnected) { - // String operationContext = ((CallConnected) event).getOperationContext(); - // String correlationId = ((CallConnected) event).getCorrelationId(); - - // log.info("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ "); - // log.info("Received callConnected.CallConnectionId : " + callConnectionId); - - // if ("LobbyCall".equals(operationContext)) { - // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") - // .append("Received call event : ").append(event.getClass()).append("\n") - // .append("Lobby Call Connection Id: ").append(callConnectionId).append("\n") - // .append("Correlation Id: ").append(correlationId).append("\n"); - - // // Record lobby caller id and connection id - // CallConnection lobbyCallConnection = acsClient.getCallConnection(callConnectionId); - // CallConnectionProperties callConnectionProperties = lobbyCallConnection.getCallProperties(); - // lobbyCallerId = callConnectionProperties.getSource().getRawId(); - // lobbyCallConnectionId = callConnectionProperties.getCallConnectionId(); - // log.info("Lobby Caller Id: " + lobbyCallerId); - // log.info("Lobby Connection Id: " + lobbyCallConnectionId); - - // // Play lobby waiting message - // CallMedia callMedia = lobbyCallConnection.getCallMedia(); - // TextSource textSource = new TextSource().setText("You are currently in a lobby call, we will notify the admin that you are waiting."); - // textSource.setVoiceName("en-US-NancyNeural"); - // CommunicationUserIdentifier playTo = new CommunicationUserIdentifier(lobbyCallerId); - // callMedia.play(textSource, List.of(playTo)); - // } - // } else if (event instanceof PlayCompleted) { - // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") - // .append("Received event: ").append(event.getClass()).append("\n"); - - // // Notify Target Call user via websocket - // // In Java/Spring, you would use a WebSocket messaging template or similar - // // For now, just log the message - // String confirmMessageToTargetCall = "Target Call user has been notified that the lobby call is connected."; - // msgLog.append("Target Call notified with message: ").append(confirmMessageToTargetCall).append("\n"); - // log.info("Target Call notified with message: " + confirmMessageToTargetCall); - // return ResponseEntity.ok("Target Call notified with message: " + confirmMessageToTargetCall); - // // } else if (event instanceof MoveParticipantsSucceeded) { - // // String correlationId = event.getCorrelationId(); - // // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") - // // .append("Received event: ").append(event.getClass()).append("\n") - // // .append("Call Connection Id: ").append(callConnectionId).append("\n") - // // .append("Correlation Id: ").append(correlationId).append("\n"); - // } else if (event instanceof CallDisconnected) { - // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") - // .append("Received event: ").append(event.getClass()).append("\n") - // .append("Call Connection Id: ").append(callConnectionId).append("\n"); - // } - // } - // } catch (Exception ex) { - // msgLog.append("Error processing event: ").append(ex.getMessage()).append("\n"); - // } - // log.info(msgLog.toString()); - // return ResponseEntity.ok(msgLog.toString()); - // } + @Tag(name = "STEP 00. Call Automation Events", description = "Configure Event Grid webhook to point to /api/lobbyCallEventHandler endpoint") + @PostMapping("/api/lobbyCallEventHandler") + public ResponseEntity lobbyCallEventHandler(@RequestBody final String reqBody) { + log.info("Lobby call event received"); + List events = EventGridEvent.fromString(reqBody); + var response = ResponseEntity.ok().build(); + + for (EventGridEvent eventGridEvent : events) { + if (eventGridEvent.getEventType().equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION)) { + response = handleSubscriptionValidation(eventGridEvent.getData()); + } else if (eventGridEvent.getEventType().equals(SystemEventNames.COMMUNICATION_INCOMING_CALL)) { + response = handleIncomingCall(eventGridEvent.getData()); + } + } + + return response; + } @Tag(name = "STEP 00. Call Automation Events", description = "Call Back Events") @PostMapping("/api/callbacks") - public ResponseEntity callbackEventsOld(@RequestBody final String reqBody) { + public ResponseEntity callbackEvents(@RequestBody final String reqBody) { StringBuilder msgLog = new StringBuilder(); try { List events = CallAutomationEventParser.parseEvents(reqBody); @@ -279,7 +210,6 @@ public ResponseEntity callbackEventsOld(@RequestBody final String reqBod log.info("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ "); log.info("Received callConnected.CallConnectionId : " + callConnectionId); - log.info("Operation Context : " + operationContext); if ("LobbyCall".equals(operationContext)) { msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") @@ -300,70 +230,37 @@ public ResponseEntity callbackEventsOld(@RequestBody final String reqBod TextSource textSource = new TextSource().setText("You are currently in a lobby call, we will notify the admin that you are waiting."); textSource.setVoiceName("en-US-NancyNeural"); CommunicationUserIdentifier playTo = new CommunicationUserIdentifier(lobbyCallerId); - PlayOptions playToOptions = new PlayOptions(textSource, List.of(playTo)); - playToOptions.setOperationContext("playToContext"); callMedia.play(textSource, List.of(playTo)); } } else if (event instanceof PlayCompleted) { msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") .append("Received event: ").append(event.getClass()).append("\n"); - // Move Participant logic - try { - msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~\n") - .append("Move Participant operation started..\n") - .append("Source Caller Id: ").append(lobbyCallerId).append("\n") - .append("Source Connection Id: ").append(lobbyCallConnectionId).append("\n") - .append("Target Connection Id: ").append(targetCallConnectionId).append("\n"); - - CallConnection targetConnection = acsClient.getCallConnection(targetCallConnectionId); - // CallConnection sourceConnection = client.getCallConnection(lobbyConnectionId); - - CommunicationIdentifier participantToMove; - if (lobbyCallerId.startsWith("+")) { - participantToMove = new PhoneNumberIdentifier(lobbyCallerId); - } else if (lobbyCallerId.startsWith("8:acs:")) { - participantToMove = new CommunicationUserIdentifier(lobbyCallerId); - } else { - return ResponseEntity.badRequest().body("Invalid participant format. Use phone number (+1234567890) or ACS user ID (8:acs:...)"); - } - - MoveParticipantsOptions options = new MoveParticipantsOptions( - java.util.Collections.singletonList(participantToMove), - lobbyCallConnectionId - ); - targetConnection.moveParticipants(options); - // If no exception is thrown, the operation is considered successful - msgLog.append("\nMove Participants operation completed successfully.\n"); - - log.info(msgLog.toString()); - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); - } catch (Exception ex) { - log.info("Error in manual move participants operation: " + ex.getMessage()); - return ResponseEntity.badRequest().body( - String.format("{\"Success\":false,\"Error\":\"%s\",\"Message\":\"Move participants operation failed.\"}", ex.getMessage()) - ); - } - } else if (event instanceof MoveParticipantSucceeded) { - String correlationId = event.getCorrelationId(); - msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") - .append("Received event: ").append(event.getClass()).append("\n") - .append("Call Connection Id: ").append(callConnectionId).append("\n") - .append("Correlation Id: ").append(correlationId).append("\n"); - } else if (event instanceof CallDisconnected) { + // Notify Target Call user via websocket + // In Java/Spring, you would use a WebSocket messaging template or similar + // For now, just log the message + String confirmMessageToTargetCall = "Target Call user has been notified that the lobby call is connected."; + msgLog.append("Target Call notified with message: ").append(confirmMessageToTargetCall).append("\n"); + log.info("Target Call notified with message: " + confirmMessageToTargetCall); + return ResponseEntity.ok("Target Call notified with message: " + confirmMessageToTargetCall); + // } else if (event instanceof MoveParticipantsSucceeded) { + // String correlationId = event.getCorrelationId(); + // msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") + // .append("Received event: ").append(event.getClass()).append("\n") + // .append("Call Connection Id: ").append(callConnectionId).append("\n") + // .append("Correlation Id: ").append(correlationId).append("\n"); + } + else if (event instanceof CallDisconnected) { msgLog.append("~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ \n") .append("Received event: ").append(event.getClass()).append("\n") - .append("Call Connection Id: ").append(((CallDisconnected) event).getCallConnectionId()).append("\n"); + .append("Call Connection Id: ").append(callConnectionId).append("\n"); } } } catch (Exception ex) { msgLog.append("Error processing event: ").append(ex.getMessage()).append("\n"); } - - if (msgLog.length() > 0) { - log.info(msgLog.toString()); - } - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(msgLog.toString()); + log.info(msgLog.toString()); + return ResponseEntity.ok(msgLog.toString()); } @Tag(name = "STEP 01. Set Configuration", description = "Assign the global variables for the Call Automation sample") @@ -374,9 +271,7 @@ public ResponseEntity setConfiguration(@RequestBody ConfigurationRequest acsConnectionString = ""; cognitiveServiceEndpoint = ""; callbackUriHost = ""; - pmaEndpoint = ""; - acsGeneratedId = ""; - webSocketToken = ""; + acsGeneratedIdForTargetCallSender = ""; lobbyCallConnectionId = ""; lobbyCallerId = ""; @@ -392,49 +287,28 @@ public ResponseEntity setConfiguration(@RequestBody ConfigurationRequest .filter(s -> !s.isEmpty()) .orElseThrow(() -> new IllegalArgumentException("CognitiveServiceEndpoint is required")) ); - configuration.setPmaEndpoint( - Optional.ofNullable(configurationRequest.getPmaEndpoint()) - .filter(s -> !s.isEmpty()) - .orElseThrow(() -> new IllegalArgumentException("PmaEndpoint is required")) - ); configuration.setCallbackUriHost( Optional.ofNullable(configurationRequest.getCallbackUriHost()) .filter(s -> !s.isEmpty()) .orElseThrow(() -> new IllegalArgumentException("CallbackUriHost is required")) ); - configuration.setAcsGeneratedId( - Optional.ofNullable(configurationRequest.getAcsGeneratedId()) + configuration.setAcsGeneratedIdForTargetCallSender( + Optional.ofNullable(configurationRequest.getAcsGeneratedIdForTargetCallSender()) .filter(s -> !s.isEmpty()) .orElseThrow(() -> new IllegalArgumentException("AcsGeneratedId is required")) ); - configuration.setWebSocketToken( - Optional.ofNullable(configurationRequest.getWebSocketToken()) - .filter(s -> !s.isEmpty()) - .orElseThrow(() -> new IllegalArgumentException("WebSocketToken is required")) - ); } // Assign to global variables acsConnectionString = configuration.getAcsConnectionString(); cognitiveServiceEndpoint = configuration.getCognitiveServiceEndpoint(); callbackUriHost = configuration.getCallbackUriHost(); - pmaEndpoint = configuration.getPmaEndpoint(); - acsGeneratedId = configuration.getAcsGeneratedId(); - webSocketToken = configuration.getWebSocketToken(); + acsGeneratedIdForTargetCallSender = configuration.getAcsGeneratedIdForTargetCallSender(); - acsClient = initClient(pmaEndpoint, acsConnectionString); - - // Update WebSocket configuration with the new token - if (webSocketConfig != null) { - webSocketConfig.updateWebSocketEndpoint(webSocketToken); - log.info("WebSocket endpoint updated with token: /ws/{}", webSocketToken); - } + acsClient = initClient(acsConnectionString); log.info("Initialized call automation client."); - return ResponseEntity.ok("Configuration set successfully. Initialized call automation client.\n" + - "WebSocket endpoints available:\n" + - "- HTTP: ws://localhost:8080/ws/" + webSocketToken + "\n" + - "- HTTPS: wss://localhost:8443/ws/" + webSocketToken); + return ResponseEntity.ok("Configuration set successfully. Initialized call automation client."); } catch (Exception e) { log.error("Error configuring: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to configure call automation client."); @@ -555,7 +429,7 @@ private CallConnection getCallConnection(CallAutomationClient client, String cal return client.getCallConnection(callConnectionId); } - private CallAutomationClient initClient(String endpoint, String connectionString) { + private CallAutomationClient initClient(String connectionString) { try { if (connectionString == null || connectionString.trim().isEmpty()) { log.error("ACS Connection String is null or empty"); @@ -565,9 +439,9 @@ private CallAutomationClient initClient(String endpoint, String connectionString log.info("Initializing Call Automation Client with connection string length: {}", connectionString.length()); var client = new CallAutomationClientBuilder() - .endpoint(endpoint) .connectionString(connectionString) .buildClient(); + log.info("Call Automation Client initialized successfully."); return client; } catch (NullPointerException e) { diff --git a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java index 5e00231..2611fb8 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java +++ b/CallAutomation_LobbyCall_Sample/src/main/java/com/communication/callautomation/WebSocketConfig.java @@ -14,27 +14,9 @@ public class WebSocketConfig implements WebSocketConfigurer { @Autowired private LobbyWebSocketHandler lobbyWebSocketHandler; - private WebSocketHandlerRegistry currentRegistry; - private String currentSocketToken = "default"; - @Override public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) { - this.currentRegistry = registry; // Initial registration with default token - registry.addHandler(lobbyWebSocketHandler, "/ws/" + currentSocketToken).setAllowedOrigins("*"); - } - - public void updateWebSocketEndpoint(String newToken) { - if (newToken != null && !newToken.isEmpty() && !newToken.equals(currentSocketToken)) { - this.currentSocketToken = newToken; - // Re-register with new token - if (currentRegistry != null) { - currentRegistry.addHandler(lobbyWebSocketHandler, "/ws/" + newToken).setAllowedOrigins("*"); - } - } - } - - public String getCurrentSocketToken() { - return currentSocketToken; + registry.addHandler(lobbyWebSocketHandler, "/ws").setAllowedOrigins("*"); } } \ No newline at end of file diff --git a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml index a3c613c..4dc1e2f 100644 --- a/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml +++ b/CallAutomation_LobbyCall_Sample/src/main/resources/application.yml @@ -29,7 +29,7 @@ server: # Dev tunnel configuration devtunnel: - url: https://2zm3fjdh-8443.inc1.devtunnels.ms # Set via environment variable or leave empty for local only + url: # Set via environment variable or leave empty for local only enabled: true # Set to true when using dev tunnels applicationinsights: From 1e0b7b871ed55925cea522d9f9565e24353f1d17 Mon Sep 17 00:00:00 2001 From: v-kuppu Date: Mon, 11 Aug 2025 12:23:21 +0530 Subject: [PATCH 6/8] Update README.md - removed sensitive data --- CallAutomation_LobbyCall_Sample/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CallAutomation_LobbyCall_Sample/README.md b/CallAutomation_LobbyCall_Sample/README.md index 142fc4f..cb13067 100644 --- a/CallAutomation_LobbyCall_Sample/README.md +++ b/CallAutomation_LobbyCall_Sample/README.md @@ -32,6 +32,5 @@ In the swagger app, provide these values for the setConfiguration endpoint to co 1. `acsConnectionString`: Azure Communication Service resource's connection string. 2. `cognitiveServiceEndpoint`: Cognitive service endpoint. 3. `callbackUriHost`: Base url of the app. (For local development replace the dev tunnel url) -4. `pmaEndpoint` : PMA service endpoint. -5. `acsGeneratedId`: Communication Identifier generated through the ACS. -6. `webSocketToken`: Web Socket Token Key. \ No newline at end of file +4. `acsGeneratedId`: Communication Identifier generated through the ACS. +5. `webSocketToken`: Web Socket Token Key. From f1f2f023d737326f4d34ab2ddc42f463f63a9959 Mon Sep 17 00:00:00 2001 From: nageshy Date: Thu, 23 Oct 2025 09:58:48 +0530 Subject: [PATCH 7/8] Updated readme --- CallAutomation_LobbyCall_Sample/README.md | 157 +++++++++++++++++++--- 1 file changed, 139 insertions(+), 18 deletions(-) diff --git a/CallAutomation_LobbyCall_Sample/README.md b/CallAutomation_LobbyCall_Sample/README.md index cb13067..d4bca93 100644 --- a/CallAutomation_LobbyCall_Sample/README.md +++ b/CallAutomation_LobbyCall_Sample/README.md @@ -4,33 +4,154 @@ # Call Automation - Lobby Call Sample -In this sample, we cover how you can use Call Automation SDK Lobby Call Sample. +This sample demonstrates how to implement a lobby call feature using Azure Communication Services Call Automation SDK. The application manages incoming calls by placing callers in a lobby, playing a waiting message, and then moving participants to a target call when approved. -### Setup and host your Azure DevTunnel +## Features -[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. +- **Lobby Call Management**: Automatically answer incoming calls and place them in a lobby +- **Text-to-Speech**: Play waiting messages to lobby participants using Azure Cognitive Services +- **Participant Management**: Move participants between lobby and target calls +- **WebSocket Support**: Real-time communication for call state updates +- **Event-Driven Architecture**: Handle Call Automation events via webhooks +- **Dev Tunnel Integration**: Easy development with Azure Dev Tunnels for webhook delivery + +## Prerequisites + +- Java 17 or later +- Maven 3.6 or later +- Azure Communication Services resource +- Azure Cognitive Services resource (for text-to-speech) +- Azure Dev Tunnels CLI (for local development) + +## Setup and Configuration + +### 1. Setup Azure Dev Tunnel + +[Azure DevTunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) enables you to expose your local development server to the internet for webhook delivery. ```bash +# Create a new dev tunnel devtunnel create --allow-anonymous -devtunnel port create -p 8080 + +# Create a port mapping for the application +devtunnel port create -p 8443 + +# Start the tunnel devtunnel host ``` -### Run the application +Copy the HTTPS URL provided by dev tunnel (e.g., `https://abc123-8443.inc1.devtunnels.ms`) for use in configuration. + +### 2. Build and Run the Application + +Navigate to the project directory and run: + +```bash +# Compile the application +mvn compile + +# Build the package +mvn package + +# Run the application +mvn spring-boot:run +``` + +Alternative execution method: +```bash +mvn exec:java +``` + +The application will start on port 8443 and be accessible at: +- **Local**: http://localhost:8443/swagger-ui/index.html +- **Dev Tunnel**: https://your-tunnel-url/swagger-ui/index.html + +### 3. Configure Application Settings + +Use the Swagger UI to configure the application via the `/api/setConfiguration` endpoint: + +**Required Configuration Parameters:** + +1. **`acsConnectionString`**: Your Azure Communication Services connection string + - Format: `endpoint=https://your-acs-resource.communication.azure.com/;accesskey=your-access-key` + +2. **`cognitiveServiceEndpoint`**: Azure Cognitive Services endpoint for text-to-speech + - Format: `https://your-cognitive-service.cognitiveservices.azure.com/` + +3. **`callbackUriHost`**: Your application's base URL for webhook callbacks + - Local: `http://localhost:8443` + - Dev Tunnel: `https://your-tunnel-url` (without trailing slash) + +4. **`pmaEndpoint`**: Azure Communication Services endpoint + - Format: `https://your-acs-resource.communication.azure.com` + +5. **`acsGeneratedId`**: Communication user ID for receiving lobby calls + - Generate using ACS Identity SDK or Azure portal + +6. **`webSocketToken`**: Unique token for WebSocket endpoint security + - Use any unique string (e.g., UUID or custom token) + +## API Endpoints + +The application provides the following REST endpoints: + +### Core Endpoints +- **`POST /api/setConfiguration`** - Configure application settings +- **`POST /api/lobbyCallEventHandler`** - EventGrid webhook for incoming calls +- **`POST /api/callbacks`** - Call Automation event callbacks +- **`POST /targetCallToAcsUser`** - Create a target call to an ACS user +- **`GET /getParticipants`** - List participants in the lobby call +- **`GET /terminateCalls`** - Terminate all active calls + +### WebSocket Endpoint +- **`/ws/{webSocketToken}`** - Real-time communication endpoint + +## Usage Flow + +1. **Configure the application** using `/api/setConfiguration` +2. **Set up EventGrid webhook** to point to `/api/lobbyCallEventHandler` +3. **Create a target call** using `/targetCallToAcsUser` with an ACS user ID +4. **Incoming calls** are automatically answered and placed in lobby +5. **Lobby participants** hear a waiting message via text-to-speech +6. **Participants are automatically moved** to the target call after the message completes + +## Development Features + +### Environment Variables +Configure dev tunnel support using environment variables: +- `DEVTUNNEL_URL`: Your dev tunnel URL +- `DEVTUNNEL_ENABLED`: Set to `true` to enable dev tunnel features + +### WebSocket Integration +The application includes WebSocket support for real-time updates. WebSocket endpoints are dynamically configured based on the `webSocketToken` parameter. + +### Dynamic Server Configuration +The Swagger UI automatically detects and displays available servers (local and dev tunnel) based on your configuration. + +## Technology Stack + +- **Spring Boot 3.0.6** - Web framework +- **Azure Communication Services Call Automation SDK** - Call management +- **Azure EventGrid** - Event handling +- **WebSocket** - Real-time communication +- **SpringDoc OpenAPI** - API documentation +- **Maven** - Build tool +- **Java 17** - Runtime environment + +## Troubleshooting + +### Common Issues -- 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 and POST methods to run the Sample Application +1. **Dev Tunnel 502 Errors**: Ensure your application is running on port 8443 before starting the tunnel +2. **Webhook Delivery Failures**: Verify your `callbackUriHost` matches your dev tunnel URL exactly +3. **Audio Issues**: Confirm your Cognitive Services endpoint is correctly configured +4. **Connection Issues**: Check that your ACS connection string is valid and has the required permissions -### Configuring settings +### Logs and Monitoring +The application provides detailed logging for all call events and operations. Check the console output for debugging information. -In the swagger app, provide these values for the setConfiguration endpoint to configure settings +## Additional Resources -1. `acsConnectionString`: Azure Communication Service resource's connection string. -2. `cognitiveServiceEndpoint`: Cognitive service endpoint. -3. `callbackUriHost`: Base url of the app. (For local development replace the dev tunnel url) -4. `acsGeneratedId`: Communication Identifier generated through the ACS. -5. `webSocketToken`: Web Socket Token Key. +- [Azure Communication Services Documentation](https://docs.microsoft.com/en-us/azure/communication-services/) +- [Call Automation SDK Reference](https://docs.microsoft.com/en-us/azure/communication-services/concepts/call-automation/) +- [Azure Dev Tunnels Documentation](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/) From 5e80c4c2ce53ddf75a1e840f48963514bb18b2b3 Mon Sep 17 00:00:00 2001 From: "Kishore Uppu (Centific Technologies Inc)" Date: Mon, 27 Oct 2025 01:37:30 +0530 Subject: [PATCH 8/8] Lobby Call Support Sample - Addressed review comments --- CallAutomation_LobbyCall_Sample/README.md | 8 +++++++- .../resources/Lobby_Call_Support_Scenario.jpg | Bin 0 -> 128065 bytes 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 CallAutomation_LobbyCall_Sample/resources/Lobby_Call_Support_Scenario.jpg diff --git a/CallAutomation_LobbyCall_Sample/README.md b/CallAutomation_LobbyCall_Sample/README.md index d4bca93..8491db3 100644 --- a/CallAutomation_LobbyCall_Sample/README.md +++ b/CallAutomation_LobbyCall_Sample/README.md @@ -4,7 +4,8 @@ # Call Automation - Lobby Call Sample -This sample demonstrates how to implement a lobby call feature using Azure Communication Services Call Automation SDK. The application manages incoming calls by placing callers in a lobby, playing a waiting message, and then moving participants to a target call when approved. +This sample demonstrates how to utilize the Call Automation SDK to implement a Lobby Call scenario. Users join a lobby call and remain on hold until an user in the target call confirms their participation. Once approved, Call Automation (bot) automatically connects the lobby users to the designated target call. +The sample uses a client application (java script sample) available in [Web Client Quickstart](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/users/v-kuppu/LobbyCallConfirmSample). ## Features @@ -15,6 +16,11 @@ This sample demonstrates how to implement a lobby call feature using Azure Commu - **Event-Driven Architecture**: Handle Call Automation events via webhooks - **Dev Tunnel Integration**: Easy development with Azure Dev Tunnels for webhook delivery +# Design + +![Lobby Call Support](./resources/Lobby_Call_Support_Scenario.jpg) + + ## Prerequisites - Java 17 or later diff --git a/CallAutomation_LobbyCall_Sample/resources/Lobby_Call_Support_Scenario.jpg b/CallAutomation_LobbyCall_Sample/resources/Lobby_Call_Support_Scenario.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92225d4e602310278e4a69aed681d388a4290064 GIT binary patch literal 128065 zcmeFa2V9fOwkRGqVpl{6D#a3-geD~*U4?*jAqk-clul@p(Cgk-x&j8ID@aKKL3#;A zrFSWzhbX-xy~B%V-?R6=|9j4P=a&C{?|0vx{K&VMS>McBYu3!HDQj}rfA|$}UPDz~ z6>#JT0C0r<0UVAV=~q`)Hovd0tE#RA{WYNxKo3W60sv0VZWw*FyVnejjISO4`s*7% z(jHo3AN_d#oj}ibqxVPd06@3!-@*Au(I>5}v6gg%@AN+&483qVv8;5M)%I8T#t+!y zS6KcB?CJK%jgE8w2aJL1E7M^sI()zfaMDSaLVK_Y4oqZL)$fa5+|K57y8E* z;0Uk=Tmz^BoB@^q5jrFexCsygNF5FVlmQGs{Rlt48R+5Yv7eR^- zCr_R_!*u4<>2s$~o;=HZ_8ims^UUW@ow>kr;XDf+KK~<=BR}3`IC_Hq;`!4jPt#-n zo#F5!fceBxnG@F-j(`9^F&|-IK5|$K;G&EBCx#>R@KHi;`mQYfFnOK9A!LqocY9s>tZKaSTEhtGjP2K5`P%=y0aU|Eg^juj&S$* zB{H?3u!~LVmOjz~miI1>@WrJi zS}Xp_u4t~McB!kxQ`q9*Y1Cj&Q-9?`WL_XUIdBo;Z24%T7-<^da`9rb0?RK+CR-sO zUy1g>8}0hvugPp>YEuM0JcF+YE^-87)D8i0D{>*4gxtbf0cQCY_{1T=tuIOJ$;<5> z-Q&?nQ8z}p+^Iu=Q{W*0_kz)L`F({CuiA2~-;?^Nfp6Y1QWPn(rI8;5|~TLlxu$F)C^x4O&}E~IU>RNWa<_gd3s6BhpE5Uku*0gKCmz#0Ze@R`T`ir8_Hu`HGfOH@k`%)$AKz!+y*Kaxg zg`#BI^i?*$qDO{5BLmqr8&ctcmpvQhf6H+u?5b~R# z{K{`Ry45pP&hVG)9Is0eHf6g8r2WP;({DM-S{@%+iyqo%810B5oj!kR>34R1%aKd$ z$&!!ypA!00LVxPepRMfAe(2Ah`qwe^&#~;!(f&_+=ueCBHv{-ji}5d3`hU)*er{Kq zLDVIFdsMvxoTY8lr%L`Xd2w&xy)zD8vTrecHVegZsdFQC9vCp=<5|RS%Y}~gqr#yvsYtziFm^P_Qz>W)BS#$ z()Y|Mq8qR^yrx--p}Ay~C^tVo+-x&<6@)&}rueI+=%yRb5SW})P0S%x z;d5;*=KQ`Y2jOxosXLuj`85{oo-ZDJ@Jw`WIx8QNp!lw-Lq4U;XMJcyC)r9$xur}x zPtK(;<+Ie?*DIQr(-rpRLI@RyfIT+$zEFVPHmGxu-Ji(~70?S4+WLXhkf0_ckSQ zr=r{sdFee`JesrUbMFR^n`;rv%Jq%>=OI!n4QdRq*8a}(?PtnV6yk*jOY%c36vNC; zYi<;pA36TGYd`1WN97x*XJP$yUkj@ppx@7X35yPP`W+Sj8U1J6pECGU5B_WmfA$A| z_KE*<#)a+4{DE1e8Mb&C&ja~0p1b#VZ#M%I+z^+wlub%!ic_eOeoAKw?oI4UrdgVQ zj}NLIvEQs>Un`%d7;Czt2+&~Npn99TNVlLkWz9pi@E3|xkeHdX6R zl6Jl0Gt(?J`kdWe^$IGlRs^Q-RoL(IzD0)`2lTo*hZfpO9s;NwV=FJ8YBWFjR4ZZX zd#jkYy~Z#K)EaCqJ| zK*5h~*xv&K@4wp5isOC+xb}PM{|35=MCuLQm@G8~TV`ftS~LmeqT=f_(S+bPLG5}7 zJr!+)jviB6{g|@dyZ9+AAtF8|DP-F^(!=3Csm#DP&Ou@SQN_75ABs=vu1At*qcV?!@@JIpXcsOfqEtU5_%Qxy_K7FMAxme5pm2pBpvhivg9BRDBJR&HeFJQ#+aqu_HQ9V~#uy5l9tJ zr`qjvJ?r$~KKnFPAXPSPJ&q=^{0d-H{=X5^|JBJ?{5L+?H04AMEcH%1*iGrWu;R;I z#^Gi?@D;GT@|T}16{2!l+)x>IWz1r7eEg0YMB8#`%=CKQKox^)R{Q`1uH9fgCN8B8 zR~eOW+G9yNxQLJD`cOcHaK!y@0Fg-)8C_652yb9wj$Ld;nNE|FOEn;r>Pk z1(&1JW;@p3G(|}Re)?nTKZHp;b@9XQ47{1$LNfAvtXYow*$!Q)?=BVuj!F;+8_pq> zK_T~yX(C?h^|5LVsE?~0iFLqHQdZ%< zKA*HILz1lnUDiBS@!w{&wEB<(P93X$WKPADN?#pI{P@O&Z2X(vO1L|Wx=q<_FmT${ zavP;&^v?;y?+rNcsYI0%oeR`UqFZ}r!*%y4+7%2M@fL5Ix$0R?+A>%qN?l1Z76?n_ z-a7;|0(d|CLl*y?iR=e&tyUj;A*KW$N8F<>=JG=cO1X%8eKL$nMaL z1vMao%hZAOyGBB>swf)l9SRczs!+-_BN1>dJsBMnMReR)!>b(z=dHOzfR)$sxZ=-4 zRrJaF0N;FyieywYI~f79-;bAh_b&O~$enwRqoahlad_d2;&kZh#E>RUJ%{;4P zQ1QK zd!Jk@N~`VkI^lR1{0_f&c3NcP)E&N|h1LWt1V)gvqUEZ<;bOE;W;NY8rjCwZ*QzdZ znM7u_zcI;8T==`c)(u6u(5`IGmafk=nqIE1uOJI~U)? z#Z|4l(Blc7v459EAmEX*$;TCf7T0=<^i0QT)@D{J@FBude%RUB(YOq0qNmn3AqomY zdEK4w5s*Z@c_QTw#9}+^&DjR=HWSBG>$9eLo6R<|w}#HR7PVu`k{8?ha&>}~y>rYBnq+;?GR6*OZasF%btPgTcNlx7yB^iscp9c!GXQ!-9 z(u9d#r_#cg@kR=dXItYxDKpNVvQ>AAO3L?ws8rn>wSse7C2lZ51J?D5@a*tS*Tdw;^v(cUD5$ zM|F!oY2J1upINsxGTzZasZf7# z)`909eh$w$o@oU1a4>5i5T(r$hno{}QPKEo4pk-k(-RqymW7&+%BcDVokgbM&qCT! zRM5uSq;@1rt{Hw1YN**W917#GcJP6n#?+456RF{c0H!orTCcElsf|nbtPrlid_d@x zY-C}<{7Xwjhl}6%l0Wms`HBROgt*purTa^7oL$eo)mPIs!iE@Sz=>)yQHrW%oTLd1 zcsJc_q^tMQZmrHkqD;7fTwtNmjQ!WQ{wC&XoH;&aSL)|0R%E0GTE?aFjAe;pxvKp^ zgR5@NDQOpx(w^2Z8C5;G@`A1G8^@lI;(B&GGmw7IpQ~77-wYZ{2eR2;#BnC?!aM`5 zR;N>)5v;H(v-P>c{u0FCriu+^-8OhnLR--K!x#OgfXcgMZQY}I&YPmkRipf6<+3cDEaaUK%ho3eX;%h@_bY~*Kzs*>00FM)=W#9Bmv@Dw+3^Wb78DAT#8nYt zAtSAV*yWdLOvp__HlhZ(fL(I#XFRqf$FBJ4mEgD(s$|mPo8vx$6#sQphw;QEEjBn( zrwHY2&g7{+0A?=%nP0aP8r#v`9kj zyvhKIqIdFZ*M~N)?tR6oKC4UN(I3Ii=igs|@M6at^oCm+BM=i*4=S_A20s7#xI=Ot z+-@QwWbT+4?|{RkyPR3UX4X3&X}2j94jDiOfn4&}mG4eSnv+?-&9q3DSddGFB>P_G z%0!e#ka9YAc|Pg~mv&L?IcFYx7}HY;?UZNpbec2UU%B8B;%Yj)atP23nvC%=asOO2 z{2B+3z}yi^zBf!)H=ti=YZkHlUDV{uYI;ks#4?5}Jg&wFcf)Jj=fCmH_1^KWTT9;e z|A1_Qnrm`uo>@-CiWwMy7RtdM;CCk<$ zezAvQer@-2NZUV*;_`R!Pq)aEaz&SnxWFci+ z;P+lcxJXd;17(`;wHcV|(7TXv`Eyrm0Eq+-~_vJ%xgcD~q8M zP^h%vL<}|37(DOk-vnA&61}oi}6f2<* zN7Af7Co*)0f-hX%%TS?TH+wy&6V{Z}AbOII0(w;dTu}-_;A^kH#Sr?p7 zm>o|I*7He2^+LB!;O8)xa;c$gvt>xQWMPbKILFhn(6aKNxb2B@#e%$x#&~m5dG8FL zT9Z|pr(AU7Ygdsrmr5FEC2V_fm8~tzO3TqTFWZuty|+k8MP**)F6djzxrk4X`R>Te z7sd8vHxrWdj}eL`*F}pxH(tfI8@CtfNej{p2D~c5I@jrE1S4t>0XacQKAGQ?JQVdf zFtZ)k05r)Oi98EYoFaXqMaMaA2JGpy^q zgD1v)5%}g{+PFb#22~-Iz)^kPeN)gTd;;i!zsfmtFOoR07~fEt%NDOhRsXWHu z+EZ=_VbkC#kve(xw4jwLtm%FN1~R*Z#+QhvibCNkbN!_sywD;hgaz*juKNRBgLfwq zp%ubeLGi^?2Gc{)*uLcJH*-wuEb@nzHJ?HJg-vWTi1;Pnyacd96$Jr{1Il_h2Tw{} zIX26yS!|@)wpNsp9tjJ~S5r`0RU`?B?kFoigN?q?mXeywftq5-(^@z8L2RjnRau?^ zG9j~w=DyIs<;8gj=-WoF%et&q3=@&_5OlZL)vJl}L@YMLX%$z8aBo#b!SZsJ6V?YZ zcRP=7kMoe&-Iy3jpX>_4@>_Jha?6S1&t>0FJwFjYfd;8oQQFNA2qb7*YKXL)d$}4F zA6R)lF!^rF_p$Zpxf%?Qi-ze6V2 zaNDOB!A!vxzbxz3MoP@Q)Nvn>qOc(Yg8CiQ#J+qj@fp>`h# z$siXxAcK=$nXkIr;Ckl)zIwWC=5=Jbx}5=d(O_exehryBiY^OeHYzpfabQcCqMT`R zuuOYfq3;FmY3hvBh)@$VtG1vFzyfNi(lb2|Z3rLSXZ`07DpeyxJ=ThnJ#VDl*mg;w zKDd?rG5eywNN$sfLq^1uwPD8d^vE=;CGi8MD0&-lYW1T87&yb=xW~G zc`H7fl98j04ns(fWDipWd$$);;Dx;8ecGgiMR;q~-8XgF$zhIF3}$Uq)uQ-7t=1 z&;H0u8#^1Sxf|iT%BY&>M$b%UFeXxB>aX-vUK!+n%?@5ItGr-9#4+zRCXlFlo-|q; zQu>xG1RwYEi9St<-C!f#t=kN{NtT%1v0g(~nl`(dHpu5R-y|cn^FxX*3%-q)Ql0E5 zDZy}aW+f0@h6V{{CwW78zto#8_I&6;Xot$aGZ(U(Q0LMtF9Z5s45&P7u;BC!+{SYs z(e*7~S3c+QEdfN4IB}Q^5(FW^?q(DfUB9RmqBZ1L(um_mysGk1Hgvp?7q@m~&OOd> zH2=6Vj3p>z+$1C3Mts4iT#mu)3b{{^6kn9)2pcN)yfJmAr)S-V^70PiC{|@6YtkxQ z4H6K^j}{0NaFQy&%w{a;BrHdfL3G~j1!pF>DJNGbSsC~tkwO=V`)+P-V(LN|UC?~v z_NXsA$sFW7PW5Fetcvkh{vv(V;)X7^E=;- zUR4#GPC#ApcMc{w&vEW8-=oP2at4tPL=7evqiTi|2G{CcMXyG0&9Qz1wYjSxwA!~@ z{3auVHjxG4e02+tI?=vagt5pOJzeLRl+RYUR+=r3FW9MShFt7|cDDJvUZA_q#GO_D zVfXdsNxZ!QLA@((`E$4$x3`{I&;)8YDSU_~pmv_JDfm2p;#^tdrtB$7<}p$x&nf zJDv`C#~Zj#^TgN>9i>DyHHS==afWiIR2_6Xhjv>DR8|$Aj(OW{YL6c}3PHwbb84%o zgp-nh>_N#^sF#|@*~ZL-yTw-Lu!F|L)z-GT*AxXkE$88iJ*|B!jQQIAfMjvVz^;vw>VN1nRM?R0xsvRyzDib;tbq5 z1ON{*VFw-;0*}*A)@LbhQ38uj)I=!l2ZkI1V07b0?xfO_LqK-VRX=p~;N#8jJBog= z=Ld406TA8ew~xP?z5QP?;QoK|`03iH=8>0tHAzvOcPi^Yhk}VP;!NypxVe%jSy!C9 zV!SGVFGOI}kEMQqJCe|l*HkV;NC1PqLxxjDV3IBEU4v4+v7byonNdxxSCa8NIz3`< z%i)(pB$9F{8Q zY6RYAE+tkBCoUK)70*-+Z8+aHcKY9jk?5BgLo1|?rf{fjqavGY{ zuYCd`ZwkXCJ7rXs(6mp6wSC$xg;SD?Lvl*|EpEm4M{= z@PgM50l!Q(xyCr!$~sEwVu;tW(~wOUyjhY+DuKB;rOW6~Qm)dEwpX!HrIlA-G+1|Z zC!6bbnlFnm-5df&%&Gg8S1r4hb~S0AHJOQw>?B5~jlGQX^bw&VsKNP8@cKFZeVYZGx*{o6nFID>m>@F=`_%3WFvfdbvCf>pc+g0#};7QcW+Hxh! zL+f{T+G>>Y7mcpy?H28@j7*QEFI`pkJ=h~*RjFjeWq&Jd}mAfuYibT0I>Eq6(X`C)Oi=@<9|h zL+nc>p@{e#D00}B9W*}ue*dW&!l3%2!1PQ$U zz9U0kZT6X;R`r0maf5Pal$n*A*yqsV*^#}h;BgvG8bxL;W@k!+?1y!_nGeHA(bIHA`sOD#w@qe+AITGM5qr4>v3XsiYc>}-Wc#w(eLZzk^DH=l`h(6rv8JJN zG3kZ*#a5ZAY4>oZea^HDh4z_FX&v)=qXtMWIehbz6!nTNKjOkuF*Cx6NN zCBr^X$7qSBM97=001hpd5JEGq#z+ll!$3`65t2mihd-*csIh{)0m@Wa!52-xIIGj8 z6=9m1E3BEMRroUf9(UT3mW50k!pz!-SlR4-KJiXWw$sk^r zn-5P$Qo3^larA*w*rJRy-8Q_iiFThH8d21?`xakED1kRQZN4yaIcK3JpEOogrys5z zPjX&>WB8g{L;I6Lv~On)=3)~UODfuArB6#Kw&52&oc28l?T}B5k{2%##9N0xXN_S5 zXR`2Y(i+LmTT`N7?+r9*1TG~X{ZtUxAss5%fH9nh>uKr=IzsD}*lS}9Gq?5m7gx@RaOWy8S?NG{?%*B`L=8G_6YXXAznRYbYHnOMmBfkCSoH~4p$EimK zH>z*N+iy!XaS%ZUxVMS>IxH zpJ-z0<5O;!4MO9TOrYx1+!W1`o2DVC4TC`fMZte&i2eA9?<@Tn_V7xosnC`X;t=5V z-K}scs7TjJGpsAJ1CydQnAFpa$vp&^FPvNhpvnKGdqKZ(|KATB$Kgz>!YnWP>WWG~ z|Dvl~QnfWq+iPC_O!1)`EsQxW-3KoBYqy*{cheKWv1gT59ou)6_lzMk`CvLs6@n8cU7^$uNGBf9Tuo+Sz|-Yb zC4JFNM(=VeI>a+OOXe5%Mf_Z`gSl|ETA^%j$b>T12YFw#^X+ok!YSBB8`YV+ z4NivYsbwO`z8n;zbLYknX40^)2g4XgD$-^@nSm)~&3Jb>=i4Xc7aOi5HqhLewDD+x zp>=i9H${lkkt`*)s?+hIbLgGmSdHDBa##Ok4v(C#(n3Z{a7&WZOhAx+>bB|DwuvCe z5`;$|{Ba~hBB*1$^5Qf}BG~+`gWB6K)KBEb=j0&ZsCXUE&rYAaiyNwK*O-m zz>uQVN5feMVrfdr)m^wS0GDRo?`8dZl33!`Gak`A&VV1D>;FVJYYS5>DZ4t-{BlJg z>k)3Zt8#x?w^MRxU1H8Ra(_PS5CDpQanig}kVa$OXVD4N2mrr)T{>*LTX1rpz7gv- z3xH7RF7f|tV3L}PvMg5G_Y5d8*>O1p$P%nj@&^O=_XbwNY(dBjrAQMIhl|K`E>kRK zXB+6jFEC=WeijBRQajc;EL$SiBe%zv0KMQi!!x^W1vfKHweYp)@n|V94ygqCG9wfc zW4ZIbZS6OJo1E_tEf~bx3krfWP;qC3&}HrBwMoGg-}B=(h-mZ+w--T-#rJ!9jBje5 z9^SDVNoa6soh}t3##9sYUBHl0e5&}&%RdJCl* z8EI3ZKPMwWV7}%F2PvpX?(quuhZXAjb-`AV-IjyD90kuf4bQ&bWJNpA*aKV2=YvRY z8idJ=*^o6TM#I07^=*aKqev;H`v=$EuE-{6LrL#ME^3^{WILz%A`@!t%Y5?K$Egc0 zs=Mz8nCoUk#KeeZ2Y7cMK^+Q8&z8B$Q`e;<6#q6Sk!ks%T5Ww&`RVyrpAs#@B2H7~ z^e*9q&BC(7@!hyxQE~n5+S@1&p72ONI}OMbA1*^F;(uR9t- z#%Q`eBhlLOOS4tl$ETvH$J>-KC8+D)L!JB~rl`d48Uq~L#)KDLxBppn|L?#9>b{_n zB9!-aGS`Dz41euFnEuj%fM}yRe#6pq_h!3}sob@6OTcmc`QK^(4|}dN81;1IEUInh zr|pHHIe7!tas&NfYuZx#T00}-fxcj+#*BBLkxLqzn{`Sm-6>ruA!!pL(w7hBKL_em zQ)PcX>q)=d-m9|=>U9%+-Y=gaY9Ngc;G`H3AO1+^jm9R?eMid>V;(_7MJIQYKwC8{6F=Equ1@0G@2HNb(VN z)S68I%e0yehQuCB+g=jVd@W5*RthyBY!*Fs&mUrgtbinD*2Q+`Xufhj7gA2iA0K>F zIs;)b&cc(nIl~>-ioN5bx=nCCY3a}zp{B{%UXfiam^*3`#I48PqoOh$^denbD%&wt zB6$E~to&NGNc)mk#~5X8VY_VV&Tvul9FVUO5<@-ON33+nTQQT1 z-3doSvuoTfT8xmVR-xY{3%-yJR^Q~W(5Fj0*laseDY5mibR_T_LE?t86fhI#;~ zUHM3_K8BSIaRd7CuD-nK9IRlS}!Yi@2IP-Q;#1X?hsI>SsR78qsG3{ zpm6l1O7`1T=Q;zTG?ioRg~;sFrW^$dN88G1sri{(P$&YSmCVFAI-xO>88_D{&mK%B zPLx%ZVEwHN$Zmp27lfUVkT!;tcG{rfsNQ;K z2E}TgM^9^4Ui-A%>wn%?1}w6kppDX-8qto%5%*NNO_p4G+Teq^U}D(p3#8?1W{1UPs>mfSDGeX@_kKh9!mQGSeaKw7Dqf{+Rk z=6PzBO?&Mx##7;d}YHBb!H-B8Ez;NOi z-|!)zCy1aVu=ch07J3YM?-uS^44KO`Mm{G}D0*!3c&b)bFgfXHaDlCbySYO_B#Wn5 zmLWq-qV9JEOf4}fI}uV$qQM*^1qnih7s{ex3ZZSZY17cwkOZvfdsMfJ{kVmp*-xcI z+JVI)@w3|S^RtX3#wdZop77Xs8~$Qkj!v+IuUYK?P=O3{wz5gz6N1W5YwTg{u_|~2lu2QrEh!7&8`;ea`(L8(Ki)%s9~aY!QGLvvU^Vt z*4%@b!UQr$$ZQX9b;=a9hAh{{q}2(x<4aB|K5aoR#H3a>=VoPl9L%&~BiR@6?YU+V z7GUS3zWK#>NL{z`DBI1F9K;5R$LuA>iBWc8N_!Tz3r*>)U>BC`%0?cI;8Wd&5R#MA zB3W$VnAFyTl=?`srFx$jfyr#!KIHV|HZ+XT_NGW{foR*E`ve0x|Ly00Q{4=S!}fOj z-7oy_e^};CYmcG;5p7`pdmM#s(vlm~S;2<@YjN&Pi(ORbO#hmFXS6P5L9S@FDt=rx z(EZuVh$Se`{W97H;zZ(?=45NDpoRc$-tu+umUN+qcs6h=mzX=TA0_g1b|MxfnZiXm z&0)go84q`dip#~>qu&skz9AMi@*#~OWak7?i-hI2_HmCiWS7BI`BhJB|E#m-J`WQr zhQFvSlb~JcpxxabIwh;>y%lQO)|4Twm;g2InFO3U{co81KN7(R`?fik_Ugx0%f5wp z?;o|kv1E&AE`7H7CWBMdCP#J{zk+<0=5?~a`m&&m{gGOERqYl z82pHsiPePeRz~yXJZtWQjTMrz(F=P<6cdJO))2E$_u)i*SSLiq1vWBMp`_wf^UzaZW^#;ugOIMGit`GMBSU7wT_EW z32#@?&B%Q{yU%DP&Cc%oOrqpDflr%#dvF}8y9lEY;!-Khu&@cK!5j*uWWyG4De-Tu zyD%hIh_voWl)DBrd{YVGsOtlzPv`_i%E}Pl(2!kmC8M(Cm{8U3O(+$SOi0LfLlQqX z=d5@pxGjrLrSgx-mRmIJ?*!c6iTTOq(OT<<+6(~D>GbnI65@YF+W8LV3e|x^sTLqa zNX#_fArxqr#krrrA)6T(>zq{!A7CkJ4hb%9#z#>IOGs9|%dM$|)Hm(=Y_3_QItha@ ztMTJ*9_9YAk-Pyb8u;<>U~+QRs-8w%7U~O{LWmF5a=mmRiI8LMK7cSi1o&UK$Y`3R zHdndbB}`qOL}qQDRn>iink+?jbjQsVSlUL0q1`NDjll;LggisW=V=FnF}abo9Z82; z3X<9%BYp)8nd6RE2fLptkCNU{zFQB=!%ym{K=%j)v^csRuW_)ouilIp2}}cCEk?gx z7sx{gHOJGPKUq+)Dt+P5pqxhyK%!$$%U~LExToXWjjBQ%SDD0v#K0+!GK-9kycYjO z9FTdn3)Bry_1>6m5Unimw}u}REEQ6iZZ5x$cJ9-eaSzmjjA<2Nl88a$dcvOkk} z=?Jx{jnKtN`2~co-zy!7p+(?E{IrNi`M?Z##!CHR@=0Kn>KdxE4dV{IyY+}%_j#&_ zbq(65$-+6EwJqaodFsRavAS#!?ZeSo|{ z04$*PXeU zcd4-zY)&WQ;7VfLs=RlGtt{zuc$VWfoI! zyv=@(5AW2~m3NAgF~sK=wPC&J-m+@9soP=XYV*C?`YS+ zyxicBH=JV8>@Ac20d=KUXF`SA!q7d~-ZOw4+_h4nw%R{9XT1y@-g`HZaDU6)ZzGh- z(v>^Si6wGJE|@u^)q&pcB7xGXgq5WdaDf$cr1Z*I*F-e+!58Q2gz@(+4`V;(c!u

CNWjye`g+=)YXX&sx2M*(x)CFH0)r|9NhI+Z(I!GYq}7*eH-^L~zcW$Vr5fqZRx z)dW*|FJD`b%+eT+_7BygpQoG-3VQZ(Z07mucX^*E*41FwzPAR@%EAc8l7 zdwi&^wF;BXF&GEV=NqRui0T$td1+nC{h+K9* zg_7yjJlnah6Y{*ut++2^7xJYwYouz`ym7I*PBz|k5O(^K!@=Ur;952^S7+ocz&rcn z-;v>wKd!wNJ^NQClkfq!udN1?wpAZ?K;KBvFt`bD7peU%qa(li5Vih##e-I4Dd3;1 zL`c%#TtIR8o2xA{`vO}8Mg1|?l*Ly5wYmN7EI^;il;sb%^w3heS~A*CEeCU;NJ*~?UoZ2uMMz~dCcB%13F`<1 zf(lPXwN!|q9k59{B#S0651WsD5~Vp85ufI6R;A&-xv75$NUj^sY94le(Xn;_CiX`- z5K?+?A9?oeudE!OROb$n))XA97X!9qW35zlC$oxrpecBu8(zts0ytg$w4c=GkB0FVDHbS%~)g_Jxn^|J0Pd$u@pXdFYXLF zE35C`I?`=`+2?{M(@s*Ij&9yYfw}r#*d|vYe6`@Ch zqWR5wxRWRLff)+C%_29&l~N_ld%>|z3%lI{V}^soN$LvNr>D#rYe?}O4tIdLBT$j& z+c5SSHbE~kBS`I6%0Kij)VIa8dru91UlCe8Rzu|wA8xO+a;t{X?O%55Kw_1Qw1V;J zXN`hn-X$@^I&LDKB-8k$2pylkw%!F=D|)_;!OJc zh{m-TejI)h?R(di~T3CZp~==QookGEL<5tBm$CSbYi{&F{EeBh_)9}a*j2x<(xt#zhI#o&J1{ybA zY-w$9CQLa!YQH_VsNnWBHzeEBSaO3gq|TGz-wwXDpY4m~cF3w2j&MvU*}@BYY@Wz8 zUJi@sd^6^eU2>J6kzhR=!tso=F~PHu*Fqs;sEKTvbk-4-m6}Bg@7ghAV$lKjm}a`s z_5P(6z`pqBzv}t_4^PhI(VwPxX(m{oCMzV-1@{Po@Z89|mshgvSS!i!s8>V!K6=Dn zb>RRPn2QE)5WJK(%p2{e{B1zOFy;X!zMK`n8N#mwDrNhoVH6_h>rLfgk>=1TImXAP zRbdY=?GIQ@8MmN>W*0W8C>DgzPS4o4J_Vi``oNIwV8YCA(-AWU>+N9d5rcw z%idXd^sSvE01<|wwHD&s&lkxsBP)x&(xV_tVUuvR+5w7jmp#IcgbtJYcPms z{?N|E^wh@l871~TnVxYkgOeGhMfDNe@LJ7vcQ;K|)1Vy3>F~IGqKqlAahlRt1)UA$ zd-SlT3UBB9c9vJwSBhh3D?)nZJ`nPN@`c`^m-!>;+4H6a8WDr z&*!)Mnsygra&MIitwgg4uJMKJO~=qgU%+%}k$iD2T>_jr(;KS}F}UUeN2hENXs28>7WdFCv?M9V z{QB&s2t?d;==mXFILTk5#po%OuegYlO#Z|(=!HPGhVo?VimL``AzKvKl&wn*Mf5hC zHIsx2-On6IV#Ex`_d?RIEvk;pI_|{bjr}E)z{0QB+bs?OS7P3E(hrFrME5PTm9cO6 zq!l>ijkj&b)4la!JBNU8-QSN)$bS4yOa1|Hm3MFn{b300f*eZXEvG>_GVnxV+=rSo z_Jf<1&Ga#_nLuV{lx8=*VPMvG^@;fh3Q-Nio#g6p4Y8T&(J4CWjkY7?l8?Eb%)0b7 znf;9=J|ixwMo@3Lv4J~&gWwzzk^}_GXWV^DzXE*5jUNbGKAXYTW>70!B6}Lut3LMpbTqfMvVj1Kqby3gvJw%3FNn>keM|_4 zVsXqY1VS`iTjF&B?TbN0NBPAXHttu5(HMyTN1B;$xBbhKDSb2yyt5-6Lgziv!= z)B7edGKVFP8=yA0P3$pddS7-d+tjguil1jz6P*5v1Nmib8(nFffU3;y@5(;|@!^|U zB%b*K6BV&CCSPi3F+V&@erk5@oi}9t;>{u0RxJPY= z7Hb6c*Dub7SPxjgq`?O`lhbtt+PMol-vm{^i0B^>mD*>=H3Afl{o2fq)nyun8+a!f ze%MhZRL!A)a>cuB;Kj`VzO>k7F3;4o$22uYMnUg~8NsjWX%fakK`D`^%s<+Mm!soz ztg7@0YD@}eak(z#n6-Gfb-v})Oq|u~Mbin%c96(OsM})Zy3Zvv#J=6<}{^+*HF+2DLTCC~%TayPePqTsr=gb_xXER5jfCn8z)Ix`V~6Wa93hJi)q8?MY@g zF)a*sX+vF<7JbG`y0TufTx_(xanOFS44L6)H3pJ^&c*iaf$Au%C7YT_W{8F{39dby z6Jb;I!{W9T=f|l_JSEWY4gp2*yAw)YEL{y!H*Gj2zVX)?5ZZ+7Tx02osX&yaXp>EdHV}hMAq7!KziIE`3R8% z<$OKcR|zx@dZ|K7Lw<<9rGES0ToUo$aH#91L8tz37D1TuMcfCSm2#$2LE52z9F9ef zz!A!~7+(2F8Xz9!*4sc+2cVi?OHkm6hA#C=qLrttx=T`E7sNPt|G|tpf}FW0VJaS~ zz|ZUj0`W`{tvR#wpcK6z9-KTTce-S_)w@vahix3#N| z$GTI?iiFq>yIzn0nPiHn&0>Uftgy^g(qZgqzpW`eT-iS z0Jw4mmiX-!L>}l}Y-!o>mEhBGw8Z48WEgHbfoPmBlQp3-mw)Bk?XCC0dup!UA$CAs zl~9KdFr^ym>8ebjNTF^Szag;?CB1}a3;&C|_YP|^Yu`n2X6&Mah)5p+LHd9Oq<@YC zM4FI-^67W50hTPx7vJt@W;JUC;BZ`?>GIa&4^orj5tvDLg8>Bsw#5?>El)#;9WM z*y(NIrpFFtl!v6#uDm9)FQH&4%qt#-u7N^Vnu>`t5as2|-gahBdUo_qcGc#i?H6)y zWH;II&y?b#^ZIbHq?5m9EA?(;>{PDTkj3WO%ibW!yssUf`Y&uoZVzk0CisFw?P}eh zfb22=&%`3u7iG)m4R^h1@h3SX@eA{95!ZxducS|@4?k%yOE+}s&#BTY+X6W)gqfKm zVP?AEnw?}%`O`njNoj?#3#T(-HS+4rDTA^ z$Rtpb6#Mi*k9km>Fo+Dc=n2{W;q>2T&{4ah$PLVxnR?-;^)ziLD4_PI;*DHB5u3&2 zeh!Q71|qMU5B(CAP!>Eg+0!-~b^Jr(cHFoNw1h+scw=#gAHYzOoIs43CT0`6Gd9=n zI*^(1+H-K11OT2rw`Vom?Vc-Cp0kUbkyT|$`YF5~iXA=F-Rdmv$srpzY=ka>tsi!* z$1Z8(x@dfsh~j3j4gaP2SvXtf<$RJS>$Tc(2EtyrFO^_-RL3C&-?)%?P1m~{{Z49q!KVy2kt^o!7Dw3kz{;;i?AbSE5g~{R=Ox7C zx3;lkrX3s74opNA1!ft-TJrMA*6~ZiyW^`M`C1*s>hV$2k*vltl`v45X4n-h<2A;2M`3F4S(YUXX2@)q$NF}8ojQXikiMObalQ5|F@D7vLn+JO ztZszDNS-%CG!ZL>79OMZ>L|jtWWCs+mK?!DMKCzmkpx5Vmm#ngaf=mdMg|N``}bV( z%P^a%4$VVfaTa=V7u2=1Lb9oawpXh{UN$(CG?+v9gf`;0m^~rG!p#K?H~zwm&-ICK zk#ux*o8nW&o=Yk6o$QG+L&GsmSO`bf01R7s%{$igbnL>6az%q{%8x)%S-YD}Y26Wp zUc*TA=j4K&D8+9;e3n2nO*jWTg=g17tAw9lWB4mQ9kdf6CpEO0sB0s4n{%XW)f1uq z;$$Olnt-SyKBGskX26hUFo|EjSFV(-nmg04o#xEfUA{+R_1E5eSn0q}K`$gnT(;OB3DN&m=7LR!|;2T(Zt)LQ-)!mlKC=t`qna)}V z$jHw|n%JAT`Op}7Q;Zn0ZgchF^u3(bp2w3hRsjhP-AT4E_$07nRC#ds-s^hi_RL;j$C>u z#!@ZC5(VHW1sl^b8S$0bHNA$jz{X4IxvXfB2tGu}dh#!#G zKIca6W0HhbYM^<8PvwMh3;~lyp~EJ~-eW?hJU`s{!1H5XPTy`}nAh@p?RTERJdY-g z?>vD!hM9K9D?8n34$L> z2M4=%){W&EZyZ`AUN&qAPP^3y>SPcC*GP+p)W3Li(mi;1Mo^Rp<6_jU!ZO;WN4d|$ zR~BRKrte6IOg1Rk#~u$_axc@1Ws}OYfuwP(kd2+Zd$6#M`UKaWt-~c-_4n4pTDusa zj(TRoW+BE6H-wRD*mbC{nRS_U?)Jr`iykVu*Q)?M!Vzs6k~1Polz|08AV+CvjT~J& zjTXP0+ZdU$dcn`J~rl zy%EQ#{Vi`pqM8N!qVEdqve}P6^3LXU35|_ct$mKT5<@576a3m)B6xCh4|(fZ zu&+<;m)Y23)0PcfW>a^c_MsA_bBt#EgAjkc9ihdwWMA!|WR+W$cC&9TbbFuMN~Q8w z-zYcES`hzUdBEDqw>{+TX94EZ-mMV=GXh+MJUM{{U&T(K+^m|5ONNhIIXOMNw-1#b z_P|FbUB2@~f_`n)jsC1JFa-L}(|e>MkgZW$i>T0Vd+PMpkD+N= z4>*XJ43!d3$O&Jvc3OxA2k|1a9C!r%&hwJn%IZ;aPI%1pl{@dykjgW;wW0PsmJWvq2ji>BXoodbKp6ytw+C`bE~^q+z*?JU)MF1|jMFH)pZSPz-R^Im{n>(naRv>8FAIzS5yZ?gpika$;cz_kv zQP8M`7e@%3<_4)B^)GhlUr1omx-Htg%N+M)B6E71MTct|R2?1qRWkLxS?-%(a;g@4 z)~H6?58&tN@OL@hw5g!Y{F!Mx;*mAL{?7BqbHOBK{wzAl@^ije(IxH1%k|wd*bqMn* zy`|=cJ_}~j@qcaO7`YO4(Rd=mevIZ;a{Q<8F8H2`jeTpJFFOTjbDk z2H4DigYUW(D?9MRXLf+EwRTWBlo4xie(GS(SUZ4K6pp2C_iMSNKtT*!VMsUMVFOXFeKpI(2M%cGd!T5XaXtArCEM`G;<>LtmET7+fI2P z<%f##h2E`4=Ul_Qsk+ksHzUEQ{4J^ET*s>Cbmi_e<@r{lD;oAF)8DEL-Y31g67yv9 zS!uvGj11kULp6FyQv+s1vFvQJ#4_M!PaB6k1SRfWu5*JaaTj`+o4q=DRf9r&Tk4gV zrgy+zX|E5Rg{{Q&lLmnjKeFcO$U&E^Thy6i59YN^B`9rsL^bu>rPhRNS!PxF zhO_8|&PPYG==KMnuk$|dRH%={no>cl0yau!FL4 zt<|>EKk?TJcKbj_6@&7vNmezv35`iPS)7VsDS9ukzyv>FvV5mjAiOk1TlOW_$-xpq zTv>sC@~zjEx{Zncp_T$RJK%K~y0L8o!e4f$#eI1+)*bVx+V9kx>AiyfXQ4&y%h}KXs5)XDmi|}z}&5(J7&yv9Wnbh)98kvk0gablz6`y2%A)(Ri?3d zyP3$z5bKT~=|2S|x#$dmq`*u5lYP@P{&KT&!^-tgRaPcr(G3*va?b32l$Bi({%pbg zX0)UEgiWG(TsL#F*%d|nYy6%Wi4Ygdx?N_~-)^mavj$qEo$h)4){B)SLk=l~i0m&} zmc3nTW~V^9&8`WMlb8bPZ~Q+sh=|NKYQC2M7QKVFTJPg{qnK@T^^b!c5XGL;r3dnWQUSZ`1d ztvE}{z5&Pwh3P)&tMIx)wJW0R!@`xCbJt$c5cxoHs7*W6CXb9kl=awWT#w@h%iPD6<8lX*ScFYRf4;-7NTeadC77 zdpp`kuI?Fiyj&DIci`*z($Pm1L2UjqJO&!IEpjLbHw_GTyjpHXui=8&fGx_5+xyXB zPxpYc#(^U0B-hKJznoytigf$27)Yz1Q75Nj0~rtBuHX-^ot<%LLas&{xfRb1c`FpA zU05&Iq0+raMWDV$(2++t%O)3RemW*!!!?NRvqZ#Qumo!kn%jQf$lHFOnN#@}yB4X| zq&}K+W4PfoVHoZ{5|`u3+YS^}YI%rU^J|LxfwD3JTXLZFVFDh|zK+>!DgE%bVjk?9 zT_BaZ>hcKo^5UQ34D9Y?##f|ELLb&31n^p5MBhKMF*|dbWPcl}h98C4mF{0kJ6L*@E%~0g4jORX>mI+mP{*A|HqEl%dH#$netyoO1@>14jUYE6 z;$~Kr<mleR zuFQnKnX|t;Hu+7y{x&NZHXU0R58;dDe zFujipOv%{Y+Z)o!g70A(=VI{d?mvw;YV*v0aN;^vtpkK-!S>ijL-T z(~Ju}J~;PFVt)7x+1e{91;;0POf7ek&2#xh>FRuF6w~rUS#3`{ao&c){;NiWff zJ@y4`$j)i^7B;e!SBC+4pGdh{47$6RjvrLzt0-~Z5&Ii`{bulpXQ0ueg7PXz-%}r@ z2Z{P6-8nL2uFSTl; zon6XM;T{bV{ zO}Qj^hK5L(nC7!+BN6?E3N}N0Pi_-NAmU70@7R0|mySI6N7gsu(WM>8#;EK8kltBf z?;P=iuJj=@>g4ssZuInF>d`U>nX^@pDHzBv{@#&nE$eaIj@};MjR4sxZW9>smkZ9aocivkD3C&-@z0sDeEIu>5pJ0sx_zRuoqG&~f6c?&d}u zKUV`$|KjwMr1b!Sp0#duuIqfU8gEPB-PQ7oBLV{!hi92ocLH-)De_oS}wBQw{Y(&S?lJ;gNs+VU1KzN{N`kVeAIF*7s78|z2Ch2Q%4F7{U^s_^7ngE z;r#+nEMPSf6QV4UR(2|CY%>4iG_o!{F>;t#E{Wl~=^b;Sq}qszLE*dX*(t6o8V))8 ziO%x7C>c`r?RnRssWzW)eOuXi#=AISw%t62kl^Sn ziLFd@EBCjIX|?1Rf;M^mjh7QkHgL>;BTA zzd6YbB;xfKg}(;RLI#igxpP>ja^(Nt{v9aK^wWrDqDFWFdRNrS4BI_Q(}lI2l{P;b zo9J8$<4uDX(5iI%VN1!YROM-YqNjwj5Yp^5^7l?Zk>l{(Wz&W>7+CvVa#IL_37r=7 zDQ*(%cI?->7e@0A{Q9Fv{&1t)BkD8NXlJ0VpT#zMM1otjnBqcwYmwG0W&@wu?1RH> z;U_P#aWf?_8P8z1flauobIqp>jnG|4$V?;Y$Uyg8+e9tu&QS&-mOxDZNW6CRfYJE( z__mm8ahKvZmC$sBBInz0#HisQiJoiy(^`f0WQ(umo`ILH-Mwqw{m`2;c~4Hxb3D2A zC%!_z_K|fi1W;18G19-Y!9hd5Kcg!*BO~E0&*#5*&VI8LP=`)mdnfAf^rNzfgJns^ z>H3ZLuqKy!%TLoMQQagO)8IQ#CFV$Ab-aYKr+>BW+Bd!PUtN;9T#4hH{QtrqVi{`r zc=D6e+9&%l;+hy>Un?jj=NBzlZnt-%dVwG6f;WboCe)ZMqzyj?QY#yJX7XrGj!S?- zME+}yzUt+B!&$MD+xBi$njRzGw&1`%I@kJP7G-sV0$qDQPQO>YE9>iS*H5Nd?@~UT zs%hs5xvmp(m}2i#Jzo>jgY#l7CSI^{P?%~ayyI8p%Pv}evwOKwAIYx0?F4-QF^V75 zl<+Gz^h)H8qavX@OEK{%tb`U8T{)AuDHc&TGBgVcqNGip0Up-G+G;Y~%x7$+EzrV2 zzTM;?I+21b2~xBcQC9UNR5*`zhlV#N$B16rSvOQo*iF`N3Wa|2n?%I<-L=VHt_AMZ zct2Tk*V?@i3PX9l47W(#5Z3qPPqN!XUWpyw7KLyxTnj+Si0|E7^{<(-fy9%1jn+cK2$gg)cJv zrpDi!-S4U9a?e~giJWn-9W^LUTnPHs+qY}|u)l*6*0W}o?yBLtViIT={FD=iM9Q3p8|YkX<%fiS(}0E`49P`}g=+|q{Fuk(RM+!)Lo za9n-V&U3lw|Do>xC)dxBl|vrNiL-1;4_jRaQRCCpoRg)HE_j+uc{)zCv7cV9V2|N9 z4cLa=>m053kZm@GW2yD*-Ox4+X~Nh%HhLM5P1ZmQi3FLKp)mPA=}FWM{}SSmj@B?g z?Ph7GgJN;q;4~}esJP^q;`kXY)^$-y7{L~iw3eimftzE*YM}`t3N*fOHuNYOp`oDd zE;Da3mHYRoom6b?<2xlj=Y^$H+H^&qQq7u%460cejfje6*lyqQE7rNPBQ-Z$8MvP9 zbz@LFUQ5Ac4g6(m=mQzvJ*JV_NygMjPYVL7Au@;8qxtZdSDvLuhMt%I7_zb`nRsMJ zs8ctxv;vzc8L)M-{4q7^F#{m^bcHggZ~~cKX!NUI_bE9 zAI^WIyK3I(oR*F%J3tGZ0a-KJ>RKVZce1$m?&T7_v%K)mg#OCA9<~_Rz#FM^6evHRo9rN!>mdA01*b zT(_lNDuK*~YCc`-?B=HIuyhjF&W>?+TVdZdwUp7MJ+g%V#@?+tabRQ2 zP0`XJG%w<#$~iWd;8)ct8aMxxSNqnew?wOQ`THf|vM3v}AE-QBp#RXsL>sX%4ODdQ zU?ho92Wm#C@NE8s!({HqI0i@HSv?IX(iJ#;@+LnR$7f&MIU!k3mz3Wz{|x)Zr#-GT zM2UE?5v_e;1Z_`C+sTxF8z68c5)nK|M!P!Wn^*Z*D+|`-V@tD@Qy&zEuhcr@t3p2% z1M)6!YZe5qXB6T3Y;&#!UXBr6G%(!85E#YLS^JmAJv6R_pj-P@mGz0%2X#YVD*7DZ zw$N1~30F2z9^oufDZk^4m}!a<0YUa2F6Kp2TF8y81&PN=2j@>dxgu zKKx#9Jvssz9kr%rcG7aX6!M+S$7Br*Wma(z)+EKTvePL)E@^Uh_3MgQC; z;TT$o_uoC>&iu@Y8e}5Bkl|ID-?e3!t#ACitoE-LnDDu=_|RKxc6MfFPy2F9sb-HV zurXgrA?!lYP*U#SLf0+OQ?n`xILPy+Cev*89nIcASEah4AT(JS zZu+_Jj+w3NP7f-{?-;1N{*GatJGtpapg;G{lzsycrmec8g;xo2xEL-3l{^W886dL= zOUKiOiQrFnPoU1YggP3v!%Xgn-xYnh;-(1BxAsjPpF$%pIuVTIg^}K@g zx7ny0HpRpI1z-XVnta%m-y8;*=%FxK1J)kP8E@;ihO1#(+TMi=*R3w(%C`5z4U?d@ z0-B=JS=9V|nQ2IX{hW*w`xJb`6k~?G*{Y;Ugo8@hE`OU?P>Q1#l#VPnCN?DVIkWj z26@f1k~N3yLW678YC*rPJ7nws)VlMXM>Oi{r*8~x-TWK@ArsSnYoHvR-MrfGsE`dT zZk3d;K7Z|rjMrP0!GE2-KOG$4Lw3AeQoI55`#6#wo;cJ`vx1mxe%Lr5zqdd+mI5<; z4I#n@^%3;RVo{j~7wZ_b5;1<*V7SnN1GP5vl}Cd_3pY|S;zWGsd2H8ca({<-x}SW2 zi~ZXC;~U3@-N6g9mrK%HuoG8}_0smmM6jWj;UtK6jakZD&e@?nR~5Issq&Ld@7h6? z%Uo8?@!h-jd^N1$FhGsu^?IqI;4w$(KSQSOp6 zg|j4^ZYCEmPVjLVkSF*wR-#C*b98V;FQEhPyeA+){gyCTcDgdemJsv)L-7vOW^%_R zwuxgl#i{Qm!)7s4OjRKxXK7DG-Rp!{xs3iC`^xyLp68*9k3kH!M_XwAv!MHL>^+%j z-Os#-%;3?c})lAOQM=V z$94n4Bp4)MT4O844cKHC9E^d)kcD&ar%`20n^lILB}k{wiOgsVQ$f`?uo}rQ0~pM# zR?qNhPrmAEL3Q}A<;PdUo#50zR}zq{;%UJg*NvuK3O`YO<{J`|7ZfrVsakWV3gZ-5 z+$|eqYechP4~RD+wR7=~{xu(^t4H03U~nm|ErTU9VO>aXQYd6*Nm?<>amT6Z1=hBt z>Y@}mu+P@??v@RKdn}?Xih{^Oj@#s zbM1%%AY~&MK93EDD-2>C_HkIr=sM9kACXF=T~WZAg6l>XKMR!jXI408LqSE7n{$9J z(h2kcK1}A}bbXn*_7!{AAX!%k)HU6HIh6_Pb!!c(+`WSdi7QkCi;b>KG7OspG+v

AUR$p1PMe$*`cVx3$&410_zLaO77Iz7D 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 z zglD`%{kX;XT^DMX>3}?}CKH{LJ1P1kOH8n*7kfoBU%Ix~=HVZ+l;lnAVyg!6*(mYP zVy=dO?|NHZ%?*Z)41X>eH24Ltw%SfEHK(?vdZ=NpYp$c*;3tDyw%(AKzlSDWBcBk+_7S6V@#W zyOWK-{G+}3Ux!NlV_*N@mKf3-{>+4xo;TnGC<{Fs8x|P;)vCdYFe4OdDVtT1nKe&f zbBX1CfIz9=j;RM6qCF?P0dcmKPw%7+bS_VXs|y93sTH+?g(`a2JKA7hwBq?hsjNOAwwZCDCpni}W3&UPFoYFs` zd%?OS&C|CJ?9{Yi5!e=R(L+f0suX|p1*@C{9(8SkdGMLtTdqC&H~Fso_V?lb>Fi^N z+PDMTs)Nq-_4h`evDZrvPIG*Aw9I=)Rc&^^uYw!`?FPjE#9V(;WkVk5i@7(P@0{}L zy(Ie;cwmY`_IJlmfI@AuE>WOFLB$L4Q_@Y&qk^_eX&083$9j4?GS$=MjOo<6`x~Wh zmG%h^!8`(JeK-o-+2D(>7dB zRCEL|UMm%@rEJw`T*H4Re(qE0}E?5?YVem zE~oCY*Y=zCGqialkvqD!k_0~(aPveLaj#EKn>^?p9HtDhkzXGZEMhjCOPPVF$9u-0 z*%`0zPKSo!C9UvFSaC(^{cVcMIV|@*SuOnvu~^k4ZQw+$I%L=`~nVVl*7b@-<492XeT&&qC!yM?#&y;~|)*4I$3`)l7 zdq2w^jQduugr9xBW^9v0d~|HLyY*n}zi-7}@3Ng&_5b>vgh+b=z!6EKpwJWQaKkOMRu z?tykqw;T4?jHK`Dtc+BxAj7Gj2O?VXYSX-$e-l06zh}N`#G&6$j&Ib1-K}Z7Bmd6xlZ>lrLs_)yt{fY3B<1 zTjja)9N`&NHX}$=XP4H}p1T9|3EL>M-5@C*tJ$#6>+!`ZuqhxzpNCYF0u zm|JeVU^-^v7%3>INQc&Ul86syaQU-F6jR1~0dlU(5=swnQ{D@^e}?aSZp>*JgHW9jQakB>0R zQzfpUvIN<-DM7so`w~-VO#BU7SLsL|{oY`|#DXBxUTJl_gU*$kJDXb;A0?CVdC7~r zFGgQ8i{(sS6)5-}=Dr8ELQZMMQ}@2fZ2tb{O#>&crxBvL!;`J;_WW!3_(WYRoP)yd z7?sasKi*H-l!xUK$Vsq-QUl{=w#_13f)Z#kZ((uB??}kkWXp;U=Wa$KxELO7M^@j& z7muY@mfegMbc}5twi@VJ2&FI7Lcyc*4guyClsvt9wJ@~SE-W^gbW#!fOeoeCKFkU- zsP|)*nh^@d<9mnBsfm44Ls#!wtun<~vvUB11&saJc8;w6r9mh&^VcCZCRh@d3KL2& z^N&QNyUcou%JU+~qkFfvYQ76Rddsk|6CcK--afmKrir*57Hc*Arm+TCWbQfa3^VIU zJ!()s^DKAH=d5T)5{h`;f0X03v0JE>FHdGaDz~>jdt+NdCJfcz+|M?18TU)&3i>Fg zCq6z=FCB#fWf^Uy(`&V}P8!@RUdvC@9!N`zAW4FV zup`B4VOu~Tyi|XVAqnil+l5%EDBdb%l}MVFX5@t9Jboi3C3&y)B>%79zr9p@FFq*!jZ!)Mc1()hqVK2t=ZCfdEYUVytgnC{X|T+ z+l9LjeZ5!MpaCTm+eHGwK=5_W@9tA)?H-`k%fq!lOf}(sPrw1NIvLWl>)Q`t988Ik zSiT{VmK@l~zy<iNQBFUv3TR#=v66?T5KZ$2*blJ`Uw0EIJCga&VL=OpQeu3QQ`_$ zV2hBRHTSFM62t2?VjtClU%O~V;qtY;X0?!+J95`uio$ggF*ZF;qo|+>18k{;(#$}} z<|%2VfzVGHnk6)OiE8I`^fig6iB1m_Ls~Scep#XtlA^t|)&%{LbmzhFkGbrWBfpw% zzUpCJz5r`0n`jDc>eBlYb?aEfvf}2{_|ir0Qu<)HZH@1`w2hgxlFidk2B4CyOhdQk zg>!0#I_{0Ft;qiEAWI7Gvz61l$Q!FfPLPF&NJ-2!*PPj@i*`3>rn;Cwx|O1e)!Do9 z-q4RfTiDA%gH{Zh^tU*zO$J;{33wO5hFrD(Wh=fTfZp}GA0Q+ts74%7J_RoKE% zMk`!ZW4cFJPcbhkdRL$Aqfk$>CTE^HH!w78i?AtM&F&EMjz6mDu9)W zv#cW)_oO9VbHb6J@2bMgAq8F$A{7>U=RHLQRMWv!{Ux!1j7WcGARS z?^rUaON99cs{a(oI`x~s@a)z@bVxx6^EEbK(2W@4O{g)~e3u;2@g$UDT^g>&GhF)P z;Id2*P?oEb9e(qt*hhSPmyTLzk3B)Cxz+SZ`Cx<`pQ%r#nM1>dcjj}q3rf#eD(~EO z+@$1>c>mxV<{Wf-)mE=9(=Ut1=VzB8U30lzdJ3ESqCsnk!u*zn1K(sSe(;GU73-K9 zDpgLVmFA!KFTQ&p6x)$|!w}FyotfY*#tqOxbsuZmOR^Uto{;ceF)QdA!xXMRb(Y7S z{yc~KSbkX7v#Pt>M11(IV+IS*I$5M9I50mxw~$`tCoTA^pID&6z%DDV!0pCsoW%YG zl%Mb9LT*KJ@X;(na8BFNCP&7=%nUAd-SnyLy*~M17#Xooz%H>mz5E4h2+_GXeSQBf zRYQqD34+9$(5RAanSpdE!=l}=Q}y$@Y&`@qP|`GvlW?2uRauW zo=ffYjGpa_wB}N@u@TW7gY!)c+%$8z1QFBP~5WduSkllv^?yyMNRzBx*FBqZ#f#|FxfV7$M8WhWf_ zoSFY876Rs(&O*`pINlcNaWLo%(cRSrq|?3$X>O9t9N+AMoTHq(7-9dmOf`3`$e#iX zG>Ess*`2?BX`eaJFwBOQ_UHfL6BW8Hh@g9iPs@@$hBj91TK9`ZbORB|FsaQGFS1wU zp{^>tcwD0lhNQ2rzbcbn`JGQ$Ag$S8z3=Pk&BE0E?|-+G$dCAqw12nDWVTyGqbekv zsu!IV;WFc)k+SZels@8ei6M zPcFmDt?yMguN`z>_WAdElVDWEXK6L>)`e+O$-9n=%^Mp9C|-gArkTB0|665 zCb1(0PM9Y$#PZA@WDrsTsYce2S|9C+1c-3Yu!Tlg+WM*D@%M?q+lxq<`nJMQ8>Sd5 zz;Smv20!L6U(y(ua6F3k`v(1_RmxIWCmau{kteK3%da_>%f z^(-QQj3vd&%X(OiJ9}Ahi;MfpfSeFq+Z=Se-tPYX;R=g+;$c{8(hi%c*Iz&~RDgEh zQa@#ADCOlRq%B0qD%!lHE9_?AK4=xMW58@BbmC)(*NU#i#D8;C8`1T-O?;ftI7A6? zvy3Ua7W<~tO_!m%E$Jzv(n7NKAMneuP~}nE{vzixcIXvO$ zbgSQzFSM#G800%*q3r`UH4f!&Fz~(?yl7n4%&}3L?u7&a92MPC3?M@FoltR%rENZ? zuoF>XI@FsH2bfsJ`F6@wDeQ|`mP`mbzCj=m7Z=gxg&jDYAKVSFxZoR-ndo5Ol&S>r zWpNb?svVH7?-8>XhV_d>XqFy8UqBcGz{D?wPQ_&zT?)r);5z&h_;Y*S0v5!!bPEsUFo0Kyl83&1kH5X>q~6<*m>r(37@us{&=l5`5%5-@M!nI#Vc zf@^Xnb#;`D31xPfc36f&?gB(dDQZsrc%ODS50t$K8%%Lhxh~%y+Pj3n;@gJMUL9(t z^^2b^HC)R5Qby+;y|`5fMu2`~tLytWZx-4#2Xncxhoja(#1teB_|iht60PoBUJ;Qv z>C2M^iOGx4{KXbJzTb{)1g7<1sfh;Vno@)L!FHs`aa5PNUb5zXImx^hZ(q8KoNr@S z;c8Edjs)4{_@sH{#5{xPZ6P8MY+XpSR@n2LK2H2TYW(HXLZLj?m zw5w`J2$g2rIx*A56HJv`n<>ZbpIet-X=edCQSaJsOz`KT`*>=IamS4$sTRojxV;IE zfQCJqk$b*)6h3(AGUPF1R=j(*CrSFM`f#@l%`OMu&RAeWdjbdK*iTStzrP_Y)Wg$h zc#=f}VusDpH2WOt>^%9H%?gQoA9t=#%`9Sr3K0~?aN-A2k zfE^uY$s@y0wD{KqyWbzi1$?Wuyrwz?K`Er4M~H5D ztLv_$z17j>u@+#eU#%8-1gV%FJNofTZr-nTo@u|#+9l>t2Y$gP!OR+hLXm!6wcw8e zndot0bcE>j<@ZzcD1Pt_wdAY%%R?fE#!-&N2|!;-2y7@xMjj(z7u)B;1+K350m>e` z)Kl}uGWD*4hM#p~G;qwyZvDop+Ja=G$^;Z9-U&*;dQWhGT;TL6aExO{Ayd&hS`AqP zk?htldf(Lq71h)zU0;(bHh{NW zG!w_CK=VVi6n~4r*}IAxJ9j-iJn}t%|9H~9`I{5el|nL0;GHq_&0<{qY&M9Odi?CN zqu?OQMFtr^}p;aa15v=GLa4r8sh#xA7EUO+b>$-92?@Xi=i zwX;YJ|rquzBa`>8B&VM@-p! zDBUttl-gViAGmhaM}F|o_)`p_2tdxq@AsDq%n>on`Dt3 zzP(KYPL7cG8Cl?OZLZB;$sFa4O!*u-zB;+v@@C81R0ei_Qdfu!?he$%QovxGlh#&IHV50+?2qb>oZ>swYkcY0%G)S zxOY)Y{MQSYgsz(Qdka2%*J9f?szkMYSZKX5;vJBH|C`XmL4ZhxY|zn>PrLBi!pHmJ zMQxzc;Pvw%hAaSRv$Zph`lSIZI9wbf=3mI*1X-@`JzYB0x%w{Gw}zc~i3#zxq@^?0|-GfE?i0u&{aqTnBV0-WTn zPpwO51{DE-M}x)LvnkJj9~sc03d|W?=nI$)(>#jtFtZM{-GJcUGl$2Edjb#v*k31ofkRm40t``| z72iepGh6=7%ZA3!VXQVquZr`ziF8|afQOv!8qvJM1(XV#?IXxzc#4ZJD+t>>M=#^; zBRA`wl`qz!{c;G4*EbN@@YBJD`8>I430)!KRG|>r@QkR@u94B!{QCJ0CJS091(f3&{vgondB4wMA?U8SZx!f%nZ!muYypN;25*QI zU9}!Fx&N-iY80fpcWQnv6>FN9nUi|LTuSG&TbQ9IN3<_+yNe$R`(13&YiRd_e95^a zwn^eGuzzv1G%>ewnqw`A5hSdv=BE!ED!-m3^j{lOQdn3Pq+W0swo93RPw zU~DXU!EJT1`<}(+BZ~e(V&S&hJ!`3tG~8X>zS}oRP+FGcPPV7!(FXU#xSNZNy)ulO zpU>P&fUuWW5(_>SIGt~W~^<@l1(n#k_*jcWjxVp~bCQpY=fI)7dFO1)?^D$r6 zQNoMc|e2b?+IGL6fv&UbWqHa0}m3qe=ZFo%7UJ@R70lF)?ka3o`u`Dn` z0tRs{Y}n~jL`ZU}kguHiubp{2@W8`|vQG3N;sHKtYly)oTJxaCDyD3U4jUFg z-*k3DMIbFyBWxfoqARh=ULvc)jDvPmZFB9QH4g1;)KVGDBRgIE-}gmx-rAdm#^jm{9&3zGW!lcA{)tx|n3t8W~5mbaAVU zPLbzCuC@7Rg$fiqa8hgX?1UN-0J3fm%h##sSlg9zeL`u>%)4K9?V9`C<>pqp;i`t7 zeP@U=(H}vd$8qrYRT0`XCW2k_;U}{DBo@)=*z&g%a|t?vGaaEkF7P{2ss@Cd#3}um ztyFV#@I>(!K$@GMCXbiDra<3(@s4F&pvt+?vW;T2+>|34tJdn<>gc4IP4&zvS%(L9 zz~52W0D}Uypr%`&pkSIcg?Xdh&+y^ed7aZ|T^iQ-ePJ;6UCgIG+G3DokY8{*j}U!3 zR0r3Qh)fvGkg|&Y(`mla4ymym>=^j{E^!e`2@Ms1v}UbMhwsQ7{(YvQ>%o z6bM2vJNI)f@m0g#{u{PnmtlM$I#IZNDKZr}_I`#83%-lQ39eRf z%Aj=WYyIs4M1~lwlgF~+UGy=5^PtI7UBlSl%=o&PC?PzVy3x6Zq93SD(?9GGU0f7w zS33@4s$Ayi6v$~|VMJCxolqfKX*!?SJi8Y1B*fdc%+xM>HfC`3j*Xi~-2L*w#-OB6 z1H~SbcV~V}A2eVuAU&BvqFj0~mj?X1`lL?1VO@@CHG$B4r&~CC)0oQuGWC`s|=3S-=ULM9OD$h0V}IC-;LG_xcx7fuSkf*~~OR zlA%{E&+C;6FVl4ny;HosVJ+G_YIQ=~XUei1SA#EEipjO`p4o35-)z@X>S+DOSVf}8 zoKbc}ImfV`DYbRyCImJE*UPl{>>`le9sx~>FlQX=A*N3L;Dfw$NmHi`d-RtYlnpjX zNO%{=_IbzAJqhTnbsa_I{>DSSKKL5u5*X{5mXA6ZtgJn?R2v!?H2 zBW>o3yL`uYEdzKOCQ9(2=*LfmI zB;T-XX40}0bjLE{m4{R!yruB{^Xx?ekw=^E&<>Psn7FhCm~7-GO+3*M1=`@_Su1DZ{hP>uiNr$feUQ@OcpYgsR9Cfcg-8L5JX zyfjI%N$~Z|CI#6SdkOWvR|<@sUr#UuLCvP6R@N-%8JFEPrbGh0Y%6kGrCqiCvMcs4 zNS^jqg83;@^!4}kRuU@nOynPF5u-jWL5(1gd%bms{`AX^j|=aMaK*I}tK8n~u2!F4 zN^#zrm>q!{UOXS$Z9S+q^vc(dGRzhr#B~t|ihXVll$w59bdnLbsP9urTgWhVVT^w? zODr?E^m}C!LokmVNo;NT-WF)>TwKN#CGy|)_X}rT%z<=lcHP~1F5H`KlR-5(b<3Sx z)wCcxK|mI~u4tiq)+Pse8h>bd(H-qiWps@^lxq{{plD_5vqDS;{y2|@eM~#a#*;w zEJ{P9exZu1-rLKWi~R zBv7+v=hC|iIg&9Ajx`*FHM~MLA;X0sW&M#;<}|<7#Hfn24_(#o-GkaZzHpZHYFvYu zK)*Usw2UL%&e#@uA^*IcI`&=STN#eyC#G@PgMGL3S48HxgQ5m0k=X{Vs&H!E`V0w(1d2SkQrQ(YFiOK`dKWv_!=J4QN$C<;dX1MXM}jiE>cITnoana zqLIP|g^+V|ECB(_5|oQrzFu1A5S>lb?8U2VhLyCX`w3pInggS(CVY7O4kU1j;vGXR~6CWRz<{Lqw|o3{V#IOt#uc{Ivu zjy*tb-yb0K=H(765*|@!XyE1%Q)E+{+tE1Ob;XerX`5HOafuc*3#xmy_+pSe1hGiV z%u=SBb#zSs{JtI09xziIx@2aLNuQ@)K5cM*$fk;ZeUgvg^1**xP5(>3j{X&*RkaAW zKBy2;HTe1$_tk%?uYYKC$s2sGVvOhRk6kgkdUD?B0voQ`Cd$i?2qfTVe%_dyoI{Th z{4D7KGnt|Hq;n^%YmPk(c-2qXMs(cU-dzgGRQ68*QOD;Y^mzqLxzt z1=>lTB4#*`zj7tucI$g)@m0vcBtG40@3c>2NbUD3vp$%_8qPzO?n zh6H%b^2n}SgI!}+!a}co^2-P~CQw;3XJp@L8+Fchv!l<8Ex4bY%J7pALU{Y$=SmkQ`foO+*cObi<5s$sC; z9^2}>6J}iL1pz>j;LajE?;qYMBwqL3p8A*8G>gMbsj`_v*MD=8*(}SWahu_Z1`2^CaZ`{-?D#(BG`RSrC#W{LV=hmXdo-v`e|4vXF zf{MGGHEU@vy(Ud^~o|cvR*EJAi9L zT(sta3>G%o;A%k7nn4^xcmv!{ku3Ezg;@j|@Yvms%zAm844_e{*FM{XP|Y4+*4e0f z@KXzOT=Vs4M)a|?&siSpqVW5ca&+=jJRLX3NKVzF0cW$Bkg%-4eNr!Dhw-t`c**`; zY_Al2sdk?yPc>xxVbjfYpdMoCR03+n$74%oalQ$h3GIl`VLSKPdA3>71sSdA>{lR^9U-=yCN`nJ00`5&xAdN>2SyM1Pwpyy zii$gDbVQqWmB$nf3!ItL_f_^#c`iD=I5x z7(3qZShPY@e@bjKz|7sT&8cIGJ?IU`aK@u3l`D70qY}d`62|?k1&3qHwVgo=3y)`K z7jl<;Tgr~d1rW*!a!88*(z9c_WimP_MylXI5olrc>}yrC9NKl_lbpK=#;!3jpB2jA zti`U~S4RjqGDKNgY?Nu|-iG;nB4C*9U;S|T{(u(+R&mwe$7*KOKha>NmfQhC(BIY! zyxCevdEDH9$~x>mN$@N#?wKy?IelYNgZReeiBUod7@D{0^0CT5^ja-qZ-XsxIgQ8n zSit#*!&5wCZ;DkL|KOBs@Tbg!s{fgvl+`PU|Iy z6`OWLgYvco=Aw(K|B1u*0fGKL86B~g;--AUxVmea5niJtP{g_^keC^G8y0zPU}sal z4}!$NOO1`4&6_cKpGCo82N2>7dGl3gOa5W+#AehSY#>#~1F*VnP_jkR>v;7));I<< zbQWa?7MlNip;KeBgI)^qGn%!>5qjFu#W_kr;GzFs*#Ett@AHIc#W$XhRRi?E&p+@M z365;_cqOK}o;1g=R8RunHBB_=(DDCX2RjrQKdg_sT=g9WbQrtxL}4@3>ws+(TG49= zzk@g-Cp(o8uD%4y&F-}SMQhwhrH|{s+OrurK61(2D_C!jAJqbk^gU0`0sEe{5LN9PdEJ~A06Xx{Tb#R}X#9q|!Ao2E8Y^yc{|BEAMtV>) zjEpQo67o~-}qABU)TCPjnV@C95qOi`S_4{WLJbEhCqT6O(&ldS?1EPQr$< z2`fyES)#A#Xz5GsT!KAMCeBzEUnZ)lIgV**r7R)$4fK?i4rW^Vc`3!UZnyIjuRD4E z-q>rW;^Mbu>JuqH>|RKeik|GK-1H)mOU|Fjx`j zm&QH^I>UF;T4ys`19)Se;)P#|N^R)qzFV=u#)}Mb7vtd^3&+c{`v9mB*!< zd`}DtTp7HARQ_gw75ktlWa^#dC=;sh{ldM@E)lHUSOpp((XW~}7kg6U4bL@oovn+v z@#1L|9Kt4w*BrXH{zoP*s8uzF@5uds;gJ8m>whU;^k04R|C0Xy&rRCz6nP literal 0 HcmV?d00001