diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b5389db..36bbc30af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). # [9.10.0] - Exceptions: Added `getRawRequest()` and `getRawResponse()` methods to `VonageApiResponseException` for debugging API errors +- Messages: RCS TTL is now publicly settable on all RCS message types, with validation between 300 and 2592000 seconds # [9.9.0] - Video: Added post-call transcription options diff --git a/src/main/java/com/vonage/client/messages/rcs/CardOrientation.java b/src/main/java/com/vonage/client/messages/rcs/CardOrientation.java new file mode 100644 index 000000000..d5f24ab0c --- /dev/null +++ b/src/main/java/com/vonage/client/messages/rcs/CardOrientation.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.messages.rcs; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The orientation of a rich card. + * + * @since 9.9.0 + */ +public enum CardOrientation { + /** + * Vertical card orientation. + */ + VERTICAL, + + /** + * Horizontal card orientation. + */ + HORIZONTAL; + + @JsonValue + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/com/vonage/client/messages/rcs/CardWidth.java b/src/main/java/com/vonage/client/messages/rcs/CardWidth.java new file mode 100644 index 000000000..656d7c350 --- /dev/null +++ b/src/main/java/com/vonage/client/messages/rcs/CardWidth.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.messages.rcs; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The width of the rich cards displayed in a carousel. + * + * @since 9.9.0 + */ +public enum CardWidth { + /** + * Small card width. + */ + SMALL, + + /** + * Medium card width. + */ + MEDIUM; + + @JsonValue + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/com/vonage/client/messages/rcs/ImageAlignment.java b/src/main/java/com/vonage/client/messages/rcs/ImageAlignment.java new file mode 100644 index 000000000..9f7734f9d --- /dev/null +++ b/src/main/java/com/vonage/client/messages/rcs/ImageAlignment.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.messages.rcs; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The alignment of the thumbnail image in a rich card. + * This property only applies when sending rich cards with a card_orientation of HORIZONTAL. + * + * @since 9.9.0 + */ +public enum ImageAlignment { + /** + * Image aligned to the right. + */ + RIGHT, + + /** + * Image aligned to the left. + */ + LEFT; + + @JsonValue + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/com/vonage/client/messages/rcs/Rcs.java b/src/main/java/com/vonage/client/messages/rcs/Rcs.java index 6f1274630..7196077aa 100644 --- a/src/main/java/com/vonage/client/messages/rcs/Rcs.java +++ b/src/main/java/com/vonage/client/messages/rcs/Rcs.java @@ -24,11 +24,20 @@ * @since 9.6.0 */ public final class Rcs extends JsonableBaseObject { - private String category; - Boolean trustedRecipient; + String category; + CardOrientation cardOrientation; + ImageAlignment imageAlignment; + CardWidth cardWidth; Rcs() {} + private Rcs(Builder builder) { + this.category = builder.category; + this.cardOrientation = builder.cardOrientation; + this.imageAlignment = builder.imageAlignment; + this.cardWidth = builder.cardWidth; + } + /** * Creates an RCS options object with the specified category. * @@ -39,37 +48,132 @@ public Rcs(String category) { } /** - * Creates an RCS options object with the specified category and trusted recipient flag. + * The RCS message category. * - * @param category The RCS message category. - * @param trustedRecipient Whether the recipient is trusted. + * @return The category as a string, or {@code null} if not set. + */ + @JsonProperty("category") + public String getCategory() { + return category; + } + + /** + * The orientation of the rich card. + * + * @return The card orientation, or {@code null} if not set. * - * @since 9.8.0 + * @since 9.9.0 */ - public Rcs(String category, Boolean trustedRecipient) { - this.category = category; - this.trustedRecipient = trustedRecipient; + @JsonProperty("card_orientation") + public CardOrientation getCardOrientation() { + return cardOrientation; } /** - * The RCS message category. + * The alignment of the thumbnail image in the rich card. + * This property only applies when sending rich cards with a card_orientation of HORIZONTAL. * - * @return The category as a string, or {@code null} if not set. + * @return The image alignment, or {@code null} if not set. + * + * @since 9.9.0 */ - @JsonProperty("category") - public String getCategory() { - return category; + @JsonProperty("image_alignment") + public ImageAlignment getImageAlignment() { + return imageAlignment; } /** - * Whether the recipient is trusted. + * The width of the rich cards displayed in the carousel. + * + * @return The card width, or {@code null} if not set. * - * @return The trusted recipient flag as a Boolean, or {@code null} if not set. + * @since 9.9.0 + */ + @JsonProperty("card_width") + public CardWidth getCardWidth() { + return cardWidth; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + * + * @since 9.9.0 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for constructing an Rcs object with fluent API. * - * @since 9.8.0 + * @since 9.9.0 */ - @JsonProperty("trusted_recipient") - public Boolean getTrustedRecipient() { - return trustedRecipient; + public static final class Builder { + private String category; + private CardOrientation cardOrientation; + private ImageAlignment imageAlignment; + private CardWidth cardWidth; + + Builder() {} + + /** + * (OPTIONAL) + * Sets the RCS message category. + * + * @param category The RCS category. + * @return This builder. + */ + public Builder category(String category) { + this.category = category; + return this; + } + + /** + * (OPTIONAL) + * Sets the orientation of the rich card. + * + * @param cardOrientation The card orientation. + * @return This builder. + */ + public Builder cardOrientation(CardOrientation cardOrientation) { + this.cardOrientation = cardOrientation; + return this; + } + + /** + * (OPTIONAL) + * Sets the alignment of the thumbnail image in the rich card. + * This property only applies when sending rich cards with a card_orientation of HORIZONTAL. + * + * @param imageAlignment The image alignment. + * @return This builder. + */ + public Builder imageAlignment(ImageAlignment imageAlignment) { + this.imageAlignment = imageAlignment; + return this; + } + + /** + * (OPTIONAL) + * Sets the width of the rich cards displayed in the carousel. + * + * @param cardWidth The card width. + * @return This builder. + */ + public Builder cardWidth(CardWidth cardWidth) { + this.cardWidth = cardWidth; + return this; + } + + /** + * Builds the Rcs object. + * + * @return A new Rcs instance. + */ + public Rcs build() { + return new Rcs(this); + } } } diff --git a/src/main/java/com/vonage/client/messages/rcs/RcsRequest.java b/src/main/java/com/vonage/client/messages/rcs/RcsRequest.java index 9b3a056b9..68848500e 100644 --- a/src/main/java/com/vonage/client/messages/rcs/RcsRequest.java +++ b/src/main/java/com/vonage/client/messages/rcs/RcsRequest.java @@ -27,10 +27,16 @@ */ public abstract class RcsRequest extends MessageRequest { protected Rcs rcs; + protected Boolean trustedRecipient; protected RcsRequest(Builder builder, MessageType messageType) { super(builder, Channel.RCS, messageType); this.rcs = builder.rcs; + this.trustedRecipient = builder.trustedRecipient; + int min = 20, max = 259200; + if (ttl != null && (ttl < min || ttl > max)) { + throw new IllegalArgumentException("TTL must be between "+min+" and "+max+" seconds."); + } } @JsonProperty("ttl") @@ -38,6 +44,11 @@ public Integer getTtl() { return ttl; } + @JsonProperty("trusted_recipient") + public Boolean getTrustedRecipient() { + return trustedRecipient; + } + @JsonProperty("rcs") public Rcs getRcs() { return rcs; @@ -46,6 +57,7 @@ public Rcs getRcs() { @SuppressWarnings("unchecked") protected abstract static class Builder> extends MessageRequest.Builder { protected Rcs rcs; + protected Boolean trustedRecipient; /** * (OPTIONAL) @@ -61,19 +73,6 @@ public B rcs(Rcs rcs) { return (B) this; } - /** - * (OPTIONAL) - * Sets the RCS message category. - * - * @param category The RCS category. - * @return This builder. - * - * @since 9.5.0 - */ - public B rcsCategory(String category) { - return rcs(new Rcs(category)); - } - /** * (OPTIONAL) * Indicates if the recipient is trusted. @@ -84,15 +83,20 @@ public B rcsCategory(String category) { * @since 9.8.0 */ public B trustedRecipient(Boolean trustedRecipient) { - if (rcs == null) { - rcs = new Rcs(); - } - rcs.trustedRecipient = trustedRecipient; + this.trustedRecipient = trustedRecipient; return (B) this; } + /** + * (OPTIONAL) + * Sets the time-to-live for the RCS message. + * + * @param ttl The duration in seconds the delivery of a message will be attempted, + * between 300 and 2592000 seconds. + * @return This builder. + */ @Override - protected B ttl(int ttl) { + public B ttl(int ttl) { return super.ttl(ttl); } } diff --git a/src/test/java/com/vonage/client/messages/rcs/RcsTest.java b/src/test/java/com/vonage/client/messages/rcs/RcsTest.java new file mode 100644 index 000000000..ea9839aa2 --- /dev/null +++ b/src/test/java/com/vonage/client/messages/rcs/RcsTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2025 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.messages.rcs; + +import com.vonage.client.Jsonable; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class RcsTest { + + @Test + public void testConstructorWithCategory() { + String category = "transaction"; + Rcs rcs = new Rcs(category); + assertEquals(category, rcs.getCategory()); + assertNull(rcs.getCardOrientation()); + assertNull(rcs.getImageAlignment()); + assertNull(rcs.getCardWidth()); + } + + @Test + public void testBuilderAllFields() { + String category = "authentication"; + CardOrientation orientation = CardOrientation.HORIZONTAL; + ImageAlignment alignment = ImageAlignment.RIGHT; + CardWidth width = CardWidth.MEDIUM; + + Rcs rcs = Rcs.builder() + .category(category) + .cardOrientation(orientation) + .imageAlignment(alignment) + .cardWidth(width) + .build(); + + assertEquals(category, rcs.getCategory()); + assertEquals(orientation, rcs.getCardOrientation()); + assertEquals(alignment, rcs.getImageAlignment()); + assertEquals(width, rcs.getCardWidth()); + } + + @Test + public void testBuilderPartialFields() { + CardOrientation orientation = CardOrientation.VERTICAL; + CardWidth width = CardWidth.SMALL; + + Rcs rcs = Rcs.builder() + .cardOrientation(orientation) + .cardWidth(width) + .build(); + + assertNull(rcs.getCategory()); + assertEquals(orientation, rcs.getCardOrientation()); + assertNull(rcs.getImageAlignment()); + assertEquals(width, rcs.getCardWidth()); + } + + @Test + public void testSerializeWithAllFields() { + Rcs rcs = Rcs.builder() + .category("promotion") + .cardOrientation(CardOrientation.HORIZONTAL) + .imageAlignment(ImageAlignment.LEFT) + .cardWidth(CardWidth.SMALL) + .build(); + + String json = rcs.toJson(); + assertTrue(json.contains("\"category\":\"promotion\"")); + assertFalse(json.contains("trusted_recipient")); + assertTrue(json.contains("\"card_orientation\":\"HORIZONTAL\"")); + assertTrue(json.contains("\"image_alignment\":\"LEFT\"")); + assertTrue(json.contains("\"card_width\":\"SMALL\"")); + } + + @Test + public void testSerializeWithMinimalFields() { + Rcs rcs = Rcs.builder() + .cardOrientation(CardOrientation.VERTICAL) + .build(); + + String json = rcs.toJson(); + assertTrue(json.contains("\"card_orientation\":\"VERTICAL\"")); + assertFalse(json.contains("category")); + assertFalse(json.contains("trusted_recipient")); + } + + @Test + public void testDeserializeWithAllFields() throws Exception { + String json = "{\"category\":\"transaction\"," + + "\"card_orientation\":\"HORIZONTAL\",\"image_alignment\":\"RIGHT\"," + + "\"card_width\":\"MEDIUM\"}"; + + Rcs rcs = Jsonable.fromJson(json, Rcs.class); + assertNotNull(rcs); + assertEquals("transaction", rcs.getCategory()); + assertEquals(CardOrientation.HORIZONTAL, rcs.getCardOrientation()); + assertEquals(ImageAlignment.RIGHT, rcs.getImageAlignment()); + assertEquals(CardWidth.MEDIUM, rcs.getCardWidth()); + } + + @Test + public void testDeserializeWithPartialFields() throws Exception { + String json = "{\"card_orientation\":\"VERTICAL\",\"card_width\":\"SMALL\"}"; + + Rcs rcs = Jsonable.fromJson(json, Rcs.class); + assertNotNull(rcs); + assertNull(rcs.getCategory()); + assertEquals(CardOrientation.VERTICAL, rcs.getCardOrientation()); + assertNull(rcs.getImageAlignment()); + assertEquals(CardWidth.SMALL, rcs.getCardWidth()); + } + + @Test + public void testEmptyRcs() { + Rcs rcs = Rcs.builder().build(); + assertNull(rcs.getCategory()); + assertNull(rcs.getCardOrientation()); + assertNull(rcs.getImageAlignment()); + assertNull(rcs.getCardWidth()); + } +} diff --git a/src/test/java/com/vonage/client/messages/rcs/RcsTextRequestTest.java b/src/test/java/com/vonage/client/messages/rcs/RcsTextRequestTest.java index 068e29081..0d8675ce7 100644 --- a/src/test/java/com/vonage/client/messages/rcs/RcsTextRequestTest.java +++ b/src/test/java/com/vonage/client/messages/rcs/RcsTextRequestTest.java @@ -55,7 +55,14 @@ public void testRequiredParametersOnly() { @Test public void testTtlTooShort() { assertThrows(IllegalArgumentException.class, () -> - RcsTextRequest.builder().from(from).to(to).text(message).ttl(0).build() + RcsTextRequest.builder().from(from).to(to).text(message).ttl(19).build() + ); + } + + @Test + public void testTtlTooLong() { + assertThrows(IllegalArgumentException.class, () -> + RcsTextRequest.builder().from(from).to(to).text(message).ttl(2592001).build() ); } @@ -202,7 +209,7 @@ public void testWithTrustedRecipient() { String json = rcs.toJson(); assertTrue(json.contains("\"trusted_recipient\":true")); - assertTrue(json.contains("\"rcs\":")); + assertFalse(json.contains("\"rcs\":")); } @Test @@ -213,7 +220,7 @@ public void testTrustedRecipientFalse() { String json = rcs.toJson(); assertTrue(json.contains("\"trusted_recipient\":false")); - assertTrue(json.contains("\"rcs\":")); + assertFalse(json.contains("\"rcs\":")); } @Test