From 9d8ae28b7a5c04868f4dd58b7b63fcb2039946f5 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:51:03 +0200 Subject: [PATCH] Implemented test notification sending --- .../administration/NotificationManagement.tsx | 88 ++++++++++++++++--- .../notifications/NotificationEndpoint.kt | 4 + .../notifications/NotificationService.kt | 30 ++++++- 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/src/main/frontend/components/administration/NotificationManagement.tsx b/src/main/frontend/components/administration/NotificationManagement.tsx index 799e151..ba05645 100644 --- a/src/main/frontend/components/administration/NotificationManagement.tsx +++ b/src/main/frontend/components/administration/NotificationManagement.tsx @@ -14,17 +14,21 @@ import { ModalFooter, ModalHeader, Textarea, + Tooltip, useDisclosure } from "@nextui-org/react"; import {MessageTemplateEndpoint, NotificationEndpoint} from "Frontend/generated/endpoints"; import {toast} from "sonner"; -import {Pencil} from "@phosphor-icons/react"; +import {PaperPlaneRight, 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"; +import {Form, Formik} from "formik"; +import Input from "Frontend/components/general/Input"; function NotificationManagementLayout({getConfig, getConfigs, formik}: any) { - const {isOpen, onOpen, onOpenChange} = useDisclosure(); + const editorModal = useDisclosure(); + const testNotificationModal = useDisclosure(); const [availableTemplates, setAvailableTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); const [templateContent, setTemplateContent] = useState(""); @@ -53,7 +57,7 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) { } } - async function openModal(template: MessageTemplateDto) { + async function openEditor(template: MessageTemplateDto) { setSelectedTemplate(template); let templateContent = await MessageTemplateEndpoint.read(template.key, TemplateType.MJML); @@ -66,13 +70,26 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) { } setTemplateContent(templateContent); - onOpen(); + editorModal.onOpen(); + } + + function openTestNotification(template: MessageTemplateDto) { + setSelectedTemplate(template); + testNotificationModal.onOpen(); } async function saveTemplate(template: MessageTemplateDto) { await MessageTemplateEndpoint.save(template.key, TemplateType.MJML, templateContent); } + function generateValidationSchema(placeholders: string[]) { + const shape: { [key: string]: Yup.StringSchema } = {}; + placeholders.forEach(placeholder => { + shape[placeholder] = Yup.string().required(`Placeholder ${placeholder} is required`); + }); + return Yup.object().shape(shape); + } + return (
@@ -102,12 +119,22 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
{availableTemplates.map((template: MessageTemplateDto) => - + + + + + +

{template.description}

)} @@ -117,7 +144,7 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) {
- + {(onClose) => ( <> @@ -190,6 +217,45 @@ function NotificationManagementLayout({getConfig, getConfigs, formik}: any) { )} + + + + {(onClose) => ( + <> + { + await NotificationEndpoint.sendTestNotification(selectedTemplate?.key, values); + toast.success("Test notification to you has been sent"); + onClose(); + }} + validationSchema={generateValidationSchema(selectedTemplate?.availablePlaceholders as string[])} + > +
+ + Send {selectedTemplate?.name} Test Message + + +

Fill the placeholders of the + template

+ {selectedTemplate?.availablePlaceholders?.map((placeholder) => + + )} +
+ + + + +
+
+ + )} +
+
); } diff --git a/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationEndpoint.kt index 879c25d..9364c51 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationEndpoint.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationEndpoint.kt @@ -19,4 +19,8 @@ class NotificationEndpoint( fun verifyCredentials(provider: String, credentials: Map): Boolean { return notificationService.testCredentials(provider, credentials) } + + fun sendTestNotification(templateKey: String, placeholders: Map): Boolean { + return notificationService.sendTestNotification(templateKey, placeholders) + } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationService.kt b/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationService.kt index 71a8e4b..d93820a 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/notifications/NotificationService.kt @@ -4,12 +4,15 @@ import de.grimsi.gameyfin.core.events.PasswordResetRequestEvent import de.grimsi.gameyfin.notifications.providers.AbstractNotificationProvider import de.grimsi.gameyfin.notifications.templates.MessageTemplateService import de.grimsi.gameyfin.notifications.templates.MessageTemplates +import de.grimsi.gameyfin.users.UserService import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.context.ApplicationContext import org.springframework.context.event.EventListener import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.EnableAsync +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service import java.util.* @@ -17,7 +20,8 @@ import java.util.* @Service class NotificationService( private val applicationContext: ApplicationContext, - private val templateService: MessageTemplateService + private val templateService: MessageTemplateService, + private val userService: UserService ) { val log: KLogger = KotlinLogging.logger {} @@ -47,6 +51,30 @@ class NotificationService( } } + /** + * Sends a test notification. + * Recipient is always the current user to prevent misuse. + */ + fun sendTestNotification(templateKey: String, placeholders: Map): Boolean { + + if (!enabled) { + log.error { "No notification provider available, can't send test message" } + return false + } + + try { + val auth: Authentication = SecurityContextHolder.getContext().authentication + val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found") + val template = templateService.getMessageTemplate(templateKey) + sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders) + } catch (e: Exception) { + log.error(e) { "Failed to send test notification" } + return false + } + + return true + } + @Async @EventListener(PasswordResetRequestEvent::class) fun onPasswordResetRequest(event: PasswordResetRequestEvent) {