Add user notification inbox and delivery controls
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { connect } from '@tursodatabase/database';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
if (!process.env.SMTP_HOST || !process.env.SMTP_FROM) throw new Error('SMTP delivery is not configured.');
|
||||
const database = await connect(resolve(process.env.TDR_DATABASE_PATH || './data/shd-build.db'));
|
||||
const transport = 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
|
||||
});
|
||||
const origin = (process.env.APP_ORIGIN || 'http://localhost:5173').replace(/\/+$/, '');
|
||||
const recipients = await (await database.prepare(`SELECT DISTINCT n.user_id,e.email
|
||||
FROM notifications n
|
||||
JOIN notification_preferences p ON p.user_id=n.user_id AND p.category=n.category AND p.email_frequency='daily'
|
||||
JOIN user_emails e ON e.user_id=n.user_id
|
||||
WHERE n.emailed_at IS NULL AND n.created_at>=p.updated_at`)).all();
|
||||
let sent = 0;
|
||||
for (const recipient of recipients) {
|
||||
const items = await (await database.prepare(`SELECT n.id,n.title,n.body,n.link FROM notifications n
|
||||
JOIN notification_preferences p ON p.user_id=n.user_id AND p.category=n.category AND p.email_frequency='daily'
|
||||
WHERE n.user_id=? AND n.emailed_at IS NULL AND n.created_at>=p.updated_at ORDER BY n.created_at LIMIT 100`)).all(recipient.user_id);
|
||||
if (!items.length) continue;
|
||||
const escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]);
|
||||
const rows = items.map((item) => `<div style="border-top:1px solid #34393a;padding:16px 0"><strong>${escape(item.title)}</strong><p style="color:#a5aaab">${escape(item.body)}</p>${item.link ? `<a style="color:#f6a821" href="${origin}${item.link}">VIEW INTELLIGENCE</a>` : ''}</div>`).join('');
|
||||
await transport.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
to: recipient.email,
|
||||
subject: `SHD.build daily notification summary · ${items.length} update${items.length === 1 ? '' : 's'}`,
|
||||
text: items.map((item) => `${item.title}\n${item.body}\n${item.link ? `${origin}${item.link}` : ''}`).join('\n\n'),
|
||||
html: `<div style="background:#0a0c0d;color:#ecebe5;padding:28px;font-family:Arial,sans-serif"><p style="color:#f6a821;font-size:12px;letter-spacing:2px">SHD DAILY INTELLIGENCE</p><h1>${items.length} notification${items.length === 1 ? '' : 's'}</h1>${rows}</div>`
|
||||
});
|
||||
const now = new Date().toISOString();
|
||||
for (const item of items) await (await database.prepare('UPDATE notifications SET emailed_at=? WHERE id=? AND emailed_at IS NULL')).run(now, item.id);
|
||||
sent += 1;
|
||||
}
|
||||
console.log(`Sent ${sent} daily notification digest${sent === 1 ? '' : 's'}.`);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { connect } from '@tursodatabase/database';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const targetName = process.argv[2] || 'Aewyn';
|
||||
const database = await connect(resolve(process.env.TDR_DATABASE_PATH || './data/shd-build.db'));
|
||||
const user = await (await database.prepare(`SELECT users.id,COALESCE(user_profiles.display_name,users.username) AS display_name
|
||||
FROM users LEFT JOIN user_profiles ON user_profiles.user_id=users.id
|
||||
WHERE users.login_username=? COLLATE NOCASE OR users.username=? COLLATE NOCASE OR user_profiles.display_name=? COLLATE NOCASE
|
||||
LIMIT 1`)).get(targetName, targetName, targetName);
|
||||
if (!user) throw new Error(`No user matched ${targetName}.`);
|
||||
const now = Date.now();
|
||||
const samples = [
|
||||
['isac', 'ISAC AVATAR OVERSIGHT', '[TEST] Custom avatar removed', 'Your custom avatar did not meet SHD.build image requirements and was removed. Oversight reason: The image contained unrelated promotional text.', '/account#avatar', 1],
|
||||
['contributions', 'ISAC DATA OVERSIGHT', '[TEST] Contribution approved', 'Quick Draw brand-set data was approved and deployed to the live builder registry.', '/admin/brands', 1],
|
||||
['contributions', 'ISAC DATA OVERSIGHT', '[TEST] Contribution needs revision', 'A talent update was not approved. Oversight reason: Tier II effect value does not match the supplied game evidence.', '/admin/talents', 1],
|
||||
['comments', 'SHD COMMUNITY', '[TEST] New comment on your build', 'An agent commented on “First Wave Vector Critical Build”: This setup worked really well in Legendary missions.', '/builds', 0]
|
||||
];
|
||||
for (const [index, sample] of samples.entries()) {
|
||||
await (await database.prepare(`INSERT INTO notifications
|
||||
(id,user_id,category,source,title,body,link,mandatory,created_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?)`)).run(randomUUID(), user.id, ...sample, new Date(now - index * 60_000).toISOString());
|
||||
}
|
||||
console.log(`Added ${samples.length} test notifications for ${user.display_name}.`);
|
||||
Reference in New Issue
Block a user