- * I am placing this code in the Public Domain. Do with it as you will.
- * This software comes with no guarantees or warranties but with
- * plenty of well-wishing instead!
- * Please visit http://iharder.net/base64
- * periodically to check for updates or to contribute improvements.
- *
- * @deprecated This class is being deprecated together with {@link RequestSigner}
- */
-@Deprecated
-class Base64 {
-
- /* ******** P U B L I C F I E L D S ******** */
-
-
- /**
- * No options specified. Value is zero.
- */
- public final static int NO_OPTIONS = 0;
-
- /**
- * Specify that gzipped data should not be automatically gunzipped.
- */
- public final static int DONT_GUNZIP = 4;
-
- /**
- * Encode using Base64-like encoding that is URL- and Filename-safe as described
- * in Section 4 of RFC3548:
- * http://www.faqs.org/rfcs/rfc3548.html.
- * It is important to note that data encoded this way is not officially valid Base64,
- * or at the very least should not be called Base64 without also specifying that is
- * was encoded using the URL- and Filename-safe dialect.
- */
- public final static int URL_SAFE = 16;
-
-
- /**
- * Encode using the special "ordered" dialect of Base64 described here:
- * http://www.faqs.org/qa/rfcc-1940.html.
- */
- public final static int ORDERED = 32;
-
-
- /* ******** P R I V A T E F I E L D S ******** */
-
-
- /**
- * The equals sign (=) as a byte.
- */
- private final static byte EQUALS_SIGN = (byte) '=';
-
-
- /**
- * Preferred encoding.
- */
- private final static String PREFERRED_ENCODING = "US-ASCII";
-
-
- private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
- private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
-
-
- /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
-
- /**
- * Translates a Base64 value to either its 6-bit reconstruction value
- * or a negative number indicating some other meaning.
- **/
- private final static byte[] _STANDARD_DECODABET = {
- -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
- -5, -5, // Whitespace: Tab and Linefeed
- -9, -9, // Decimal 11 - 12
- -5, // Whitespace: Carriage Return
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
- -9, -9, -9, -9, -9, // Decimal 27 - 31
- -5, // Whitespace: Space
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
- 62, // Plus sign at decimal 43
- -9, -9, -9, // Decimal 44 - 46
- 63, // Slash at decimal 47
- 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
- -9, -9, -9, // Decimal 58 - 60
- -1, // Equals sign at decimal 61
- -9, -9, -9, // Decimal 62 - 64
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
- 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
- -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
- 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
- 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
- -9, -9, -9, -9, -9 // Decimal 123 - 127
- , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
- };
-
-
- /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
-
- /**
- * Used in decoding URL- and Filename-safe dialects of Base64.
- */
- private final static byte[] _URL_SAFE_DECODABET = {
- -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
- -5, -5, // Whitespace: Tab and Linefeed
- -9, -9, // Decimal 11 - 12
- -5, // Whitespace: Carriage Return
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
- -9, -9, -9, -9, -9, // Decimal 27 - 31
- -5, // Whitespace: Space
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
- -9, // Plus sign at decimal 43
- -9, // Decimal 44
- 62, // Minus sign at decimal 45
- -9, // Decimal 46
- -9, // Slash at decimal 47
- 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
- -9, -9, -9, // Decimal 58 - 60
- -1, // Equals sign at decimal 61
- -9, -9, -9, // Decimal 62 - 64
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
- 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
- -9, -9, -9, -9, // Decimal 91 - 94
- 63, // Underscore at decimal 95
- -9, // Decimal 96
- 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
- 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
- -9, -9, -9, -9, -9 // Decimal 123 - 127
- , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
- };
-
-
-
- /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
-
- /**
- * Used in decoding the "ordered" dialect of Base64.
- */
- private final static byte[] _ORDERED_DECODABET = {
- -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
- -5, -5, // Whitespace: Tab and Linefeed
- -9, -9, // Decimal 11 - 12
- -5, // Whitespace: Carriage Return
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
- -9, -9, -9, -9, -9, // Decimal 27 - 31
- -5, // Whitespace: Space
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
- -9, // Plus sign at decimal 43
- -9, // Decimal 44
- 0, // Minus sign at decimal 45
- -9, // Decimal 46
- -9, // Slash at decimal 47
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine
- -9, -9, -9, // Decimal 58 - 60
- -1, // Equals sign at decimal 61
- -9, -9, -9, // Decimal 62 - 64
- 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M'
- 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z'
- -9, -9, -9, -9, // Decimal 91 - 94
- 37, // Underscore at decimal 95
- -9, // Decimal 96
- 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm'
- 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z'
- -9, -9, -9, -9, -9 // Decimal 123 - 127
- , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255
- };
-
-
- /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */
-
- /**
- * Returns one of the _SOMETHING_DECODABET byte arrays depending on
- * the options specified.
- * It's possible, though silly, to specify ORDERED and URL_SAFE
- * in which case one of them will be picked, though there is
- * no guarantee as to which one will be picked.
- */
- private final static byte[] getDecodabet(int options) {
- if ((options & URL_SAFE) == URL_SAFE) {
- return _URL_SAFE_DECODABET;
- } else if ((options & ORDERED) == ORDERED) {
- return _ORDERED_DECODABET;
- } else {
- return _STANDARD_DECODABET;
- }
- } // end getAlphabet
-
-
- /**
- * Defeats instantiation.
- */
- private Base64() {
- }
-
-
-
- /* ******** D E C O D I N G M E T H O D S ******** */
-
-
- /**
- * Decodes four bytes from array source
- * and writes the resulting bytes (up to three of them)
- * to destination.
- * The source and destination arrays can be manipulated
- * anywhere along their length by specifying
- * srcOffset and destOffset.
- * This method does not check to make sure your arrays
- * are large enough to accomodate srcOffset + 4 for
- * the source array or destOffset + 3 for
- * the destination array.
- * This method returns the actual number of bytes that
- * were converted from the Base64 encoding.
- *
This is the lowest level of the decoding methods with
- * all possible parameters.
- *
- * @param source the array to convert
- * @param srcOffset the index where conversion begins
- * @param destination the array to hold the conversion
- * @param destOffset the index where output will be put
- * @param options alphabet type is pulled from this (standard, url-safe, ordered)
- * @return the number of decoded bytes converted
- * @throws NullPointerException if source or destination arrays are null
- * @throws IllegalArgumentException if srcOffset or destOffset are invalid
- * or there is not enough room in the array.
- * @since 1.3
- */
- private static int decode4to3(
- byte[] source, int srcOffset,
- byte[] destination, int destOffset, int options) {
-
- // Lots of error checking and exception throwing
- if (source == null) {
- throw new NullPointerException("Source array was null.");
- } // end if
- if (destination == null) {
- throw new NullPointerException("Destination array was null.");
- } // end if
- if (srcOffset < 0 || srcOffset + 3 >= source.length) {
- throw new IllegalArgumentException(String.format(
- "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset));
- } // end if
- if (destOffset < 0 || destOffset + 2 >= destination.length) {
- throw new IllegalArgumentException(String.format(
- "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset));
- } // end if
-
-
- byte[] DECODABET = getDecodabet(options);
-
- // Example: Dk==
- if (source[srcOffset + 2] == EQUALS_SIGN) {
- // Two ways to do the same thing. Don't know which way I like best.
- //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
- // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
- int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
- | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12);
-
- destination[destOffset] = (byte) (outBuff >>> 16);
- return 1;
- }
-
- // Example: DkL=
- else if (source[srcOffset + 3] == EQUALS_SIGN) {
- // Two ways to do the same thing. Don't know which way I like best.
- //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
- // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
- // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
- int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
- | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
- | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6);
-
- destination[destOffset] = (byte) (outBuff >>> 16);
- destination[destOffset + 1] = (byte) (outBuff >>> 8);
- return 2;
- }
-
- // Example: DkLE
- else {
- // Two ways to do the same thing. Don't know which way I like best.
- //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
- // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
- // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
- // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
- int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
- | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
- | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6)
- | ((DECODABET[source[srcOffset + 3]] & 0xFF));
-
-
- destination[destOffset] = (byte) (outBuff >> 16);
- destination[destOffset + 1] = (byte) (outBuff >> 8);
- destination[destOffset + 2] = (byte) (outBuff);
-
- return 3;
- }
- } // end decodeToBytes
-
-
- /**
- * Low-level access to decoding ASCII characters in
- * the form of a byte array. Ignores GUNZIP option, if
- * it's set. This is not generally a recommended method,
- * although it is used internally as part of the decoding process.
- * Special case: if len = 0, an empty array is returned. Still,
- * if you need more speed and reduced memory footprint (and aren't
- * gzipping), consider this method.
- *
- * @param source The Base64 encoded data
- * @param off The offset of where to begin decoding
- * @param len The length of characters to decode
- * @param options Can specify options such as alphabet type to use
- * @return decoded data
- * @throws java.io.IOException If bogus characters exist in source data
- * @since 1.3
- */
- public static byte[] decode(byte[] source, int off, int len, int options)
- throws java.io.IOException {
-
- // Lots of error checking and exception throwing
- if (source == null) {
- throw new NullPointerException("Cannot decode null source array.");
- } // end if
- if (off < 0 || off + len > source.length) {
- throw new IllegalArgumentException(String.format(
- "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len));
- } // end if
-
- if (len == 0) {
- return new byte[0];
- } else if (len < 4) {
- throw new IllegalArgumentException(
- "Base64-encoded string must have at least four characters, but length specified was " + len);
- } // end if
-
- byte[] DECODABET = getDecodabet(options);
-
- int len34 = len * 3 / 4; // Estimate on array size
- byte[] outBuff = new byte[len34]; // Upper limit on size of output
- int outBuffPosn = 0; // Keep track of where we're writing
-
- byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space
- int b4Posn = 0; // Keep track of four byte input buffer
- int i = 0; // Source array counter
- byte sbiDecode = 0; // Special value from DECODABET
-
- for (i = off; i < off + len; i++) { // Loop through source
-
- sbiDecode = DECODABET[source[i] & 0xFF];
-
- // White space, Equals sign, or legit Base64 character
- // Note the values such as -5 and -9 in the
- // DECODABETs at the top of the file.
- if (sbiDecode >= WHITE_SPACE_ENC) {
- if (sbiDecode >= EQUALS_SIGN_ENC) {
- b4[b4Posn++] = source[i]; // Save non-whitespace
- if (b4Posn > 3) { // Time to decode?
- outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options);
- b4Posn = 0;
-
- // If that was the equals sign, break out of 'for' loop
- if (source[i] == EQUALS_SIGN) {
- break;
- } // end if: equals sign
- } // end if: quartet built
- } // end if: equals sign or better
- } // end if: white space, equals sign or better
- else {
- // There's a bad input character in the Base64 stream.
- throw new java.io.IOException(String.format(
- "Bad Base64 input character decimal %d in array position %d", ((int) source[i]) & 0xFF, i));
- } // end else:
- } // each input character
-
- byte[] out = new byte[outBuffPosn];
- System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
- return out;
- } // end decode
-
-
- /**
- * Decodes data from Base64 notation, automatically
- * detecting gzip-compressed data and decompressing it.
- *
- * @param s the string to decode
- * @return the decoded data
- * @throws java.io.IOException If there is a problem
- * @since 1.4
- */
- public static byte[] decode(String s) throws java.io.IOException {
- return decode(s, NO_OPTIONS);
- }
-
-
- /**
- * Decodes data from Base64 notation, automatically
- * detecting gzip-compressed data and decompressing it.
- *
- * @param s the string to decode
- * @param options encode options such as URL_SAFE
- * @return the decoded data
- * @throws java.io.IOException if there is an error
- * @throws NullPointerException if s is null
- * @since 1.4
- */
- public static byte[] decode(String s, int options) throws java.io.IOException {
-
- if (s == null) {
- throw new NullPointerException("Input string was null.");
- } // end if
-
- byte[] bytes;
- try {
- bytes = s.getBytes(PREFERRED_ENCODING);
- } // end try
- catch (java.io.UnsupportedEncodingException uee) {
- bytes = s.getBytes();
- } // end catch
- //
-
- // Decode
- bytes = decode(bytes, 0, bytes.length, options);
-
- // Check to see if it's gzip-compressed
- // GZIP Magic Two-Byte Number: 0x8b1f (35615)
- boolean dontGunzip = (options & DONT_GUNZIP) != 0;
- if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) {
-
- int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
- if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) {
- java.io.ByteArrayInputStream bais = null;
- java.util.zip.GZIPInputStream gzis = null;
- java.io.ByteArrayOutputStream baos = null;
- byte[] buffer = new byte[2048];
- int length = 0;
-
- try {
- baos = new java.io.ByteArrayOutputStream();
- bais = new java.io.ByteArrayInputStream(bytes);
- gzis = new java.util.zip.GZIPInputStream(bais);
-
- while ((length = gzis.read(buffer)) >= 0) {
- baos.write(buffer, 0, length);
- } // end while: reading input
-
- // No error? Get new bytes.
- bytes = baos.toByteArray();
-
- } // end try
- catch (java.io.IOException e) {
- e.printStackTrace();
- // Just return originally-decoded bytes
- } // end catch
- finally {
- try {
- baos.close();
- } catch (Exception e) {
- }
- try {
- gzis.close();
- } catch (Exception e) {
- }
- try {
- bais.close();
- } catch (Exception e) {
- }
- } // end finally
-
- } // end if: gzipped
- } // end if: bytes.length >= 2
-
- return bytes;
- } // end decode
-
-
-} // end class Base64
diff --git a/api/src/main/java/com/messagebird/MessageBirdClient.java b/api/src/main/java/com/messagebird/MessageBirdClient.java
index 4a2859a4..583dcb40 100644
--- a/api/src/main/java/com/messagebird/MessageBirdClient.java
+++ b/api/src/main/java/com/messagebird/MessageBirdClient.java
@@ -105,6 +105,9 @@ public class MessageBirdClient {
private static final String VOICELEGS_SUFFIX_PATH = "/legs";
static final String FILES_PATH = "/files";
static final String TEMPLATES_PATH = "/templates";
+ static final String UNPAUSE_TEMAPLATE_PATH = "/unpause";
+ static final String OUTBOUND_SMS_PRICING_PATH = "/pricing/sms/outbound";
+ static final String OUTBOUND_SMS_PRICING_SMPP_PATH = "/pricing/sms/outbound/smpp/%s";
static final String RECORDING_DOWNLOAD_FORMAT = ".wav";
@@ -470,7 +473,7 @@ public Verify verifyToken(String id, String token) throws NotFoundException, Gen
* @throws UnauthorizedException if client is unauthorized
* @throws GeneralException general exception
*/
- Verify getVerifyObject(String id) throws NotFoundException, GeneralException, UnauthorizedException {
+ public Verify getVerifyObject(String id) throws NotFoundException, GeneralException, UnauthorizedException {
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("ID cannot be empty for verify");
}
@@ -1874,6 +1877,31 @@ public TemplateResponse createWhatsAppTemplate(final Template template)
return messageBirdService.sendPayLoad(url, template, TemplateResponse.class);
}
+ /**
+ * Update a WhatsApp message template through MessageBird.
+ *
+ * @param template {@link Template} object to be created
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @param language A language code as returned by getWhatsAppTemplateBy in the language variable
+ * @return {@link TemplateResponse} response object
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws IllegalArgumentException invalid template format
+ */
+ public TemplateResponse updateWhatsAppTemplate(final Template template, final String templateName, final String language)
+ throws UnauthorizedException, GeneralException, IllegalArgumentException {
+ template.validate();
+
+ String url = String.format(
+ "%s%s%s/%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language);
+
+ return messageBirdService.sendPayLoad("PUT",url, template, TemplateResponse.class);
+ }
/**
* Gets a WhatsAppTemplate listing with specified pagination options.
*
@@ -1894,6 +1922,34 @@ public TemplateList listWhatsAppTemplates(final int offset, final int limit)
return messageBirdService.requestList(url, offset, limit, TemplateList.class);
}
+ /**
+ * Gets a WhatsAppTemplate listing with specified pagination options and a wabaID or channelID filter.
+ *
+ * @param offset Number of objects to skip.
+ * @param limit Number of objects to take.
+ * @param wabaID The WABA ID to filter templates by.
+ * @param channelID A channel ID filter to return only templates that can be sent via that channel.
+ * @return List of templates.
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public TemplateList listWhatsAppTemplates(final int offset, final int limit, final String wabaID, final String channelID)
+ throws UnauthorizedException, GeneralException, IllegalArgumentException {
+ validateWABAIDAndChannelIDArguments(wabaID, channelID);
+
+ Map map = new LinkedHashMap<>();
+ if (wabaID != null) map.put("wabaId", wabaID);
+ if (channelID != null) map.put("channelId", channelID);
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ return messageBirdService.requestList(url, map, offset, limit, TemplateList.class);
+ }
+
/**
* Gets a template listing with default pagination options.
*
@@ -1913,12 +1969,13 @@ public TemplateList listWhatsAppTemplates() throws UnauthorizedException, Genera
*
* @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
* @return {@code List} template list
- * @throws UnauthorizedException if client is unauthorized
- * @throws GeneralException general exception
- * @throws NotFoundException if template name is not found
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name is not found
+ * @throws IllegalArgumentException if the provided arguments are not valid
*/
public List getWhatsAppTemplatesBy(final String templateName)
- throws GeneralException, UnauthorizedException, NotFoundException {
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
if (templateName == null) {
throw new IllegalArgumentException("Template name must be specified.");
}
@@ -1933,19 +1990,50 @@ public List getWhatsAppTemplatesBy(final String templateName)
return messageBirdService.requestByIdAsList(url, templateName, TemplateResponse.class);
}
+ /**
+ * Retrieves the template of an existing template name.
+ *
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @param wabaID An optional WABA ID to look for the template ID under.
+ * @param channelID An optional channel ID to specify. If the template can be sent via the channel, it will return the template.
+ *
+ * @return {@code List} template list
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name is not found under the given WABA or cannot be sent under the supplied channel ID
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public List getWhatsAppTemplatesBy(final String templateName, final String wabaID, final String channelID)
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
+ if (templateName == null) {
+ throw new IllegalArgumentException("Template name must be specified.");
+ }
+
+ String id = String.format("%s%s", templateName, getWabaIDOrChannelIDQuery(wabaID, channelID));
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ return messageBirdService.requestByIdAsList(url, id, TemplateResponse.class);
+ }
+
/**
- * Retrieves the template of an existing template name and language.
+ * Retrieves the template of an existing template name and language under the first waba connected to the requesting user.
*
* @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
* @param language A language code as returned by getWhatsAppTemplateBy in the language variable
*
- * @return {@code TemplateResponse} template list
+ * @return {@code TemplateResponse} template
* @throws UnauthorizedException if client is unauthorized
* @throws GeneralException general exception
- * @throws NotFoundException if template name and language are not found
+ * @throws NotFoundException if template name and language are not found under the first waba connected to the requesting user.
+ * @throws IllegalArgumentException if the provided arguments are not valid
*/
public TemplateResponse fetchWhatsAppTemplateBy(final String templateName, final String language)
- throws GeneralException, UnauthorizedException, NotFoundException {
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
if (templateName == null || language == null) {
throw new IllegalArgumentException("Template name and language must be specified.");
}
@@ -1961,6 +2049,79 @@ public TemplateResponse fetchWhatsAppTemplateBy(final String templateName, final
return messageBirdService.request(url, TemplateResponse.class);
}
+ /**
+ * Retrieves the template of an existing template name and language under a WABA or for a channel.
+ *
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @param language A language code as returned by getWhatsAppTemplateBy in the language variable
+ * @param wabaID An optional WABA ID to look for the template ID under.
+ * @param channelID An optional channel ID to specify. If the template can be sent via the channel, it will return the template.
+ *
+ * @return {@code TemplateResponse} template
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name and language are not found under the given WABA or cannot be sent under the supplied channel ID.
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public TemplateResponse fetchWhatsAppTemplateBy(final String templateName, final String language, final String wabaID, final String channelID)
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
+ if (templateName == null || language == null) {
+ throw new IllegalArgumentException("Template name and language must be specified.");
+ }
+
+ String url = String.format(
+ "%s%s%s/%s/%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language,
+ getWabaIDOrChannelIDQuery(wabaID, channelID)
+ );
+ return messageBirdService.request(url, TemplateResponse.class);
+ }
+
+ /**
+ * Validates the WABA ID and Channel ID argument pair.
+ *
+ * @param wabaID A WABA ID.
+ * @param channelID A channel ID.
+ * @throws IllegalArgumentException if the argument pair is invalid.
+ */
+ private void validateWABAIDAndChannelIDArguments(String wabaID, String channelID)
+ throws IllegalArgumentException {
+ if (wabaID == null && channelID == null) {
+ throw new IllegalArgumentException("wabaID or channelID must be specified");
+ }
+
+ if (wabaID != null && channelID != null) {
+ throw new IllegalArgumentException("only supply wabaID or channelID - not both");
+ }
+ }
+
+ /**
+ * Validates the WABA ID and Channel ID argument pair and returns a valid query parameter string.
+ *
+ * @param wabaID A WABA ID.
+ * @param channelID A channel ID.
+ * @throws IllegalArgumentException if the argument pair is invalid.
+ */
+ private String getWabaIDOrChannelIDQuery(String wabaID, String channelID)
+ throws IllegalArgumentException {
+ validateWABAIDAndChannelIDArguments(wabaID, channelID);
+
+ String query = "";
+
+ if (wabaID != null) {
+ query = String.format("?wabaId=%s", wabaID);
+ }
+ if (channelID != null) {
+ query = String.format("?channelId=%s", channelID);
+ }
+
+ return query;
+ }
+
/**
* Delete templates of an existing template name.
*
@@ -1985,30 +2146,21 @@ public void deleteTemplatesBy(final String templateName)
messageBirdService.delete(url, null);
}
- /**
- * Delete template of an existing template name and language.
- *
- * @param templateName A template name which is created on the MessageBird platform
- * @param language A language which is created on the MessageBird platform
- * @throws UnauthorizedException if client is unauthorized
- * @throws GeneralException general exception
- * @throws NotFoundException if template name or language are not found
- */
- public void deleteTemplatesBy(final String templateName, final String language)
- throws UnauthorizedException, GeneralException, NotFoundException {
- if (templateName == null || language == null) {
- throw new IllegalArgumentException("Template name and language must be specified.");
+ public void unpauseTemplatesByTemplateName(final String templateName)
+ throws UnauthorizedException, GeneralException {
+ if (templateName == null) {
+ throw new IllegalArgumentException("Template name must be specified.");
}
String url = String.format(
- "%s%s%s/%s/%s",
- INTEGRATIONS_BASE_URL_V2,
- INTEGRATIONS_WHATSAPP_PATH,
- TEMPLATES_PATH,
- templateName,
- language
+ "%s%s%s%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ UNPAUSE_TEMAPLATE_PATH,
+ templateName
);
- messageBirdService.delete(url, null);
+ messageBirdService.sendPayLoad("POST", url, "", null);
}
/**
@@ -2095,4 +2247,41 @@ public void deleteChildAccount(final String id) throws UnauthorizedException, Ge
System.out.println("url: " + url);
messageBirdService.deleteByID(url, id);
}
+
+ /**
+ * Returns outbound pricing for the default SMS configuration for the authenticated account.
+ *
+ * @return outbound pricing for the default SMS configuration for the authenticated account
+ *
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if pricing information could not be found
+ *
+ * @see Pricing API
+ */
+ public OutboundSmsPriceResponse getOutboundSmsPrices() throws GeneralException, UnauthorizedException, NotFoundException {
+ return messageBirdService.request(OUTBOUND_SMS_PRICING_PATH, OutboundSmsPriceResponse.class);
+ }
+
+ /**
+ * Returns outbound SMS pricing for a specific SMPP username.
+ *
+ * @param smppUsername the SMPP SystemID provided by MessageBird
+ *
+ * @return outbound SMS pricing for the given SMPP username
+ *
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if pricing information could not be found for the given SMPP username
+ *
+ * @see Pricing API
+ */
+ public OutboundSmsPriceResponse getOutboundSmsPrices(final String smppUsername) throws GeneralException, UnauthorizedException, NotFoundException {
+ if (smppUsername == null) {
+ throw new IllegalArgumentException("SMPP username must be specified.");
+ }
+
+ final String url = String.format(OUTBOUND_SMS_PRICING_SMPP_PATH, smppUsername);
+ return messageBirdService.request(url, OutboundSmsPriceResponse.class);
+ }
}
diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
index 892db5c6..ef179b45 100644
--- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
+++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
@@ -2,15 +2,14 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.MapperFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.*;
import com.messagebird.exceptions.GeneralException;
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
import com.messagebird.objects.ErrorReport;
import com.messagebird.objects.PagedPaging;
+import org.apache.maven.artifact.versioning.ComparableVersion;
+
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -63,8 +62,7 @@ public class MessageBirdServiceImpl implements MessageBirdService {
private static final String[] PROTOCOL_LISTS = new String[]{"http://", "https://"};
private static final List PROTOCOLS = Arrays.asList(PROTOCOL_LISTS);
- // Used when the actual version can not be parsed.
- private static final double DEFAULT_JAVA_VERSION = 0.0;
+ private static final ComparableVersion JAVA_VERSION = getJavaVersion();
// Indicates whether we've overridden HttpURLConnection's behaviour to
// allow PATCH requests yet. Also see docs on allowPatchRequestsIfNeeded().
@@ -74,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService {
private final String accessKey;
private final String serviceUrl;
- private final String clientVersion = "3.2.2";
+ private final String clientVersion = "6.3.1";
private final String userAgentString;
private Proxy proxy = null;
@@ -91,15 +89,17 @@ public MessageBirdServiceImpl(final String accessKey, final String serviceUrl) {
}
- private String determineUserAgentString() {
- double javaVersion = DEFAULT_JAVA_VERSION;
+ private static ComparableVersion getJavaVersion() {
try {
- javaVersion = getVersion();
- } catch (GeneralException e) {
- // Do nothing: leave the version at its default.
+ String version = System.getProperty("java.version");
+ return new ComparableVersion(version);
+ } catch (IllegalArgumentException e) {
+ return new ComparableVersion("0.0");
}
+ }
- return String.format("MessageBird Java/%s ApiClient/%s", javaVersion, clientVersion);
+ private String determineUserAgentString() {
+ return String.format("MessageBird Java/%s ApiClient/%s", JAVA_VERSION, clientVersion);
}
/**
@@ -382,6 +382,9 @@
APIResponse doRequest(final String method, final String url, final Map 1.6) {
+ ComparableVersion java6 = new ComparableVersion("1.6");
+ if (JAVA_VERSION.compareTo(java6) > 0) {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
}
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZ");
}
- private double getVersion() throws GeneralException {
- String version = System.getProperty("java.version");
-
- try {
- int pos = version.indexOf('.');
- pos = version.indexOf('.', pos + 1);
-
- return Double.parseDouble(version.substring(0, pos));
- } catch (RuntimeException e) {
- // Thrown if the index is out of bounds, or when we can't parse a
- // double for some reason.
- throw new GeneralException(e);
- }
- }
-
/**
* Get the MessageBird error report data.
*
@@ -805,7 +787,7 @@ private String getPathVariables(final Map map) {
// the value is returned from the next() call
bpath.append(encodeKeyValuePair(param.getKey(), iterator.next()));
count++;
- }
+ }
} else {
// If the value is not a collection, create the querystring value directly.
bpath.append(encodeKeyValuePair(param.getKey(), param.getValue()));
diff --git a/api/src/main/java/com/messagebird/RequestSigner.java b/api/src/main/java/com/messagebird/RequestSigner.java
index 32c257a2..d79e464c 100644
--- a/api/src/main/java/com/messagebird/RequestSigner.java
+++ b/api/src/main/java/com/messagebird/RequestSigner.java
@@ -4,13 +4,13 @@
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
-import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import java.util.Base64;
/**
* RequestSigner is used to verify HTTP requests and is an implementation of:
@@ -27,7 +27,7 @@ public class RequestSigner {
private static final String ALGORITHM_HMAC_SHA256 = "HmacSHA256";
private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
- private SecretKeySpec secret;
+ private final SecretKeySpec secret;
/**
* Constructs a new RequestSigner instance.
@@ -55,8 +55,8 @@ public RequestSigner(byte[] key) {
@Deprecated
public boolean isMatch(String expectedSignature, Request request) {
try {
- return isMatch(Base64.decode(expectedSignature), request);
- } catch (IOException e) {
+ return isMatch(Base64.getDecoder().decode(expectedSignature), request);
+ } catch (IllegalArgumentException e) {
throw new RequestSigningException(e);
}
}
diff --git a/api/src/main/java/com/messagebird/RequestValidator.java b/api/src/main/java/com/messagebird/RequestValidator.java
index f2a4613b..24ae6343 100644
--- a/api/src/main/java/com/messagebird/RequestValidator.java
+++ b/api/src/main/java/com/messagebird/RequestValidator.java
@@ -5,13 +5,14 @@
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
-import com.auth0.jwt.interfaces.Clock;
+import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.messagebird.exceptions.RequestValidationException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.time.Clock;
/**
* RequestValidator validates request signature signed by MessageBird services.
@@ -128,7 +129,8 @@ public DecodedJWT validateSignature(Clock clock, String signature, String url, b
if (!skipURLValidation)
builder.withClaim("url_hash", calculateSha256(url.getBytes()));
- boolean payloadHashClaimExist = !jwt.getClaim("payload_hash").isNull();
+ Claim payloadHashClaim = jwt.getClaim("payload_hash");
+ boolean payloadHashClaimExist = !(payloadHashClaim.isNull() || payloadHashClaim.isMissing());
if (requestBody != null && requestBody.length > 0) {
if (!payloadHashClaimExist) {
throw new RequestValidationException("The Claim 'payload_hash' is not set but payload is present.");
diff --git a/api/src/main/java/com/messagebird/objects/Language.java b/api/src/main/java/com/messagebird/objects/Language.java
index 6ce76027..ccb63351 100644
--- a/api/src/main/java/com/messagebird/objects/Language.java
+++ b/api/src/main/java/com/messagebird/objects/Language.java
@@ -1,5 +1,7 @@
package com.messagebird.objects;
+import com.fasterxml.jackson.annotation.JsonValue;
+
/**
* Created by faizan on 09/12/15.
*/
@@ -11,7 +13,7 @@ public enum Language {
EN_US("en-us"),
ES_ES("es-es"),
FR_FR("fr-fr"),
- RU_RU("ru_ru"),
+ RU_RU("ru-ru"),
ZH_CN("zh-cn"),
EN_AU("en-au"),
ES_MX("es-mx"),
@@ -25,13 +27,21 @@ public enum Language {
PT_BR("pt-br"),
RO_RO("ro-ro");
- private String code;
+ final String code;
Language(String code) {
this.code = code;
}
+ @JsonValue
+ public String getCode() {
+ return code;
+ }
+
+ @Override
public String toString() {
- return this.code;
+ return "Language{" +
+ "code='" + code + '\'' +
+ '}';
}
}
diff --git a/api/src/main/java/com/messagebird/objects/MessageResponse.java b/api/src/main/java/com/messagebird/objects/MessageResponse.java
index 91d4d6da..aa104e52 100644
--- a/api/src/main/java/com/messagebird/objects/MessageResponse.java
+++ b/api/src/main/java/com/messagebird/objects/MessageResponse.java
@@ -3,6 +3,7 @@
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
+import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
@@ -382,7 +383,7 @@ static public class Price implements Serializable {
private static final long serialVersionUID = -4104837036540050532L;
- private float amount;
+ private BigDecimal amount;
private String currency;
public Price() {
@@ -397,6 +398,10 @@ public String toString() {
}
public float getAmount() {
+ return amount.floatValue();
+ }
+
+ public BigDecimal getAmountDecimal() {
return amount;
}
diff --git a/api/src/main/java/com/messagebird/objects/OutboundSmsPrice.java b/api/src/main/java/com/messagebird/objects/OutboundSmsPrice.java
new file mode 100644
index 00000000..3733cf69
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/OutboundSmsPrice.java
@@ -0,0 +1,60 @@
+package com.messagebird.objects;
+
+import java.math.BigDecimal;
+
+public class OutboundSmsPrice {
+ private BigDecimal price;
+ private String currencyCode;
+ private String mccmnc;
+ private String mcc;
+ private String mnc;
+ private String countryName;
+ private String countryIsoCode;
+ private String operatorName;
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public String getCurrencyCode() {
+ return currencyCode;
+ }
+
+ public String getMccmnc() {
+ return mccmnc;
+ }
+
+ public String getMcc() {
+ return mcc;
+ }
+
+ public String getMnc() {
+ return mnc;
+ }
+
+ public String getCountryName() {
+ return countryName;
+ }
+
+ public String getCountryIsoCode() {
+ return countryIsoCode;
+ }
+
+ public String getOperatorName() {
+ return operatorName;
+ }
+
+ @Override
+ public String toString() {
+ return "OutboundSmsPrice{" +
+ "price=" + price +
+ ", currencyCode='" + currencyCode + '\'' +
+ ", mccmnc='" + mccmnc + '\'' +
+ ", mcc='" + mcc + '\'' +
+ ", mnc='" + mnc + '\'' +
+ ", countryName='" + countryName + '\'' +
+ ", countryIsoCode='" + countryIsoCode + '\'' +
+ ", operatorName='" + operatorName + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/OutboundSmsPriceResponse.java b/api/src/main/java/com/messagebird/objects/OutboundSmsPriceResponse.java
new file mode 100644
index 00000000..6d036882
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/OutboundSmsPriceResponse.java
@@ -0,0 +1,36 @@
+package com.messagebird.objects;
+
+import java.util.List;
+
+public class OutboundSmsPriceResponse {
+ private int gateway;
+ private String currencyCode;
+ private int totalCount;
+ private List prices;
+
+ public int getGateway() {
+ return gateway;
+ }
+
+ public String getCurrencyCode() {
+ return currencyCode;
+ }
+
+ public int getTotalCount() {
+ return totalCount;
+ }
+
+ public List getPrices() {
+ return prices;
+ }
+
+ @Override
+ public String toString() {
+ return "OutboundSmsPriceResponse{" +
+ "gateway=" + gateway +
+ ", currencyCode='" + currencyCode + '\'' +
+ ", totalCount=" + totalCount +
+ ", prices=" + prices +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/VerifyRequest.java b/api/src/main/java/com/messagebird/objects/VerifyRequest.java
index 70de2591..bee684e9 100644
--- a/api/src/main/java/com/messagebird/objects/VerifyRequest.java
+++ b/api/src/main/java/com/messagebird/objects/VerifyRequest.java
@@ -15,6 +15,7 @@ public class VerifyRequest implements Serializable {
private String template;
private Integer timeout;
private Integer tokenLength;
+ private Integer maxAttempts;
private Gender voice;
private Language language;
private String subject;
@@ -124,4 +125,12 @@ public void setSubject(String subject) {
public String getSubject() {
return subject;
}
+
+ public Integer getMaxAttempts() {
+ return maxAttempts;
+ }
+
+ public void setMaxAttempts(Integer maxAttempts) {
+ this.maxAttempts = maxAttempts;
+ }
}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java
index 9ba82bd8..8b4fe164 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java
@@ -1,9 +1,16 @@
package com.messagebird.objects.conversations;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
/**
* ConversationContent wraps actual content. The field that should be set here
* is indicated by ConversationContentType.
+ *
+ *
Unknown keys are ignored on deserialization so that consumers parsing
+ * webhook payloads in their own handlers are not broken by content fields added
+ * after their SDK version. Serialization is unaffected.
*/
+@JsonIgnoreProperties(ignoreUnknown = true)
public class ConversationContent {
private ConversationContentMedia audio;
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java
index 2be027be..1dea8280 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java
@@ -22,6 +22,7 @@ public class ConversationMessage {
private Date updatedDatetime;
private Map source;
private ConversationMessageTag tag;
+ private ConversationMessageMetadata metadata;
/**
* See: {@link ConversationPlatformConstants}
*/
@@ -115,6 +116,14 @@ public void setTag(ConversationMessageTag tag) {
this.tag = tag;
}
+ public ConversationMessageMetadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(ConversationMessageMetadata metadata) {
+ this.metadata = metadata;
+ }
+
public String getPlatform() {
return platform;
}
@@ -146,6 +155,7 @@ public String toString() {
", updatedDatetime=" + updatedDatetime +
", source=" + source +
", tag=" + tag +
+ ", metadata=" + metadata +
", platform='" + platform + '\'' +
'}';
}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java
new file mode 100644
index 00000000..c41e814b
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java
@@ -0,0 +1,42 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.util.Date;
+
+/**
+ * Inner metadata attached to a conversation message. Present on both incoming
+ * messages and status webhook payloads. {@code sender.userId} always contains
+ * the BSUID when Meta provides one. When both identifiers exist, the phone
+ * number appears in the parent {@code from} field, not in this object.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationMessageMetadata {
+
+ private ConversationSenderMetadata sender;
+ private Date receivedAt;
+
+ public ConversationSenderMetadata getSender() {
+ return sender;
+ }
+
+ public void setSender(ConversationSenderMetadata sender) {
+ this.sender = sender;
+ }
+
+ public Date getReceivedAt() {
+ return receivedAt;
+ }
+
+ public void setReceivedAt(Date receivedAt) {
+ this.receivedAt = receivedAt;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationMessageMetadata{" +
+ "sender=" + sender +
+ ", receivedAt=" + receivedAt +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java
index 475c7b74..aee5c5f5 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java
@@ -15,7 +15,25 @@ public enum ConversationMessageStatus {
READ("read"),
RECEIVED("received"),
SENT("sent"),
- UNSUPPORTED("unsupported");
+ UNSUPPORTED("unsupported"),
+ ACCEPTED("accepted"),
+ REJECTED("rejected"),
+ UNKNOWN("unknown"),
+ //WA specific statuses
+ TRANSMITTED("transmitted"),
+ //SMS specific statuses
+ DELIVERY_FAILED("delivery_failed"),
+ BUFFERED("buffered"),
+ EXPIRED("expired"),
+ //Email specific statuses
+ CLICKED("clicked"),
+ OPENED("opened"),
+ BOUNCE("bounce"),
+ SPAM_COMPLAINT("spam_complaint"),
+ OUT_OF_BOUNDED("out_of_bounded"),
+ DELAYED("delayed"),
+ LIST_UNSUBSCRIBE("list_unsubscribe"),
+ DISPATCHED("dispatched");
private final String status;
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java
new file mode 100644
index 00000000..7537da85
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java
@@ -0,0 +1,50 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Identifies the recipient of an outbound WhatsApp message, as reported back on
+ * status webhook payloads under {@code status.metadata.recipient}. Mirrors
+ * {@link ConversationSenderMetadata} on the inbound side.
+ *
+ *
{@code userId} is the recipient's BSUID (e.g. "US.13491208655302741918");
+ * {@code parentUserId} is the parent business-scoped user ID of the enterprise
+ * that owns the business portfolio it was scoped against (e.g.
+ * "US.ENT.11815799212886844830").
+ *
+ *
Either field may be {@code null}: Meta only supplies them for accounts
+ * enrolled in the BSUID rollout, and the enclosing {@code recipient} object is
+ * omitted entirely when neither is present. This is the only place a status
+ * payload carries the recipient's own identity — {@code messageMetadata.to} is
+ * an echo of the address the message was addressed to.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationRecipientMetadata {
+
+ private String userId;
+ private String parentUserId;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getParentUserId() {
+ return parentUserId;
+ }
+
+ public void setParentUserId(String parentUserId) {
+ this.parentUserId = parentUserId;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationRecipientMetadata{" +
+ "userId='" + userId + '\'' +
+ ", parentUserId='" + parentUserId + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java
new file mode 100644
index 00000000..b7fdcbb0
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java
@@ -0,0 +1,66 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Metadata about the sender of a WhatsApp message. {@code userId} always
+ * contains the BSUID (e.g. "US.13491208655302741918") when Meta supplies one.
+ * When both a phone number and a BSUID are available, the phone number appears
+ * in the parent message's {@code from} field — not here.
+ *
+ *
{@code parentUserId} carries the sender's parent business-scoped user ID
+ * (e.g. "US.ENT.11815799212886844830"), which identifies the enterprise that
+ * owns the business portfolio the {@code userId} was scoped against. It is only
+ * present for accounts enrolled in Meta's parent-BSUID rollout; for everyone
+ * else it stays {@code null}.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationSenderMetadata {
+
+ private String userId;
+ private String parentUserId;
+ private String username;
+ private String displayName;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getParentUserId() {
+ return parentUserId;
+ }
+
+ public void setParentUserId(String parentUserId) {
+ this.parentUserId = parentUserId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationSenderMetadata{" +
+ "userId='" + userId + '\'' +
+ ", parentUserId='" + parentUserId + '\'' +
+ ", username='" + username + '\'' +
+ ", displayName='" + displayName + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java
new file mode 100644
index 00000000..47a4a8d1
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java
@@ -0,0 +1,92 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@code messageMetadata} block delivered inside status webhook payloads
+ * (e.g. {@code statusSent}, {@code statusDelivered}). Reflects the original
+ * message that triggered the status update.
+ *
+ *
This class is not produced by any SDK request — it is a standalone POJO
+ * intended for consumers who deserialize incoming webhook payloads in their
+ * own HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}.
+ *
+ *
Both {@code from} and {@code to} accept either a phone number or a
+ * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918").
+ * The BSUID is also available via {@code metadata.sender.userId}.
+ *
+ *
{@code to} echoes back the address the message was originally addressed
+ * to, so it is not a reliable source of the recipient's BSUID. That identity
+ * lives alongside this block, under {@code status.metadata.recipient} — see
+ * {@link ConversationStatusMetadata}.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationStatusMessageMetadata {
+
+ private String id;
+ private String from;
+ private String to;
+ private String type;
+ private ConversationContent content;
+ private ConversationMessageMetadata metadata;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ public String getTo() {
+ return to;
+ }
+
+ public void setTo(String to) {
+ this.to = to;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public ConversationContent getContent() {
+ return content;
+ }
+
+ public void setContent(ConversationContent content) {
+ this.content = content;
+ }
+
+ public ConversationMessageMetadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(ConversationMessageMetadata metadata) {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationStatusMessageMetadata{" +
+ "id='" + id + '\'' +
+ ", from='" + from + '\'' +
+ ", to='" + to + '\'' +
+ ", type='" + type + '\'' +
+ ", content=" + content +
+ ", metadata=" + metadata +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java
new file mode 100644
index 00000000..0b4d5f7f
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java
@@ -0,0 +1,85 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * The {@code status.metadata} block delivered inside status webhook payloads
+ * (e.g. {@code statusSent}, {@code statusDelivered}).
+ *
+ *
This class is not produced by any SDK request — it is a standalone POJO
+ * intended for consumers who deserialize incoming webhook payloads in their own
+ * HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}.
+ *
+ *
Note the mixed casing of this object. {@code pricing} and
+ * {@code conversation} are near-verbatim passthroughs of Meta's own objects and
+ * so keep their snake_case keys ({@code pricing_model}, {@code category}, …);
+ * they are exposed here as raw maps rather than modelled types, because their
+ * contents track Meta's schema rather than ours. {@code recipient} is ours and
+ * follows the camelCase convention used everywhere else in the API. Any other
+ * key present on the payload — for example {@code biz_opaque_callback_data} —
+ * is collected into {@link #getAdditionalProperties()} rather than dropped.
+ *
+ *
{@code recipient} is absent from payloads for accounts that never receive
+ * BSUIDs, in which case {@link #getRecipient()} returns {@code null}.
+ */
+public class ConversationStatusMetadata {
+
+ private Map pricing;
+ private Map conversation;
+ private ConversationRecipientMetadata recipient;
+ private final Map additionalProperties = new LinkedHashMap<>();
+
+ public Map getPricing() {
+ return pricing;
+ }
+
+ public void setPricing(Map pricing) {
+ this.pricing = pricing;
+ }
+
+ public Map getConversation() {
+ return conversation;
+ }
+
+ public void setConversation(Map conversation) {
+ this.conversation = conversation;
+ }
+
+ public ConversationRecipientMetadata getRecipient() {
+ return recipient;
+ }
+
+ public void setRecipient(ConversationRecipientMetadata recipient) {
+ this.recipient = recipient;
+ }
+
+ /**
+ * Every key on the payload that has no dedicated accessor above, in the
+ * order it was encountered. Empty when the payload holds nothing else.
+ *
+ * @return the unmodelled remainder of the metadata object
+ */
+ @JsonAnyGetter
+ public Map getAdditionalProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setAdditionalProperty(String name, Object value) {
+ additionalProperties.put(name, value);
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationStatusMetadata{" +
+ "pricing=" + pricing +
+ ", conversation=" + conversation +
+ ", recipient=" + recipient +
+ ", additionalProperties=" + additionalProperties +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java b/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java
index 9dd25b3a..d3a7c20b 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java
@@ -8,6 +8,9 @@ public class MessageComponent {
private String sub_type;
private int index;
private List parameters;
+ private int card_index;
+ private List cards;
+ private List components;
public void setType(MessageComponentType type) {
this.type = type;
@@ -41,13 +44,39 @@ public void setParameters(List parameters) {
this.parameters = parameters;
}
+ public void setCards(List cards) {
+ this.cards = cards;
+ }
+
+ public List getCards() {
+ return cards;
+ }
+
+ public int getCard_index() {
+ return card_index;
+ }
+
+ public void setCard_index(int card_index) {
+ this.card_index = card_index;
+ }
+
+ public List getComponents() {
+ return components;
+ }
+
+ public void setComponents(List components) {
+ this.components = components;
+ }
+
@Override
public String toString() {
return "MessageComponent{" +
"type='" + type + '\'' +
", sub_type='" + sub_type + '\'' +
- ", index=" + index +
- ", parameters=" + parameters +
+ ", index=" + index + '\'' +
+ ", parameters=" + parameters + '\'' +
+ ", components=" + components + '\'' +
+ ", cards=" + cards +
'}';
}
}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
index 327525fd..9f9290cf 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
@@ -3,13 +3,28 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.*;
+
public enum MessageComponentType {
HEADER("header"),
BODY("body"),
FOOTER("footer"),
- BUTTON("button");
-
+ BUTTON("button"),
+ CARD("card"),
+ CAROUSEL("carousel"),
+ LIMITED_TIME_OFFER("limited_time_offer"),
+ COPY_CODE("copy_code");
+
+ private static final Map TYPE_MAP;
+
+ static {
+ Map map = new HashMap<>();
+ for (MessageComponentType componentType : MessageComponentType.values()) {
+ map.put(componentType.getType().toLowerCase(), componentType);
+ }
+ TYPE_MAP = Collections.unmodifiableMap(map);
+ }
private final String type;
@@ -19,13 +34,8 @@ public enum MessageComponentType {
@JsonCreator
public static MessageComponentType forValue(String value) {
- for (MessageComponentType componentType: MessageComponentType.values()) {
- if (componentType.getType().equals(value)) {
- return componentType;
- }
- }
-
- return null;
+ Objects.requireNonNull(value, "Value cannot be null");
+ return TYPE_MAP.get(value.toLowerCase(Locale.ROOT));
}
@JsonValue
diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
index 8de37ab7..6432369f 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
@@ -1,5 +1,8 @@
package com.messagebird.objects.conversations;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+
public class MessageParam {
private TemplateMediaType type;
@@ -9,6 +12,11 @@ public class MessageParam {
private String dateTime;
private Media document;
private Media image;
+ private Media video;
+ @JsonProperty("expiration_time")
+ private String expirationTime;
+ @JsonProperty("coupon_code")
+ private String couponCode;
public TemplateMediaType getType() {
return type;
@@ -23,6 +31,9 @@ public String getText() {
}
public void setText(String text) {
+ if (StringUtils.isBlank(text)) {
+ throw new IllegalArgumentException("Text cannot be null or empty");
+ }
this.text = text;
}
@@ -47,6 +58,9 @@ public String getDateTime() {
}
public void setDateTime(String dateTime) {
+ if (StringUtils.isBlank(dateTime)) {
+ throw new IllegalArgumentException("dateTime cannot be null or empty");
+ }
this.dateTime = dateTime;
}
@@ -66,16 +80,46 @@ public void setImage(Media image) {
this.image = image;
}
+ public Media getVideo() { return video; }
+
+ public void setVideo(Media video) { this.video = video; }
+
+ public String getExpirationTime() {
+ return expirationTime;
+ }
+
+ public void setExpirationTime(String expirationTime) {
+ if (StringUtils.isBlank(expirationTime)) {
+ throw new IllegalArgumentException("expirationTime cannot be null or empty");
+ }
+ this.expirationTime = expirationTime;
+ }
+
+ public String getCouponCode() {
+ return couponCode;
+ }
+
+ public void setCouponCode(String couponCode) {
+ if (StringUtils.isBlank(couponCode)) {
+ throw new IllegalArgumentException("couponCode cannot be null or empty");
+ }
+ this.couponCode = couponCode;
+ }
+
@Override
public String toString() {
- return "MessageParam{" +
- "type=" + type +
- ", text='" + text + '\'' +
- ", payload='" + payload + '\'' +
- ", currency=" + currency +
- ", dateTime='" + dateTime + '\'' +
- ", document=" + document +
- ", image=" + image +
- '}';
+ StringBuilder sb = new StringBuilder("MessageParam{");
+ sb.append("type=").append(type)
+ .append(", text='").append(text).append('\'')
+ .append(", payload='").append(payload).append('\'')
+ .append(", currency=").append(currency)
+ .append(", dateTime='").append(dateTime).append('\'')
+ .append(", document=").append(document)
+ .append(", image=").append(image)
+ .append(", video=").append(video)
+ .append(", expirationTime='").append(expirationTime).append('\'')
+ .append(", couponCode='").append(couponCode).append('\'')
+ .append('}');
+ return sb.toString();
}
}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
index 51afa594..580dcc97 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
@@ -2,15 +2,32 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Collections;
public enum TemplateMediaType {
IMAGE("image"),
DOCUMENT("document"),
+ VIDEO("video"),
TEXT("text"),
CURRENCY("currency"),
DATETIME("date_time"),
- PAYLOAD("payload");
+ PAYLOAD("payload"),
+ EXPIRATION_TIME("expiration_time"),
+ COUPON_CODE("coupon_code");
+
+ private static final Map TYPE_MAP;
+
+ static {
+ Map map = new HashMap<>();
+ for (TemplateMediaType templateMediaType : TemplateMediaType.values()) {
+ map.put(templateMediaType.getType().toLowerCase(), templateMediaType);
+ }
+ TYPE_MAP = Collections.unmodifiableMap(map);
+ }
+
private final String type;
@@ -20,13 +37,10 @@ public enum TemplateMediaType {
@JsonCreator
public static TemplateMediaType forValue(String value) {
- for (TemplateMediaType templateMediaType: TemplateMediaType.values()) {
- if (templateMediaType.getType().equals(value)) {
- return templateMediaType;
- }
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null");
}
-
- return null;
+ return TYPE_MAP.get(value.toLowerCase());
}
@JsonValue
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java b/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java
index 15f4d31d..3edb1db9 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java
@@ -11,16 +11,9 @@
*/
public enum HSMCategory {
- ACCOUNT_UPDATE("ACCOUNT_UPDATE"),
- PAYMENT_UPDATE("PAYMENT_UPDATE"),
- PERSONAL_FINANCE_UPDATE("PERSONAL_FINANCE_UPDATE"),
- SHIPPING_UPDATE("SHIPPING_UPDATE"),
- RESERVATION_UPDATE("RESERVATION_UPDATE"),
- ISSUE_RESOLUTION("ISSUE_RESOLUTION"),
- APPOINTMENT_UPDATE("APPOINTMENT_UPDATE"),
- TRANSPORTATION_UPDATE("TRANSPORTATION_UPDATE"),
- TICKET_UPDATE("TICKET_UPDATE"),
- ALERT_UPDATE("ALERT_UPDATE");
+ AUTHENTICATION("AUTHENTICATION"),
+ UTILITY("UTILITY"),
+ MARKETING("MARKETING");
private final String category;
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java
index 51b7efba..ed93847f 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java
@@ -1,5 +1,8 @@
package com.messagebird.objects.integrations;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+
import java.util.List;
/**
@@ -13,7 +16,16 @@ public class HSMComponent {
private HSMComponentType type;
private HSMComponentFormat format;
private String text;
- private List buttons;
+ @JsonProperty("add_security_recommendation")
+ private Boolean addSecurityRecommendation;
+ @JsonProperty("code_expiration_minutes")
+ private Integer codeExpirationMinutes;
+ private List buttons;
+ @JsonProperty("has_expiration")
+ private Boolean hasExpiration;
+
+ private List cards;
+
private HSMExample example;
public HSMComponentType getType() {
@@ -37,6 +49,9 @@ public String getText() {
}
public void setText(String text) {
+ if (StringUtils.isBlank(text)) {
+ throw new IllegalArgumentException("Text cannot be null or empty");
+ }
this.text = text;
}
@@ -48,6 +63,14 @@ public void setButtons(List buttons) {
this.buttons = buttons;
}
+ public List getCards() {
+ return cards;
+ }
+
+ public void setCards(List cards) {
+ this.cards = cards;
+ }
+
public HSMExample getExample() {
return example;
}
@@ -56,15 +79,44 @@ public void setExample(HSMExample example) {
this.example = example;
}
+ public Boolean getAddSecurityRecommendation() {
+ return addSecurityRecommendation;
+ }
+
+ public void setAddSecurityRecommendation(Boolean addSecurityRecommendation) {
+ this.addSecurityRecommendation = addSecurityRecommendation;
+ }
+
+ public Integer getCodeExpirationMinutes() {
+ return codeExpirationMinutes;
+ }
+
+ public void setCodeExpirationMinutes(Integer codeExpirationMinutes) {
+ this.codeExpirationMinutes = codeExpirationMinutes;
+ }
+
+ public Boolean getHasExpiration() {
+ return hasExpiration;
+ }
+
+ public void setHasExpiration(Boolean hasExpiration) {
+ this.hasExpiration = hasExpiration;
+ }
+
@Override
public String toString() {
- return "HSMComponent{" +
- "type='" + type + '\'' +
- ", format='" + format + '\'' +
- ", text='" + text + '\'' +
- ", buttons=" + buttons +
- ", example=" + example +
- '}';
+ StringBuilder sb = new StringBuilder("HSMComponent{");
+ sb.append("type=").append(type)
+ .append(", format=").append(format)
+ .append(", text='").append(text).append('\'')
+ .append(", addSecurityRecommendation=").append(addSecurityRecommendation)
+ .append(", codeExpirationMinutes=").append(codeExpirationMinutes)
+ .append(", buttons=").append(buttons)
+ .append(", hasExpiration=").append(hasExpiration)
+ .append(", cards=").append(cards)
+ .append(", example=").append(example)
+ .append('}');
+ return sb.toString();
}
/**
@@ -73,8 +125,12 @@ public String toString() {
* @throws IllegalArgumentException Occurs when validation is not passed.
*/
public void validateComponent() throws IllegalArgumentException {
- this.validateButtons();
- this.validateComponentExample();
+ try {
+ this.validateButtons();
+ this.validateComponentExample();
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Component validation failed: " + e.getMessage(), e);
+ }
}
/**
@@ -121,9 +177,7 @@ private void validateComponentExample() throws IllegalArgumentException {
* @throws IllegalArgumentException Occurs when type is not {@code HEADER} and format is not {@code TEXT}.
*/
private void checkHeaderText() throws IllegalArgumentException {
- if (!(type.equals(HSMComponentType.HEADER)
- && format.equals(HSMComponentFormat.TEXT))
- ) {
+ if (!(HSMComponentType.HEADER.equals(type) && HSMComponentFormat.TEXT.equals(format))) {
throw new IllegalArgumentException("\"header_text\" is available for only HEADER type and TEXT format.");
}
}
@@ -134,10 +188,9 @@ private void checkHeaderText() throws IllegalArgumentException {
* @throws IllegalArgumentException Occurs when type is not {@code HEADER} and format is not {@code IMAGE}.
*/
private void checkHeaderUrl() throws IllegalArgumentException {
- if (!(type.equals(HSMComponentType.HEADER)
- && format.equals(HSMComponentFormat.IMAGE))
- ) {
- throw new IllegalArgumentException("\"header_url\" is available for only HEADER type and IMAGE format.");
+ if (!(HSMComponentType.HEADER.equals(type) &&
+ (HSMComponentFormat.IMAGE.equals(format) || HSMComponentFormat.VIDEO.equals(format) || HSMComponentFormat.DOCUMENT.equals(format)))) {
+ throw new IllegalArgumentException("\"header_url\" is available for only HEADER type and IMAGE, VIDEO, or DOCUMENT formats.");
}
}
}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButton.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButton.java
index 6438f79e..75204df0 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButton.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButton.java
@@ -1,5 +1,7 @@
package com.messagebird.objects.integrations;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
import java.util.List;
/**
@@ -10,79 +12,121 @@
*/
public class HSMComponentButton {
- private HSMComponentButtonType type;
- private String text;
- private String url;
- private String phone_number;
- private List example;
-
- public HSMComponentButtonType getType() {
- return type;
- }
-
- public void setType(HSMComponentButtonType type) {
- this.type = type;
- }
-
- public String getText() {
- return text;
- }
-
- public void setText(String text) {
- this.text = text;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- public String getPhone_number() {
- return phone_number;
- }
-
- public void setPhone_number(String phone_number) {
- this.phone_number = phone_number;
- }
-
- public List getExample() {
- return example;
- }
-
- public void setExample(List example) {
- this.example = example;
- }
-
- @Override
- public String toString() {
- return "HSMComponentButton{" +
- "type=" + type +
- ", text='" + text + '\'' +
- ", url='" + url + '\'' +
- ", phone_number='" + phone_number + '\'' +
- ", example=" + example +
- '}';
- }
-
- /**
- * Check if example field is able to use.
- *
- * @throws IllegalArgumentException Occurs if button type is not {@code URL} or {@code QUICK_REPLY}.
- */
- public void validateButtonExample() throws IllegalArgumentException {
- final boolean isExampleEmpty = this.example == null || this.example.isEmpty();
- final boolean isNotProperType = !(this.type.equals(HSMComponentButtonType.URL)
- || this.type.equals(HSMComponentButtonType.QUICK_REPLY));
-
- if (isExampleEmpty) {
- return;
- }
-
- if (isNotProperType) {
- throw new IllegalArgumentException("An example field in HSMComponentButton is available for only URL or QUICK_REPLY button types.");
- }
- }
+ private HSMComponentButtonType type;
+ private String text;
+ private String url;
+ private String phone_number;
+ private List example;
+
+ //fields used by the authentification template
+ @JsonProperty("otp_type")
+ private HSMOTPButtonType otpType;
+ @JsonProperty("autofill_text")
+ private String autofillText;
+ @JsonProperty("package_name")
+ private String packageName;
+ @JsonProperty("signature_hash")
+ private String signatureHash;
+
+ public HSMComponentButtonType getType() {
+ return type;
+ }
+
+ public void setType(HSMComponentButtonType type) {
+ this.type = type;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getPhone_number() {
+ return phone_number;
+ }
+
+ public void setPhone_number(String phone_number) {
+ this.phone_number = phone_number;
+ }
+
+ public List getExample() {
+ return example;
+ }
+
+ public void setExample(List example) {
+ this.example = example;
+ }
+ public HSMOTPButtonType getOtpType() {
+ return otpType;
+ }
+
+ public void setOtpType(HSMOTPButtonType otpType) {
+ this.otpType = otpType;
+ }
+
+ public String getAutofillText() {
+ return autofillText;
+ }
+
+ public void setAutofillText(String autofillText) {
+ this.autofillText = autofillText;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public String getSignatureHash() {
+ return signatureHash;
+ }
+
+ public void setSignatureHash(String signatureHash) {
+ this.signatureHash = signatureHash;
+ }
+ @Override
+ public String toString() {
+ return "HSMComponentButton{" +
+ "type=" + type +
+ ", text='" + text + '\'' +
+ ", url='" + url + '\'' +
+ ", phone_number='" + phone_number + '\'' +
+ ", example=" + example +
+ '}';
+ }
+
+ /**
+ * Check if example field is able to use.
+ *
+ * @throws IllegalArgumentException Occurs if button type is not {@code URL} or {@code QUICK_REPLY}.
+ */
+ public void validateButtonExample() throws IllegalArgumentException {
+ final boolean isExampleEmpty = this.example == null || this.example.isEmpty();
+ final boolean isNotProperType = !(this.type.equals(HSMComponentButtonType.URL)
+ || this.type.equals(HSMComponentButtonType.QUICK_REPLY)
+ || this.type.equals(HSMComponentButtonType.COPY_CODE)
+ );
+
+ if (isExampleEmpty) {
+ return;
+ }
+
+ if (isNotProperType) {
+ throw new IllegalArgumentException("An example field in HSMComponentButton is available for only URL or QUICK_REPLY button types.");
+ }
+ }
}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButtonType.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButtonType.java
index 937f6baf..06e6eb91 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButtonType.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButtonType.java
@@ -13,7 +13,9 @@ public enum HSMComponentButtonType {
PHONE_NUMBER("PHONE_NUMBER"),
URL("URL"),
- QUICK_REPLY("QUICK_REPLY");
+ QUICK_REPLY("QUICK_REPLY"),
+ OTP("OTP"),
+ COPY_CODE("COPY_CODE");
private final String type;
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentCard.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentCard.java
new file mode 100644
index 00000000..23411941
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentCard.java
@@ -0,0 +1,21 @@
+package com.messagebird.objects.integrations;
+
+import java.util.List;
+
+/**
+ * HSMComponentCard
+ *
+ * @author AlexL-mb
+ * @see HSMComponentCard
+ */
+public class HSMComponentCard {
+ private List components;
+
+ public List getComponents() {
+ return components;
+ }
+
+ public void setComponents(List components) {
+ this.components = components;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java
index 0acda414..8610146a 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java
@@ -2,6 +2,11 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Objects;
/**
* An enum for HSMComponentType
@@ -12,7 +17,19 @@ public enum HSMComponentType {
BODY("BODY"),
HEADER("HEADER"),
FOOTER("FOOTER"),
- BUTTONS("BUTTONS");
+ BUTTONS("BUTTONS"),
+ CAROUSEL("CAROUSEL"),
+ LIMITED_TIME_OFFER("LIMITED_TIME_OFFER");
+
+ private static final Map TYPE_MAP;
+
+ static {
+ Map map = new HashMap<>();
+ for (HSMComponentType hsmComponentType : HSMComponentType.values()) {
+ map.put(hsmComponentType.getType().toLowerCase(), hsmComponentType);
+ }
+ TYPE_MAP = Collections.unmodifiableMap(map);
+ }
private final String type;
@@ -22,13 +39,8 @@ public enum HSMComponentType {
@JsonCreator
public static HSMComponentType forValue(String value) {
- for (HSMComponentType hsmComponentType : HSMComponentType.values()) {
- if (hsmComponentType.getType().equals(value)) {
- return hsmComponentType;
- }
- }
-
- return null;
+ Objects.requireNonNull(value, "Value cannot be null");
+ return TYPE_MAP.get(value.toLowerCase(Locale.ROOT));
}
@JsonValue
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMOTPButtonType.java b/api/src/main/java/com/messagebird/objects/integrations/HSMOTPButtonType.java
new file mode 100644
index 00000000..499ef1f3
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMOTPButtonType.java
@@ -0,0 +1,38 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+public enum HSMOTPButtonType {
+ ONE_TAP("ONE_TAP"),
+ COPY_CODE("COPY_CODE");
+
+ private final String type;
+
+ HSMOTPButtonType(String type) {
+ this.type = type;
+ }
+ @JsonCreator
+ public static HSMOTPButtonType forValue(String value) {
+ for (HSMOTPButtonType OTPButtonType : HSMOTPButtonType.values()) {
+ if (OTPButtonType.getType().equals(value)) {
+ return OTPButtonType;
+ }
+ }
+ return null;
+ }
+
+ @JsonValue
+ public String toJson() {
+ return getType();
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ return getType();
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMQualityScore.java b/api/src/main/java/com/messagebird/objects/integrations/HSMQualityScore.java
new file mode 100644
index 00000000..112492fd
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMQualityScore.java
@@ -0,0 +1,42 @@
+package com.messagebird.objects.integrations;
+
+import java.util.List;
+
+public class HSMQualityScore {
+ private String score;
+ private long date;
+ private List reasons;
+
+ public String getScore() {
+ return score;
+ }
+
+ public void setScore(String score) {
+ this.score = score;
+ }
+
+ public long getDate() {
+ return date;
+ }
+
+ public void setDate(long date) {
+ this.date = date;
+ }
+
+ public List getReasons() {
+ return reasons;
+ }
+
+ public void setReasons(List reasons) {
+ this.reasons = reasons;
+ }
+
+ @Override
+ public String toString() {
+ return "HSMQualityScore{" +
+ "score='" + score + '\'' +
+ ", date=" + date +
+ ", reasons=" + reasons +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMStatus.java b/api/src/main/java/com/messagebird/objects/integrations/HSMStatus.java
index fa179b0f..1b1980f1 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/HSMStatus.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMStatus.java
@@ -16,7 +16,9 @@ public enum HSMStatus {
PENDING("PENDING"),
REJECTED("REJECTED"),
PENDING_DELETION("PENDING_DELETION"),
- DELETED("DELETED");
+ DELETED("DELETED"),
+ DISABLED("DISABLED"),
+ PAUSED("PAUSED");
private final String status;
diff --git a/api/src/main/java/com/messagebird/objects/integrations/Template.java b/api/src/main/java/com/messagebird/objects/integrations/Template.java
index 715aeb48..f6a83e93 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/Template.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/Template.java
@@ -12,18 +12,23 @@ public class Template {
private String name;
private String language;
+ private String wabaID;
private List components;
private HSMCategory category;
+ private boolean ctaURLLinkTrackingOptedOut;
public Template() {
}
- public Template(String name, String language,
- List components, HSMCategory category) {
+
+ public Template(String name, String language, String wabaID,
+ List components, HSMCategory category, boolean ctaURLLinkTrackingOptedOut) {
this.name = name;
this.language = language;
+ this.wabaID = wabaID;
this.components = components;
this.category = category;
+ this.ctaURLLinkTrackingOptedOut = ctaURLLinkTrackingOptedOut;
}
public String getName() {
@@ -42,6 +47,14 @@ public void setLanguage(String language) {
this.language = language;
}
+ public String getWABAID() {
+ return wabaID;
+ }
+
+ public void setWABAID(String wabaID) {
+ this.wabaID = wabaID;
+ }
+
public List getComponents() {
return components;
}
@@ -58,13 +71,23 @@ public void setCategory(HSMCategory category) {
this.category = category;
}
+ public void setCtaURLLinkTrackingOptedOut (boolean ctaURLLinkTrackingOptedOut) {
+ this.ctaURLLinkTrackingOptedOut = ctaURLLinkTrackingOptedOut;
+ }
+
+ public boolean getCtaURLLinkTrackingOptedOut () {
+ return ctaURLLinkTrackingOptedOut;
+ }
+
@Override
public String toString() {
return "WhatsAppTemplate{" +
"name='" + name + '\'' +
", language='" + language + '\'' +
+ ", wabaID='" + wabaID + '\'' +
", components=" + components +
", category='" + category + '\'' +
+ ", ctaURLLinkTrackingOptedOut='" + ctaURLLinkTrackingOptedOut + '\'' +
'}';
}
@@ -77,6 +100,7 @@ public void validate() throws IllegalArgumentException {
this.validateComponents();
this.validateName();
this.validateLanguage();
+ this.validateWABAID();
this.validateCategory();
}
@@ -123,6 +147,19 @@ private void validateLanguage() {
}
}
+ /**
+ * Check if wabaID field is valid.
+ *
+ * @throws IllegalArgumentException If wabaID field is null or empty string.
+ */
+ private void validateWABAID() {
+ if (this.wabaID == null) {
+ throw new IllegalArgumentException("A \"wabaID\" field is required.");
+ } else if (this.wabaID.length() == 0) {
+ throw new IllegalArgumentException("A \"wabaID\" field can not be an empty string.");
+ }
+ }
+
/**
* Check if category field is valid.
*
diff --git a/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java b/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java
index bce35f82..3c75fabe 100644
--- a/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java
+++ b/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java
@@ -18,6 +18,13 @@ public class TemplateResponse implements Serializable {
private List components;
private HSMStatus status;
private String rejectedReason;
+ private String wabaID;
+ private String namespace;
+
+ private boolean ctaURLLinkTrackingOptedOut;
+
+ private HSMQualityScore qualityScore;
+
private Date createdAt;
private Date updatedAt;
@@ -73,6 +80,22 @@ public void setRejectedReason(String rejectedReason) {
this.rejectedReason = rejectedReason;
}
+ public String getWabaID() {
+ return wabaID;
+ }
+
+ public void setWabaID(String wabaID) {
+ this.wabaID = wabaID;
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
public Date getCreatedAt() {
return createdAt;
}
@@ -89,6 +112,22 @@ public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
+ public boolean isCtaURLLinkTrackingOptedOut() {
+ return ctaURLLinkTrackingOptedOut;
+ }
+
+ public void setCtaURLLinkTrackingOptedOut(boolean ctaURLLinkTrackingOptedOut) {
+ this.ctaURLLinkTrackingOptedOut = ctaURLLinkTrackingOptedOut;
+ }
+
+ public HSMQualityScore getQualityScore() {
+ return qualityScore;
+ }
+
+ public void setQualityScore(HSMQualityScore qualityScore) {
+ this.qualityScore = qualityScore;
+ }
+
@Override
public String toString() {
return "WhatsAppTemplateResponse{" +
@@ -98,8 +137,13 @@ public String toString() {
", components=" + components +
", status='" + status + '\'' +
", rejectedReason='" + rejectedReason + '\'' +
+ ", wabaID='" + wabaID + '\'' +
+ ", namespace='" + namespace + '\'' +
+ ", ctaURLLinkTrackingOptedOut='" + ctaURLLinkTrackingOptedOut + '\'' +
+ ", qualityScore='" + qualityScore + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
+
}
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java
index 8ece36ec..573b42be 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java
@@ -63,10 +63,12 @@ public Webhook getWebhook() {
return webhook;
}
+ @JsonIgnore
public void setWebhook(String url) {
this.setWebhook(url, null);
}
+ @JsonIgnore
public void setWebhook(String url, String token) {
this.webhook.setUrl(url);
this.webhook.setToken(token);
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java
index d64f0d2c..53130c77 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java
@@ -23,6 +23,9 @@ public class VoiceCallFlow implements Serializable {
@JsonProperty("default")
private boolean defaultCall;
+ @JsonProperty("maxDuration")
+ private Integer maxDuration;
+
private Date createdAt;
private Date updatedAt;
@@ -37,10 +40,12 @@ public void setId(String id) {
this.id = id;
}
+ @Deprecated
public String getTitle() {
return title;
}
+ @Deprecated
public void setTitle(String title) {
this.title = title;
}
@@ -69,6 +74,10 @@ public void setDefaultCall(boolean defaultCall) {
this.defaultCall = defaultCall;
}
+ public Integer getMaxDuration() { return maxDuration; }
+
+ public void setMaxDuration(Integer maxDuration) { this.maxDuration = maxDuration; }
+
public Date getCreatedAt() {
return createdAt;
}
@@ -101,6 +110,7 @@ public String toString() {
", record=" + record +
", steps=" + steps +
", default=" + defaultCall +
+ ", maxDuration=" + maxDuration +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java
index 3990cfb2..e447d9d9 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java
@@ -36,10 +36,12 @@ public void setId(String id) {
this.id = id;
}
+ @Deprecated
public String getTitle() {
return title;
}
+ @Deprecated
public void setTitle(String title) {
this.title = title;
}
diff --git a/api/src/test/java/com/messagebird/ContactTest.java b/api/src/test/java/com/messagebird/ContactTest.java
index 2377733d..5dfdea38 100644
--- a/api/src/test/java/com/messagebird/ContactTest.java
+++ b/api/src/test/java/com/messagebird/ContactTest.java
@@ -8,6 +8,7 @@
import org.mockito.Mockito;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
+import static org.junit.Assume.assumeNotNull;
import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals;
/**
@@ -30,6 +31,7 @@ public class ContactTest {
@BeforeClass
public static void setUpClass() throws UnauthorizedException, GeneralException {
String accessKey = System.getProperty("messageBirdAccessKey");
+ assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey to run", accessKey);
msisdn = generateMsisdn();
diff --git a/api/src/test/java/com/messagebird/ConversationMessagesTest.java b/api/src/test/java/com/messagebird/ConversationMessagesTest.java
index 81357f88..beac543a 100644
--- a/api/src/test/java/com/messagebird/ConversationMessagesTest.java
+++ b/api/src/test/java/com/messagebird/ConversationMessagesTest.java
@@ -1,5 +1,6 @@
package com.messagebird;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.messagebird.exceptions.GeneralException;
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
@@ -23,6 +24,19 @@ public class ConversationMessagesTest {
private static final String JSON_CONVERSATION_MESSAGE_TEXT = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}";
private static final String JSON_CONVERSATION_MESSAGE_VIDEO = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"video\",\"direction\": \"received\",\"content\": {\"video\": { \"url\": \"https://example.com/video.mp4\" } },\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}";
private static final String JSON_CONVERSATION_SEND_MESSAGE_RESPONSE = "{\"id\":\"mesid\",\"status\":\"accepted\",\"fallback\":{\"id\":\"mesid\"}}";
+ private static final String JSON_CONVERSATION_MESSAGE_BSUID = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"metadata\": {\"sender\": {\"displayName\": \"Alice\",\"username\": \"alice_shop\",\"userId\": \"US.13491208655302741918\",\"parentUserId\": \"US.ENT.11815799212886844830\"},\"receivedAt\": \"2025-04-15T16:00:00Z\"},\"createdDatetime\": \"2025-04-15T16:00:00Z\",\"updatedDatetime\": \"2025-04-15T16:00:00Z\"}";
+ private static final String JSON_STATUS_MESSAGE_METADATA = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"content\": {\"text\": \"Hello! Your order has been shipped.\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"0001-01-01T00:00:00Z\"}}";
+
+ private static final String JSON_STATUS_METADATA_WITH_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}},\"biz_opaque_callback_data\": \"order-1234\",\"recipient\": {\"userId\": \"US.13491208655302741918\",\"parentUserId\": \"US.ENT.11815799212886844830\"}}";
+ private static final String JSON_STATUS_METADATA_WITHOUT_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}}}";
+ private static final String JSON_STATUS_METADATA_PARENT_ONLY = "{\"recipient\": {\"parentUserId\": \"US.ENT.11815799212886844830\"}}";
+
+ /**
+ * The same payload as JSON_STATUS_MESSAGE_METADATA with an unrecognised key
+ * added at every nesting level, standing in for fields the platform adds
+ * after this SDK version ships.
+ */
+ private static final String JSON_STATUS_MESSAGE_METADATA_UNKNOWN_FIELDS = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"futureTopLevelField\": \"ignored\",\"content\": {\"text\": \"Hello! Your order has been shipped.\",\"futureContentField\": \"ignored\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\",\"futureSenderField\": \"ignored\"},\"receivedAt\": \"0001-01-01T00:00:00Z\",\"futureMetadataField\": \"ignored\"}}";
/**
* Epsilon to use when checking two latitudes or longitudes for equality.
@@ -183,6 +197,96 @@ public void testViewConversationMessageLocation() throws GeneralException, NotFo
assertEquals(4.911627, location.getLongitude(), EPSILON_LOCATION_EQUALITY);
}
+ @Test
+ public void testViewConversationMessageWithBsuidMetadata() throws GeneralException, NotFoundException, UnauthorizedException {
+ MessageBirdService messageBirdService = SpyService
+ .expects("GET", "messages/mesid")
+ .withConversationsAPIBaseURL()
+ .andReturns(new APIResponse(JSON_CONVERSATION_MESSAGE_BSUID));
+ MessageBirdClient messageBirdClient = new MessageBirdClient(messageBirdService);
+
+ ConversationMessage message = messageBirdClient.viewConversationMessage("mesid");
+
+ ConversationMessageMetadata metadata = message.getMetadata();
+ assertNotNull(metadata);
+ assertNotNull(metadata.getReceivedAt());
+ ConversationSenderMetadata sender = metadata.getSender();
+ assertEquals("Alice", sender.getDisplayName());
+ assertEquals("alice_shop", sender.getUsername());
+ assertEquals("US.13491208655302741918", sender.getUserId());
+ assertEquals("US.ENT.11815799212886844830", sender.getParentUserId());
+ }
+
+ @Test
+ public void testStatusMessageMetadataToleratesUnknownFields() throws Exception {
+ // A plain mapper, and unknown keys at every level: webhook payload POJOs
+ // must not force consumers to disable FAIL_ON_UNKNOWN_PROPERTIES, and
+ // must survive fields the platform adds after this version ships.
+ ConversationStatusMessageMetadata md = new ObjectMapper().readValue(
+ JSON_STATUS_MESSAGE_METADATA_UNKNOWN_FIELDS, ConversationStatusMessageMetadata.class);
+
+ assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId());
+ assertEquals("Hello! Your order has been shipped.", md.getContent().getText());
+ assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId());
+ assertNotNull(md.getMetadata().getReceivedAt());
+ }
+
+ @Test
+ public void testStatusMetadataDeserializesRecipient() throws Exception {
+ // A plain mapper: these payload POJOs must not require the caller to
+ // disable FAIL_ON_UNKNOWN_PROPERTIES.
+ ConversationStatusMetadata metadata = new ObjectMapper().readValue(
+ JSON_STATUS_METADATA_WITH_RECIPIENT, ConversationStatusMetadata.class);
+
+ ConversationRecipientMetadata recipient = metadata.getRecipient();
+ assertNotNull(recipient);
+ assertEquals("US.13491208655302741918", recipient.getUserId());
+ assertEquals("US.ENT.11815799212886844830", recipient.getParentUserId());
+
+ // Meta's own objects pass through untouched, snake_case keys and all.
+ assertEquals("CBP", metadata.getPricing().get("pricing_model"));
+ assertEquals("a1b2c3d4", metadata.getConversation().get("id"));
+
+ // Anything else on the payload is kept rather than dropped.
+ assertEquals("order-1234", metadata.getAdditionalProperties().get("biz_opaque_callback_data"));
+ }
+
+ @Test
+ public void testStatusMetadataWithoutRecipientIsNull() throws Exception {
+ ConversationStatusMetadata metadata = new ObjectMapper().readValue(
+ JSON_STATUS_METADATA_WITHOUT_RECIPIENT, ConversationStatusMetadata.class);
+
+ // Accounts that never receive BSUIDs get payloads with no recipient key
+ // at all — the rest of the metadata must still parse.
+ assertNull(metadata.getRecipient());
+ assertEquals("CBP", metadata.getPricing().get("pricing_model"));
+ assertTrue(metadata.getAdditionalProperties().isEmpty());
+ }
+
+ @Test
+ public void testStatusMetadataRecipientWithParentOnly() throws Exception {
+ ConversationStatusMetadata metadata = new ObjectMapper().readValue(
+ JSON_STATUS_METADATA_PARENT_ONLY, ConversationStatusMetadata.class);
+
+ ConversationRecipientMetadata recipient = metadata.getRecipient();
+ assertNotNull(recipient);
+ assertNull(recipient.getUserId());
+ assertEquals("US.ENT.11815799212886844830", recipient.getParentUserId());
+ }
+
+ @Test
+ public void testStatusMessageMetadataDeserializes() throws Exception {
+ ConversationStatusMessageMetadata md = new ObjectMapper().readValue(
+ JSON_STATUS_MESSAGE_METADATA, ConversationStatusMessageMetadata.class);
+
+ assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId());
+ assertEquals("15551234567", md.getFrom());
+ assertEquals("US.13491208655302741918", md.getTo());
+ assertEquals("text", md.getType());
+ assertEquals("Hello! Your order has been shipped.", md.getContent().getText());
+ assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId());
+ }
+
@Test
public void testViewConversationMessageText() throws GeneralException, NotFoundException, UnauthorizedException {
MessageBirdService messageBirdService = SpyService
diff --git a/api/src/test/java/com/messagebird/MessageBirdClientTest.java b/api/src/test/java/com/messagebird/MessageBirdClientTest.java
index 11113319..b11c2f54 100644
--- a/api/src/test/java/com/messagebird/MessageBirdClientTest.java
+++ b/api/src/test/java/com/messagebird/MessageBirdClientTest.java
@@ -16,6 +16,7 @@
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
+import static org.junit.Assume.assumeNotNull;
import java.math.BigInteger;
import java.util.Collections;
@@ -44,7 +45,10 @@ public class MessageBirdClientTest {
@BeforeClass
public static void setUpClass() {
messageBirdAccessKey = System.getProperty("messageBirdAccessKey");
- messageBirdMSISDN = new BigInteger(System.getProperty("messageBirdMSISDN"));
+ String msisdn = System.getProperty("messageBirdMSISDN");
+ assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey and -DmessageBirdMSISDN to run",
+ messageBirdAccessKey, msisdn);
+ messageBirdMSISDN = new BigInteger(msisdn);
}
@Before
@@ -427,7 +431,6 @@ public void shouldThrowIllegalArgumentExceptionWhenSourceOfVoiceCallIsMissing()
voiceCall.setDestination("ANY_DESTINATION");
final VoiceCallFlow voiceCallFlow = new VoiceCallFlow();
- voiceCallFlow.setTitle("Test title");
VoiceStep voiceStep = new VoiceStep();
voiceStep.setAction("say");
@@ -448,7 +451,6 @@ public void shouldThrowIllegalArgumentExceptionWhenDestinationOfVoiceCallIsMissi
voiceCall.setSource("ANY_SOURCE");
final VoiceCallFlow voiceCallFlow = new VoiceCallFlow();
- voiceCallFlow.setTitle("Test title");
VoiceStep voiceStep = new VoiceStep();
voiceStep.setAction("say");
@@ -1068,6 +1070,83 @@ public void testCreateWhatsAppTemplate() throws UnauthorizedException, GeneralEx
assertEquals(response.getLanguage(), templateResponse.getLanguage());
assertEquals(response.getCategory(), templateResponse.getCategory());
assertEquals(response.getStatus(), templateResponse.getStatus());
+ assertEquals(response.getWabaID(), templateResponse.getWabaID());
+ assertEquals(response.getCreatedAt(), templateResponse.getCreatedAt());
+ assertEquals(response.getUpdatedAt(), templateResponse.getUpdatedAt());
+
+ /* verify components */
+ for (int i = 0; i < response.getComponents().size(); i++) {
+ assertEquals(response.getComponents().get(i).getType(), templateResponse.getComponents().get(i).getType());
+ assertEquals(response.getComponents().get(i).getFormat(), templateResponse.getComponents().get(i).getFormat());
+ assertEquals(response.getComponents().get(i).getText(), templateResponse.getComponents().get(i).getText());
+ }
+ }
+ @Test
+ public void testCreateWhatsAppCarouselTemplate() throws UnauthorizedException, GeneralException {
+ final TemplateResponse templateResponse = TestUtil.createWhatsAppCarouselTemplateResponse("sample_template_name", "ko");
+ final Template template = TestUtil.createWhatsAppCarouselTemplate("sample_template_name", "ko");
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ when(messageBirdServiceMock.sendPayLoad(url, template, TemplateResponse.class))
+ .thenReturn(templateResponse);
+
+ final TemplateResponse response = messageBirdClientInjectMock.createWhatsAppTemplate(template);
+
+ verify(messageBirdServiceMock, times(1)).sendPayLoad(url, template, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), templateResponse.getName());
+ assertEquals(response.getLanguage(), templateResponse.getLanguage());
+ assertEquals(response.getCategory(), templateResponse.getCategory());
+ assertEquals(response.getStatus(), templateResponse.getStatus());
+ assertEquals(response.getWabaID(), templateResponse.getWabaID());
+ assertEquals(response.getCreatedAt(), templateResponse.getCreatedAt());
+ assertEquals(response.getUpdatedAt(), templateResponse.getUpdatedAt());
+
+ /* verify components */
+ for (int i = 0; i < response.getComponents().size(); i++) {
+ assertEquals(response.getComponents().get(i).getType(), templateResponse.getComponents().get(i).getType());
+ assertEquals(response.getComponents().get(i).getFormat(), templateResponse.getComponents().get(i).getFormat());
+ assertEquals(response.getComponents().get(i).getText(), templateResponse.getComponents().get(i).getText());
+ }
+ }
+
+ @Test
+ public void testUpdateWhatsAppTemplate() throws UnauthorizedException, GeneralException {
+ final TemplateResponse templateResponse = TestUtil.createWhatsAppTemplateResponse("sample_template_name", "ko");
+ final Template template = TestUtil.createWhatsAppTemplate("sample_template_name", "ko");
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s/%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ "sample_template_name",
+ "ko"
+ );
+
+ when(messageBirdServiceMock.sendPayLoad("PUT",url, template, TemplateResponse.class))
+ .thenReturn(templateResponse);
+
+ final TemplateResponse response = messageBirdClientInjectMock.updateWhatsAppTemplate(template,"sample_template_name","ko");
+ verify(messageBirdServiceMock, times(1)).sendPayLoad("PUT",url, template, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), templateResponse.getName());
+ assertEquals(response.getLanguage(), templateResponse.getLanguage());
+ assertEquals(response.getCategory(), templateResponse.getCategory());
+ assertEquals(response.getStatus(), templateResponse.getStatus());
+ assertEquals(response.getWabaID(), templateResponse.getWabaID());
assertEquals(response.getCreatedAt(), templateResponse.getCreatedAt());
assertEquals(response.getUpdatedAt(), templateResponse.getUpdatedAt());
@@ -1103,6 +1182,86 @@ public void testListWhatsAppTemplates() throws UnauthorizedException, GeneralExc
}
}
+ @Test
+ public void testListWhatsAppTemplatesDefault() throws UnauthorizedException, GeneralException {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ when(messageBirdServiceMock.requestList(url, 0, 10, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates();
+ verify(messageBirdServiceMock, times(1)).requestList(url, 0, 10, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
+ @Test
+ public void testListWhatsAppTemplatesByWABAID() throws UnauthorizedException, GeneralException {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ final String wabaID = "testWABAID";
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ Map map = new LinkedHashMap<>();
+ map.put("wabaId", wabaID);
+
+ when(messageBirdServiceMock.requestList(url, map, 0, 0, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates(0, 0, wabaID, null);
+ verify(messageBirdServiceMock, times(1)).requestList(url, map, 0, 0, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
+ @Test
+ public void testListWhatsAppTemplatesByChannelID() throws UnauthorizedException, GeneralException {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ final String channelID = "channel-id";
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ Map map = new LinkedHashMap<>();
+ map.put("channelId", channelID);
+
+ when(messageBirdServiceMock.requestList(url, map, 0, 0, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates(0, 0, null, channelID);
+ verify(messageBirdServiceMock, times(1)).requestList(url, map, 0, 0, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
@Test
public void testGetWhatsAppTemplatesBy() throws GeneralException, UnauthorizedException, NotFoundException {
final String templateName = "sample_template_name";
@@ -1134,6 +1293,80 @@ public void testGetWhatsAppTemplatesBy() throws GeneralException, UnauthorizedEx
}
}
+ @Test
+ public void testGetWhatsAppTemplatesByNameAndWABAID() throws GeneralException, UnauthorizedException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String wabaID = "testWABAID";
+ final TemplateResponse templateResponse1 = TestUtil.createWhatsAppTemplateResponse(templateName, "en_US");
+ final TemplateResponse templateResponse2 = TestUtil.createWhatsAppTemplateResponse("another_template", "en_US");
+ final List templateList = new ArrayList<>();
+ templateList.add(templateResponse1);
+ templateList.add(templateResponse2);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ String id = String.format(
+ "%s?wabaId=%s",
+ templateName,
+ wabaID
+ );
+
+ when(messageBirdServiceMock.requestByIdAsList(url, id, TemplateResponse.class))
+ .thenReturn(templateList);
+
+ final List response = messageBirdClientInjectMock.getWhatsAppTemplatesBy(templateName, wabaID, null);
+ verify(messageBirdServiceMock, times(1)).requestByIdAsList(url, id, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.size(), templateList.size());
+ for(int i = 0; i < response.size() ; i++) {
+ assertReflectionEquals(response.get(i), templateList.get(i));
+ }
+ }
+
+ @Test
+ public void testGetWhatsAppTemplatesByNameForChannelID() throws GeneralException, UnauthorizedException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String channelID = "channel-id";
+ final TemplateResponse templateResponse1 = TestUtil.createWhatsAppTemplateResponse(templateName, "en_US");
+ final TemplateResponse templateResponse2 = TestUtil.createWhatsAppTemplateResponse("another_template", "en_US");
+ final List templateList = new ArrayList<>();
+ templateList.add(templateResponse1);
+ templateList.add(templateResponse2);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ String id = String.format(
+ "%s?channelId=%s",
+ templateName,
+ channelID
+ );
+
+ when(messageBirdServiceMock.requestByIdAsList(url, id, TemplateResponse.class))
+ .thenReturn(templateList);
+
+ final List response = messageBirdClientInjectMock.getWhatsAppTemplatesBy(templateName, null, channelID);
+ verify(messageBirdServiceMock, times(1)).requestByIdAsList(url, id, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.size(), templateList.size());
+ for(int i = 0; i < response.size() ; i++) {
+ assertReflectionEquals(response.get(i), templateList.get(i));
+ }
+ }
+
@Test
public void testFetchWhatsAppTemplateBy() throws UnauthorizedException, GeneralException, NotFoundException {
final String templateName = "sample_template_name";
@@ -1165,45 +1398,86 @@ public void testFetchWhatsAppTemplateBy() throws UnauthorizedException, GeneralE
}
@Test
- public void testDeleteTemplatesByName() throws UnauthorizedException, GeneralException, NotFoundException {
+ public void testFetchWhatsAppTemplateByNameAndLanguageAndWABAID() throws UnauthorizedException, GeneralException, NotFoundException {
final String templateName = "sample_template_name";
+ final String language = "ko";
+ final String wabaID = "testWABAID";
+ final TemplateResponse template = TestUtil.createWhatsAppTemplateResponse(templateName, language);
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
String url = String.format(
- "%s%s%s/%s",
+ "%s%s%s/%s/%s?wabaId=%s",
INTEGRATIONS_BASE_URL_V2,
INTEGRATIONS_WHATSAPP_PATH,
TEMPLATES_PATH,
- templateName
+ templateName,
+ language,
+ wabaID
);
- when(messageBirdServiceMock.delete(url, null)).thenReturn(null);
- messageBirdClientInjectMock.deleteTemplatesBy(templateName);
- verify(messageBirdServiceMock).delete(url, null);
+ when(messageBirdServiceMock.request(url, TemplateResponse.class))
+ .thenReturn(template);
+
+ final TemplateResponse response = messageBirdClientInjectMock.fetchWhatsAppTemplateBy(templateName, language, wabaID, null);
+ verify(messageBirdServiceMock, times(1)).request(url, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), template.getName());
+ assertEquals(response.getLanguage(), template.getLanguage());
+ assertEquals(response.getStatus(), template.getStatus());
+ assertReflectionEquals(response.getComponents(), template.getComponents());
}
@Test
- public void testDeleteTemplatesByNameAndLanguage()
- throws UnauthorizedException, GeneralException, NotFoundException {
+ public void testFetchWhatsAppTemplateByNameAndLanguageForChannelID() throws UnauthorizedException, GeneralException, NotFoundException {
final String templateName = "sample_template_name";
- final String language = "en_US";
+ final String language = "ko";
+ final String channelID = "channel-id";
+ final TemplateResponse template = TestUtil.createWhatsAppTemplateResponse(templateName, language);
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
String url = String.format(
- "%s%s%s/%s/%s",
+ "%s%s%s/%s/%s?channelId=%s",
INTEGRATIONS_BASE_URL_V2,
INTEGRATIONS_WHATSAPP_PATH,
TEMPLATES_PATH,
templateName,
- language
+ language,
+ channelID
+ );
+
+ when(messageBirdServiceMock.request(url, TemplateResponse.class))
+ .thenReturn(template);
+
+ final TemplateResponse response = messageBirdClientInjectMock.fetchWhatsAppTemplateBy(templateName, language, null, channelID);
+ verify(messageBirdServiceMock, times(1)).request(url, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), template.getName());
+ assertEquals(response.getLanguage(), template.getLanguage());
+ assertEquals(response.getStatus(), template.getStatus());
+ assertReflectionEquals(response.getComponents(), template.getComponents());
+ }
+
+ @Test
+ public void testDeleteTemplatesByName() throws UnauthorizedException, GeneralException, NotFoundException {
+ final String templateName = "sample_template_name";
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName
);
when(messageBirdServiceMock.delete(url, null)).thenReturn(null);
- messageBirdClientInjectMock.deleteTemplatesBy(templateName, language);
+ messageBirdClientInjectMock.deleteTemplatesBy(templateName);
verify(messageBirdServiceMock).delete(url, null);
}
@@ -1322,5 +1596,25 @@ private ConversationSendRequest createDummyConversationRequest() {
return request;
}
+ @Test
+ public void testUnpauseTemplatesByTemplateName_Success() throws UnauthorizedException, GeneralException {
+ final Template template = TestUtil.createWhatsAppTemplate("sample_template_name", "ko");
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+ String url = String.format(
+ "%s%s%s%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ UNPAUSE_TEMAPLATE_PATH,
+ "sample_template_name"
+ );
+ messageBirdClientInjectMock.unpauseTemplatesByTemplateName("sample_template_name");
+ verify(messageBirdServiceMock).sendPayLoad("POST", url, "", null);
+ }
-}
\ No newline at end of file
+ @Test(expected = GeneralException.class)
+ public void testUnpauseTemplatesByTemplateName_NotFound() throws UnauthorizedException, GeneralException {
+ messageBirdClient.unpauseTemplatesByTemplateName("foo");
+ }
+}
diff --git a/api/src/test/java/com/messagebird/OutboundSmsPricesTest.java b/api/src/test/java/com/messagebird/OutboundSmsPricesTest.java
new file mode 100644
index 00000000..665dba71
--- /dev/null
+++ b/api/src/test/java/com/messagebird/OutboundSmsPricesTest.java
@@ -0,0 +1,83 @@
+package com.messagebird;
+
+import com.messagebird.exceptions.GeneralException;
+import com.messagebird.exceptions.NotFoundException;
+import com.messagebird.exceptions.UnauthorizedException;
+import com.messagebird.objects.OutboundSmsPriceResponse;
+import com.messagebird.util.Resources;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+
+public class OutboundSmsPricesTest {
+
+ @Test
+ public void testGetOutboundSmsPrices() throws GeneralException, UnauthorizedException, NotFoundException {
+ String responseFixture = Resources.readResourceText("/fixtures/outbound_sms_prices.json");
+
+ MessageBirdService messageBirdService = SpyService
+ .expects("GET", "pricing/sms/outbound")
+ .withRestAPIBaseURL()
+ .andReturns(new APIResponse(responseFixture, 200));
+ MessageBirdClient messageBirdClient = new MessageBirdClient(messageBirdService);
+
+ assertReceivedExpectedResponse(messageBirdClient.getOutboundSmsPrices());
+ }
+
+ @Test
+ public void testGetOutboundSmsPricesSmppUsername() throws GeneralException, UnauthorizedException, NotFoundException {
+ String responseFixture = Resources.readResourceText("/fixtures/outbound_sms_prices.json");
+
+ MessageBirdService messageBirdService = SpyService
+ .expects("GET", "pricing/sms/outbound/smpp/test-smpp-user")
+ .withRestAPIBaseURL()
+ .andReturns(new APIResponse(responseFixture, 200));
+ MessageBirdClient messageBirdClient = new MessageBirdClient(messageBirdService);
+
+ assertReceivedExpectedResponse(messageBirdClient.getOutboundSmsPrices("test-smpp-user"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetOutboundSmsPricesSmppUsernameNull() throws GeneralException, UnauthorizedException, NotFoundException {
+ new MessageBirdClient(mock(MessageBirdService.class)).getOutboundSmsPrices(null);
+ }
+
+ private static void assertReceivedExpectedResponse(OutboundSmsPriceResponse outboundSmsPriceResponse) {
+ assertEquals(10, outboundSmsPriceResponse.getGateway());
+ assertEquals("EUR", outboundSmsPriceResponse.getCurrencyCode());
+ assertEquals(3, outboundSmsPriceResponse.getTotalCount());
+
+ assertEquals(3, outboundSmsPriceResponse.getPrices().size());
+
+ assertEquals(new BigDecimal("0.060000"), outboundSmsPriceResponse.getPrices().get(0).getPrice());
+ assertEquals("EUR", outboundSmsPriceResponse.getPrices().get(0).getCurrencyCode());
+ assertEquals("0", outboundSmsPriceResponse.getPrices().get(0).getMccmnc());
+ assertEquals("0", outboundSmsPriceResponse.getPrices().get(0).getMcc());
+ assertNull(outboundSmsPriceResponse.getPrices().get(0).getMnc());
+ assertEquals("Default Rate", outboundSmsPriceResponse.getPrices().get(0).getCountryName());
+ assertEquals("XX", outboundSmsPriceResponse.getPrices().get(0).getCountryIsoCode());
+ assertEquals("Default Rate", outboundSmsPriceResponse.getPrices().get(0).getOperatorName());
+
+ assertEquals(new BigDecimal("0.047000"), outboundSmsPriceResponse.getPrices().get(1).getPrice());
+ assertEquals("EUR", outboundSmsPriceResponse.getPrices().get(1).getCurrencyCode());
+ assertEquals("202", outboundSmsPriceResponse.getPrices().get(1).getMccmnc());
+ assertEquals("202", outboundSmsPriceResponse.getPrices().get(1).getMcc());
+ assertNull(outboundSmsPriceResponse.getPrices().get(1).getMnc());
+ assertEquals("Greece", outboundSmsPriceResponse.getPrices().get(1).getCountryName());
+ assertEquals("GR", outboundSmsPriceResponse.getPrices().get(1).getCountryIsoCode());
+ assertNull(outboundSmsPriceResponse.getPrices().get(1).getOperatorName());
+
+ assertEquals(new BigDecimal("0.045000"), outboundSmsPriceResponse.getPrices().get(2).getPrice());
+ assertEquals("EUR", outboundSmsPriceResponse.getPrices().get(2).getCurrencyCode());
+ assertEquals("20205", outboundSmsPriceResponse.getPrices().get(2).getMccmnc());
+ assertEquals("202", outboundSmsPriceResponse.getPrices().get(2).getMcc());
+ assertEquals("05", outboundSmsPriceResponse.getPrices().get(2).getMnc());
+ assertEquals("Greece", outboundSmsPriceResponse.getPrices().get(2).getCountryName());
+ assertEquals("GR", outboundSmsPriceResponse.getPrices().get(2).getCountryIsoCode());
+ assertEquals("Vodafone", outboundSmsPriceResponse.getPrices().get(2).getOperatorName());
+ }
+}
diff --git a/api/src/test/java/com/messagebird/RequestValidatorTest.java b/api/src/test/java/com/messagebird/RequestValidatorTest.java
index a8d38132..23c64ef0 100644
--- a/api/src/test/java/com/messagebird/RequestValidatorTest.java
+++ b/api/src/test/java/com/messagebird/RequestValidatorTest.java
@@ -1,6 +1,5 @@
package com.messagebird;
-import com.auth0.jwt.interfaces.Clock;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.messagebird.exceptions.RequestValidationException;
@@ -13,7 +12,10 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Instant;
import java.time.OffsetDateTime;
+import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
@@ -64,10 +66,7 @@ public static Collection