Skip to content

Commit

Permalink
Add auto subscribe app tutorial (#76)
Browse files Browse the repository at this point in the history
* docs (SMS): Add tutorial for SMS auto-subscribe-app
* ci: Adding examples compilation to CI/CD
  • Loading branch information
JPPortier authored Apr 23, 2024
1 parent 188ebc5 commit 5ff4651
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/samples-compilation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
cd sample-app
mvn -B clean package
mvn -B -f pom-webhooks.xml clean package
cd ../examples
./compile.sh
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
3 changes: 3 additions & 0 deletions examples/compile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

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

This directory contains sample related to Java SDK tutorials: [](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 is using the SpringBoot configuration file: [`application.yaml`](src/main/resources/application.yaml) file and set:

#### Sinch credentials
Located in `credentials` section (*you can find all of the credentials you need on your [Sinch dashboard](https://dashboard.sinch.com)*):
- `project-id`: YOUR_project_id
- `key-id`: YOUR_access_key_id
- `key-secret`: YOUR_access_key_secret

#### Server port
Located in `server` section:
- port: The port to be used to listen incoming request. <em>Default: 8090</em>

### Starting server locally

Compile and run the application as server onto you localhost.
```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 is having to be used to configure your callback from [Sinch dashboard](https://dashboard.sinch.com/sms/api/services)
52 changes: 52 additions & 0 deletions examples/tutorials/sms/auto-subscribe-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>com.sinch.sdk</groupId>
<artifactId>sinch-sdk-java-sample-webhooks-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Sinch Java SDK auto-subscribe Sample Application</name>
<description>Demo Project for auto-subscribe</description>

<properties>
<sinch.sdk.java.version>[1.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 App {

public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.mycompany.app;

import com.sinch.sdk.domains.sms.SMSService;
import com.sinch.sdk.domains.sms.models.InboundText;
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.RestController;

@RestController
public class AutoSubscribeController {

private final SMSService smsService;
private final AutoSubscribeService service;

@Autowired
public AutoSubscribeController(SMSService smsService, AutoSubscribeService service) {
this.smsService = smsService;
this.service = service;
}

@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
public void smsDeliveryEvent(@RequestBody String body) {

// decode the request payload
var event = smsService.webHooks().parse(body);

// let business layer process the request
if (Objects.requireNonNull(event) instanceof InboundText e) {
service.processInboundEvent(e);
} else {
throw new IllegalStateException("Unexpected value: " + event);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.mycompany.app;

import com.sinch.sdk.domains.sms.SMSService;
import com.sinch.sdk.domains.sms.models.Group;
import com.sinch.sdk.domains.sms.models.InboundText;
import com.sinch.sdk.domains.sms.models.requests.GroupUpdateRequestParameters;
import com.sinch.sdk.domains.sms.models.requests.SendSmsBatchTextRequest;
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 AutoSubscribeService {

private static final Logger LOGGER = Logger.getLogger(AutoSubscribeService.class.getName());

private final SMSService smsService;
private final Group group;

@Autowired
public AutoSubscribeService(SMSService smsService) {
this.smsService = smsService;
this.group = new GroupManager(smsService).getGroup();
}

public void processInboundEvent(InboundText event) {

LOGGER.info("Received event:" + event);

var from = event.getFrom();
var to = event.getTo();
var body = event.getBody().trim();

var membersList = getMembersList(group);
var isMemberInGroup = isMemberInGroup(membersList, from);

String response;

if (body.equals("SUBSCRIBE")) {
response = subscribe(group, isMemberInGroup, to, from);
} else if (body.equals("STOP")) {
response = unsubscribe(group, isMemberInGroup, to, from);
} else {
response =
"Thanks for your interest. If you want to subscribe to this group, text \"SUBSCRIBE\" to +%s"
.formatted(to);
}

sendResponse(to, from, response);
}

private Collection<String> getMembersList(Group group) {
return smsService.groups().listMembers(group.getId());
}

private boolean isMemberInGroup(Collection<String> membersList, String member) {
return membersList.contains(member);
}

private String subscribe(
Group group, boolean isMemberInGroup, String groupPhoneNumber, String member) {

if (isMemberInGroup) {
return "You already subscribed to '%s'. Text \"STOP\" to +%s to leave this group."
.formatted(group.getName(), groupPhoneNumber);
}

var request =
GroupUpdateRequestParameters.builder().setAdd(Collections.singletonList(member)).build();

smsService.groups().update(group.getId(), request);
return "Congratulations! You are now subscribed to '%s'. Text \"STOP\" to +%s to leave this group."
.formatted(group.getName(), groupPhoneNumber);
}

private String unsubscribe(
Group group, boolean isMemberInGroup, String groupPhoneNumber, String member) {

if (!isMemberInGroup) {
return "You did not subscribed to '%s'. Text \"SUBSCRIBE\" to +%s to join this group."
.formatted(group.getName(), groupPhoneNumber);
}

var request =
GroupUpdateRequestParameters.builder().setRemove(Collections.singletonList(member)).build();

smsService.groups().update(group.getId(), request);
return "We're sorry to see you go. You can always rejoin '%s' by texting \"SUBSCRIBE\" to +%s."
.formatted(group.getName(), groupPhoneNumber);
}

private void sendResponse(String from, String to, String response) {

var request =
SendSmsBatchTextRequest.builder()
.setTo(Collections.singletonList(to))
.setBody(response)
.setFrom(from)
.build();

smsService.batches().send(request);

LOGGER.info("Replied: '%s'".formatted(response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.mycompany.app;

import com.sinch.sdk.SinchClient;
import com.sinch.sdk.domains.sms.SMSService;
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.project-id}")
String projectId;

@Value("${credentials.key-id}")
String keyId;

@Value("${credentials.key-secret}")
String keySecret;

@Bean
public SMSService smsService() {

var configuration =
Configuration.builder()
.setProjectId(projectId)
.setKeyId(keyId)
.setKeySecret(keySecret)
.build();

return new SinchClient(configuration).sms();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mycompany.app;

import com.sinch.sdk.domains.sms.GroupsService;
import com.sinch.sdk.domains.sms.SMSService;
import com.sinch.sdk.domains.sms.models.Group;
import com.sinch.sdk.domains.sms.models.requests.GroupCreateRequestParameters;
import java.util.Optional;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class GroupManager {

private static final Logger LOGGER = Logger.getLogger(GroupManager.class.getName());

private final String GROUP_NAME = "Sinch Pirates";

private final SMSService smsService;

@Autowired
public GroupManager(SMSService smsService) {
this.smsService = smsService;
}

public Group getGroup() {

GroupsService service = smsService.groups();

// ensure we do not create a new group if already existing with same name
Optional<Group> group = retrieveGroup(service);
return group.orElseGet(() -> createGroup(service));
}

/*
* Retrieve group ID if group is existing
*/
private Optional<Group> retrieveGroup(GroupsService service) {

Optional<Group> found =
service.list().stream().filter(group -> group.getName().equals(GROUP_NAME)).findFirst();

found.ifPresent(
group ->
LOGGER.info("Group '%s' find with id '%s'".formatted(group.getName(), group.getId())));
return found;
}

/*`
* Create a new group
*/
private Group createGroup(GroupsService service) {

var request = GroupCreateRequestParameters.builder().setName(GROUP_NAME).build();

var group = service.create(request);

LOGGER.info("Group '%s' created with id '%s'".formatted(group.getName(), group.getId()));
return group;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# springboot related config file

logging:
level:
com: INFO

server:
port: 8090

credentials:
project-id:
key-id:
key-secret:

Loading

0 comments on commit 5ff4651

Please sign in to comment.