diff options
Diffstat (limited to 'src')
8 files changed, 296 insertions, 4 deletions
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java new file mode 100644 index 0000000..3d9a3fe --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java @@ -0,0 +1,29 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import eu.mulk.mulkcms2.benki.posts.Post; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import java.time.OffsetDateTime; +import java.util.Collection; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; + +@Entity +@Table(name = "newsletters", schema = "benki") +public class Newsletter extends PanacheEntityBase { + + @Id + @Column(name = "id", nullable = false) + public Integer id; + + @Column(name = "date", nullable = false) + public OffsetDateTime date = OffsetDateTime.now(); + + @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY) + @OrderBy("date") + public Collection<Post<?>> posts; +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java new file mode 100644 index 0000000..1f13c08 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java @@ -0,0 +1,117 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import static java.util.stream.Collectors.partitioningBy; +import static java.util.stream.Collectors.toList; + +import eu.mulk.mulkcms2.benki.bookmarks.Bookmark; +import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; +import eu.mulk.mulkcms2.benki.posts.Post; +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.panache.common.Sort; +import io.quarkus.qute.TemplateExtension; +import io.quarkus.qute.api.CheckedTemplate; +import io.quarkus.scheduler.Scheduled; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.annotation.CheckForNull; +import javax.enterprise.context.Dependent; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hibernate.Session; + +@Dependent +public class NewsletterSender { + + private static final DateTimeFormatter humanDateFormatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); + + @ConfigProperty(name = "mulkcms.newsletter.time-zone") + ZoneId newsletterTimeZone; + + @PersistenceContext EntityManager em; + + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance newsletter( + Newsletter newsletter, List<Bookmark> bookmarks, List<LazychatMessage> lazychatMessages); + } + + @Scheduled(cron = "0 0 0 ? * Mon") + @Transactional + void run() throws InterruptedException, TimeoutException, ExecutionException { + var session = em.unwrap(Session.class); + + List<Post<?>> posts = Post.list("newsletter IS NULL", Sort.ascending("date")); + Post.fetchTexts(posts); + + if (posts.isEmpty()) { + return; + } + + var postsByClass = posts.stream().collect(partitioningBy(Post::isBookmark)); + var bookmarks = + postsByClass.getOrDefault(Boolean.TRUE, List.of()).stream() + .map(x -> (Bookmark) x) + .collect(toList()); + var lazychatMessages = + postsByClass.getOrDefault(Boolean.FALSE, List.of()).stream() + .map(x -> (LazychatMessage) x) + .collect(toList()); + + var date = OffsetDateTime.now(newsletterTimeZone); + var newsletterNumber = + (int) + session + .createQuery("SELECT max(id) FROM Newsletter", Integer.class) + .uniqueResultOptional() + .map(x -> x + 1) + .orElse(1); + + var newsletter = new Newsletter(); + newsletter.id = newsletterNumber; + newsletter.date = date; + newsletter.persist(); + + posts.forEach(post -> post.newsletter = newsletter); + + var subscriberEmails = + NewsletterSubscription.<NewsletterSubscription>streamAll() + .map(x -> x.email) + .toArray(String[]::new); + + var mailText = Templates.newsletter(newsletter, bookmarks, lazychatMessages); + var sendJob = + mailText + .subject(String.format("MulkCMS newsletter #%d", newsletterNumber)) + .bcc(subscriberEmails) + .send(); + sendJob.toCompletableFuture().get(10000, TimeUnit.SECONDS); + } + + @TemplateExtension + @CheckForNull + static String humanDate(@CheckForNull LocalDate x) { + if (x == null) { + return null; + } + return humanDateFormatter.format(x); + } + + @TemplateExtension + @CheckForNull + static String humanDate(@CheckForNull OffsetDateTime x) { + if (x == null) { + return null; + } + return humanDateFormatter.format(x); + } +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java new file mode 100644 index 0000000..7aeda60 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java @@ -0,0 +1,29 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import java.time.OffsetDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.Email; +import org.hibernate.annotations.NaturalId; + +@Entity +@Table(name = "newsletter_subscriptions", schema = "benki") +public class NewsletterSubscription extends PanacheEntityBase { + + @Id + @Column(name = "id", nullable = false) + @GeneratedValue + public Integer id; + + @Column(name = "start_date", nullable = false) + public OffsetDateTime startDate = OffsetDateTime.now(); + + @NaturalId + @Column(name = "email", nullable = false) + @Email + public String email; +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java index fd023d7..346b71f 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java @@ -5,12 +5,14 @@ import static java.util.stream.Collectors.toList; import eu.mulk.mulkcms2.benki.accesscontrol.Role; import eu.mulk.mulkcms2.benki.bookmarks.Bookmark; import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; +import eu.mulk.mulkcms2.benki.newsletter.Newsletter; import eu.mulk.mulkcms2.benki.users.User; import eu.mulk.mulkcms2.benki.users.User_; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -70,6 +72,12 @@ public abstract class Post<Text extends PostText<?>> extends PanacheEntityBase { public OffsetDateTime date; @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "newsletter", referencedColumnName = "id", nullable = true) + @CheckForNull + @JsonbTransient + public Newsletter newsletter; + + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner", referencedColumnName = "id") @CheckForNull @JsonbTransient @@ -340,18 +348,22 @@ public abstract class Post<Text extends PostText<?>> extends PanacheEntityBase { } // Fetch texts (to avoid n+1 selects). - var postIds = forwardResults.stream().map(x -> x.id).collect(toList()); + fetchTexts(forwardResults); + + return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults); + } + + public static <T extends Post<?>> void fetchTexts(Collection<T> posts) { + var postIds = posts.stream().map(x -> x.id).collect(toList()); if (!postIds.isEmpty()) { find("SELECT p FROM Post p LEFT JOIN FETCH p.texts WHERE p.id IN (?1)", postIds).stream() .count(); } - - return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults); } @CheckForNull - protected Text getText() { + public Text getText() { var texts = getTexts(); if (texts.isEmpty()) { return null; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d56d21..b7c54da 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,6 +9,7 @@ quarkus.log.level = INFO mulkcms.tag-base = hub.benkard.de mulkcms.posts.default-max-results = 25 +mulkcms.newsletter.time-zone = Europe/Vienna quarkus.datasource.db-kind = postgresql quarkus.datasource.jdbc.driver = org.postgresql.Driver @@ -61,6 +62,13 @@ mulkcms.jwt.signing-key = MulkCMS-IdP mulkcms.jwt.issuer = https://matthias.benkard.de mulkcms.jwt.validity = P1D +# E-mail settings +quarkus.mailer.from = mulkcms@benkard.de +quarkus.mailer.host = mail.benkard.de +quarkus.mailer.port = 587 +quarkus.mailer.start-tls = REQUIRED +quarkus.mailer.username = mulkcms@benkard.de + # Deployment docker.registry = docker.benkard.de @@ -89,6 +97,9 @@ kubernetes.env-vars[0].value = database-password kubernetes.env-vars[1].name = QUARKUS_OIDC_CREDENTIALS_SECRET kubernetes.env-vars[1].secret = mulkcms2-secrets kubernetes.env-vars[1].value = keycloak-secret +kubernetes.env-vars[2].name = QUARKUS_MAILER_PASSWORD +kubernetes.env-vars[2].secret = mulkcms2-secrets +kubernetes.env-vars[2].value = email-password kubernetes.secret-volumes[0].volume-name = secrets kubernetes.secret-volumes[0].secret-name = mulkcms2-secrets kubernetes.secret-volumes[0].default-mode = 0444 diff --git a/src/main/resources/db/changeLog-1.7.xml b/src/main/resources/db/changeLog-1.7.xml new file mode 100644 index 0000000..8824115 --- /dev/null +++ b/src/main/resources/db/changeLog-1.7.xml @@ -0,0 +1,55 @@ +<?xml version="1.1" encoding="UTF-8" standalone="no"?> +<databaseChangeLog + xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + http://www.liquibase.org/xml/ns/dbchangelog + http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.10.xsd"> + + <changeSet author="mulk" id="1.7-1"> + <createTable tableName="newsletters" schemaName="benki"> + <column name="id" type="INTEGER"> + <constraints primaryKeyName="newsletters_pkey" nullable="false" primaryKey="true"/> + </column> + + <column name="date" type="TIMESTAMP WITH TIME ZONE"> + <constraints nullable="false"/> + </column> + </createTable> + + <addColumn tableName="bookmarks" schemaName="benki"> + <column name="newsletter" type="INTEGER"> + <constraints foreignKeyName="bookmarks_newsletter_fkey" references="benki.newsletters(id)"/> + </column> + </addColumn> + + <addColumn tableName="lazychat_messages" schemaName="benki"> + <column name="newsletter" type="INTEGER"> + <constraints foreignKeyName="lazychat_messages_newsletter_fkey" references="benki.newsletters(id)"/> + </column> + </addColumn> + + <addColumn tableName="posts" schemaName="benki"> + <column name="newsletter" type="INTEGER"> + <constraints foreignKeyName="posts_newsletter_fkey" references="benki.newsletters(id)"/> + </column> + </addColumn> + </changeSet> + + <changeSet author="mulk" id="1.7-2"> + <createTable tableName="newsletter_subscriptions" schemaName="benki"> + <column name="id" type="SERIAL" autoIncrement="true"> + <constraints primaryKeyName="newsletter_subscriptions_pkey" nullable="false" primaryKey="true"/> + </column> + + <column name="start_date" type="TIMESTAMP WITH TIME ZONE" defaultValue="now()"> + <constraints nullable="false"/> + </column> + + <column name="email" type="VARCHAR"> + <constraints nullable="false" unique="true" uniqueConstraintName="newsletter_subscriptions_email_key"/> + </column> + </createTable> + </changeSet> + +</databaseChangeLog> diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml index f1c0849..7b4b700 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -13,5 +13,6 @@ <include file="db/changeLog-1.4.xml"/> <include file="db/changeLog-1.5.xml"/> <include file="db/changeLog-1.6.xml"/> + <include file="db/changeLog-1.7.xml"/> </databaseChangeLog> diff --git a/src/main/resources/templates/NewsletterSender/newsletter.txt b/src/main/resources/templates/NewsletterSender/newsletter.txt new file mode 100644 index 0000000..1c4228f --- /dev/null +++ b/src/main/resources/templates/NewsletterSender/newsletter.txt @@ -0,0 +1,38 @@ +{@int newsletterNumber} +{@java.time.LocalDate date} +{@java.util.List<eu.mulk.mulkcms2.benki.bookmarks.Bookmark> bookmarks} +{@java.util.List<eu.mulk.mulkcms2.benki.lazychat.LazychatMessage> lazychatMessages} +{@java.lang.String unsubscribeUri} +New Blog Posts +============== + +{#for post in lazychatMessages} +* {post.date.humanDate} <https://matthias.benkard.de/posts/{post.id}> +{post.text.content} + + +{/for} + + +New Bookmarks +============= + +{#for post in bookmarks} +* {post.date.humanDate} <https://matthias.benkard.de/posts/{post.id}> +* {post.title} +* <{post.uri}> + +{post.text.description} + + +{/for} + + + +Your Subscription +================= + +You are receiving this email because you are subscribed to the MulkCMS +newsletter. To unsubscribe, send an email to: + + <mulkcms+unsubscribe@benkard.de> |