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}