From b9f74d1f0ff087e08388b0d3339bc6b741991791 Mon Sep 17 00:00:00 2001 From: f43nd1r Date: Wed, 20 Feb 2019 01:02:05 +0100 Subject: [PATCH] implement weekly report sending --- build.gradle | 1 + .../com/faendir/acra/BackendApplication.java | 4 +- .../com/faendir/acra/model/MailSettings.java | 39 +++------ .../com/faendir/acra/model/Stacktrace.java | 1 + .../java/com/faendir/acra/model/User.java | 10 +++ .../com/faendir/acra/service/MailService.java | 82 ++++++++++++------- .../db/changelog/db.changelog-master.yaml | 70 ++++++++++++++++ .../com/faendir/acra/messages_de.properties | 5 +- .../com/faendir/acra/messages_en.properties | 5 +- 9 files changed, 155 insertions(+), 62 deletions(-) diff --git a/build.gradle b/build.gradle index 694741a..64de84f 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'mysql:mysql-connector-java' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.liquibase:liquibase-core' implementation 'org.yaml:snakeyaml' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' diff --git a/src/main/java/com/faendir/acra/BackendApplication.java b/src/main/java/com/faendir/acra/BackendApplication.java index b10abb6..00c484c 100644 --- a/src/main/java/com/faendir/acra/BackendApplication.java +++ b/src/main/java/com/faendir/acra/BackendApplication.java @@ -19,19 +19,21 @@ package com.faendir.acra; import com.faendir.acra.config.AcraConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.PropertySource; import org.springframework.lang.NonNull; -import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(exclude = {SecurityAutoConfiguration.class}) @PropertySource("classpath:default.properties") @PropertySource(value = "file:${user.home}/.config/acrarium/application.properties", ignoreResourceNotFound = true) @PropertySource(value = "file:${user.home}/.acra/application.properties", ignoreResourceNotFound = true) @EnableConfigurationProperties(AcraConfiguration.class) +@Import(MailSenderAutoConfiguration.class) public class BackendApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(BackendApplication.class, args); diff --git a/src/main/java/com/faendir/acra/model/MailSettings.java b/src/main/java/com/faendir/acra/model/MailSettings.java index 8830b60..c5c348b 100644 --- a/src/main/java/com/faendir/acra/model/MailSettings.java +++ b/src/main/java/com/faendir/acra/model/MailSettings.java @@ -23,10 +23,9 @@ import org.springframework.data.annotation.PersistenceConstructor; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.IdClass; +import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import java.io.Serializable; -import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalUnit; import java.util.Objects; /** @@ -43,11 +42,12 @@ public class MailSettings { @Id @ManyToOne @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "username") private User user; - private SendMode sendMode; - private boolean all; private boolean newBug; private boolean regression; + private boolean spike; + private boolean summary; @PersistenceConstructor MailSettings() { @@ -61,37 +61,20 @@ public class MailSettings { return user; } - public SendMode getSendMode() { - return sendMode; - } - - public boolean isAll() { - return all; - } - - public boolean isNewBug() { + public boolean getNewBug() { return newBug; } - public boolean isRegression() { + public boolean getRegression() { return regression; } - public enum SendMode { - OFF(null), - INSTANT(null), - HOURLY(ChronoUnit.HOURS), - DAILY(ChronoUnit.DAYS), - WEEKLY(ChronoUnit.WEEKS); - private final TemporalUnit unit; + public boolean getSpike() { + return spike; + } - SendMode(TemporalUnit unit) { - this.unit = unit; - } - - public TemporalUnit getUnit() { - return unit; - } + public boolean getSummary() { + return summary; } static class ID implements Serializable { diff --git a/src/main/java/com/faendir/acra/model/Stacktrace.java b/src/main/java/com/faendir/acra/model/Stacktrace.java index 3a59f8e..c3a8f6a 100644 --- a/src/main/java/com/faendir/acra/model/Stacktrace.java +++ b/src/main/java/com/faendir/acra/model/Stacktrace.java @@ -49,6 +49,7 @@ public class Stacktrace { private Bug bug; @Type(type = "text") private String stacktrace; @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, optional = false, fetch = FetchType.EAGER) + @OnDelete(action = OnDeleteAction.CASCADE) private Version version; @PersistenceConstructor diff --git a/src/main/java/com/faendir/acra/model/User.java b/src/main/java/com/faendir/acra/model/User.java index 0b07673..96d223b 100644 --- a/src/main/java/com/faendir/acra/model/User.java +++ b/src/main/java/com/faendir/acra/model/User.java @@ -48,6 +48,7 @@ public class User implements UserDetails { @ElementCollection(fetch = FetchType.EAGER) private Set permissions; private String password; + private String mail; @PersistenceConstructor User() { @@ -112,6 +113,15 @@ public class User implements UserDetails { return true; } + public String getMail() { + return mail; + } + + public void setMail(String mail) { + this.mail = mail; + } + + public enum Role implements GrantedAuthority { ADMIN, USER, diff --git a/src/main/java/com/faendir/acra/service/MailService.java b/src/main/java/com/faendir/acra/service/MailService.java index a164d92..f6b4dbc 100644 --- a/src/main/java/com/faendir/acra/service/MailService.java +++ b/src/main/java/com/faendir/acra/service/MailService.java @@ -16,29 +16,42 @@ package com.faendir.acra.service; +import com.faendir.acra.i18n.Messages; import com.faendir.acra.model.App; import com.faendir.acra.model.Bug; import com.faendir.acra.model.MailSettings; -import com.faendir.acra.model.Report; -import com.faendir.acra.model.Stacktrace; -import com.faendir.acra.model.Version; +import com.faendir.acra.model.QBug; +import com.faendir.acra.model.User; +import com.faendir.acra.ui.view.bug.tabs.ReportTab; +import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQuery; +import com.vaadin.flow.i18n.I18NProvider; +import com.vaadin.flow.router.RouteConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.event.EventListener; import org.springframework.lang.NonNull; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; import javax.persistence.EntityManager; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; +import static com.faendir.acra.model.QBug.bug; import static com.faendir.acra.model.QMailSettings.mailSettings; import static com.faendir.acra.model.QReport.report; +import static com.faendir.acra.model.QStacktrace.stacktrace1; /** * @author lukas @@ -47,48 +60,55 @@ import static com.faendir.acra.model.QReport.report; @Service @EnableScheduling @EnableAsync +@ConditionalOnProperty(prefix = "spring.mail", name = "host") public class MailService { private final EntityManager entityManager; + @NonNull + private final I18NProvider i18nProvider; + @NonNull + private final JavaMailSender mailSender; - public MailService(@NonNull EntityManager entityManager) { + public MailService(@NonNull EntityManager entityManager, @NonNull I18NProvider i18nProvider, @NonNull JavaMailSender mailSender) { this.entityManager = entityManager; + this.i18nProvider = i18nProvider; + this.mailSender = mailSender; } @EventListener @Async public void onNewReport(NewReportEvent event) { - - } - - @Scheduled(cron = "0 0 * * * *") - public void checkHourly() { - check(MailSettings.SendMode.HOURLY); - } - - @Scheduled(cron = "0 0 0 * * *") - public void checkDaily() { - check(MailSettings.SendMode.DAILY); } @Scheduled(cron = "0 0 0 * * SUN") - public void checkWeekly() { - check(MailSettings.SendMode.WEEKLY); - } - - private void check(MailSettings.SendMode sendMode) { - List settings = new JPAQuery<>(entityManager).select(mailSettings).from(mailSettings).where(mailSettings.sendMode.eq(sendMode)).fetch(); - Map> appMap = settings.stream().collect(Collectors.groupingBy(MailSettings::getApp)); - for (Map.Entry> appEntry : appMap.entrySet()) { - List reports = new JPAQuery<>(entityManager).select(report).where(report.date.after(ZonedDateTime.now().minus(1, sendMode.getUnit())).and(report.stacktrace.bug.app.eq(appEntry.getKey()))).fetch(); - if(!reports.isEmpty()) { - Map> bugMap = reports.stream().collect(Collectors.groupingBy(r -> r.getStacktrace().getBug())); - for (Map.Entry> bugEntry : bugMap.entrySet()) { - boolean newBug = new JPAQuery<>().select(report).where(report.stacktrace.bug.eq(bugEntry.getKey()).and(report.notIn(bugEntry.getValue()))).fetchFirst() == null; - if (!newBug && bugEntry.getKey().getSolvedVersion() != null) { - int maxNewVersion = reports.stream().map(Report::getStacktrace).map(Stacktrace::getVersion).mapToInt(Version::getCode).max().orElseThrow(IllegalStateException::new); - boolean regression = new JPAQuery<>().select(report).where(report.stacktrace.bug.eq(bugEntry.getKey()).and(report.notIn(bugEntry.getValue())).and(report.stacktrace.version.code.goe(maxNewVersion))).fetchFirst() == null; + public void weeklyReport() { + Map> settings = new JPAQuery<>(entityManager).select(mailSettings).from(mailSettings).where(mailSettings.summary.isTrue()).fetch() + .stream() + .collect(Collectors.groupingBy(MailSettings::getApp, Collectors.toList())); + RouteConfiguration configuration = RouteConfiguration.forApplicationScope(); + for (Map.Entry> entry : settings.entrySet()) { + List tuples = new JPAQuery<>(entityManager).from(bug).join(stacktrace1).on(bug.eq(stacktrace1.bug)).join(report).on(stacktrace1.eq(report.stacktrace)).where(bug.app.eq(entry.getKey()).and(report.date.after(ZonedDateTime.now().minus(1, ChronoUnit.WEEKS)))) + .groupBy(bug) + .select(bug, report.count(), report.installationId.countDistinct()) + .fetch(); + String body = tuples.stream().map(tuple -> { + Bug bug = tuple.get(QBug.bug); + return i18nProvider.getTranslation(Messages.WEEKLY_MAIL_BUG_TEMPLATE, Locale.ENGLISH, configuration.getUrl(ReportTab.class, bug.getId()), bug.getTitle(), tuple.get(report.count()), tuple.get(report.installationId.countDistinct())); + }).collect(Collectors.joining("\n")); + if (body.isEmpty()) body = i18nProvider.getTranslation(Messages.WEEKLY_MAIL_NO_REPORTS, Locale.ENGLISH); + try { + MimeMessage template = mailSender.createMimeMessage(); + template.setContent(body, "text/html"); + template.setSubject(i18nProvider.getTranslation(Messages.WEEKLY_MAIL_SUBJECT, Locale.ENGLISH, entry.getKey().getName())); + for (MailSettings s : entry.getValue()) { + User user = s.getUser(); + if (user.getMail() != null) { + MimeMessage message = new MimeMessage(template); + message.setRecipients(Message.RecipientType.TO, user.getMail()); + mailSender.send(message); } } + } catch (MessagingException e) { + e.printStackTrace(); } } } diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 8bc8b2f..9e566d4 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -727,3 +727,73 @@ databaseChangeLog: columnName: solved - dropTable: tableName: proguard_mapping + - changeSet: + id: 2019-02-19-mail + author: lukas + changes: + - createTable: + tableName: mail_settings + columns: + - column: + name: app_id + type: INT + constraints: + nullable: false + referencedTableName: app + referencedColumnNames: id + foreignKeyName: FK_mail_app + deferrable: false + initiallyDeffered: false + - column: + name: username + type: VARCHAR(255) + constraints: + nullable: false + referencedTableName: user + referencedColumnNames: username + foreignKeyName: FK_mail_user + deferrable: false + initiallyDeffered: false + - column: + name: new_bug + type: BOOLEAN + constraints: + nullable: false + deferrable: false + initiallyDeffered: false + - column: + name: regression + type: BOOLEAN + constraints: + nullable: false + deferrable: false + initiallyDeffered: false + - column: + name: spike + type: BOOLEAN + constraints: + nullable: false + deferrable: false + initiallyDeffered: false + - column: + name: summary + type: BOOLEAN + constraints: + nullable: false + deferrable: false + initiallyDeffered: false + - addPrimaryKey: + tableName: mail_settings + columnNames: app_id, username + constraintName: PK_mail + - addColumn: + tableName: user + columns: + - column: + name: mail + type: VARCHAR(255) + constraints: + nullable: true + deferrable: false + initiallyDeffered: false + diff --git a/src/main/resources/i18n/com/faendir/acra/messages_de.properties b/src/main/resources/i18n/com/faendir/acra/messages_de.properties index a51180f..40178ab 100644 --- a/src/main/resources/i18n/com/faendir/acra/messages_de.properties +++ b/src/main/resources/i18n/com/faendir/acra/messages_de.properties @@ -133,4 +133,7 @@ api=API-Zugriff about=Info overview=Übersicht versions=Versionen -newVersion=Neue Version \ No newline at end of file +newVersion=Neue Version +weeklyMailSubject=Wöchentliche Zusammenfassung für %s +weeklyMailBugTemplate=- %s: %d Berichte von %d Nutzern +weeklyMailNoReports=Glückwunsch, es wurden keine Bugs gemeldet! \ No newline at end of file diff --git a/src/main/resources/i18n/com/faendir/acra/messages_en.properties b/src/main/resources/i18n/com/faendir/acra/messages_en.properties index 1b49e50..de3d63e 100644 --- a/src/main/resources/i18n/com/faendir/acra/messages_en.properties +++ b/src/main/resources/i18n/com/faendir/acra/messages_en.properties @@ -133,4 +133,7 @@ api=API Access about=About overview=Overview versions=Versions -newVersion=New Version \ No newline at end of file +newVersion=New Version +weeklyMailBugTemplate=- %s: %d reports by %d users +weeklyMailSubject=Weekly summary for %s +weeklyMailNoReports=Congratulations, there were no bugs reported! \ No newline at end of file