Skip to content

Commit

Permalink
Merge pull request #12 from sinch/voice-lead-capture-app-tutorial-pro…
Browse files Browse the repository at this point in the history
…posal

Voice lead capture app tutorial
  • Loading branch information
JPPortier authored Jul 27, 2024
2 parents 2603143 + 95bfc7e commit 5b6a018
Show file tree
Hide file tree
Showing 10 changed files with 533 additions and 0 deletions.
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)
80 changes: 80 additions & 0 deletions tutorials/voice/capture-leads-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Qualify leads application sample

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.

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

#### 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)
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

0 comments on commit 5b6a018

Please sign in to comment.