mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implemented types for notification templates (currently only MJML is used, but it's now easily expandable)
Added default templates
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 220 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -70,6 +70,7 @@ dependencies {
|
||||
|
||||
// Notifications
|
||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||
implementation("ch.digitalfondue.mjml4j:mjml4j:1.0.3")
|
||||
|
||||
// Development
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Link,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
@@ -19,6 +20,7 @@ import {MessageTemplateEndpoint, NotificationEndpoint} from "Frontend/generated/
|
||||
import {toast} from "sonner";
|
||||
import {Pencil} from "@phosphor-icons/react";
|
||||
import MessageTemplateDto from "Frontend/generated/de/grimsi/gameyfin/notifications/templates/MessageTemplateDto";
|
||||
import TemplateType from "Frontend/generated/de/grimsi/gameyfin/notifications/templates/TemplateType";
|
||||
|
||||
function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
|
||||
|
||||
@@ -26,6 +28,7 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
|
||||
const [availableTemplates, setAvailableTemplates] = useState<MessageTemplateDto[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<MessageTemplateDto | null>(null);
|
||||
const [templateContent, setTemplateContent] = useState<string>("");
|
||||
const [defaultPlaceholders, setDefaultPlaceholders] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
MessageTemplateEndpoint.getAll().then((response: any) => {
|
||||
@@ -53,7 +56,9 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
|
||||
async function openModal(template: MessageTemplateDto) {
|
||||
setSelectedTemplate(template);
|
||||
|
||||
let templateContent = await MessageTemplateEndpoint.read(template.key);
|
||||
let templateContent = await MessageTemplateEndpoint.read(template.key, TemplateType.MJML);
|
||||
let defaultPlaceholders = await MessageTemplateEndpoint.getDefaultPlaceholders(TemplateType.MJML);
|
||||
setDefaultPlaceholders(defaultPlaceholders ? defaultPlaceholders as string[] : []);
|
||||
|
||||
if (templateContent === undefined) {
|
||||
toast.error("Can't read template content");
|
||||
@@ -65,7 +70,7 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
|
||||
}
|
||||
|
||||
async function saveTemplate(template: MessageTemplateDto) {
|
||||
await MessageTemplateEndpoint.save(template.key, templateContent);
|
||||
await MessageTemplateEndpoint.save(template.key, TemplateType.MJML, templateContent);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -119,14 +124,40 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
|
||||
<ModalHeader
|
||||
className="flex flex-col gap-1">Edit {selectedTemplate?.name} Template</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-row gap-2">
|
||||
<p>Available placeholders:</p>
|
||||
{selectedTemplate?.availablePlaceholders?.map((placeholder) =>
|
||||
<Chip radius="sm"
|
||||
key={placeholder}
|
||||
color={templateContent.includes(`{${placeholder as string}}`) ? "success" : "danger"}
|
||||
>{placeholder}</Chip>
|
||||
)}
|
||||
<div className="flex flex-row justify-between items-end">
|
||||
<table cellPadding="4rem">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Required placeholders:</td>
|
||||
<td>
|
||||
<div className="flex flex-row gap-2">
|
||||
{selectedTemplate?.availablePlaceholders?.map((placeholder) =>
|
||||
<Chip radius="sm"
|
||||
key={placeholder}
|
||||
color={templateContent.includes(`{${placeholder as string}}`) ? "success" : "danger"}
|
||||
>{placeholder}</Chip>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Optional placeholders:</td>
|
||||
<td>
|
||||
<div className="flex flex-row gap-2">
|
||||
{defaultPlaceholders.map((placeholder) =>
|
||||
<Chip radius="sm"
|
||||
key={placeholder}
|
||||
color={templateContent.includes(`{${placeholder as string}}`) ? "success" : "default"}
|
||||
>{placeholder}</Chip>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<small className="text-right">Powered by <Link href="https://documentation.mjml.io/"
|
||||
target="_blank">mjml.io</Link>
|
||||
</small>
|
||||
</div>
|
||||
<Textarea
|
||||
size="lg"
|
||||
|
||||
@@ -16,9 +16,7 @@ import reactor.core.publisher.Sinks
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Service
|
||||
class LogService(
|
||||
@@ -26,16 +24,16 @@ class LogService(
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val LOG_CONFIG_TEMPLATE = "log-config-template.xml"
|
||||
private const val LOG_CONFIG_TEMPLATE = "templates/log-config-template.xml"
|
||||
private const val LOG_FILE_NAME = "gameyfin"
|
||||
private val LOG_REFRESH_INTERVAL = 5.seconds
|
||||
private val LOG_STREAM_RETENTION = 1.days
|
||||
private const val LOG_STREAM_RETENTION = 1000
|
||||
}
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
private var logFilePath: Path? = null
|
||||
private val sink: Sinks.Many<String> = Sinks.many().replay().limit(LOG_STREAM_RETENTION.toJavaDuration())
|
||||
private val sink: Sinks.Many<String> = Sinks.many().replay().limit(LOG_STREAM_RETENTION)
|
||||
private var tailer: AsyncFileTailer? = null
|
||||
|
||||
@EventListener(ApplicationStartedEvent::class)
|
||||
|
||||
@@ -35,8 +35,16 @@ class NotificationService(
|
||||
?: throw IllegalArgumentException("Provider '$provider' not found")
|
||||
}
|
||||
|
||||
fun sendNotification(recipient: String, title: String, message: String) {
|
||||
providers.filter { it.enabled }.forEach { it.sendNotification(recipient, title, message) }
|
||||
fun sendNotification(
|
||||
recipient: String,
|
||||
title: String,
|
||||
template: MessageTemplates,
|
||||
placeholders: Map<String, String>
|
||||
) {
|
||||
providers.filter { it.enabled }.forEach {
|
||||
val content = templateService.fillMessageTemplate(template, it.supportedTemplateType, placeholders)
|
||||
it.sendNotification(recipient, title, content)
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
@@ -52,15 +60,11 @@ class NotificationService(
|
||||
|
||||
val token = event.token
|
||||
val resetLink = event.baseUrl + "/reset-password?token=${token.token}"
|
||||
val content = templateService.fillMessageTemplate(
|
||||
MessageTemplates.PasswordResetRequest,
|
||||
mapOf("username" to token.user.username, "resetLink" to resetLink)
|
||||
)
|
||||
|
||||
sendNotification(
|
||||
token.user.email,
|
||||
"[Gameyfin] Password Reset Request",
|
||||
content
|
||||
MessageTemplates.PasswordResetRequest,
|
||||
mapOf("username" to token.user.username, "resetLink" to resetLink)
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
@@ -1,10 +1,12 @@
|
||||
package de.grimsi.gameyfin.notifications.providers
|
||||
|
||||
import de.grimsi.gameyfin.config.ConfigService
|
||||
import de.grimsi.gameyfin.notifications.templates.TemplateType
|
||||
import java.util.*
|
||||
|
||||
abstract class AbstractNotificationProvider(
|
||||
val providerKey: String,
|
||||
val supportedTemplateType: TemplateType,
|
||||
protected val config: ConfigService
|
||||
) {
|
||||
protected companion object {
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.notifications.providers
|
||||
|
||||
import de.grimsi.gameyfin.config.ConfigProperties
|
||||
import de.grimsi.gameyfin.config.ConfigService
|
||||
import de.grimsi.gameyfin.notifications.templates.TemplateType
|
||||
import jakarta.mail.Message
|
||||
import jakarta.mail.MessagingException
|
||||
import jakarta.mail.Session
|
||||
@@ -13,7 +14,7 @@ import java.util.*
|
||||
@Service
|
||||
class EmailNotificationProvider(
|
||||
config: ConfigService
|
||||
) : AbstractNotificationProvider("email", config) {
|
||||
) : AbstractNotificationProvider("email", TemplateType.MJML, config) {
|
||||
|
||||
private val storedCredentials: Properties
|
||||
get() {
|
||||
@@ -63,7 +64,7 @@ class EmailNotificationProvider(
|
||||
mimeMessage.setFrom(InternetAddress(credentials["username"] as String))
|
||||
mimeMessage.setRecipients(Message.RecipientType.TO, recipient)
|
||||
mimeMessage.subject = title
|
||||
mimeMessage.setText(message)
|
||||
mimeMessage.setContent(message, "text/html; charset=utf-8")
|
||||
|
||||
val transport = session.getTransport("smtp")
|
||||
transport.connect(
|
||||
|
||||
+8
-4
@@ -17,11 +17,15 @@ class MessageTemplateEndpoint(
|
||||
return messageTemplateService.getMessageTemplate(key)
|
||||
}
|
||||
|
||||
fun read(key: String): String {
|
||||
return messageTemplateService.getMessageTemplateContent(key)
|
||||
fun getDefaultPlaceholders(type: TemplateType): Set<String> {
|
||||
return messageTemplateService.getDefaultTemplatePlaceholders(type).keys
|
||||
}
|
||||
|
||||
fun save(key: String, content: String) {
|
||||
messageTemplateService.setMessageTemplateContent(key, content)
|
||||
fun read(key: String, templateType: TemplateType): String {
|
||||
return messageTemplateService.getMessageTemplateContent(key, templateType)
|
||||
}
|
||||
|
||||
fun save(key: String, templateType: TemplateType, content: String) {
|
||||
messageTemplateService.setMessageTemplateContent(key, templateType, content)
|
||||
}
|
||||
}
|
||||
+48
-12
@@ -1,9 +1,11 @@
|
||||
package de.grimsi.gameyfin.notifications.templates
|
||||
|
||||
import ch.digitalfondue.mjml4j.Mjml4j
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
@@ -12,7 +14,7 @@ class MessageTemplateService {
|
||||
|
||||
companion object {
|
||||
private const val TEMPLATE_PATH = "templates"
|
||||
private const val TEMPLATE_EXTENSION = "html"
|
||||
private const val DEFAULT_TEMPLATE_PATH = "templates/notifications"
|
||||
}
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
@@ -32,33 +34,67 @@ class MessageTemplateService {
|
||||
?: throw IllegalArgumentException("Message template with key '$key' not found")
|
||||
}
|
||||
|
||||
fun getMessageTemplateContent(key: String): String {
|
||||
log.info { "Reading message template content for key '$key'" }
|
||||
return getOrCreateTemplateFile(key).readText()
|
||||
fun getMessageTemplateContent(key: String, type: TemplateType): String {
|
||||
log.info { "Reading message template content for '$key.${type.extension}'" }
|
||||
return getTemplateFile(key, type).readText()
|
||||
}
|
||||
|
||||
fun fillMessageTemplate(template: MessageTemplates, placeholders: Map<String, String>): String {
|
||||
fun fillMessageTemplate(template: MessageTemplates, type: TemplateType, placeholders: Map<String, String>): String {
|
||||
if (placeholders.keys != template.availablePlaceholders.toSet()) {
|
||||
throw IllegalArgumentException("Placeholders do not match available placeholders for template '${template.key}'")
|
||||
}
|
||||
|
||||
val content = getMessageTemplateContent(template.key)
|
||||
return placeholders.entries.fold(content) { acc, (placeholder, value) ->
|
||||
acc.replace("{$placeholder}", value)
|
||||
val content = getMessageTemplateContent(template.key, type)
|
||||
|
||||
return when (type) {
|
||||
TemplateType.TEXT -> fillTextTemplate(content, placeholders)
|
||||
TemplateType.MJML -> fillMjmlTemplate(content, placeholders)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMessageTemplateContent(key: String, content: String) {
|
||||
fun setMessageTemplateContent(key: String, type: TemplateType, content: String) {
|
||||
log.info { "Saving message template content for key '$key'" }
|
||||
getOrCreateTemplateFile(key).writeText(content)
|
||||
getOrCreateTemplateFile(key, type).writeText(content)
|
||||
}
|
||||
|
||||
private fun getOrCreateTemplateFile(key: String): Path {
|
||||
val path = Path.of("./", TEMPLATE_PATH, "$key.$TEMPLATE_EXTENSION")
|
||||
fun getDefaultTemplatePlaceholders(templateType: TemplateType): Map<String, String> {
|
||||
return when (templateType) {
|
||||
TemplateType.TEXT -> emptyMap()
|
||||
TemplateType.MJML -> MjmlTemplate.placeholders
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTemplateFile(key: String, type: TemplateType): Path {
|
||||
val path = Path.of("./", TEMPLATE_PATH, "$key.${type.extension}")
|
||||
if (Files.notExists(path)) return getDefaultTemplateFile(key, type)
|
||||
return path
|
||||
}
|
||||
|
||||
private fun getOrCreateTemplateFile(key: String, type: TemplateType): Path {
|
||||
val path = Path.of("./", TEMPLATE_PATH, "$key.${type.extension}")
|
||||
if (Files.notExists(path)) {
|
||||
Files.createDirectories(path.parent)
|
||||
Files.createFile(path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private fun getDefaultTemplateFile(key: String, type: TemplateType): Path {
|
||||
log.info { "No custom message template found for '$key.${type.extension}', returning default" }
|
||||
val resourceUrl = javaClass.classLoader.getResource("$DEFAULT_TEMPLATE_PATH/$key.${type.extension}")
|
||||
?: throw IllegalStateException("Default template file not found for '$key.${type.extension}'")
|
||||
return Paths.get(resourceUrl.toURI())
|
||||
}
|
||||
|
||||
private fun fillTextTemplate(content: String, placeholders: Map<String, String>): String {
|
||||
return placeholders.entries.fold(content) { acc, (placeholder, value) ->
|
||||
acc.replace("{$placeholder}", value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillMjmlTemplate(content: String, placeholders: Map<String, String>): String {
|
||||
val withDefaultPlaceholders = placeholders + getDefaultTemplatePlaceholders(TemplateType.MJML)
|
||||
val contentWithFilledPlaceholders = fillTextTemplate(content, withDefaultPlaceholders)
|
||||
return Mjml4j.render(contentWithFilledPlaceholders)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.grimsi.gameyfin.notifications.templates
|
||||
|
||||
class MjmlTemplate() {
|
||||
companion object {
|
||||
val placeholders: Map<String, String> = mapOf<String, String>(
|
||||
"logo" to "data:image/png;base64,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",
|
||||
"gradient" to "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAABCAYAAACouxZ2AAAAo0lEQVRIS+VUwRGAIAyDWdzA/QdwKzwQKKUpKCennr6AmrYkpHZZN2eKz5Ybw0IhwuPx5+KQlhKr4kWa69iQ22JcuhLsPd/X4bup3KQA1cz5lULyGGAZ/7RpYWfrQvl1ftvctrWZo4vv+uiX5QfFhri9yTNDfhO15+jS86vnt6s7fM/nPINz/8Mzz8yyd3hGfVMx8F3PgHlU+UP30+gsI02Rn3dirSVLy0JP4wAAAABJRU5ErkJggg=="
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.grimsi.gameyfin.notifications.templates
|
||||
|
||||
enum class TemplateType(val extension: String) {
|
||||
MJML("mjml"), TEXT("txt")
|
||||
}
|
||||
+4
@@ -14,6 +14,10 @@
|
||||
<pattern>%d{yyy-MM-dd'T'HH:mm:ss.SSS} %-5level %-40.40logger{39} : %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Spams the logs on DEBUG due to a loop (log -> push to client -> log the push -> repeat) -->
|
||||
<logger name="com.vaadin.hilla.push.PushEndpoint" level="INFO"/>
|
||||
|
||||
<root level="{LOG_LEVEL}">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
@@ -0,0 +1,30 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>[Gameyfin] Password reset request</mj-title>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Arial, sans-serif"/>
|
||||
<mj-text font-size="16px"/>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image width="128px" src="{logo}"/>
|
||||
<mj-image height="2px" padding-bottom="20px" src="{gradient}"/>
|
||||
|
||||
<mj-text font-size="20px" font-family="helvetica">Hey {username},
|
||||
<br/>
|
||||
<br/>
|
||||
</mj-text>
|
||||
<mj-text>a password reset for your email has been requested.
|
||||
<br/>
|
||||
You can reset your password here: {resetLink}
|
||||
<br/>
|
||||
If you did not request a reset you can safely ignore this message.
|
||||
<br/>
|
||||
</mj-text>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,28 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>[Gameyfin] Password reset request</mj-title>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Arial, sans-serif"/>
|
||||
<mj-text font-size="16px"/>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image width="128px" src="{logo}"/>
|
||||
<mj-image height="2px" padding-bottom="20px" src="{gradient}"/>
|
||||
|
||||
<mj-text font-size="20px" font-family="helvetica">Hello there,
|
||||
<br/>
|
||||
<br/>
|
||||
</mj-text>
|
||||
<mj-text>you have been invited to Gameyfin!
|
||||
<br/>
|
||||
Create your account here: {invitationLink}
|
||||
<br/>
|
||||
</mj-text>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,27 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>[Gameyfin] Password reset request</mj-title>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Arial, sans-serif"/>
|
||||
<mj-text font-size="16px"/>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image width="128px" src="{logo}"/>
|
||||
<mj-image height="2px" padding-bottom="20px" src="{gradient}"/>
|
||||
|
||||
<mj-text font-size="20px" font-family="helvetica">Hey {username},
|
||||
<br/>
|
||||
<br/>
|
||||
</mj-text>
|
||||
<mj-text>your registration was successful!
|
||||
<br/>
|
||||
You can now start browsing games in Gameyfin.
|
||||
</mj-text>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
Reference in New Issue
Block a user