Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 examples/compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export SDK_VERSION="${SDK_VERSION}"
(cd webhooks && ./compile.sh) || exit 1
(cd client && mvn -Puse-version clean package) || exit 1
(cd getting-started && ./compile.sh) || exit 1
(cd tutorials && ./compile.sh) || exit 1
10 changes: 10 additions & 0 deletions examples/tutorials/compile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh

DIRECTORIES="
voice/capture-leads-app
"

for DIRECTORY in $DIRECTORIES
do
(cd "$DIRECTORY" && echo "$PWD" && mvn -Puse-version clean package) || exit 1
done
80 changes: 80 additions & 0 deletions examples/tutorials/voice/capture-leads-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Sinch Java SDK Voice Tutorial: Qualify 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.

It is not a correct use of the CLI outside 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:

#### Required Sinch credentials

Located in `credentials` section (*you can find all the credentials you need on your [Sinch dashboard](https://dashboard.sinch.com)*):

- `application-api-key`: YOUR_application_key
- `application-api-secret`: YOUR_application_secret

#### 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. <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:

```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)
61 changes: 61 additions & 0 deletions examples/tutorials/voice/capture-leads-app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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>4.0.0</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>

<profiles>
<profile>
<id>use-version</id>
<properties>
<sinch.sdk.java.version>${env.SDK_VERSION}</sinch.sdk.java.version>
</properties>
</profile>
</profiles>

<properties>
<sinch.sdk.java.version>[2.0.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,96 @@
package com.mycompany.app.voice;

import com.sinch.sdk.core.utils.StringUtil;
import com.sinch.sdk.domains.voice.VoiceService;
import com.sinch.sdk.domains.voice.api.v1.CalloutsService;
import com.sinch.sdk.domains.voice.models.v1.callouts.request.CalloutRequestCustom;
import com.sinch.sdk.domains.voice.models.v1.svaml.SvamlControl;
import com.sinch.sdk.domains.voice.models.v1.svaml.action.AnsweringMachineDetectionQuery;
import com.sinch.sdk.domains.voice.models.v1.svaml.action.SvamlActionConnectPstn;
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.v1().callouts();
}

@Override
public void run(String... args) {

while (true) {
String phoneNumber = promptPhoneNumber();

proceedCallout(phoneNumber);
}
}

void proceedCallout(String phoneNumber) {
var response =
calloutsService.custom(
CalloutRequestCustom.builder()
.setIce(
SvamlControl.builder()
.setAction(
SvamlActionConnectPstn.builder()
.setNumber(phoneNumber)
.setCli(sinchNumber)
.setAmd(
AnsweringMachineDetectionQuery.builder()
.setEnabled(true)
.build())
.build())
.build())
.build());

echo("Callout response: '%s'", response);
}

private String 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 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,93 @@
package com.mycompany.app.voice;

import com.sinch.sdk.domains.voice.VoiceService;
import com.sinch.sdk.domains.voice.api.v1.WebHooksService;
import com.sinch.sdk.domains.voice.models.v1.svaml.SvamlControl;
import com.sinch.sdk.domains.voice.models.v1.webhooks.AnsweredCallEvent;
import com.sinch.sdk.domains.voice.models.v1.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.v1().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.parseEvent(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(null);
}

ResponseEntity<String> responseEntity = ResponseEntity.ok().body(null);

String serializedResponse = webhooks.serializeResponse(response.get());

return ResponseEntity.ok().body(serializedResponse);
}

void validateRequest(Map<String, String> headers, String body) {

// ensure valid authentication to handle request
// set this value to true to validate request from Sinch servers
// see
// https://developers.sinch.com/docs/voice/api-reference/authentication/callback-signed-request
// for more information
boolean ensureValidAuthentication = false;
if (ensureValidAuthentication) {
return;
}

var validAuth =
webhooks.validateAuthenticationHeader(
// 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