diff --git a/api/pom.xml b/api/pom.xml index 2208a7c9..b64ee8b0 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,10 +4,7 @@ com.messagebird messagebird-api - 5.1.1 + 6.4.0 jar ${project.groupId}:${project.artifactId} @@ -30,6 +27,12 @@ MessageBird https://www.messagebird.com + + Venkateswaran S + venkat.sankaran@bird.com + MessageBird + https://www.messagebird.com + Sam Wierema sam@messagebird.com @@ -59,19 +62,51 @@ + test false + + + UTF-8 + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + **/ContactTest.java + **/MessageBirdClientTest.java + + + + + + + + + integration + + false + UTF-8 + disable-doclint - [8,11,) + [1.8,11) none - true UTF-8 @@ -79,6 +114,17 @@ + + + + + org.codehaus.plexus + plexus-utils + 3.6.1 + + + + com.fasterxml.jackson.core @@ -93,7 +139,12 @@ com.auth0 java-jwt - 3.17.0 + 4.4.0 + + + org.apache.maven + maven-artifact + 3.9.6 junit @@ -121,13 +172,6 @@ - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - @@ -222,14 +266,22 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://oss.sonatype.org/ - false + central + false + validated diff --git a/api/src/main/java/com/messagebird/Base64.java b/api/src/main/java/com/messagebird/Base64.java deleted file mode 100644 index f96ef9fa..00000000 --- a/api/src/main/java/com/messagebird/Base64.java +++ /dev/null @@ -1,512 +0,0 @@ -package com.messagebird; - -/** - * Cutted version of iharder's base64 implementation - * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.3.7 - * @todo replace with actual library on next major bump - * - *

Encodes and decodes to and from Base64 notation.

- *

Homepage: http://iharder.net/base64.

- * - *

- * 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 276ba3c8..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. * @@ -2118,6 +2146,23 @@ public void deleteTemplatesBy(final String templateName) messageBirdService.delete(url, null); } + 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, + UNPAUSE_TEMAPLATE_PATH, + templateName + ); + messageBirdService.sendPayLoad("POST", url, "", null); + } + /** * Function to create a child account * @@ -2202,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 56314ab8..ef179b45 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -8,6 +8,8 @@ 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; @@ -60,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(). @@ -71,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "5.1.1"; + private final String clientVersion = "6.3.1"; private final String userAgentString; private Proxy proxy = null; @@ -88,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); } /** @@ -360,7 +363,7 @@ private void handleHttpFailStatuses(final int status, String body) throws Unauth

APIResponse doRequest(final String method, final String url, final Map headers, final P payload) throws GeneralException { HttpURLConnection connection = null; InputStream inputStream = null; - + if (METHOD_PATCH.equalsIgnoreCase(method)) { // It'd perhaps be cleaner to call this in the constructor, but // we'd then need to throw GeneralExceptions from there. This means @@ -379,6 +382,9 @@

APIResponse doRequest(final String method, final String url, final Map HttpURLConnection getConnection(final String serviceUrl, final P body connection.setRequestProperty("Content-Type", "application/json"); ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(Include.NON_NULL); - mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); // Specifically set the date format for POST requests so scheduled // messages and other things relying on specific date formats don't // fail when sending. @@ -632,34 +637,13 @@ private void setAdditionalHeaders(HttpURLConnection connection, 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. * @@ -803,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 9fedb2f2..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. */ @@ -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/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 069c3f62..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; @@ -10,6 +13,10 @@ public class MessageParam { 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; @@ -24,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; } @@ -48,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; } @@ -71,17 +84,42 @@ public void setImage(Media image) { 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 + '\'' + - ", video=" + video + - '}'; + 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 fe5494fd..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,6 +2,9 @@ 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 { @@ -11,7 +14,20 @@ public enum TemplateMediaType { 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; @@ -21,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 c88d974b..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,8 +11,8 @@ */ public enum HSMCategory { - OTP("OTP"), - TRANSACTIONAL("TRANSACTIONAL"), + 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 a61741f5..f6a83e93 100644 --- a/api/src/main/java/com/messagebird/objects/integrations/Template.java +++ b/api/src/main/java/com/messagebird/objects/integrations/Template.java @@ -15,17 +15,20 @@ public class Template { private String wabaID; private List components; private HSMCategory category; + private boolean ctaURLLinkTrackingOptedOut; public Template() { } + public Template(String name, String language, String wabaID, - List components, HSMCategory category) { + 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() { @@ -68,6 +71,14 @@ 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{" + @@ -76,6 +87,7 @@ public String toString() { ", wabaID='" + wabaID + '\'' + ", components=" + components + ", category='" + category + '\'' + + ", ctaURLLinkTrackingOptedOut='" + ctaURLLinkTrackingOptedOut + '\'' + '}'; } 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 426ae64a..3c75fabe 100644 --- a/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java +++ b/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java @@ -20,6 +20,11 @@ public class TemplateResponse implements Serializable { private String rejectedReason; private String wabaID; private String namespace; + + private boolean ctaURLLinkTrackingOptedOut; + + private HSMQualityScore qualityScore; + private Date createdAt; private Date updatedAt; @@ -107,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{" + @@ -118,8 +139,11 @@ public String toString() { ", 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 9554ac58..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; @@ -71,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; } @@ -103,6 +110,7 @@ public String toString() { ", record=" + record + ", steps=" + steps + ", default=" + defaultCall + + ", maxDuration=" + maxDuration + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; 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 50459497..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 @@ -1077,6 +1081,82 @@ public void testCreateWhatsAppTemplate() throws UnauthorizedException, GeneralEx 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()); + + /* 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 testListWhatsAppTemplates() throws UnauthorizedException, GeneralException { @@ -1516,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 data() throws IOException { public void testWebhookSignature() throws Throwable { RequestValidator validator = new RequestValidator(testCase.secret != null ? testCase.secret : ""); - Clock clock = mock(Clock.class); - Date clockDate = spy(Date.from(OffsetDateTime.parse(testCase.timestamp).toInstant())); - when(clock.getToday()).thenReturn(clockDate); - + Clock clock = Clock.fixed(OffsetDateTime.parse(testCase.timestamp).toInstant(), ZoneId.systemDefault()); ThrowingRunnable runnable = () -> validator.validateSignature(clock, testCase.token, testCase.url, (testCase.payload == null) ? null : testCase.payload.getBytes(StandardCharsets.UTF_8)); diff --git a/api/src/test/java/com/messagebird/TestUtil.java b/api/src/test/java/com/messagebird/TestUtil.java index e3852308..3e9c5a33 100644 --- a/api/src/test/java/com/messagebird/TestUtil.java +++ b/api/src/test/java/com/messagebird/TestUtil.java @@ -6,6 +6,7 @@ import com.messagebird.objects.integrations.HSMComponent; import com.messagebird.objects.integrations.HSMComponentButton; import com.messagebird.objects.integrations.HSMComponentButtonType; +import com.messagebird.objects.integrations.HSMComponentCard; import com.messagebird.objects.integrations.HSMComponentFormat; import com.messagebird.objects.integrations.HSMComponentType; import com.messagebird.objects.integrations.HSMExample; @@ -30,6 +31,7 @@ static VoiceCall createVoiceCall(String destination) { final VoiceCallFlow voiceCallFlow = new VoiceCallFlow(); VoiceStep voiceStep = new VoiceStep(); voiceStep.setAction("say"); + voiceCallFlow.setMaxDuration(28800); final VoiceStepOption voiceStepOption = new VoiceStepOption(); voiceStepOption.setPayload("This is a journey into sound. Good bye!"); @@ -300,12 +302,31 @@ private static HSMComponent createHSMComponentButton() { return buttonComponent; } + private static HSMComponent createHSMComponentCarousel() { + final HSMComponent carouselComponent = new HSMComponent(); + carouselComponent.setType(HSMComponentType.CAROUSEL); + + final List cards = new ArrayList<>(); + final HSMComponentCard card = new HSMComponentCard(); + final List cardComponents = new ArrayList<>(); + cardComponents.add(createHSMComponentHeader()); + cardComponents.add(createHSMComponentBody()); + cardComponents.add(createHSMComponentButton()); + card.setComponents(cardComponents); + cards.add(card); + + carouselComponent.setCards(cards); + + return carouselComponent; + } + public static TemplateResponse createWhatsAppTemplateResponse(final String templateName, final String language) { final TemplateResponse templateResponse = new TemplateResponse(); templateResponse.setName(templateName); templateResponse.setLanguage(language); - templateResponse.setCategory(HSMCategory.OTP); + templateResponse.setCategory(HSMCategory.AUTHENTICATION); templateResponse.setStatus(HSMStatus.NEW); + templateResponse.setCtaURLLinkTrackingOptedOut(true); templateResponse.setCreatedAt(new Date()); templateResponse.setUpdatedAt(new Date()); @@ -326,7 +347,7 @@ public static Template createWhatsAppTemplate(final String templateName, final S final Template template = new Template(); template.setName(templateName); template.setLanguage(language); - template.setCategory(HSMCategory.OTP); + template.setCategory(HSMCategory.AUTHENTICATION); final List components = new ArrayList<>(); components.add(createHSMComponentHeader()); @@ -340,6 +361,43 @@ public static Template createWhatsAppTemplate(final String templateName, final S return template; } + public static TemplateResponse createWhatsAppCarouselTemplateResponse(final String templateName, final String language) { + final TemplateResponse templateResponse = new TemplateResponse(); + templateResponse.setName(templateName); + templateResponse.setLanguage(language); + templateResponse.setCategory(HSMCategory.MARKETING); + templateResponse.setStatus(HSMStatus.NEW); + templateResponse.setCreatedAt(new Date()); + templateResponse.setUpdatedAt(new Date()); + + final List components = new ArrayList<>(); + components.add(createHSMComponentBody()); + components.add(createHSMComponentCarousel()); + templateResponse.setComponents(components); + + templateResponse.setWabaID("testWABAID"); + templateResponse.setNamespace("testNamespace"); + + return templateResponse; + } + + public static Template createWhatsAppCarouselTemplate(final String templateName, final String language) { + final Template template = new Template(); + template.setName(templateName); + template.setLanguage(language); + template.setCategory(HSMCategory.MARKETING); + + final List components = new ArrayList<>(); + components.add(createHSMComponentBody()); + components.add(createHSMComponentCarousel()); + template.setComponents(components); + + template.setWABAID("testWABAID"); + + return template; + } + + public static TemplateList createWhatsAppTemplateList(final String templateName) { final TemplateResponse template1 = TestUtil.createWhatsAppTemplateResponse(templateName, "en_US"); final TemplateResponse template2 = TestUtil.createWhatsAppTemplateResponse(templateName, "ko"); diff --git a/api/src/test/java/com/messagebird/objects/conversations/MessageComponentTypeTest.java b/api/src/test/java/com/messagebird/objects/conversations/MessageComponentTypeTest.java new file mode 100644 index 00000000..385b1652 --- /dev/null +++ b/api/src/test/java/com/messagebird/objects/conversations/MessageComponentTypeTest.java @@ -0,0 +1,28 @@ +package com.messagebird.objects.conversations; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class MessageComponentTypeTest { + @Test + public void testMessageComponentTypeForValueValid() { + assertEquals(MessageComponentType.HEADER, MessageComponentType.forValue("header")); + assertEquals(MessageComponentType.BUTTON, MessageComponentType.forValue("button")); + } + + @Test(expected = NullPointerException.class) + public void testMessageComponentTypeForValueNull() { + MessageComponentType.forValue(null); + } + + @Test + public void testMessageComponentTypeForValueInvalid() { + assertNull(MessageComponentType.forValue("invalid_type")); + } + + @Test + public void testMessageComponentTypeToString() { + assertEquals("header", MessageComponentType.HEADER.toString()); + } +} diff --git a/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java b/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java new file mode 100644 index 00000000..f1147d2f --- /dev/null +++ b/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java @@ -0,0 +1,30 @@ +package com.messagebird.objects.conversations; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class MessageParamTest { + @Test + public void testMessageParamToString() { + MessageParam param = new MessageParam(); + param.setType(TemplateMediaType.IMAGE); + param.setText("Sample text"); + param.setPayload("Sample payload"); + + String expected = "MessageParam{type=image, text='Sample text', payload='Sample payload', currency=null, dateTime='null', document=null, image=null, video=null, expirationTime='null', couponCode='null'}"; + assertEquals(expected, param.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMessageParamSetTextInvalid() { + MessageParam param = new MessageParam(); + param.setText(""); + } + + @Test + public void testMessageParamSetTextValid() { + MessageParam param = new MessageParam(); + param.setText("Valid text"); + assertEquals("Valid text", param.getText()); + } +} diff --git a/api/src/test/java/com/messagebird/objects/conversations/TemplateMediaTypeTest.java b/api/src/test/java/com/messagebird/objects/conversations/TemplateMediaTypeTest.java new file mode 100644 index 00000000..d30bde28 --- /dev/null +++ b/api/src/test/java/com/messagebird/objects/conversations/TemplateMediaTypeTest.java @@ -0,0 +1,27 @@ +package com.messagebird.objects.conversations; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class TemplateMediaTypeTest { + @Test + public void testTemplateMediaTypeForValueValid() { + assertEquals(TemplateMediaType.VIDEO, TemplateMediaType.forValue("video")); + } + + @Test(expected = IllegalArgumentException.class) + public void testTemplateMediaTypeForValueNull() { + TemplateMediaType.forValue(null); + } + + @Test + public void testTemplateMediaTypeForValueInvalid() { + assertNull(TemplateMediaType.forValue("non_existing_value")); + } + + @Test + public void testTemplateMediaTypeToString() { + assertEquals("video", TemplateMediaType.VIDEO.toString()); + } +} diff --git a/api/src/test/java/com/messagebird/objects/integrations/HSMComponentTest.java b/api/src/test/java/com/messagebird/objects/integrations/HSMComponentTest.java new file mode 100644 index 00000000..9af4bb64 --- /dev/null +++ b/api/src/test/java/com/messagebird/objects/integrations/HSMComponentTest.java @@ -0,0 +1,26 @@ +package com.messagebird.objects.integrations; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class HSMComponentTest { + @Test + public void testToString() { + HSMComponent component = new HSMComponent(); + component.setType(HSMComponentType.BODY); + component.setFormat(HSMComponentFormat.TEXT); + component.setText("Test text"); + component.setAddSecurityRecommendation(true); + component.setCodeExpirationMinutes(10); + component.setHasExpiration(true); + + String expected = "HSMComponent{type=BODY, format=TEXT, text='Test text', addSecurityRecommendation=true, codeExpirationMinutes=10, buttons=null, hasExpiration=true, cards=null, example=null}"; + assertEquals(expected, component.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetTextInvalid() { + HSMComponent component = new HSMComponent(); + component.setText(""); + } +} diff --git a/api/src/test/java/com/messagebird/objects/integrations/HSMComponentTypeTest.java b/api/src/test/java/com/messagebird/objects/integrations/HSMComponentTypeTest.java new file mode 100644 index 00000000..47053509 --- /dev/null +++ b/api/src/test/java/com/messagebird/objects/integrations/HSMComponentTypeTest.java @@ -0,0 +1,23 @@ +package com.messagebird.objects.integrations; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class HSMComponentTypeTest { + @Test + public void testForValueValid() { + assertEquals(HSMComponentType.BODY, HSMComponentType.forValue("BODY")); + assertEquals(HSMComponentType.BODY, HSMComponentType.forValue("body")); + } + + @Test(expected = NullPointerException.class) + public void testForValueNull() { + HSMComponentType.forValue(null); + } + + @Test + public void testForValueInvalid() { + assertNull(HSMComponentType.forValue("INVALID")); + } +} diff --git a/api/src/test/resources/fixtures/outbound_sms_prices.json b/api/src/test/resources/fixtures/outbound_sms_prices.json new file mode 100644 index 00000000..261401d3 --- /dev/null +++ b/api/src/test/resources/fixtures/outbound_sms_prices.json @@ -0,0 +1,37 @@ +{ + "gateway": 10, + "currencyCode": "EUR", + "totalCount": 3, + "prices": [ + { + "price": "0.060000", + "currencyCode": "EUR", + "mccmnc": "0", + "mcc": "0", + "mnc": null, + "countryName": "Default Rate", + "countryIsoCode": "XX", + "operatorName": "Default Rate" + }, + { + "price": "0.047000", + "currencyCode": "EUR", + "mccmnc": "202", + "mcc": "202", + "mnc": null, + "countryName": "Greece", + "countryIsoCode": "GR", + "operatorName": null + }, + { + "price": "0.045000", + "currencyCode": "EUR", + "mccmnc": "20205", + "mcc": "202", + "mnc": "05", + "countryName": "Greece", + "countryIsoCode": "GR", + "operatorName": "Vodafone" + } + ] +} diff --git a/examples/pom.xml b/examples/pom.xml index 91323182..cb9b7489 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ com.messagebird examples - 5.1.1 + 6.4.0 @@ -20,7 +20,7 @@ com.messagebird messagebird-api - 5.1.1 + 6.4.0 compile diff --git a/examples/src/main/java/ExampleConversationSendCarouselTemplate.java b/examples/src/main/java/ExampleConversationSendCarouselTemplate.java new file mode 100644 index 00000000..f386917d --- /dev/null +++ b/examples/src/main/java/ExampleConversationSendCarouselTemplate.java @@ -0,0 +1,116 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.conversations.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ExampleConversationSendCarouselTemplate { + + public static void main(String[] args) { + + if (args.length < 3) { + System.out.println("Please at least specify your access key, the channel id and destination address.\n" + + "Usage : java -jar test_accesskey(Required) channel_id(Required) to(Required)"); + return; + } + + //First create your service object + final MessageBirdService wsr = new MessageBirdServiceImpl(args[0]); + //Add the service to the client + final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + ConversationContent conversationContent = new ConversationContent(); + ConversationContentHsm conversationContentHsm = new ConversationContentHsm(); + conversationContentHsm.setNamespace("c663a566_a4de_492f_86b2_028cbf612345"); + conversationContentHsm.setTemplateName("carousel_template_test_hello"); + ConversationHsmLanguage language = new ConversationHsmLanguage(); + language.setCode("en_US"); + conversationContentHsm.setLanguage(language); + + + +// card0 components + List card0Components = new ArrayList<>(); +// card0 header + MessageComponent card0HeaderComponent = new MessageComponent(); + card0HeaderComponent.setType(MessageComponentType.HEADER); + MessageParam imageParam = new MessageParam(); + imageParam.setType(TemplateMediaType.IMAGE); + Media media = new Media(); + media.setUrl("https://upload.wikimedia.org/wikipedia/commons/f/f9/Phoenicopterus_ruber_in_S%C3%A3o_Paulo_Zoo.jpg"); + imageParam.setImage(media); + card0HeaderComponent.setParameters(Collections.singletonList(imageParam)); + card0Components.add(card0HeaderComponent); +// card0 body + MessageComponent card0BodyComponent = new MessageComponent(); + card0BodyComponent.setType(MessageComponentType.BODY); + MessageParam textParam = new MessageParam(); + textParam.setType(TemplateMediaType.TEXT); + textParam.setText("dummy text"); + card0BodyComponent.setParameters(Collections.singletonList(textParam)); + card0Components.add(card0BodyComponent); +// card0 button + MessageComponent card0ButtonComponent = new MessageComponent(); + card0ButtonComponent.setType(MessageComponentType.BUTTON); + card0ButtonComponent.setSub_type("quick_reply"); + card0ButtonComponent.setIndex(0); + MessageParam buttonParam = new MessageParam(); + buttonParam.setType(TemplateMediaType.PAYLOAD); + buttonParam.setPayload("dummy button"); + card0ButtonComponent.setParameters(Collections.singletonList(buttonParam)); + card0Components.add(card0ButtonComponent); + +// card0 + MessageComponent card0 = new MessageComponent(); + card0.setType(MessageComponentType.CARD); + card0.setCard_index(0); + card0.setComponents(card0Components); + + +// cards list + List cards = new ArrayList<>(); + cards.add(card0); + +// carousel component + MessageComponent carousel = new MessageComponent(); + carousel.setType(MessageComponentType.CAROUSEL); + carousel.setCards(cards); +// body component + MessageComponent body = new MessageComponent(); + body.setType(MessageComponentType.BODY); + MessageParam bodyParam = new MessageParam(); + bodyParam.setType(TemplateMediaType.TEXT); + bodyParam.setText("Jackson"); + body.setParameters(Collections.singletonList(bodyParam)); + +// set components to message + List messageComponents = new ArrayList<>(); + messageComponents.add(body); + messageComponents.add(carousel); + conversationContentHsm.setComponents(messageComponents); + + conversationContent.setHsm(conversationContentHsm); + ConversationSendRequest request = new ConversationSendRequest( + args[2], + ConversationContentType.HSM, + conversationContent, + args[1], + "", + null, + null, + null); + + try { + ConversationSendResponse sendResponse = messageBirdClient.sendMessage(request); + System.out.println(sendResponse.toString()); + + } catch (GeneralException | UnauthorizedException exception) { + exception.printStackTrace(); + } + } +} diff --git a/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java new file mode 100644 index 00000000..4d5dcd3f --- /dev/null +++ b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java @@ -0,0 +1,92 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.conversations.*; + +import java.util.ArrayList; +import java.util.List; + +public class ExampleConversationSendHSMCopyCodeTemplate { + + public static void main(String[] args) { + if (args.length < 6) { + System.out.println("Please at least specify your access key, the channel id and destination address.\n" + + "Usage : java -jar test_accesskey(Required) channel_id(Required) from(Required) destination(Required) templateName(Required) namespace(Required) couponCodeInput(Required)"); + return; + } + + final String accessKey = args[0]; + final String from = args[1]; + final String destination = args[2]; + final String templateName = args[3]; + final String namespace = args[4]; + final String couponCodeInput = args[5]; + + //First create your service object + final MessageBirdService wsr = new MessageBirdServiceImpl(accessKey); + //Add the service to the client + final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + ConversationContent conversationContent = new ConversationContent(); + ConversationContentHsm conversationContentHsm = new ConversationContentHsm(); + conversationContentHsm.setNamespace(namespace); + conversationContentHsm.setTemplateName(templateName); + ConversationHsmLanguage language = new ConversationHsmLanguage(); + language.setCode("en"); + conversationContentHsm.setLanguage(language); + List messageComponents = new ArrayList<>(); + + // Add LTO component + MessageComponent messageCopyCodeComponent = new MessageComponent(); + messageCopyCodeComponent.setType(MessageComponentType.BUTTON); + messageCopyCodeComponent.setSub_type(MessageComponentType.COPY_CODE.toString()); + List messageCCParams = new ArrayList<>(); + + MessageParam couponCodeParam = new MessageParam(); + couponCodeParam.setType(TemplateMediaType.COUPON_CODE); + couponCodeParam.setCouponCode(couponCodeInput); + messageCCParams.add(couponCodeParam); + + messageCopyCodeComponent.setParameters(messageCCParams); + + // Add body component + MessageComponent messageBodyComponent = new MessageComponent(); + messageBodyComponent.setType(MessageComponentType.BODY); + List messageBodyParams = new ArrayList<>(); + + MessageParam text = new MessageParam(); + text.setType(TemplateMediaType.TEXT); + text.setText("Bob"); + messageBodyParams.add(text); + + messageBodyComponent.setParameters(messageBodyParams); + + messageComponents.add(messageCopyCodeComponent); + messageComponents.add(messageBodyComponent); + conversationContentHsm.setComponents(messageComponents); + conversationContent.setHsm(conversationContentHsm); + ConversationSendRequest request = new ConversationSendRequest( + destination, + ConversationContentType.HSM, + conversationContent, + from, + "", + null, + null, + null); + + try { + System.out.println(request.toString()); + ConversationSendResponse sendResponse = messageBirdClient.sendMessage(request); + System.out.println(sendResponse.toString()); + + } catch (UnauthorizedException e) { + System.err.println("Authorization failed. Please check your access key: " + e.getMessage()); + } catch (GeneralException e) { + System.err.println("An error occurred while sending the message: " + e.getMessage()); + } + } + +} diff --git a/examples/src/main/java/ExampleConversationSendHSMLimitedTimeOfferTemplate.java b/examples/src/main/java/ExampleConversationSendHSMLimitedTimeOfferTemplate.java new file mode 100644 index 00000000..65b959a8 --- /dev/null +++ b/examples/src/main/java/ExampleConversationSendHSMLimitedTimeOfferTemplate.java @@ -0,0 +1,90 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.conversations.*; + +import java.util.ArrayList; +import java.util.List; + +public class ExampleConversationSendHSMLimitedTimeOfferTemplate { + + public static void main(String[] args) { + if (args.length < 6) { + System.out.println("Please at least specify your access key, the channel id and destination address.\n" + + "Usage : java -jar test_accesskey(Required) channel_id(Required) from(Required) destination(Required) templateName(Required) namespace(Required) expirationTimeInput(Required)"); + return; + } + + final String accessKey = args[0]; + final String from = args[1]; + final String destination = args[2]; + final String templateName = args[3]; + final String namespace = args[4]; + final String expirationTimeInput = args[5]; + + //First create your service object + final MessageBirdService wsr = new MessageBirdServiceImpl(accessKey); + //Add the service to the client + final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + ConversationContent conversationContent = new ConversationContent(); + ConversationContentHsm conversationContentHsm = new ConversationContentHsm(); + conversationContentHsm.setNamespace(namespace); + conversationContentHsm.setTemplateName(templateName); + ConversationHsmLanguage language = new ConversationHsmLanguage(); + language.setCode("en"); + conversationContentHsm.setLanguage(language); + List messageComponents = new ArrayList<>(); + + // Add LTO component + MessageComponent messageLTOComponent = new MessageComponent(); + messageLTOComponent.setType(MessageComponentType.LIMITED_TIME_OFFER); + List messageLTOParams = new ArrayList<>(); + + MessageParam expirationTime = new MessageParam(); + expirationTime.setType(TemplateMediaType.EXPIRATION_TIME); + expirationTime.setExpirationTime(expirationTimeInput); + messageLTOParams.add(expirationTime); + + messageLTOComponent.setParameters(messageLTOParams); + + // Add body component + MessageComponent messageBodyComponent = new MessageComponent(); + messageBodyComponent.setType(MessageComponentType.BODY); + List messageBodyParams = new ArrayList<>(); + + MessageParam text = new MessageParam(); + text.setType(TemplateMediaType.TEXT); + text.setText("Bob"); + messageBodyParams.add(text); + + messageBodyComponent.setParameters(messageBodyParams); + + messageComponents.add(messageLTOComponent); + messageComponents.add(messageBodyComponent); + conversationContentHsm.setComponents(messageComponents); + conversationContent.setHsm(conversationContentHsm); + ConversationSendRequest request = new ConversationSendRequest( + destination, + ConversationContentType.HSM, + conversationContent, + from, + "", + null, + null, + null); + + try { + ConversationSendResponse sendResponse = messageBirdClient.sendMessage(request); + System.out.println(sendResponse.toString()); + + } catch (UnauthorizedException e) { + System.err.println("Authorization failed. Please check your access key: " + e.getMessage()); + } catch (GeneralException e) { + System.err.println("An error occurred while sending the message: " + e.getMessage()); + } + } + +} diff --git a/examples/src/main/java/ExampleCreateAuthTemplate.java b/examples/src/main/java/ExampleCreateAuthTemplate.java new file mode 100644 index 00000000..31b5f415 --- /dev/null +++ b/examples/src/main/java/ExampleCreateAuthTemplate.java @@ -0,0 +1,69 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.integrations.*; + +import java.util.ArrayList; +import java.util.List; + +public class ExampleCreateAuthTemplate { + public static void main(String[] args) { + + if (args.length < 3) { + System.out.println("Please specify your access key and a template name and WABA ID example : java -jar test_accesskey \"My template name\" \"WABA ID\""); + return; + } + + // First create your service object + MessageBirdService wsr = new MessageBirdServiceImpl(args[0]); + + // Add the service to the client + MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + /* body */ + HSMComponent bodyComponent = new HSMComponent(); + bodyComponent.setType(HSMComponentType.BODY); + bodyComponent.setAddSecurityRecommendation(true); + + /* footer */ + HSMComponent footerComponent = new HSMComponent(); + footerComponent.setType(HSMComponentType.FOOTER); + footerComponent.setCodeExpirationMinutes(8); + + /* button */ + HSMComponent buttonComponent = new HSMComponent(); + List buttons = new ArrayList<>(); + HSMComponentButton otpButton = new HSMComponentButton(); + otpButton.setOtpType(HSMOTPButtonType.ONE_TAP); + otpButton.setText("Copy code"); + otpButton.setAutofillText("Autofill"); + otpButton.setPackageName("com.example.luckyshrub"); + otpButton.setSignatureHash("K8a%2FAINcGX7"); + + buttons.add(otpButton); + buttonComponent.setType(HSMComponentType.BUTTONS); + buttonComponent.setButtons(buttons); + + /* set components */ + Template template = new Template(); + List components = new ArrayList<>(); + components.add(bodyComponent); + components.add(footerComponent); + components.add(buttonComponent); + + template.setName(args[1]); + template.setLanguage("en_US"); + template.setWABAID(args[2]); + template.setComponents(components); + template.setCategory(HSMCategory.AUTHENTICATION); + + try { + TemplateResponse response = messageBirdClient.createWhatsAppTemplate(template); + System.out.println(response.toString()); + } catch (GeneralException | UnauthorizedException | IllegalArgumentException exception) { + exception.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/examples/src/main/java/ExampleCreateCarouselTemplate.java b/examples/src/main/java/ExampleCreateCarouselTemplate.java new file mode 100644 index 00000000..54bea0dd --- /dev/null +++ b/examples/src/main/java/ExampleCreateCarouselTemplate.java @@ -0,0 +1,113 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.integrations.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Create template. + * + * @see Doc - Create template + * @author AlexL-mb + */ +public class ExampleCreateCarouselTemplate { + + public static void main(String[] args) { + if (args.length < 3) { + System.out.println("Please specify your access key and a template name and WABA ID example : java -jar test_accesskey \"My template name\" \"WABA ID\""); + return; + } + + // First create your service object + MessageBirdService wsr = new MessageBirdServiceImpl(args[0]); + + // Add the service to the client + MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + /* body */ + HSMComponent bodyComponent = new HSMComponent(); + final HSMExample bodyExample = new HSMExample(); + final List> bodyText = new ArrayList<>(); + bodyText.add(Arrays.asList("John")); + bodyExample.setBody_text(bodyText); + bodyComponent.setType(HSMComponentType.BODY); + bodyComponent.setText("Hey {{1}}! This is a sample template from Java."); + bodyComponent.setExample(bodyExample); + + /* carousel */ + final HSMComponent carouselComponent = new HSMComponent(); + carouselComponent.setType(HSMComponentType.CAROUSEL); + + /* cards */ + final List cards = new ArrayList<>(); + /* card 0 */ + final HSMComponentCard card = new HSMComponentCard(); + final List cardComponents = new ArrayList<>(); + + /* card header */ + final HSMComponent headerComponent = new HSMComponent(); + final HSMExample headerExample = new HSMExample(); + headerExample.setHeader_url(Arrays.asList("https://images.freeimages.com/images/small-previews/c5a/colourful-paper-rip-1-1195879.jpg")); + + headerComponent.setType(HSMComponentType.HEADER); + headerComponent.setFormat(HSMComponentFormat.IMAGE); + headerComponent.setExample(headerExample); + cardComponents.add(headerComponent); + + /* card body */ + final HSMComponent cardBodyComponent = new HSMComponent(); + final HSMExample cardBodyExample = new HSMExample(); + final List> cardBodyText = new ArrayList<>(); + cardBodyText.add(Arrays.asList("John")); + cardBodyExample.setBody_text(cardBodyText); + + cardBodyComponent.setType(HSMComponentType.BODY); + cardBodyComponent.setText("Hey {{1}}! This is a sample template from Java."); + cardBodyComponent.setExample(cardBodyExample); + cardComponents.add(cardBodyComponent); + + /* card buttons */ + final HSMComponent buttonComponent = new HSMComponent(); + final List buttons = new ArrayList<>(); + final HSMComponentButton button = new HSMComponentButton(); + button.setType(HSMComponentButtonType.URL); + button.setText("Touch it"); + button.setUrl("https://www.messagebird.com"); + button.setExample(Arrays.asList("https://developers.messagebird.com")); + buttons.add(button); + buttonComponent.setType(HSMComponentType.BUTTONS); + buttonComponent.setButtons(buttons); + cardComponents.add(buttonComponent); + + card.setComponents(cardComponents); + + cards.add(card); + + carouselComponent.setCards(cards); + + /* set components */ + Template template = new Template(); + List components = new ArrayList<>(); + components.add(bodyComponent); + components.add(carouselComponent); + + template.setName(args[1]); + template.setLanguage("en_US"); + template.setWABAID(args[2]); + template.setComponents(components); + template.setCategory(HSMCategory.MARKETING); + + try { + TemplateResponse response = messageBirdClient.createWhatsAppTemplate(template); + System.out.println(response.toString()); + } catch (GeneralException | UnauthorizedException | IllegalArgumentException exception) { + exception.printStackTrace(); + } + + } +} diff --git a/examples/src/main/java/ExampleCreateCouponTemplate.java b/examples/src/main/java/ExampleCreateCouponTemplate.java new file mode 100644 index 00000000..f5480434 --- /dev/null +++ b/examples/src/main/java/ExampleCreateCouponTemplate.java @@ -0,0 +1,81 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.integrations.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Create template. + * + * @see Doc - Create template + * @author AlexL-mb + */ +public class ExampleCreateCouponTemplate { + + public static void main(String[] args) { + if (args.length < 3) { + System.out.println("Please specify your access key and a template name and WABA ID example : java -jar test_accesskey \"My template name\" \"WABA ID\""); + return; + } + + // First create your service object + final MessageBirdService wsr = new MessageBirdServiceImpl(args[0]); + + // Add the service to the client + final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + /* header */ + final HSMComponent headerComponent = new HSMComponent(); + headerComponent.setType(HSMComponentType.HEADER); + headerComponent.setFormat(HSMComponentFormat.TEXT); + headerComponent.setText("Our Fall Sale is on!"); + + /* body */ + final HSMComponent bodyComponent = new HSMComponent(); + final HSMExample bodyExample = new HSMExample(); + final List> bodyText = new ArrayList<>(); + bodyText.add(Arrays.asList("25OFF", "25%")); + bodyExample.setBody_text(bodyText); + + bodyComponent.setType(HSMComponentType.BODY); + bodyComponent.setText("Shop now through November and use code {{1}} to get {{2}} off of all merchandise!"); + bodyComponent.setExample(bodyExample); + + + /* button */ + final HSMComponent buttonComponent = new HSMComponent(); + final List buttons = new ArrayList<>(); + final HSMComponentButton button = new HSMComponentButton(); + button.setType(HSMComponentButtonType.COPY_CODE); + button.setExample(Arrays.asList("CODE25")); + buttons.add(button); + buttonComponent.setType(HSMComponentType.BUTTONS); + buttonComponent.setButtons(buttons); + + /* set components */ + final Template template = new Template(); + final List components = new ArrayList<>(); + components.add(headerComponent); + components.add(bodyComponent); + components.add(buttonComponent); + + template.setName(args[1]); + template.setLanguage("en_US"); + template.setWABAID(args[2]); + template.setComponents(components); + template.setCategory(HSMCategory.MARKETING); + template.setCtaURLLinkTrackingOptedOut(true); + + try { + TemplateResponse response = messageBirdClient.createWhatsAppTemplate(template); + System.out.println(response.toString()); + } catch (GeneralException | UnauthorizedException | IllegalArgumentException exception) { + exception.printStackTrace(); + } + } +} diff --git a/examples/src/main/java/ExampleCreateLimitedTimeOfferTemplate.java b/examples/src/main/java/ExampleCreateLimitedTimeOfferTemplate.java new file mode 100644 index 00000000..dc721f9f --- /dev/null +++ b/examples/src/main/java/ExampleCreateLimitedTimeOfferTemplate.java @@ -0,0 +1,95 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.integrations.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ExampleCreateLimitedTimeOfferTemplate { + + public static void main(String[] args) { + if (args.length < 4) { + System.out.println("Please specify your access key and a template name and WABA ID example : java -jar test_accesskey(Required) templateName(Required) wabaID(required)"); + return; + } + + final String accessKey = args[0]; + final String templateName = args[1]; + final String wabaID = args[2]; + + // First create your service object + final MessageBirdService wsr = new MessageBirdServiceImpl(accessKey); + + // Add the service to the client + final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + // header + final HSMComponent headerComponent = new HSMComponent(); + final HSMExample headerExample = new HSMExample(); + headerExample.setHeader_url(Arrays.asList("https://images.freeimages.com/images/small-previews/c5a/colourful-paper-rip-1-1195879.jpg")); + + headerComponent.setType(HSMComponentType.HEADER); + headerComponent.setFormat(HSMComponentFormat.IMAGE); + headerComponent.setExample(headerExample); + + // limited time offer + final HSMComponent ltoComponent = new HSMComponent(); + ltoComponent.setType(HSMComponentType.LIMITED_TIME_OFFER); + ltoComponent.setText("Expiring offer!"); + ltoComponent.setHasExpiration(true); + + // body + final HSMComponent bodyComponent = new HSMComponent(); + final HSMExample bodyExample = new HSMExample(); + final List> bodyText = new ArrayList<>(); + bodyText.add(Arrays.asList("John", "CARIBE25")); + bodyExample.setBody_text(bodyText); + + bodyComponent.setType(HSMComponentType.BODY); + bodyComponent.setText("Good news, {{1}}! Use code {{2}} to get 0% off all packages!"); + bodyComponent.setExample(bodyExample); + + // buttons + final HSMComponent buttonComponent = new HSMComponent(); + final List buttons = new ArrayList<>(); + + final HSMComponentButton buttonCopyCode = new HSMComponentButton(); + buttonCopyCode.setType(HSMComponentButtonType.COPY_CODE); + buttonCopyCode.setExample(Arrays.asList("CARIBE25")); + + final HSMComponentButton buttonBookNow = new HSMComponentButton(); + buttonBookNow.setType(HSMComponentButtonType.URL); + buttonBookNow.setText("Book now!"); + buttonBookNow.setUrl("https://www.bird.com?code={{1}}"); + buttonBookNow.setExample(Arrays.asList("https://www.bird.com?code=CARIBE25")); + + buttons.addAll(Arrays.asList(buttonCopyCode, buttonBookNow)); + buttonComponent.setType(HSMComponentType.BUTTONS); + buttonComponent.setButtons(buttons); + + // set components + final Template template = new Template(); + final List components = new ArrayList<>(); + components.addAll(Arrays.asList(headerComponent, ltoComponent, bodyComponent, buttonComponent)); + + template.setName(templateName); + template.setLanguage("en_US"); + template.setWABAID(wabaID); + template.setComponents(components); + template.setCategory(HSMCategory.MARKETING); + + try { + TemplateResponse response = messageBirdClient.createWhatsAppTemplate(template); + System.out.println(response.toString()); + } catch (UnauthorizedException e) { + System.err.println("Authorization failed. Please check your access key: " + e.getMessage()); + } catch (GeneralException e) { + System.err.println("An error occurred while sending the message: " + e.getMessage()); + } + } + +} diff --git a/examples/src/main/java/ExampleCreateTemplate.java b/examples/src/main/java/ExampleCreateTemplate.java index 9e8a1e96..336340a6 100644 --- a/examples/src/main/java/ExampleCreateTemplate.java +++ b/examples/src/main/java/ExampleCreateTemplate.java @@ -86,7 +86,7 @@ public static void main(String[] args) { template.setLanguage("en_US"); template.setWABAID(args[2]); template.setComponents(components); - template.setCategory(HSMCategory.OTP); + template.setCategory(HSMCategory.AUTHENTICATION); try { TemplateResponse response = messageBirdClient.createWhatsAppTemplate(template); diff --git a/examples/src/main/java/ExampleGetOutboundSmsPrices.java b/examples/src/main/java/ExampleGetOutboundSmsPrices.java new file mode 100644 index 00000000..e90d5151 --- /dev/null +++ b/examples/src/main/java/ExampleGetOutboundSmsPrices.java @@ -0,0 +1,33 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.MessageBirdException; +import com.messagebird.objects.OutboundSmsPriceResponse; + +public class ExampleGetOutboundSmsPrices { + + public static void main(String[] args) { + if (args.length != 1 && args.length != 2) { + System.out.println("Please specify your access key and (optionally) SMPP username"); + return; + } + + final MessageBirdClient messageBirdClient = new MessageBirdClient(new MessageBirdServiceImpl(args[0])); + + try { + System.out.println("Get a list of outbound SMS prices"); + + final OutboundSmsPriceResponse outboundSmsPriceResponse; + + if (args.length == 2) { + final String smppUsername = args[1]; + outboundSmsPriceResponse = messageBirdClient.getOutboundSmsPrices(smppUsername); + } else { + outboundSmsPriceResponse = messageBirdClient.getOutboundSmsPrices(); + } + + System.out.println("response: " + outboundSmsPriceResponse); + } catch (MessageBirdException e) { + e.printStackTrace(); + } + } +} diff --git a/examples/src/main/java/ExampleSendVoiceCall.java b/examples/src/main/java/ExampleSendVoiceCall.java index c61fddd0..13381ca9 100644 --- a/examples/src/main/java/ExampleSendVoiceCall.java +++ b/examples/src/main/java/ExampleSendVoiceCall.java @@ -46,6 +46,7 @@ public static void main(String[] args) { voiceStep.setOptions(voiceStepOption); voiceCallFlow.setSteps(Collections.singletonList(voiceStep)); + voiceCallFlow.setMaxDuration(28800); voiceCall.setCallFlow(voiceCallFlow); //Sending request to client final VoiceCallResponse response = messageBirdClient.sendVoiceCall(voiceCall);