Files
shd.build/scripts/system-notification-dispatch.mjs
jb dd84dff055
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s
feat: add community publishing and news syndication
2026-08-01 21:42:09 -04:00

105 lines
3.7 KiB
JavaScript

import { connect } from '@tursodatabase/database';
import nodemailer from 'nodemailer';
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
const database = await connect(
resolve(process.env.TDR_DATABASE_PATH || './data/shd-build.db'),
);
const now = new Date().toISOString();
const due = await (
await database.prepare(`SELECT delivery.id AS delivery_id,delivery.user_id,message.*
FROM automated_system_message_deliveries delivery
JOIN automated_system_messages message ON message.id=delivery.message_id
WHERE delivery.delivered_at IS NULL AND delivery.scheduled_for<=? AND message.enabled=1
ORDER BY delivery.scheduled_for LIMIT 250`)
).all(now);
const smtpConfigured = Boolean(process.env.SMTP_HOST && process.env.SMTP_FROM);
const transport = smtpConfigured
? nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT || 587),
secure: process.env.SMTP_SECURE === 'true',
auth: process.env.SMTP_USER
? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD }
: undefined,
requireTLS: process.env.SMTP_SECURE !== 'true',
disableFileAccess: true,
disableUrlAccess: true,
})
: null;
const origin = (process.env.APP_ORIGIN || 'http://localhost:5173').replace(
/\/+$/,
'',
);
const escapeHtml = (value) =>
String(value).replace(
/[&<>"']/g,
(character) =>
({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
})[character],
);
let delivered = 0;
for (const message of due) {
const notificationId = randomUUID();
await (
await database.prepare(`INSERT INTO notifications
(id,user_id,category,source,title,body,link,actions_json,mandatory,created_at)
VALUES(?,?,'isac',?,?,?,?,?,1,?)`)
).run(
notificationId,
message.user_id,
message.source,
message.title,
message.body,
message.link ?? null,
message.actions_json ?? null,
now,
);
await (
await database.prepare(`UPDATE automated_system_message_deliveries
SET delivered_at=?,notification_id=? WHERE id=? AND delivered_at IS NULL`)
).run(now, notificationId, message.delivery_id);
const preference = await (
await database.prepare(
"SELECT email_frequency FROM notification_preferences WHERE user_id=? AND category='isac'",
)
).get(message.user_id);
if (transport && preference?.email_frequency === 'immediate') {
const identity = await (
await database.prepare('SELECT email FROM user_emails WHERE user_id=?')
).get(message.user_id);
if (typeof identity?.email === 'string') {
try {
await transport.sendMail({
from: process.env.SMTP_FROM,
to: identity.email,
subject: `SHD.build · ${message.title}`,
text: `${message.body}\n\n${message.link ? `${origin}${message.link}` : origin}`,
html: `<div style="background:#0a0c0d;color:#ecebe5;padding:28px;font-family:Arial,sans-serif"><p style="color:#f6a821;font-size:12px;letter-spacing:2px">${escapeHtml(message.source)}</p><h1>${escapeHtml(message.title)}</h1><p style="color:#a5aaab">${escapeHtml(message.body)}</p>${message.link ? `<a style="color:#f6a821" href="${escapeHtml(`${origin}${message.link}`)}">OPEN SHD.BUILD</a>` : ''}</div>`,
});
await (
await database.prepare(
'UPDATE notifications SET emailed_at=? WHERE id=?',
)
).run(now, notificationId);
} catch (error) {
console.error(
`Unable to email automated notification ${notificationId}`,
error,
);
}
}
}
delivered += 1;
}
console.log(
`Delivered ${delivered} automated ISAC system notification${delivered === 1 ? '' : 's'}.`,
);