Skip to content

Commit

Permalink
Closes #288 [FEATURE] Send mails to moderators when a new report is s…
Browse files Browse the repository at this point in the history
…ubmitted via the report form
  • Loading branch information
bukajsytlos committed Dec 29, 2018
1 parent 0752e01 commit 9efafd8
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.faforever.api.moderationreport;

import com.faforever.api.AbstractIntegrationTest;
import com.faforever.api.config.FafApiProperties;
import com.faforever.api.data.DataController;
import com.faforever.api.email.EmailService;
import com.faforever.commons.api.dto.Game;
import com.faforever.commons.api.dto.ModerationReport;
import com.faforever.commons.api.dto.ModerationReportStatus;
import com.faforever.commons.api.dto.Player;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithUserDetails;
Expand All @@ -21,6 +25,9 @@
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
Expand All @@ -38,6 +45,11 @@
public class ModerationReportTest extends AbstractIntegrationTest {
private ModerationReport validModerationReport;
private Set<Player> reportedUsers;
@Autowired
private FafApiProperties properties;

@MockBean
private EmailService emailService;

@Override
@Before
Expand Down Expand Up @@ -85,6 +97,23 @@ public void userCanCreateValidModerationReport() throws Exception {
.andExpect(jsonPath("$.data.relationships.reportedUsers.data", hasSize(2)));
}

@Test
@WithUserDetails(AUTH_USER)
public void newModerationReportNotifiesModerators() throws Exception {
properties.getModerationReport()
.setNotificationEmailSubject("Moderation Report notification")
.setNotificationEmailBodyTemplate("New moderation report has been reported by {0}.\nDescription: {1}\nIncident code: {2}\nReported Users: {3}");
mockMvc.perform(
post("/data/moderationReport")
.header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE)
.content(createJsonApiContent(validModerationReport)))
.andExpect(status().isCreated());
verify(emailService, times(1)).sendMail(
eq(Sets.newHashSet("admin@faforever.com", "moderator@faforever.com")),
eq("Moderation Report notification"),
eq("New moderation report has been reported by null.\nDescription: Report description\nIncident code: Incident code\nReported Users: [ADMIN, MODERATOR]"));
}

@Test
@WithUserDetails(AUTH_USER)
public void userCannotCreateReportWithModeratorsData() throws Exception {
Expand Down Expand Up @@ -242,7 +271,7 @@ public void reporterCannotUpdateReportStatus() throws Exception {
@WithUserDetails(AUTH_MODERATOR)
public void moderatorCanUpdateReportStatus() throws Exception {
final ModerationReport updatedReportStatusModerationReport = (ModerationReport) new ModerationReport()
.setReportStatus(ModerationReportStatus.AWAITING)
.setReportStatus(ModerationReportStatus.COMPLETED)
.setId("1");
mockMvc.perform(
patch("/data/moderationReport/1")
Expand All @@ -251,7 +280,7 @@ public void moderatorCanUpdateReportStatus() throws Exception {
.andExpect(status().isNoContent());
mockMvc.perform(
get("/data/moderationReport/1"))
.andExpect(jsonPath("$.data.attributes.reportStatus", is(ModerationReportStatus.AWAITING.name())));
.andExpect(jsonPath("$.data.attributes.reportStatus", is(ModerationReportStatus.COMPLETED.name())));
}

@Test
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/faforever/api/config/FafApiProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class FafApiProperties {
private Anope anope = new Anope();
private Rating rating = new Rating();
private Tutorial tutorial = new Tutorial();
private ModerationReport moderationReport = new ModerationReport();

@Data
public static class OAuth2 {
Expand Down Expand Up @@ -250,4 +251,10 @@ public class Rating {
public static class Tutorial {
private String thumbnailUrlFormat;
}

@Data
public static class ModerationReport {
private String notificationEmailSubject = "";
private String notificationEmailBodyTemplate = "";
}
}
9 changes: 7 additions & 2 deletions src/main/java/com/faforever/api/config/elide/ElideConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import com.faforever.api.data.checks.permission.HasBanUpdate;
import com.faforever.api.data.checks.permission.HasLadder1v1Update;
import com.faforever.api.data.checks.permission.IsModerator;
import com.faforever.api.data.domain.ModerationReport;
import com.faforever.api.data.listeners.ModerationReportListener;
import com.faforever.api.security.ExtendedAuditLogger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.elide.Elide;
import com.yahoo.elide.ElideSettingsBuilder;
import com.yahoo.elide.annotation.OnCreatePostCommit;
import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.core.filter.dialect.CaseSensitivityStrategy;
import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect;
Expand All @@ -39,18 +42,20 @@ public class ElideConfig {
public static final String DEFAULT_CACHE_NAME = "Elide.defaultCache";

@Bean
public Elide elide(SpringHibernateDataStore springHibernateDataStore, ObjectMapper objectMapper, EntityDictionary entityDictionary, ExtendedAuditLogger extendedAuditLogger) {
public Elide elide(SpringHibernateDataStore springHibernateDataStore, ObjectMapper objectMapper, EntityDictionary entityDictionary, ExtendedAuditLogger extendedAuditLogger, ModerationReportListener moderationReportListener) {
RSQLFilterDialect rsqlFilterDialect = new RSQLFilterDialect(entityDictionary, new CaseSensitivityStrategy.UseColumnCollation());

registerAdditionalConverters();

return new Elide(new ElideSettingsBuilder(springHibernateDataStore)
Elide elide = new Elide(new ElideSettingsBuilder(springHibernateDataStore)
.withJsonApiMapper(new JsonApiMapper(entityDictionary, objectMapper))
.withAuditLogger(extendedAuditLogger)
.withEntityDictionary(entityDictionary)
.withJoinFilterDialect(rsqlFilterDialect)
.withSubqueryFilterDialect(rsqlFilterDialect)
.build());
entityDictionary.bindTrigger(ModerationReport.class, OnCreatePostCommit.class, moderationReportListener);
return elide;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.faforever.api.data.checks.IsInAwaitingState;
import com.faforever.api.data.checks.Prefab;
import com.faforever.api.data.checks.permission.IsModerator;
import com.faforever.api.data.listeners.ModerationReportListener;
import com.faforever.api.security.FafUserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yahoo.elide.annotation.Audit;
Expand All @@ -22,6 +23,7 @@
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
Expand All @@ -48,6 +50,7 @@
@CreatePermission(expression = Prefab.ALL)
@Audit(action = Action.CREATE, logStatement = "Moderation report `{0}` has been reported", logExpressions = "${moderationReport}")
@Audit(action = Action.UPDATE, logStatement = "Moderation report `{0}` has been updated", logExpressions = "${moderationReport}")
@EntityListeners(ModerationReportListener.class)
public class ModerationReport extends AbstractEntity implements OwnableEntity {
public static final String TYPE_NAME = "moderationReport";
private ModerationReportStatus reportStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.faforever.api.data.listeners;

import com.faforever.api.config.FafApiProperties;
import com.faforever.api.data.domain.LegacyAccessLevel;
import com.faforever.api.data.domain.LobbyGroup;
import com.faforever.api.data.domain.Login;
import com.faforever.api.data.domain.ModerationReport;
import com.faforever.api.email.EmailService;
import com.faforever.api.user.LobbyGroupRepository;
import com.google.common.collect.Sets;
import com.yahoo.elide.functions.LifeCycleHook;
import com.yahoo.elide.security.RequestScope;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Component
public class ModerationReportListener implements LifeCycleHook<ModerationReport> {
private FafApiProperties properties;
private EmailService emailService;
private LobbyGroupRepository lobbyGroupRepository;

@Inject
public ModerationReportListener(FafApiProperties properties, EmailService emailService, LobbyGroupRepository lobbyGroupRepository) {
this.properties = properties;
this.emailService = emailService;
this.lobbyGroupRepository = lobbyGroupRepository;
}

@Override
public void execute(ModerationReport moderationReport, RequestScope requestScope, Optional changes) {
final HashSet<LegacyAccessLevel> moderatorLevels = Sets.newHashSet(LegacyAccessLevel.ROLE_MODERATOR, LegacyAccessLevel.ROLE_ADMINISTRATOR);
final Set<String> moderatorEmailAddresses = lobbyGroupRepository.findAllByAccessLevelIn(moderatorLevels).stream()
.map(LobbyGroup::getUser)
.map(Login::getEmail)
.collect(Collectors.toSet());
emailService.sendMail(
moderatorEmailAddresses,
properties.getModerationReport().getNotificationEmailSubject(),
MessageFormat.format(properties.getModerationReport().getNotificationEmailBodyTemplate(),
moderationReport.getReporter().getLogin(),
moderationReport.getReportDescription(),
moderationReport.getGameIncidentTimecode(),
moderationReport.getReportedUsers().stream().map(Login::getLogin).collect(Collectors.toSet())
)
);
}
}
10 changes: 9 additions & 1 deletion src/main/java/com/faforever/api/email/EmailSender.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.faforever.api.email;

import com.google.common.collect.Sets;

import java.util.Set;

public interface EmailSender {
void sendMail(String fromEmail, String fromName, String toEmail, String subject, String content);
default void sendMail(String fromEmail, String fromName, String toEmail, String subject, String content) {
sendMail(fromEmail, fromName, Sets.newHashSet(toEmail), subject, content);
}

void sendMail(String fromEmail, String fromName, Set<String> toEmails, String subject, String content);
}
12 changes: 12 additions & 0 deletions src/main/java/com/faforever/api/email/EmailService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.text.MessageFormat;
import java.util.Set;
import java.util.regex.Pattern;

@Service
Expand Down Expand Up @@ -64,4 +65,15 @@ public void sendPasswordResetMail(String username, String email, String password
MessageFormat.format(passwordReset.getHtmlFormat(), username, passwordResetUrl)
);
}

@SneakyThrows
public void sendMail(Set<String> receiversEmails, String subject, String body) {
emailSender.sendMail(
properties.getMail().getFromEmailAddress(),
properties.getMail().getFromEmailName(),
receiversEmails,
subject,
body
);
}
}
6 changes: 4 additions & 2 deletions src/main/java/com/faforever/api/email/JavaEmailSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;

import java.util.Set;

// TODO we might want to use spring mail, but it would've taken too much time when I first looked at it
@Component
@ConditionalOnProperty("spring.mail.host")
Expand All @@ -18,11 +20,11 @@ public JavaEmailSender(JavaMailSender mailSender) {
}

@Override
public void sendMail(String fromEmail, String fromName, String toEmail, String subject, String content) {
public void sendMail(String fromEmail, String fromName, Set<String> toEmails, String subject, String content) {
MimeMessagePreparator messagePreparator = mimeMessage -> {
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
messageHelper.setFrom(fromEmail, fromName);
messageHelper.setTo(toEmail);
messageHelper.setTo(toEmails.toArray(new String[]{}));
messageHelper.setSubject(subject);
messageHelper.setText(content, true);
};
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/com/faforever/api/email/MandrillEmailSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Component
@Slf4j
Expand All @@ -22,19 +24,22 @@ public MandrillEmailSender(MandrillApi mandrillApi) {

@Override
@SneakyThrows
public void sendMail(String fromEmail, String fromName, String toEmail, String subject, String content) {
public void sendMail(String fromEmail, String fromName, Set<String> toEmails, String subject, String content) {
MandrillMessage message = new MandrillMessage();
message.setFromEmail(fromEmail);
message.setFromName(fromName);
message.setSubject(subject);
message.setHtml(content);
message.setAutoText(true);

Recipient recipient = new Recipient();
recipient.setEmail(toEmail);
message.setTo(Collections.singletonList(recipient));
final List<Recipient> recipients = toEmails.stream().map(toEmail -> {
final Recipient recipient = new Recipient();
recipient.setEmail(toEmail);
return recipient;
}).collect(Collectors.toList());
message.setTo(recipients);

log.debug("Sending activation email to: {}", toEmail);
log.debug("Sending activation email to: {}", toEmails);
mandrillApi.messages().send(message, false);
}
}
6 changes: 4 additions & 2 deletions src/main/java/com/faforever/api/email/NoopEmailSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
@Slf4j
@Conditional(MailSenderCondition.class)
public class NoopEmailSender implements EmailSender {
@Override
public void sendMail(String fromEmail, String fromName, String toEmail, String subject, String content) {
public void sendMail(String fromEmail, String fromName, Set<String> toEmails, String subject, String content) {
log.debug("Would send email from '{} <{}>' to '{}' with subject '{}' and text: {}",
fromName, fromEmail, toEmail, subject, content);
fromName, fromEmail, toEmails, subject, content);
}

static class MailSenderCondition extends NoneNestedConditions {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/faforever/api/user/LobbyGroupRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.faforever.api.user;

import com.faforever.api.data.domain.LegacyAccessLevel;
import com.faforever.api.data.domain.LobbyGroup;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.Set;

@Repository
public interface LobbyGroupRepository extends JpaRepository<LobbyGroup, Integer> {
Set<LobbyGroup> findAllByAccessLevelIn(Collection<LegacyAccessLevel> accessLevels);
}
10 changes: 10 additions & 0 deletions src/test/java/com/faforever/api/email/EmailServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.faforever.api.config.FafApiProperties.Registration;
import com.faforever.api.error.ApiExceptionWithCode;
import com.faforever.api.error.ErrorCode;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -13,6 +14,8 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.HashSet;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -85,4 +88,11 @@ public void sendPasswordResetMail() {

verify(emailSender).sendMail("foo@bar.com", "foobar", "junit@example.com", "Hello", "Hello junit, bla: http://example.com");
}

@Test
public void sendMail() {
final HashSet<String> receiversEmails = Sets.newHashSet("mail@example.com", "mail@test.com");
instance.sendMail(receiversEmails, "subject", "body");
verify(emailSender).sendMail("foo@bar.com", "foobar", receiversEmails, "subject", "body");
}
}

0 comments on commit 9efafd8

Please sign in to comment.