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);
097    }
098
099    /**
100     * Send mail to all mentioned users in the mentionable object
101     * @param mentionableObject the mentionable object
102     */
103    @SuppressWarnings("unchecked")
104    protected void _sendMailToMentionedUsers(MentionableObject mentionableObject)
105    {
106        T ametysObject = (T) mentionableObject.ametysObject();
107        I18nizableText i18nSubject = _getMailSubject(mentionableObject);
108
109        I18nizableText i18nMessage = _getMailMessage(mentionableObject);
110        
111        for (UserIdentity mentionedUserIdentity : mentionableObject.mentionedUsers())
112        {
113            if (_canSendMailToMentionedUser(ametysObject, mentionableObject.author(), mentionedUserIdentity))
114            {
115                // Get mentioned user
116                User mentionedUser = _userManager.getUser(mentionedUserIdentity);
117                if (mentionedUser == null)
118                {
119                    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());
120                    continue;
121                }
122                
123                if (StringUtils.isBlank(mentionedUser.getEmail()))
124                {
125                    getLogger().warn("Could not send a notification e-mail to user {}: this user has no email address", mentionedUser);
126                    continue;
127                }
128                
129                String language = StringUtils.defaultIfBlank(mentionedUser.getLanguage(), _userLanguagesManager.getDefaultLanguage());
130                String subject = _i18nUtils.translate(i18nSubject, language);
131                
132                String body = _createBody(mentionedUser, i18nMessage, mentionableObject, language);
133                _sendMail(mentionedUser, subject, body);
134            }
135        }
136    }
137    
138    /**
139     * create a mail body with arguments
140     * @param user the user
141     * @param message the message
142     * @param mentionableObject the object
143     * @param language the language to use
144     * @return the mail body
145     */
146    protected String _createBody(User user, I18nizableText message, MentionableObject mentionableObject, String language)
147    {
148        try
149        {
150            String contentWithReplacedMentions = _transformSyntaxTextToReadableTextWithColors(mentionableObject.content(), user.getIdentity());
151            MailBodyBuilder mailBuilder =  getStandardMailBodyHelper()
152                                                                 .withLanguage(mentionableObject.language())
153                                                                 .withTitle(_getMailTitle(mentionableObject))
154                                                                 .addMessage(message)
155                                                                 .withLink(mentionableObject.linkToAmetysObject().linkUrl(), mentionableObject.linkToAmetysObject().linkText());
156            User author = mentionableObject.author();
157            if (author != null)
158            {
159                UserInput userInput = new UserInput(author, mentionableObject.creationDate(), contentWithReplacedMentions);
160                mailBuilder.withUserInputs(List.of(userInput), _getMailMessageType());
161            }
162            else
163            {
164                String unknownAuthorName = _i18nUtils.translate(new I18nizableText("plugin.core", "PLUGINS_CORE_USERS_UNKNOWN_USER"));
165                UnauthenticatedUserInput userInput = new UnauthenticatedUserInput(unknownAuthorName, mentionableObject.creationDate(), contentWithReplacedMentions);
166                mailBuilder.withUnauthenticatedUserInputs(List.of(userInput), _getMailMessageType());
167            }
168            
169            return mailBuilder.withLanguage(language).build();
170        }
171        catch (IOException e)
172        {
173            getLogger().warn("Could not send a notification e-mail to " + user + ": an error occured while sending the email", e);
174        }
175        return null;
176    }
177
178    /**
179     * Get a standard mail body helper
180     * @return the standard mail body helper
181     */
182    protected MailBodyBuilder getStandardMailBodyHelper()
183    {
184        return StandardMailBodyHelper.newHTMLBody();
185    }
186    
187    /**
188     * Get the type of message to display before the message
189     * @return  the message type
190     */
191    protected abstract I18nizableText _getMailMessageType();
192
193    /**
194     * Get the title of the mail body
195     * @param mentionableObject the mentionable object
196     * @return the mail title
197     */
198    protected abstract I18nizableText _getMailTitle(MentionableObject mentionableObject);
199
200    /**
201     * Transform syntax text to readable text with colors according to whether the recipient is the one being tagged or not
202     * @param syntaxText the syntax text
203     * @param recipient the recipient
204     * @return the readable text
205     */
206    protected abstract String _transformSyntaxTextToReadableTextWithColors(String syntaxText, UserIdentity recipient);
207
208    /**
209     * <code>true</code> if we can send a mail to the mentioned user
210     * @param ametysObject the ametys object
211     * @param authorIdentity the author
212     * @param mentionedUserIdentity the mentioned user
213     * @return <code>true</code> if we can send a mail to the mentioned user
214     */
215    protected boolean _canSendMailToMentionedUser(T ametysObject, User authorIdentity, UserIdentity mentionedUserIdentity)
216    {
217        return authorIdentity != null && !mentionedUserIdentity.equals(authorIdentity.getIdentity());
218    }
219    
220    /**
221     * Retrieves the notification mail's subject
222     * @param mentionableObject the mentionable object
223     * @return the notification mail's subject
224     */
225    protected abstract I18nizableText _getMailSubject(MentionableObject mentionableObject);
226
227    /**
228     * Retrieves the notification mail's subject
229     * @param mentionableObject the mentionable object
230     * @return the notification mail's subject
231     */
232    protected abstract I18nizableText _getMailMessage(MentionableObject mentionableObject);
233    
234    /**
235     * Get all information of the mentionable object from the arguments
236     * @param arguments the arguments map
237     * @return the mentionable object
238     * @throws Exception if an error occurs.
239     */
240    protected abstract MentionableObject _getMentionableObjectFromArguments(Map<String, Object> arguments) throws Exception;
241
242    /**
243     * Send mail to the given user
244     * @param user the user
245     * @param subject the mail subject
246     * @param body the mail body
247     */
248    protected void _sendMail(User user, String subject, String body)
249    {
250        try
251        {
252            
253            if (StringUtils.isBlank(body))
254            {
255                getLogger().warn("Could not send a notification e-mail to {}: the email body is empty", user);
256            }
257            else
258            {
259                SendMailHelper.newMail()
260                    .withSubject(subject)
261                    .withHTMLBody(body)
262                    .withRecipient(user.getEmail())
263                    .sendMail();
264            }
265        }
266        catch (MessagingException | IOException e)
267        {
268            getLogger().warn("Could not send a notification e-mail to " + user + ": an error occured while sending the email", e);
269        }
270    }
271    
272    /**
273     * Link to the ametys object
274     * @param linkUrl the link to the ametys object
275     * @param linkText the text of the link to the ametys object
276     */
277    public record LinkToAmetysObject(String linkUrl, I18nizableText linkText) { /* empty */ }
278
279    
280    /**
281     * A record to get all information of the mentionable object
282     * @param content the content
283     * @param author the author
284     * @param mentionedUsers the mentioned users in the mentionable object
285     * @param creationDate the creation date
286     * @param ametysObject the ametys object holding the mentionable object
287     * @param linkToAmetysObject the link of the ametys object
288     * @param language the language
289     */
290    public record MentionableObject(User author, String content, Collection<UserIdentity> mentionedUsers, ZonedDateTime creationDate, AmetysObject ametysObject, LinkToAmetysObject linkToAmetysObject, String language) { /* empty */ }
291}