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}