- * 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.
- *
- *
- * @author Robert Harder
- * @author rob@iharder.net
- * @version 2.3.7
- */
-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 0bff2580..583dcb40 100644
--- a/api/src/main/java/com/messagebird/MessageBirdClient.java
+++ b/api/src/main/java/com/messagebird/MessageBirdClient.java
@@ -3,35 +3,7 @@
import com.messagebird.exceptions.GeneralException;
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
-import com.messagebird.objects.Balance;
-import com.messagebird.objects.Contact;
-import com.messagebird.objects.ContactList;
-import com.messagebird.objects.ContactRequest;
-import com.messagebird.objects.ErrorReport;
-import com.messagebird.objects.FileUploadResponse;
-import com.messagebird.objects.Group;
-import com.messagebird.objects.GroupList;
-import com.messagebird.objects.GroupRequest;
-import com.messagebird.objects.Hlr;
-import com.messagebird.objects.Lookup;
-import com.messagebird.objects.LookupHlr;
-import com.messagebird.objects.Message;
-import com.messagebird.objects.MessageList;
-import com.messagebird.objects.MessageResponse;
-import com.messagebird.objects.MsgType;
-import com.messagebird.objects.PagedPaging;
-import com.messagebird.objects.PhoneNumbersLookup;
-import com.messagebird.objects.PhoneNumbersResponse;
-import com.messagebird.objects.PurchasedNumber;
-import com.messagebird.objects.PurchasedNumberCreatedResponse;
-import com.messagebird.objects.PurchasedNumbersResponse;
-import com.messagebird.objects.PurchasedNumbersFilter;
-import com.messagebird.objects.Verify;
-import com.messagebird.objects.VerifyMessage;
-import com.messagebird.objects.VerifyRequest;
-import com.messagebird.objects.VoiceMessage;
-import com.messagebird.objects.VoiceMessageList;
-import com.messagebird.objects.VoiceMessageResponse;
+import com.messagebird.objects.*;
import com.messagebird.objects.conversations.Conversation;
import com.messagebird.objects.conversations.ConversationList;
import com.messagebird.objects.conversations.ConversationMessage;
@@ -46,6 +18,9 @@
import com.messagebird.objects.conversations.ConversationWebhookCreateRequest;
import com.messagebird.objects.conversations.ConversationWebhookList;
import com.messagebird.objects.conversations.ConversationWebhookUpdateRequest;
+import com.messagebird.objects.integrations.Template;
+import com.messagebird.objects.integrations.TemplateList;
+import com.messagebird.objects.integrations.TemplateResponse;
import com.messagebird.objects.voicecalls.RecordingResponse;
import com.messagebird.objects.voicecalls.TranscriptionResponse;
import com.messagebird.objects.voicecalls.VoiceCall;
@@ -59,20 +34,19 @@
import com.messagebird.objects.voicecalls.Webhook;
import com.messagebird.objects.voicecalls.WebhookList;
import com.messagebird.objects.voicecalls.WebhookResponseData;
-
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
-import java.nio.charset.StandardCharsets;
import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import java.util.HashSet;
/**
* Message bird general client
@@ -98,11 +72,14 @@ public class MessageBirdClient {
* can, however, override this behaviour by providing absolute URLs
* ourselves.
*/
- private static final String CONVERSATIONS_BASE_URL = "https://conversations.messagebird.com/v1";
+ static final String CONVERSATIONS_BASE_URL = "https://conversations.messagebird.com/v1";
static final String VOICE_CALLS_BASE_URL = "https://voice.messagebird.com";
static final String NUMBERS_CALLS_BASE_URL = "https://numbers.messagebird.com/v1";
static final String MESSAGING_BASE_URL = "https://messaging.messagebird.com/v1";
+ static final String INTEGRATIONS_BASE_URL_V2 = "https://integrations.messagebird.com/v2";
+ static final String INTEGRATIONS_BASE_URL_V3 = "https://integrations.messagebird.com/v3";
private static String[] supportedLanguages = {"de-DE", "en-AU", "en-UK", "en-US", "es-ES", "es-LA", "fr-FR", "it-IT", "nl-NL", "pt-BR"};
+ static final String PARTNER_ACCOUNTS_BASE_URL = "https://partner-accounts.messagebird.com";
private static final String BALANCEPATH = "/balance";
private static final String CONTACTPATH = "/contacts";
@@ -114,10 +91,11 @@ public class MessageBirdClient {
private static final String VERIFYPATH = "/verify";
private static final String VERIFYEMAILPATH = "/verify/messages/email";
private static final String VOICEMESSAGESPATH = "/voicemessages";
- private static final String CONVERSATION_PATH = "/conversations";
- private static final String CONVERSATION_SEND_PATH = "/send";
- private static final String CONVERSATION_MESSAGE_PATH = "/messages";
- private static final String CONVERSATION_WEBHOOK_PATH = "/webhooks";
+ static final String CONVERSATION_PATH = "/conversations";
+ static final String CONVERSATION_SEND_PATH = "/send";
+ static final String CONVERSATION_MESSAGE_PATH = "/messages";
+ static final String CONVERSATION_WEBHOOK_PATH = "/webhooks";
+ static final String INTEGRATIONS_WHATSAPP_PATH = "/platforms/whatsapp";
static final String VOICECALLSPATH = "/calls";
static final String LEGSPATH = "/legs";
static final String RECORDINGPATH = "/recordings";
@@ -126,6 +104,10 @@ public class MessageBirdClient {
static final String VOICECALLFLOWPATH = "/call-flows";
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";
@@ -138,6 +120,9 @@ public class MessageBirdClient {
private static final String[] MESSAGE_LIST_FILTERS_VALS = {"originator", "recipient", "direction", "searchterm", "type", "contact_id", "status", "from", "until"};
private static final Set MESSAGE_LIST_FILTERS = new HashSet<>(Arrays.asList(MESSAGE_LIST_FILTERS_VALS));
+ private static final String[] CONVERSATION_MESSAGE_LIST_FILTERS_VALS = {"ids", "from"};
+ private static final Set CONVERSATION_MESSAGE_LIST_FILTERS = new HashSet<>(Arrays.asList(CONVERSATION_MESSAGE_LIST_FILTERS_VALS));
+
private final String DOWNLOADS = "Downloads";
private MessageBirdService messageBirdService;
@@ -488,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");
}
@@ -496,7 +481,7 @@ Verify getVerifyObject(String id) throws NotFoundException, GeneralException, Un
}
/**
- * @param id id is for the email message part of a verify object
+ * @param messageId is for the email message part of a verify object
* @return Verify object
* @throws NotFoundException if id is not found
* @throws UnauthorizedException if client is unauthorized
@@ -698,8 +683,8 @@ public VoiceCallFlowResponse sendVoiceCallFlow(final VoiceCallFlowRequest voiceC
* @param id String
* @param voiceCallFlowRequest VoiceCallFlowRequest
* @return VoiceCallFlowResponse
- * @throws UnauthorizedException
- * @throws GeneralException
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
*/
public VoiceCallFlowResponse updateVoiceCallFlow(String id, VoiceCallFlowRequest voiceCallFlowRequest)
throws UnauthorizedException, GeneralException {
@@ -1027,6 +1012,23 @@ public ConversationMessage viewConversationMessage(final String messageId)
return messageBirdService.requestByID(url, messageId, ConversationMessage.class);
}
+ /**
+ * Gets conversation messages based on query param.
+ *
+ * @param queryParams only `ids` and `from` is available as an option
+ * @return The retrieved messages.
+ */
+ public ConversationMessageList listConversationMessagesWithQueryParam(Map queryParams)
+ throws NotFoundException, GeneralException, UnauthorizedException {
+ for (String queryParam : queryParams.keySet()) {
+ if (!CONVERSATION_MESSAGE_LIST_FILTERS.contains(queryParam)) {
+ throw new IllegalArgumentException("Invalid filter name: " + queryParam);
+ }
+ }
+ String url = CONVERSATIONS_BASE_URL + CONVERSATION_MESSAGE_PATH;
+ return messageBirdService.requestByID(url, null, queryParams, ConversationMessageList.class);
+ }
+
/**
* Sends a message to an existing Conversation.
*
@@ -1848,4 +1850,438 @@ public String downloadFile(String id, String filename, String basePath) throws G
final String url = String.format("%s%s/%s", MESSAGING_BASE_URL, FILES_PATH, id);
return messageBirdService.getBinaryData(url, basePath, filename);
}
+
+ /****************************************************************************************************/
+ /** WhatsApp Templates **/
+ /****************************************************************************************************/
+
+ /**
+ * Create a WhatsApp message template through messagebird.
+ *
+ * @param template {@link Template} object to be created
+ * @return {@link TemplateResponse} response object
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws IllegalArgumentException invalid template format
+ */
+ public TemplateResponse createWhatsAppTemplate(final Template template)
+ throws UnauthorizedException, GeneralException, IllegalArgumentException {
+ template.validate();
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ 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.
+ *
+ * @param offset Number of objects to skip.
+ * @param limit Number of objects to take.
+ * @return List of templates.
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ */
+ public TemplateList listWhatsAppTemplates(final int offset, final int limit)
+ throws UnauthorizedException, GeneralException {
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ return messageBirdService.requestList(url, offset, limit, TemplateList.class);
+ }
+
+ /**
+ * Gets a WhatsAppTemplate listing with specified pagination options and a wabaID or channelID filter.
+ *
+ * @param offset Number of objects to skip.
+ * @param limit Number of objects to take.
+ * @param wabaID The WABA ID to filter templates by.
+ * @param channelID A channel ID filter to return only templates that can be sent via that channel.
+ * @return List of templates.
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public TemplateList listWhatsAppTemplates(final int offset, final int limit, final String wabaID, final String channelID)
+ throws UnauthorizedException, GeneralException, IllegalArgumentException {
+ validateWABAIDAndChannelIDArguments(wabaID, channelID);
+
+ Map map = new LinkedHashMap<>();
+ if (wabaID != null) map.put("wabaId", wabaID);
+ if (channelID != null) map.put("channelId", channelID);
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ return messageBirdService.requestList(url, map, offset, limit, TemplateList.class);
+ }
+
+ /**
+ * Gets a template listing with default pagination options.
+ *
+ * @return List of whatsapp templates.
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ */
+ public TemplateList listWhatsAppTemplates() throws UnauthorizedException, GeneralException {
+ final int offset = 0;
+ final int limit = 10;
+
+ return listWhatsAppTemplates(offset, limit);
+ }
+
+ /**
+ * Retrieves the template of an existing template name.
+ *
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @return {@code List} template list
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name is not found
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public List getWhatsAppTemplatesBy(final String templateName)
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
+ if (templateName == null) {
+ throw new IllegalArgumentException("Template name must be specified.");
+ }
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ return messageBirdService.requestByIdAsList(url, templateName, TemplateResponse.class);
+ }
+
+ /**
+ * Retrieves the template of an existing template name.
+ *
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @param wabaID An optional WABA ID to look for the template ID under.
+ * @param channelID An optional channel ID to specify. If the template can be sent via the channel, it will return the template.
+ *
+ * @return {@code List} template list
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name is not found under the given WABA or cannot be sent under the supplied channel ID
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public List getWhatsAppTemplatesBy(final String templateName, final String wabaID, final String channelID)
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
+ if (templateName == null) {
+ throw new IllegalArgumentException("Template name must be specified.");
+ }
+
+ String id = String.format("%s%s", templateName, getWabaIDOrChannelIDQuery(wabaID, channelID));
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ return messageBirdService.requestByIdAsList(url, id, TemplateResponse.class);
+ }
+
+ /**
+ * Retrieves the template of an existing template name and language under the first waba connected to the requesting user.
+ *
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @param language A language code as returned by getWhatsAppTemplateBy in the language variable
+ *
+ * @return {@code TemplateResponse} template
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name and language are not found under the first waba connected to the requesting user.
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public TemplateResponse fetchWhatsAppTemplateBy(final String templateName, final String language)
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
+ if (templateName == null || language == null) {
+ throw new IllegalArgumentException("Template name and language must be specified.");
+ }
+
+ String url = String.format(
+ "%s%s%s/%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language
+ );
+ return messageBirdService.request(url, TemplateResponse.class);
+ }
+
+ /**
+ * Retrieves the template of an existing template name and language under a WABA or for a channel.
+ *
+ * @param templateName A name as returned by getWhatsAppTemplateBy in the name variable
+ * @param language A language code as returned by getWhatsAppTemplateBy in the language variable
+ * @param wabaID An optional WABA ID to look for the template ID under.
+ * @param channelID An optional channel ID to specify. If the template can be sent via the channel, it will return the template.
+ *
+ * @return {@code TemplateResponse} template
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name and language are not found under the given WABA or cannot be sent under the supplied channel ID.
+ * @throws IllegalArgumentException if the provided arguments are not valid
+ */
+ public TemplateResponse fetchWhatsAppTemplateBy(final String templateName, final String language, final String wabaID, final String channelID)
+ throws GeneralException, UnauthorizedException, NotFoundException, IllegalArgumentException {
+ if (templateName == null || language == null) {
+ throw new IllegalArgumentException("Template name and language must be specified.");
+ }
+
+ String url = String.format(
+ "%s%s%s/%s/%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language,
+ getWabaIDOrChannelIDQuery(wabaID, channelID)
+ );
+ return messageBirdService.request(url, TemplateResponse.class);
+ }
+
+ /**
+ * Validates the WABA ID and Channel ID argument pair.
+ *
+ * @param wabaID A WABA ID.
+ * @param channelID A channel ID.
+ * @throws IllegalArgumentException if the argument pair is invalid.
+ */
+ private void validateWABAIDAndChannelIDArguments(String wabaID, String channelID)
+ throws IllegalArgumentException {
+ if (wabaID == null && channelID == null) {
+ throw new IllegalArgumentException("wabaID or channelID must be specified");
+ }
+
+ if (wabaID != null && channelID != null) {
+ throw new IllegalArgumentException("only supply wabaID or channelID - not both");
+ }
+ }
+
+ /**
+ * Validates the WABA ID and Channel ID argument pair and returns a valid query parameter string.
+ *
+ * @param wabaID A WABA ID.
+ * @param channelID A channel ID.
+ * @throws IllegalArgumentException if the argument pair is invalid.
+ */
+ private String getWabaIDOrChannelIDQuery(String wabaID, String channelID)
+ throws IllegalArgumentException {
+ validateWABAIDAndChannelIDArguments(wabaID, channelID);
+
+ String query = "";
+
+ if (wabaID != null) {
+ query = String.format("?wabaId=%s", wabaID);
+ }
+ if (channelID != null) {
+ query = String.format("?channelId=%s", channelID);
+ }
+
+ return query;
+ }
+
+ /**
+ * Delete templates of an existing template name.
+ *
+ * @param templateName A template name which is created on the MessageBird platform
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if template name is not found
+ */
+ public void deleteTemplatesBy(final String templateName)
+ throws UnauthorizedException, GeneralException, NotFoundException {
+ if (templateName == null) {
+ throw new IllegalArgumentException("Template name must be specified.");
+ }
+
+ String url = String.format(
+ "%s%s%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ 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
+ *
+ * @param childAccountRequest of child account to create
+ * @return ChildAccountResponse created
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ */
+ public ChildAccountCreateResponse createChildAccount(final ChildAccountRequest childAccountRequest) throws UnauthorizedException, GeneralException {
+ if (childAccountRequest.getName() == null || childAccountRequest.getName().isEmpty()) {
+ throw new IllegalArgumentException("Name must be specified.");
+ }
+
+ String url = String.format("%s%s", PARTNER_ACCOUNTS_BASE_URL, "/child-accounts");
+ return messageBirdService.sendPayLoad(url, childAccountRequest, ChildAccountCreateResponse.class);
+ }
+
+ /**
+ * Function to update a child account
+ *
+ * @param id of child account to update
+ * @return ChildAccountResponse created
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ */
+ public ChildAccountResponse updateChildAccount(final String name, final String id) throws UnauthorizedException, GeneralException {
+ if (name == null) {
+ throw new IllegalArgumentException("Name must be specified.");
+ }
+
+ if (id == null) {
+ throw new IllegalArgumentException("Child account id must be specified.");
+ }
+ final ChildAccountRequest childAccountRequest = new ChildAccountRequest();
+ childAccountRequest.setName(name);
+ final String url = String.format("%s/child-accounts/%s", PARTNER_ACCOUNTS_BASE_URL, id);
+ return messageBirdService.sendPayLoad("PATCH", url, childAccountRequest, ChildAccountResponse.class);
+ }
+
+ /**
+ * Function to get a child account
+ *
+ * @param id of child account to update
+ * @return ChildAccountResponse created
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if id is not found
+ */
+ public ChildAccountDetailedResponse getChildAccountById(final String id) throws UnauthorizedException, GeneralException, NotFoundException {
+ if (id == null) {
+ throw new IllegalArgumentException("Child account id must be specified.");
+ }
+ return messageBirdService.requestByID(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts", id, ChildAccountDetailedResponse.class);
+ }
+
+ /**
+ * Function to get a child account
+ *
+ * @return ChildAccountResponse created
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ */
+ public List getChildAccounts(final Integer offset, final Integer limit) throws UnauthorizedException, GeneralException {
+ verifyOffsetAndLimit(offset, limit);
+ return messageBirdService.requestList(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts", offset, limit, List.class);
+ }
+
+ /**
+ * Function to delete a child account
+ *
+ * @param id of child account to delete
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if id is not found
+ */
+ public void deleteChildAccount(final String id) throws UnauthorizedException, GeneralException, NotFoundException {
+ if (id == null) {
+ throw new IllegalArgumentException("Child account id must be specified.");
+ }
+
+ String url = String.format("%s/child-accounts", PARTNER_ACCOUNTS_BASE_URL);
+ 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/MessageBirdService.java b/api/src/main/java/com/messagebird/MessageBirdService.java
index fdbf6f43..43201fb7 100644
--- a/api/src/main/java/com/messagebird/MessageBirdService.java
+++ b/api/src/main/java/com/messagebird/MessageBirdService.java
@@ -1,16 +1,30 @@
package com.messagebird;
-import java.util.Map;
-
import com.messagebird.exceptions.GeneralException;
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
import com.messagebird.objects.PagedPaging;
+import java.util.List;
+import java.util.Map;
/**
* Created by rvt on 1/7/15.
*/
public interface MessageBirdService {
+
+ /**
+ * Send GET request . It will retrieve a json object R back.
+ *
+ * @author ssk910
+ * @param request path to the request, for example "/messages/id/language"
+ * @param clazz Class type to return
+ * @return new class with returned dataset
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if id not found
+ */
+ R request(String request, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException;
+
/**
* Execute a object by ID request. It will add the id to the request parameter and retreive a json object R back.
*
@@ -25,6 +39,20 @@ public interface MessageBirdService {
R requestByID(String request, String id, Map params, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException;
+ /**
+ * Execute a object by ID request. It will add the id to the request parameter and retrieve a list of an object E back.
+ *
+ * @author ssk910
+ * @param request path to the request, for example "/messages"
+ * @param id id of the object to request. id can be null in case request's that don't need a id, for example /balance
+ * @param elementClass Class type of List to return
+ * @return new list of elementClass
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException
+ */
+ List requestByIdAsList(String request, String id, Class elementClass) throws UnauthorizedException, GeneralException, NotFoundException;
+
/**
* Delete a object by ID.
*
@@ -36,6 +64,19 @@ public interface MessageBirdService {
*/
void deleteByID(String request, String id) throws UnauthorizedException, GeneralException, NotFoundException;
+ /**
+ * Send DELETE request. It will retrieve a json object R back.
+ *
+ * @author ssk910
+ * @param request path to the request, for example "/messages/id/language"
+ * @param clazz Class type to return
+ * @return class with returned dataset
+ * @throws UnauthorizedException if client is unauthorized
+ * @throws GeneralException general exception
+ * @throws NotFoundException if id not found
+ */
+ R delete(String request, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException;
+
/**
* Request a List 'of' object.
* Allow to request a listMessage or listViewMessages objects.
diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
index e31187b4..ef179b45 100644
--- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
+++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
@@ -1,6 +1,20 @@
package com.messagebird;
-import java.io.*;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.*;
+import com.messagebird.exceptions.GeneralException;
+import com.messagebird.exceptions.NotFoundException;
+import com.messagebird.exceptions.UnauthorizedException;
+import com.messagebird.objects.ErrorReport;
+import com.messagebird.objects.PagedPaging;
+import org.apache.maven.artifact.versioning.ComparableVersion;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
@@ -10,18 +24,16 @@
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
-import java.util.*;
-
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.MapperFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.messagebird.exceptions.GeneralException;
-import com.messagebird.exceptions.NotFoundException;
-import com.messagebird.exceptions.UnauthorizedException;
-import com.messagebird.objects.ErrorReport;
-import com.messagebird.objects.PagedPaging;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
/**
* Implementation of MessageBirdService
@@ -50,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().
@@ -61,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService {
private final String accessKey;
private final String serviceUrl;
- private final String clientVersion = "3.1.1";
+ private final String clientVersion = "6.3.1";
private final String userAgentString;
private Proxy proxy = null;
@@ -78,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);
}
/**
@@ -98,6 +111,12 @@ public MessageBirdServiceImpl(final String accessKey) {
this(accessKey, "https://rest.messagebird.com");
}
+ @Override
+ public R request(String request, Class clazz)
+ throws UnauthorizedException, GeneralException, NotFoundException {
+ return getJsonData(request, null, "GET", clazz);
+ }
+
@Override
public R requestByID(String request, String id, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException {
String path = "";
@@ -121,11 +140,27 @@ public R requestByID(String request, String id, Map params,
return getJsonData(request + path + queryParams, null, "GET", clazz);
}
+ @Override
+ public List requestByIdAsList(String request, String id, Class elementClass)
+ throws UnauthorizedException, GeneralException, NotFoundException {
+ String path = "";
+ if (id != null) {
+ path = "/" + id;
+ }
+
+ return getJsonDataAsList(request + path, null, "GET", elementClass);
+ }
+
@Override
public void deleteByID(String request, String id) throws UnauthorizedException, GeneralException, NotFoundException {
getJsonData(request + "/" + id, null, "DELETE", null);
}
+ @Override
+ public R delete(String request, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException {
+ return getJsonData(request, null, "DELETE", clazz);
+ }
+
@Override
public R requestList(String request, Integer offset, Integer limit, Class clazz) throws UnauthorizedException, GeneralException {
Map map = new LinkedHashMap<>();
@@ -212,6 +247,10 @@ public T getJsonData(final String request, final P payload, final String
return getJsonData(request, payload, requestType, new HashMap<>(), clazz);
}
+ public
List getJsonDataAsList(final String request, final P payload, final String requestType, final Class elementClass) throws UnauthorizedException, GeneralException, NotFoundException {
+ return getJsonDataAsList(request, payload, requestType, new HashMap<>(), elementClass);
+ }
+
public T getJsonData(final String request, final P payload, final String requestType, final Map headers, final Class clazz) throws UnauthorizedException, GeneralException, NotFoundException {
if (request == null) {
throw new IllegalArgumentException(REQUEST_VALUE_MUST_BE_SPECIFIED);
@@ -235,7 +274,10 @@ public T getJsonData(final String request, final P payload, final String
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
- return mapper.readValue(body, clazz);
+ // Prevents mismatched exception when clazz is null
+ return clazz == null
+ ? null
+ : this.readValue(mapper, body, clazz);
} catch (IOException ioe) {
throw new GeneralException(ioe);
}
@@ -246,6 +288,54 @@ public T getJsonData(final String request, final P payload, final String
return null;
}
+ // todo: need to refactor for duplicated code.
+ public
List getJsonDataAsList(final String request,
+ final P payload, final String requestType, final Map headers, final Class elementClass)
+ throws UnauthorizedException, GeneralException, NotFoundException {
+ if (request == null) {
+ throw new IllegalArgumentException(REQUEST_VALUE_MUST_BE_SPECIFIED);
+ }
+
+ String url = request;
+ if (!isURLAbsolute(url)) {
+ url = serviceUrl + url;
+ }
+ final APIResponse apiResponse = doRequest(requestType, url, headers, payload);
+
+ final String body = apiResponse.getBody();
+ final int status = apiResponse.getStatus();
+
+ if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_CREATED || status == HttpURLConnection.HTTP_ACCEPTED) {
+ try {
+ final ObjectMapper mapper = new ObjectMapper();
+ // If we as new properties, we don't want the system to fail, we rather want to ignore them
+ mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ // Enable case insensitivity to avoid parsing errors if parameters' case in api response doesn't match sdk's
+ mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
+ mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
+
+ // Prevents mismatched exception when clazz is null
+ return this.readValueAsList(mapper, body, elementClass);
+ } catch (IOException ioe) {
+ throw new GeneralException(ioe);
+ }
+ } else if (status == HttpURLConnection.HTTP_NO_CONTENT) {
+ return Collections.emptyList(); // no content doesn't mean an error
+ }
+ handleHttpFailStatuses(status, body);
+ return Collections.emptyList();
+ }
+
+ private T readValue(ObjectMapper mapper, String content, Class clazz)
+ throws JsonProcessingException {
+ return mapper.readValue(content, clazz);
+ }
+
+ private List readValueAsList(ObjectMapper mapper, String content, final Class elementClass)
+ throws JsonProcessingException {
+ return mapper.readValue(content, mapper.getTypeFactory().constructCollectionType(List.class, elementClass));
+ }
+
private void handleHttpFailStatuses(final int status, String body) throws UnauthorizedException, NotFoundException, GeneralException {
if (status == HttpURLConnection.HTTP_UNAUTHORIZED) {
final List errorReport = getErrorReportOrNull(body);
@@ -292,6 +382,9 @@
APIResponse doRequest(final String method, final String url, final Map 1.6) {
+ ComparableVersion java6 = new ComparableVersion("1.6");
+ if (JAVA_VERSION.compareTo(java6) > 0) {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
}
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZ");
}
- private double getVersion() throws GeneralException {
- String version = System.getProperty("java.version");
-
- try {
- int pos = version.indexOf('.');
- pos = version.indexOf('.', pos + 1);
-
- return Double.parseDouble(version.substring(0, pos));
- } catch (RuntimeException e) {
- // Thrown if the index is out of bounds, or when we can't parse a
- // double for some reason.
- throw new GeneralException(e);
- }
- }
-
/**
* Get the MessageBird error report data.
*
@@ -715,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/Request.java b/api/src/main/java/com/messagebird/Request.java
index 41d9283b..46048b0e 100644
--- a/api/src/main/java/com/messagebird/Request.java
+++ b/api/src/main/java/com/messagebird/Request.java
@@ -5,7 +5,10 @@
/**
* Holds request data needed to calculate a signature hash for incoming
* webhooks.
+ *
+ * @deprecated This class is being deprecated together with {@link RequestSigner}
*/
+@Deprecated
public class Request {
private final String timestamp;
@@ -17,11 +20,13 @@ public class Request {
/**
* Constructs a new request instance.
*
- * @param timestamp Timestamp provided in the MessageBird-Request-Timestamp
- * header.
+ * @param timestamp Timestamp provided in the MessageBird-Request-Timestamp
+ * header.
* @param queryParameters Query parameters in abc=foo&def=ghi format.
- * @param data Raw body of this request.
+ * @param data Raw body of this request.
+ * @deprecated
*/
+ @Deprecated
public Request(String timestamp, String queryParameters, byte[] data) {
if (timestamp == null || timestamp.isEmpty()) {
throw new IllegalArgumentException("Timestamp can not be null or empty");
diff --git a/api/src/main/java/com/messagebird/RequestSigner.java b/api/src/main/java/com/messagebird/RequestSigner.java
index e2f91d93..d79e464c 100644
--- a/api/src/main/java/com/messagebird/RequestSigner.java
+++ b/api/src/main/java/com/messagebird/RequestSigner.java
@@ -4,26 +4,30 @@
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:
* https://developers.messagebird.com/docs/verify-http-requests. Retrieve your
* signing key at https://dashboard.messagebird.com/developers/settings.
+ *
+ * @deprecated This class is being deprecated.
+ *
Use {@link RequestValidator} instead.
*/
+@Deprecated
public class RequestSigner {
private static final String ALGORITHM_SHA256 = "SHA-256";
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.
@@ -31,7 +35,9 @@ public class RequestSigner {
* @param key Signing key. Can be retrieved through
* https://dashboard.messagebird.com/developers/settings. This
* is NOT your API key.
+ * @deprecated Use {@link RequestValidator#RequestValidator(String)} )} instead.
*/
+ @Deprecated
public RequestSigner(byte[] key) {
this.secret = new SecretKeySpec(key, ALGORITHM_HMAC_SHA256);
}
@@ -42,13 +48,15 @@ public RequestSigner(byte[] key) {
*
* @param expectedSignature Signature from the MessageBird-Signature
* header in its original base64 encoded state.
- * @param request Request containing the values from the incoming webhook.
+ * @param request Request containing the values from the incoming webhook.
* @return True if the computed signature matches the expected signature.
+ * @deprecated Use {@link RequestValidator#validateSignature(String, String, byte[])} instead.
*/
+ @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);
}
}
@@ -59,9 +67,11 @@ public boolean isMatch(String expectedSignature, Request request) {
*
* @param expectedSignature Decoded (with base64) signature
* from the MessageBird-Signature header
- * @param request Request containing the values from the incoming webhook.
+ * @param request Request containing the values from the incoming webhook.
* @return True if the computed signature matches the expected signature.
+ * @deprecated Use {@link RequestValidator#validateSignature(String, String, byte[])} instead.
*/
+ @Deprecated
public boolean isMatch(byte[] expectedSignature, Request request) {
return Arrays.equals(computeSignature(request), expectedSignature);
}
@@ -93,7 +103,7 @@ private byte[] getSha256Hash(byte[] bytes) {
/**
* Stitches the two arrays together and returns a new one.
*
- * @param first Start of the new array.
+ * @param first Start of the new array.
* @param second End of the new array.
* @return New array based on first and second.
*/
diff --git a/api/src/main/java/com/messagebird/RequestValidator.java b/api/src/main/java/com/messagebird/RequestValidator.java
new file mode 100644
index 00000000..24ae6343
--- /dev/null
+++ b/api/src/main/java/com/messagebird/RequestValidator.java
@@ -0,0 +1,204 @@
+package com.messagebird;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier.BaseVerification;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.exceptions.SignatureVerificationException;
+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.
+ *
+ * @see Verify HTTP Requests
+ */
+public class RequestValidator {
+
+ /**
+ * Signature of signed request is set with header name 'MessageBird-Signature-JWT'
+ */
+ public static final String SIGNATURE_HEADER = "MessageBird-Signature-JWT";
+ private static final String ALGORITHM_SHA256 = "SHA-256";
+ private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
+ 'e', 'f'};
+
+ private final Algorithm HMAC256, HMAC384, HMAC512;
+
+ /**
+ * This field instructs Validator to not validate url_hash claim.
+ * It is recommended to not skip URL validation to ensure high security.
+ * but the ability to skip URL validation is necessary in some cases, e.g.
+ * your service is behind proxy or when you want to validate it yourself.
+ * Note that when true, no query parameters should be trusted.
+ * Defaults to false.
+ */
+ private final boolean skipURLValidation;
+
+ /**
+ * RequestValidator validates request signature with a customer signature key.
+ *
+ * @param signatureKey customer signature key. Can be retrieved through
+ * Developer Settings.
+ * This is NOT your API key.
+ * @see Verify HTTP Requests
+ */
+ public RequestValidator(String signatureKey) {
+ this(signatureKey, false);
+ }
+
+ /**
+ * RequestValidator validates webhook signature with a customer signature key.
+ *
+ * @param signatureKey customer signature key. Can be retrieved through
+ * Developer Settings.
+ * This is NOT your API key.
+ * @param skipURLValidation whether url_hash claim validation should be skipped.
+ * Note that when true, no query parameters should be trusted.
+ * @see Verify HTTP Requests
+ */
+ public RequestValidator(String signatureKey, boolean skipURLValidation) {
+ this.HMAC256 = Algorithm.HMAC256(signatureKey);
+ this.HMAC384 = Algorithm.HMAC384(signatureKey);
+ this.HMAC512 = Algorithm.HMAC512(signatureKey);
+ this.skipURLValidation = skipURLValidation;
+ }
+
+ /**
+ * Returns raw signature payload after validating a signature successfully,
+ * otherwise throws {@code RequestValidationException}.
+ *
+ * This JWT is signed with a MessageBird account unique secret key, ensuring the request is from MessageBird and
+ * a specific account.
+ * The JWT contains the following claims:
+ *
+ *
+ *
"url_hash" - the raw URL hashed with SHA256 ensuring the URL wasn't altered.
+ *
"payload_hash" - the raw payload hashed with SHA256 ensuring the payload wasn't altered.
+ *
"jti" - a unique token ID to implement an optional non-replay check (NOT validated by default).
+ *
"nbf" - the not before timestamp.
+ *
"exp" - the expiration timestamp is ensuring that a request isn't captured and used at a later time.
+ *
"iss" - the issuer name, always MessageBird.
+ *
+ *
+ * @param clock custom {@link Clock} instance to validate timestamp claims.
+ * @param signature the actual signature.
+ * @param url the raw url including the protocol, hostname and query string,
+ * {@code https://example.com/?example=42}.
+ * @param requestBody the raw request body.
+ * @return raw signature payload as {@link DecodedJWT} object.
+ * @throws RequestValidationException when the signature is invalid.
+ * @see Verify HTTP Requests
+ */
+ public DecodedJWT validateSignature(Clock clock, String signature, String url, byte[] requestBody)
+ throws RequestValidationException {
+ if (signature == null || signature.length() == 0)
+ throw new RequestValidationException("The signature can not be empty.");
+
+ if (!skipURLValidation && (url == null || url.length() == 0))
+ throw new RequestValidationException("The url can not be empty.");
+
+ DecodedJWT jwt = JWT.decode(signature);
+
+ Algorithm algorithm;
+ switch (jwt.getAlgorithm()) {
+ case "HS256":
+ algorithm = HMAC256;
+ break;
+ case "HS384":
+ algorithm = HMAC384;
+ break;
+ case "HS512":
+ algorithm = HMAC512;
+ break;
+ default:
+ throw new RequestValidationException(String.format("The signing method '%s' is invalid.", jwt.getAlgorithm()));
+ }
+
+ BaseVerification builder = (BaseVerification) JWT.require(algorithm)
+ .withIssuer("MessageBird")
+ .ignoreIssuedAt()
+ .acceptLeeway(1);
+
+ if (!skipURLValidation)
+ builder.withClaim("url_hash", calculateSha256(url.getBytes()));
+
+ 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.");
+ }
+ builder.withClaim("payload_hash", calculateSha256(requestBody));
+ } else if (payloadHashClaimExist) {
+ throw new RequestValidationException("The Claim 'payload_hash' is set but actual payload is missing.");
+ }
+
+ JWTVerifier verifier = clock == null ? builder.build() : builder.build(clock);
+
+ try {
+ return verifier.verify(jwt);
+ } catch (SignatureVerificationException e) {
+ throw new RequestValidationException("Signature is invalid.", e);
+ } catch (JWTVerificationException e) {
+ throw new RequestValidationException(e.getMessage(), e.getCause());
+ }
+ }
+
+ /**
+ * Returns raw signature payload after validating a signature successfully,
+ * otherwise throws {@code RequestValidationException}.
+ *
+ * @param signature the actual signature.
+ * @param url the raw url including the protocol, hostname and query string,
+ * {@code https://example.com/?example=42}.
+ * @param requestBody the raw request body.
+ * @return raw signature payload as {@link DecodedJWT} object.
+ * @throws RequestValidationException when the signature is invalid.
+ * @see RequestValidator#validateSignature(Clock, String, String, byte[])
+ */
+ public DecodedJWT validateSignature(String signature, String url, byte[] requestBody)
+ throws RequestValidationException {
+ return validateSignature(null, signature, url, requestBody);
+ }
+
+ /**
+ * Validates request signature with URL validation disabled.
+ * Note that no query parameters should be trusted and this only works if {@code RequestValidator} is constructed
+ * with {@code skipURLValidation} set to true.
+ *
+ * @param signature the actual signature.
+ * @param requestBody the raw request body.
+ * @return raw signature payload as {@link DecodedJWT} object.
+ * @throws RequestValidationException when the signature is invalid.
+ * @see RequestValidator#validateSignature(String, String, byte[])
+ */
+ public DecodedJWT validateSignature(String signature, byte[] requestBody)
+ throws RequestValidationException {
+ return validateSignature(null, signature, null, requestBody);
+ }
+
+ private static String calculateSha256(byte[] bytes) {
+ try {
+ return encodeHex(MessageDigest.getInstance(ALGORITHM_SHA256).digest(bytes));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RequestValidationException(e);
+ }
+ }
+
+ private static String encodeHex(final byte[] data) {
+ final int l = data.length;
+ final char[] out = new char[l << 1];
+ for (int i = 0, j = 0; i < l; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+}
diff --git a/api/src/main/java/com/messagebird/exceptions/RequestSigningException.java b/api/src/main/java/com/messagebird/exceptions/RequestSigningException.java
index 9f993375..d69a1922 100644
--- a/api/src/main/java/com/messagebird/exceptions/RequestSigningException.java
+++ b/api/src/main/java/com/messagebird/exceptions/RequestSigningException.java
@@ -2,7 +2,10 @@
/**
* Thrown if an error occurs during request signing.
+ *
+ * @deprecated This class is being deprecated together with {@link com.messagebird.RequestSigner}
*/
+@Deprecated
public class RequestSigningException extends RuntimeException {
public RequestSigningException() {
diff --git a/api/src/main/java/com/messagebird/exceptions/RequestValidationException.java b/api/src/main/java/com/messagebird/exceptions/RequestValidationException.java
new file mode 100644
index 00000000..f759b211
--- /dev/null
+++ b/api/src/main/java/com/messagebird/exceptions/RequestValidationException.java
@@ -0,0 +1,22 @@
+package com.messagebird.exceptions;
+
+/**
+ * Thrown if an error occurs during request signing.
+ */
+public class RequestValidationException extends RuntimeException {
+
+ public RequestValidationException() {
+ }
+
+ public RequestValidationException(String message) {
+ super(message);
+ }
+
+ public RequestValidationException(Throwable cause) {
+ super(cause);
+ }
+
+ public RequestValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/AccessKey.java b/api/src/main/java/com/messagebird/objects/AccessKey.java
new file mode 100644
index 00000000..4d529711
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/AccessKey.java
@@ -0,0 +1,98 @@
+package com.messagebird.objects;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class AccessKey {
+ private String id;
+ @JsonProperty("access_key")
+ private String accessKey;
+ private String mode;
+ private String description;
+ @JsonProperty("core_user_id")
+ private int coreUserId;
+ @JsonProperty("user_id")
+ private int userId;
+ @JsonProperty("external_id")
+ private int externalId;
+ private List roles;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getAccessKey() {
+ return accessKey;
+ }
+
+ public void setAccessKey(String accessKey) {
+ this.accessKey = accessKey;
+ }
+
+ public String getMode() {
+ return mode;
+ }
+
+ public void setMode(String mode) {
+ this.mode = mode;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public int getCoreUserId() {
+ return coreUserId;
+ }
+
+ public void setCoreUserId(int coreUserId) {
+ this.coreUserId = coreUserId;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public void setUserId(int userId) {
+ this.userId = userId;
+ }
+
+ public int getExternalId() {
+ return externalId;
+ }
+
+ public void setExternalId(int externalId) {
+ this.externalId = externalId;
+ }
+
+ public List getRoles() {
+ return roles;
+ }
+
+ public void setRoles(List roles) {
+ this.roles = roles;
+ }
+
+ @Override
+ public String toString() {
+ return "AccessKey{" +
+ "id='" + id + '\'' +
+ ", access_key='" + accessKey + '\'' +
+ ", mod='" + mode + '\'' +
+ ", description='" + description + '\'' +
+ ", core_user_id=" + coreUserId +
+ ", user_id=" + userId +
+ ", external_id=" + externalId +
+ ", roles=" + roles +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/ChildAccountCreateResponse.java b/api/src/main/java/com/messagebird/objects/ChildAccountCreateResponse.java
new file mode 100644
index 00000000..40ec38a4
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/ChildAccountCreateResponse.java
@@ -0,0 +1,53 @@
+package com.messagebird.objects;
+
+import java.util.List;
+
+public class ChildAccountCreateResponse extends ChildAccountResponse{
+ private List accessKeys;
+ private String signingKey;
+ private String invoiceAggregation;
+ private String paymentMoment;
+
+ public List getAccessKeys() {
+ return accessKeys;
+ }
+
+ public void setAccessKeys(List accessKeys) {
+ this.accessKeys = accessKeys;
+ }
+
+ public String getSigningKey() {
+ return signingKey;
+ }
+
+ public void setSigningKey(String signingKey) {
+ this.signingKey = signingKey;
+ }
+
+ public String getInvoiceAggregation() {
+ return invoiceAggregation;
+ }
+
+ public void setInvoiceAggregation(String invoiceAggregation) {
+ this.invoiceAggregation = invoiceAggregation;
+ }
+
+ public String getPaymentMoment() {
+ return paymentMoment;
+ }
+
+ public void setPaymentMoment(String paymentMoment) {
+ this.paymentMoment = paymentMoment;
+ }
+
+ @Override
+ public String toString() {
+ return "ChildAccountCreateResponse{" +
+ "id='" + getId() + '\'' +
+ ", name='" + getName() + '\'' +
+ ", accessKeys=" + accessKeys + '\'' +
+ ", invoiceAggregation='" + invoiceAggregation + '\'' +
+ ", paymentMoment='" + paymentMoment + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/ChildAccountDetailedResponse.java b/api/src/main/java/com/messagebird/objects/ChildAccountDetailedResponse.java
new file mode 100644
index 00000000..e39d5969
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/ChildAccountDetailedResponse.java
@@ -0,0 +1,22 @@
+package com.messagebird.objects;
+
+public class ChildAccountDetailedResponse extends ChildAccountResponse{
+ private String invoiceAggregation;
+
+ public String getInvoiceAggregation() {
+ return invoiceAggregation;
+ }
+
+ public void setInvoiceAggregation(String invoiceAggregation) {
+ this.invoiceAggregation = invoiceAggregation;
+ }
+
+ @Override
+ public String toString() {
+ return "ChildAccountDetailedResponse{" +
+ "id='" + getId() + '\'' +
+ ", name='" + getName() + '\'' +
+ ", invoiceAggregation='" + invoiceAggregation + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/ChildAccountRequest.java b/api/src/main/java/com/messagebird/objects/ChildAccountRequest.java
new file mode 100644
index 00000000..aa0e9e5f
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/ChildAccountRequest.java
@@ -0,0 +1,13 @@
+package com.messagebird.objects;
+
+public class ChildAccountRequest {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/ChildAccountResponse.java b/api/src/main/java/com/messagebird/objects/ChildAccountResponse.java
new file mode 100644
index 00000000..de232f05
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/ChildAccountResponse.java
@@ -0,0 +1,34 @@
+package com.messagebird.objects;
+
+import java.io.Serializable;
+
+public class ChildAccountResponse implements Serializable {
+ private static final long serialVersionUID = -8605510461438669942L;
+
+ private String id;
+ private String name;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "ChildAccountResponse{" +
+ "id='" + id + '\'' +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/Language.java b/api/src/main/java/com/messagebird/objects/Language.java
index 6ce76027..ccb63351 100644
--- a/api/src/main/java/com/messagebird/objects/Language.java
+++ b/api/src/main/java/com/messagebird/objects/Language.java
@@ -1,5 +1,7 @@
package com.messagebird.objects;
+import com.fasterxml.jackson.annotation.JsonValue;
+
/**
* Created by faizan on 09/12/15.
*/
@@ -11,7 +13,7 @@ public enum Language {
EN_US("en-us"),
ES_ES("es-es"),
FR_FR("fr-fr"),
- RU_RU("ru_ru"),
+ RU_RU("ru-ru"),
ZH_CN("zh-cn"),
EN_AU("en-au"),
ES_MX("es-mx"),
@@ -25,13 +27,21 @@ public enum Language {
PT_BR("pt-br"),
RO_RO("ro-ro");
- private String code;
+ final String code;
Language(String code) {
this.code = code;
}
+ @JsonValue
+ public String getCode() {
+ return code;
+ }
+
+ @Override
public String toString() {
- return this.code;
+ return "Language{" +
+ "code='" + code + '\'' +
+ '}';
}
}
diff --git a/api/src/main/java/com/messagebird/objects/MessageResponse.java b/api/src/main/java/com/messagebird/objects/MessageResponse.java
index 8b480e25..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;
@@ -196,7 +197,7 @@ public Map getTypeDetails() {
static public class Recipients implements Serializable {
private static final long serialVersionUID = 547164972757802213L;
-
+ private Integer totalCount;
private Integer totalSentCount;
private Integer totalDeliveredCount;
private Integer totalDeliveryFailedCount;
@@ -208,13 +209,18 @@ public Recipients() {
@Override
public String toString() {
return "Recipients{" +
- "totalSentCount=" + totalSentCount +
+ "totalCount=" + totalCount +
+ ", totalSentCount=" + totalSentCount +
", totalDeliveredCount=" + totalDeliveredCount +
", totalDeliveryFailedCount=" + totalDeliveryFailedCount +
", items=" + items +
'}';
}
+ public Integer getTotalCount() {
+ return totalCount;
+ }
+
/**
* The count of recipients that have the message pending (status sent, and buffered).
*
@@ -260,10 +266,20 @@ static public class Items implements Serializable {
private static final long serialVersionUID = -4104837036540050532L;
private BigInteger recipient;
+ private BigInteger originator;
private String status;
private Date statusDatetime;
+ private String recipientCountry;
+ private Integer recipientCountryPrefix;
+ private String recipientOperator;
+ private Integer messageLength;
+ private String statusReason;
@Nullable
private Price price;
+ private String mccmnc;
+ private String mcc;
+ private String mnc;
+ private int messagePartCount;
public Items() {
}
@@ -272,10 +288,20 @@ public Items() {
public String toString() {
return "Items{" +
"recipient=" + recipient +
+ ", originator=" + originator +
", status='" + status + '\'' +
", statusDatetime=" + statusDatetime +
+ ", recipientCountry='" + recipientCountry + '\'' +
+ ", recipientCountryPrefix=" + recipientCountryPrefix +
+ ", recipientOperator='" + recipientOperator + '\'' +
+ ", messageLength=" + messageLength +
+ ", statusReason='" + statusReason + '\'' +
", price=" + price +
- "}";
+ ", mccmnc='" + mccmnc + '\'' +
+ ", mcc='" + mcc + '\'' +
+ ", mnc='" + mnc + '\'' +
+ ", messagePartCount=" + messagePartCount +
+ '}';
}
/**
@@ -309,6 +335,45 @@ public Price getPrice() {
return price;
}
+ public BigInteger getOriginator() {
+ return originator;
+ }
+
+ public String getRecipientCountry() {
+ return recipientCountry;
+ }
+
+ public Integer getRecipientCountryPrefix() {
+ return recipientCountryPrefix;
+ }
+
+ public String getRecipientOperator() {
+ return recipientOperator;
+ }
+
+ public Integer getMessageLength() {
+ return messageLength;
+ }
+
+ public String getStatusReason() {
+ return statusReason;
+ }
+
+ public String getMccmnc() {
+ return mccmnc;
+ }
+
+ public String getMcc() {
+ return mcc;
+ }
+
+ public String getMnc() {
+ return mnc;
+ }
+
+ public int getMessagePartCount() {
+ return messagePartCount;
+ }
}
/**
@@ -318,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() {
@@ -333,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/ConversationContentHsm.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationContentHsm.java
index 3da74c2c..824f7fc8 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationContentHsm.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationContentHsm.java
@@ -18,17 +18,20 @@ public class ConversationContentHsm {
private String templateName;
private ConversationHsmLanguage language;
private List params;
+ private List components;
public ConversationContentHsm(
final String namespace,
final String templateName,
final ConversationHsmLanguage language,
- final List params
+ final List params,
+ final List components
) {
this.namespace = namespace;
this.templateName = templateName;
this.language = language;
this.params = params;
+ this.components = components;
}
public ConversationContentHsm(
@@ -85,6 +88,14 @@ public void setParams(final List params) {
this.params = params;
}
+ public List getComponents() {
+ return components;
+ }
+
+ public void setComponents(List components) {
+ this.components = components;
+ }
+
@Override
public String toString() {
return "ConversationContentHsm{" +
@@ -92,6 +103,7 @@ public String toString() {
", templateName='" + templateName + '\'' +
", language=" + language +
", params=" + params +
+ ", components=" + components +
'}';
}
}
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 82c935dd..1dea8280 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java
@@ -13,6 +13,7 @@ public class ConversationMessage {
private String id;
private String conversationId;
private String channelId;
+ private String trackId;
private ConversationMessageDirection direction;
private ConversationMessageStatus status;
private ConversationContentType type;
@@ -21,6 +22,7 @@ public class ConversationMessage {
private Date updatedDatetime;
private Map source;
private ConversationMessageTag tag;
+ private ConversationMessageMetadata metadata;
/**
* See: {@link ConversationPlatformConstants}
*/
@@ -114,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;
}
@@ -122,6 +132,14 @@ public void setPlatform(String platform) {
this.platform = platform;
}
+ public String getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(String trackId) {
+ this.trackId = trackId;
+ }
+
@Override
public String toString() {
return "ConversationMessage{" +
@@ -131,12 +149,14 @@ public String toString() {
", direction=" + direction +
", status=" + status +
", type=" + type +
+ ", trackID=" + trackId +
", content=" + content +
", createdDatetime=" + createdDatetime +
", updatedDatetime=" + updatedDatetime +
", source=" + source +
", tag=" + tag +
+ ", metadata=" + metadata +
", platform='" + platform + '\'' +
'}';
}
-}
+}
\ No newline at end of file
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/ConversationMessageRequest.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageRequest.java
index 626e6075..420213e3 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageRequest.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageRequest.java
@@ -11,6 +11,8 @@ public class ConversationMessageRequest {
private ConversationContent content;
private String channelId;
private String reportUrl;
+ private String trackId;
+ private String ttl;
private Map source;
public ConversationContentType getType() {
@@ -53,6 +55,21 @@ public void setSource(Map source) {
this.source = source;
}
+ public String getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(String trackId) {
+ this.trackId = trackId;
+ }
+ public String getTtl() {
+ return ttl;
+ }
+
+ public void setTtl(String ttl) {
+ this.ttl = ttl;
+ }
+
@Override
public String toString() {
return "ConversationMessageRequest{" +
@@ -61,6 +78,8 @@ public String toString() {
", channelId='" + channelId + '\'' +
", reportUrl='" + reportUrl + '\'' +
", source=" + source +
+ ", trackID=" + trackId +
+ ", ttl=" + ttl +
'}';
}
-}
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java
index 475c7b74..aee5c5f5 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageStatus.java
@@ -15,7 +15,25 @@ public enum ConversationMessageStatus {
READ("read"),
RECEIVED("received"),
SENT("sent"),
- UNSUPPORTED("unsupported");
+ UNSUPPORTED("unsupported"),
+ ACCEPTED("accepted"),
+ REJECTED("rejected"),
+ UNKNOWN("unknown"),
+ //WA specific statuses
+ TRANSMITTED("transmitted"),
+ //SMS specific statuses
+ DELIVERY_FAILED("delivery_failed"),
+ BUFFERED("buffered"),
+ EXPIRED("expired"),
+ //Email specific statuses
+ CLICKED("clicked"),
+ OPENED("opened"),
+ BOUNCE("bounce"),
+ SPAM_COMPLAINT("spam_complaint"),
+ OUT_OF_BOUNDED("out_of_bounded"),
+ DELAYED("delayed"),
+ LIST_UNSUBSCRIBE("list_unsubscribe"),
+ DISPATCHED("dispatched");
private final String status;
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java
new file mode 100644
index 00000000..7537da85
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java
@@ -0,0 +1,50 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Identifies the recipient of an outbound WhatsApp message, as reported back on
+ * status webhook payloads under {@code status.metadata.recipient}. Mirrors
+ * {@link ConversationSenderMetadata} on the inbound side.
+ *
+ *
{@code userId} is the recipient's BSUID (e.g. "US.13491208655302741918");
+ * {@code parentUserId} is the parent business-scoped user ID of the enterprise
+ * that owns the business portfolio it was scoped against (e.g.
+ * "US.ENT.11815799212886844830").
+ *
+ *
Either field may be {@code null}: Meta only supplies them for accounts
+ * enrolled in the BSUID rollout, and the enclosing {@code recipient} object is
+ * omitted entirely when neither is present. This is the only place a status
+ * payload carries the recipient's own identity — {@code messageMetadata.to} is
+ * an echo of the address the message was addressed to.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationRecipientMetadata {
+
+ private String userId;
+ private String parentUserId;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getParentUserId() {
+ return parentUserId;
+ }
+
+ public void setParentUserId(String parentUserId) {
+ this.parentUserId = parentUserId;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationRecipientMetadata{" +
+ "userId='" + userId + '\'' +
+ ", parentUserId='" + parentUserId + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSendRequest.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSendRequest.java
index f0f9fd5c..40ace8d7 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationSendRequest.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSendRequest.java
@@ -8,6 +8,8 @@ public class ConversationSendRequest {
private ConversationContent content;
private String from;
private String reportUrl;
+ private String trackId;
+ private String ttl;
private ConversationFallbackOption fallback;
private Map source;
private ConversationMessageTag tag;
@@ -90,6 +92,21 @@ public void setTag(ConversationMessageTag tag) {
this.tag = tag;
}
+ public void setTrackId(String trackId) {
+ this.trackId = trackId;
+ }
+
+ public String getTrackId() {
+ return trackId;
+ }
+
+ public String getTtl() {
+ return ttl;
+ }
+
+ public void setTtl(String ttl) {
+ this.ttl = ttl;
+ }
@Override
public String toString() {
return "ConversationSendRequest{" +
@@ -98,11 +115,11 @@ public String toString() {
", content=" + content +
", from='" + from + '\'' +
", reportUrl='" + reportUrl + '\'' +
+ ", trackId='" + trackId + '\'' +
+ ", ttl='" + ttl + '\'' +
", fallback=" + fallback + '\'' +
", tags=" + tag +
", source='" + source + '\'' +
'}';
}
-}
-
-
+}
\ No newline at end of file
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/ConversationStartRequest.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStartRequest.java
index 36ebc68a..6f7c84fe 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/ConversationStartRequest.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStartRequest.java
@@ -14,6 +14,8 @@ public class ConversationStartRequest {
private ConversationMessageTag tag;
private String channelId;
private String reportUrl;
+ private String trackId;
+ private String ttl;
public ConversationStartRequest(
final String to,
@@ -91,6 +93,14 @@ public void setTag(ConversationMessageTag tag) {
this.tag = tag;
}
+ public String getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(String trackId) {
+ this.trackId = trackId;
+ }
+
@Override
public String toString() {
return "ConversationStartRequest{" +
@@ -101,6 +111,8 @@ public String toString() {
", tag=" + tag +
", channelId='" + channelId + '\'' +
", reportUrl='" + reportUrl + '\'' +
+ ", trackId='" + trackId + '\'' +
+ ", ttl='" + ttl + '\'' +
'}';
}
-}
+}
\ No newline at end of file
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/HSMCurrency.java b/api/src/main/java/com/messagebird/objects/conversations/HSMCurrency.java
new file mode 100644
index 00000000..24ea4be6
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/HSMCurrency.java
@@ -0,0 +1,31 @@
+package com.messagebird.objects.conversations;
+
+public class HSMCurrency {
+
+ private String currencyCode;
+ private int amount;
+
+ public String getCurrencyCode() {
+ return currencyCode;
+ }
+
+ public void setCurrencyCode(String currencyCode) {
+ this.currencyCode = currencyCode;
+ }
+
+ public int getAmount() {
+ return amount;
+ }
+
+ public void setAmount(int amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public String toString() {
+ return "HSMCurrency{" +
+ "currencyCode='" + currencyCode + '\'' +
+ ", amount=" + amount +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/Media.java b/api/src/main/java/com/messagebird/objects/conversations/Media.java
new file mode 100644
index 00000000..4a9bf4ea
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/Media.java
@@ -0,0 +1,31 @@
+package com.messagebird.objects.conversations;
+
+public class Media {
+
+ private String url;
+ private String caption;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ @Override
+ public String toString() {
+ return "Media{" +
+ "url='" + url + '\'' +
+ ", caption='" + caption + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java b/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java
new file mode 100644
index 00000000..d3a7c20b
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageComponent.java
@@ -0,0 +1,82 @@
+package com.messagebird.objects.conversations;
+
+import java.util.List;
+
+public class MessageComponent {
+
+ private MessageComponentType type;
+ 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;
+ }
+
+ public MessageComponentType getType() {
+ return type;
+ }
+
+ public String getSub_type() {
+ return sub_type;
+ }
+
+ public void setSub_type(String sub_type) {
+ this.sub_type = sub_type;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ public List getParameters() {
+ return parameters;
+ }
+
+ 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 + '\'' +
+ ", 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
new file mode 100644
index 00000000..9f9290cf
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
@@ -0,0 +1,54 @@
+package com.messagebird.objects.conversations;
+
+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"),
+ 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;
+
+ MessageComponentType(final String type) {
+ this.type = type;
+ }
+
+ @JsonCreator
+ public static MessageComponentType forValue(String value) {
+ Objects.requireNonNull(value, "Value cannot be null");
+ return TYPE_MAP.get(value.toLowerCase(Locale.ROOT));
+ }
+
+ @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/conversations/MessageParam.java b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
new file mode 100644
index 00000000..6432369f
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
@@ -0,0 +1,125 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+
+public class MessageParam {
+
+ private TemplateMediaType type;
+ private String text;
+ private String payload;
+ private HSMCurrency currency;
+ private String dateTime;
+ private Media document;
+ private Media image;
+ private Media video;
+ @JsonProperty("expiration_time")
+ private String expirationTime;
+ @JsonProperty("coupon_code")
+ private String couponCode;
+
+ public TemplateMediaType getType() {
+ return type;
+ }
+
+ public void setType(TemplateMediaType type) {
+ this.type = type;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ if (StringUtils.isBlank(text)) {
+ throw new IllegalArgumentException("Text cannot be null or empty");
+ }
+ this.text = text;
+ }
+
+ public String getPayload() {
+ return payload;
+ }
+
+ public void setPayload(String payload) {
+ this.payload = payload;
+ }
+
+ public HSMCurrency getCurrency() {
+ return currency;
+ }
+
+ public void setCurrency(HSMCurrency currency) {
+ this.currency = currency;
+ }
+
+ public String getDateTime() {
+ return dateTime;
+ }
+
+ public void setDateTime(String dateTime) {
+ if (StringUtils.isBlank(dateTime)) {
+ throw new IllegalArgumentException("dateTime cannot be null or empty");
+ }
+ this.dateTime = dateTime;
+ }
+
+ public Media getDocument() {
+ return document;
+ }
+
+ public void setDocument(Media document) {
+ this.document = document;
+ }
+
+ public Media getImage() {
+ return image;
+ }
+
+ public void setImage(Media image) {
+ this.image = image;
+ }
+
+ public Media getVideo() { return video; }
+
+ public void setVideo(Media video) { this.video = video; }
+
+ public String getExpirationTime() {
+ return expirationTime;
+ }
+
+ public void setExpirationTime(String expirationTime) {
+ if (StringUtils.isBlank(expirationTime)) {
+ throw new IllegalArgumentException("expirationTime cannot be null or empty");
+ }
+ this.expirationTime = expirationTime;
+ }
+
+ public String getCouponCode() {
+ return couponCode;
+ }
+
+ public void setCouponCode(String couponCode) {
+ if (StringUtils.isBlank(couponCode)) {
+ throw new IllegalArgumentException("couponCode cannot be null or empty");
+ }
+ this.couponCode = couponCode;
+ }
+
+ @Override
+ public String toString() {
+ 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
new file mode 100644
index 00000000..580dcc97
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
@@ -0,0 +1,60 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Collections;
+
+public enum TemplateMediaType {
+
+ IMAGE("image"),
+ DOCUMENT("document"),
+ VIDEO("video"),
+ TEXT("text"),
+ CURRENCY("currency"),
+ DATETIME("date_time"),
+ PAYLOAD("payload"),
+ 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;
+
+ TemplateMediaType(final String type) {
+ this.type = type;
+ }
+
+ @JsonCreator
+ public static TemplateMediaType forValue(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null");
+ }
+ return TYPE_MAP.get(value.toLowerCase());
+ }
+
+ @JsonValue
+ public String toJson() {
+ return getType();
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ return getType();
+ }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java b/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java
new file mode 100644
index 00000000..3edb1db9
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMCategory.java
@@ -0,0 +1,48 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * An enum for HSMComponentFormat
+ *
+ * @see HSMComponentFormat
+ * @author ssk910
+ */
+public enum HSMCategory {
+
+ AUTHENTICATION("AUTHENTICATION"),
+ UTILITY("UTILITY"),
+ MARKETING("MARKETING");
+
+ private final String category;
+
+ HSMCategory(String category) {
+ this.category = category;
+ }
+
+ @JsonCreator
+ public static HSMCategory forValue(String value) {
+ for (HSMCategory hsmCategory : HSMCategory.values()) {
+ if (hsmCategory.getCategory().equals(value)) {
+ return hsmCategory;
+ }
+ }
+
+ return null;
+ }
+
+ @JsonValue
+ public String toJson() {
+ return getCategory();
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ @Override
+ public String toString() {
+ return getCategory();
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java
new file mode 100644
index 00000000..ed93847f
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponent.java
@@ -0,0 +1,196 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+
+/**
+ * A class for HSMComponent object
+ *
+ * @see HSMComponent
+ * @author ssk910
+ */
+public class HSMComponent {
+
+ private HSMComponentType type;
+ private HSMComponentFormat format;
+ private String text;
+ @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() {
+ return type;
+ }
+
+ public void setType(HSMComponentType type) {
+ this.type = type;
+ }
+
+ public HSMComponentFormat getFormat() {
+ return format;
+ }
+
+ public void setFormat(HSMComponentFormat format) {
+ this.format = format;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ if (StringUtils.isBlank(text)) {
+ throw new IllegalArgumentException("Text cannot be null or empty");
+ }
+ this.text = text;
+ }
+
+ public List getButtons() {
+ return buttons;
+ }
+
+ 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;
+ }
+
+ 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() {
+ 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();
+ }
+
+ /**
+ * Check if this component is valid.
+ *
+ * @throws IllegalArgumentException Occurs when validation is not passed.
+ */
+ public void validateComponent() throws IllegalArgumentException {
+ try {
+ this.validateButtons();
+ this.validateComponentExample();
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Component validation failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Check if button list is valid.
+ *
+ * @throws IllegalArgumentException Occurs when validation is not passed.
+ */
+ private void validateButtons() throws IllegalArgumentException {
+ if (this.buttons == null) {
+ return;
+ }
+
+ for (final HSMComponentButton button : this.buttons) {
+ button.validateButtonExample();
+ }
+ }
+
+ /**
+ * Check for header_text and header_url.
+ *
+ * @throws IllegalArgumentException Occurs when {@code header_text} or {@code header_url} is not able to use.
+ */
+ private void validateComponentExample() throws IllegalArgumentException {
+ final boolean isExampleNotNull = this.example != null;
+ final boolean isHeaderTextNotEmpty =
+ isExampleNotNull && !(this.example.getHeader_text() == null || this.example.getHeader_text()
+ .isEmpty());
+ final boolean isHeaderUrlNotEmpty =
+ isExampleNotNull && !(this.example.getHeader_url() == null || this.example.getHeader_url()
+ .isEmpty());
+
+ if (isHeaderTextNotEmpty) {
+ this.checkHeaderText();
+ }
+
+ if (isHeaderUrlNotEmpty) {
+ this.checkHeaderUrl();
+ }
+ }
+
+ /**
+ * Check if header_text is able to use.
+ *
+ * @throws IllegalArgumentException Occurs when type is not {@code HEADER} and format is not {@code TEXT}.
+ */
+ private void checkHeaderText() throws IllegalArgumentException {
+ if (!(HSMComponentType.HEADER.equals(type) && HSMComponentFormat.TEXT.equals(format))) {
+ throw new IllegalArgumentException("\"header_text\" is available for only HEADER type and TEXT format.");
+ }
+ }
+
+ /**
+ * Check if header_url is able to use.
+ *
+ * @throws IllegalArgumentException Occurs when type is not {@code HEADER} and format is not {@code IMAGE}.
+ */
+ private void checkHeaderUrl() throws IllegalArgumentException {
+ 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
new file mode 100644
index 00000000..75204df0
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButton.java
@@ -0,0 +1,132 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * HSMComponentButton
+ *
+ * @see HSMComponentButton
+ * @author ssk910
+ */
+public class HSMComponentButton {
+
+ 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
new file mode 100644
index 00000000..06e6eb91
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentButtonType.java
@@ -0,0 +1,50 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * HSMComponentButtonType
+ *
+ * @see HSMComponentButtonType
+ * @author ssk910
+ */
+public enum HSMComponentButtonType {
+
+ PHONE_NUMBER("PHONE_NUMBER"),
+ URL("URL"),
+ QUICK_REPLY("QUICK_REPLY"),
+ OTP("OTP"),
+ COPY_CODE("COPY_CODE");
+
+ private final String type;
+
+ HSMComponentButtonType(String type) {
+ this.type = type;
+ }
+
+ @JsonCreator
+ public static HSMComponentButtonType forValue(String value) {
+ for (HSMComponentButtonType hsmComponentButtonType : HSMComponentButtonType.values()) {
+ if (hsmComponentButtonType.getType().equals(value)) {
+ return hsmComponentButtonType;
+ }
+ }
+
+ 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/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/HSMComponentFormat.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentFormat.java
new file mode 100644
index 00000000..ac7d199d
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentFormat.java
@@ -0,0 +1,49 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * An enum for HSMComponentFormat
+ *
+ * @see HSMComponentFormat
+ * @author ssk910
+ */
+public enum HSMComponentFormat {
+
+ TEXT("TEXT"),
+ IMAGE("IMAGE"),
+ DOCUMENT("DOCUMENT"),
+ VIDEO("VIDEO");
+
+ private final String format;
+
+ HSMComponentFormat(String format) {
+ this.format = format;
+ }
+
+ @JsonCreator
+ public static HSMComponentFormat forValue(String value) {
+ for (HSMComponentFormat hsmComponentFormat : HSMComponentFormat.values()) {
+ if (hsmComponentFormat.getFormat().equals(value)) {
+ return hsmComponentFormat;
+ }
+ }
+
+ return null;
+ }
+
+ @JsonValue
+ public String toJson() {
+ return getFormat();
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ @Override
+ public String toString() {
+ return getFormat();
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java
new file mode 100644
index 00000000..8610146a
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMComponentType.java
@@ -0,0 +1,59 @@
+package com.messagebird.objects.integrations;
+
+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
+ *
+ * @see HSMComponentType
+ */
+public enum HSMComponentType {
+ BODY("BODY"),
+ HEADER("HEADER"),
+ FOOTER("FOOTER"),
+ 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;
+
+ HSMComponentType(String type) {
+ this.type = type;
+ }
+
+ @JsonCreator
+ public static HSMComponentType forValue(String value) {
+ Objects.requireNonNull(value, "Value cannot be null");
+ return TYPE_MAP.get(value.toLowerCase(Locale.ROOT));
+ }
+
+ @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/HSMExample.java b/api/src/main/java/com/messagebird/objects/integrations/HSMExample.java
new file mode 100644
index 00000000..a28fcc38
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMExample.java
@@ -0,0 +1,54 @@
+package com.messagebird.objects.integrations;
+
+import java.util.List;
+
+/**
+ * HSMExample object
+ *
+ * @see HSMExample object
+ * @author ssk910
+ */
+public class HSMExample {
+
+ /* Example values for HEADER type components, TEXT format */
+ private List header_text;
+
+ /* Example set of values for the body text variables */
+ private List> body_text;
+
+ /* Example values for HEADER type components, IMAGE format */
+ private List header_url;
+
+ public List getHeader_text() {
+ return header_text;
+ }
+
+ public void setHeader_text(List header_text) {
+ this.header_text = header_text;
+ }
+
+ public List> getBody_text() {
+ return body_text;
+ }
+
+ public void setBody_text(List> body_text) {
+ this.body_text = body_text;
+ }
+
+ public List getHeader_url() {
+ return header_url;
+ }
+
+ public void setHeader_url(List header_url) {
+ this.header_url = header_url;
+ }
+
+ @Override
+ public String toString() {
+ return "HSMExample{" +
+ "header_text=" + header_text +
+ ", body_text=" + body_text +
+ ", header_url=" + header_url +
+ '}';
+ }
+}
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
new file mode 100644
index 00000000..1b1980f1
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/HSMStatus.java
@@ -0,0 +1,53 @@
+package com.messagebird.objects.integrations;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * An enum for HSMStatus object
+ *
+ * @see HSMStatus object
+ * @author ssk910
+ */
+public enum HSMStatus {
+
+ NEW("NEW"),
+ APPROVED("APPROVED"),
+ PENDING("PENDING"),
+ REJECTED("REJECTED"),
+ PENDING_DELETION("PENDING_DELETION"),
+ DELETED("DELETED"),
+ DISABLED("DISABLED"),
+ PAUSED("PAUSED");
+
+ private final String status;
+
+ HSMStatus(String status) {
+ this.status = status;
+ }
+
+ @JsonCreator
+ public static HSMStatus forValue(String value) {
+ for (HSMStatus hsmStatus : HSMStatus.values()) {
+ if (hsmStatus.getStatus().equals(value)) {
+ return hsmStatus;
+ }
+ }
+
+ return null;
+ }
+
+ @JsonValue
+ public String toJson() {
+ return getStatus();
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return getStatus();
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/Template.java b/api/src/main/java/com/messagebird/objects/integrations/Template.java
new file mode 100644
index 00000000..f6a83e93
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/Template.java
@@ -0,0 +1,173 @@
+package com.messagebird.objects.integrations;
+
+import java.util.List;
+
+/**
+ * Template Object as integrations API request.
+ *
+ * @see Integrations API
+ * @author ssk910
+ */
+public class Template {
+
+ private String name;
+ private String language;
+ private String wabaID;
+ private List components;
+ private HSMCategory category;
+ private boolean ctaURLLinkTrackingOptedOut;
+
+ public Template() {
+ }
+
+
+ public Template(String name, String language, String wabaID,
+ List components, HSMCategory category, boolean ctaURLLinkTrackingOptedOut) {
+ this.name = name;
+ this.language = language;
+ this.wabaID = wabaID;
+ this.components = components;
+ this.category = category;
+ this.ctaURLLinkTrackingOptedOut = ctaURLLinkTrackingOptedOut;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getWABAID() {
+ return wabaID;
+ }
+
+ public void setWABAID(String wabaID) {
+ this.wabaID = wabaID;
+ }
+
+ public List getComponents() {
+ return components;
+ }
+
+ public void setComponents(List components) {
+ this.components = components;
+ }
+
+ public HSMCategory getCategory() {
+ return category;
+ }
+
+ public void setCategory(HSMCategory category) {
+ this.category = category;
+ }
+
+ public void setCtaURLLinkTrackingOptedOut (boolean ctaURLLinkTrackingOptedOut) {
+ this.ctaURLLinkTrackingOptedOut = ctaURLLinkTrackingOptedOut;
+ }
+
+ public boolean getCtaURLLinkTrackingOptedOut () {
+ return ctaURLLinkTrackingOptedOut;
+ }
+
+ @Override
+ public String toString() {
+ return "WhatsAppTemplate{" +
+ "name='" + name + '\'' +
+ ", language='" + language + '\'' +
+ ", wabaID='" + wabaID + '\'' +
+ ", components=" + components +
+ ", category='" + category + '\'' +
+ ", ctaURLLinkTrackingOptedOut='" + ctaURLLinkTrackingOptedOut + '\'' +
+ '}';
+ }
+
+ /**
+ * Validate required fields: components, name, language, category
+ *
+ * @throws IllegalArgumentException if required fields are invalid.
+ */
+ public void validate() throws IllegalArgumentException {
+ this.validateComponents();
+ this.validateName();
+ this.validateLanguage();
+ this.validateWABAID();
+ this.validateCategory();
+ }
+
+ /**
+ * Check if components field is valid.
+ *
+ * @throws IllegalArgumentException If components field is null or empty list.
+ */
+ private void validateComponents() throws IllegalArgumentException {
+ final boolean componentsNotEmpty = !(this.components == null || this.components.isEmpty());
+
+ if (componentsNotEmpty) {
+ for (final HSMComponent component : this.components) {
+ component.validateComponent();
+ }
+ } else {
+ throw new IllegalArgumentException("A \"components\" field is required and should not be empty list.");
+ }
+ }
+
+ /**
+ * Check if name field is valid.
+ *
+ * @throws IllegalArgumentException If name field is null or empty string.
+ */
+ private void validateName() {
+ if (this.name == null) {
+ throw new IllegalArgumentException("A \"name\" field is required.");
+ } else if (this.name.length() == 0) {
+ throw new IllegalArgumentException("A \"name\" field can not be an empty string.");
+ }
+ }
+
+ /**
+ * Check if language field is valid.
+ *
+ * @throws IllegalArgumentException If language field is null or empty string.
+ */
+ private void validateLanguage() {
+ if (this.language == null) {
+ throw new IllegalArgumentException("A \"language\" field is required.");
+ } else if (this.language.length() == 0) {
+ throw new IllegalArgumentException("A \"language\" field can not be an empty string.");
+ }
+ }
+
+ /**
+ * Check if wabaID field is valid.
+ *
+ * @throws IllegalArgumentException If wabaID field is null or empty string.
+ */
+ private void validateWABAID() {
+ if (this.wabaID == null) {
+ throw new IllegalArgumentException("A \"wabaID\" field is required.");
+ } else if (this.wabaID.length() == 0) {
+ throw new IllegalArgumentException("A \"wabaID\" field can not be an empty string.");
+ }
+ }
+
+ /**
+ * Check if category field is valid.
+ *
+ * @throws IllegalArgumentException If category field is null.
+ */
+ private void validateCategory() {
+ if (this.category == null) {
+ throw new IllegalArgumentException("A \"category\" field is required.");
+ }
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/TemplateList.java b/api/src/main/java/com/messagebird/objects/integrations/TemplateList.java
new file mode 100644
index 00000000..5cea4023
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/TemplateList.java
@@ -0,0 +1,12 @@
+package com.messagebird.objects.integrations;
+
+import com.messagebird.objects.ListBase;
+
+/**
+ * Response object representing the Template list type.
+ *
+ * @author ssk910
+ */
+public class TemplateList extends ListBase {
+
+}
diff --git a/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java b/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java
new file mode 100644
index 00000000..3c75fabe
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/integrations/TemplateResponse.java
@@ -0,0 +1,149 @@
+package com.messagebird.objects.integrations;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Template response using integrations API.
+ *
+ * @author ssk910
+ */
+public class TemplateResponse implements Serializable {
+
+ private static final long serialVersionUID = 7154209824478715861L;
+ private String name;
+ private String language;
+ private HSMCategory category;
+ private List components;
+ private HSMStatus status;
+ private String rejectedReason;
+ private String wabaID;
+ private String namespace;
+
+ private boolean ctaURLLinkTrackingOptedOut;
+
+ private HSMQualityScore qualityScore;
+
+ private Date createdAt;
+ private Date updatedAt;
+
+ public static long getSerialVersionUID() {
+ return serialVersionUID;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public HSMCategory getCategory() {
+ return category;
+ }
+
+ public void setCategory(HSMCategory category) {
+ this.category = category;
+ }
+
+ public List getComponents() {
+ return components;
+ }
+
+ public void setComponents(List components) {
+ this.components = components;
+ }
+
+ public HSMStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(HSMStatus status) {
+ this.status = status;
+ }
+
+ public String getRejectedReason() {
+ return rejectedReason;
+ }
+
+ public void setRejectedReason(String rejectedReason) {
+ this.rejectedReason = rejectedReason;
+ }
+
+ public String getWabaID() {
+ return wabaID;
+ }
+
+ public void setWabaID(String wabaID) {
+ this.wabaID = wabaID;
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public Date getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public Date getUpdatedAt() {
+ return updatedAt;
+ }
+
+ 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{" +
+ "name='" + name + '\'' +
+ ", language='" + language + '\'' +
+ ", category='" + category + '\'' +
+ ", components=" + components +
+ ", status='" + status + '\'' +
+ ", rejectedReason='" + rejectedReason + '\'' +
+ ", wabaID='" + wabaID + '\'' +
+ ", namespace='" + namespace + '\'' +
+ ", ctaURLLinkTrackingOptedOut='" + ctaURLLinkTrackingOptedOut + '\'' +
+ ", qualityScore='" + qualityScore + '\'' +
+ ", createdAt=" + createdAt +
+ ", updatedAt=" + updatedAt +
+ '}';
+ }
+
+}
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/SipResponseCode.java b/api/src/main/java/com/messagebird/objects/voicecalls/SipResponseCode.java
new file mode 100644
index 00000000..7061c8fe
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/SipResponseCode.java
@@ -0,0 +1,73 @@
+package com.messagebird.objects.voicecalls;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+
+/**
+ * More details, including additional descriptions and common caused can be found here: https://developers.messagebird.com/api/voice-calling/#sip-status-codes
+ * @author leandropinto
+ *
+ */
+public enum SipResponseCode {
+ //Successful
+ OK,
+ //The server understood the request, but is refusing to fulfill it.
+ FORBIDDEN,
+ //The server has definitive information that the user does not exist at the domain specified in the Request-URI.
+ NOT_FOUND,
+ //Couldn't find the user in time.
+ REQUEST_TIMEOUT,
+ //The user existed once, but is not available here any more.
+ GONE,
+ //Callee currently unavailable.
+ TEMPORARILY_UNAVAILABLE,
+ //Request-URI incomplete.
+ ADDRESS_INCOMPLETE,
+ //Callee is busy.
+ BUSY_HERE,
+ //Some aspect of the session description or the Request-URI is not acceptable.
+ NOT_ACCEPTABLE_HERE,
+ //The server could not fulfill the request due to some unexpected condition.
+ INTERNAL_SERVER_ERROR,
+ //The server does not have the ability to fulfill the request, such as because it does not recognize the request method.
+ NOT_IMPLEMENTED,
+ //The server is acting as a gateway or proxy, and received an invalid response from a downstream server while attempting to fulfill the request.
+ BAD_GATEWAY,
+ //The server is undergoing maintenance or is temporarily overloaded and so cannot process the request.
+ SERVICE_UNAVAILABLE;
+
+ @JsonCreator
+ public static SipResponseCode forValue(Integer value) {
+ switch (value) {
+ case 200:
+ return OK;
+ case 403:
+ return FORBIDDEN;
+ case 404:
+ return NOT_FOUND;
+ case 408:
+ return REQUEST_TIMEOUT;
+ case 410:
+ return GONE;
+ case 480:
+ return TEMPORARILY_UNAVAILABLE;
+ case 484:
+ return ADDRESS_INCOMPLETE;
+ case 486:
+ return BUSY_HERE;
+ case 488:
+ return NOT_ACCEPTABLE_HERE;
+ case 500:
+ return INTERNAL_SERVER_ERROR;
+ case 501:
+ return NOT_IMPLEMENTED;
+ case 502:
+ return BAD_GATEWAY;
+ case 503:
+ return SERVICE_UNAVAILABLE;
+
+ default:
+ throw new IllegalArgumentException("Unknown sip response code: " + value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java
index 8ece36ec..573b42be 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCall.java
@@ -63,10 +63,12 @@ public Webhook getWebhook() {
return webhook;
}
+ @JsonIgnore
public void setWebhook(String url) {
this.setWebhook(url, null);
}
+ @JsonIgnore
public void setWebhook(String url, String token) {
this.webhook.setUrl(url);
this.webhook.setToken(token);
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java
index d64f0d2c..53130c77 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlow.java
@@ -23,6 +23,9 @@ public class VoiceCallFlow implements Serializable {
@JsonProperty("default")
private boolean defaultCall;
+ @JsonProperty("maxDuration")
+ private Integer maxDuration;
+
private Date createdAt;
private Date updatedAt;
@@ -37,10 +40,12 @@ public void setId(String id) {
this.id = id;
}
+ @Deprecated
public String getTitle() {
return title;
}
+ @Deprecated
public void setTitle(String title) {
this.title = title;
}
@@ -69,6 +74,10 @@ public void setDefaultCall(boolean defaultCall) {
this.defaultCall = defaultCall;
}
+ public Integer getMaxDuration() { return maxDuration; }
+
+ public void setMaxDuration(Integer maxDuration) { this.maxDuration = maxDuration; }
+
public Date getCreatedAt() {
return createdAt;
}
@@ -101,6 +110,7 @@ public String toString() {
", record=" + record +
", steps=" + steps +
", default=" + defaultCall +
+ ", maxDuration=" + maxDuration +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java
index 3990cfb2..e447d9d9 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallFlowRequest.java
@@ -36,10 +36,12 @@ public void setId(String id) {
this.id = id;
}
+ @Deprecated
public String getTitle() {
return title;
}
+ @Deprecated
public void setTitle(String title) {
this.title = title;
}
diff --git a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallLeg.java b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallLeg.java
index 93dc0fa2..d09b7bdf 100644
--- a/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallLeg.java
+++ b/api/src/main/java/com/messagebird/objects/voicecalls/VoiceCallLeg.java
@@ -28,6 +28,7 @@ public class VoiceCallLeg {
public final Date updatedAt;
public final Date answeredAt;
public final Date endedAt;
+ public final SipResponseCode sipResponseCode;
@JsonCreator
@@ -44,7 +45,8 @@ public VoiceCallLeg(
@JsonProperty("createdAt") Date createdAt,
@JsonProperty("updatedAt") Date updatedAt,
@JsonProperty("answeredAt") Date answeredAt,
- @JsonProperty("endedAt") Date endedAt
+ @JsonProperty("endedAt") Date endedAt,
+ @JsonProperty("sipResponseCode") SipResponseCode sipResponseCode
) {
this.id = id;
this.callID = callID;
@@ -59,6 +61,7 @@ public VoiceCallLeg(
this.updatedAt = updatedAt;
this.answeredAt = answeredAt;
this.endedAt = endedAt;
+ this.sipResponseCode = sipResponseCode;
}
@Override
@@ -77,6 +80,7 @@ public String toString() {
", updatedAt='" + updatedAt + '\'' +
", answeredAt='" + answeredAt + '\'' +
", endedAt='" + endedAt + '\'' +
+ ", sipResponseCode='" + sipResponseCode + '\'' +
'}';
}
@@ -97,11 +101,12 @@ public boolean equals(Object o) {
Objects.equals(createdAt, that.createdAt) &&
Objects.equals(updatedAt, that.updatedAt) &&
Objects.equals(answeredAt, that.answeredAt) &&
- Objects.equals(endedAt, that.endedAt);
+ Objects.equals(endedAt, that.endedAt) &&
+ Objects.equals(sipResponseCode, that.sipResponseCode);
}
@Override
public int hashCode() {
- return Objects.hash(id, callID, source, destination, status, direction, cost, currency, duration, createdAt, updatedAt, answeredAt, endedAt);
+ return Objects.hash(id, callID, source, destination, status, direction, cost, currency, duration, createdAt, updatedAt, answeredAt, endedAt, sipResponseCode);
}
}
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 956b177b..b11c2f54 100644
--- a/api/src/test/java/com/messagebird/MessageBirdClientTest.java
+++ b/api/src/test/java/com/messagebird/MessageBirdClientTest.java
@@ -4,13 +4,20 @@
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
import com.messagebird.objects.*;
+import com.messagebird.objects.conversations.*;
+import com.messagebird.objects.integrations.Template;
+import com.messagebird.objects.integrations.TemplateList;
+import com.messagebird.objects.integrations.TemplateResponse;
import com.messagebird.objects.voicecalls.*;
+import java.util.ArrayList;
+
+import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
+import static org.junit.Assume.assumeNotNull;
-import java.io.*;
import java.math.BigInteger;
import java.util.Collections;
import java.util.HashMap;
@@ -19,6 +26,8 @@
import java.util.Map;
import static com.messagebird.MessageBirdClient.*;
+import static com.messagebird.TestUtil.*;
+import static com.messagebird.TestUtil.createChildAccountDetailedResponse;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals;
@@ -36,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
@@ -118,7 +130,7 @@ public void testListScheduledMessages() throws Exception {
filters.put("status", "scheduled");
when(messageBirdServiceMock.requestList("/messages", filters, null, null, MessageList.class))
- .thenReturn(mockedResponse);
+ .thenReturn(mockedResponse);
final MessageList response = messageBirdClientMock.listMessagesFiltered(null, null, filters);
assertNotNull(response);
@@ -129,7 +141,7 @@ public void testListScheduledMessages() throws Exception {
public void testListScheduledMessagesWrongFilter() throws Exception {
Map filters = new LinkedHashMap<>();
filters.put("does not exist", null);
-
+
messageBirdClient.listMessagesFiltered(null, null, filters);
}
@@ -147,7 +159,7 @@ public void testSendDeleteMessage() throws Exception {
assertNotNull(mr.getId());
assertEquals(mr.getReference(), reference);
assertEquals(mr.getBody(), body);
- assertEquals(mr.getDatacoding(), DataCodingType.plain);
+ assertEquals(DataCodingType.plain,mr.getDatacoding());
// Deleting of a message is not yet supported in test mode
// Thread.sleep(1000);
@@ -217,8 +229,8 @@ public void testSendDeleteFlashMessage() throws Exception {
final String body = "Body test message Über € " + messageBirdMSISDN;
final MessageResponse mr = messageBirdClient.sendFlashMessage("originator", body, Collections.singletonList(messageBirdMSISDN));
assertNotNull(mr.getId());
- assertSame(mr.getType(), MsgType.flash);
- assertSame(mr.getMclass(), MClassType.flash);
+ assertSame(MsgType.flash,mr.getType() );
+ assertSame(MClassType.flash, mr.getMclass());
// Deleting of a message is not yet supported in test mode
// Thread.sleep(1000);
@@ -289,8 +301,8 @@ public void testSendVoiceMessage() throws Exception {
final VoiceMessageResponse mr = messageBirdClient.sendVoiceMessage(vm);
assertNotNull(mr.getId());
assertEquals(mr.getBody(), body);
- assertSame(mr.getIfMachine(), IfMachineType.hangup);
- assertSame(mr.getVoice(), VoiceType.male);
+ assertSame(IfMachineType.hangup, mr.getIfMachine() );
+ assertSame(VoiceType.male, mr.getVoice());
// Deleting of a message is not yet supported in test mode
// Thread.sleep(1000);
@@ -317,17 +329,6 @@ public void testSendVoiceMessage2() throws Exception {
assertNotNull(mr.getId());
assertEquals(mr.getBody(), body);
assertEquals(mr.getReference(), reference);
-
- Thread.sleep(500);
- // Viewing of a message is not yet supported in test mode
- // final VoiceMessageResponse mr2 = messageBirdClient.viewVoiceMessage(mr.getId());
- // assertTrue(mr2.getId() != null);
- // assertTrue(mr2.getBody().equals(body));
- // assertTrue(mr2.getReference().equals(reference));
-
- // Deleting of a message is not yet supported in test mode
- // Thread.sleep(1000);
- // Gives 404 messageBirdClient.deleteVoiceMessage(mr.getId());
}
/**
@@ -362,7 +363,7 @@ public void testSendVerifyToken1() throws UnauthorizedException, GeneralExceptio
}
@Test
- public void testSendVerifyTokenAndGetVerifyObject() throws UnauthorizedException, GeneralException, NotFoundException {
+ public void testSendVerifyTokenAndGetVerifyObject() throws UnauthorizedException, GeneralException {
Verify verify = messageBirdClient.sendVerifyToken(messageBirdMSISDN.toString());
assertFalse("href is empty", verify.getHref().isEmpty());
assertFalse("id is empty", verify.getId().isEmpty());
@@ -376,7 +377,7 @@ public void testSendVerifyTokenAndGetVerifyObject() throws UnauthorizedException
}
@Test
- public void testVerifyToken() throws UnauthorizedException, GeneralException, UnsupportedEncodingException {
+ public void testVerifyToken() throws UnauthorizedException, GeneralException {
Verify verify = messageBirdClient.sendVerifyToken(messageBirdMSISDN.toString());
assertFalse("href is empty", verify.getHref().isEmpty());
@@ -392,7 +393,7 @@ public void testVerifyToken() throws UnauthorizedException, GeneralException, Un
}
@Test
- public void testDeleteVerifyToken() throws UnauthorizedException, GeneralException, NotFoundException, UnsupportedEncodingException {
+ public void testDeleteVerifyToken() throws UnauthorizedException, GeneralException {
Verify verify = messageBirdClient.sendVerifyToken(messageBirdMSISDN.toString());
assertFalse("href is empty", verify.getHref().isEmpty());
try {
@@ -430,7 +431,6 @@ public void shouldThrowIllegalArgumentExceptionWhenSourceOfVoiceCallIsMissing()
voiceCall.setDestination("ANY_DESTINATION");
final VoiceCallFlow voiceCallFlow = new VoiceCallFlow();
- voiceCallFlow.setTitle("Test title");
VoiceStep voiceStep = new VoiceStep();
voiceStep.setAction("say");
@@ -446,13 +446,11 @@ public void shouldThrowIllegalArgumentExceptionWhenSourceOfVoiceCallIsMissing()
}
@Test(expected = IllegalArgumentException.class)
- public void shouldThrowIllegalArgumentExceptionWhenDestinationOfVoiceCallIsMissing() throws UnauthorizedException,
- GeneralException {
+ public void shouldThrowIllegalArgumentExceptionWhenDestinationOfVoiceCallIsMissing() throws UnauthorizedException, GeneralException {
final VoiceCall voiceCall = new VoiceCall();
voiceCall.setSource("ANY_SOURCE");
final VoiceCallFlow voiceCallFlow = new VoiceCallFlow();
- voiceCallFlow.setTitle("Test title");
VoiceStep voiceStep = new VoiceStep();
voiceStep.setAction("say");
@@ -499,8 +497,7 @@ public void testViewVoiceCall() throws UnauthorizedException, GeneralException,
}
@Test
- public void testDeleteVoiceCall() throws UnauthorizedException,
- GeneralException, NotFoundException {
+ public void testDeleteVoiceCall() throws UnauthorizedException, GeneralException, NotFoundException {
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
@@ -807,11 +804,11 @@ public void testListNumbersForPurchase() throws IllegalArgumentException, Genera
MessageBirdClient messageBirdClientMock = new MessageBirdClient(messageBirdServiceMock);
when(messageBirdServiceMock.requestByID(url, "NL", PhoneNumbersResponse.class))
- .thenReturn(mockedResponse);
+ .thenReturn(mockedResponse);
final PhoneNumbersResponse response = messageBirdClientMock.listNumbersForPurchase("NL");
verify(messageBirdServiceMock, times(1)).requestByID(url, "NL", PhoneNumbersResponse.class);
-
+
assertNotNull(response);
assertEquals(response, mockedResponse);
}
@@ -830,9 +827,9 @@ public void testListNumbersForPurchaseWithParams() throws IllegalArgumentExcepti
options.setLimit(1);
options.setNumber(562);
options.setSearchPattern(PhoneNumberSearchPattern.START);
-
+
when(messageBirdServiceMock.requestByID(url, "US", options.toHashMap(), PhoneNumbersResponse.class))
- .thenReturn(mockedResponse);
+ .thenReturn(mockedResponse);
final PhoneNumbersResponse response = messageBirdClientMock.listNumbersForPurchase("US", options);
verify(messageBirdServiceMock, times(1)).requestByID(url, "US", options.toHashMap(), PhoneNumbersResponse.class);
@@ -848,26 +845,26 @@ public void testPurchaseNumber() throws UnauthorizedException, GeneralException
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientMock = new MessageBirdClient(messageBirdServiceMock);
-
+
final Map payload = new LinkedHashMap();
payload.put("number", "15625267429");
payload.put("countryCode", "US");
payload.put("billingIntervalMonths", 1);
-
+
when(messageBirdServiceMock.sendPayLoad(url, payload, PurchasedNumberCreatedResponse.class))
- .thenReturn(purchasedNumberMockData);
+ .thenReturn(purchasedNumberMockData);
final PurchasedNumberCreatedResponse response = messageBirdClientMock.purchaseNumber("15625267429", "US", 1);
verify(messageBirdServiceMock, times(1)).sendPayLoad(url, payload, PurchasedNumberCreatedResponse.class);
assertNotNull(response);
assertEquals(response, purchasedNumberMockData);
}
-
+
@Test
public void testListPurchasedNumbers() throws UnauthorizedException, GeneralException, NotFoundException {
final String url = String.format("%s/phone-numbers", NUMBERS_CALLS_BASE_URL);
-
+
PurchasedNumbersResponse purchasedNumbersMockData = new PurchasedNumbersResponse();
-
+
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientMock = new MessageBirdClient(messageBirdServiceMock);
@@ -878,7 +875,7 @@ public void testListPurchasedNumbers() throws UnauthorizedException, GeneralExce
filter.addTag("tag");
when(messageBirdServiceMock.requestByID(url, null, filter.toHashMap(), PurchasedNumbersResponse.class))
- .thenReturn(purchasedNumbersMockData);
+ .thenReturn(purchasedNumbersMockData);
final PurchasedNumbersResponse response = messageBirdClientMock.listPurchasedNumbers(filter);
@@ -890,13 +887,13 @@ public void testListPurchasedNumbers() throws UnauthorizedException, GeneralExce
@Test
public void testViewPurchasedNumber() throws UnauthorizedException, GeneralException, NotFoundException {
final String url = String.format("%s/phone-numbers", NUMBERS_CALLS_BASE_URL);
-
+
PurchasedNumber purchasedNumberMockData = new PurchasedNumber();
-
+
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientMock = new MessageBirdClient(messageBirdServiceMock);
when(messageBirdServiceMock.requestByID(url, "15625267429", PurchasedNumber.class))
- .thenReturn(purchasedNumberMockData);
+ .thenReturn(purchasedNumberMockData);
final PurchasedNumber response = messageBirdClientMock.viewPurchasedNumber("15625267429");
verify(messageBirdServiceMock, times(1)).requestByID(url, "15625267429", PurchasedNumber.class);
@@ -908,17 +905,17 @@ public void testViewPurchasedNumber() throws UnauthorizedException, GeneralExce
public void updatePurchasedNumber() throws UnauthorizedException, GeneralException {
final String phoneNumber = "15625267429";
final String url = String.format("%s/phone-numbers/%s", NUMBERS_CALLS_BASE_URL, phoneNumber);
-
+
PurchasedNumber updatedNumberMock = new PurchasedNumber();
-
+
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientMock = new MessageBirdClient(messageBirdServiceMock);
-
+
final Map> payload = new HashMap>();
payload.put("tags", Collections.singletonList("tag"));
-
+
when(messageBirdServiceMock.sendPayLoad("PATCH", url, payload, PurchasedNumber.class))
- .thenReturn(updatedNumberMock);
+ .thenReturn(updatedNumberMock);
final PurchasedNumber response = messageBirdClientMock.updateNumber(phoneNumber, "tag");
verify(messageBirdServiceMock, times(1)).sendPayLoad("PATCH", url, payload, PurchasedNumber.class);
assertNotNull(response);
@@ -929,10 +926,10 @@ public void updatePurchasedNumber() throws UnauthorizedException, GeneralExcept
public void deletePurchasedNumber() throws UnauthorizedException, GeneralException, NotFoundException {
final String phoneNumber = "15625267429";
final String url = String.format("%s/phone-numbers", NUMBERS_CALLS_BASE_URL);
-
+
MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
MessageBirdClient messageBirdClientMock = new MessageBirdClient(messageBirdServiceMock);
-
+
messageBirdClientMock.cancelNumber(phoneNumber);
verify(messageBirdServiceMock, times(1)).deleteByID(url, phoneNumber);
}
@@ -1042,4 +1039,582 @@ public void testDownloadFileWithNullBasePath() throws GeneralException, Unauthor
String url = MESSAGING_BASE_URL + FILES_PATH + "/" + id;
verify(messageBirdServiceMock, times(1)).getBinaryData(url, null, filename);
}
+
+ /****************************************************************************************************/
+ /** Testing WhatsApp Templates **/
+ /****************************************************************************************************/
+
+ @Test
+ public void testCreateWhatsAppTemplate() 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",
+ 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 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 {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ when(messageBirdServiceMock.requestList(url, 0, 0, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates(0, 0);
+ verify(messageBirdServiceMock, times(1)).requestList(url, 0, 0, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
+ @Test
+ public void testListWhatsAppTemplatesDefault() throws UnauthorizedException, GeneralException {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ when(messageBirdServiceMock.requestList(url, 0, 10, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates();
+ verify(messageBirdServiceMock, times(1)).requestList(url, 0, 10, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
+ @Test
+ public void testListWhatsAppTemplatesByWABAID() throws UnauthorizedException, GeneralException {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ final String wabaID = "testWABAID";
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ Map map = new LinkedHashMap<>();
+ map.put("wabaId", wabaID);
+
+ when(messageBirdServiceMock.requestList(url, map, 0, 0, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates(0, 0, wabaID, null);
+ verify(messageBirdServiceMock, times(1)).requestList(url, map, 0, 0, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
+ @Test
+ public void testListWhatsAppTemplatesByChannelID() throws UnauthorizedException, GeneralException {
+ final TemplateList templateList = TestUtil.createWhatsAppTemplateList("sample_template_name");
+ final String channelID = "channel-id";
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V3,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ Map map = new LinkedHashMap<>();
+ map.put("channelId", channelID);
+
+ when(messageBirdServiceMock.requestList(url, map, 0, 0, TemplateList.class))
+ .thenReturn(templateList);
+
+ final TemplateList response = messageBirdClientInjectMock.listWhatsAppTemplates(0, 0, null, channelID);
+ verify(messageBirdServiceMock, times(1)).requestList(url, map, 0, 0, TemplateList.class);
+ assertNotNull(response);
+ for(int i = 0; i < response.getItems().size() ; i++) {
+ assertReflectionEquals(response.getItems().get(i), templateList.getItems().get(i));
+ }
+ }
+
+ @Test
+ public void testGetWhatsAppTemplatesBy() throws GeneralException, UnauthorizedException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final TemplateResponse templateResponse1 = TestUtil.createWhatsAppTemplateResponse(templateName, "en_US");
+ final TemplateResponse templateResponse2 = TestUtil.createWhatsAppTemplateResponse("another_template", "en_US");
+ final List templateList = new ArrayList<>();
+ templateList.add(templateResponse1);
+ templateList.add(templateResponse2);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+
+ when(messageBirdServiceMock.requestByIdAsList(url, templateName, TemplateResponse.class))
+ .thenReturn(templateList);
+
+ final List response = messageBirdClientInjectMock.getWhatsAppTemplatesBy(templateName);
+ verify(messageBirdServiceMock, times(1)).requestByIdAsList(url, templateName, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.size(), templateList.size());
+ for(int i = 0; i < response.size() ; i++) {
+ assertReflectionEquals(response.get(i), templateList.get(i));
+ }
+ }
+
+ @Test
+ public void testGetWhatsAppTemplatesByNameAndWABAID() throws GeneralException, UnauthorizedException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String wabaID = "testWABAID";
+ final TemplateResponse templateResponse1 = TestUtil.createWhatsAppTemplateResponse(templateName, "en_US");
+ final TemplateResponse templateResponse2 = TestUtil.createWhatsAppTemplateResponse("another_template", "en_US");
+ final List templateList = new ArrayList<>();
+ templateList.add(templateResponse1);
+ templateList.add(templateResponse2);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ String id = String.format(
+ "%s?wabaId=%s",
+ templateName,
+ wabaID
+ );
+
+ when(messageBirdServiceMock.requestByIdAsList(url, id, TemplateResponse.class))
+ .thenReturn(templateList);
+
+ final List response = messageBirdClientInjectMock.getWhatsAppTemplatesBy(templateName, wabaID, null);
+ verify(messageBirdServiceMock, times(1)).requestByIdAsList(url, id, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.size(), templateList.size());
+ for(int i = 0; i < response.size() ; i++) {
+ assertReflectionEquals(response.get(i), templateList.get(i));
+ }
+ }
+
+ @Test
+ public void testGetWhatsAppTemplatesByNameForChannelID() throws GeneralException, UnauthorizedException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String channelID = "channel-id";
+ final TemplateResponse templateResponse1 = TestUtil.createWhatsAppTemplateResponse(templateName, "en_US");
+ final TemplateResponse templateResponse2 = TestUtil.createWhatsAppTemplateResponse("another_template", "en_US");
+ final List templateList = new ArrayList<>();
+ templateList.add(templateResponse1);
+ templateList.add(templateResponse2);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH
+ );
+ String id = String.format(
+ "%s?channelId=%s",
+ templateName,
+ channelID
+ );
+
+ when(messageBirdServiceMock.requestByIdAsList(url, id, TemplateResponse.class))
+ .thenReturn(templateList);
+
+ final List response = messageBirdClientInjectMock.getWhatsAppTemplatesBy(templateName, null, channelID);
+ verify(messageBirdServiceMock, times(1)).requestByIdAsList(url, id, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.size(), templateList.size());
+ for(int i = 0; i < response.size() ; i++) {
+ assertReflectionEquals(response.get(i), templateList.get(i));
+ }
+ }
+
+ @Test
+ public void testFetchWhatsAppTemplateBy() throws UnauthorizedException, GeneralException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String language = "ko";
+ final TemplateResponse template = TestUtil.createWhatsAppTemplateResponse(templateName, language);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s/%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language
+ );
+
+ when(messageBirdServiceMock.request(url, TemplateResponse.class))
+ .thenReturn(template);
+
+ final TemplateResponse response = messageBirdClientInjectMock.fetchWhatsAppTemplateBy(templateName, language);
+ verify(messageBirdServiceMock, times(1)).request(url, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), template.getName());
+ assertEquals(response.getLanguage(), template.getLanguage());
+ assertEquals(response.getStatus(), template.getStatus());
+ assertReflectionEquals(response.getComponents(), template.getComponents());
+ }
+
+ @Test
+ public void testFetchWhatsAppTemplateByNameAndLanguageAndWABAID() throws UnauthorizedException, GeneralException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String language = "ko";
+ final String wabaID = "testWABAID";
+ final TemplateResponse template = TestUtil.createWhatsAppTemplateResponse(templateName, language);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s/%s/%s?wabaId=%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language,
+ wabaID
+ );
+
+ when(messageBirdServiceMock.request(url, TemplateResponse.class))
+ .thenReturn(template);
+
+ final TemplateResponse response = messageBirdClientInjectMock.fetchWhatsAppTemplateBy(templateName, language, wabaID, null);
+ verify(messageBirdServiceMock, times(1)).request(url, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), template.getName());
+ assertEquals(response.getLanguage(), template.getLanguage());
+ assertEquals(response.getStatus(), template.getStatus());
+ assertReflectionEquals(response.getComponents(), template.getComponents());
+ }
+
+ @Test
+ public void testFetchWhatsAppTemplateByNameAndLanguageForChannelID() throws UnauthorizedException, GeneralException, NotFoundException {
+ final String templateName = "sample_template_name";
+ final String language = "ko";
+ final String channelID = "channel-id";
+ final TemplateResponse template = TestUtil.createWhatsAppTemplateResponse(templateName, language);
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s/%s/%s?channelId=%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName,
+ language,
+ channelID
+ );
+
+ when(messageBirdServiceMock.request(url, TemplateResponse.class))
+ .thenReturn(template);
+
+ final TemplateResponse response = messageBirdClientInjectMock.fetchWhatsAppTemplateBy(templateName, language, null, channelID);
+ verify(messageBirdServiceMock, times(1)).request(url, TemplateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getName(), template.getName());
+ assertEquals(response.getLanguage(), template.getLanguage());
+ assertEquals(response.getStatus(), template.getStatus());
+ assertReflectionEquals(response.getComponents(), template.getComponents());
+ }
+
+ @Test
+ public void testDeleteTemplatesByName() throws UnauthorizedException, GeneralException, NotFoundException {
+ final String templateName = "sample_template_name";
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ String url = String.format(
+ "%s%s%s/%s",
+ INTEGRATIONS_BASE_URL_V2,
+ INTEGRATIONS_WHATSAPP_PATH,
+ TEMPLATES_PATH,
+ templateName
+ );
+
+ when(messageBirdServiceMock.delete(url, null)).thenReturn(null);
+ messageBirdClientInjectMock.deleteTemplatesBy(templateName);
+ verify(messageBirdServiceMock).delete(url, null);
+ }
+
+ @Test
+ public void testCreateChildAccounts() throws GeneralException, UnauthorizedException {
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+ ChildAccountCreateResponse childAccountCreateResponse = createChildAccountCreateResponse();
+ ChildAccountRequest childAccountRequest = new ChildAccountRequest();
+ childAccountRequest.setName("name");
+ when(messageBirdServiceMock.sendPayLoad(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts" , childAccountRequest, ChildAccountCreateResponse.class))
+ .thenReturn(childAccountCreateResponse);
+ final ChildAccountCreateResponse response = messageBirdClientInjectMock.createChildAccount(childAccountRequest);
+
+ verify(messageBirdServiceMock, times(1))
+ .sendPayLoad(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts" , childAccountRequest, ChildAccountCreateResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getId(), childAccountCreateResponse.getId());
+ assertEquals(response.getName(), childAccountCreateResponse.getName());
+ assertEquals(response.getInvoiceAggregation(), childAccountCreateResponse.getInvoiceAggregation());
+ assertEquals(response.getSigningKey(), childAccountCreateResponse.getSigningKey());
+ assertEquals(response.getAccessKeys().get(0).getId(), childAccountCreateResponse.getAccessKeys().get(0).getId());
+ assertEquals(response.getAccessKeys().get(0).getAccessKey(), childAccountCreateResponse.getAccessKeys().get(0).getAccessKey());
+ assertEquals(response.getAccessKeys().get(0).getMode(), childAccountCreateResponse.getAccessKeys().get(0).getMode());
+ }
+
+ @Test
+ public void testGetChildAccount() throws GeneralException, UnauthorizedException, NotFoundException {
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+ ChildAccountDetailedResponse childAccountDetailedResponse = createChildAccountDetailedResponse();
+ when(messageBirdServiceMock.requestByID(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts", "ANY_ID", ChildAccountDetailedResponse.class))
+ .thenReturn(childAccountDetailedResponse);
+ ChildAccountDetailedResponse response = messageBirdClientInjectMock.getChildAccountById("ANY_ID");
+
+ verify(messageBirdServiceMock, times(1))
+ .requestByID(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts", "ANY_ID", ChildAccountDetailedResponse.class);
+ assertNotNull(response);
+ assertEquals(response.getId(), childAccountDetailedResponse.getId());
+ assertEquals(response.getName(), childAccountDetailedResponse.getName());
+ assertEquals(response.getInvoiceAggregation(), childAccountDetailedResponse.getInvoiceAggregation());
+ }
+
+ @Test
+ public void testGetChildAccounts() throws GeneralException, UnauthorizedException {
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+ List childAccountResponses = Collections.singletonList(createChildAccountResponse());
+ when(messageBirdServiceMock.requestList(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts", 1, 10, List.class))
+ .thenReturn(childAccountResponses);
+
+ List response = messageBirdClientInjectMock.getChildAccounts(1, 10);
+
+ verify(messageBirdServiceMock, times(1))
+ .requestList(PARTNER_ACCOUNTS_BASE_URL + "/child-accounts", 1, 10, List.class);
+ assertNotNull(response);
+ assertEquals(response.get(0).getId(), childAccountResponses.get(0).getId());
+ assertEquals(response.get(0).getName(), childAccountResponses.get(0).getName());
+ }
+
+ @Test
+ public void testUpdateChildAccount() throws GeneralException, UnauthorizedException {
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+ ChildAccountResponse childAccountResponse = createChildAccountResponse();
+ when(messageBirdServiceMock.sendPayLoad(any(), any(), any(), any()))
+ .thenReturn(childAccountResponse);
+ ChildAccountResponse response = messageBirdClientInjectMock.updateChildAccount("ANY_NAME", "ANY_ID");
+
+ verify(messageBirdServiceMock, times(1))
+ .sendPayLoad(any(), any(), any(), any());
+ assertNotNull(response);
+ assertEquals(response.getId(), childAccountResponse.getId());
+ assertEquals(response.getName(), childAccountResponse.getName());
+ }
+
+ @Test
+ public void testDeleteChildAccount() throws GeneralException, UnauthorizedException, NotFoundException {
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+ String url = String.format("%s/child-accounts/%s", PARTNER_ACCOUNTS_BASE_URL, "ANY_ID");
+ doNothing().when(messageBirdServiceMock).deleteByID(url, "ANY_ID");
+ messageBirdClientInjectMock.deleteChildAccount("id");
+ }
+
+ @Test
+ public void testConversationMessage() throws Exception {
+ ConversationSendRequest request = createDummyConversationRequest();
+ ConversationSendResponse conversationSendResponse = new ConversationSendResponse();
+ conversationSendResponse.setStatus("ACCEPTED");
+ conversationSendResponse.setId("1234");
+
+ MessageBirdService messageBirdServiceMock = mock(MessageBirdService.class);
+ MessageBirdClient messageBirdClientInjectMock = new MessageBirdClient(messageBirdServiceMock);
+
+ when(messageBirdServiceMock.sendPayLoad( CONVERSATIONS_BASE_URL + CONVERSATION_SEND_PATH, request, ConversationSendResponse.class))
+ .thenReturn(conversationSendResponse);
+ ConversationSendResponse response = messageBirdClientInjectMock.sendMessage(request);
+
+ verify(messageBirdServiceMock, times(1))
+ .sendPayLoad(CONVERSATIONS_BASE_URL + CONVERSATION_SEND_PATH, request, ConversationSendResponse.class);
+ assertNotNull(response.getId());
+ assertNotNull(response.getStatus());
+ }
+
+ private ConversationSendRequest createDummyConversationRequest() {
+ ConversationContent conversationContent = new ConversationContent();
+ conversationContent.setText("test");
+ ConversationSendRequest request = new ConversationSendRequest();
+ request.setFrom("channelid");
+ request.setTo("+34123123123");
+ request.setTtl("15s");
+ request.setType(ConversationContentType.TEXT);
+ request.setContent(conversationContent);
+ request.setTrackId("mycampaign");
+ 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);
+ }
+
+ @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/RequestSignerTest.java b/api/src/test/java/com/messagebird/RequestSignerTest.java
index 435ef1ce..17adc0dc 100644
--- a/api/src/test/java/com/messagebird/RequestSignerTest.java
+++ b/api/src/test/java/com/messagebird/RequestSignerTest.java
@@ -6,6 +6,10 @@
import static org.junit.Assert.*;
+/**
+ * @deprecated This class is being deprecated together with {@link RequestSigner}
+ */
+@Deprecated
public class RequestSignerTest {
/**
@@ -87,24 +91,24 @@ public void testWithRealSignature() {
byte[] spoiledBody = getBytes("get shit spoiled");
assertTrue(
- "Definitely valid signature is threaten as invalid",
- requestSigner.isMatch(requestSignature, new Request(requestTimestamp, requestParams, requestBody))
+ "Definitely valid signature is threaten as invalid",
+ requestSigner.isMatch(requestSignature, new Request(requestTimestamp, requestParams, requestBody))
);
assertFalse(
- "Invalid signature is threaten as invalid",
- requestSigner.isMatch(spoiledSignature, new Request(requestTimestamp, requestParams, requestBody))
+ "Invalid signature is threaten as invalid",
+ requestSigner.isMatch(spoiledSignature, new Request(requestTimestamp, requestParams, requestBody))
);
assertFalse(
- "Signature is still valid with replaced timestamp",
- requestSigner.isMatch(requestSignature, new Request(spoiledTimestamp, requestParams, requestBody))
+ "Signature is still valid with replaced timestamp",
+ requestSigner.isMatch(requestSignature, new Request(spoiledTimestamp, requestParams, requestBody))
);
assertFalse(
- "Signature is still valid with replaced params",
- requestSigner.isMatch(requestSignature, new Request(requestTimestamp, spoiledParams, requestBody))
+ "Signature is still valid with replaced params",
+ requestSigner.isMatch(requestSignature, new Request(requestTimestamp, spoiledParams, requestBody))
);
assertFalse(
- "Signature is still valid with replaced body",
- requestSigner.isMatch(requestSignature, new Request(requestTimestamp, requestParams, spoiledBody))
+ "Signature is still valid with replaced body",
+ requestSigner.isMatch(requestSignature, new Request(requestTimestamp, requestParams, spoiledBody))
);
}
}
diff --git a/api/src/test/java/com/messagebird/RequestValidatorTest.java b/api/src/test/java/com/messagebird/RequestValidatorTest.java
new file mode 100644
index 00000000..23c64ef0
--- /dev/null
+++ b/api/src/test/java/com/messagebird/RequestValidatorTest.java
@@ -0,0 +1,102 @@
+package com.messagebird;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.messagebird.exceptions.RequestValidationException;
+import com.messagebird.util.Resources;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+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;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+@RunWith(Parameterized.class)
+public class RequestValidatorTest {
+ /**
+ * Error Map that maps test data expected outcome to actual error message.
+ */
+ private static final Map ERROR_MAP = new HashMap() {
+ {
+ put("invalid jwt: claim nbf is in the future", "The Token can't be used before");
+ put("invalid jwt: claim exp is in the past", "The Token has expired on");
+ put("invalid jwt: claim url_hash is invalid", "The Claim 'url_hash' value doesn't match the required one.");
+ put("invalid jwt: claim payload_hash is invalid",
+ "The Claim 'payload_hash' value doesn't match the required one.");
+ put("invalid jwt: signature is invalid", "Signature is invalid.");
+ put("invalid jwt: claim payload_hash is set but actual payload is missing",
+ "The Claim 'payload_hash' is set but actual payload is missing.");
+ put("invalid jwt: claim payload_hash is not set but payload is present",
+ "The Claim 'payload_hash' is not set but payload is present.");
+ put("invalid jwt: signing method none is invalid", "The signing method 'none' is invalid.");
+
+ }
+ };
+ private final WebhookSignatureTestCase testCase;
+
+ public RequestValidatorTest(String testName, WebhookSignatureTestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection