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