diff --git a/client/src/main/com/sinch/sdk/domains/sms/SMSService.java b/client/src/main/com/sinch/sdk/domains/sms/SMSService.java index 2947577e..daa31986 100644 --- a/client/src/main/com/sinch/sdk/domains/sms/SMSService.java +++ b/client/src/main/com/sinch/sdk/domains/sms/SMSService.java @@ -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(); } diff --git a/client/src/main/com/sinch/sdk/domains/sms/WebHooksService.java b/client/src/main/com/sinch/sdk/domains/sms/WebHooksService.java new file mode 100644 index 00000000..e412a587 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/sms/WebHooksService.java @@ -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 + * + *

Callbacks + * + *

A callback is an HTTP POST request with a notification made by the Sinch SMS REST API to a URI + * of your choosing. + * + *

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. + * + *

The SMS REST API offers the following callback options which can be configured for your + * account upon request to your account manager. + * + *

+ * + * @see https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/ + * @since 1.0 + */ +public interface WebHooksService { + + /** + * Incoming SMSWebhook + * + *

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 https://dashboard.sinch.com/sms/api. + * + * @param jsonPayload The incoming message to your sinch number + * @param A type of Batch + * @return Decoded payload + * @see https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/operation/incomingSMS + * @since 1.0 + */ + > T incomingSMS(String jsonPayload) throws ApiException; +} diff --git a/client/src/main/com/sinch/sdk/domains/sms/adapters/SMSService.java b/client/src/main/com/sinch/sdk/domains/sms/adapters/SMSService.java index fd2f3a26..bbc23470 100644 --- a/client/src/main/com/sinch/sdk/domains/sms/adapters/SMSService.java +++ b/client/src/main/com/sinch/sdk/domains/sms/adapters/SMSService.java @@ -2,6 +2,7 @@ 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 { @@ -9,6 +10,7 @@ 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; @@ -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; + } } diff --git a/client/src/main/com/sinch/sdk/domains/sms/adapters/WebHooksService.java b/client/src/main/com/sinch/sdk/domains/sms/adapters/WebHooksService.java new file mode 100644 index 00000000..bac5184d --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/sms/adapters/WebHooksService.java @@ -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 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); + } + } +} diff --git a/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/BaseIncomingSMS.java b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/BaseIncomingSMS.java new file mode 100644 index 00000000..7fde29c9 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/BaseIncomingSMS.java @@ -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 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 { + + 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 getClientReference() { + return Optional.ofNullable(clientReference); + } + + public Optional getOperatorId() { + return Optional.ofNullable(operatorId); + } + + public Optional getSentAt() { + return Optional.ofNullable(sentAt); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/IncomingSMSBinary.java b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/IncomingSMSBinary.java new file mode 100644 index 00000000..80127924 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/IncomingSMSBinary.java @@ -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 { + + 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 https://community.sinch.com/t5/Glossary/MSISDN/ta-p/7628 + * @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(); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/IncomingSMSText.java b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/IncomingSMSText.java new file mode 100644 index 00000000..46c5eb08 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/IncomingSMSText.java @@ -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 { + + /** + * 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 https://community.sinch.com/t5/Glossary/MSISDN/ta-p/7628 + * @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(); + } +} diff --git a/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/package-info.java b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/package-info.java new file mode 100644 index 00000000..daf15120 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/sms/models/webhooks/package-info.java @@ -0,0 +1,6 @@ +/** + * SMS WebHooks models + * + * @since 1.0 + */ +package com.sinch.sdk.domains.sms.models.webhooks; diff --git a/client/src/test/java/com/sinch/sdk/domains/sms/adapters/WebHKooksServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/sms/adapters/WebHKooksServiceTest.java new file mode 100644 index 00000000..cfac690a --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/sms/adapters/WebHKooksServiceTest.java @@ -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)); + } +} diff --git a/core/src/main/com/sinch/sdk/core/exceptions/ApiException.java b/core/src/main/com/sinch/sdk/core/exceptions/ApiException.java index d7b31989..883fb3fa 100644 --- a/core/src/main/com/sinch/sdk/core/exceptions/ApiException.java +++ b/core/src/main/com/sinch/sdk/core/exceptions/ApiException.java @@ -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; diff --git a/core/src/main/com/sinch/sdk/core/exceptions/ApiMappingException.java b/core/src/main/com/sinch/sdk/core/exceptions/ApiMappingException.java new file mode 100644 index 00000000..426aedcd --- /dev/null +++ b/core/src/main/com/sinch/sdk/core/exceptions/ApiMappingException.java @@ -0,0 +1,8 @@ +package com.sinch.sdk.core.exceptions; + +public class ApiMappingException extends ApiException { + + public ApiMappingException(String payload, Throwable throwable) { + super(String.format("Unable to map string '%s'", payload), throwable); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/sms/models/dto/v1/webhooks/IncomingSMSBinaryDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/sms/models/dto/v1/webhooks/IncomingSMSBinaryDtoTest.java new file mode 100644 index 00000000..c4427793 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/sms/models/dto/v1/webhooks/IncomingSMSBinaryDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.sms.models.dto.v1.webhooks; + +import com.adelean.inject.resources.junit.jupiter.GivenJsonResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.sms.models.webhooks.IncomingSMSBinary; +import java.time.OffsetDateTime; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class IncomingSMSBinaryDtoTest extends BaseTest { + + @GivenJsonResource("/domains/sms/v1/webhooks/IncomingSMSBinary.json") + IncomingSMSBinary client; + + public static IncomingSMSBinary incomingSMSBinary = + new IncomingSMSBinary( + "VGV4dCBtZXNzYWdl", + "16051234567", + "01XXXXX21XXXXX119Z8P1XXXXX", + OffsetDateTime.parse("2022-08-24T14:15:22Z"), + "13185551234", + null, + "operator", + OffsetDateTime.parse("2022-08-24T14:15:44Z"), + "10010203040506070809000a0b0c0d0e0f"); + + @Test + void deserialize() { + + Assertions.assertThat(incomingSMSBinary).usingRecursiveComparison().isEqualTo(client); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/sms/models/dto/v1/webhooks/IncomingSMSTextDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/sms/models/dto/v1/webhooks/IncomingSMSTextDtoTest.java new file mode 100644 index 00000000..c049199b --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/sms/models/dto/v1/webhooks/IncomingSMSTextDtoTest.java @@ -0,0 +1,33 @@ +package com.sinch.sdk.domains.sms.models.dto.v1.webhooks; + +import com.adelean.inject.resources.junit.jupiter.GivenJsonResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.sms.models.webhooks.IncomingSMSText; +import java.time.OffsetDateTime; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class IncomingSMSTextDtoTest extends BaseTest { + + @GivenJsonResource("/domains/sms/v1/webhooks/IncomingSMSText.json") + IncomingSMSText client; + + public static IncomingSMSText incomingSMSText = + new IncomingSMSText( + "This is a test message.", + "16051234567", + "01XXXXX21XXXXX119Z8P1XXXXX", + OffsetDateTime.parse("2022-08-24T14:15:22Z"), + "13185551234", + null, + "string", + null); + + @Test + void deserialize() { + + Assertions.assertThat(incomingSMSText).usingRecursiveComparison().isEqualTo(client); + } +} diff --git a/test-resources/src/test/resources/domains/sms/v1/webhooks/IncomingSMSBinary.json b/test-resources/src/test/resources/domains/sms/v1/webhooks/IncomingSMSBinary.json new file mode 100644 index 00000000..2c698b57 --- /dev/null +++ b/test-resources/src/test/resources/domains/sms/v1/webhooks/IncomingSMSBinary.json @@ -0,0 +1,11 @@ +{ + "body": "VGV4dCBtZXNzYWdl", + "from": "16051234567", + "id": "01XXXXX21XXXXX119Z8P1XXXXX", + "operator_id": "operator", + "received_at": "2022-08-24T14:15:22Z", + "sent_at": "2022-08-24T14:15:44Z", + "to": "13185551234", + "type": "mo_binary", + "udh": "10010203040506070809000a0b0c0d0e0f" +} \ No newline at end of file diff --git a/test-resources/src/test/resources/domains/sms/v1/webhooks/IncomingSMSText.json b/test-resources/src/test/resources/domains/sms/v1/webhooks/IncomingSMSText.json new file mode 100644 index 00000000..a97b50f0 --- /dev/null +++ b/test-resources/src/test/resources/domains/sms/v1/webhooks/IncomingSMSText.json @@ -0,0 +1,9 @@ +{ + "body": "This is a test message.", + "from": "16051234567", + "id": "01XXXXX21XXXXX119Z8P1XXXXX", + "operator_id": "string", + "received_at": "2022-08-24T14:15:22Z", + "to": "13185551234", + "type": "mo_text" +} \ No newline at end of file