Member updates: audience defaults to newsletter opt-ins (Sendy list status), explicit 'everyone' option; sendy.status()

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-14 13:47:13 -05:00
parent 7bb956e896
commit 024170e7f8
5 changed files with 29 additions and 8 deletions
+12 -4
View File
@@ -4,21 +4,29 @@
// Log: DATA_DIR/updates-log.json. Nothing here sends on its own: every send is a click in Admin > Releases.
const fs = require('fs');
const path = require('path');
let R = null; // { dataDir, accounts, releases, mailer, drip, adminEmail }
let R = null; // { dataDir, accounts, releases, mailer, drip, sendy, adminEmail }
const SITE = 'https://instantadpay.com';
const FILE = () => path.join(R.dataDir, 'updates-log.json');
function log() { try { return JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) { return { sends: [] }; } }
function saveLog(l) { try { fs.writeFileSync(FILE(), JSON.stringify(l)); } catch (e) {} }
function init(refs) { R = refs; }
const AUDIENCES = { active30: 'Active in the last 30 days', active90: 'Active in the last 90 days', all: 'Every member' };
const AUDIENCES = { optin: 'Newsletter opt-ins (ticked the box at sign-up)', optin30: 'Newsletter opt-ins active in the last 30 days', all: 'Every member, including those who declined the newsletter' };
// the sign-up checkbox subscribes the member to the Sendy newsletter list, so Sendy is the record of who opted in
const optinCache = new Map(); // email -> { t, v }
async function optedIn(email) {
const c = optinCache.get(email); if (c && Date.now() - c.t < 6 * 3600000) return c.v;
const st = R.sendy ? await R.sendy.status(email) : ''; const v = st === 'Subscribed';
optinCache.set(email, { t: Date.now(), v }); return v;
}
async function recipients(kind) {
const all = await R.accounts.listAll(20000);
const now = Date.now(); const days = kind === 'active30' ? 30 : kind === 'active90' ? 90 : 0;
const now = Date.now(); const days = kind === 'optin30' ? 30 : 0; const needOptin = kind !== 'all';
const out = [];
for (const a of all) {
if (!a.email || /@(demo|example)\./i.test(a.email)) continue;
if (days && now - Math.max(a.lastSeen || 0, a.created || 0) > days * 86400000) continue;
if (needOptin && !(await optedIn(a.email))) continue;
out.push(a);
}
return out;
@@ -50,7 +58,7 @@ async function send(input, opts) {
return { ok: true, test: true, to };
}
if (running) return { error: 'A send is already running (' + running.sent + ' of ' + running.total + '). Wait for it to finish.' };
const kind = AUDIENCES[input.audience] ? input.audience : 'active30';
const kind = AUDIENCES[input.audience] ? input.audience : 'optin';
const list = await recipients(kind);
if (!list.length) return { error: 'Nobody in that audience.' };
const rec = { id: Date.now().toString(36), ts: Date.now(), subject: compose(input, null).subject, noteIds, audience: kind, total: list.length, sent: 0, skipped: 0, failed: 0, status: 'running' };