001/*
002 *  Copyright 2024 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.cms.repository.mentions;
017
018import java.io.IOException;
019import java.time.ZonedDateTime;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.core.observation.AsyncObserver;
032import org.ametys.core.observation.Event;
033import org.ametys.core.ui.mail.StandardMailBodyHelper;
034import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
035import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder.UnauthenticatedUserInput;
036import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder.UserInput;
037import org.ametys.core.user.CurrentUserProvider;
038import org.ametys.core.user.User;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.user.UserManager;
041import org.ametys.core.util.I18nUtils;
042import org.ametys.core.util.language.UserLanguagesManager;
043import org.ametys.core.util.mail.SendMailHelper;
044import org.ametys.plugins.repository.AmetysObject;
045import org.ametys.plugins.repository.AmetysObjectResolver;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.plugin.component.AbstractLogEnabled;
048
049import jakarta.mail.MessagingException;
050
051/**
052 * Abstract observer to send mails to mentioned users in {@link AmetysObject}
053 * @param <T> type of the {@link AmetysObject}
054 */
055public abstract class AbstractNotifyMentionsObserver<T extends AmetysObject> extends AbstractLogEnabled implements Component, Serviceable, AsyncObserver
056{
057    /** The ametys object resolver */
058    protected AmetysObjectResolver _resolver;
059    /** The user manager */
060    protected UserManager _userManager;
061    /** The current user provider */
062    protected CurrentUserProvider _currentUserProvider;
063    /** The i18n utils. */
064    protected I18nUtils _i18nUtils;
065    
066    /** Cache for resolved users */
067    protected Map<UserIdentity, User> _resolvedUsers = new HashMap<>();
068    
069    /** The mention utils */
070    protected MentionUtils _mentionUtils;
071    /** The user languages manager */
072    protected UserLanguagesManager _userLanguagesManager;
073    
074    public void service(ServiceManager manager) throws ServiceException
075    {
076        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
077        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
078        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
079        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
080        _mentionUtils = (MentionUtils) manager.lookup(MentionUtils.ROLE);
081        _userLanguagesManager = (UserLanguagesManager) manager.lookup(UserLanguagesManager.ROLE);
082    }
083
084    public int getPriority()
085    {
086        return MIN_PRIORITY;
087    }
088
089    public void observe(Event event, Map<String, Object> transientVars) throws Exception
090    {
091        Map<String, Object> arguments = event.getArguments();
092
093        MentionableObject mentionableObject = _getMentionableObjectFromArguments(arguments);
094
095        // Send mail to all mentioned users in the added mentionable object
096        _sendMailToMentionedUsers(mentionableObject, arguments);
097    }
098
099    /**
100     * Send mail to all mentioned users in the mentionable object
101     * @param mentionableObject the mentionable object
102     * @param arguments the event arguments
103     */
104    @SuppressWarnings("unchecked")
105    protected void _sendMailToMentionedUsers(MentionableObject mentionableObject, Map<String, Object> arguments)
106    {
107        T ametysObject = (T) mentionableObject.ametysObject();
108        I18nizableText i18nSubject = _getMailSubject(mentionableObject);
109
110        I18nizableText i18nMessage = _getMailMessage(mentionableObject);
111        
112        for (UserIdentity mentionedUserIdentity : mentionableObject.mentionedUsers())
113        {
114            if (_canSendMailToMentionedUser(ametysObject, mentionableObject.author(), mentionedUserIdentity, arguments))
115            {
116                // Get mentioned user
117                User mentionedUser = _userManager.getUser(mentionedUserIdentity);
118                if (mentionedUser == null)
119                {
120                    getLogger().warn("Could not send a notification e-mail to user with login {}: there is no user with this login in population {}", mentionedUserIdentity.getLogin(), mentionedUserIdentity.getPopulationId());
121                    continue;
122                }
123                
124                if (StringUtils.isBlank(mentionedUser.getEmail()))
125                {
126                    getLogger().warn("Could not send a notification e-mail to user {}: this user has no email address", mentionedUser);
127                    continue;
128                }
129                
130                String language = StringUtils.defaultIfBlank(mentionedUser.getLanguage(), StringUtils.defaultIfBlank(mentionableObject.language(), _userLanguagesManager.getDefaultLanguage()));
131                String subject = _i18nUtils.translate(i18nSubject, language);
132                
133                String body = _createBody(mentionedUser, i18nMessage, mentionableObject, language);
134                _sendMail(mentionedUser, subject, body);
135            }
136        }
137    }
138    
139    /**
140     * create a mail body with arguments
141     * @param user the user
142     * @param message the message
143     * @param mentionableObject the object
144     * @param language the language to use
145     * @return the mail body
146     */
147    protected String _createBody(User user, I18nizableText message, MentionableObject mentionableObject, String language)
148    {
149        try
150        {
151            String contentWithReplacedMentions = _transformSyntaxTextToReadableTextWithColors(mentionableObject.content(), user.getIdentity());
152            MailBodyBuilder mailBuilder =  getStandardMailBodyHelper()
153                                                                 .withLanguage(mentionableObject.language())
154                                                                 .withTitle(_getMailTitle(mentionableObject))
155                                                                 .addMessage(message)
156                                                                 .withLink(mentionableObject.linkToAmetysObject().linkUrl(), mentionableObject.linkToAmetysObject().linkText());
157            User author = mentionableObject.author();
158            if (author != null)
159            {
160                UserInput userInput = new UserInput(author, mentionableObject.creationDate(), contentWithReplacedMentions);
161                mailBuilder.withUserInputs(List.of(userInput), _getMailMessageType());
162            }
163            else
164            {
165                String unknownAuthorName = _i18nUtils.translate(new I18nizableText("plugin.core", "PLUGINS_CORE_USERS_UNKNOWN_USER"));
166                UnauthenticatedUserInput userInput = new UnauthenticatedUserInput(unknownAuthorName, mentionableObject.creationDate(), contentWithReplacedMentions);
167                mailBuilder.withUnauthenticatedUserInputs(List.of(userInput), _getMailMessageType());
168            }
169            
170            return mailBuilder.withLanguage(language).build();
171        }
172        catch (IOException e)
173        {
174            getLogger().warn("Could not send a notification e-mail to " + user + ": an error occured while sending the email", e);
175        }
176        return null;
177    }
178
179    /**
180     * Get a standard mail body helper
181     * @return the standard mail body helper
182     */
183    protected MailBodyBuilder getStandardMailBodyHelper()
184    {
185        return StandardMailBodyHelper.newHTMLBody();
186    }
187    
188    /**
189     * Get the type of message to display before the message
190     * @return  the message type
191     */
192    protected abstract I18nizableText _getMailMessageType();
193
194    /**
195     * Get the title of the mail body
196     * @param mentionableObject the mentionable object
197     * @return the mail title
198     */
199    protected abstract I18nizableText _getMailTitle(MentionableObject mentionableObject);
200
201    /**
202     * Transform syntax text to readable text with colors according to whether the recipient is the one being tagged or not
203     * @param syntaxText the syntax text
204     * @param recipient the recipient
205     * @return the readable text
206     */
207    protected abstract String _transformSyntaxTextToReadableTextWithColors(String syntaxText, UserIdentity recipient);
208
209    /**
210     * <code>true</code> if we can send a mail to the mentioned user
211     * @param ametysObject the ametys object
212     * @param authorIdentity the author
213     * @param mentionedUserIdentity the mentioned user
214     * @param arguments the event arguments
215     * @return <code>true</code> if we can send a mail to the mentioned user
216     */
217    protected boolean _canSendMailToMentionedUser(T ametysObject, User authorIdentity, UserIdentity mentionedUserIdentity, Map<String, Object> arguments)
218    {
219        return authorIdentity != null && !mentionedUserIdentity.equals(authorIdentity.getIdentity());
220    }
221    
222    /**
223     * Retrieves the notification mail's subject
224     * @param mentionableObject the mentionable object
225     * @return the notification mail's subject
226     */
227    protected abstract I18nizableText _getMailSubject(MentionableObject mentionableObject);
228
229    /**
230     * Retrieves the notification mail's subject
231     * @param mentionableObject the mentionable object
232     * @return the notification mail's subject
233     */
234    protected abstract I18nizableText _getMailMessage(MentionableObject mentionableObject);
235    
236    /**
237     * Get all information of the mentionable object from the arguments
238     * @param arguments the arguments map
239     * @return the mentionable object
240     * @throws Exception if an error occurs.
241     */
242    protected abstract MentionableObject _getMentionableObjectFromArguments(Map<String, Object> arguments) throws Exception;
243
244    /**
245     * Send mail to the given user
246     * @param user the user
247     * @param subject the mail subject
248     * @param body the mail body
249     */
250    protected void _sendMail(User user, String subject, String body)
251    {
252        try
253        {
254            
255            if (StringUtils.isBlank(body))
256            {
257                getLogger().warn("Could not send a notification e-mail to {}: the email body is empty", user);
258            }
259            else
260            {
261                SendMailHelper.newMail()
262                    .withSubject(subject)
263                    .withHTMLBody(body)
264                    .withRecipient(user.getEmail())
265                    .sendMail();
266            }
267        }
268        catch (MessagingException | IOException e)
269        {
270            getLogger().warn("Could not send a notification e-mail to " + user + ": an error occured while sending the email", e);
271        }
272    }
273    
274    /**
275     * Link to the ametys object
276     * @param linkUrl the link to the ametys object
277     * @param linkText the text of the link to the ametys object
278     */
279    public record LinkToAmetysObject(String linkUrl, I18nizableText linkText) { /* empty */ }
280
281    
282    /**
283     * A record to get all information of the mentionable object
284     * @param content the content
285     * @param author the author
286     * @param mentionedUsers the mentioned users in the mentionable object
287     * @param creationDate the creation date
288     * @param ametysObject the ametys object holding the mentionable object
289     * @param linkToAmetysObject the link of the ametys object
290     * @param language the language
291     */
292    public record MentionableObject(User author, String content, Collection<UserIdentity> mentionedUsers, ZonedDateTime creationDate, AmetysObject ametysObject, LinkToAmetysObject linkToAmetysObject, String language) { /* empty */ }
293}