diff --git a/src/lib/notifications/notificationTypes.ts b/src/lib/notifications/notificationTypes.ts index 56d95d95fc9ac8acea0cf938c7753998a7be799e..4e5dad176731a8599938f4047f353aa98cfe30da 100644 --- a/src/lib/notifications/notificationTypes.ts +++ b/src/lib/notifications/notificationTypes.ts @@ -8,4 +8,5 @@ export const NotificationType = { USE_VOUCHER: {key: 1<<4, name: "use-voucher"} as Type<"use-voucher">, SEND_TRANSFER: {key: 1<<5, name: "send-transfer"} as Type<"send-transfer">, RECEIVE_TRANSFER: {key: 1<<6, name: "receive-transfer"} as Type<"receive-transfer">, + NEWS: {key: 1<<7, name: "news"} as Type<"news">, }; diff --git a/src/lib/notifications/types.ts b/src/lib/notifications/types.ts index 9fb6607ad6ede62ed4ad6c8e29bfdc3f5e9bdbc7..66c31aaff19c5a27d3891193b92230aa91d740e1 100644 --- a/src/lib/notifications/types.ts +++ b/src/lib/notifications/types.ts @@ -1,4 +1,4 @@ -export type PossibleNotificationType = "buy" | "refund" | "deposit" | "withdraw" | "use-voucher" | "send-transfer" | "receive-transfer"; +export type PossibleNotificationType = "buy" | "refund" | "deposit" | "withdraw" | "use-voucher" | "send-transfer" | "receive-transfer" | "news"; export type Item = {name: string, code: string, price: number, premium: number|undefined}; export type User = {name: string, id: number} @@ -11,6 +11,7 @@ export type WithdrawNotificationData = {amount: number, balanceBefore: number, b export type UseVoucherNotificationData = {voucher: {code: string, value: number}, balanceBefore: number, balanceAfter: number}; export type SendTransferNotificationData = {amount: number, receiver: User, balanceBefore: number, balanceAfter: number}; export type ReceiveTransferNotificationData = {amount: number, sender: User, balanceBefore: number, balanceAfter: number}; +export type NewsNotificationData = {title: string, textVersion: string, htmlVersion: string}; export type NotificationData<T extends PossibleNotificationType> = T extends "buy" ? BuyNotificationData : T extends "refund" ? RefundNotificationData : @@ -19,6 +20,7 @@ export type NotificationData<T extends PossibleNotificationType> = T extends "use-voucher" ? UseVoucherNotificationData : T extends "send-transfer" ? SendTransferNotificationData : T extends "receive-transfer" ? ReceiveTransferNotificationData : + T extends "news" ? NewsNotificationData : never; export type HandlerReturnSuccess = {status: "success"}; diff --git a/src/lib/server/notifications/formatter.ts b/src/lib/server/notifications/formatter.ts index 24fbfc974c245a956be24f33cac0d017649b2a7c..f001541779231fbfc521a5bd9c1c1f24b4b2fb35 100644 --- a/src/lib/server/notifications/formatter.ts +++ b/src/lib/server/notifications/formatter.ts @@ -23,6 +23,9 @@ export function getMessageTitle<T extends PossibleNotificationType>(type: Notifi case "receive-transfer": { return "Überweisung erhalten"; } + case "news": { + return "Neu bei der Getränkekasse"; + } default: { console.error(`Unknown notification type`, type); return `Unknown notification type ${type}`; @@ -70,6 +73,9 @@ Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`; return `Du hast ${(amount/100).toFixed(2)}€ von ${sender.name} erhalten. Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`; } + case "news": { + return (data as NotificationData<"news">).textVersion; + } default: { console.error(`Unknown notification type`, type); return `Unknown notification type ${type}`; @@ -118,6 +124,9 @@ export function formatMessageToHtml<T extends PossibleNotificationType>(type: No return `<p>Du hast ${(amount / 100).toFixed(2)}€ von ${escapeHtml(sender.name)} erhalten.</p> <p>Dein neuer Kontostand beträgt ${(balanceAfter / 100).toFixed(2)}€.</p>`; } + case "news": { + return (data as NotificationData<"news">).htmlVersion; + } default: { console.error(`Unknown notification type`, type); return `Unknown notification type ${escapeHtml(JSON.stringify(type))}`; diff --git a/src/lib/server/notifications/handler.ts b/src/lib/server/notifications/handler.ts index 830b6c6e53adc6555b6d1f4da6514946b911e8b5..bbdfd4773223c4c6bb7a3ebff8f761da37828d7e 100644 --- a/src/lib/server/notifications/handler.ts +++ b/src/lib/server/notifications/handler.ts @@ -2,17 +2,21 @@ import { db } from "$lib/server/database"; import { ChannelType } from "./channelTypes"; import type { HandlerReturnError, HandlerReturnIgnored, HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types"; -export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{ +export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number | null, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{ let notificationChannels: NotificationChannel[]; - let userId: number; - if(typeof user === "number"){ - userId = user; + if(user === null){ + notificationChannels = await getNotificationChannels(type.key); }else{ - userId = user.id; - notificationChannels = user.notificationChannels?.filter(channel => channel.notificationTypes & type.key); - } - if(!notificationChannels){ - notificationChannels = await getNotificationChannels(userId, type.key); + let userId: number; + if(typeof user === "number"){ + userId = user; + }else{ + userId = user.id; + notificationChannels = user.notificationChannels?.filter(channel => channel.notificationTypes & type.key); + } + if(!notificationChannels){ + notificationChannels = await getNotificationChannels(userId, type.key); + } } return Promise.all(notificationChannels.map(channel => { const channelType = Object.values(ChannelType).find(type => type.key === channel.channelType); @@ -34,9 +38,17 @@ export async function sendNotification<T extends PossibleNotificationType>(user: })); }; -async function getNotificationChannels(userId: number, notificationType: number): Promise<NotificationChannel[]> { - const allChannels = await db.notificationChannel.findMany({ - where: { userId } - }); +function getNotificationChannels(userId: number, notificationType: number): Promise<NotificationChannel[]>; +function getNotificationChannels(notificationType: number): Promise<NotificationChannel[]> +async function getNotificationChannels(userId: number, notificationType?: number): Promise<NotificationChannel[]> { + let allChannels; + if(notificationType === undefined){ + notificationType = userId; + allChannels = await db.notificationChannel.findMany(); + }else{ + allChannels = await db.notificationChannel.findMany({ + where: { userId } + }); + } return allChannels.filter(channel => channel.notificationTypes & notificationType); } diff --git a/src/routes/admin/mail/+page.server.ts b/src/routes/admin/mail/+page.server.ts index bce0dc30c818ae0e808467bad44f549815f2ac01..0fa2364fe63ae6a9d6fe81092205f1efdf855854 100644 --- a/src/routes/admin/mail/+page.server.ts +++ b/src/routes/admin/mail/+page.server.ts @@ -2,6 +2,8 @@ import { error, type Actions } from "@sveltejs/kit"; import { getUsers } from "../api/users"; import { sendMail } from "$lib/server/mail"; import { format } from "./formatter"; +import { sendNotification } from "$lib/server/notifications/handler"; +import { NotificationType } from "$lib/notifications/notificationTypes"; export async function load(){ const users = await getUsers(); @@ -13,24 +15,34 @@ export const actions: Actions = { const data = await event.request.formData(); const subject = data.get("subject"); const message = data.get("content"); - const users = data.get("users"); - if(typeof subject !== "string" || typeof message !== "string" || typeof users !== "string") throw error(400, "Invalid request"); - const userIDs = users.split(",").map(id => parseInt(id)); - const usersToSend = await getUsers(userIDs); - const result: {accepted: number[], rejected: number[], error: {userId: number, error: any}[]} = { accepted: [], rejected: [], error: [] }; - for(const user of usersToSend){ - const mail = format(subject, message, user); - try{ - const r = await sendMail(mail); - if(r.accepted.length > 0) result.accepted.push(user.id); - else if(r.rejected.length > 0) result.rejected.push(user.id); - }catch(err){ - result.error.push({ - userId: user.id, - error: err - }); + const type = data.get("type"); + if(type === "mail"){ + const users = data.get("users"); + if(typeof subject !== "string" || typeof message !== "string" || typeof users !== "string") throw error(400, "Invalid request"); + const userIDs = users.split(",").map(id => parseInt(id)); + const usersToSend = await getUsers(userIDs); + const result: {accepted: number[], rejected: number[], error: {userId: number, error: any}[]} = { accepted: [], rejected: [], error: [] }; + for(const user of usersToSend){ + const mail = format(subject, message, user); + try{ + const r = await sendMail(mail); + if(r.accepted.length > 0) result.accepted.push(user.id); + else if(r.rejected.length > 0) result.rejected.push(user.id); + }catch(err){ + result.error.push({ + userId: user.id, + error: err + }); + } } + return result; + }else if(type === "notification"){ + if(typeof subject !== "string" || typeof message !== "string") throw error(400, "Invalid request"); + const notification = format(subject, message); + sendNotification(null, NotificationType.NEWS, notification); + return { status: "success" }; + }else{ + throw error(400, "Invalid request"); } - return result; } }; diff --git a/src/routes/admin/mail/+page.svelte b/src/routes/admin/mail/+page.svelte index b1ea9f4c41049a14d21e33992b8c282ec4770706..a61f15280c82d875f23a9a2e386cd3dde543a169 100644 --- a/src/routes/admin/mail/+page.svelte +++ b/src/routes/admin/mail/+page.svelte @@ -7,15 +7,17 @@ export let data: {users: User[]}; let subject = "", content = ""; + let type: "mail"|"notification" = "mail"; let minBalance, maxBalance; $: filter = (user: User) => { + if(type === "notification") return false; if(minBalance != null && user.balance < minBalance) return false; if(maxBalance != null && user.balance > maxBalance) return false; return true; }; $: matchedUsers = data.users.filter(filter); $: exampleUser = matchedUsers[Math.floor(Math.random()*matchedUsers.length)]; - $: preview = exampleUser ? format(subject, content, exampleUser) : null; + $: preview = format(subject, content, exampleUser); </script> <style> @@ -32,43 +34,67 @@ <a href="/admin">Zurück</a> +<h2>Typ</h2> +<select bind:value={type}> + <option value="mail">Mail</option> + <option value="notification">Benachrichtigung</option> +</select> + +{#if type === "mail"} <h2>Empfänger</h2> <label>Minimales Guthaben: <input type="number" bind:value={minBalance} /></label><br> <label>Maximales Guthaben: <input type="number" bind:value={maxBalance} /></label><br> +{/if} <h2>Mail</h2> -<form method="post" action="?/send" use:enhance={({form, data, cancel})=>{ - return async ({result})=>{ - if(result.type === "success"){ - const { rejected, accepted, error } = result.data; - if(error.length){ - console.error("Error sending mails", error); - addMessage(MessageType.ERROR, "Fehler beim Senden der Mails an: " + error.map(e=>e.userId).join(", ")); - } - if(rejected.length){ - addMessage(MessageType.ERROR, "Mails konnten nicht an: " + rejected.map(e=>e.userId).join(", ") + " gesendet werden"); +<form method="post" action="?/send" use:enhance={({formElement, formData, cancel})=>{ + if(type === "mail"){ + return async ({result})=>{ + if(result.type === "success"){ + const { rejected, accepted, error } = result.data; + if(error.length){ + console.error("Error sending mails", error); + addMessage(MessageType.ERROR, "Fehler beim Senden der Mails an: " + error.map(e=>e.userId).join(", ")); + } + if(rejected.length){ + addMessage(MessageType.ERROR, "Mails konnten nicht an: " + rejected.map(e=>e.userId).join(", ") + " gesendet werden"); + } + if(accepted.length){ + addMessage(MessageType.SUCCESS, "Mails erfolgreich an " + accepted.length + " Nutzer gesendet"); + } } - if(accepted.length){ - addMessage(MessageType.SUCCESS, "Mails erfolgreich an " + accepted.length + " Nutzer gesendet"); + }; + }else{ + return async ({result})=>{ + if(result.type === "success"){ + addMessage(MessageType.SUCCESS, "Benachrichtigung erfolgreich versendet"); + }else{ + addMessage(MessageType.ERROR, "Benachrichtigung konnte nicht versendet werden"); + console.error(result); } - } - }; + }; + } }}> <input type="text" name="subject" placeholder="Betreff" bind:value={subject} required /><br> <textarea name="content" placeholder="Inhalt" bind:value={content} required></textarea><br> + <input type="hidden" name="type" value={type} /> + {#if type === "mail"} <input type="hidden" name="users" value={matchedUsers.map(u=>u.id).join(",")} /> + {/if} <button type="submit">Senden</button> </form> <h2>Vorschau</h2> -{#if preview} +{#if type==="mail" && !matchedUsers.length} +<p class="preview">Die Mail wird an keinen Nutzer gesendet</p> +{:else} <div class="preview"> + {#if type === "mail"} <p><b>An: </b>{matchedUsers.map(u=>u.email).join(", ")}</p> - <h3>{preview.subject}</h3> + {/if} + <h3>{type === "mail" ? preview.subject : preview.title}</h3> <div> - {@html preview.html} + {@html type === "mail" ? preview.html : preview.htmlVersion} </div> </div> -{:else} -<p class="preview">Die Mail wird an keinen Nutzer gesendet</p> {/if} diff --git a/src/routes/admin/mail/formatter.ts b/src/routes/admin/mail/formatter.ts index 45b15c5d13a86a6c7686c292cfa8402cd43d618b..6605af5b3cbee224c4cc2e4a93eef5e85e78a642 100644 --- a/src/routes/admin/mail/formatter.ts +++ b/src/routes/admin/mail/formatter.ts @@ -3,6 +3,7 @@ import MarkdownItHighlight from "markdown-it-highlightjs"; import hljs from "highlight.js"; import hljsCSS from "highlight.js/styles/github.css?inline"; import type { User } from "@prisma/client"; +import type { NewsNotificationData } from "$lib/notifications/types"; const md = new MarkdownIt({ breaks: true, @@ -16,6 +17,7 @@ const md = new MarkdownIt({ hljs }); md.core.ruler.push("replacePlaceholders", state=>{ + if(!state.env.user) return; for(const token of state.tokens){ handleToken(token, state.env.user); } @@ -40,15 +42,27 @@ type Mail = { html: string }; -export function format(subject: string, content: string, user: User): Mail { - const html = md.render(content, {user}); - const css = html.includes("hljs") ? `<style>${hljsCSS}</style>` : ""; - return { - to: { name: user.name, address: user.email }, - subject: replacePlaceholders(subject, user), - text: replacePlaceholders(content, user), - html: css + html - }; +export function format(subject: string, content: string, user: User): Mail; +export function format(subject: string, content: string): NewsNotificationData; +export function format(subject: string, content: string, user?: User) { + if(user){ + const html = md.render(content, {user}); + const css = html.includes("hljs") ? `<style>${hljsCSS}</style>` : ""; + return { + to: { name: user.name, address: user.email }, + subject: replacePlaceholders(subject, user), + text: replacePlaceholders(content, user), + html: css + html + }; + }else{ + const html = md.render(content); + const css = html.includes("hljs") ? `<style>${hljsCSS}</style>` : ""; + return { + title: subject, + textVersion: content, + htmlVersion: css + html + }; + } } function replacePlaceholders(content: string, user: User): string {