Implemented types for notification templates (currently only MJML is used, but it's now easily expandable)

Added default templates
This commit is contained in:
grimsi
2024-09-24 14:26:21 +02:00
parent 8cf6236b1d
commit c0a790bda4
16 changed files with 222 additions and 41 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+1
View File
@@ -70,6 +70,7 @@ dependencies {
// Notifications // Notifications
implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("ch.digitalfondue.mjml4j:mjml4j:1.0.3")
// Development // Development
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
@@ -7,6 +7,7 @@ import {
Button, Button,
Card, Card,
Chip, Chip,
Link,
Modal, Modal,
ModalBody, ModalBody,
ModalContent, ModalContent,
@@ -19,6 +20,7 @@ import {MessageTemplateEndpoint, NotificationEndpoint} from "Frontend/generated/
import {toast} from "sonner"; import {toast} from "sonner";
import {Pencil} from "@phosphor-icons/react"; import {Pencil} from "@phosphor-icons/react";
import MessageTemplateDto from "Frontend/generated/de/grimsi/gameyfin/notifications/templates/MessageTemplateDto"; 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) { function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
@@ -26,6 +28,7 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
const [availableTemplates, setAvailableTemplates] = useState<MessageTemplateDto[]>([]); const [availableTemplates, setAvailableTemplates] = useState<MessageTemplateDto[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<MessageTemplateDto | null>(null); const [selectedTemplate, setSelectedTemplate] = useState<MessageTemplateDto | null>(null);
const [templateContent, setTemplateContent] = useState<string>(""); const [templateContent, setTemplateContent] = useState<string>("");
const [defaultPlaceholders, setDefaultPlaceholders] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
MessageTemplateEndpoint.getAll().then((response: any) => { MessageTemplateEndpoint.getAll().then((response: any) => {
@@ -53,7 +56,9 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
async function openModal(template: MessageTemplateDto) { async function openModal(template: MessageTemplateDto) {
setSelectedTemplate(template); 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) { if (templateContent === undefined) {
toast.error("Can't read template content"); toast.error("Can't read template content");
@@ -65,7 +70,7 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
} }
async function saveTemplate(template: MessageTemplateDto) { async function saveTemplate(template: MessageTemplateDto) {
await MessageTemplateEndpoint.save(template.key, templateContent); await MessageTemplateEndpoint.save(template.key, TemplateType.MJML, templateContent);
} }
return ( return (
@@ -119,8 +124,13 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
<ModalHeader <ModalHeader
className="flex flex-col gap-1">Edit {selectedTemplate?.name} Template</ModalHeader> className="flex flex-col gap-1">Edit {selectedTemplate?.name} Template</ModalHeader>
<ModalBody> <ModalBody>
<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"> <div className="flex flex-row gap-2">
<p>Available placeholders:</p>
{selectedTemplate?.availablePlaceholders?.map((placeholder) => {selectedTemplate?.availablePlaceholders?.map((placeholder) =>
<Chip radius="sm" <Chip radius="sm"
key={placeholder} key={placeholder}
@@ -128,6 +138,27 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
>{placeholder}</Chip> >{placeholder}</Chip>
)} )}
</div> </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 <Textarea
size="lg" size="lg"
autoFocus autoFocus
@@ -16,9 +16,7 @@ import reactor.core.publisher.Sinks
import java.io.InputStream import java.io.InputStream
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@Service @Service
class LogService( class LogService(
@@ -26,16 +24,16 @@ class LogService(
) { ) {
companion object { 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 const val LOG_FILE_NAME = "gameyfin"
private val LOG_REFRESH_INTERVAL = 5.seconds 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 val log = KotlinLogging.logger {}
private var logFilePath: Path? = null 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 private var tailer: AsyncFileTailer? = null
@EventListener(ApplicationStartedEvent::class) @EventListener(ApplicationStartedEvent::class)
@@ -35,8 +35,16 @@ class NotificationService(
?: throw IllegalArgumentException("Provider '$provider' not found") ?: throw IllegalArgumentException("Provider '$provider' not found")
} }
fun sendNotification(recipient: String, title: String, message: String) { fun sendNotification(
providers.filter { it.enabled }.forEach { it.sendNotification(recipient, title, message) } 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 @Async
@@ -52,15 +60,11 @@ class NotificationService(
val token = event.token val token = event.token
val resetLink = event.baseUrl + "/reset-password?token=${token.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( sendNotification(
token.user.email, token.user.email,
"[Gameyfin] Password Reset Request", "[Gameyfin] Password Reset Request",
content MessageTemplates.PasswordResetRequest,
mapOf("username" to token.user.username, "resetLink" to resetLink)
) )
} }
} }
@@ -1,10 +1,12 @@
package de.grimsi.gameyfin.notifications.providers package de.grimsi.gameyfin.notifications.providers
import de.grimsi.gameyfin.config.ConfigService import de.grimsi.gameyfin.config.ConfigService
import de.grimsi.gameyfin.notifications.templates.TemplateType
import java.util.* import java.util.*
abstract class AbstractNotificationProvider( abstract class AbstractNotificationProvider(
val providerKey: String, val providerKey: String,
val supportedTemplateType: TemplateType,
protected val config: ConfigService protected val config: ConfigService
) { ) {
protected companion object { protected companion object {
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.notifications.providers
import de.grimsi.gameyfin.config.ConfigProperties import de.grimsi.gameyfin.config.ConfigProperties
import de.grimsi.gameyfin.config.ConfigService import de.grimsi.gameyfin.config.ConfigService
import de.grimsi.gameyfin.notifications.templates.TemplateType
import jakarta.mail.Message import jakarta.mail.Message
import jakarta.mail.MessagingException import jakarta.mail.MessagingException
import jakarta.mail.Session import jakarta.mail.Session
@@ -13,7 +14,7 @@ import java.util.*
@Service @Service
class EmailNotificationProvider( class EmailNotificationProvider(
config: ConfigService config: ConfigService
) : AbstractNotificationProvider("email", config) { ) : AbstractNotificationProvider("email", TemplateType.MJML, config) {
private val storedCredentials: Properties private val storedCredentials: Properties
get() { get() {
@@ -63,7 +64,7 @@ class EmailNotificationProvider(
mimeMessage.setFrom(InternetAddress(credentials["username"] as String)) mimeMessage.setFrom(InternetAddress(credentials["username"] as String))
mimeMessage.setRecipients(Message.RecipientType.TO, recipient) mimeMessage.setRecipients(Message.RecipientType.TO, recipient)
mimeMessage.subject = title mimeMessage.subject = title
mimeMessage.setText(message) mimeMessage.setContent(message, "text/html; charset=utf-8")
val transport = session.getTransport("smtp") val transport = session.getTransport("smtp")
transport.connect( transport.connect(
@@ -17,11 +17,15 @@ class MessageTemplateEndpoint(
return messageTemplateService.getMessageTemplate(key) return messageTemplateService.getMessageTemplate(key)
} }
fun read(key: String): String { fun getDefaultPlaceholders(type: TemplateType): Set<String> {
return messageTemplateService.getMessageTemplateContent(key) return messageTemplateService.getDefaultTemplatePlaceholders(type).keys
} }
fun save(key: String, content: String) { fun read(key: String, templateType: TemplateType): String {
messageTemplateService.setMessageTemplateContent(key, content) return messageTemplateService.getMessageTemplateContent(key, templateType)
}
fun save(key: String, templateType: TemplateType, content: String) {
messageTemplateService.setMessageTemplateContent(key, templateType, content)
} }
} }
@@ -1,9 +1,11 @@
package de.grimsi.gameyfin.notifications.templates package de.grimsi.gameyfin.notifications.templates
import ch.digitalfondue.mjml4j.Mjml4j
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.readText import kotlin.io.path.readText
import kotlin.io.path.writeText import kotlin.io.path.writeText
@@ -12,7 +14,7 @@ class MessageTemplateService {
companion object { companion object {
private const val TEMPLATE_PATH = "templates" 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 {} private val log = KotlinLogging.logger {}
@@ -32,33 +34,67 @@ class MessageTemplateService {
?: throw IllegalArgumentException("Message template with key '$key' not found") ?: throw IllegalArgumentException("Message template with key '$key' not found")
} }
fun getMessageTemplateContent(key: String): String { fun getMessageTemplateContent(key: String, type: TemplateType): String {
log.info { "Reading message template content for key '$key'" } log.info { "Reading message template content for '$key.${type.extension}'" }
return getOrCreateTemplateFile(key).readText() 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()) { if (placeholders.keys != template.availablePlaceholders.toSet()) {
throw IllegalArgumentException("Placeholders do not match available placeholders for template '${template.key}'") throw IllegalArgumentException("Placeholders do not match available placeholders for template '${template.key}'")
} }
val content = getMessageTemplateContent(template.key) val content = getMessageTemplateContent(template.key, type)
return placeholders.entries.fold(content) { acc, (placeholder, value) ->
acc.replace("{$placeholder}", value) 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'" } log.info { "Saving message template content for key '$key'" }
getOrCreateTemplateFile(key).writeText(content) getOrCreateTemplateFile(key, type).writeText(content)
} }
private fun getOrCreateTemplateFile(key: String): Path { fun getDefaultTemplatePlaceholders(templateType: TemplateType): Map<String, String> {
val path = Path.of("./", TEMPLATE_PATH, "$key.$TEMPLATE_EXTENSION") 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)) { if (Files.notExists(path)) {
Files.createDirectories(path.parent) Files.createDirectories(path.parent)
Files.createFile(path) Files.createFile(path)
} }
return 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,iVBORw0KGgoAAAANSUhEUgAAAIAAAAB2CAYAAAAA9ZvPAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAANNklEQVR42u2de3Bc1X3Hv99zdyUkItsV4MSZmHYU2jQCG1srsLWRkIwvXgtrC25rt8E4DU2adtI2aZtOO/3Lw/SRthNI20wSQtNpCYGA7WDHu1iWfIklWxY2tvyoWzItRO2UEBwDBiPZQtq959s/JIGNH1iyVtrH+f4n7e69557f5zy+53EP4VRiEn972ZMLCZsQkKDLkOLXp5Y/co3Jlt9OyCfVCmL+eOAdAEWoNWs2epWvYZEU+oR8gs0EoqMBF8B3A+8AKBKtjz81F9GRZhokYbGKRPU7AcfZAT8XgIjLusJUS8uuyDy8utQD2yjrA5k6gIQ0oWLtaoAC0j2Nm2vgWR+ATyABYNZoAPVuCecFSryrAQpTyViqsqJqOG6sfAJJQbUEgbGgToUcAHmmX23cXOMZJI3QBg430aI8l/dzAMywVi9/6hpvGLfTyAfQSmD+aF2tabm/A2AGLFrmleiiCOSL9DmiZhBRaGZ6ZA6AadTd8S2LwuPoNMR1Guu8zbQcANPVtjd9f56AbQCuy6d0GReaaaj2GzZWCGYrxtt3B0DpaAM2mIxX9jiAW/MxfQ6AHOtoU90DAO7O1/Q5AC6hRMOO6taG7Ysn+/u7mrZ+FtIf5fMzuk7geyzaqVcqF9HCB+nDhg0kGicZ/ASEb+b7MzsAxuTHn/61N1/mw4SqhdFxGBJf2t676sgkgl8L8UlAeZ+/JdcErLlxY1lLLHXteRlBcz0wOoU6pp1Lep/7h0nZPbEdwOw8zYJhQM8I+DMa3FwSNYDfuLMmtNaH4L8OraB0P4CvXuz7BF6zzH76ftxvJ2r3hhXZAuD6vMoAoR9AIDAYqQw7Hmu/962ibgJisVRlVTQaB4wPIz8bhrGJjLJa6fM7e+/66UTt3mGv/DEAS/Ig4GdA9AoKjOel/vWZtc8XfR+gsX5nDTzrezRJwd4hoBzQxOdUiG/t7G3bNNH7H2qM/T2g1TO3wEL9ANMAU7Mrq/Z8rf3O4aLuBLa07PpA9u2RZaBpg9RKaP7ococrGl9/cSRa+acT/VHyE9s+A+BL05wFgwC6CKQysu1P7P7kS0XuAjaYJUuaF3uUT8nPDGWbQROdqllTC2U8mXVdXcsGJ/K7tsZUi6RvTEPJtyAOgwgABVcPvNn9cN/vZoraBsbjHXOVjTSD8gH8CmA/BCEnM6fZaMW/BRMMfmvT1lpYbQFQlpumHCcM2C3aIEts29S19nhRjwO0tOyKDA1hqWTbSPhhiDpydAUbczx1OtGSn2xJXWsz2AZozlRyCOIohLS1NrWpZ82hXK4M+UzLozfMOAD19Ttr6Hk+RP/0GSUIzMIUr3ub8rGEho0VQxmkAH10Ci7XLyqAZcDybOfGYO2pXKX7c8lUZWbgrTiM8Y2UBFA77QDEYgcrLQfixsAHmARUO8GVzDPtsXiaT/8LoaWTvMAZAL0UAoLBpp7VfblM7X3+YzXKeknCtmUGBpsIlkOa3iZgQf2eGo82SahNGEvEeCtXYGqNp74M8pMT9Jb9pILQMj1SVdbZfpkWbVLVemJj9cjbdrkBfBCtymr+pXpNOQGgtqG3uiwMl0vWN0CrYOcXwwaEO+Op+0D8+eVYNAFdBkrJsP0Hu+96KVdp2oAN5oVlNy42oXwSfmbYNpOj28CmsRMos6CuZzEIn5SPbLZZY3vRhOJQa9O2Zlk+dJHOqCVwWFBAIPjg0M+mxKJdTOvjT80No2EzoWQ/sMpYVXOSJWzSAHxscc+Ho55WQkiAPT7OnUjJaxng5ES+v6Ih9cuwPMfuETghsFtUoIi3Ld216niu0ju+DUyWbYbwQ2Trxjf3XGkBi0wkEa8ORJdass1QPoQ6gSQLq4yL6LnmIwPfxb7Lt3uZDFMAqgD0gUgTTKV7VuXUot3TuLnGevQp+LKvJQDOykUzekkAfrF+T41nPZ+U/7OB8b1oQmH12s/RKVmt37RpbXi5P8iMcCWN/mQoMrSrq2vtYK4SNr4NDBY+YJIhbO3ZvfVc6Zw4fjh2sPJqZuIQfA9KCqi9ss2Hes8+9LOv836fT/19CN3zw2dbv5cvNI5uA/OSENoA2wSi/MLPm6t8AyK/tHj/zaJNCEpQmUYCZbjSKZX81KMzHfw1DRurs15kOQ19Sq0A5ks6vyROoyKWOjKagKLeKf4/V1n7B9N90w3YYI42Llos0CflZ4HRN3XkUekqhRVBWQPe277/zrem42ar40/NDY3XTCh5BFhFoJrI3z5T8QMg/NUP9yV6c2nRqrKDSwG1EfBDqA7vtLL535AWOQA8MJCp/pupvuqqxnQNAB+Sz8xAQsAscrTnVGgNaTEDMChhXV9f/RWPyCVjqcqRCsaNhU8iKekdd1ToKjYARkD0UOwwXviDXXtbX5jscFGiMb0Q1ksQYSJDNhqhbGrG3mZcIYCDpHbAakfhA0D0AwwEBVeNsDPou2NS8+ktLVvmlGXKE5ISxPaVsGYeZuqtDVPfD/opjDogdmS9TPCdZ37r9UKuAc4Q6CUVQAp69iWmZD69LHPVfZIeLBIznAVwlFBatKlHuu656LB1pDAK+djGBjI95+eGczqfXsDFfDSPhKAiajofPmdl0bqC6wMMkuiSmAptpv3AgcRLLsDn14QAeiEF1mPw+K7fmFRNmCcA0AI6LCiQNYFU1f3cc/UZF+PzNNbfsek3KqumpCacSQBOAOgmFcBi24EDy467+J5fE4LoApSSwklv/sgXALIAj4I2DSrVt3/ZoWl7GV7haHTzhxTIIJgz8HpOVxZNBwD9AAJQgQkznX2TtGhFrhMgumWZZjab3vjs2pPTefMcAKAjEr4dsbbj0KFlL7r4XrAm3E8pNbos/O4ZrQlzAIDZfOxg09ddnM/p5PaTCmQVVGSu6nhsmmYmC8gFFGnYpT+GkNrae/eP8zWNDoBcGvWrK/+5szNxOp/T6F4TV+JyADgAnBwATg4AJwdASWjFwo6rm5p2zHOhLyEbOP6iSErJLHRHJKO/wCVeFOkAKHA1NOyoLoNZDsgn0ZoNw/nji7TdQYlFCcAG07gkvhjk6Fm5QjOgqAtvEQMQj3fMNWIzwCSkVcT4WbmFsSHDAXAlwV8S/DpDPSnCuCq9xFzAJ27trAfwiLOwJQjA0qW7fkE0aQCVLnQlBsCSJdtnUeE2AB90YSsxAGKxg1EPZZsBLHAhK0EAotFT/yTgDheuEgQgFktVQvg9FyrXCXRyADg5AJwcAE4OACcHgJMDwMkB4OQAcHIAODkAnBwATg4AJweAU1EBUFX1AQdpDpWXq4Lj8Y652WykmZQ/PBQm3XaOIgdg9NRw3SwpCbAtm0UdKSLnZ4Y7zRgA9fV7aqyX8T3RHzyDFQRnA66sFzoAWUI7L/TBO6eGA75ofDGMUa6EFxcAwv1H+257bvzPBbHuBR65UlTC4nQjwHKNfdGpyAAgsPdjNa98+djYO6sX3rLnC4T9RxTgOTrOBk48/KdMJHvv+JGsC+p7Wgk94LK3RGoACp8/sm/Z/wLAwvpdN4H2eyjhdxAKPEli5+yXT2VKAAB+91hf4+MA8PHY7nkAtgOYXWIxtwQOCwoIBB8aOp7zt3znCwAvcVhfGO/lD3NoK6D5pRBxAicEdoNIZ8JouuPZlScL8TmuBABraNcfO3bbG4DMCPc+TuDWIo55FsB+kikAQbpnVVGcdxCZfAnQXx47cFs3ANwU63kQwF1FWMrHzjtAgIzpaM+jt3zPNAAHylXx1wBw4y27fwfCF4skP86A6KUUGM+k2vfc+bxzAedrUF64rm9/fab2lj0rKXyjGDJiJJp99LpXh76+6T/XjjgbeEmLo9//0f7mF26q271Q4kZAhWb3TsLw/977z66u5GtuHOD9/f73n+9r+s7HY7vniSYFoCr/PTksicO0CCwQvJW5tnsqDpQuOQAE/ITl/NxHGnormLFbAFyfx891AkC3qHQFkO7oLUyLlk8AWEGfev7Z+Js3xvZuFrEkHy2agJSkoGf/Cnck3ZQCQPztfx1s3FUb2/uAiNV5kvZ+EQGv8NTwUhdvqNuns2fsyNEKn+964T7v7dlxW/HGeojf5ljBevfzC/zN8b/PuQ7e5z7v+fy8654B0EsyMGLQs//2Phe+3NcAp0W7Liw/tYziQzNRygmNnRo+4k4Nn3YAiC9GwoiXNeETnIbZPQKDgLpIpqzNulPDZxIAAls8Rp4OTXYfgTk5CrkldFhAIIvAapazaHkCwMu2DH9oR8ItBvj53Fg0Bp617tTwPATAyvLT3jC+ImrpFFm0oyTSoE0dcqeG5zcAEr5iPK2Q8JtX0nkDFJAKPHdqeEEBcNjz9GNr8a0JLuQ8A6AXQiAxOHaoyVm0AgTg7dCzX6M1D13msu1+gIGo9PAbA50vvugsWkEDQODvvNB7EFTZRb47CKCLVIqw7f9xoNlZtGIBgMAzAtYBOtvuWQCHBQaGDCoVdRatKAGgXpU4m9ANoxaN3ZACw2zqR323veKyqMgBoDVPgPYnlPnsfx++9d+dRSst/T9WP4R07dFqFgAAAABJRU5ErkJggg==",
"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")
}
@@ -14,6 +14,10 @@
<pattern>%d{yyy-MM-dd'T'HH:mm:ss.SSS} %-5level %-40.40logger{39} : %m%n</pattern> <pattern>%d{yyy-MM-dd'T'HH:mm:ss.SSS} %-5level %-40.40logger{39} : %m%n</pattern>
</encoder> </encoder>
</appender> </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}"> <root level="{LOG_LEVEL}">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/> <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>