001/* 002 * Copyright 2021 Anyware Services 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 org.ametys.plugins.workspaces.chat; 017 018import java.io.ByteArrayInputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.nio.charset.StandardCharsets; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.commons.codec.digest.Sha2Crypt; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.excalibur.source.Source; 040import org.apache.excalibur.source.SourceResolver; 041import org.apache.http.Consts; 042import org.apache.http.HttpEntity; 043import org.apache.http.client.methods.CloseableHttpResponse; 044import org.apache.http.client.methods.HttpGet; 045import org.apache.http.client.methods.HttpPost; 046import org.apache.http.client.methods.HttpUriRequest; 047import org.apache.http.entity.ContentType; 048import org.apache.http.entity.StringEntity; 049import org.apache.http.entity.mime.HttpMultipartMode; 050import org.apache.http.entity.mime.MultipartEntityBuilder; 051import org.apache.http.impl.client.CloseableHttpClient; 052import org.apache.http.impl.client.HttpClients; 053import org.apache.http.util.EntityUtils; 054import org.apache.poi.util.IOUtils; 055import org.apache.tika.Tika; 056 057import org.ametys.cms.repository.Content; 058import org.ametys.core.authentication.AuthenticateAction; 059import org.ametys.core.ui.Callable; 060import org.ametys.core.user.CurrentUserProvider; 061import org.ametys.core.user.User; 062import org.ametys.core.user.UserIdentity; 063import org.ametys.core.user.UserManager; 064import org.ametys.core.userpref.UserPreferencesException; 065import org.ametys.core.userpref.UserPreferencesManager; 066import org.ametys.core.util.CryptoHelper; 067import org.ametys.core.util.JSONUtils; 068import org.ametys.core.util.URIUtils; 069import org.ametys.plugins.userdirectory.UserDirectoryHelper; 070import org.ametys.runtime.config.Config; 071import org.ametys.runtime.plugin.component.AbstractLogEnabled; 072 073/** 074 * Helper for chat 075 */ 076public class ChatHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 077{ 078 /** The Avalon role */ 079 public static final String ROLE = ChatHelper.class.getName(); 080 081 private static final String __USERPREF_PREF_PASSWORD = "workspaces.chat-connector-password"; 082 083 private static final String __USERPREF_PREF_TOKEN = "workspaces.chat-connector-token"; 084 085 private static final String __USERPREF_PREF_ID = "workspaces.chat-connector-id"; 086 087 private static final String __USERPREF_CONTEXT = "/workspaces.chat-connector"; 088 089 /** Rocket.Chat admin ID */ 090 private static final String CONFIG_ADMIN_ID = "workspaces.chat.rocket.admin.id"; 091 092 /** Rocket.Chat admin Token */ 093 private static final String CONFIG_ADMIN_TOKEN = "workspaces.chat.rocket.admin.token"; 094 095 /** Rocket.Chat URL */ 096 private static final String CONFIG_URL = "workspaces.chat.rocket.url"; 097 098 /** JSON Utils */ 099 protected JSONUtils _jsonUtils; 100 101 /** User Manager */ 102 protected UserManager _userManager; 103 104 /** User Preferences */ 105 protected UserPreferencesManager _userPreferencesManager; 106 107 /** Cryptography */ 108 protected CryptoHelper _cryptoHelper; 109 110 /** Current user provider */ 111 protected CurrentUserProvider _currentUserProvider; 112 113 private SourceResolver _sourceResolver; 114 115 private UserDirectoryHelper _userDirectoryHelper; 116 117 private Context _context; 118 119 public void contextualize(Context context) throws ContextException 120 { 121 _context = context; 122 } 123 124 public void service(ServiceManager manager) throws ServiceException 125 { 126 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 127 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 128 _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE); 129 _cryptoHelper = (CryptoHelper) manager.lookup("org.ametys.plugins.workspaces.chat.cryptoHelper"); 130 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 131 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 132 _userDirectoryHelper = (UserDirectoryHelper) manager.lookup(UserDirectoryHelper.ROLE); 133 } 134 135 private Map<String, Object> _doGet(String api, Map<String, String> parameters) throws IOException 136 { 137 return _doGet(api, parameters, Config.getInstance().getValue(CONFIG_ADMIN_TOKEN), Config.getInstance().getValue(CONFIG_ADMIN_ID)); 138 } 139 140 private Map<String, Object> _doGet(String api, Map<String, String> parameters, String authToken, String userId) throws IOException 141 { 142 String path = Config.getInstance().getValue(CONFIG_URL) + "/api/" + api; 143 144 String uri = URIUtils.encodeURI(path, parameters); 145 HttpGet request = new HttpGet(uri); 146 request.setHeader("Content-Type", "application/json"); 147 148 return _execRequest(request, authToken, userId); 149 } 150 151 private Map<String, Object> _doPOST(String api, Map<String, Object> parameters) throws IOException 152 { 153 return _doPOST(api, parameters, Config.getInstance().getValue(CONFIG_ADMIN_TOKEN), Config.getInstance().getValue(CONFIG_ADMIN_ID)); 154 } 155 156 private Map<String, Object> _doPOST(String api, Map<String, Object> parameters, String authToken, String userId) throws IOException 157 { 158 String path = Config.getInstance().getValue(CONFIG_URL) + "/api/" + api; 159 160 HttpPost request = new HttpPost(path); 161 162 String json = _jsonUtils.convertObjectToJson(parameters); 163 request.setEntity(new StringEntity(json, ContentType.create("application/json", StandardCharsets.UTF_8))); 164 request.setHeader("Content-Type", "application/json"); 165 166 return _execRequest(request, authToken, userId); 167 } 168 169 private Map<String, Object> _doMultipartPOST(String api, Map<String, Object> parameters) throws IOException 170 { 171 return _doMultipartPOST(api, parameters, Config.getInstance().getValue(CONFIG_ADMIN_TOKEN), Config.getInstance().getValue(CONFIG_ADMIN_ID)); 172 } 173 174 private Map<String, Object> _doMultipartPOST(String api, Map<String, Object> parameters, String authToken, String userId) throws IOException 175 { 176 String path = Config.getInstance().getValue(CONFIG_URL) + "/api/" + api; 177 178 HttpPost request = new HttpPost(path); 179 180 MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 181 builder.setMode(HttpMultipartMode.RFC6532); 182 183 for (Entry<String, Object> p : parameters.entrySet()) 184 { 185 if (p.getValue() instanceof String) 186 { 187 builder.addTextBody(p.getKey(), (String) p.getValue(), ContentType.create("text/plain", Consts.UTF_8)); 188 } 189 else if (p.getValue() instanceof InputStream is) 190 { 191 byte[] imageAsBytes = IOUtils.toByteArray(is); 192 ByteArrayInputStream bis = new ByteArrayInputStream(imageAsBytes); 193 Tika tika = new Tika(); 194 String mimeType = tika.detect(imageAsBytes); 195 196 builder.addBinaryBody(p.getKey(), bis, ContentType.create(mimeType), p.getKey()); 197 } 198 else 199 { 200 throw new UnsupportedOperationException("Cannot post the type " + p.getValue().getClass().getName() + " for parameter " + p.getKey()); 201 } 202 } 203 204 HttpEntity multipart = builder.build(); 205 request.setEntity(multipart); 206 207 return _execRequest(request, authToken, userId); 208 } 209 210 211 private boolean _isPOSTSucessful(String api, Map<String, Object> parameters) throws IOException 212 { 213 return _isOperationSuccessful(_doPOST(api, parameters)); 214 } 215 216 private Map<String, Object> _execRequest(HttpUriRequest request, String authToken, String userId) throws IOException 217 { 218 request.setHeader("X-Auth-Token", authToken); 219 request.setHeader("X-User-Id", userId); 220 221 try (CloseableHttpClient httpClient = HttpClients.createDefault(); 222 CloseableHttpResponse response = httpClient.execute(request)) 223 { 224 Map<String, Object> convertJsonToMap = _jsonUtils.convertJsonToMap(EntityUtils.toString(response.getEntity())); 225 return convertJsonToMap; 226 } 227 } 228 229 private String _getError(Map<String, Object> info) 230 { 231 if (info.containsKey("error")) 232 { 233 return (String) info.get("error"); 234 } 235 else if (info.containsKey("message")) 236 { 237 return (String) info.get("message"); 238 } 239 else 240 { 241 return ""; 242 } 243 } 244 245 /** 246 * Creates a new private group. If the group already exists the operation will succeed. 247 * @param roomName name of the group 248 * @param create Create if necessary 249 * @return The group info. Can be null if no creation allowed 250 * @throws IOException something went wrong 251 */ 252 @SuppressWarnings("unchecked") 253 public Map<String, Object> getRoom(String roomName, boolean create) throws IOException 254 { 255 Map<String, Object> groupInfo = _doGet("v1/groups.info", Map.of("roomName", roomName)); 256 if (!_isOperationSuccessful(groupInfo)) 257 { 258 if (create) 259 { 260 groupInfo = _doPOST("v1/groups.create", Map.of("name", roomName)); 261 if (!_isOperationSuccessful(groupInfo)) 262 { 263 throw new IOException("Could not create room " + roomName + ", because " + _getError(groupInfo)); 264 } 265 } 266 else 267 { 268 return null; 269 } 270 } 271 return (Map<String, Object>) groupInfo.get("group"); 272 } 273 274 /** 275 * Remove a private group. If the group does not exist the operation will succeed. 276 * @param roomName name of the group 277 * @throws IOException something went wrong 278 */ 279 public void deleteRoom(String roomName) throws IOException 280 { 281 if (getRoom(roomName, false) != null) 282 { 283 Map<String, Object> deleteInfos = _doPOST("v1/groups.delete", Map.of("roomName", roomName)); 284 if (!_isOperationSuccessful(deleteInfos)) 285 { 286 throw new IOException("Could not delete room " + roomName + ", because " + _getError(deleteInfos)); 287 } 288 } 289 } 290 291 /** 292 * Get (or create) a new user. 293 * @param userIdentity the user that will be mirrored into chat 294 * @param create Create if missing 295 * @return the user info or null if user does not exist in chat and create was not required 296 * @throws IOException something went wrong 297 * @throws UserPreferencesException error while reading the user preferences 298 */ 299 @SuppressWarnings("unchecked") 300 public Map<String, Object> getUser(UserIdentity userIdentity, boolean create) throws IOException, UserPreferencesException 301 { 302 User user = _userManager.getUser(userIdentity); 303 if (user == null) 304 { 305 throw new IllegalStateException("Cannot create user in Rocket.Chat for unexisting user " + UserIdentity.userIdentityToString(userIdentity)); 306 } 307 308 Map<String, Object> userInfo = _doGet("v1/users.info", Map.of("username", userIdentitytoUserName(userIdentity))); 309 if (!_isOperationSuccessful(userInfo)) 310 { 311 if (create) 312 { 313 Map<String, String> ametysUserInfo = _getAmetysUserInfo(user); 314 String userName = ametysUserInfo.get("userName"); 315 String userEmail = ametysUserInfo.get("userEmail"); 316 317 userInfo = _doPOST("v1/users.create", Map.of("username", userIdentitytoUserName(userIdentity), 318 "email", userEmail, 319 "name", userName, 320 "verified", true, 321 "password", _getUserPassword(userIdentity))); 322 if (!_isOperationSuccessful(userInfo)) 323 { 324 throw new IllegalStateException("Cannot create user in Rocket.Chat for " + UserIdentity.userIdentityToString(userIdentity) + ": " + _getError(userInfo)); 325 } 326 327 getLogger().debug("User " + UserIdentity.userIdentityToString(userIdentity) + " created on the chat server"); 328 329 _updateAvatar(userIdentity); 330 } 331 else 332 { 333 return null; 334 } 335 } 336 337 Map<String, Object> userMap = (Map<String, Object>) userInfo.get("user"); 338 339 _userPreferencesManager.addUserPreference(userIdentity, __USERPREF_CONTEXT, Collections.emptyMap(), __USERPREF_PREF_ID, (String) userMap.get("_id")); 340 341 return userMap; 342 } 343 344 private Map<String, String> _getAmetysUserInfo(User user) 345 { 346 String userName = user.getFullName(); 347 String userEmail = user.getEmail(); 348 349 Content userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), null); 350 if (userContent != null) 351 { 352 userName = StringUtils.defaultIfBlank(StringUtils.join(new String[] {userContent.getValue("firstname"), userContent.getValue("lastname")}, " "), userName); 353 userEmail = StringUtils.defaultIfBlank(userContent.getValue("email"), userEmail); 354 } 355 356 return Map.of("userName", userName, 357 "userEmail", userEmail); 358 } 359 360 /** 361 * Update user name, email, avatar... on chat server 362 * @param userIdentity The user to update 363 * @param changePassword Update the user password (slow) 364 * @throws UserPreferencesException If an error occurred while getting user infos 365 * @throws IOException If an error occurred while updating 366 * @throws InterruptedException If an error occurred while updating 367 */ 368 public void updateUserInfos(UserIdentity userIdentity, boolean changePassword) throws IOException, UserPreferencesException, InterruptedException 369 { 370 User user = _userManager.getUser(userIdentity); 371 if (user == null) 372 { 373 throw new IllegalStateException("Cannot update user in Rocket.Chat for unexisting user " + UserIdentity.userIdentityToString(userIdentity)); 374 } 375 376 Map<String, String> ametysUserInfo = _getAmetysUserInfo(user); 377 String userName = ametysUserInfo.get("userName"); 378 String userEmail = ametysUserInfo.get("userEmail"); 379 380 Map<String, String> data = new HashMap<>(); 381 data.put("email", userEmail); 382 data.put("name", userName); 383 if (changePassword) 384 { 385 data.put("password", _getUserPassword(userIdentity)); 386 } 387 388 Map<String, Object> updateInfos = _doPOST("v1/users.update", Map.of("userId", _getUserId(userIdentity), 389 "data", data)); 390 if (!_isOperationSuccessful(updateInfos)) 391 { 392 throw new IOException("Cannot update user " + UserIdentity.userIdentityToString(userIdentity) + " on chat server: " + _getError(updateInfos)); 393 } 394 395 if (changePassword) 396 { 397 // When changing password, it unlogs people and this takes time 398 Thread.sleep(1000); 399 } 400 401 _updateAvatar(userIdentity); 402 } 403 404 private void _updateAvatar(UserIdentity user) 405 { 406 ContextHelper.getRequest(_context).setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true); 407 408 Source src = null; 409 try 410 { 411 src = _sourceResolver.resolveURI("cocoon://_plugins/user-directory/user/" + user.getPopulationId() + "/" + user.getLogin() + "/image_64"); 412 try (InputStream is = src.getInputStream()) 413 { 414 Map<String, Object> avatarInfo = _doMultipartPOST("v1/users.setAvatar", Map.of("username", userIdentitytoUserName(user), 415 "image", is)); 416 if (!_isOperationSuccessful(avatarInfo)) 417 { 418 getLogger().warn("Fail to update avatar for user " + UserIdentity.userIdentityToString(user) + ": " + _getError(avatarInfo)); 419 } 420 } 421 } 422 catch (Exception e) 423 { 424 getLogger().warn("Fail to update avatar for user " + UserIdentity.userIdentityToString(user), e); 425 } 426 finally 427 { 428 _sourceResolver.release(src); 429 } 430 431 } 432 433 private boolean _isUserInGroup(UserIdentity userIdentity, String roomName) throws IOException 434 { 435 Map<String, Object> getMembers = _doGet("v1/groups.members", Map.of("roomName", roomName)); 436 437 if (_isOperationSuccessful(getMembers)) 438 { 439 @SuppressWarnings("unchecked") 440 List<Map<String, String>> membersObject = (List<Map<String, String>>) getMembers.get("members"); 441 for (Map<String, String> map : membersObject) 442 { 443 if (userIdentitytoUserName(userIdentity).equalsIgnoreCase(map.get("username"))) 444 { 445 return true; 446 } 447 } 448 } 449 450 return false; 451 } 452 453 /** 454 * Adds a user to the private group. 455 * @param userIdentity user to invite 456 * @param roomName name of the group where to invite the user 457 * @throws IOException something went wrong 458 * @throws UserPreferencesException If the user need to be created and its password cannot be stored 459 */ 460 public void addUserToRoom(UserIdentity userIdentity, String roomName) throws IOException, UserPreferencesException 461 { 462 if (!_isUserInGroup(userIdentity, roomName)) 463 { 464 Map<String, Object> userInfo = getUser(userIdentity, true); 465 Map<String, Object> groupInfo = getRoom(roomName, true); 466 467 String roomId = (String) groupInfo.get("_id"); 468 String userId = (String) userInfo.get("_id"); 469 470 Map<String, Object> inviteInfo = _doPOST("v1/groups.invite", Map.of("roomId", roomId, 471 "userId", userId)); 472 473 if (!_isOperationSuccessful(inviteInfo)) 474 { 475 throw new IOException("Could not add user " + UserIdentity.userIdentityToString(userIdentity) + " to room " + roomName + ": " + _getError(inviteInfo)); 476 } 477 } 478 } 479 480 /** 481 * Remove all existing user 482 * @param roomName The room name 483 * @param except Do not remove these users 484 * @throws IOException If an error occurred 485 */ 486 public void removeAllUsersFromRoom(String roomName, List<UserIdentity> except) throws IOException 487 { 488 List<String> exceptUsernames = except.stream().map(ChatHelper::userIdentitytoUserName).collect(Collectors.toList()); 489 490 Map<String, Object> groupInfo = getRoom(roomName, false); 491 String roomId = (String) groupInfo.get("_id"); 492 493 Map<String, Object> membersInfo = _doGet("v1/groups.members", Map.of("roomId", roomId)); 494 if (_isOperationSuccessful(membersInfo)) 495 { 496 @SuppressWarnings("unchecked") 497 List<Map<String, Object>> members = (List<Map<String, Object>>) membersInfo.get("members"); 498 for (Map<String, Object> member : members) 499 { 500 if (!exceptUsernames.contains(member.get("username"))) 501 { 502 _doPOST("v1/groups.kick", Map.of("roomId", roomId, 503 "userId", (String) member.get("_id"))); 504 } 505 } 506 } 507 } 508 509 /** 510 * Removes a user from the private group. 511 * @param user user to kick 512 * @param roomName name of the group from where to kick the user 513 * @return true if success 514 * @throws IOException something went wrong 515 * @throws UserPreferencesException If an error occurred with user password 516 */ 517 public boolean removeUserFromRoom(UserIdentity user, String roomName) throws IOException, UserPreferencesException 518 { 519 if (_isUserInGroup(user, roomName)) 520 { 521 Map<String, Object> userInfo = getUser(user, false); 522 Map<String, Object> groupInfo = getRoom(roomName, false); 523 524 if (userInfo != null && groupInfo != null) 525 { 526 String roomId = (String) groupInfo.get("_id"); 527 String userId = (String) userInfo.get("_id"); 528 529 return _isPOSTSucessful("v1/groups.kick", Map.of("roomId", roomId, 530 "userId", userId)); 531 } 532 else 533 { 534 return false; 535 } 536 } 537 538 return true; 539 } 540 541 /** 542 * Convert a useridentity to a chat server username 543 * @param userIdentity The user to convert 544 * @return the chat username 545 */ 546 public static String userIdentitytoUserName(UserIdentity userIdentity) 547 { 548 return UserIdentity.userIdentityToString(userIdentity).replaceAll("[@# ]", "_"); 549 } 550 551 private String _getUserPassword(UserIdentity userIdentity) throws UserPreferencesException 552 { 553 String cryptedPassword = _userPreferencesManager.getUserPreferenceAsString(userIdentity, __USERPREF_CONTEXT, Collections.emptyMap(), __USERPREF_PREF_PASSWORD); 554 if (!StringUtils.isBlank(cryptedPassword)) 555 { 556 try 557 { 558 return _cryptoHelper.decrypt(cryptedPassword); 559 } 560 catch (CryptoHelper.WrongKeyException e) 561 { 562 getLogger().warn("Password of user {} cannot be decrypted, and thus will be reset", UserIdentity.userIdentityToString(userIdentity), e); 563 } 564 } 565 566 return _generateAndStorePassword(userIdentity); 567 } 568 569 private String _getUserAuthToken(UserIdentity userIdentity) throws UserPreferencesException, IOException, InterruptedException 570 { 571 String cryptedToken = _userPreferencesManager.getUserPreferenceAsString(userIdentity, __USERPREF_CONTEXT, Collections.emptyMap(), __USERPREF_PREF_TOKEN); 572 if (!StringUtils.isBlank(cryptedToken)) 573 { 574 try 575 { 576 return _cryptoHelper.decrypt(cryptedToken); 577 } 578 catch (CryptoHelper.WrongKeyException e) 579 { 580 getLogger().warn("Token of user {} cannot be decrypted, and thus will be reset", UserIdentity.userIdentityToString(userIdentity), e); 581 } 582 } 583 return _generateAndStoreAuthToken(userIdentity, true); 584 } 585 586 587 private String _getUserId(UserIdentity userIdentity) throws UserPreferencesException 588 { 589 String userId = _userPreferencesManager.getUserPreferenceAsString(userIdentity, __USERPREF_CONTEXT, Collections.emptyMap(), __USERPREF_PREF_ID); 590 return userId; 591 } 592 593 private String _generateAndStorePassword(UserIdentity user) throws UserPreferencesException 594 { 595 Double random = Math.random(); 596 byte[] randoms = {random.byteValue()}; 597 String randomPassword = Sha2Crypt.sha256Crypt(randoms); 598 599 String cryptedPassword = _cryptoHelper.encrypt(randomPassword); 600 _userPreferencesManager.addUserPreference(user, __USERPREF_CONTEXT, Collections.emptyMap(), __USERPREF_PREF_PASSWORD, cryptedPassword); 601 602 return randomPassword; 603 } 604 605 606 @SuppressWarnings("unchecked") 607 private String _generateAndStoreAuthToken(UserIdentity user, boolean tryToChangePassword) throws IOException, UserPreferencesException, InterruptedException 608 { 609 Map<String, Object> loginInfo = _doPOST("v1/login", Map.of("user", userIdentitytoUserName(user), 610 "password", _getUserPassword(user))); 611 if (_isOperationSuccessful(loginInfo)) 612 { 613 String authToken = (String) ((Map<String, Object>) loginInfo.get("data")).get("authToken"); 614 String cryptedAuthToken = _cryptoHelper.encrypt(authToken); 615 _userPreferencesManager.addUserPreference(user, __USERPREF_CONTEXT, Collections.emptyMap(), __USERPREF_PREF_TOKEN, cryptedAuthToken); 616 617 return authToken; 618 } 619 else if (tryToChangePassword) 620 { 621 this.updateUserInfos(user, true); 622 623 return _generateAndStoreAuthToken(user, false); 624 } 625 else 626 { 627 throw new IOException("Could not log user " + UserIdentity.userIdentityToString(user) + " into chat " + _getError(loginInfo)); 628 } 629 } 630 631 private String _getUserStatus(UserIdentity user, String authToken, String userId) throws IOException 632 { 633 Map<String, Object> statusInfo = _doGet("v1/users.getStatus", Map.of("user", userIdentitytoUserName(user)), authToken, userId); 634 if (_isOperationSuccessful(statusInfo)) 635 { 636 return (String) statusInfo.get("status"); 637 } 638 else 639 { 640 return null; 641 } 642 } 643 644 /** 645 * Read the JSON result to test for success 646 * @param result the JSON result of a rest call 647 * @return true if success 648 */ 649 protected boolean _isOperationSuccessful(Map<String, Object> result) 650 { 651 Boolean success = false; 652 if (result != null) 653 { 654 Object successObj = result.get("success"); 655 if (successObj instanceof Boolean) 656 { 657 success = (Boolean) successObj; 658 } 659 else if (successObj instanceof String) 660 { 661 success = "true".equalsIgnoreCase((String) successObj); 662 } 663 else 664 { 665 Object statusObj = result.get("status"); 666 if (statusObj instanceof String) 667 { 668 success = "success".equalsIgnoreCase((String) statusObj); 669 } 670 } 671 } 672 return success; 673 } 674 675 /** 676 * Login the current user to the given room 677 * @param roomName The room to log in 678 * @return The info about the user 679 * @throws UserPreferencesException If the user password stored in prefs has an issue 680 * @throws IOException If an error occurred 681 * @throws InterruptedException If an error occurred 682 */ 683 @Callable 684 public Map<String, Object> login(String roomName) throws IOException, UserPreferencesException, InterruptedException 685 { 686 // Get current user in Ametys 687 UserIdentity user = _currentUserProvider.getUser(); 688 689 // Ensure user exists on chat server 690 getUser(user, true); 691 692 // Ensure the room exists on chat server 693 getRoom(roomName, true); 694 695 // Ensure the user is part of the room on chat server 696 addUserToRoom(user, roomName); 697 698 // Get the login info of the user 699 String authToken = _getUserAuthToken(user); 700 String userId = _getUserId(user); 701 702 // Ensure token validity by getting status on chat server 703 String status = _getUserStatus(user, authToken, userId); 704 if (status == null) 705 { 706 // If we cannot get status, this is probably because the auth token has expired or the user was recreated. Try a new one 707 authToken = _generateAndStoreAuthToken(user, true); 708 709 status = _getUserStatus(user, authToken, userId); 710 711 if (status == null) 712 { 713 throw new IllegalStateException("Cannot get the status of user " + UserIdentity.userIdentityToString(user)); 714 } 715 } 716 717 return Map.of("userId", userId, 718 "authToken", authToken, 719 "userName", userIdentitytoUserName(user), 720 "status", status); 721 } 722}