001/*
002 *  Copyright 2020 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.mobileapp;
017
018import java.time.LocalDate;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.collections4.CollectionUtils;
032
033import org.ametys.core.user.CurrentUserProvider;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.core.userpref.UserPreferencesException;
036import org.ametys.core.userpref.UserPreferencesManager;
037import org.ametys.core.util.JSONUtils;
038import org.ametys.plugins.workspaces.project.objects.Project;
039import org.ametys.runtime.plugin.component.AbstractLogEnabled;
040import org.ametys.web.repository.site.Site;
041
042/**
043 * Helper to store/retreive conf per user
044 */
045public class UserPreferencesHelper extends AbstractLogEnabled implements Serviceable, Component
046{
047    /** The avalon role. */
048    public static final String ROLE = UserPreferencesHelper.class.getName();
049
050    private static final String __USERPREF_KEY_LANG = "lang";
051    private static final String __USERPREF_KEY_SITE = "site";
052    private static final String __USERPREF_KEY_ENABLED = "enabled";
053    private static final String __USERPREF_KEY_ALL_FEEDS = "allFeeds";
054    private static final String __USERPREF_KEY_FEEDS = "feeds";
055    private static final String __USERPREF_KEY_ALL_PROJECTS = "allProjects";
056    private static final String __USERPREF_KEY_PROJECTS = "projects";
057    private static final String __USERPREF_KEY_ALL_TYPES = "allTypes";
058    private static final String __USERPREF_KEY_TYPES = "types";
059
060    /** User Preferences Manager */
061    protected UserPreferencesManager _userPreferencesManager;
062
063    /** The current user provider */
064    protected CurrentUserProvider _currentUserProvider;
065
066    /** JSON Utils */
067    protected JSONUtils _jsonUtils;
068
069
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE);
073        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
074        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
075    }
076
077    /**
078     * Get the list of impacted tokens for a user without any feeds consideration. The global notification preferences flag is honoured.
079     * @param user user to check
080     * @return a map with a Set of tokens for each feedId
081     */
082    public Set<String> getUserImpactedToken(UserIdentity user)
083    {
084        Set<String> tokens = new HashSet<>();
085        try
086        {
087            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
088            for (Map.Entry<String, String> entry : unTypedUserPrefs.entrySet())
089            {
090                String token = entry.getKey();
091                String value = entry.getValue();
092                Map<String, Object> prefs = _jsonUtils.convertJsonToMap(value);
093                
094                boolean enabled = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ENABLED), true);
095                if (enabled)
096                {
097                    tokens.add(token);
098                }
099            }
100        }
101        catch (UserPreferencesException e)
102        {
103            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
104        }
105        
106        return tokens;
107    }
108
109    /**
110     * Get the list of impacted tokens for each feeds, for a user
111     * @param user user to check
112     * @param feedIds list of feeds to check
113     * @param site the site for the validated content
114     * @return a map with a Set of tokens for each feedId
115     */
116    @SuppressWarnings("unchecked")
117    public Map<String, Set<String>> getUserImpactedTokens(UserIdentity user, Set<String> feedIds, Site site)
118    {
119        Map<String, Set<String>> feedsAndTokens = new HashMap<>();
120        try
121        {
122            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
123            for (Map.Entry<String, String> entry : unTypedUserPrefs.entrySet())
124            {
125                String token = entry.getKey();
126                String value = entry.getValue();
127                Map<String, Object> prefs = _jsonUtils.convertJsonToMap(value);
128                
129                List<String> feeds = (List<String>) prefs.get(__USERPREF_KEY_FEEDS);
130                String siteName = (String) prefs.get(__USERPREF_KEY_SITE);
131                
132                boolean enabled = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ENABLED), true);
133                boolean allFeeds = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ALL_FEEDS), feeds == null);
134                
135                if (enabled)
136                {
137                    boolean hasSiteQueries = site.hasValue(QueriesHelper.QUERY_CONTAINER_SITE_CONF_ID);
138                    
139                    if (siteName == null || hasSiteQueries && siteName.equals(site.getName()))
140                    {
141                        Collection<String> matchingFeeds = allFeeds || feeds == null ? feedIds : CollectionUtils.intersection(feeds, feedIds);
142                        
143                        for (String feedId : matchingFeeds)
144                        {
145                            Set tokens = feedsAndTokens.computeIfAbsent(feedId, __ -> new HashSet<>());
146                            tokens.add(token);
147                        }
148                    }
149                }
150            }
151        }
152        catch (UserPreferencesException e)
153        {
154            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
155        }
156
157        return feedsAndTokens;
158    }
159
160    /**
161     * Get the list of impacted tokens for each feeds, for a user
162     * @param user user to check
163     * @param project test if a notification should be sent for this project
164     * @param eventType test if a notification should be sent for this event type
165     * @return a map with a the set of tokens impacted for each languages
166     */
167    @SuppressWarnings("unchecked")
168    public Map<String, Set<String>> getUserImpactedTokens(UserIdentity user, Project project, String eventType)
169    {
170        Map<String, Set<String>> tokensForLanguage = new HashMap<>();
171        try
172        {
173            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
174            for (Map.Entry<String, String> entry : unTypedUserPrefs.entrySet())
175            {
176                String token = entry.getKey();
177                String value = entry.getValue();
178                Map<String, Object> prefs = _jsonUtils.convertJsonToMap(value);
179                
180                List<String> projects = (List<String>) prefs.get(__USERPREF_KEY_PROJECTS);
181                List<String> types = (List<String>) prefs.get(__USERPREF_KEY_TYPES);
182                
183                boolean enabled = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ENABLED), true);
184                boolean allProjects = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ALL_PROJECTS), projects == null);
185                boolean allTypes = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ALL_TYPES), types == null);
186                
187                if (enabled)
188                {
189                    boolean projectMatch = allProjects || projects == null || projects.contains(project.getId());
190                    boolean typeMatch = allTypes || types == null || types.contains(eventType);
191                    
192                    if (projectMatch && typeMatch)
193                    {
194                        String lang = (String) prefs.get(__USERPREF_KEY_LANG);
195                        
196                        Set<String> tokens = tokensForLanguage.computeIfAbsent(lang, __ -> new HashSet<>());
197                        tokens.add(token);
198                    }
199                }
200            }
201        }
202        catch (UserPreferencesException e)
203        {
204            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
205        }
206
207        return tokensForLanguage;
208    }
209
210    private boolean _getValueOrDefault(Boolean value, boolean defaultValue)
211    {
212        return value != null ? value : defaultValue;
213    }
214
215    /**
216     * Get all notification tokens for current user
217     * @return the list of notification tokens
218     */
219    public Set<String> getNotificationTokens()
220    {
221        UserIdentity user = _currentUserProvider.getUser();
222        return getNotificationTokens(user);
223    }
224
225    /**
226     * Get all notification tokens of a given user. This list does not take into account user's notification preferences such as disabling all notifications.
227     * @param user the user impacted
228     * @return the list of notification tokens
229     */
230    public Set<String> getNotificationTokens(UserIdentity user)
231    {
232        try
233        {
234            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
235            return unTypedUserPrefs.keySet();
236        }
237        catch (UserPreferencesException e)
238        {
239            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
240        }
241
242        return Collections.EMPTY_SET;
243    }
244
245    /**
246     * Removes a notification token for the current user
247     * @param pushToken the token to remove
248     */
249    public void removeNotificationToken(String pushToken)
250    {
251        UserIdentity user = _currentUserProvider.getUser();
252        removeNotificationToken(pushToken, user);
253    }
254
255    /**
256     * Removes a notification token for a user
257     * @param pushToken the token to remove
258     * @param user the user impacted
259     */
260    public void removeNotificationToken(String pushToken, UserIdentity user)
261    {
262        try
263        {
264            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
265            unTypedUserPrefs.remove(pushToken);
266            _userPreferencesManager.setUserPreferences(user, "/mobileapp", Collections.emptyMap(), unTypedUserPrefs);
267        }
268        catch (UserPreferencesException e)
269        {
270            getLogger().error("Impossible to remove a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
271        }
272    }
273
274    /**
275     * Remove all the notification tokens for a user
276     * @param user the user impacted
277     */
278    public void removeAllNotificationTokens(UserIdentity user)
279    {
280        try
281        {
282            _userPreferencesManager.removeAllUserPreferences(user, "/mobileapp", Collections.emptyMap());
283        }
284        catch (UserPreferencesException e)
285        {
286            getLogger().error("Impossible to remove all user notification tokens, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
287        }
288    }
289
290    /**
291     * Save the notification settings for the current user
292     * @param pushToken the token to impact
293     * @param enabled if notifications are enabled
294     * @param allFeeds if all feeds are allowed to be notified
295     * @param feeds list of feeds ID for notifications
296     * @param allProjects if all projects are allowed to be notified
297     * @param projects list of project ID for notifications
298     * @param allTypes if all types are allowed to be notified
299     * @param types list of notifications types for notifications in projects
300     * @param lang lang of this device
301     */
302    public void setNotificationSettings(String pushToken, boolean enabled, boolean allFeeds, List<String> feeds, boolean allProjects, List<String> projects, boolean allTypes, List<String> types, String lang)
303    {
304        UserIdentity user = _currentUserProvider.getUser();
305        setNotificationSettings(pushToken, enabled, allFeeds, feeds, allProjects, projects, allTypes, types, lang, user);
306    }
307
308    /**
309     * Save the notification settings for a user
310     * @param pushToken token impacted by this settings
311     * @param enabled if notifications are enabled
312     * @param allFeeds if all feeds are allowed to be notified
313     * @param feeds list of feeds ID for notifications
314     * @param allProjects if all projects are allowed to be notified
315     * @param projects list of project ID for notifications
316     * @param allTypes if all types are allowed to be notified
317     * @param types list of notifications types for notifications in projects
318     * @param lang lang of this device
319     * @param user the user impacted
320     */
321    public synchronized void setNotificationSettings(String pushToken, boolean enabled, boolean allFeeds, List<String> feeds, boolean allProjects, List<String> projects, boolean allTypes, List<String> types, String lang, UserIdentity user)
322    {
323        // synchronized because it happens that the app send multiple calls nearly simultaneously
324        Map<String, Object> values = new HashMap<>();
325        values.put(__USERPREF_KEY_LANG, lang);
326        
327        values.put(__USERPREF_KEY_ENABLED, enabled);
328        
329        values.put(__USERPREF_KEY_ALL_FEEDS, allFeeds);
330        if (feeds != null)
331        {
332            values.put(__USERPREF_KEY_FEEDS, feeds);
333        }
334        
335        values.put(__USERPREF_KEY_ALL_PROJECTS, allProjects);
336        if (projects != null)
337        {
338            values.put(__USERPREF_KEY_PROJECTS, projects);
339        }
340
341        values.put(__USERPREF_KEY_ALL_TYPES, allTypes);
342        if (types != null)
343        {
344            values.put(__USERPREF_KEY_TYPES, types);
345        }
346
347        values.put("epochDay", LocalDate.now().toEpochDay());
348
349        String valuesAsString = _jsonUtils.convertObjectToJson(values);
350
351        try
352        {
353            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
354            if (unTypedUserPrefs == null)
355            {
356                unTypedUserPrefs = new HashMap<>();
357            }
358
359            unTypedUserPrefs.put(pushToken, valuesAsString);
360
361            _userPreferencesManager.setUserPreferences(user, "/mobileapp", Collections.emptyMap(), unTypedUserPrefs);
362        }
363        catch (UserPreferencesException e)
364        {
365            getLogger().error("Impossible to set user preferences for user '" + UserIdentity.userIdentityToString(user) + "'", e);
366        }
367    }
368
369    /**
370     * Get the notification settings for a token
371     * @param pushToken the token to read
372     * @param user the user impacted
373     * @return a map containing feeds, projects and types as Set&lt;String&gt;, and epochDay as the last modification date
374     */
375    public Map<String, Object> getNotificationSettings(String pushToken, UserIdentity user)
376    {
377        try
378        {
379            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
380            if (unTypedUserPrefs != null && unTypedUserPrefs.containsKey(pushToken))
381            {
382                String prefs = unTypedUserPrefs.get(pushToken);
383                return _jsonUtils.convertJsonToMap(prefs);
384            }
385        }
386        catch (UserPreferencesException e)
387        {
388            getLogger().error("Impossible to retreive user preferences for user '" + UserIdentity.userIdentityToString(user) + "'", e);
389        }
390        return null;
391    }
392
393}