107 lines
3.4 KiB
TypeScript
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 };
|
|
}
|