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