From 54845dfb24f9fe491869018ec81245a9c4c7340b Mon Sep 17 00:00:00 2001 From: Alex Sberna Date: Fri, 19 Jul 2024 15:51:20 -0400 Subject: [PATCH 01/20] initial commit of tutorial demo --- .../capture-leads-app/.vscode/settings.json | 3 + tutorials/voice/capture-leads-app/README.md | 58 +++++++ tutorials/voice/capture-leads-app/pom.xml | 52 ++++++ .../src/main/java/com/mycompany/app/App.java | 12 ++ .../com/mycompany/app/CalloutService.java | 64 +++++++ .../main/java/com/mycompany/app/Config.java | 29 ++++ .../mycompany/app/QualifyLeadsController.java | 40 +++++ .../mycompany/app/QualifyLeadsService.java | 160 ++++++++++++++++++ .../src/main/resources/application.yaml | 12 ++ 9 files changed, 430 insertions(+) create mode 100644 tutorials/voice/capture-leads-app/.vscode/settings.json create mode 100644 tutorials/voice/capture-leads-app/README.md create mode 100644 tutorials/voice/capture-leads-app/pom.xml create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java create mode 100644 tutorials/voice/capture-leads-app/src/main/resources/application.yaml diff --git a/tutorials/voice/capture-leads-app/.vscode/settings.json b/tutorials/voice/capture-leads-app/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/tutorials/voice/capture-leads-app/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/tutorials/voice/capture-leads-app/README.md b/tutorials/voice/capture-leads-app/README.md new file mode 100644 index 0000000..29cbbe1 --- /dev/null +++ b/tutorials/voice/capture-leads-app/README.md @@ -0,0 +1,58 @@ +# qualify leads application sample + +This directory contains sample related to Java SDK tutorials: [auto-subscribe](https://developers.sinch.com/docs/sms/tutorials/sms/tutorials/java-sdk/auto-subscribe) + +## Requirements + +- JDK 21 or later +- [Maven](https://maven.apache.org/) +- [ngrok](https://ngrok.com/docs) +- [Sinch account](https://dashboard.sinch.com) + +## Usage + +### Configure application settings + +Application settings are using the SpringBoot configuration file: [`application.yaml`](src/main/resources/application.yaml) file and enable to configure: + +#### Sinch credentials +Located in `credentials` section (*you can find all of the credentials you need on your [Sinch dashboard](https://dashboard.sinch.com)*): +- `application-key`: YOUR_application_key +- `application-secret`: YOUR_application_secret + +#### Server port +Located in `server` section: +- port: The port to be used to listen to incoming requests. Default: 8090 + +### Starting server locally + +Compile and run the application as server locally. +```bash +mvn spring-boot:run +``` + +### Use ngrok to forward request to local server + +Forwarding request to same `8090` port used above: + +*Note: The `8090` value is coming from default config and can be changed (see [Server port](#Server port) configuration section)* + +```bash +ngrok http 8090 +``` + +ngrok output will contains output like: +``` +ngrok (Ctrl+C to quit) + +... +Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http://localhost:8090 + +``` +The line +``` +Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http://localhost:8090 +``` +Contains `https://0e64-78-117-86-140.ngrok-free.app` value. + +This value must be used to configure callback's URL from your [Sinch dashboard](https://dashboard.sinch.com/sms/api/services) \ No newline at end of file diff --git a/tutorials/voice/capture-leads-app/pom.xml b/tutorials/voice/capture-leads-app/pom.xml new file mode 100644 index 0000000..0abdbbb --- /dev/null +++ b/tutorials/voice/capture-leads-app/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + my.company.com + sinch-sdk-java-tuturial-auto-subscribe + 0.0.1-SNAPSHOT + Sinch Java SDK Capture Leads Sample Application + Demo Project for Capturing Leads + + + [1.2.0,) + 21 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + com.sinch.sdk + sinch-sdk-java + ${sinch.sdk.java.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java new file mode 100644 index 0000000..195ae60 --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java @@ -0,0 +1,12 @@ +package com.mycompany.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java new file mode 100644 index 0000000..f8bb189 --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java @@ -0,0 +1,64 @@ +package com.mycompany.app; + +import java.util.Scanner; +import java.util.logging.Logger; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.domains.voice.models.requests.CalloutRequestParametersCustom; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; +import com.sinch.sdk.domains.voice.models.svaml.AnsweringMachineDetection; +import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; +import com.sinch.sdk.models.E164PhoneNumber; + +@Service +public class CalloutService { + + private static final Logger LOGGER = Logger.getLogger(CalloutService.class.getName()); + private final VoiceService voiceService; + + @Autowired + public CalloutService(VoiceService voiceService) { + this.voiceService = voiceService; + } + + public void makeCallout() { + var response = voiceService.callouts().custom(CalloutRequestParametersCustom.builder() + .setIce(SVAMLControl.builder() + .setAction(ActionConnectPstn.builder() + .setNumber(E164PhoneNumber.valueOf(prompt("Enter the phone number you want to call: "))) + .setCli("YOUR_sinch_number") + .setAnsweringMachineDetection(AnsweringMachineDetection.builder() + .setEnabled(true) + .build()) + .build()) + .build()) + .build()); + + echo("Callout response: '%s'", response); + } + + private String prompt(String prompt) { + + String input = null; + Scanner scanner = new Scanner(System.in); + + while (StringUtil.isEmpty(input)) { + System.out.println(prompt + " ([Q] to quit): "); + input = scanner.nextLine(); + } + + if ("Q".equalsIgnoreCase(input)) { + System.out.println("Quit application"); + System.exit(0); + } + return input.trim(); + } + + private void echo(String text, Object... args) { + System.out.println(" " + String.format(text, args)); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java new file mode 100644 index 0000000..b20efbb --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java @@ -0,0 +1,29 @@ +package com.mycompany.app; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.models.Configuration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; + +@org.springframework.context.annotation.Configuration +public class Config { + + @Value("${credentials.application-key}") + String applicationKey; + + @Value("${credentials.application-secret}") + String applicationSecret; + + @Bean + public VoiceService voiceService() { + + var configuration = + Configuration.builder() + .setApplicationKey(applicationKey) + .setApplicationSecret(applicationSecret) + .build(); + + return new SinchClient(configuration).voice(); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java new file mode 100644 index 0000000..789c1db --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java @@ -0,0 +1,40 @@ +package com.mycompany.app; + +import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; +import java.util.Objects; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class QualifyLeadsController { + + private final VoiceService voiceService; + private final QualifyLeadsService service; + + @Autowired + public QualifyLeadsController(VoiceService voiceService, QualifyLeadsService service) { + this.voiceService = voiceService; + this.service = service; + } + + @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public String callEvent(@RequestBody String body) { + + // decode the request payload + var event = voiceService.webhooks().unserializeWebhooksEvent(body); + + // let business layer process the request + if (Objects.requireNonNull(event) instanceof CallEvent e) { + return voiceService.webhooks().serializeWebhooksResponse(service.processCallEvent(e)); + } else { + throw new IllegalStateException("Unexpected value: " + event); + } + } + +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java new file mode 100644 index 0000000..e1dfa63 --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java @@ -0,0 +1,160 @@ +package com.mycompany.app; + +import com.sinch.sdk.domains.voice.models.DestinationSip; +import com.sinch.sdk.domains.voice.models.TransportType; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectSip; +import com.sinch.sdk.domains.voice.models.svaml.ActionHangUp; +import com.sinch.sdk.domains.voice.models.svaml.ActionRunMenu; +import com.sinch.sdk.domains.voice.models.svaml.InstructionSay; +import com.sinch.sdk.domains.voice.models.svaml.Menu; +import com.sinch.sdk.domains.voice.models.svaml.MenuOption; +import com.sinch.sdk.domains.voice.models.svaml.MenuOptionAction; +import com.sinch.sdk.domains.voice.models.svaml.MenuOptionActionType; +import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; +import com.sinch.sdk.domains.voice.models.webhooks.AmdAnswerStatusType; +import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; +import com.sinch.sdk.models.DualToneMultiFrequency; +import com.sinch.sdk.models.E164PhoneNumber; + +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class QualifyLeadsService { + + private static final Logger LOGGER = Logger.getLogger(QualifyLeadsService.class.getName()); + + @Autowired + public QualifyLeadsService() {} + + public SVAMLControl processCallEvent(CallEvent event) { + + LOGGER.info("Received event:" + event); + + if (event instanceof AnsweredCallEvent) { + return parseAnsweredCallEvent(((AnsweredCallEvent) event)); + } + if (event instanceof PromptInputEvent) { + return parsePromptInputEvent(((PromptInputEvent) event)); + } + else { + throw new IllegalStateException("Unexpected value: " + event); + } + + } + + public SVAMLControl parseAnsweredCallEvent(AnsweredCallEvent event) { + + var amdResult = event.getAmd(); + + if (amdResult.getStatus() == AmdAnswerStatusType.MACHINE) { + return machineResponse(); + } + if (amdResult.getStatus() == AmdAnswerStatusType.HUMAN) { + return humanResponse(); + } + else { + throw new IllegalStateException("Unexpected value: " + event); + } + + } + + public SVAMLControl parsePromptInputEvent(PromptInputEvent event) { + var menuResult = event.getMenuResult(); + + if (menuResult.getValue() == "sip") { + return sipResponse(); + } + if (menuResult.getValue() == "non-sip") { + return nonSipResponse(); + } + else { + return defaultResponse(); + } + } + + SVAMLControl sipResponse() { + + return SVAMLControl.builder() + .setAction(ActionConnectSip.builder() + .setDestination(DestinationSip.valueOf("YOUR_sip_address")) + .setCli("YOUR_sinch_number") + .setTransport(TransportType.TLS) + .build()) + .setInstructions(Collections.singletonList(InstructionSay.builder() + .setText("Thanks for agreeing to speak to one of our sales reps! We'll now connect your call.") + .build())) + .build(); + } + + SVAMLControl nonSipResponse() { + + return SVAMLControl.builder() + .setAction(ActionConnectPstn.builder() + .setNumber(E164PhoneNumber.valueOf("YOUR_phone_number")) + .setCli("YOUR_sinch_number") + .build()) + .setInstructions(Collections.singletonList(InstructionSay.builder() + .setText(null) + .build())) + .build(); + } + + SVAMLControl defaultResponse() { + + return SVAMLControl.builder() + .setAction(ActionHangUp.builder().build()) + .setInstructions(Collections.singletonList(InstructionSay.builder() + .setText("Thank you for trying our tutorial! This call will now end.") + .build() + )).build(); + } + + SVAMLControl humanResponse() { + + var option1 = MenuOption.builder() + .setDtfm(DualToneMultiFrequency.valueOf("1")) + .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "sip")) + .build(); + + var option2 = MenuOption.builder() + .setDtfm(DualToneMultiFrequency.valueOf("2")) + .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "non-sip")) + .build(); + + Collection options = Collections.emptyList(); + + options.add(option1); + options.add(option2); + + return SVAMLControl.builder() + .setAction(ActionRunMenu.builder() + .setBarge(false) + .setMenus(Collections.singletonList(Menu.builder() + .setId("main") + .setMainPrompt("Hi, you awesome person! Press 1 if you have performed this tutorial using a sip infrastructure. Press 2 if you have not used a sip infrastructure. Press any other digit to end this call.") + .setRepeatPrompt("Again, simply press 1 if you have used sip, press 2 if you have not, or press any other digit to end this call.") + .setRepeats(2) + .setOptions(options) + .build())) + .build()) + .build(); + } + + SVAMLControl machineResponse() { + + return SVAMLControl.builder() + .setAction(ActionHangUp.builder() + .build()) + .setInstructions(Collections.singletonList(InstructionSay.builder() + .setText("Hi there! We tried to reach you to speak with you about our awesome products. We will try again later. Bye!") + .build())) + .build(); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml new file mode 100644 index 0000000..1c2fae9 --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml @@ -0,0 +1,12 @@ +# springboot related config file + +logging: + level: + com: INFO + +server: + port: 8090 + +credentials: + application-key: + application-secret: From 8c0eef64dc7b4ea654542f75d854cdcfcf3819a0 Mon Sep 17 00:00:00 2001 From: Alex Sberna Date: Fri, 19 Jul 2024 15:53:53 -0400 Subject: [PATCH 02/20] adding missing text --- .../src/main/java/com/mycompany/app/QualifyLeadsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java index e1dfa63..77fbf15 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java @@ -101,7 +101,7 @@ SVAMLControl nonSipResponse() { .setCli("YOUR_sinch_number") .build()) .setInstructions(Collections.singletonList(InstructionSay.builder() - .setText(null) + .setText("Thanks for agreeing to speak to one of our sales reps! We'll now connect your call.") .build())) .build(); } From 158d0495d66619852a2f656c329e751c43d3d17a Mon Sep 17 00:00:00 2001 From: Alex Sberna Date: Fri, 19 Jul 2024 15:58:48 -0400 Subject: [PATCH 03/20] connecting calloutService to controller --- .../java/com/mycompany/app/QualifyLeadsController.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java index 789c1db..ec5ef50 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java @@ -15,11 +15,17 @@ public class QualifyLeadsController { private final VoiceService voiceService; private final QualifyLeadsService service; + private final CalloutService calloutService; @Autowired - public QualifyLeadsController(VoiceService voiceService, QualifyLeadsService service) { + public QualifyLeadsController(VoiceService voiceService, QualifyLeadsService service, CalloutService calloutService) { this.voiceService = voiceService; this.service = service; + this.calloutService = calloutService; + } + + public void callout() { + calloutService.makeCallout(); } @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE) @@ -36,5 +42,4 @@ public String callEvent(@RequestBody String body) { throw new IllegalStateException("Unexpected value: " + event); } } - } From 216ce04a14fafa72db8cbef1cd2a2b611a75952d Mon Sep 17 00:00:00 2001 From: Alex Sberna Date: Fri, 19 Jul 2024 16:04:35 -0400 Subject: [PATCH 04/20] updating readme --- tutorials/voice/capture-leads-app/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/voice/capture-leads-app/README.md b/tutorials/voice/capture-leads-app/README.md index 29cbbe1..66d4f2c 100644 --- a/tutorials/voice/capture-leads-app/README.md +++ b/tutorials/voice/capture-leads-app/README.md @@ -1,6 +1,6 @@ # qualify leads application sample -This directory contains sample related to Java SDK tutorials: [auto-subscribe](https://developers.sinch.com/docs/sms/tutorials/sms/tutorials/java-sdk/auto-subscribe) +This directory contains sample related to Java SDK tutorials: [qualify leads](https://developers.sinch.com/docs/sms/tutorials/sms/tutorials/java-sdk/auto-subscribe) ## Requirements @@ -55,4 +55,4 @@ Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http: ``` Contains `https://0e64-78-117-86-140.ngrok-free.app` value. -This value must be used to configure callback's URL from your [Sinch dashboard](https://dashboard.sinch.com/sms/api/services) \ No newline at end of file +This value must be used to configure the callback URL on your [Sinch dashboard](https://dashboard.sinch.com/voice/apps) \ No newline at end of file From 704e89e25c52a8a4f8ba52bd2999d80c9778331c Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 07:55:20 +0200 Subject: [PATCH 05/20] remove '.vscode' directory from repository --- .gitignore | 1 + tutorials/voice/capture-leads-app/.vscode/settings.json | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 tutorials/voice/capture-leads-app/.vscode/settings.json diff --git a/.gitignore b/.gitignore index c9f8a31..8cc84f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea +.vscode **/target \ No newline at end of file diff --git a/tutorials/voice/capture-leads-app/.vscode/settings.json b/tutorials/voice/capture-leads-app/.vscode/settings.json deleted file mode 100644 index 7b016a8..0000000 --- a/tutorials/voice/capture-leads-app/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.compile.nullAnalysis.mode": "automatic" -} \ No newline at end of file From f884d27db19a53ec98363a754445f110148caf0a Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 08:08:56 +0200 Subject: [PATCH 06/20] refactor (Voice/tutorial): Move to 'template like' directory/files structure --- .../app/{App.java => Application.java} | 4 +- .../com/mycompany/app/CalloutService.java | 64 ------- .../main/java/com/mycompany/app/Config.java | 4 +- .../mycompany/app/QualifyLeadsController.java | 45 ----- .../mycompany/app/QualifyLeadsService.java | 160 ---------------- .../mycompany/app/voice/CalloutService.java | 71 +++++++ .../com/mycompany/app/voice/Controller.java | 48 +++++ .../app/voice/ServerBusinessLogic.java | 176 ++++++++++++++++++ .../src/main/resources/application.yaml | 4 +- 9 files changed, 301 insertions(+), 275 deletions(-) rename tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/{App.java => Application.java} (73%) delete mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java delete mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java delete mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Application.java similarity index 73% rename from tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java rename to tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Application.java index 195ae60..03b399f 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/App.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Application.java @@ -4,9 +4,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class App { +public class Application { public static void main(String[] args) { - SpringApplication.run(App.class, args); + SpringApplication.run(Application.class, args); } } diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java deleted file mode 100644 index f8bb189..0000000 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/CalloutService.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.mycompany.app; - -import java.util.Scanner; -import java.util.logging.Logger; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import com.sinch.sdk.core.utils.StringUtil; -import com.sinch.sdk.domains.voice.VoiceService; -import com.sinch.sdk.domains.voice.models.requests.CalloutRequestParametersCustom; -import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; -import com.sinch.sdk.domains.voice.models.svaml.AnsweringMachineDetection; -import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; -import com.sinch.sdk.models.E164PhoneNumber; - -@Service -public class CalloutService { - - private static final Logger LOGGER = Logger.getLogger(CalloutService.class.getName()); - private final VoiceService voiceService; - - @Autowired - public CalloutService(VoiceService voiceService) { - this.voiceService = voiceService; - } - - public void makeCallout() { - var response = voiceService.callouts().custom(CalloutRequestParametersCustom.builder() - .setIce(SVAMLControl.builder() - .setAction(ActionConnectPstn.builder() - .setNumber(E164PhoneNumber.valueOf(prompt("Enter the phone number you want to call: "))) - .setCli("YOUR_sinch_number") - .setAnsweringMachineDetection(AnsweringMachineDetection.builder() - .setEnabled(true) - .build()) - .build()) - .build()) - .build()); - - echo("Callout response: '%s'", response); - } - - private String prompt(String prompt) { - - String input = null; - Scanner scanner = new Scanner(System.in); - - while (StringUtil.isEmpty(input)) { - System.out.println(prompt + " ([Q] to quit): "); - input = scanner.nextLine(); - } - - if ("Q".equalsIgnoreCase(input)) { - System.out.println("Quit application"); - System.exit(0); - } - return input.trim(); - } - - private void echo(String text, Object... args) { - System.out.println(" " + String.format(text, args)); - } -} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java index b20efbb..0401753 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/Config.java @@ -9,10 +9,10 @@ @org.springframework.context.annotation.Configuration public class Config { - @Value("${credentials.application-key}") + @Value("${credentials.application-api-key}") String applicationKey; - @Value("${credentials.application-secret}") + @Value("${credentials.application-api-secret}") String applicationSecret; @Bean diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java deleted file mode 100644 index ec5ef50..0000000 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.mycompany.app; - -import com.sinch.sdk.domains.voice.VoiceService; -import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; -import java.util.Objects; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class QualifyLeadsController { - - private final VoiceService voiceService; - private final QualifyLeadsService service; - private final CalloutService calloutService; - - @Autowired - public QualifyLeadsController(VoiceService voiceService, QualifyLeadsService service, CalloutService calloutService) { - this.voiceService = voiceService; - this.service = service; - this.calloutService = calloutService; - } - - public void callout() { - calloutService.makeCallout(); - } - - @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE) - @ResponseBody - public String callEvent(@RequestBody String body) { - - // decode the request payload - var event = voiceService.webhooks().unserializeWebhooksEvent(body); - - // let business layer process the request - if (Objects.requireNonNull(event) instanceof CallEvent e) { - return voiceService.webhooks().serializeWebhooksResponse(service.processCallEvent(e)); - } else { - throw new IllegalStateException("Unexpected value: " + event); - } - } -} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java deleted file mode 100644 index 77fbf15..0000000 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/QualifyLeadsService.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.mycompany.app; - -import com.sinch.sdk.domains.voice.models.DestinationSip; -import com.sinch.sdk.domains.voice.models.TransportType; -import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; -import com.sinch.sdk.domains.voice.models.svaml.ActionConnectSip; -import com.sinch.sdk.domains.voice.models.svaml.ActionHangUp; -import com.sinch.sdk.domains.voice.models.svaml.ActionRunMenu; -import com.sinch.sdk.domains.voice.models.svaml.InstructionSay; -import com.sinch.sdk.domains.voice.models.svaml.Menu; -import com.sinch.sdk.domains.voice.models.svaml.MenuOption; -import com.sinch.sdk.domains.voice.models.svaml.MenuOptionAction; -import com.sinch.sdk.domains.voice.models.svaml.MenuOptionActionType; -import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; -import com.sinch.sdk.domains.voice.models.webhooks.AmdAnswerStatusType; -import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; -import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; -import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; -import com.sinch.sdk.models.DualToneMultiFrequency; -import com.sinch.sdk.models.E164PhoneNumber; - -import java.util.Collection; -import java.util.Collections; -import java.util.logging.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class QualifyLeadsService { - - private static final Logger LOGGER = Logger.getLogger(QualifyLeadsService.class.getName()); - - @Autowired - public QualifyLeadsService() {} - - public SVAMLControl processCallEvent(CallEvent event) { - - LOGGER.info("Received event:" + event); - - if (event instanceof AnsweredCallEvent) { - return parseAnsweredCallEvent(((AnsweredCallEvent) event)); - } - if (event instanceof PromptInputEvent) { - return parsePromptInputEvent(((PromptInputEvent) event)); - } - else { - throw new IllegalStateException("Unexpected value: " + event); - } - - } - - public SVAMLControl parseAnsweredCallEvent(AnsweredCallEvent event) { - - var amdResult = event.getAmd(); - - if (amdResult.getStatus() == AmdAnswerStatusType.MACHINE) { - return machineResponse(); - } - if (amdResult.getStatus() == AmdAnswerStatusType.HUMAN) { - return humanResponse(); - } - else { - throw new IllegalStateException("Unexpected value: " + event); - } - - } - - public SVAMLControl parsePromptInputEvent(PromptInputEvent event) { - var menuResult = event.getMenuResult(); - - if (menuResult.getValue() == "sip") { - return sipResponse(); - } - if (menuResult.getValue() == "non-sip") { - return nonSipResponse(); - } - else { - return defaultResponse(); - } - } - - SVAMLControl sipResponse() { - - return SVAMLControl.builder() - .setAction(ActionConnectSip.builder() - .setDestination(DestinationSip.valueOf("YOUR_sip_address")) - .setCli("YOUR_sinch_number") - .setTransport(TransportType.TLS) - .build()) - .setInstructions(Collections.singletonList(InstructionSay.builder() - .setText("Thanks for agreeing to speak to one of our sales reps! We'll now connect your call.") - .build())) - .build(); - } - - SVAMLControl nonSipResponse() { - - return SVAMLControl.builder() - .setAction(ActionConnectPstn.builder() - .setNumber(E164PhoneNumber.valueOf("YOUR_phone_number")) - .setCli("YOUR_sinch_number") - .build()) - .setInstructions(Collections.singletonList(InstructionSay.builder() - .setText("Thanks for agreeing to speak to one of our sales reps! We'll now connect your call.") - .build())) - .build(); - } - - SVAMLControl defaultResponse() { - - return SVAMLControl.builder() - .setAction(ActionHangUp.builder().build()) - .setInstructions(Collections.singletonList(InstructionSay.builder() - .setText("Thank you for trying our tutorial! This call will now end.") - .build() - )).build(); - } - - SVAMLControl humanResponse() { - - var option1 = MenuOption.builder() - .setDtfm(DualToneMultiFrequency.valueOf("1")) - .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "sip")) - .build(); - - var option2 = MenuOption.builder() - .setDtfm(DualToneMultiFrequency.valueOf("2")) - .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "non-sip")) - .build(); - - Collection options = Collections.emptyList(); - - options.add(option1); - options.add(option2); - - return SVAMLControl.builder() - .setAction(ActionRunMenu.builder() - .setBarge(false) - .setMenus(Collections.singletonList(Menu.builder() - .setId("main") - .setMainPrompt("Hi, you awesome person! Press 1 if you have performed this tutorial using a sip infrastructure. Press 2 if you have not used a sip infrastructure. Press any other digit to end this call.") - .setRepeatPrompt("Again, simply press 1 if you have used sip, press 2 if you have not, or press any other digit to end this call.") - .setRepeats(2) - .setOptions(options) - .build())) - .build()) - .build(); - } - - SVAMLControl machineResponse() { - - return SVAMLControl.builder() - .setAction(ActionHangUp.builder() - .build()) - .setInstructions(Collections.singletonList(InstructionSay.builder() - .setText("Hi there! We tried to reach you to speak with you about our awesome products. We will try again later. Bye!") - .build())) - .build(); - } -} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java new file mode 100644 index 0000000..ce68587 --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java @@ -0,0 +1,71 @@ +package com.mycompany.app; + +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.domains.voice.models.requests.CalloutRequestParametersCustom; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; +import com.sinch.sdk.domains.voice.models.svaml.AnsweringMachineDetection; +import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; +import com.sinch.sdk.models.E164PhoneNumber; +import java.util.Scanner; +import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class CalloutService { + + private static final Logger LOGGER = Logger.getLogger(CalloutService.class.getName()); + private final VoiceService voiceService; + + @Autowired + public CalloutService(VoiceService voiceService) { + this.voiceService = voiceService; + } + + public void makeCallout() { + var response = + voiceService + .callouts() + .custom( + CalloutRequestParametersCustom.builder() + .setIce( + SVAMLControl.builder() + .setAction( + ActionConnectPstn.builder() + .setNumber( + E164PhoneNumber.valueOf( + prompt("Enter the phone number you want to call: "))) + .setCli("YOUR_sinch_number") + .setAnsweringMachineDetection( + AnsweringMachineDetection.builder() + .setEnabled(true) + .build()) + .build()) + .build()) + .build()); + + echo("Callout response: '%s'", response); + } + + private String prompt(String prompt) { + + String input = null; + Scanner scanner = new Scanner(System.in); + + while (StringUtil.isEmpty(input)) { + System.out.println(prompt + " ([Q] to quit): "); + input = scanner.nextLine(); + } + + if ("Q".equalsIgnoreCase(input)) { + System.out.println("Quit application"); + System.exit(0); + } + return input.trim(); + } + + private void echo(String text, Object... args) { + System.out.println(" " + String.format(text, args)); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java new file mode 100644 index 0000000..8f86677 --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java @@ -0,0 +1,48 @@ +package com.mycompany.app.voice; + +import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; +import java.util.Map; +import java.util.Objects; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController("Voice") +public class Controller { + + private final VoiceService voiceService; + private final ServerBusinessLogic webhooksBusinessLogic; + + @Autowired + public Controller(VoiceService voiceService, ServerBusinessLogic webhooksBusinessLogic) { + this.voiceService = voiceService; + this.webhooksBusinessLogic = webhooksBusinessLogic; + } + + @PostMapping( + value = "/VoiceEvent", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity VoiceEvent( + @RequestHeader Map headers, @RequestBody String body) { + + // decode the request payload + var event = voiceService.webhooks().unserializeWebhooksEvent(body); + + // let business layer process the request + if (Objects.requireNonNull(event) instanceof CallEvent e) { + return ResponseEntity.ok() + .body( + voiceService + .webhooks() + .serializeWebhooksResponse(webhooksBusinessLogic.processCallEvent(e))); + } else { + throw new IllegalStateException("Unexpected value: " + event); + } + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java new file mode 100644 index 0000000..0bfb80a --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -0,0 +1,176 @@ +package com.mycompany.app.voice; + +import com.sinch.sdk.domains.voice.models.DestinationSip; +import com.sinch.sdk.domains.voice.models.TransportType; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectSip; +import com.sinch.sdk.domains.voice.models.svaml.ActionHangUp; +import com.sinch.sdk.domains.voice.models.svaml.ActionRunMenu; +import com.sinch.sdk.domains.voice.models.svaml.InstructionSay; +import com.sinch.sdk.domains.voice.models.svaml.Menu; +import com.sinch.sdk.domains.voice.models.svaml.MenuOption; +import com.sinch.sdk.domains.voice.models.svaml.MenuOptionAction; +import com.sinch.sdk.domains.voice.models.svaml.MenuOptionActionType; +import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; +import com.sinch.sdk.domains.voice.models.webhooks.AmdAnswerStatusType; +import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; +import com.sinch.sdk.models.DualToneMultiFrequency; +import com.sinch.sdk.models.E164PhoneNumber; +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Logger; +import org.springframework.stereotype.Component; + +@Component("VoiceServerBusinessLogic") +public class ServerBusinessLogic { + + private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); + + public SVAMLControl processCallEvent(CallEvent event) { + + LOGGER.info("Received event:" + event); + + if (event instanceof AnsweredCallEvent) { + return parseAnsweredCallEvent(((AnsweredCallEvent) event)); + } + if (event instanceof PromptInputEvent) { + return parsePromptInputEvent(((PromptInputEvent) event)); + } else { + throw new IllegalStateException("Unexpected value: " + event); + } + } + + public SVAMLControl parseAnsweredCallEvent(AnsweredCallEvent event) { + + var amdResult = event.getAmd(); + + if (amdResult.getStatus() == AmdAnswerStatusType.MACHINE) { + return machineResponse(); + } + if (amdResult.getStatus() == AmdAnswerStatusType.HUMAN) { + return humanResponse(); + } else { + throw new IllegalStateException("Unexpected value: " + event); + } + } + + public SVAMLControl parsePromptInputEvent(PromptInputEvent event) { + var menuResult = event.getMenuResult(); + + if (menuResult.getValue() == "sip") { + return sipResponse(); + } + if (menuResult.getValue() == "non-sip") { + return nonSipResponse(); + } else { + return defaultResponse(); + } + } + + SVAMLControl sipResponse() { + + return SVAMLControl.builder() + .setAction( + ActionConnectSip.builder() + .setDestination(DestinationSip.valueOf("YOUR_sip_address")) + .setCli("YOUR_sinch_number") + .setTransport(TransportType.TLS) + .build()) + .setInstructions( + Collections.singletonList( + InstructionSay.builder() + .setText( + "Thanks for agreeing to speak to one of our sales reps! We'll now connect" + + " your call.") + .build())) + .build(); + } + + SVAMLControl nonSipResponse() { + + return SVAMLControl.builder() + .setAction( + ActionConnectPstn.builder() + .setNumber(E164PhoneNumber.valueOf("YOUR_phone_number")) + .setCli("YOUR_sinch_number") + .build()) + .setInstructions( + Collections.singletonList( + InstructionSay.builder() + .setText( + "Thanks for agreeing to speak to one of our sales reps! We'll now connect" + + " your call.") + .build())) + .build(); + } + + SVAMLControl defaultResponse() { + + return SVAMLControl.builder() + .setAction(ActionHangUp.builder().build()) + .setInstructions( + Collections.singletonList( + InstructionSay.builder() + .setText("Thank you for trying our tutorial! This call will now end.") + .build())) + .build(); + } + + SVAMLControl humanResponse() { + + var option1 = + MenuOption.builder() + .setDtfm(DualToneMultiFrequency.valueOf("1")) + .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "sip")) + .build(); + + var option2 = + MenuOption.builder() + .setDtfm(DualToneMultiFrequency.valueOf("2")) + .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "non-sip")) + .build(); + + Collection options = Collections.emptyList(); + + options.add(option1); + options.add(option2); + + return SVAMLControl.builder() + .setAction( + ActionRunMenu.builder() + .setBarge(false) + .setMenus( + Collections.singletonList( + Menu.builder() + .setId("main") + .setMainPrompt( + "Hi, you awesome person! Press 1 if you have performed this" + + " tutorial using a sip infrastructure. Press 2 if you have" + + " not used a sip infrastructure. Press any other digit to end" + + " this call.") + .setRepeatPrompt( + "Again, simply press 1 if you have used sip, press 2 if you have" + + " not, or press any other digit to end this call.") + .setRepeats(2) + .setOptions(options) + .build())) + .build()) + .build(); + } + + SVAMLControl machineResponse() { + + return SVAMLControl.builder() + .setAction(ActionHangUp.builder().build()) + .setInstructions( + Collections.singletonList( + InstructionSay.builder() + .setText( + "Hi there! We tried to reach you to speak with you about our awesome" + + " products. We will try again later. Bye!") + .build())) + .build(); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml index 1c2fae9..d0761db 100644 --- a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml +++ b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml @@ -8,5 +8,5 @@ server: port: 8090 credentials: - application-key: - application-secret: + application-api-key: + application-api-secret: From 6e3fd6a3a6b9486a150ac8eef85f18e6f70bed2f Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 08:13:40 +0200 Subject: [PATCH 07/20] feature (Voice/tutorial): Add authentication validation --- .../com/mycompany/app/voice/Controller.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java index 8f86677..3653490 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java @@ -1,26 +1,29 @@ package com.mycompany.app.voice; import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.domains.voice.WebHooksService; import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; import java.util.Map; import java.util.Objects; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; @RestController("Voice") public class Controller { - private final VoiceService voiceService; + private final WebHooksService webhooks; private final ServerBusinessLogic webhooksBusinessLogic; @Autowired public Controller(VoiceService voiceService, ServerBusinessLogic webhooksBusinessLogic) { - this.voiceService = voiceService; + this.webhooks = voiceService.webhooks(); this.webhooksBusinessLogic = webhooksBusinessLogic; } @@ -31,16 +34,30 @@ public Controller(VoiceService voiceService, ServerBusinessLogic webhooksBusines public ResponseEntity VoiceEvent( @RequestHeader Map headers, @RequestBody String body) { + // ensure valid authentication to handle request + var validAuth = + webhooks.validateAuthenticatedRequest( + // The HTTP verb this controller is managing + "POST", + // The URI this controller is managing + "/VoiceEvent", + // request headers + headers, + // request payload body + body); + + // token validation failed + if (!validAuth) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + // decode the request payload - var event = voiceService.webhooks().unserializeWebhooksEvent(body); + var event = webhooks.unserializeWebhooksEvent(body); // let business layer process the request if (Objects.requireNonNull(event) instanceof CallEvent e) { return ResponseEntity.ok() - .body( - voiceService - .webhooks() - .serializeWebhooksResponse(webhooksBusinessLogic.processCallEvent(e))); + .body(webhooks.serializeWebhooksResponse(webhooksBusinessLogic.processCallEvent(e))); } else { throw new IllegalStateException("Unexpected value: " + event); } From 332aa47e18af54a0432dec43c41725d77ad2737a Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 08:25:23 +0200 Subject: [PATCH 08/20] refactor (Voice/tutorial): Move business logic event detection onto controller layer --- .../com/mycompany/app/voice/Controller.java | 26 ++++++++++++----- .../app/voice/ServerBusinessLogic.java | 29 +++++-------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java index 3653490..87f228d 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java @@ -2,9 +2,11 @@ import com.sinch.sdk.domains.voice.VoiceService; import com.sinch.sdk.domains.voice.WebHooksService; -import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; +import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; +import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; import java.util.Map; -import java.util.Objects; +import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -54,12 +56,22 @@ public ResponseEntity VoiceEvent( // decode the request payload var event = webhooks.unserializeWebhooksEvent(body); + Optional response = Optional.empty(); + // let business layer process the request - if (Objects.requireNonNull(event) instanceof CallEvent e) { - return ResponseEntity.ok() - .body(webhooks.serializeWebhooksResponse(webhooksBusinessLogic.processCallEvent(e))); - } else { - throw new IllegalStateException("Unexpected value: " + event); + if (event instanceof AnsweredCallEvent e) { + response = Optional.of(webhooksBusinessLogic.answeredCallEvent(e)); + } + if (event instanceof PromptInputEvent e) { + response = Optional.of(webhooksBusinessLogic.promptInputEvent(e)); + } + + if (response.isEmpty()) { + return ResponseEntity.ok().body(""); } + + String serializedResponse = webhooks.serializeWebhooksResponse(response.get()); + + return ResponseEntity.ok().body(serializedResponse); } } diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index 0bfb80a..fe292f7 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -14,7 +14,6 @@ import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; import com.sinch.sdk.domains.voice.models.webhooks.AmdAnswerStatusType; import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; -import com.sinch.sdk.domains.voice.models.webhooks.CallEvent; import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; import com.sinch.sdk.models.DualToneMultiFrequency; import com.sinch.sdk.models.E164PhoneNumber; @@ -28,21 +27,7 @@ public class ServerBusinessLogic { private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); - public SVAMLControl processCallEvent(CallEvent event) { - - LOGGER.info("Received event:" + event); - - if (event instanceof AnsweredCallEvent) { - return parseAnsweredCallEvent(((AnsweredCallEvent) event)); - } - if (event instanceof PromptInputEvent) { - return parsePromptInputEvent(((PromptInputEvent) event)); - } else { - throw new IllegalStateException("Unexpected value: " + event); - } - } - - public SVAMLControl parseAnsweredCallEvent(AnsweredCallEvent event) { + public SVAMLControl answeredCallEvent(AnsweredCallEvent event) { var amdResult = event.getAmd(); @@ -56,7 +41,7 @@ public SVAMLControl parseAnsweredCallEvent(AnsweredCallEvent event) { } } - public SVAMLControl parsePromptInputEvent(PromptInputEvent event) { + public SVAMLControl promptInputEvent(PromptInputEvent event) { var menuResult = event.getMenuResult(); if (menuResult.getValue() == "sip") { @@ -69,7 +54,7 @@ public SVAMLControl parsePromptInputEvent(PromptInputEvent event) { } } - SVAMLControl sipResponse() { + private SVAMLControl sipResponse() { return SVAMLControl.builder() .setAction( @@ -88,7 +73,7 @@ SVAMLControl sipResponse() { .build(); } - SVAMLControl nonSipResponse() { + private SVAMLControl nonSipResponse() { return SVAMLControl.builder() .setAction( @@ -106,7 +91,7 @@ SVAMLControl nonSipResponse() { .build(); } - SVAMLControl defaultResponse() { + private SVAMLControl defaultResponse() { return SVAMLControl.builder() .setAction(ActionHangUp.builder().build()) @@ -118,7 +103,7 @@ SVAMLControl defaultResponse() { .build(); } - SVAMLControl humanResponse() { + private SVAMLControl humanResponse() { var option1 = MenuOption.builder() @@ -160,7 +145,7 @@ SVAMLControl humanResponse() { .build(); } - SVAMLControl machineResponse() { + private SVAMLControl machineResponse() { return SVAMLControl.builder() .setAction(ActionHangUp.builder().build()) From 7eaab7a582b5e2e9f3c67f37c999906e7a51de31 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 08:28:17 +0200 Subject: [PATCH 09/20] refactor (Voice/tutorial): Simplify controller event handler entry point: split into 2 functions --- .../com/mycompany/app/voice/Controller.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java index 87f228d..901c920 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/Controller.java @@ -36,22 +36,7 @@ public Controller(VoiceService voiceService, ServerBusinessLogic webhooksBusines public ResponseEntity VoiceEvent( @RequestHeader Map headers, @RequestBody String body) { - // ensure valid authentication to handle request - var validAuth = - webhooks.validateAuthenticatedRequest( - // The HTTP verb this controller is managing - "POST", - // The URI this controller is managing - "/VoiceEvent", - // request headers - headers, - // request payload body - body); - - // token validation failed - if (!validAuth) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); - } + validateRequest(headers, body); // decode the request payload var event = webhooks.unserializeWebhooksEvent(body); @@ -74,4 +59,23 @@ public ResponseEntity VoiceEvent( return ResponseEntity.ok().body(serializedResponse); } + + void validateRequest(Map headers, String body) { + + var validAuth = + webhooks.validateAuthenticatedRequest( + // The HTTP verb this controller is managing + "POST", + // The URI this controller is managing + "/VoiceEvent", + // request headers + headers, + // request payload body + body); + + // token validation failed + if (!validAuth) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + } } From f212617f2126561adc3c16df1f774399ae751368 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 08:53:30 +0200 Subject: [PATCH 10/20] refactor (Voice/tutorial): Best practice: use shared constants and variables --- .../app/voice/ServerBusinessLogic.java | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index fe292f7..08b47c0 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -6,6 +6,7 @@ import com.sinch.sdk.domains.voice.models.svaml.ActionConnectSip; import com.sinch.sdk.domains.voice.models.svaml.ActionHangUp; import com.sinch.sdk.domains.voice.models.svaml.ActionRunMenu; +import com.sinch.sdk.domains.voice.models.svaml.Instruction; import com.sinch.sdk.domains.voice.models.svaml.InstructionSay; import com.sinch.sdk.domains.voice.models.svaml.Menu; import com.sinch.sdk.domains.voice.models.svaml.MenuOption; @@ -17,8 +18,10 @@ import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; import com.sinch.sdk.models.DualToneMultiFrequency; import com.sinch.sdk.models.E164PhoneNumber; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.logging.Logger; import org.springframework.stereotype.Component; @@ -27,6 +30,22 @@ public class ServerBusinessLogic { private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); + private final String SIP_MENU = "sip"; + + private final String NON_SIP_MENU = "non-sip"; + + private final String SIP_ADDRESS = "YOUR_sip_address"; + private final String SINCH_NUMBER = "YOUR_sinch_number"; + private final String PHONE_NUMBER = "YOUR_phone_number"; + + private final List responseInstructions = + Collections.singletonList( + InstructionSay.builder() + .setText( + "Thanks for agreeing to speak to one of our sales reps! We'll now connect" + + " your call.") + .build()); + public SVAMLControl answeredCallEvent(AnsweredCallEvent event) { var amdResult = event.getAmd(); @@ -44,10 +63,10 @@ public SVAMLControl answeredCallEvent(AnsweredCallEvent event) { public SVAMLControl promptInputEvent(PromptInputEvent event) { var menuResult = event.getMenuResult(); - if (menuResult.getValue() == "sip") { + if (SIP_MENU.equals(menuResult.getValue())) { return sipResponse(); } - if (menuResult.getValue() == "non-sip") { + if (NON_SIP_MENU.equals(menuResult.getValue())) { return nonSipResponse(); } else { return defaultResponse(); @@ -59,17 +78,11 @@ private SVAMLControl sipResponse() { return SVAMLControl.builder() .setAction( ActionConnectSip.builder() - .setDestination(DestinationSip.valueOf("YOUR_sip_address")) - .setCli("YOUR_sinch_number") + .setDestination(DestinationSip.valueOf(SIP_ADDRESS)) + .setCli(SINCH_NUMBER) .setTransport(TransportType.TLS) .build()) - .setInstructions( - Collections.singletonList( - InstructionSay.builder() - .setText( - "Thanks for agreeing to speak to one of our sales reps! We'll now connect" - + " your call.") - .build())) + .setInstructions(responseInstructions) .build(); } @@ -78,49 +91,58 @@ private SVAMLControl nonSipResponse() { return SVAMLControl.builder() .setAction( ActionConnectPstn.builder() - .setNumber(E164PhoneNumber.valueOf("YOUR_phone_number")) - .setCli("YOUR_sinch_number") + .setNumber(E164PhoneNumber.valueOf(PHONE_NUMBER)) + .setCli(SINCH_NUMBER) .build()) - .setInstructions( - Collections.singletonList( - InstructionSay.builder() - .setText( - "Thanks for agreeing to speak to one of our sales reps! We'll now connect" - + " your call.") - .build())) + .setInstructions(responseInstructions) .build(); } private SVAMLControl defaultResponse() { + List defaultResponseInstructions = + Collections.singletonList( + InstructionSay.builder() + .setText("Thank you for trying our tutorial! This call will now end.") + .build()); + return SVAMLControl.builder() .setAction(ActionHangUp.builder().build()) - .setInstructions( - Collections.singletonList( - InstructionSay.builder() - .setText("Thank you for trying our tutorial! This call will now end.") - .build())) + .setInstructions(defaultResponseInstructions) .build(); } private SVAMLControl humanResponse() { - var option1 = + String SIP_MENU_OPTION = "1"; + String NON_SIP_MENU_OPTION = "2"; + + String mainPrompt = + String.format( + "Hi, you awesome person! Press '%s' if you have performed this tutorial using a sip" + + " infrastructure. Press '%s' if you have not used a sip infrastructure. Press any" + + " other digit to end this call.", + SIP_MENU_OPTION, NON_SIP_MENU_OPTION); + + String repeatPrompt = + String.format( + "Again, simply press '%s' if you have used sip, press '%s' if you have not, or press" + + " any other digit to end this call.", + SIP_MENU_OPTION, NON_SIP_MENU_OPTION); + + MenuOption option1 = MenuOption.builder() - .setDtfm(DualToneMultiFrequency.valueOf("1")) - .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "sip")) + .setDtfm(DualToneMultiFrequency.valueOf(SIP_MENU_OPTION)) + .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, SIP_MENU)) .build(); - var option2 = + MenuOption option2 = MenuOption.builder() - .setDtfm(DualToneMultiFrequency.valueOf("2")) - .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, "non-sip")) + .setDtfm(DualToneMultiFrequency.valueOf(NON_SIP_MENU_OPTION)) + .setAction(MenuOptionAction.from(MenuOptionActionType.RETURN, NON_SIP_MENU)) .build(); - Collection options = Collections.emptyList(); - - options.add(option1); - options.add(option2); + Collection options = Arrays.asList(option1, option2); return SVAMLControl.builder() .setAction( @@ -130,14 +152,8 @@ private SVAMLControl humanResponse() { Collections.singletonList( Menu.builder() .setId("main") - .setMainPrompt( - "Hi, you awesome person! Press 1 if you have performed this" - + " tutorial using a sip infrastructure. Press 2 if you have" - + " not used a sip infrastructure. Press any other digit to end" - + " this call.") - .setRepeatPrompt( - "Again, simply press 1 if you have used sip, press 2 if you have" - + " not, or press any other digit to end this call.") + .setMainPrompt(mainPrompt) + .setRepeatPrompt(repeatPrompt) .setRepeats(2) .setOptions(options) .build())) @@ -147,15 +163,17 @@ private SVAMLControl humanResponse() { private SVAMLControl machineResponse() { + List instructions = + Collections.singletonList( + InstructionSay.builder() + .setText( + "Hi there! We tried to reach you to speak with you about our awesome products." + + " We will try again later. Bye!") + .build()); + return SVAMLControl.builder() .setAction(ActionHangUp.builder().build()) - .setInstructions( - Collections.singletonList( - InstructionSay.builder() - .setText( - "Hi there! We tried to reach you to speak with you about our awesome" - + " products. We will try again later. Bye!") - .build())) + .setInstructions(instructions) .build(); } } From bea61950e333d52ffc0c114dfc98832c7a84afd5 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 10:20:56 +0200 Subject: [PATCH 11/20] refactor (Voice/tutorial): Best practice: reduce Cyclomatic complexity --- .../java/com/mycompany/app/voice/ServerBusinessLogic.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index 08b47c0..acc57d6 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -55,9 +55,8 @@ public SVAMLControl answeredCallEvent(AnsweredCallEvent event) { } if (amdResult.getStatus() == AmdAnswerStatusType.HUMAN) { return humanResponse(); - } else { - throw new IllegalStateException("Unexpected value: " + event); } + throw new IllegalStateException("Unexpected value: " + event); } public SVAMLControl promptInputEvent(PromptInputEvent event) { @@ -68,9 +67,8 @@ public SVAMLControl promptInputEvent(PromptInputEvent event) { } if (NON_SIP_MENU.equals(menuResult.getValue())) { return nonSipResponse(); - } else { - return defaultResponse(); } + return defaultResponse(); } private SVAMLControl sipResponse() { From 5eb8289c5b95d04155f31d150a005a38cbe5acf1 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 12:30:38 +0200 Subject: [PATCH 12/20] feature (Voice/tutorial): Use CLIHelper as 'a CLI for backend' --- .../com/mycompany/app/voice/CLIHelper.java | 95 +++++++++++++++++++ .../mycompany/app/voice/CalloutService.java | 71 -------------- 2 files changed, 95 insertions(+), 71 deletions(-) create mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java delete mode 100644 tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java new file mode 100644 index 0000000..c7b4cfd --- /dev/null +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java @@ -0,0 +1,95 @@ +package com.mycompany.app.voice; + +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.domains.voice.CalloutsService; +import com.sinch.sdk.domains.voice.VoiceService; +import com.sinch.sdk.domains.voice.models.requests.CalloutRequestParametersCustom; +import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; +import com.sinch.sdk.domains.voice.models.svaml.AnsweringMachineDetection; +import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; +import com.sinch.sdk.models.E164PhoneNumber; +import java.util.Scanner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +// wWe are basing this tutorial onto mixin a command line feature with server side backend +// This is not a proper CLI usage outside an educational purpose and this class should have been +// dedicated and dedicated CLI application +public class CLIHelper implements CommandLineRunner { + + private final CalloutsService calloutsService; + + private final String SINCH_NUMBER = "YOUR_sinch_number"; + + @Autowired + public CLIHelper(VoiceService voiceService) { + this.calloutsService = voiceService.callouts(); + } + + @Override + public void run(String... args) { + + while (true) { + E164PhoneNumber phoneNumber = promptPhoneNumber(); + + proceedCallout(phoneNumber); + } + } + + void proceedCallout(E164PhoneNumber phoneNumber) { + var response = + calloutsService.custom( + CalloutRequestParametersCustom.builder() + .setIce( + SVAMLControl.builder() + .setAction( + ActionConnectPstn.builder() + .setNumber(phoneNumber) + .setCli(SINCH_NUMBER) + .setAnsweringMachineDetection( + AnsweringMachineDetection.builder().setEnabled(true).build()) + .build()) + .build()) + .build()); + + echo("Callout response: '%s'", response); + } + + private E164PhoneNumber promptPhoneNumber() { + String input; + boolean valid; + do { + input = prompt("\nEnter the phone number you want to call"); + valid = E164PhoneNumber.validate(input); + if (!valid) { + echo("Invalid number '%s'", input); + } + } while (!valid); + + return E164PhoneNumber.valueOf(input); + } + + private String prompt(String prompt) { + + String input = null; + Scanner scanner = new Scanner(System.in); + + while (StringUtil.isEmpty(input)) { + System.out.println(prompt + " ([Q] to quit): "); + input = scanner.nextLine(); + } + + if ("Q".equalsIgnoreCase(input)) { + System.out.println("Quit application"); + System.exit(0); + } + + return input.replaceAll(" ", ""); + } + + private void echo(String text, Object... args) { + System.out.println(" " + String.format(text, args)); + } +} diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java deleted file mode 100644 index ce68587..0000000 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CalloutService.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.mycompany.app; - -import com.sinch.sdk.core.utils.StringUtil; -import com.sinch.sdk.domains.voice.VoiceService; -import com.sinch.sdk.domains.voice.models.requests.CalloutRequestParametersCustom; -import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; -import com.sinch.sdk.domains.voice.models.svaml.AnsweringMachineDetection; -import com.sinch.sdk.domains.voice.models.svaml.SVAMLControl; -import com.sinch.sdk.models.E164PhoneNumber; -import java.util.Scanner; -import java.util.logging.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class CalloutService { - - private static final Logger LOGGER = Logger.getLogger(CalloutService.class.getName()); - private final VoiceService voiceService; - - @Autowired - public CalloutService(VoiceService voiceService) { - this.voiceService = voiceService; - } - - public void makeCallout() { - var response = - voiceService - .callouts() - .custom( - CalloutRequestParametersCustom.builder() - .setIce( - SVAMLControl.builder() - .setAction( - ActionConnectPstn.builder() - .setNumber( - E164PhoneNumber.valueOf( - prompt("Enter the phone number you want to call: "))) - .setCli("YOUR_sinch_number") - .setAnsweringMachineDetection( - AnsweringMachineDetection.builder() - .setEnabled(true) - .build()) - .build()) - .build()) - .build()); - - echo("Callout response: '%s'", response); - } - - private String prompt(String prompt) { - - String input = null; - Scanner scanner = new Scanner(System.in); - - while (StringUtil.isEmpty(input)) { - System.out.println(prompt + " ([Q] to quit): "); - input = scanner.nextLine(); - } - - if ("Q".equalsIgnoreCase(input)) { - System.out.println("Quit application"); - System.exit(0); - } - return input.trim(); - } - - private void echo(String text, Object... args) { - System.out.println(" " + String.format(text, args)); - } -} From e0a4ccdf7e7c4c449ee2151aa6cc0dd3a9512508 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 13:29:04 +0200 Subject: [PATCH 13/20] refactor (Voice/tutorial): Extend settings coming from configuration file --- .../java/com/mycompany/app/voice/CLIHelper.java | 8 +++++--- .../mycompany/app/voice/ServerBusinessLogic.java | 15 ++++++++++----- .../src/main/resources/application.yaml | 2 ++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java index c7b4cfd..6180b43 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java @@ -10,6 +10,7 @@ import com.sinch.sdk.models.E164PhoneNumber; import java.util.Scanner; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @@ -19,9 +20,10 @@ // dedicated and dedicated CLI application public class CLIHelper implements CommandLineRunner { - private final CalloutsService calloutsService; + @Value("${sinch_number}") + String sinchNumber; - private final String SINCH_NUMBER = "YOUR_sinch_number"; + private final CalloutsService calloutsService; @Autowired public CLIHelper(VoiceService voiceService) { @@ -47,7 +49,7 @@ void proceedCallout(E164PhoneNumber phoneNumber) { .setAction( ActionConnectPstn.builder() .setNumber(phoneNumber) - .setCli(SINCH_NUMBER) + .setCli(sinchNumber) .setAnsweringMachineDetection( AnsweringMachineDetection.builder().setEnabled(true).build()) .build()) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index acc57d6..73498c1 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component("VoiceServerBusinessLogic") @@ -34,10 +35,14 @@ public class ServerBusinessLogic { private final String NON_SIP_MENU = "non-sip"; - private final String SIP_ADDRESS = "YOUR_sip_address"; - private final String SINCH_NUMBER = "YOUR_sinch_number"; private final String PHONE_NUMBER = "YOUR_phone_number"; + @Value("${sinch_number}") + String sinchNumber; + + @Value("${sip_address}") + String sipAddress; + private final List responseInstructions = Collections.singletonList( InstructionSay.builder() @@ -76,8 +81,8 @@ private SVAMLControl sipResponse() { return SVAMLControl.builder() .setAction( ActionConnectSip.builder() - .setDestination(DestinationSip.valueOf(SIP_ADDRESS)) - .setCli(SINCH_NUMBER) + .setDestination(DestinationSip.valueOf(sipAddress)) + .setCli(sinchNumber) .setTransport(TransportType.TLS) .build()) .setInstructions(responseInstructions) @@ -90,7 +95,7 @@ private SVAMLControl nonSipResponse() { .setAction( ActionConnectPstn.builder() .setNumber(E164PhoneNumber.valueOf(PHONE_NUMBER)) - .setCli(SINCH_NUMBER) + .setCli(sinchNumber) .build()) .setInstructions(responseInstructions) .build(); diff --git a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml index d0761db..c921005 100644 --- a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml +++ b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml @@ -10,3 +10,5 @@ server: credentials: application-api-key: application-api-secret: +sinch_number: YOUR_sinch_number +sip_address: YOUR_sip_address From 50c6c3248c0bc5621bb0de1626d1ea3046ce777d Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 13:32:53 +0200 Subject: [PATCH 14/20] build: Compile tutorial --- tutorials/compile.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorials/compile.sh b/tutorials/compile.sh index c1e6228..cdeab37 100755 --- a/tutorials/compile.sh +++ b/tutorials/compile.sh @@ -1,3 +1,4 @@ #!/bin/sh (cd sms/auto-subscribe-app && mvn clean package) +(cd voice/capture-leads-app && mvn clean package) From 5e7b69fb81b39503d6253f4c77191bd5dcd883d5 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 16:37:29 +0200 Subject: [PATCH 15/20] refactor (Voice/tutorial): README & cleanup --- tutorials/voice/capture-leads-app/README.md | 20 ++++++++++++++----- .../com/mycompany/app/voice/CLIHelper.java | 5 +---- .../app/voice/ServerBusinessLogic.java | 4 ++-- .../src/main/resources/application.yaml | 4 ++-- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/tutorials/voice/capture-leads-app/README.md b/tutorials/voice/capture-leads-app/README.md index 66d4f2c..c066ecb 100644 --- a/tutorials/voice/capture-leads-app/README.md +++ b/tutorials/voice/capture-leads-app/README.md @@ -1,6 +1,11 @@ -# qualify leads application sample +# Qualify leads application sample -This directory contains sample related to Java SDK tutorials: [qualify leads](https://developers.sinch.com/docs/sms/tutorials/sms/tutorials/java-sdk/auto-subscribe) +This directory contains sample related to Java SDK tutorials: [qualify leads](https://developers.sinch.com/docs/voice/tutorials/capture-leads) + +## DISCLAIMER +This tutorial is based on mixing a command-line function with a server-side backend service. + +It is not a correct use of the CLI outside of an educational purpose. ## Requirements @@ -17,8 +22,13 @@ Application settings are using the SpringBoot configuration file: [`application. #### Sinch credentials Located in `credentials` section (*you can find all of the credentials you need on your [Sinch dashboard](https://dashboard.sinch.com)*): -- `application-key`: YOUR_application_key -- `application-secret`: YOUR_application_secret +- `application-api-key`: YOUR_application_key +- `application-api-secret`: YOUR_application_secret + +#### Required values +Tutorial features are using some values used by Voice product: +- `sinch-number`: +- `sip-address`: #### Server port Located in `server` section: @@ -35,7 +45,7 @@ mvn spring-boot:run Forwarding request to same `8090` port used above: -*Note: The `8090` value is coming from default config and can be changed (see [Server port](#Server port) configuration section)* +*Note: The `8090` value is coming from default config and can be changed (see [Server port](#Server-port) configuration section)* ```bash ngrok http 8090 diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java index 6180b43..cab8bbd 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/CLIHelper.java @@ -15,12 +15,9 @@ import org.springframework.stereotype.Component; @Component -// wWe are basing this tutorial onto mixin a command line feature with server side backend -// This is not a proper CLI usage outside an educational purpose and this class should have been -// dedicated and dedicated CLI application public class CLIHelper implements CommandLineRunner { - @Value("${sinch_number}") + @Value("${sinch-number}") String sinchNumber; private final CalloutsService calloutsService; diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index 73498c1..860222d 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -37,10 +37,10 @@ public class ServerBusinessLogic { private final String PHONE_NUMBER = "YOUR_phone_number"; - @Value("${sinch_number}") + @Value("${sinch-number}") String sinchNumber; - @Value("${sip_address}") + @Value("${sip-address}") String sipAddress; private final List responseInstructions = diff --git a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml index c921005..f7cc8b8 100644 --- a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml +++ b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml @@ -10,5 +10,5 @@ server: credentials: application-api-key: application-api-secret: -sinch_number: YOUR_sinch_number -sip_address: YOUR_sip_address +sinch-number: YOUR_sinch_number +sip-address: YOUR_sip_address From 2aff3dbcc740cdcd4b231c9652a88a5b182252ce Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Wed, 24 Jul 2024 17:45:01 +0200 Subject: [PATCH 16/20] fix: Adding missing TTS tags to prompts --- .../java/com/mycompany/app/voice/ServerBusinessLogic.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index 860222d..cb195c1 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -122,15 +122,15 @@ private SVAMLControl humanResponse() { String mainPrompt = String.format( - "Hi, you awesome person! Press '%s' if you have performed this tutorial using a sip" + "#tts[Hi, you awesome person! Press '%s' if you have performed this tutorial using a sip" + " infrastructure. Press '%s' if you have not used a sip infrastructure. Press any" - + " other digit to end this call.", + + " other digit to end this call.]", SIP_MENU_OPTION, NON_SIP_MENU_OPTION); String repeatPrompt = String.format( - "Again, simply press '%s' if you have used sip, press '%s' if you have not, or press" - + " any other digit to end this call.", + "#tts[Again, simply press '%s' if you have used sip, press '%s' if you have not, or press" + + " any other digit to end this call.]", SIP_MENU_OPTION, NON_SIP_MENU_OPTION); MenuOption option1 = From 022910992c98417393013a397502ac7d809368f7 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Thu, 25 Jul 2024 08:43:28 +0200 Subject: [PATCH 17/20] feature (Voice/tutorial): Fix non-sip response --- .../app/voice/ServerBusinessLogic.java | 20 +++++++++++-------- .../src/main/resources/application.yaml | 6 ++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index cb195c1..7e1b1b0 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -35,8 +35,6 @@ public class ServerBusinessLogic { private final String NON_SIP_MENU = "non-sip"; - private final String PHONE_NUMBER = "YOUR_phone_number"; - @Value("${sinch-number}") String sinchNumber; @@ -50,6 +48,8 @@ public class ServerBusinessLogic { "Thanks for agreeing to speak to one of our sales reps! We'll now connect" + " your call.") .build()); + @Value("${sales-phone-number}") + String salesPhoneNumber; public SVAMLControl answeredCallEvent(AnsweredCallEvent event) { @@ -91,13 +91,17 @@ private SVAMLControl sipResponse() { private SVAMLControl nonSipResponse() { + String instruction = + "Thank you for choosing to speak to one of our sales reps! If this were in production, at" + + " this point you would be connected to a sales rep on your sip network. Since you do" + + " not, you have now completed this tutorial. We hope you had fun and learned" + + " something new. Be sure to keep visiting https://developers.sinch.com for more great" + + " tutorials."; + return SVAMLControl.builder() - .setAction( - ActionConnectPstn.builder() - .setNumber(E164PhoneNumber.valueOf(PHONE_NUMBER)) - .setCli(sinchNumber) - .build()) - .setInstructions(responseInstructions) + .setAction(ActionHangUp.builder().build()) + .setInstructions( + Collections.singletonList(InstructionSay.builder().setText(instruction).build())) .build(); } diff --git a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml index f7cc8b8..54512e2 100644 --- a/tutorials/voice/capture-leads-app/src/main/resources/application.yaml +++ b/tutorials/voice/capture-leads-app/src/main/resources/application.yaml @@ -10,5 +10,7 @@ server: credentials: application-api-key: application-api-secret: -sinch-number: YOUR_sinch_number -sip-address: YOUR_sip_address + +# see README.md, "Required values" section for details about these settings +sinch-number: +sip-address: From fe57612c92dc761cdb765ffe1c63b33ab5d29f26 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Thu, 25 Jul 2024 08:43:39 +0200 Subject: [PATCH 18/20] refactor (Voice/tutorial): Best practice highlight constant text used for instrcution vs inline Instruction.builder usage --- .../app/voice/ServerBusinessLogic.java | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java index 7e1b1b0..1771e14 100644 --- a/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java +++ b/tutorials/voice/capture-leads-app/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -2,11 +2,9 @@ import com.sinch.sdk.domains.voice.models.DestinationSip; import com.sinch.sdk.domains.voice.models.TransportType; -import com.sinch.sdk.domains.voice.models.svaml.ActionConnectPstn; import com.sinch.sdk.domains.voice.models.svaml.ActionConnectSip; import com.sinch.sdk.domains.voice.models.svaml.ActionHangUp; import com.sinch.sdk.domains.voice.models.svaml.ActionRunMenu; -import com.sinch.sdk.domains.voice.models.svaml.Instruction; import com.sinch.sdk.domains.voice.models.svaml.InstructionSay; import com.sinch.sdk.domains.voice.models.svaml.Menu; import com.sinch.sdk.domains.voice.models.svaml.MenuOption; @@ -17,20 +15,15 @@ import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; import com.sinch.sdk.models.DualToneMultiFrequency; -import com.sinch.sdk.models.E164PhoneNumber; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component("VoiceServerBusinessLogic") public class ServerBusinessLogic { - private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); - private final String SIP_MENU = "sip"; private final String NON_SIP_MENU = "non-sip"; @@ -41,16 +34,6 @@ public class ServerBusinessLogic { @Value("${sip-address}") String sipAddress; - private final List responseInstructions = - Collections.singletonList( - InstructionSay.builder() - .setText( - "Thanks for agreeing to speak to one of our sales reps! We'll now connect" - + " your call.") - .build()); - @Value("${sales-phone-number}") - String salesPhoneNumber; - public SVAMLControl answeredCallEvent(AnsweredCallEvent event) { var amdResult = event.getAmd(); @@ -78,6 +61,9 @@ public SVAMLControl promptInputEvent(PromptInputEvent event) { private SVAMLControl sipResponse() { + String instruction = + "Thanks for agreeing to speak to one of our sales reps! We'll now connect your call."; + return SVAMLControl.builder() .setAction( ActionConnectSip.builder() @@ -85,7 +71,8 @@ private SVAMLControl sipResponse() { .setCli(sinchNumber) .setTransport(TransportType.TLS) .build()) - .setInstructions(responseInstructions) + .setInstructions( + Collections.singletonList(InstructionSay.builder().setText(instruction).build())) .build(); } @@ -107,15 +94,12 @@ private SVAMLControl nonSipResponse() { private SVAMLControl defaultResponse() { - List defaultResponseInstructions = - Collections.singletonList( - InstructionSay.builder() - .setText("Thank you for trying our tutorial! This call will now end.") - .build()); + String instruction = "Thank you for trying our tutorial! This call will now end."; return SVAMLControl.builder() .setAction(ActionHangUp.builder().build()) - .setInstructions(defaultResponseInstructions) + .setInstructions( + Collections.singletonList(InstructionSay.builder().setText(instruction).build())) .build(); } @@ -126,15 +110,15 @@ private SVAMLControl humanResponse() { String mainPrompt = String.format( - "#tts[Hi, you awesome person! Press '%s' if you have performed this tutorial using a sip" - + " infrastructure. Press '%s' if you have not used a sip infrastructure. Press any" - + " other digit to end this call.]", + "#tts[Hi, you awesome person! Press '%s' if you have performed this tutorial using a" + + " sip infrastructure. Press '%s' if you have not used a sip infrastructure. Press" + + " any other digit to end this call.]", SIP_MENU_OPTION, NON_SIP_MENU_OPTION); String repeatPrompt = String.format( - "#tts[Again, simply press '%s' if you have used sip, press '%s' if you have not, or press" - + " any other digit to end this call.]", + "#tts[Again, simply press '%s' if you have used sip, press '%s' if you have not, or" + + " press any other digit to end this call.]", SIP_MENU_OPTION, NON_SIP_MENU_OPTION); MenuOption option1 = @@ -170,17 +154,14 @@ private SVAMLControl humanResponse() { private SVAMLControl machineResponse() { - List instructions = - Collections.singletonList( - InstructionSay.builder() - .setText( - "Hi there! We tried to reach you to speak with you about our awesome products." - + " We will try again later. Bye!") - .build()); + String instruction = + "Hi there! We tried to reach you to speak with you about our awesome products. We will try" + + " again later. Bye!"; return SVAMLControl.builder() .setAction(ActionHangUp.builder().build()) - .setInstructions(instructions) + .setInstructions( + Collections.singletonList(InstructionSay.builder().setText(instruction).build())) .build(); } } From 6e62db90b6ca1292d664c23a73cc8d0df40fa703 Mon Sep 17 00:00:00 2001 From: Alex Sberna Date: Thu, 25 Jul 2024 17:52:24 -0400 Subject: [PATCH 19/20] updating link in the readme --- tutorials/voice/capture-leads-app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/voice/capture-leads-app/README.md b/tutorials/voice/capture-leads-app/README.md index c066ecb..321281d 100644 --- a/tutorials/voice/capture-leads-app/README.md +++ b/tutorials/voice/capture-leads-app/README.md @@ -1,6 +1,6 @@ # Qualify leads application sample -This directory contains sample related to Java SDK tutorials: [qualify leads](https://developers.sinch.com/docs/voice/tutorials/capture-leads) +This directory contains samples related to Java SDK tutorials: [qualify leads](https://developers.sinch.com/docs/voice/tutorials/qualify-leads/java) ## DISCLAIMER This tutorial is based on mixing a command-line function with a server-side backend service. From 95bfc7e2f0a102efd3e92f908281a3e6dd81de3a Mon Sep 17 00:00:00 2001 From: Alex Sberna Date: Fri, 26 Jul 2024 14:13:16 -0400 Subject: [PATCH 20/20] updates to readme --- tutorials/voice/capture-leads-app/README.md | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tutorials/voice/capture-leads-app/README.md b/tutorials/voice/capture-leads-app/README.md index 321281d..fbac3b6 100644 --- a/tutorials/voice/capture-leads-app/README.md +++ b/tutorials/voice/capture-leads-app/README.md @@ -3,6 +3,7 @@ This directory contains samples related to Java SDK tutorials: [qualify leads](https://developers.sinch.com/docs/voice/tutorials/qualify-leads/java) ## DISCLAIMER + This tutorial is based on mixing a command-line function with a server-side backend service. It is not a correct use of the CLI outside of an educational purpose. @@ -20,23 +21,30 @@ It is not a correct use of the CLI outside of an educational purpose. Application settings are using the SpringBoot configuration file: [`application.yaml`](src/main/resources/application.yaml) file and enable to configure: -#### Sinch credentials +#### Required Sinch credentials + Located in `credentials` section (*you can find all of the credentials you need on your [Sinch dashboard](https://dashboard.sinch.com)*): + - `application-api-key`: YOUR_application_key - `application-api-secret`: YOUR_application_secret -#### Required values -Tutorial features are using some values used by Voice product: -- `sinch-number`: -- `sip-address`: +#### Other required values + +This tutorial uses other values that you should also assign: + +- `sinch-number`: This is the Sinch number assigned to your [Voice app](https://dashboard.sinch.com/voice/apps). +- `sip-address`: If you are performing this tutorial with a SIP infrastructure, this is where you would enter your SIP address. #### Server port + Located in `server` section: + - port: The port to be used to listen to incoming requests. Default: 8090 ### Starting server locally Compile and run the application as server locally. + ```bash mvn spring-boot:run ``` @@ -52,17 +60,21 @@ ngrok http 8090 ``` ngrok output will contains output like: -``` + +```shell ngrok (Ctrl+C to quit) ... Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http://localhost:8090 ``` + The line -``` + +```shell Forwarding https://0e64-78-117-86-140.ngrok-free.app -> http://localhost:8090 ``` + Contains `https://0e64-78-117-86-140.ngrok-free.app` value. This value must be used to configure the callback URL on your [Sinch dashboard](https://dashboard.sinch.com/voice/apps) \ No newline at end of file