103 lines
3.8 KiB
JavaScript
103 lines
3.8 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 * FROM oversight_notification_queue
|
|
WHERE delivered_at IS NULL AND deliver_after<=? ORDER BY user_id,oversight_category,created_at`)
|
|
).all(now);
|
|
const groups = new Map();
|
|
for (const item of due) {
|
|
const key = `${item.user_id}:${item.oversight_category}:${item.frequency}:${item.deliver_after}`;
|
|
if (!groups.has(key)) groups.set(key, []);
|
|
groups.get(key).push(item);
|
|
}
|
|
|
|
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(
|
|
/\/+$/,
|
|
'',
|
|
);
|
|
let delivered = 0;
|
|
|
|
for (const items of groups.values()) {
|
|
const first = items[0];
|
|
const categoryLabel =
|
|
first.oversight_category === 'avatars' ? 'avatar' : 'data change';
|
|
const title = `${items.length} ${categoryLabel} review${items.length === 1 ? '' : 's'} awaiting Oversight`;
|
|
const details = items
|
|
.slice(0, 10)
|
|
.map((item) => `• ${item.summary}`)
|
|
.join('\n');
|
|
const remainder =
|
|
items.length > 10 ? `\n• ${items.length - 10} additional review items` : '';
|
|
const body = `${details}${remainder}`;
|
|
const notificationId = randomUUID();
|
|
await (
|
|
await database.prepare(`INSERT INTO notifications
|
|
(id,user_id,category,source,title,body,link,mandatory,created_at)
|
|
VALUES(?,?,'isac','ISAC OVERSIGHT NETWORK',?,?,?,1,?)`)
|
|
).run(notificationId, first.user_id, title, body, first.link, now);
|
|
for (const item of items) {
|
|
await (
|
|
await database.prepare(
|
|
'UPDATE oversight_notification_queue SET delivered_at=? WHERE id=? AND delivered_at IS NULL',
|
|
)
|
|
).run(now, item.id);
|
|
}
|
|
const preference = await (
|
|
await database.prepare(
|
|
"SELECT email_frequency FROM notification_preferences WHERE user_id=? AND category='isac'",
|
|
)
|
|
).get(first.user_id);
|
|
if (transport && preference?.email_frequency === 'immediate') {
|
|
const identity = await (
|
|
await database.prepare('SELECT email FROM user_emails WHERE user_id=?')
|
|
).get(first.user_id);
|
|
if (typeof identity?.email === 'string') {
|
|
try {
|
|
await transport.sendMail({
|
|
from: process.env.SMTP_FROM,
|
|
to: identity.email,
|
|
subject: `SHD.build · ${title}`,
|
|
text: `${body}\n\n${origin}${first.link}`,
|
|
html: `<div style="background:#0a0c0d;color:#ecebe5;padding:28px;font-family:Arial,sans-serif"><p style="color:#f6a821;font-size:12px;letter-spacing:2px">ISAC OVERSIGHT NETWORK</p><h1>${title}</h1><p style="white-space:pre-line;color:#a5aaab">${body}</p><a style="color:#f6a821" href="${origin}${first.link}">OPEN OVERSIGHT QUEUE</a></div>`,
|
|
});
|
|
await (
|
|
await database.prepare(
|
|
'UPDATE notifications SET emailed_at=? WHERE id=?',
|
|
)
|
|
).run(now, notificationId);
|
|
} catch (error) {
|
|
console.error(
|
|
`Unable to email Oversight digest ${notificationId}`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
delivered += items.length;
|
|
}
|
|
|
|
console.log(
|
|
`Delivered ${delivered} queued Oversight review alert${delivered === 1 ? '' : 's'} in ${groups.size} digest${groups.size === 1 ? '' : 's'}.`,
|
|
);
|