diff --git a/compile.sh b/compile.sh index 5b6a7d4..54d16b3 100755 --- a/compile.sh +++ b/compile.sh @@ -3,6 +3,7 @@ mvn -f pom-ci.xml clean spotless:apply (cd templates/client && mvn clean package) || exit 1 +(cd templates/server && mvn clean package) || exit 1 (cd tutorials && ./compile.sh) || exit 1 (cd getting-started && ./compile.sh) || exit 1 diff --git a/templates/server/README.md b/templates/server/README.md new file mode 100644 index 0000000..9f6c0dc --- /dev/null +++ b/templates/server/README.md @@ -0,0 +1,78 @@ +# Backend application built using Sinch Java SDK to handle incoming webhooks + +This directory contains a server application based onto [Sinch SDK java](https://github.com/sinch/sinch-sdk-java) + +## Requirements + +- JDK 8 or later (Sinch SDK Java is requiring java 8 only but client application can use latest available version) +- [Maven](https://maven.apache.org/) +- [SpringBoot](https://spring.io/projects/spring-boot) +- [Sinch account](https://dashboard.sinch.com) +- [ngrok](https://ngrok.com/docs) + +## Configuration + +### Configure application settings + +com.mycompany.app.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)*): +- `project-id`: YOUR_project_id +- `key-id`: YOUR_access_key_id +- `key-secret`: YOUR_access_key_secret + +#### Server port +Default: 8090 + +Located in `server` section: +- port: The port to be used to listen to incoming requests. Default: 8090 + +## Usage + +### Start server +1. Edit configuration file + + See above for Configuration paragraph + +2. Start server locally. + + Compile and run the application as server locally. + ```bash + mvn spring-boot:run + ``` +### EndPoints +When server is online, declared controllers will respond to following endpoints + +| Service | Endpoint | +|--------------|--------------------| +| Numbers | /NumbersEvent | +| SMS | /SmsEvent | +| Verification | /VerificationEvent | +| Voice | /VoiceEvent | + +## 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/templates/server/pom.xml b/templates/server/pom.xml new file mode 100644 index 0000000..034dda8 --- /dev/null +++ b/templates/server/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + my.company.com + sinch-java-sdk-server-application + 0.0.1-SNAPSHOT + Sinch Java SDK Server Application + + + [1.0.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/templates/server/src/main/java/com/mycompany/app/Application.java b/templates/server/src/main/java/com/mycompany/app/Application.java new file mode 100644 index 0000000..03b399f --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/Application.java @@ -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); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/Config.java b/templates/server/src/main/java/com/mycompany/app/Config.java new file mode 100644 index 0000000..425c5a8 --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/Config.java @@ -0,0 +1,53 @@ +package com.mycompany.app; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.core.utils.StringUtil; +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; + + @Value("${credentials.application-api-key}") + String applicationKey; + + @Value("${credentials.application-api-secret}") + String applicationSecret; + + @Bean + public SinchClient sinchClient() { + + Configuration.Builder builder = Configuration.builder(); + + if (!StringUtil.isEmpty(projectId)) { + builder.setProjectId(projectId); + } + + if (!StringUtil.isEmpty(keyId)) { + builder.setKeyId(keyId); + } + if (!StringUtil.isEmpty(keySecret)) { + builder.setKeySecret(keySecret); + } + + if (!StringUtil.isEmpty(applicationKey)) { + builder.setApplicationKey(applicationKey); + } + + if (!StringUtil.isEmpty(applicationSecret)) { + builder.setApplicationSecret(applicationSecret); + } + + return new SinchClient(builder.build()); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/numbers/Controller.java b/templates/server/src/main/java/com/mycompany/app/numbers/Controller.java new file mode 100644 index 0000000..e312503 --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/numbers/Controller.java @@ -0,0 +1,41 @@ +package com.mycompany.app.numbers; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.numbers.api.v1.WebHooksService; +import com.sinch.sdk.domains.numbers.models.v1.webhooks.NumberEvent; +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.RestController; + +@RestController("Numbers") +public class Controller { + + private final SinchClient sinchClient; + private final ServerBusinessLogic webhooksBusinessLogic; + + @Autowired + public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) { + this.sinchClient = sinchClient; + this.webhooksBusinessLogic = webhooksBusinessLogic; + } + + @PostMapping( + value = "/NumbersEvent", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity NumbersEvent(@RequestBody String body) { + + WebHooksService webhooks = sinchClient.numbers().v1().webhooks(); + + // decode the request payload + NumberEvent event = webhooks.parseEvent(body); + + // let business layer process the request + webhooksBusinessLogic.numbersEvent(event); + + return ResponseEntity.ok().build(); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/numbers/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/numbers/ServerBusinessLogic.java new file mode 100644 index 0000000..773b933 --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/numbers/ServerBusinessLogic.java @@ -0,0 +1,16 @@ +package com.mycompany.app.numbers; + +import com.sinch.sdk.domains.numbers.models.v1.webhooks.NumberEvent; +import java.util.logging.Logger; +import org.springframework.stereotype.Component; + +@Component("NumbersServerBusinessLogic") +public class ServerBusinessLogic { + + private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); + + public void numbersEvent(NumberEvent event) { + + LOGGER.info("Handle event: " + event); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/sms/Controller.java b/templates/server/src/main/java/com/mycompany/app/sms/Controller.java new file mode 100644 index 0000000..890f1ca --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/sms/Controller.java @@ -0,0 +1,51 @@ +package com.mycompany.app.sms; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.sms.WebHooksService; +import com.sinch.sdk.domains.sms.models.DeliveryReportBatch; +import com.sinch.sdk.domains.sms.models.DeliveryReportRecipient; +import com.sinch.sdk.domains.sms.models.InboundBinary; +import com.sinch.sdk.domains.sms.models.InboundText; +import com.sinch.sdk.domains.sms.models.webhooks.WebhooksEvent; +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.RestController; + +@RestController("SMS") +public class Controller { + + private final SinchClient sinchClient; + private final ServerBusinessLogic webhooksBusinessLogic; + + @Autowired + public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) { + this.sinchClient = sinchClient; + this.webhooksBusinessLogic = webhooksBusinessLogic; + } + + @PostMapping( + value = "/SmsEvent", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity smsDeliveryEvent(@RequestBody String body) { + + WebHooksService webhooks = sinchClient.sms().webHooks(); + + // decode the request payload + WebhooksEvent event = webhooks.parse(body); + + // let business layer process the request + switch (event) { + case InboundBinary e -> webhooksBusinessLogic.processInboundEvent(e); + case InboundText e -> webhooksBusinessLogic.processInboundEvent(e); + case DeliveryReportRecipient e -> webhooksBusinessLogic.processDeliveryReportEvent(e); + case DeliveryReportBatch e -> webhooksBusinessLogic.processDeliveryReportEvent(e); + default -> throw new IllegalStateException("Unexpected value: " + event); + } + + return ResponseEntity.ok().build(); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/sms/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/sms/ServerBusinessLogic.java new file mode 100644 index 0000000..561012e --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/sms/ServerBusinessLogic.java @@ -0,0 +1,35 @@ +package com.mycompany.app.sms; + +import com.sinch.sdk.domains.sms.models.DeliveryReportBatch; +import com.sinch.sdk.domains.sms.models.DeliveryReportRecipient; +import com.sinch.sdk.domains.sms.models.InboundBinary; +import com.sinch.sdk.domains.sms.models.InboundText; +import com.sinch.sdk.domains.sms.models.webhooks.WebhooksEvent; +import java.util.logging.Logger; +import org.springframework.stereotype.Component; + +@Component("SMSServerBusinessLogic") +public class ServerBusinessLogic { + + private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); + + public void processInboundEvent(InboundText event) { + trace(event); + } + + public void processInboundEvent(InboundBinary event) { + trace(event); + } + + public void processDeliveryReportEvent(DeliveryReportRecipient event) { + trace(event); + } + + public void processDeliveryReportEvent(DeliveryReportBatch event) { + trace(event); + } + + private void trace(WebhooksEvent event) { + LOGGER.info("Handle event: " + event); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/verification/Controller.java b/templates/server/src/main/java/com/mycompany/app/verification/Controller.java new file mode 100644 index 0000000..ccaa633 --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/verification/Controller.java @@ -0,0 +1,78 @@ +package com.mycompany.app.verification; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.verification.api.v1.WebHooksService; +import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationRequestEvent; +import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationResultEvent; +import java.util.Map; +import java.util.logging.Logger; +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("Verification") +public class Controller { + + private static final Logger LOGGER = Logger.getLogger(Controller.class.getName()); + private final SinchClient sinchClient; + private final ServerBusinessLogic webhooksBusinessLogic; + + @Autowired + public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) { + this.sinchClient = sinchClient; + this.webhooksBusinessLogic = webhooksBusinessLogic; + } + + @PostMapping( + value = "/VerificationEvent", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity VerificationEvent( + @RequestHeader Map headers, @RequestBody String body) { + + WebHooksService webhooks = sinchClient.verification().v1().webhooks(); + + // ensure valid authentication to handle request + var validAuth = + webhooks.validateAuthenticationHeader( + // The HTTP verb this controller is managing + "POST", + // The URI this controller is managing + "/VerificationEvent", + // request headers + headers, + // request payload body + body); + + // token validation failed + if (!validAuth) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + // decode the request payload + var event = webhooks.parseEvent(body); + + // let business layer process the request + var response = + switch (event) { + case VerificationRequestEvent e -> webhooksBusinessLogic.verificationEvent(e); + case VerificationResultEvent e -> { + webhooksBusinessLogic.verificationEvent(e); + yield null; + } + default -> throw new IllegalStateException("Unexpected value: " + event); + }; + + var serializedResponse = webhooks.serializeResponse(response); + + LOGGER.finest("JSON response: " + serializedResponse); + + return ResponseEntity.ok().body(serializedResponse); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/verification/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/verification/ServerBusinessLogic.java new file mode 100644 index 0000000..6485d3e --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/verification/ServerBusinessLogic.java @@ -0,0 +1,26 @@ +package com.mycompany.app.verification; + +import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationRequestEvent; +import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationRequestEventResponse; +import com.sinch.sdk.domains.verification.models.v1.webhooks.VerificationResultEvent; +import java.util.logging.Logger; +import org.springframework.stereotype.Component; + +@Component("VerificationServerBusinessLogic") +public class ServerBusinessLogic { + + private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); + + public VerificationRequestEventResponse verificationEvent(VerificationRequestEvent event) { + + LOGGER.info("Handle event :" + event); + + // add your logic here according to SMS, FlashCall, PhoneCall, ... verification + return null; + } + + public void verificationEvent(VerificationResultEvent event) { + + LOGGER.info("Handle event: " + event); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/voice/Controller.java b/templates/server/src/main/java/com/mycompany/app/voice/Controller.java new file mode 100644 index 0000000..8150f29 --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/voice/Controller.java @@ -0,0 +1,93 @@ +package com.mycompany.app.voice; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.WebHooksService; +import com.sinch.sdk.domains.voice.models.webhooks.AnsweredCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.DisconnectCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.IncomingCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.NotifyEvent; +import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; +import java.util.Map; +import java.util.logging.Logger; +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 SinchClient sinchClient; + private final ServerBusinessLogic webhooksBusinessLogic; + private static final Logger LOGGER = Logger.getLogger(Controller.class.getName()); + + @Autowired + public Controller(SinchClient sinchClient, ServerBusinessLogic webhooksBusinessLogic) { + this.sinchClient = sinchClient; + 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) { + + WebHooksService webhooks = sinchClient.voice().webhooks(); + + // 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 payload request + var event = webhooks.unserializeWebhooksEvent(body); + + // let business layer process the request + var response = + switch (event) { + case IncomingCallEvent e -> webhooksBusinessLogic.incoming(e); + case AnsweredCallEvent e -> webhooksBusinessLogic.answered(e); + case DisconnectCallEvent e -> { + webhooksBusinessLogic.disconnect(e); + yield null; + } + case PromptInputEvent e -> { + webhooksBusinessLogic.prompt(e); + yield null; + } + case NotifyEvent e -> { + webhooksBusinessLogic.notify(e); + yield null; + } + default -> throw new IllegalStateException("Unexpected value: " + event); + }; + + String serializedResponse = ""; + if (null != response) { + serializedResponse = webhooks.serializeWebhooksResponse(response); + } + + LOGGER.finest("JSON response: " + serializedResponse); + + return ResponseEntity.ok().body(serializedResponse); + } +} diff --git a/templates/server/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java b/templates/server/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java new file mode 100644 index 0000000..0e34f80 --- /dev/null +++ b/templates/server/src/main/java/com/mycompany/app/voice/ServerBusinessLogic.java @@ -0,0 +1,45 @@ +package com.mycompany.app.voice; + +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.DisconnectCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.IncomingCallEvent; +import com.sinch.sdk.domains.voice.models.webhooks.NotifyEvent; +import com.sinch.sdk.domains.voice.models.webhooks.PromptInputEvent; +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 incoming(IncomingCallEvent event) { + + LOGGER.info("Handle event :" + event); + + return SVAMLControl.builder().build(); + } + + public SVAMLControl answered(AnsweredCallEvent event) { + + LOGGER.info("Handle event: " + event); + + return SVAMLControl.builder().build(); + } + + public void disconnect(DisconnectCallEvent event) { + + LOGGER.info("Handle event: " + event); + } + + public void prompt(PromptInputEvent event) { + + LOGGER.info("Handle event: " + event); + } + + public void notify(NotifyEvent event) { + + LOGGER.info("Handle event: " + event); + } +} diff --git a/templates/server/src/main/resources/application.yaml b/templates/server/src/main/resources/application.yaml new file mode 100644 index 0000000..1251ccf --- /dev/null +++ b/templates/server/src/main/resources/application.yaml @@ -0,0 +1,22 @@ +# springboot related config file + +logging: + level: + com: INFO + +server: + port: 8090 + +credentials: + # Unified related credentials, used by: + # - Numbers + # - SMS + project-id: + key-id: + key-secret: + + # Application related credentials, used by: + # - Verification + # - Voice + application-api-key: + application-api-secret: