Add automated ISAC system notifications

This commit is contained in:
JB
2026-07-26 02:08:34 -04:00
parent 551a5c29a1
commit 32e10c710b
20 changed files with 392 additions and 11 deletions
+70
View File
@@ -0,0 +1,70 @@
import { randomUUID } from 'node:crypto';
import { getDatabase } from '$lib/server/database';
import { createNotification } from '$lib/server/notifications';
export const AUTOMATED_MESSAGE_EVENTS = [
{ value: 'account.created', label: 'ACCOUNT CREATED', description: 'After a new SHD.build account is created' }
] as const;
export type AutomatedMessageEvent = (typeof AUTOMATED_MESSAGE_EVENTS)[number]['value'];
type MessageRow = Record<string, unknown>;
function parseActions(value: unknown): Array<{ label: string; href: string }> {
try {
const parsed = JSON.parse(String(value ?? '[]'));
if (!Array.isArray(parsed)) return [];
return parsed
.filter((item) => item && typeof item.label === 'string' && typeof item.href === 'string')
.map((item) => ({ label: item.label.slice(0, 60), href: item.href.slice(0, 500) }))
.slice(0, 8);
} catch {
return [];
}
}
async function deliver(message: MessageRow, deliveryId: string, userId: string) {
const notificationId = await createNotification({
userId,
category: 'isac',
source: String(message.source),
title: String(message.title),
body: String(message.body),
link: typeof message.link === 'string' ? message.link : null,
actions: parseActions(message.actions_json),
mandatory: true
});
if (!notificationId) return;
const database = await getDatabase();
await (await database.prepare(`UPDATE automated_system_message_deliveries
SET delivered_at=?,notification_id=? WHERE id=? AND delivered_at IS NULL`))
.run(new Date().toISOString(), notificationId, deliveryId);
}
export async function triggerAutomatedSystemMessages(input: {
eventKey: AutomatedMessageEvent;
userId: string;
eventId: string;
}) {
const database = await getDatabase();
const messages = await (await database.prepare(
'SELECT * FROM automated_system_messages WHERE event_key=? AND enabled=1 ORDER BY name'
)).all(input.eventKey);
const now = new Date();
for (const message of messages) {
const existing = await (await database.prepare(`SELECT id FROM automated_system_message_deliveries
WHERE message_id=? AND user_id=? AND event_key=? AND event_id=?`))
.get(message.id, input.userId, input.eventKey, input.eventId);
if (existing) continue;
const deliveryId = randomUUID();
const scheduledFor = new Date(now.getTime() + Number(message.send_delay_minutes ?? 0) * 60_000);
await (await database.prepare(`INSERT OR IGNORE INTO automated_system_message_deliveries
(id,message_id,user_id,event_key,event_id,scheduled_for,created_at)
VALUES(?,?,?,?,?,?,?)`)).run(
deliveryId, message.id, input.userId, input.eventKey, input.eventId,
scheduledFor.toISOString(), now.toISOString()
);
if (Number(message.send_delay_minutes ?? 0) === 0) {
await deliver(message, deliveryId, input.userId);
}
}
}
+6 -3
View File
@@ -12,6 +12,7 @@ export async function createNotification(input: {
title: string;
body: string;
link?: string | null;
actions?: Array<{ label: string; href: string }>;
mandatory?: boolean;
}) {
const database = await getDatabase();
@@ -26,10 +27,12 @@ export async function createNotification(input: {
const id = randomUUID();
const now = new Date().toISOString();
await (await database.prepare(`INSERT INTO notifications
(id,user_id,category,source,title,body,link,mandatory,created_at)
VALUES(?,?,?,?,?,?,?,?,?)`)).run(
(id,user_id,category,source,title,body,link,actions_json,mandatory,created_at)
VALUES(?,?,?,?,?,?,?,?,?,?)`)).run(
id, input.userId, input.category, input.source, input.title.slice(0, 140),
input.body.slice(0, 2000), input.link ?? null, mandatory ? 1 : 0, now
input.body.slice(0, 2000), input.link ?? null,
input.actions?.length ? JSON.stringify(input.actions.slice(0, 8)) : null,
mandatory ? 1 : 0, now
);
if (emailFrequency === 'immediate') {
const identity = await (await database.prepare('SELECT email FROM user_emails WHERE user_id=?')).get(input.userId);