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.meetings; 017 018import com.vonage.client.*; 019import com.vonage.client.auth.JWTAuthMethod; 020import com.vonage.client.common.HalPageResponse; 021import com.vonage.client.common.HttpMethod; 022import org.apache.http.HttpEntity; 023import org.apache.http.HttpResponse; 024import org.apache.http.StatusLine; 025import org.apache.http.client.HttpClient; 026import org.apache.http.client.methods.RequestBuilder; 027import org.apache.http.client.utils.URLEncodedUtils; 028import org.apache.http.entity.mime.MultipartEntityBuilder; 029import java.io.IOException; 030import java.net.URI; 031import java.nio.charset.Charset; 032import java.nio.file.Path; 033import java.util.*; 034import java.util.function.Function; 035 036/** 037 * Meetings API client. 038 * 039 * @deprecated Support for this API will be removed in the next major release. 040 */ 041@Deprecated 042public class MeetingsClient { 043 HttpClient httpClient; 044 045 final RestEndpoint<ListRoomsRequest, ListRoomsResponse> listRooms, searchThemeRooms; 046 final RestEndpoint<UUID, MeetingRoom> getRoom; 047 final RestEndpoint<MeetingRoom, MeetingRoom> createRoom; 048 final RestEndpoint<UpdateRoomRequest, MeetingRoom> updateRoom; 049 final RestEndpoint<Void, ListThemesResponse> listThemes; 050 final RestEndpoint<UUID, Theme> getTheme; 051 final RestEndpoint<Theme, Theme> createTheme, updateTheme; 052 final RestEndpoint<DeleteThemeRequest, Void> deleteTheme; 053 final RestEndpoint<String, ListRecordingsResponse> listRecordings; 054 final RestEndpoint<UUID, Recording> getRecording; 055 final RestEndpoint<UUID, Void> deleteRecording; 056 final RestEndpoint<Void, ListDialNumbersResponse> listDialNumbers; 057 final RestEndpoint<UpdateApplicationRequest, Application> updateApplication; 058 final RestEndpoint<FinalizeLogosRequest, Void> finalizeLogos; 059 final RestEndpoint<Void, GetLogoUploadUrlsResponse> getLogoUploadUrls; 060 061 /** 062 * Constructor. 063 * 064 * @param wrapper (REQUIRED) shared HTTP wrapper object used for making REST calls. 065 */ 066 public MeetingsClient(HttpWrapper wrapper) { 067 super(); 068 httpClient = wrapper.getHttpClient(); 069 070 @SuppressWarnings("unchecked") 071 class Endpoint<T, R> extends DynamicEndpoint<T, R> { 072 Endpoint(Function<T, String> pathGetter, HttpMethod method, R... type) { 073 super(DynamicEndpoint.<T, R> builder(type) 074 .authMethod(JWTAuthMethod.class).requestMethod(method) 075 .responseExceptionType(MeetingsResponseException.class) 076 .wrapper(wrapper).pathGetter((de, req) -> { 077 String base = de.getHttpWrapper().getHttpConfig().getApiEuBaseUri(); 078 return base + "/v1/meetings/" + pathGetter.apply(req); 079 }) 080 ); 081 } 082 } 083 084 listRooms = new Endpoint<>(req -> "rooms", HttpMethod.GET); 085 getRoom = new Endpoint<>(roomId -> "rooms/" + roomId, HttpMethod.GET); 086 createRoom = new Endpoint<>(req -> "rooms", HttpMethod.POST); 087 updateRoom = new Endpoint<>(req -> "rooms/" + req.roomId, HttpMethod.PATCH); 088 searchThemeRooms = new Endpoint<>(req -> "themes/" + req.themeId + "/rooms", HttpMethod.GET); 089 listThemes = new Endpoint<>(req -> "themes", HttpMethod.GET); 090 getTheme = new Endpoint<>(themeId -> "themes/" + themeId, HttpMethod.GET); 091 createTheme = new Endpoint<>(req -> "themes", HttpMethod.POST); 092 updateTheme = new Endpoint<>(theme -> "themes/" + theme.getThemeId(), HttpMethod.PATCH); 093 deleteTheme = new Endpoint<>(req -> "themes/" + req.themeId, HttpMethod.DELETE); 094 listRecordings = new Endpoint<>(sid -> "sessions/" + sid + "/recordings", HttpMethod.GET); 095 getRecording = new Endpoint<>(rid -> "recordings/" + rid, HttpMethod.GET); 096 deleteRecording = new Endpoint<>(rid -> "recordings/" + rid, HttpMethod.DELETE); 097 listDialNumbers = new Endpoint<>(req -> "dial-in-numbers", HttpMethod.GET); 098 updateApplication = new Endpoint<>(req -> "applications", HttpMethod.PATCH); 099 finalizeLogos = new Endpoint<>(req -> "themes/" + req.themeId + "/finalizeLogos", HttpMethod.PUT); 100 getLogoUploadUrls = new Endpoint<>(req -> "themes/logos-upload-urls", HttpMethod.GET); 101 } 102 103 static UUID validateThemeId(UUID themeId) { 104 return Objects.requireNonNull(themeId, "Theme ID is required."); 105 } 106 107 static UUID validateRoomId(UUID roomId) { 108 return Objects.requireNonNull(roomId, "Room ID is required."); 109 } 110 111 static UUID validateRecordingId(UUID recordingId) { 112 return Objects.requireNonNull(recordingId, "Recording ID is required."); 113 } 114 115 static String validateSessionId(String sessionId) { 116 if (sessionId == null || sessionId.trim().isEmpty()) { 117 throw new IllegalArgumentException("Session ID cannot be null or empty."); 118 } 119 return sessionId; 120 } 121 122 private int parseNextFromHalResponse(HalPageResponse response) { 123 final URI nextUrl = response.getLinks().getNextUrl(); 124 return URLEncodedUtils.parse(nextUrl, Charset.defaultCharset()) 125 .stream().filter(nvp -> "start_id".equals(nvp.getName())) 126 .findFirst().map(nvp -> Integer.parseInt(nvp.getValue())) 127 .orElseThrow(() -> new VonageClientException("Couldn't navigate to next page: "+nextUrl)); 128 129 } 130 131 private List<MeetingRoom> getAllRoomsFromResponseRecursively( 132 RestEndpoint<ListRoomsRequest, ListRoomsResponse> endpoint, ListRoomsRequest initialRequest) { 133 134 final int initialPageSize = initialRequest.pageSize != null ? initialRequest.pageSize : 1000; 135 ListRoomsRequest request = new ListRoomsRequest( 136 initialRequest.startId, initialRequest.endId, initialPageSize, initialRequest.themeId 137 ); 138 ListRoomsResponse response = endpoint.execute(request); 139 140 if (response.getTotalItems() <= response.getPageSize()) { 141 return response.getMeetingRooms(); 142 } 143 else { 144 List<MeetingRoom> rooms = new ArrayList<>(response.getMeetingRooms()); 145 do { 146 request = new ListRoomsRequest( 147 parseNextFromHalResponse(response), null, initialPageSize, request.themeId 148 ); 149 response = endpoint.execute(request); 150 rooms.addAll(response.getMeetingRooms()); 151 } 152 while (response.getPageSize() >= initialPageSize); 153 return rooms; 154 } 155 } 156 157 /** 158 * Get all listed rooms in the application. 159 * 160 * @return The list of all meeting rooms. 161 * 162 * @throws MeetingsResponseException If there is an error encountered when processing the request. 163 */ 164 public List<MeetingRoom> listRooms() { 165 return getAllRoomsFromResponseRecursively(listRooms, 166 new ListRoomsRequest(null, null, null, null) 167 ); 168 } 169 170 /** 171 * Get details of an existing room. 172 * 173 * @param roomId ID of the room to retrieve. 174 * 175 * @return The meeting room associated with the ID. 176 * 177 * @throws MeetingsResponseException If there is an error encountered when processing the request. 178 */ 179 public MeetingRoom getRoom(UUID roomId) { 180 return getRoom.execute(validateRoomId(roomId)); 181 } 182 183 /** 184 * Create a new room. 185 * 186 * @param room Properties of the meeting room. 187 * 188 * @return Details of the created meeting room. 189 * 190 * @throws MeetingsResponseException If there is an error encountered when processing the request. 191 */ 192 public MeetingRoom createRoom(MeetingRoom room) { 193 return createRoom.execute(Objects.requireNonNull(room, "Meeting room is required.")); 194 } 195 196 /** 197 * Update an existing room. 198 * 199 * @param roomId ID of the meeting room to be updated. 200 * @param roomUpdate Properties of the meeting room to change. 201 * 202 * @return Details of the updated meeting room. 203 * 204 * @throws MeetingsResponseException If there is an error encountered when processing the request. 205 */ 206 public MeetingRoom updateRoom(UUID roomId, UpdateRoomRequest roomUpdate) { 207 Objects.requireNonNull(roomUpdate, "Room update request properties is required."); 208 roomUpdate.roomId = validateRoomId(roomId); 209 return updateRoom.execute(roomUpdate); 210 } 211 212 /** 213 * Get rooms that are associated with a theme ID. 214 * 215 * @param themeId The theme ID to filter by. 216 * 217 * @return The list of rooms which use the theme. 218 * 219 * @throws MeetingsResponseException If there is an error encountered when processing the request. 220 */ 221 public List<MeetingRoom> searchRoomsByTheme(UUID themeId) { 222 return getAllRoomsFromResponseRecursively(searchThemeRooms, 223 new ListRoomsRequest(null, null, null, validateThemeId(themeId)) 224 ); 225 } 226 227 /** 228 * Get all application themes. 229 * 230 * @return The list of themes. 231 * 232 * @throws MeetingsResponseException If there is an error encountered when processing the request. 233 */ 234 public List<Theme> listThemes() { 235 return listThemes.execute(null); 236 } 237 238 /** 239 * Retrieve details of a theme by ID. 240 * 241 * @param themeId The theme ID. 242 * 243 * @return The theme associated with the ID. 244 * 245 * @throws MeetingsResponseException If there is an error encountered when processing the request. 246 */ 247 public Theme getTheme(UUID themeId) { 248 return getTheme.execute(validateThemeId(themeId)); 249 } 250 251 /** 252 * Create a new theme. 253 * 254 * @param theme The partial theme properties. 255 * 256 * @return The full created theme details. 257 * 258 * @throws MeetingsResponseException If there is an error encountered when processing the request. 259 */ 260 public Theme createTheme(Theme theme) { 261 Objects.requireNonNull(theme, "Theme creation properties are required."); 262 Objects.requireNonNull(theme.getBrandText(), "Brand text is required."); 263 Objects.requireNonNull(theme.getMainColor(), "Main color is required."); 264 return createTheme.execute(theme); 265 } 266 267 /** 268 * Update an existing theme. 269 * 270 * @param themeId ID of the theme to update. 271 * @param theme The partial theme properties to update. 272 * 273 * @return The fully updated theme details. 274 * 275 * @throws MeetingsResponseException If there is an error encountered when processing the request. 276 */ 277 public Theme updateTheme(UUID themeId, Theme theme) { 278 Objects.requireNonNull(theme, "Theme update properties are required."); 279 theme.setThemeIdAndFlagUpdate(validateThemeId(themeId)); 280 return updateTheme.execute(theme); 281 } 282 283 /** 284 * Delete a theme by its ID. 285 * 286 * @param themeId ID of the theme to delete. 287 * @param force Whether to delete the theme even if theme is used by rooms or as application default theme. 288 * 289 * @throws MeetingsResponseException If there is an error encountered when processing the request. 290 */ 291 public void deleteTheme(UUID themeId, boolean force) { 292 deleteTheme.execute(new DeleteThemeRequest(validateThemeId(themeId), force)); 293 } 294 295 /** 296 * Get recordings of a meeting session. 297 * 298 * @param sessionId The session ID to filter recordings by. 299 * 300 * @return The list of recordings for the session. 301 * 302 * @throws MeetingsResponseException If there is an error encountered when processing the request. 303 */ 304 public List<Recording> listRecordings(String sessionId) { 305 ListRecordingsResponse response = listRecordings.execute(validateSessionId(sessionId)); 306 List<Recording> recordings = response.getRecordings(); 307 return recordings != null ? recordings : Collections.emptyList(); 308 } 309 310 /** 311 * Get details of a recording. 312 * 313 * @param recordingId ID of the recording to retrieve. 314 * 315 * @return The recording properties. 316 * 317 * @throws MeetingsResponseException If there is an error encountered when processing the request. 318 */ 319 public Recording getRecording(UUID recordingId) { 320 return getRecording.execute(validateRecordingId(recordingId)); 321 } 322 323 /** 324 * Delete a recording. 325 * 326 * @param recordingId ID of the recording to delete. 327 * 328 * @throws MeetingsResponseException If there is an error encountered when processing the request. 329 */ 330 public void deleteRecording(UUID recordingId) { 331 deleteRecording.execute(validateRecordingId(recordingId)); 332 } 333 334 /** 335 * Get numbers that can be used to dial into a meeting. 336 * 337 * @return The list of dial-in numbers, along with their country code. 338 * 339 * @throws MeetingsResponseException If there is an error encountered when processing the request. 340 */ 341 public List<DialInNumber> listDialNumbers() { 342 return listDialNumbers.execute(null); 343 } 344 345 /** 346 * Update an existing application. 347 * 348 * @param updateRequest Properties of the application to update. 349 * 350 * @return The updated application details. 351 * 352 * @throws MeetingsResponseException If there is an error encountered when processing the request. 353 */ 354 public Application updateApplication(UpdateApplicationRequest updateRequest) { 355 return updateApplication.execute(Objects.requireNonNull( 356 updateRequest, "Application update properties are required.") 357 ); 358 } 359 360 /** 361 * Change logos to be permanent for a given theme. 362 * 363 * @param themeId The theme ID containing the logos. 364 * @param keys List of temporary theme's logo keys to make permanent 365 */ 366 void finalizeLogos(UUID themeId, List<String> keys) { 367 if (keys == null || keys.isEmpty()) { 368 throw new IllegalArgumentException("Logo keys are required."); 369 } 370 finalizeLogos.execute(new FinalizeLogosRequest(validateThemeId(themeId), keys)); 371 } 372 373 /** 374 * Get URLs that can be used to upload logos for a theme via a POST. 375 * 376 * @return List of URLs and respective credentials / tokens needed for uploading logos to them. 377 */ 378 List<LogoUploadsUrlResponse> listLogoUploadUrls() { 379 return getLogoUploadUrls.execute(null); 380 } 381 382 /** 383 * Finds the appropriate response object from {@linkplain #listLogoUploadUrls()} for the given logo type. 384 * 385 * @param logoType The logo type to get details for. 386 * @return The URL and credential fields for uploading the logo. 387 */ 388 LogoUploadsUrlResponse getUploadDetailsForLogoType(LogoType logoType) { 389 return listLogoUploadUrls().stream() 390 .filter(r -> logoType.equals(r.getFields().getLogoType())) 391 .findFirst() 392 .orElseThrow(() -> new IllegalArgumentException("Logo type "+logoType+" is unavailable.")); 393 } 394 395 /** 396 * Uploads a logo to the cloud so that it can be used in themes. 397 * 398 * @param logoFile Absolute path to the image. 399 * @param details Credentials, logo key and URL to facilitate the upload request. 400 */ 401 void uploadLogo(Path logoFile, LogoUploadsUrlResponse details) { 402 try { 403 LogoUploadsUrlResponse.Fields fields = details.getFields(); 404 HttpEntity entity = MultipartEntityBuilder.create() 405 .addTextBody("Content-Type", fields.getContentType()) 406 .addTextBody("key", fields.getKey()) 407 .addTextBody("logoType", fields.getLogoType().toString()) 408 .addTextBody("bucket", fields.getBucket()) 409 .addTextBody("X-Amz-Algorithm", fields.getAmzAlgorithm()) 410 .addTextBody("X-Amz-Credential", fields.getAmzCredential()) 411 .addTextBody("X-Amz-Date", fields.getAmzDate()) 412 .addTextBody("X-Amz-Security-Token", fields.getAmzSecurityToken()) 413 .addTextBody("Policy", fields.getPolicy()) 414 .addTextBody("X-Amz-Signature", fields.getAmzSignature()) 415 .addBinaryBody("file", logoFile.toFile()) 416 .build(); 417 HttpResponse response = httpClient.execute( 418 RequestBuilder.post(details.getUrl()).setEntity(entity).build() 419 ); 420 StatusLine status = response.getStatusLine(); 421 int statusCode = status.getStatusCode(); 422 if (statusCode != 204) { 423 MeetingsResponseException mrx = new MeetingsResponseException( 424 "Logo upload failed ("+statusCode+"): "+status.getReasonPhrase() 425 ); 426 mrx.setStatusCode(statusCode); 427 throw mrx; 428 } 429 } 430 catch (IOException ex) { 431 throw new VonageUnexpectedException(ex); 432 } 433 } 434 435 /** 436 * Upload a logo image and associates it with a theme. 437 * 438 * @param themeId ID of the theme which the logo will be associated with. 439 * @param logoType The logo type to upload. 440 * @param pngFile Absolute path to the logo image. For restrictions, refer to 441 * <a href=https://developer.vonage.com/en/meetings/code-snippets/theme-management#uploading-icons-and-logos> 442 * the documentation</a>. Generally, the image must be a PNG under 1MB, square, under 300x300 pixels and 443 * have a transparent background. 444 * 445 * @throws MeetingsResponseException If there is an error encountered when processing the request. 446 */ 447 public void updateThemeLogo(UUID themeId, LogoType logoType, Path pngFile) { 448 LogoUploadsUrlResponse target = getUploadDetailsForLogoType( 449 Objects.requireNonNull(logoType, "Logo type cannot be null.") 450 ); 451 uploadLogo(Objects.requireNonNull(pngFile, "Image file cannot be null."), target); 452 finalizeLogos(themeId, Collections.singletonList(target.getFields().getKey())); 453 } 454}