Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voice lead capture app tutorial #12

Merged
merged 20 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
54845df
initial commit of tutorial demo
alex-sberna Jul 19, 2024
8c0eef6
adding missing text
alex-sberna Jul 19, 2024
158d049
connecting calloutService to controller
alex-sberna Jul 19, 2024
216ce04
updating readme
alex-sberna Jul 19, 2024
704e89e
remove '.vscode' directory from repository
JPPortier Jul 24, 2024
f884d27
refactor (Voice/tutorial): Move to 'template like' directory/files st…
JPPortier Jul 24, 2024
6e3fd6a
feature (Voice/tutorial): Add authentication validation
JPPortier Jul 24, 2024
332aa47
refactor (Voice/tutorial): Move business logic event detection onto c…
JPPortier Jul 24, 2024
7eaab7a
refactor (Voice/tutorial): Simplify controller event handler entry po…
JPPortier Jul 24, 2024
f212617
refactor (Voice/tutorial): Best practice: use shared constants and va…
JPPortier Jul 24, 2024
bea6195
refactor (Voice/tutorial): Best practice: reduce Cyclomatic complexity
JPPortier Jul 24, 2024
5eb8289
feature (Voice/tutorial): Use CLIHelper as 'a CLI for backend'
JPPortier Jul 24, 2024
e0a4ccd
refactor (Voice/tutorial): Extend settings coming from configuration …
JPPortier Jul 24, 2024
50c6c32
build: Compile tutorial
JPPortier Jul 24, 2024
5e7b69f
refactor (Voice/tutorial): README & cleanup
JPPortier Jul 24, 2024
2aff3db
fix: Adding missing TTS tags to prompts
JPPortier Jul 24, 2024
0229109
feature (Voice/tutorial): Fix non-sip response
JPPortier Jul 25, 2024
fe57612
refactor (Voice/tutorial): Best practice highlight constant text used…
JPPortier Jul 25, 2024
6e62db9
updating link in the readme
alex-sberna Jul 25, 2024
95bfc7e
updates to readme
alex-sberna Jul 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
.vscode
**/target
1 change: 1 addition & 0 deletions tutorials/compile.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/bin/sh

(cd sms/auto-subscribe-app && mvn clean package)
(cd voice/capture-leads-app && mvn clean package)
68 changes: 68 additions & 0 deletions tutorials/voice/capture-leads-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Qualify leads application sample

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

- 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-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:
- port: The port to be used to listen to incoming requests. <em>Default: 8090</em>

### 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 the callback URL on your [Sinch dashboard](https://dashboard.sinch.com/voice/apps)
52 changes: 52 additions & 0 deletions tutorials/voice/capture-leads-app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>my.company.com</groupId>
<artifactId>sinch-sdk-java-tuturial-auto-subscribe</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Sinch Java SDK Capture Leads Sample Application</name>
<description>Demo Project for Capturing Leads</description>

<properties>
<sinch.sdk.java.version>[1.2.0,)</sinch.sdk.java.version>
<java.version>21</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.sinch.sdk</groupId>
<artifactId>sinch-sdk-java</artifactId>
<version>${sinch.sdk.java.version}</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mycompany.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -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-api-key}")
String applicationKey;

@Value("${credentials.application-api-secret}")
String applicationSecret;

@Bean
public VoiceService voiceService() {

var configuration =
Configuration.builder()
.setApplicationKey(applicationKey)
.setApplicationSecret(applicationSecret)
.build();

return new SinchClient(configuration).voice();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class CLIHelper implements CommandLineRunner {

@Value("${sinch-number}")
String sinchNumber;

private final CalloutsService calloutsService;

@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(sinchNumber)
.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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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.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.Optional;
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 WebHooksService webhooks;
private final ServerBusinessLogic webhooksBusinessLogic;

@Autowired
public Controller(VoiceService voiceService, ServerBusinessLogic webhooksBusinessLogic) {
this.webhooks = voiceService.webhooks();
this.webhooksBusinessLogic = webhooksBusinessLogic;
}

@PostMapping(
value = "/VoiceEvent",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> VoiceEvent(
@RequestHeader Map<String, String> headers, @RequestBody String body) {

validateRequest(headers, body);

// decode the request payload
var event = webhooks.unserializeWebhooksEvent(body);

Optional<SVAMLControl> response = Optional.empty();

// let business layer process the request
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);
}

void validateRequest(Map<String, String> 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);
}
}
}
Loading
Loading