Files
HexaHost-GameCloud/apps/worker/src/handlers/notification-dispatch.ts

107 lines
3.4 KiB
TypeScript

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<string, unknown>): Promise<void> {
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<void> {
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 = `<p>${payload.body}</p>`;
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 };
}