Sponsor to downline messaging: lineage view, daily broadcast (email+inbox), login modal
- Downline resolver (3 levels); email visible for directs only, username+ID deeper - Broadcast composer (rich editor) to directs or full downline, 1/day cap, sends email + on-site inbox - Upline messages inbox in My line; unmissable sign-in modal for unread, ack marks read - messages.js dual-mode store; sponsor_messages table; sanitizeRich reused Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+96
@@ -0,0 +1,96 @@
|
||||
// Sponsor → downline messages: team comms delivered to members' on-site inbox
|
||||
// (and by email). Distinct from solo ADS (which are paid and earn credits).
|
||||
// Dual-mode: MySQL when DATABASE_URL is set, else a JSON file in the volume.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('./db');
|
||||
|
||||
let DATA_DIR = null;
|
||||
const J = {
|
||||
db: null,
|
||||
FILE: () => path.join(DATA_DIR, 'sponsor-messages.json'),
|
||||
load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = { nextId: 1, items: [] }; } },
|
||||
save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }
|
||||
};
|
||||
function init(opts) { DATA_DIR = opts.dataDir; }
|
||||
const DAY = 86400000;
|
||||
|
||||
// has this sponsor already broadcast within the last 24h? (1/day cap)
|
||||
async function lastBroadcastAt(fromEmail) {
|
||||
const e = String(fromEmail || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
const r = await db.q('SELECT MAX(sent) m FROM sponsor_messages WHERE from_email=?', [e]);
|
||||
return r.length && r[0].m ? Number(r[0].m) : 0;
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
const mine = J.db.items.filter(i => i.fromEmail === e).map(i => i.sent);
|
||||
return mine.length ? Math.max(...mine) : 0;
|
||||
}
|
||||
// deliver one message to many recipients (already-resolved emails). Returns count.
|
||||
async function deliver(fromMember, fromEmail, recipients, subject, body) {
|
||||
const now = Date.now();
|
||||
let n = 0;
|
||||
if (db.enabled()) {
|
||||
for (const to of recipients) {
|
||||
await db.q('INSERT INTO sponsor_messages (from_member,from_email,to_email,subject,body,sent) VALUES (?,?,?,?,?,?)',
|
||||
[fromMember || 0, fromEmail, to, subject, body, now]);
|
||||
n++;
|
||||
}
|
||||
} else {
|
||||
if (!J.db) J.load();
|
||||
for (const to of recipients) {
|
||||
J.db.items.push({ id: J.db.nextId++, fromMember: fromMember || 0, fromEmail, toEmail: to, subject, body, sent: now, readTs: 0 });
|
||||
n++;
|
||||
}
|
||||
J.save();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
async function inbox(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
const rows = await db.q(`SELECT id, from_member, subject, body, sent, read_ts FROM sponsor_messages
|
||||
WHERE to_email=? ORDER BY sent DESC LIMIT 100`, [e]);
|
||||
return rows.map(r => ({ id: r.id, fromMember: r.from_member, subject: r.subject, body: r.body,
|
||||
sent: Number(r.sent), read: !!r.read_ts }));
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
return J.db.items.filter(i => i.toEmail === e).sort((a, b) => b.sent - a.sent).slice(0, 100)
|
||||
.map(i => ({ id: i.id, fromMember: i.fromMember, subject: i.subject, body: i.body, sent: i.sent, read: !!i.readTs }));
|
||||
}
|
||||
async function unreadCount(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
const r = await db.q('SELECT COUNT(*) n FROM sponsor_messages WHERE to_email=? AND read_ts IS NULL', [e]);
|
||||
return r[0].n;
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
return J.db.items.filter(i => i.toEmail === e && !i.readTs).length;
|
||||
}
|
||||
// the newest unread message (for the login modal)
|
||||
async function newestUnread(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
const rows = await db.q(`SELECT id, from_member, subject, body, sent FROM sponsor_messages
|
||||
WHERE to_email=? AND read_ts IS NULL ORDER BY sent DESC LIMIT 1`, [e]);
|
||||
if (!rows.length) return null;
|
||||
const r = rows[0];
|
||||
return { id: r.id, fromMember: r.from_member, subject: r.subject, body: r.body, sent: Number(r.sent) };
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
const u = J.db.items.filter(i => i.toEmail === e && !i.readTs).sort((a, b) => b.sent - a.sent)[0];
|
||||
return u ? { id: u.id, fromMember: u.fromMember, subject: u.subject, body: u.body, sent: u.sent } : null;
|
||||
}
|
||||
async function markRead(email, id) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
await db.q('UPDATE sponsor_messages SET read_ts=? WHERE id=? AND to_email=? AND read_ts IS NULL', [Date.now(), Number(id), e]);
|
||||
return { ok: true };
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
const i = J.db.items.find(x => x.id === Number(id) && x.toEmail === e);
|
||||
if (i && !i.readTs) { i.readTs = Date.now(); J.save(); }
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
module.exports = { init, lastBroadcastAt, deliver, inbox, unreadCount, newestUnread, markRead };
|
||||
Reference in New Issue
Block a user