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