Skip to content

Commit

Permalink
feat: Batches / WebHooks
Browse files Browse the repository at this point in the history
  • Loading branch information
JPPortier committed Nov 7, 2023
1 parent b02440e commit d03426a
Show file tree
Hide file tree
Showing 15 changed files with 457 additions and 0 deletions.
8 changes: 8 additions & 0 deletions client/src/main/com/sinch/sdk/domains/sms/SMSService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ public interface SMSService {
* @since 1.0
*/
BatchesService batches();

/**
* WebHooksService Service instance
*
* @return service instance for project
* @since 1.0
*/
WebHooksService webHooks();
}
58 changes: 58 additions & 0 deletions client/src/main/com/sinch/sdk/domains/sms/WebHooksService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.sinch.sdk.domains.sms;

import com.sinch.sdk.core.exceptions.ApiException;
import com.sinch.sdk.domains.sms.models.webhooks.BaseIncomingSMS;

/**
* SMS WebHooks
*
* <p><b>Callbacks</b>
*
* <p>A callback is an HTTP POST request with a notification made by the Sinch SMS REST API to a URI
* of your choosing.
*
* <p>The REST API expects the receiving server to respond with a response code within the 2xx
* success range. For 5xx the callback will be retried. For 429 the callback will be retried and the
* throughput will be lowered. For other status codes in the 4xx range the callback will not be
* retried. The first initial retry will happen 5 seconds after the first try. The next attempt is
* after 10 seconds, then after 20 seconds, after 40 seconds, after 80 seconds, doubling on every
* attempt. The last retry will be at 81920 seconds (or 22 hours 45 minutes) after the initial
* failed attempt.
*
* <p>The SMS REST API offers the following callback options which can be configured for your
* account upon request to your account manager.
*
* <ul>
* <li>Callback with mutual authentication over TLS (HTTPS) connection by provisioning the
* callback URL with client keystore and password.
* <li>Callback with basic authentication by provisioning the callback URL with username and
* password.
* <li>Callback with OAuth 2.0 by provisioning the callback URL with username, password and the
* URL to fetch OAuth access token.
* <li>Callback using AWS SNS by provisioning the callback URL with an Access Key ID, Secret Key
* and Region.
* </ul>
*
* @see <a
* href="https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/">https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/</a>
* @since 1.0
*/
public interface WebHooksService {

/**
* Incoming SMSWebhook
*
* <p>An inbound message is a message sent to one of your short codes or long numbers from a
* mobile phone. To receive inbound message callbacks, a URL needs to be added to your REST API.
* This URL can be specified in your @link <a
* href="https://dashboard.sinch.com/sms/api">https://dashboard.sinch.com/sms/api</a>.
*
* @param jsonPayload The incoming message to your sinch number
* @param <T> A type of Batch
* @return Decoded payload
* @see <a
* href="https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/operation/incomingSMS">https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/operation/incomingSMS</a>
* @since 1.0
*/
<T extends BaseIncomingSMS<?>> T incomingSMS(String jsonPayload) throws ApiException;
}
10 changes: 10 additions & 0 deletions client/src/main/com/sinch/sdk/domains/sms/adapters/SMSService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import com.sinch.sdk.core.http.HttpClient;
import com.sinch.sdk.domains.sms.BatchesService;
import com.sinch.sdk.domains.sms.WebHooksService;
import com.sinch.sdk.models.Configuration;

public class SMSService implements com.sinch.sdk.domains.sms.SMSService {

private final Configuration configuration;
private final HttpClient httpClient;
private BatchesService batches;
private WebHooksService webHooks;

public SMSService(Configuration configuration, HttpClient httpClient) {
this.configuration = configuration;
Expand All @@ -23,4 +25,12 @@ public BatchesService batches() {
}
return this.batches;
}

@Override
public WebHooksService webHooks() {
if (null == this.webHooks) {
this.webHooks = new com.sinch.sdk.domains.sms.adapters.WebHooksService();
}
return this.webHooks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.sinch.sdk.domains.sms.adapters;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.sinch.sdk.core.exceptions.ApiMappingException;
import com.sinch.sdk.core.utils.StringUtil;
import com.sinch.sdk.core.utils.databind.Mapper;
import com.sinch.sdk.domains.sms.models.webhooks.BaseIncomingSMS;

public class WebHooksService implements com.sinch.sdk.domains.sms.WebHooksService {

@Override
public <T extends BaseIncomingSMS<?>> T incomingSMS(String jsonPayload)
throws ApiMappingException {
try {
@SuppressWarnings("unchecked")
T generic = (T) Mapper.getInstance().readValue(jsonPayload, BaseIncomingSMS.class);
if (null == generic && !StringUtil.isEmpty(jsonPayload)) {
throw new ApiMappingException(jsonPayload, null);
}
return generic;
} catch (JsonProcessingException e) {
throw new ApiMappingException(jsonPayload, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.sinch.sdk.domains.sms.models.webhooks;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.Optional;

/**
* Base class for Incoming SMSWebhook
*
* @param <T> Type of SMS body
* @since 1.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = IncomingSMSBinary.class, name = "mo_binary"),
@JsonSubTypes.Type(value = IncomingSMSText.class, name = "mo_text")
})
public abstract class BaseIncomingSMS<T> {

static final String JSON_PROPERTY_BODY = "body";
private final T body;
static final String JSON_PROPERTY_FROM = "from";
private final String from;
static final String JSON_PROPERTY_ID = "id";
private final String id;
static final String JSON_PROPERTY_RECEIVED_AT = "received_at";
private final Instant receivedAt;
static final String JSON_PROPERTY_TO = "to";
private final String to;
static final String JSON_PROPERTY_CLIENT_REFERENCE = "client_reference";
private final String clientReference;
static final String JSON_PROPERTY_OPERATOR_ID = "operator_id";
private final String operatorId;
static final String JSON_PROPERTY_SENT_AT = "sent_at";
private final Instant sentAt;

@JsonCreator
public BaseIncomingSMS(
@JsonProperty(JSON_PROPERTY_BODY) T body,
@JsonProperty(JSON_PROPERTY_FROM) String from,
@JsonProperty(JSON_PROPERTY_ID) String id,
@JsonProperty(JSON_PROPERTY_RECEIVED_AT) OffsetDateTime receivedAt,
@JsonProperty(JSON_PROPERTY_TO) String to,
@JsonProperty(JSON_PROPERTY_CLIENT_REFERENCE) String clientReference,
@JsonProperty(JSON_PROPERTY_OPERATOR_ID) String operatorId,
@JsonProperty(JSON_PROPERTY_SENT_AT) OffsetDateTime sentAt) {
this.body = body;
this.from = from;
this.id = id;
this.receivedAt = receivedAt.toInstant();
this.to = to;
this.clientReference = clientReference;
this.operatorId = operatorId;
this.sentAt = null != sentAt ? sentAt.toInstant() : null;
}

public T getBody() {
return body;
}

public String getFrom() {
return from;
}

public String getId() {
return id;
}

public Instant getReceivedAt() {
return receivedAt;
}

public String getTo() {
return to;
}

public Optional<String> getClientReference() {
return Optional.ofNullable(clientReference);
}

public Optional<String> getOperatorId() {
return Optional.ofNullable(operatorId);
}

public Optional<Instant> getSentAt() {
return Optional.ofNullable(sentAt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.sinch.sdk.domains.sms.models.webhooks;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;

public class IncomingSMSBinary extends BaseIncomingSMS<String> {

static final String JSON_PROPERTY_UDH = "udh";
private final String udh;

/**
* Binary MO class
*
* @param body The message content Base64 encoded. Max 140 bytes together with udh.
* @param from The phone number that sent the message. @see <a
* href="https://community.sinch.com/t5/Glossary/MSISDN/ta-p/7628">https://community.sinch.com/t5/Glossary/MSISDN/ta-p/7628</a>
* @param id The ID of this inbound message.
* @param receivedAt When the system received the message.
* @param to The Sinch phone number or short code to which the message was sent.
* @param clientReference If this inbound message is in response to a previously sent message that
* contained a client reference, then this field contains that client reference. Utilizing
* this feature requires additional setup on your account. Contact your account manager to
* enable this feature.
* @param operatorId The MCC/MNC of the sender's operator if known.
* @param sendAt When the message left the originating device. Only available if provided by
* operator.
* @param udh The UDH header of a binary message HEX encoded. Max 140 bytes together with body.
*/
@JsonCreator
public IncomingSMSBinary(
@JsonProperty(JSON_PROPERTY_BODY) String body,
@JsonProperty(JSON_PROPERTY_FROM) String from,
@JsonProperty(JSON_PROPERTY_ID) String id,
@JsonProperty(JSON_PROPERTY_RECEIVED_AT) OffsetDateTime receivedAt,
@JsonProperty(JSON_PROPERTY_TO) String to,
@JsonProperty(JSON_PROPERTY_CLIENT_REFERENCE) String clientReference,
@JsonProperty(JSON_PROPERTY_OPERATOR_ID) String operatorId,
@JsonProperty(JSON_PROPERTY_SENT_AT) OffsetDateTime sendAt,
@JsonProperty(JSON_PROPERTY_UDH) String udh) {
super(body, from, id, receivedAt, to, clientReference, operatorId, sendAt);
this.udh = udh;
}

public String getUdh() {
return udh;
}

@Override
public String toString() {
return "IncomingSMSBinary{" + "udh='" + udh + '\'' + "} " + super.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.sinch.sdk.domains.sms.models.webhooks;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;

public class IncomingSMSText extends BaseIncomingSMS<String> {

/**
* Text MO class
*
* @param body The message content Base64 encoded. Max 140 bytes together with udh.
* @param from The phone number that sent the message. @see <a
* href="https://community.sinch.com/t5/Glossary/MSISDN/ta-p/7628">https://community.sinch.com/t5/Glossary/MSISDN/ta-p/7628</a>
* @param id The ID of this inbound message.
* @param receivedAt When the system received the message.
* @param to The Sinch phone number or short code to which the message was sent.
* @param clientReference If this inbound message is in response to a previously sent message that
* contained a client reference, then this field contains that client reference. Utilizing
* this feature requires additional setup on your account. Contact your account manager to
* enable this feature.
* @param operatorId The MCC/MNC of the sender's operator if known.
* @param sendAt When the message left the originating device. Only available if provided by
* operator.
*/
@JsonCreator
public IncomingSMSText(
@JsonProperty(JSON_PROPERTY_BODY) String body,
@JsonProperty(JSON_PROPERTY_FROM) String from,
@JsonProperty(JSON_PROPERTY_ID) String id,
@JsonProperty(JSON_PROPERTY_RECEIVED_AT) OffsetDateTime receivedAt,
@JsonProperty(JSON_PROPERTY_TO) String to,
@JsonProperty(JSON_PROPERTY_CLIENT_REFERENCE) String clientReference,
@JsonProperty(JSON_PROPERTY_OPERATOR_ID) String operatorId,
@JsonProperty(JSON_PROPERTY_SENT_AT) OffsetDateTime sendAt) {
super(body, from, id, receivedAt, to, clientReference, operatorId, sendAt);
}

@Override
public String toString() {
return "IncomingSMSText{} " + super.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* SMS WebHooks models
*
* @since 1.0
*/
package com.sinch.sdk.domains.sms.models.webhooks;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.sinch.sdk.domains.sms.adapters;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.adelean.inject.resources.junit.jupiter.GivenTextResource;
import com.adelean.inject.resources.junit.jupiter.TestWithResources;
import com.sinch.sdk.BaseTest;
import com.sinch.sdk.core.exceptions.ApiException;
import com.sinch.sdk.core.exceptions.ApiMappingException;
import com.sinch.sdk.domains.sms.models.dto.v1.webhooks.IncomingSMSBinaryDtoTest;
import com.sinch.sdk.domains.sms.models.dto.v1.webhooks.IncomingSMSTextDtoTest;
import com.sinch.sdk.domains.sms.models.webhooks.BaseIncomingSMS;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;

@TestWithResources
public class WebHKooksServiceTest extends BaseTest {

@GivenTextResource("/domains/sms/v1/webhooks/IncomingSMSBinary.json")
String incomingSMSBinaryJsonString;

@GivenTextResource("/domains/sms/v1/webhooks/IncomingSMSText.json")
String incomingSMSTextJsonString;

@InjectMocks WebHooksService service;

@Test
void incomingSMSBinary() throws ApiException {

BaseIncomingSMS<?> response = service.incomingSMS(incomingSMSBinaryJsonString);

Assertions.assertThat(response)
.usingRecursiveComparison()
.isEqualTo(IncomingSMSBinaryDtoTest.incomingSMSBinary);
}

@Test
void incomingSMSText() throws ApiException {

BaseIncomingSMS<?> response = service.incomingSMS(incomingSMSTextJsonString);

Assertions.assertThat(response)
.usingRecursiveComparison()
.isEqualTo(IncomingSMSTextDtoTest.incomingSMSText);
}

@Test
void handleException() throws ApiException {

String jsonPayload = incomingSMSBinaryJsonString.replace("mo_binary", "foo type");
ApiMappingException thrown =
assertThrows(ApiMappingException.class, () -> service.incomingSMS(jsonPayload));
assertTrue(thrown.getMessage().contains(jsonPayload));
}
}
4 changes: 4 additions & 0 deletions core/src/main/com/sinch/sdk/core/exceptions/ApiException.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public ApiException(String message) {
super(message);
}

public ApiException(String message, Throwable throwable) {
super(message, throwable);
}

public ApiException(String message, Throwable throwable, int code) {
super(message, throwable);
this.code = code;
Expand Down
Loading

0 comments on commit d03426a

Please sign in to comment.