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.conversations;
017
018import com.vonage.client.DynamicEndpoint;
019import com.vonage.client.HttpWrapper;
020import com.vonage.client.RestEndpoint;
021import com.vonage.client.VonageClient;
022import com.vonage.client.auth.JWTAuthMethod;
023import com.vonage.client.common.HttpMethod;
024import java.util.List;
025import java.util.Objects;
026import java.util.UUID;
027import java.util.function.Function;
028
029/**
030 * A client for communicating with the Vonage Conversations API. The standard way to obtain an instance
031 * of this class is to use {@link VonageClient#getConversationsClient()}.
032 */
033public class ConversationsClient {
034        final RestEndpoint<ListConversationsRequest, ListConversationsResponse> listConversations;
035        final RestEndpoint<Conversation, Conversation> createConversation;
036        final RestEndpoint<String, Conversation> getConversation;
037        final RestEndpoint<Conversation, Conversation> updateConversation;
038        final RestEndpoint<String, Void> deleteConversation;
039        final RestEndpoint<ListUserConversationsRequest, ListUserConversationsResponse> listUserConversations;
040        final RestEndpoint<ListMembersRequest, ListMembersResponse> listMembers;
041        final RestEndpoint<ConversationResourceRequestWrapper, Member> getMember;
042        final RestEndpoint<Member, Member> createMember;
043        final RestEndpoint<UpdateMemberRequest, Member> updateMember;
044        final RestEndpoint<ConversationResourceRequestWrapper, Void> deleteEvent;
045        final RestEndpoint<ConversationResourceRequestWrapper, Event> getEvent;
046        final RestEndpoint<ListEventsRequest, ListEventsResponse> listEvents;
047        final RestEndpoint<Event, Event> createEvent;
048
049        /**
050         * Constructor.
051         *
052         * @param wrapper (REQUIRED) shared HTTP wrapper object used for making REST calls.
053         */
054        @SuppressWarnings("unchecked")
055        public ConversationsClient(HttpWrapper wrapper) {
056                final String v1c = "/v1/conversations/", v1u = "/v1/users/", mems = "/members/", events = "/events/";
057
058                class Endpoint<T, R> extends DynamicEndpoint<T, R> {
059                        Endpoint(Function<T, String> pathGetter, HttpMethod method, R... type) {
060                                super(DynamicEndpoint.<T, R> builder(type)
061                                        .authMethod(JWTAuthMethod.class)
062                                        .responseExceptionType(ConversationsResponseException.class)
063                                        .requestMethod(method).wrapper(wrapper).pathGetter((de, req) -> {
064                                                String base = de.getHttpWrapper().getHttpConfig().getApiBaseUri();
065                                                return base + pathGetter.apply(req);
066                                        })
067                                );
068                        }
069                }
070
071                listConversations = new Endpoint<>(req -> v1c, HttpMethod.GET);
072                createConversation = new Endpoint<>(req -> v1c, HttpMethod.POST);
073                getConversation = new Endpoint<>(id -> v1c+id, HttpMethod.GET);
074                updateConversation = new Endpoint<>(req -> v1c+req.getId(), HttpMethod.PUT);
075                deleteConversation = new Endpoint<>(id -> v1c+id, HttpMethod.DELETE);
076                listUserConversations = new Endpoint<>(req -> v1u+req.userId+"/conversations", HttpMethod.GET);
077                listMembers = new Endpoint<>(req -> v1c+req.conversationId+mems, HttpMethod.GET);
078                getMember = new Endpoint<>(req -> v1c+req.conversationId+mems+req.resourceId, HttpMethod.GET);
079                createMember = new Endpoint<>(req -> v1c+req.getConversationId()+mems, HttpMethod.POST);
080                updateMember = new Endpoint<>(req -> v1c+req.conversationId+mems+req.resourceId, HttpMethod.PATCH);
081                deleteEvent = new Endpoint<>(req -> v1c+req.conversationId+events+req.resourceId, HttpMethod.DELETE);
082                getEvent = new Endpoint<>(req -> v1c+req.conversationId+events+req.resourceId, HttpMethod.GET);
083                listEvents = new Endpoint<>(req -> v1c+req.conversationId+events, HttpMethod.GET);
084                createEvent = new Endpoint<>(req -> v1c+req.conversationId+events, HttpMethod.POST);
085        }
086
087        // VALIDATION
088
089        private static String validateId(String prefix, String arg) {
090                final int prefixLength = prefix.length(), expectedLength = prefixLength + 36;
091                if (arg == null || arg.length() != expectedLength) {
092                        throw new IllegalArgumentException(
093                                        "Invalid ID: '"+arg+"' is not "+expectedLength+" characters in length."
094                        );
095                }
096                if (!arg.startsWith(prefix)) {
097                        String actualPrefix = arg.substring(0, prefixLength);
098                        throw new IllegalArgumentException(
099                                        "Invalid ID: expected prefix '"+prefix+"' but got '"+actualPrefix+"'."
100                        );
101                }
102                return prefix + UUID.fromString(arg.substring(prefixLength));
103        }
104
105        private static String validateConversationId(String id) {
106                return validateId("CON-", id);
107        }
108
109        static String validateMemberId(String id) {
110                return validateId("MEM-", id);
111        }
112
113        private static String validateUserId(String id) {
114                return validateId("USR-", id);
115        }
116
117        private static String validateEventId(int id) {
118                if (id < 0) {
119                        throw new IllegalArgumentException("Event ID cannot be negative.");
120                }
121                return String.valueOf(id);
122        }
123
124        private static <T> T validateRequest(T request) {
125                return Objects.requireNonNull(request, "Request parameter is required.");
126        }
127
128        private static <F extends AbstractConversationsFilterRequest, B extends
129                        AbstractConversationsFilterRequest.Builder<? extends F, ?>> F defaultFilterParams(B builder) {
130                return builder.pageSize(100).build();
131        }
132
133        // ENDPOINTS
134
135        /**
136         * Retrieve the first 100 Conversations in the application. Note that the returned conversations are
137         * incomplete, hence of type {@linkplain BaseConversation}. To get the full data, use the
138         * {@link #getConversation(String)} method, passing in the ID from {@linkplain BaseConversation#getId()}.
139         *
140         * @return A list of the first 100 conversations returned from the API, in default (ascending) order.
141         *
142         * @throws ConversationsResponseException If the API call fails due to a bad request (400).
143         * @see #listConversations(ListConversationsRequest)
144         */
145        public List<BaseConversation> listConversations() {
146                return listConversations(defaultFilterParams(ListConversationsRequest.builder())).getConversations();
147        }
148
149        /**
150         * Retrieve conversations in the application which match the specified filter criteria. Note that the
151         * returned conversations in {@linkplain ListConversationsResponse#getConversations()} are incomplete,
152         * hence type of {@linkplain BaseConversation}. To get the full data, use {@link #getConversation(String)}
153         * method, passing in the ID from {@linkplain BaseConversation#getId()}.
154         *
155         * @param filter Filter options to narrow down the search results.
156         *
157         * @return The search results along with HAL metadata.
158         *
159         * @throws ConversationsResponseException If the API call fails due to a bad request (400).
160         */
161        public ListConversationsResponse listConversations(ListConversationsRequest filter) {
162                return listConversations.execute(validateRequest(filter));
163        }
164
165        /**
166         * Creates a new Conversation within the application.
167         *
168         * @param request The Conversation parameters. Use {@code Conversation.builder().build()} for default settings.
169         *
170         * @return The created Conversation response with additional fields populated.
171         *
172         * @throws ConversationsResponseException If the Conversation name already exists (409), or any other API error.
173         */
174        public Conversation createConversation(Conversation request) {
175                return createConversation.execute(validateRequest(request));
176        }
177
178        /**
179         * Retrieve a conversation by its ID.
180         *
181         * @param conversationId Unique identifier of the conversation to look up.
182         *
183         * @return Details of the conversation corresponding to the specified ID.
184         *
185         * @throws ConversationsResponseException If the conversation was not found (404), or any other API error.
186         */
187        public Conversation getConversation(String conversationId) {
188                return getConversation.execute(validateConversationId(conversationId));
189        }
190
191        /**
192         * Update an existing conversation's settings / parameters.
193         *
194         * @param conversationId Unique conversation identifier.
195         * @param request Conversation object with the updated parameters. Any fields not set will be unchanged.
196         *
197         * @return The full updated conversation details.
198         *
199         * @throws ConversationsResponseException If the conversation was not found (404)
200         * or the parameters are invalid (400), e.g. the updated name already exists (409).
201         */
202        public Conversation updateConversation(String conversationId, Conversation request) {
203                validateRequest(request).id = validateConversationId(conversationId);
204                return updateConversation.execute(request);
205        }
206
207        /**
208         * Delete an existing conversation by ID.
209         *
210         * @param conversationId Unique conversation identifier.
211         *
212         * @throws ConversationsResponseException If the conversation was not found (404), or any other API error.
213         */
214        public void deleteConversation(String conversationId) {
215                deleteConversation.execute(validateConversationId(conversationId));
216        }
217
218        /**
219         * List the first 100 conversations for a given user.
220         *
221         * @param userId Unique identifier for the user.
222         *
223         * @return The list of conversations the specified user is in, with default (ascending) order.
224         *
225         * @throws ConversationsResponseException If the user was not found (404), or any other API error.
226         *
227         * @see #listUserConversations(String, ListUserConversationsRequest)
228         * @see com.vonage.client.users
229         */
230        public List<UserConversation> listUserConversations(String userId) {
231                return listUserConversations(userId,
232                                defaultFilterParams(ListUserConversationsRequest.builder())
233                ).getConversations();
234        }
235
236        /**
237         * List the first 100 conversations for a given user.
238         *
239         * @param userId Unique identifier for the user.
240         * @param filter Filter options to narrow down the search results.
241         *
242         * @return The wrapped list of user conversations, along with HAL metadata.
243         *
244         * @throws ConversationsResponseException If the user was not found (404),
245         * the filter options were invalid (400) or any other API error.
246         *
247         * @see com.vonage.client.users
248         */
249        public ListUserConversationsResponse listUserConversations(String userId, ListUserConversationsRequest filter) {
250                validateRequest(filter).userId = validateUserId(userId);
251                return listUserConversations.execute(filter);
252        }
253
254        /**
255         * List the first 100 Members for a given Conversation. Note that the returned members are
256         * incomplete, hence of type {@linkplain BaseMember}. To get the full data, use the
257         * {@link #getMember(String, String)} method, passing in the ID from {@linkplain BaseMember#getId()}.
258         *
259         * @param conversationId Unique conversation identifier.
260         *
261         * @return The list of members in default (ascending) order.
262         *
263         * @throws ConversationsResponseException If the conversation was not found (404), or any other API error.
264         *
265         * @see #listMembers(String, ListMembersRequest)
266         */
267        public List<BaseMember> listMembers(String conversationId) {
268                return listMembers(conversationId, ListMembersRequest.builder().pageSize(100).build()).getMembers();
269        }
270
271        /**
272         * Retrieve Members associated with a particular Conversation which match the specified filter criteria. Note
273         * that the returned members are incomplete, hence of type {@linkplain BaseMember}. To get the full data, use
274         * the {@link #getMember(String, String)} method, passing in the ID from {@linkplain BaseMember#getId()}.
275         *
276         * @param conversationId Unique conversation identifier.
277         * @param filter Filter options to narrow down the search results.
278         *
279         * @return The wrapped list of Members, along with HAL metadata.
280         *
281         * @throws ConversationsResponseException If the conversation was not found (404),
282         * the filter options were invalid (400) or any other API error.
283         */
284        public ListMembersResponse listMembers(String conversationId, ListMembersRequest filter) {
285                validateRequest(filter).conversationId = validateConversationId(conversationId);
286                return listMembers.execute(filter);
287        }
288
289        /**
290         * Retrieve a conversation Member by its ID.
291         *
292         * @param conversationId Unique conversation identifier.
293         * @param memberId Unique identifier for the member.
294         *
295         * @return Details of the member corresponding to the specified ID.
296         *
297         * @throws ConversationsResponseException If the conversation or member was not found (404), or any other API error.
298         */
299        public Member getMember(String conversationId, String memberId) {
300                return getMember.execute(new ConversationResourceRequestWrapper(
301                                validateConversationId(conversationId), validateMemberId(memberId)
302                ));
303        }
304
305        /**
306         * Creates a new Member for the specified conversation.
307         *
308         * @param conversationId Unique conversation identifier.
309         * @param request The Members parameters. Use {@link Member#builder()}, remember to set the mandatory parameters.
310         *
311         * @return The created Member response with additional fields populated.
312         *
313         * @throws ConversationsResponseException If the conversation was not found (404),
314         * the request parameters were invalid (400) or any other API error.
315         */
316        public Member createMember(String conversationId, Member request) {
317                validateRequest(request).setConversationId(validateConversationId(conversationId));
318                return createMember.execute(request);
319        }
320
321        /**
322         * Update an existing member's state.
323         *
324         * @param request Details of the member to update. Use {@link UpdateMemberRequest#builder()},
325         *                remember to set the mandatory parameters, including the conversation and member IDs.
326         *
327         * @return The updated Member object response.
328         *
329         * @throws ConversationsResponseException If the conversation or member were not found (404),
330         * the request parameters were invalid (400) or any other API error.
331         */
332        public Member updateMember(UpdateMemberRequest request) {
333                validateConversationId(validateRequest(request).conversationId);
334                validateMemberId(request.resourceId);
335                return updateMember.execute(request);
336        }
337
338        /**
339         * List the first 100 events for a given Conversation.
340         *
341         * @param conversationId Unique conversation identifier.
342         *
343         * @return The list of events in default (ascending) order.
344         *
345         * @throws ConversationsResponseException If the conversation was not found (404), or any other API error.
346         */
347        public List<Event> listEvents(String conversationId) {
348                return listEvents(conversationId, ListEventsRequest.builder().pageSize(100).build()).getEvents();
349        }
350
351        /**
352         * Retrieve Events associated with a particular Conversation which match the specified filter criteria.
353         *
354         * @param conversationId Unique conversation identifier.
355         * @param request Filter options to narrow down the search results.
356         *
357         * @return The wrapped list of Events, along with HAL metadata.
358         *
359         * @throws ConversationsResponseException If the conversation was not found (404), or any other API error.
360         */
361        public ListEventsResponse listEvents(String conversationId, ListEventsRequest request) {
362                validateRequest(request).conversationId = validateConversationId(conversationId);
363                return listEvents.execute(request);
364        }
365
366        /**
367         * Retrieve a conversation Event by its ID.
368         *
369         * @param conversationId Unique conversation identifier.
370         * @param eventId Sequence ID of the event to retrieve as an integer.
371         *
372         * @return Details of the event corresponding to the specified ID.
373         *
374         * @throws ConversationsResponseException If the conversation or event was not found (404), or any other API error.
375         */
376        public Event getEvent(String conversationId, int eventId) {
377                return getEvent.execute(new ConversationResourceRequestWrapper(
378                                validateConversationId(conversationId), validateEventId(eventId)
379                ));
380        }
381
382        /**
383         * Creates a new Event for the specified conversation.
384         *
385         * @param conversationId Unique conversation identifier.
386         * @param request Details of the event to create.
387         *
388         * @return The created Event response with additional fields populated.
389         *
390         * @throws ConversationsResponseException If the conversation was not found (404), or any other API error.
391         */
392        @SuppressWarnings("unchecked")
393        public <E extends Event> E createEvent(String conversationId, E request) {
394                validateRequest(request).conversationId = validateConversationId(conversationId);
395                return (E) createEvent.execute(request);
396        }
397
398        /**
399         * Deletes an event. Only message and custom events can be deleted.
400         *
401         * @param conversationId Unique conversation identifier.
402         * @param eventId Sequence ID of the event to retrieve as an integer.
403         *
404         * @throws ConversationsResponseException If the conversation or event was not found (404),
405         * the event could not be deleted, or any other API error.
406         */
407        public void deleteEvent(String conversationId, int eventId) {
408                deleteEvent.execute(new ConversationResourceRequestWrapper(
409                                validateConversationId(conversationId), validateEventId(eventId)
410                ));
411        }
412}