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<String>, 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}