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.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Map.Entry;
022import java.util.Objects;
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.core.observation.AsyncObserver;
032import org.ametys.core.observation.Event;
033import org.ametys.core.user.CurrentUserProvider;
034import org.ametys.core.user.User;
035import org.ametys.core.user.UserIdentity;
036import org.ametys.plugins.mobileapp.FeedHelper;
037import org.ametys.plugins.mobileapp.PushNotificationManager;
038import org.ametys.plugins.mobileapp.UserPreferencesHelper;
039import org.ametys.plugins.mobileapp.action.GetEventTypesAction;
040import org.ametys.plugins.repository.AmetysObjectResolver;
041import org.ametys.plugins.repository.activities.Activity;
042import org.ametys.plugins.repository.activities.ActivityType;
043import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType;
044import org.ametys.plugins.workspaces.members.ProjectMemberManager;
045import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember;
046import org.ametys.plugins.workspaces.project.ProjectManager;
047import org.ametys.plugins.workspaces.project.objects.Project;
048import org.ametys.runtime.config.Config;
049import org.ametys.runtime.plugin.PluginsManager;
050import org.ametys.runtime.plugin.component.AbstractLogEnabled;
051import org.ametys.web.renderingcontext.RenderingContext;
052import org.ametys.web.renderingcontext.RenderingContextHandler;
053/**
054 * On validation, test each query to notify impacted users
055 */
056public class ProjectActivityObserver extends AbstractLogEnabled implements AsyncObserver, Serviceable
057{
058    private static final String __PROJECT_ENABLED_CONF_ID = "plugin.mobileapp.project.enabled";
059
060    private FeedHelper _feedHelper;
061    private UserPreferencesHelper _userPreferencesHelper;
062    private PushNotificationManager _pushNotificationManager;
063    private ProjectManager _projectManager;
064    private AmetysObjectResolver _resolver;
065    private ProjectMemberManager _projectMemberHelper;
066    private CurrentUserProvider _currentUserProvider;
067    private RenderingContextHandler _renderingContextHandler;
068
069    @Override
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        _feedHelper = (FeedHelper) manager.lookup(FeedHelper.ROLE);
073        _userPreferencesHelper = (UserPreferencesHelper) manager.lookup(UserPreferencesHelper.ROLE);
074        _pushNotificationManager = (PushNotificationManager) manager.lookup(PushNotificationManager.ROLE);
075        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
076        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
077        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
078        
079        if (PluginsManager.getInstance().isPluginActive("workspaces"))
080        {
081            _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
082            _projectMemberHelper = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
083        }
084    }
085
086    public boolean supports(Event event)
087    {
088        return PluginsManager.getInstance().isPluginActive("workspaces")
089                && Config.getInstance().getValue(__PROJECT_ENABLED_CONF_ID, false, false)
090                && org.ametys.plugins.repository.ObservationConstants.EVENT_ACTIVITY_CREATED.contains(event.getId());
091    }
092
093    public int getPriority()
094    {
095        return MAX_PRIORITY;
096    }
097
098    public void observe(Event event, Map<String, Object> transientVars) throws Exception
099    {
100        String activityId = (String) event.getArguments().get(org.ametys.plugins.repository.ObservationConstants.ARGS_ACTIVITY_ID);
101        Activity activity = _resolver.resolveById(activityId);
102        
103        // Push notifications are only sent for allowed types
104        if (GetEventTypesAction.EVENT_TYPES.containsKey(activity.getEventType()))
105        {
106            ActivityType activityType = activity.getActivityType();
107            if (activityType instanceof AbstractWorkspacesActivityType)
108            {
109                RenderingContext currentRenderingContext = _renderingContextHandler.getRenderingContext();
110                try
111                {
112                    // Force FRONT RenderingContext as it is reset to BACK being in an AsyncObserver
113                    _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
114                    Project project = _projectManager.getProject(activity.getValue(AbstractWorkspacesActivityType.PROJECT_NAME));
115                    getLogger().info("Listing push notification to send for activity on project '{}'", project.getId());
116                    
117                    Map<String, Object> activity2json = activity.toJSONForClient();
118                    
119                    Map<String, Object> projectJson = _feedHelper.projectToMap(project);
120                    
121                    // we only look for tokens among project members
122                    Set<ProjectMember> members = _projectMemberHelper.getProjectMembers(project, true);
123                    List<UserIdentity> users = members.stream().map(member -> member.getUser())
124                                                               .filter(Objects::nonNull)
125                                                               .map(User::getIdentity)
126                                                               // User should not be notified by his own actions
127                                                               .filter(user -> !user.equals(_currentUserProvider.getUser()))
128                                                               .toList();
129                    
130                    // Get all map of language => tokens for all users, and generate a new big map language => tokens
131                    Map<String, Map<UserIdentity, Set<String>>> langAndTokens = new HashMap<>();
132                    for (UserIdentity user : users)
133                    {
134                        Map<String, Set<String>> tokensForUser = _userPreferencesHelper.getUserImpactedTokens(user, project, activity.getEventType());
135                        for (Entry<String, Set<String>> tokensByLang : tokensForUser.entrySet())
136                        {
137                            Map<UserIdentity, Set<String>> tokens = langAndTokens.computeIfAbsent(tokensByLang.getKey(), __ -> new HashMap<>());
138                            tokens.put(user, tokensByLang.getValue());
139                        }
140                    }
141                    
142                    if (getLogger().isDebugEnabled())
143                    {
144                        getLogger().debug("Push tokens " + langAndTokens.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue().size()).collect(Collectors.joining(", ")));
145                    }
146                    
147                    // Get the activities infos for each languages
148                    Map<String, Map<String, Object>> translatedActivityData = langAndTokens.keySet().stream()
149                            .distinct()
150                            .collect(Collectors.toMap(Function.identity(), lang -> _feedHelper.getActivityInfos(activity2json, projectJson, lang)));
151                    
152                    for (Entry<String, Map<UserIdentity, Set<String>>> entry : langAndTokens.entrySet())
153                    {
154                        String lang = entry.getKey();
155                        Map<UserIdentity, Set<String>> tokens = entry.getValue();
156                        Map<String, Object> activityData = translatedActivityData.get(lang);
157                        
158                        try
159                        {
160                            @SuppressWarnings("unchecked")
161                            Map<String, Object> category = (Map<String, Object>) ((Map<String, Object>) activityData.get("project")).get("category");
162                            String categoryLabel = ((org.ametys.runtime.i18n.I18nizableText) category.get("title")).getLabel();
163                            category.put("title", categoryLabel);
164                        }
165                        catch (Exception e)
166                        {
167                            // Nothing, this is just a hack do pass the notification framework jsonifier
168                        }
169                        
170                        String title = project.getTitle();
171                        String message = (String) activityData.get("short-description");
172                        
173                        Map<String, Object> notificationData = new HashMap<>();
174                        notificationData.put("project_name", project.getName());
175                        notificationData.put("project_id", project.getId());
176                        notificationData.put("title", title);
177                        notificationData.put("date", activityData.get("date"));
178                        notificationData.put("content_id", activityData.get("content_id"));
179                        notificationData.put("content_url", activityData.get("content_url"));
180                        
181                        _pushNotificationManager.pushNotifications(title, message, tokens, notificationData);
182                    }
183                }
184                finally
185                {
186                    _renderingContextHandler.setRenderingContext(currentRenderingContext);
187                }
188            }
189        }
190    }
191}