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.observer; 017 018import java.util.Collection; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.Set; 024import java.util.function.Function; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030 031import org.ametys.cms.ObservationConstants; 032import org.ametys.cms.data.RichText; 033import org.ametys.cms.data.RichTextHelper; 034import org.ametys.cms.data.type.ModelItemTypeConstants; 035import org.ametys.cms.repository.Content; 036import org.ametys.core.observation.AsyncObserver; 037import org.ametys.core.observation.Event; 038import org.ametys.core.right.AllowedUsers; 039import org.ametys.core.right.RightManager; 040import org.ametys.core.user.User; 041import org.ametys.core.user.UserIdentity; 042import org.ametys.core.user.UserManager; 043import org.ametys.core.user.population.UserPopulationDAO; 044import org.ametys.plugins.mobileapp.PushNotificationManager; 045import org.ametys.plugins.mobileapp.QueriesHelper; 046import org.ametys.plugins.mobileapp.UserPreferencesHelper; 047import org.ametys.plugins.queriesdirectory.Query; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.runtime.config.Config; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051import org.ametys.web.repository.content.WebContent; 052import org.ametys.web.repository.site.Site; 053 054/** 055 * On validation, test each query to notify impacted users 056 */ 057public class ContentValidatedObserver extends AbstractLogEnabled implements AsyncObserver, Serviceable 058{ 059 private static final String __DESCRIPTION_MAX_SIZE_CONF_ID = "plugin.mobileapp.push.description.richtext.max"; 060 061 private QueriesHelper _queryHelper; 062 private UserPreferencesHelper _userPreferencesHelper; 063 private PushNotificationManager _pushNotificationManager; 064 private UserManager _userManager; 065 private UserPopulationDAO _userPopulationDAO; 066 private RightManager _rightManager; 067 private RichTextHelper _richTextHelper; 068 private AmetysObjectResolver _resolver; 069 070 @Override 071 public void service(ServiceManager manager) throws ServiceException 072 { 073 _queryHelper = (QueriesHelper) manager.lookup(QueriesHelper.ROLE); 074 _userPreferencesHelper = (UserPreferencesHelper) manager.lookup(UserPreferencesHelper.ROLE); 075 _pushNotificationManager = (PushNotificationManager) manager.lookup(PushNotificationManager.ROLE); 076 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 077 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 078 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 079 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 080 _richTextHelper = (RichTextHelper) manager.lookup(RichTextHelper.ROLE); 081 } 082 083 public boolean supports(Event event) 084 { 085 return event.getId().equals(ObservationConstants.EVENT_CONTENT_VALIDATED); 086 } 087 088 public int getPriority(Event event) 089 { 090 return MIN_PRIORITY; 091 } 092 093 public void observe(Event event, Map<String, Object> transientVars) throws Exception 094 { 095 String contentId = (String) event.getArguments().get(ObservationConstants.ARGS_CONTENT_ID); 096 Content content = _resolver.resolveById(contentId); 097 098 if (!(content instanceof WebContent webContent)) 099 { 100 getLogger().debug("We currently do not support push notifications for off-site content {}", content.getId()); 101 return; 102 } 103 104 // First find the queries that whose results contain the content 105 Site site = webContent.getSite(); 106 Set<Query> queries = _queryHelper.getQueriesForResult(content.getId(), site); 107 108 getLogger().info("{} queries found for content {}", queries.size(), content.getId()); 109 110 if (queries.isEmpty()) 111 { 112 return; 113 } 114 115 // Then collect users that have read access to the content 116 Set<UserIdentity> users; 117 AllowedUsers readAccessAllowedUsers = _rightManager.getReadAccessAllowedUsers(content); 118 if (readAccessAllowedUsers.isAnonymousAllowed() || readAccessAllowedUsers.isAnyConnectedUserAllowed()) 119 { 120 List<String> userPopulationsIds = _userPopulationDAO.getUserPopulationsIds(); 121 Collection<User> allUsers = _userManager.getUsersByPopulationIds(userPopulationsIds); 122 users = allUsers.stream().map(User::getIdentity).collect(Collectors.toSet()); 123 } 124 else 125 { 126 users = readAccessAllowedUsers.resolveAllowedUsers(true); 127 } 128 129 Map<String, Query> queryMap = queries.stream().collect(Collectors.toMap(Query::getId, Function.identity())); 130 Set<String> feedIds = queryMap.keySet(); 131 132 // Then find the users that have subscribed to the queries 133 Map<String, Map<UserIdentity, Set<String>>> notificationsNeeded = new HashMap<>(); 134 for (UserIdentity user : users) 135 { 136 Map<String, Set<String>> tokensForUser = _userPreferencesHelper.getUserImpactedTokens(user, feedIds, site); 137 for (Entry<String, Set<String>> tokensByFeed : tokensForUser.entrySet()) 138 { 139 Map<UserIdentity, Set<String>> tokens = notificationsNeeded.computeIfAbsent(tokensByFeed.getKey(), __ -> new HashMap<>()); 140 tokens.put(user, tokensByFeed.getValue()); 141 } 142 } 143 144 Map<String, String> data = _queryHelper.getDataForContent(content); 145 Map<String, String> sorts = queries.stream().collect(Collectors.toMap(Query::getId, q -> _queryHelper.getSortProperty(q, true).get(0).getField())); 146 147 // Finally send the notifications 148 for (Entry<String, Map<UserIdentity, Set<String>>> entry : notificationsNeeded.entrySet()) 149 { 150 Map<String, Object> notificationData = new HashMap<>(); 151 notificationData.putAll(data); 152 String feedId = entry.getKey(); 153 notificationData.put("feed_id", feedId); 154 if (queryMap.containsKey(feedId)) 155 { 156 notificationData.put("category_name", queryMap.get(feedId).getTitle()); 157 } 158 159 String sortField = null; 160 if (sorts.containsKey(feedId)) 161 { 162 sortField = sorts.get(feedId); 163 } 164 String isoDate = _queryHelper.getContentFormattedDate(content, sortField); 165 notificationData.put("date", isoDate); 166 167 String description = _getContentDescription(content); 168 169 _pushNotificationManager.pushNotifications(content.getTitle(), description, entry.getValue(), notificationData); 170 } 171 } 172 173 /** 174 * Get a description for the notification. 175 * It will first try to read a "abstract" value in the content, and if not available, will try to read a rich-text stored in "content" (and cut it down). 176 * If none is available, an empty String is returned. 177 * @param content The content to read 178 * @return a description for this content 179 */ 180 protected String _getContentDescription(Content content) 181 { 182 Long maxSize = Config.getInstance().getValue(__DESCRIPTION_MAX_SIZE_CONF_ID); 183 String result = ""; 184 185 if (content.hasValue("abstract") && org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(content.getDefinition("abstract").getType().getId())) 186 { 187 result = content.getValue("abstract", false, ""); 188 } 189 else if (content.hasValue("content") && ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(content.getDefinition("content").getType().getId())) 190 { 191 RichText richText = content.getValue("content"); 192 result = _richTextHelper.richTextToString(richText, maxSize.intValue()); 193 } 194 195 return result; 196 } 197}