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}