001/* 002 * Copyright 2024 Vonage 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.vonage.client.messages; 017 018import com.vonage.client.*; 019import com.vonage.client.auth.JWTAuthMethod; 020import com.vonage.client.auth.ApiKeyHeaderAuthMethod; 021import com.vonage.client.common.HttpMethod; 022import com.vonage.jwt.Jwt; 023import java.util.function.BiFunction; 024import java.util.function.Function; 025 026public class MessagesClient { 027 private boolean sandbox = false; 028 final RestEndpoint<MessageRequest, MessageResponse> sendMessage, sendMessageSandbox; 029 final RestEndpoint<UpdateStatusRequest, Void> updateMessage; 030 031 /** 032 * Create a new MessagesClient. 033 * 034 * @param wrapper Http Wrapper used to create message requests. 035 */ 036 @SuppressWarnings("unchecked") 037 public MessagesClient(HttpWrapper wrapper) { 038 final String messagesPath = "/v1/messages"; 039 final class Endpoint<T, R> extends DynamicEndpoint<T, R> { 040 Endpoint(Function<HttpConfig, String> basePathGetter, R... type) { 041 super(DynamicEndpoint.<T, R> builder(type) 042 .responseExceptionType(MessageResponseException.class) 043 .wrapper(wrapper).requestMethod(HttpMethod.POST) 044 .authMethod(JWTAuthMethod.class, ApiKeyHeaderAuthMethod.class) 045 .pathGetter((de, req) -> 046 basePathGetter.apply(de.getHttpWrapper().getHttpConfig()) + messagesPath 047 ) 048 ); 049 } 050 } 051 052 sendMessage = new Endpoint<>(HttpConfig::getApiBaseUri); 053 sendMessageSandbox = new Endpoint<>(hc -> "https://messages-sandbox.nexmo.com"); 054 updateMessage = DynamicEndpoint.<UpdateStatusRequest, Void> builder(Void.class) 055 .responseExceptionType(MessageResponseException.class) 056 .wrapper(wrapper).requestMethod(HttpMethod.PATCH) 057 .authMethod(JWTAuthMethod.class).pathGetter((de, req) -> de.getHttpWrapper().getHttpConfig() 058 .getRegionalBaseUri(req.region) + messagesPath + "/" + req.messageId 059 ).build(); 060 } 061 062 /** 063 * Sends a message. The details of its format, channel, sender, recipient etc. are 064 * specified entirely by the type and contents of the MessageRequest. For example, to send 065 * a text via Viber, you would construct the request like so: <br> 066 * <pre>{@code 067 * ViberTextRequest request = ViberTextRequest.builder() 068 * .from("My Company") 069 * .to("447700900000") 070 * .text("Hello from Vonage!") 071 * .build(); 072 * }</pre><br> 073 * 074 * If the message was sent successfully, a {@link MessageResponse} will be returned containing the 075 * unique identifier of the message. Otherwise, a {@link MessageResponseException} will be thrown, 076 * which contains details of why the request failed. 077 * 078 * @param request The message request object, as described above. 079 * @return The response, if the request was successful (i.e.a 202 was received from the server). 080 * 081 * @throws MessageResponseException If the message could not be sent. This could be for the following reasons: 082 * <ul> 083 * <li><b>401:</b> Missing or invalid credentials.</li> 084 * <li><b>402:</b> Low balance.</li> 085 * <li><b>422:</b> Invalid request parameters.</li> 086 * <li><b>429:</b> Rate limit hit. Please wait and try again.</li> 087 * <li><b>500:</b> Internal server error.</li> 088 * </ul> 089 */ 090 public MessageResponse sendMessage(MessageRequest request) throws MessageResponseException { 091 return (sandbox ? sendMessageSandbox : sendMessage).execute(request); 092 } 093 094 /** 095 * Calling this method will make the client use the sandbox endpoint, which will enable you to 096 * use the <a href=https://dashboard.nexmo.com/messages/sandbox>Messages Sandbox</a>. 097 * 098 * @return This MessagesClient, for convenience. 099 * @since 7.1.0 100 */ 101 public MessagesClient useSandboxEndpoint() { 102 sandbox = true; 103 return this; 104 } 105 106 /** 107 * Calling this method will make the client use the regular endpoint returned by 108 * {@link HttpConfig#getApiBaseUri()}, which is the default behaviour. This method is 109 * provided for convenience if you wish to switch between sandbox and non-sandbox mode. 110 * 111 * @see #useSandboxEndpoint() 112 * @return This MessagesClient, for convenience. 113 * @since 7.1.0 114 */ 115 public MessagesClient useRegularEndpoint() { 116 sandbox = false; 117 return this; 118 } 119 120 /** 121 * Marks an inbound message as "read". Currently, this only applies to WhatsApp messages. 122 * 123 * @param messageId UUID of the message to acknowledge. 124 * @param region The regional server to use for this request. This must match the region of the message. 125 * 126 * @throws MessageResponseException If the acknowledgement fails. This could be for the following reasons: 127 * <ul> 128 * <li><b>401:</b> Missing or invalid credentials.</li> 129 * <li><b>404:</b> Message not found.</li> 130 * <li><b>422:</b> Operation not supported for this channel.</li> 131 * <li><b>429:</b> Rate limit hit. Please wait and try again.</li> 132 * <li><b>500:</b> Internal server error.</li> 133 * </ul> 134 * 135 * @since 8.11.0 136 */ 137 public void ackInboundMessage(String messageId, ApiRegion region) throws MessageResponseException { 138 updateMessage.execute(new UpdateStatusRequest("read", messageId, region)); 139 } 140 141 /** 142 * Revokes an outbound message. Currently, this only applies to RCS messages. 143 * 144 * @param messageId UUID of the message to revoke. 145 * @param region The regional server to use for this request. This must match the region of the message. 146 * 147 * @throws MessageResponseException If the acknowledgement fails. This could be for the following reasons: 148 * <ul> 149 * <li><b>401:</b>Missing or invalid credentials.</li> 150 * <li><b>404:</b>Message not found.</li> 151 * <li><b>422:</b>Operation not supported for this channel.</li> 152 * <li><b>429:</b>Rate limit hit. Please wait and try again.</li> 153 * <li><b>500:</b>Internal server error.</li> 154 * </ul> 155 * 156 * @since 8.11.0 157 */ 158 public void revokeOutboundMessage(String messageId, ApiRegion region) throws MessageResponseException { 159 updateMessage.execute(new UpdateStatusRequest("revoked", messageId, region)); 160 } 161 162 /** 163 * Utility method for verifying whether a token was signed by a secret. 164 * This is mostly useful when using signed callbacks to ensure that the inbound 165 * data came from Vonage servers. The signature is performed using the SHA-256 HMAC algorithm. 166 * 167 * @param jwt The JSON Web Token to verify. 168 * @param secret The symmetric secret key (HS256) to use for decrypting the token's signature. 169 * 170 * @return {@code true} if the token was signed by the secret, {@code false} otherwise. 171 * 172 * @since 7.11.0 173 */ 174 public static boolean verifySignature(String jwt, String secret) { 175 return Jwt.verifySignature(jwt, secret); 176 } 177}