import { Job } from 'bullmq'; import { z } from 'zod'; import { renderBackupFailedEmail, renderBackupSuccessEmail, renderServerReadyEmail } from '@hexahost/email-templates'; import { getSmtpConfig } from '../config'; import { sendEmail } from '../email'; import { logger } from '../logger'; const dispatchJobSchema = z.object({ userId: z.string().uuid(), type: z.string(), channel: z.enum(['EMAIL', 'WEBHOOK', 'DISCORD']), title: z.string(), body: z.string(), metadata: z.record(z.unknown()).optional(), }); async function postWebhook(url: string, payload: Record): Promise { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Webhook delivery failed: ${response.status}`); } } async function postDiscord(webhookUrl: string, content: string): Promise { const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: content.slice(0, 1900) }), }); if (!response.ok) { throw new Error(`Discord delivery failed: ${response.status}`); } } export async function processNotificationDispatchJob(job: Job): Promise<{ delivered: boolean }> { if (job.name !== 'dispatch-notification') { return { delivered: false }; } const payload = dispatchJobSchema.parse(job.data); const config = getSmtpConfig(); const locale = payload.metadata?.['locale'] === 'de' ? 'de' : 'en'; const serverName = String(payload.metadata?.['serverName'] ?? 'Server'); if (payload.channel === 'EMAIL') { const userEmail = payload.metadata?.['email']; if (typeof userEmail !== 'string') { logger.warn({ jobId: job.id }, 'Email notification missing recipient'); return { delivered: false }; } let subject = payload.title; let html = `

${payload.body}

`; if (payload.type === 'SERVER_READY') { const rendered = renderServerReadyEmail({ appName: config.APP_NAME, serverName, locale }); subject = rendered.subject; html = rendered.html; } else if (payload.type === 'BACKUP_SUCCESS') { const rendered = renderBackupSuccessEmail({ appName: config.APP_NAME, serverName, locale }); subject = rendered.subject; html = rendered.html; } else if (payload.type === 'BACKUP_FAILED') { const rendered = renderBackupFailedEmail({ appName: config.APP_NAME, serverName, locale }); subject = rendered.subject; html = rendered.html; } await sendEmail({ to: userEmail, subject, html }); return { delivered: true }; } if (payload.channel === 'WEBHOOK') { const url = process.env['NOTIFICATION_WEBHOOK_URL']; if (!url) { logger.warn('NOTIFICATION_WEBHOOK_URL not configured'); return { delivered: false }; } await postWebhook(url, { userId: payload.userId, type: payload.type, title: payload.title, body: payload.body, metadata: payload.metadata ?? {}, }); return { delivered: true }; } if (payload.channel === 'DISCORD') { const url = process.env['NOTIFICATION_DISCORD_WEBHOOK_URL']; if (!url) { logger.warn('NOTIFICATION_DISCORD_WEBHOOK_URL not configured'); return { delivered: false }; } await postDiscord(url, `**${payload.title}**\n${payload.body}`); return { delivered: true }; } return { delivered: false }; }