Files
instantadpay/db.js
T
martbost c5a846bd91 Keep the notifications worth interrupting for
Splitting system messages out of the modal was right for payout receipts,
but it would have silenced two that a member genuinely loses money by
ignoring:

  - "@someone is trying to buy. Link your wallet so it pays you" — a sale
    is blocked right now, and the referral is lost permanently once it
    routes to someone else.
  - "You missed 43 POL on InstantAdPay" — a payout passed them by, and the
    message explains exactly how to stop the next one doing the same.

So the dividing line is not system-versus-human, it is "does this need you
to do something". Those two become kind 'alert' and still interrupt; the
receipts stay kind 'notice' and stay in the inbox.

The modal no longer credits an alert to a person either. It was saying "A
message from @martbost" over machine-generated text, because system mail
is sent by member 1 at ADMIN_EMAIL. Alerts now read "Action needed on your
account".

Also reclassified "X is now in your line for good" as a notice — it is
good news about a referral the member gained, with nothing at stake.

qa/messages-notice.mjs now covers both lanes: 14 checks, including that an
alert interrupts and a flood of 25 receipts does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 08:25:08 -05:00

358 lines
19 KiB
JavaScript

// MySQL data layer (Marty: real concurrency over flat files).
// DATABASE_URL present -> mysql2 pool, schema bootstrap, one-time import of
// any existing volume JSON (accounts/sessions/
// campaigns) so nothing is lost on the switch.
// DATABASE_URL absent -> db.enabled() is false and the modules fall back to
// their JSON stores (local dev keeps working).
// The chain index stays a file: it is a rebuildable cache of the blockchain.
const fs = require('fs');
const path = require('path');
let pool = null;
let DATA_DIR = null;
function enabled() { return !!process.env.DATABASE_URL; }
async function init(opts) {
DATA_DIR = opts.dataDir;
if (!enabled()) return false;
const mysql = require('mysql2/promise');
pool = mysql.createPool(process.env.DATABASE_URL + '?connectionLimit=10&charset=utf8mb4');
await bootstrap();
await importJsonOnce();
console.log('db: MySQL connected, schema ready');
return true;
}
const q = async (sql, params) => (await pool.query(sql, params))[0];
async function bootstrap() {
await q(`CREATE TABLE IF NOT EXISTS accounts (
email VARCHAR(190) PRIMARY KEY,
pass VARCHAR(200) NULL,
sponsor_ref VARCHAR(32) NOT NULL DEFAULT '',
code VARCHAR(16) NOT NULL UNIQUE,
address VARCHAR(64) NULL UNIQUE,
created BIGINT NOT NULL,
last_seen BIGINT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS sessions (
token VARCHAR(80) PRIMARY KEY,
email VARCHAR(190) NULL,
address VARCHAR(64) NULL,
member_id INT NOT NULL DEFAULT 0,
expires BIGINT NOT NULL,
INDEX (expires)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS campaigns (
id INT AUTO_INCREMENT PRIMARY KEY,
owner_email VARCHAR(190) NOT NULL,
member_id INT NOT NULL,
type VARCHAR(12) NOT NULL,
name VARCHAR(80) NOT NULL,
target_url VARCHAR(500) NOT NULL,
image_url VARCHAR(500) NULL,
title VARCHAR(80) NULL,
body VARCHAR(200) NULL,
budget INT NOT NULL,
spent INT NOT NULL DEFAULT 0,
accrued INT NOT NULL DEFAULT 0,
imps INT NOT NULL DEFAULT 0,
clicks INT NOT NULL DEFAULT 0,
batch_imps INT NOT NULL DEFAULT 0,
status VARCHAR(12) NOT NULL DEFAULT 'active',
last_day_charged VARCHAR(10) NULL,
created BIGINT NOT NULL,
INDEX (owner_email), INDEX (type, status), INDEX (member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS earned_credits (
email VARCHAR(190) PRIMARY KEY,
balance INT NOT NULL DEFAULT 0,
granted_welcome TINYINT NOT NULL DEFAULT 0,
updated BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
const alterSafe0 = async sql => { try { await q(sql); } catch (e) { if (!['ER_DUP_FIELDNAME', 'ER_DUP_KEYNAME'].includes(e.code)) throw e; } };
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN milestones VARCHAR(255) NULL'); // comma-joined granted milestone keys
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN purchase_grade INT NOT NULL DEFAULT 0'); // part of the pool that is refunded/credited PURCHASED money: shows as purchased, funds login ads
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN login_day CHAR(10) NULL'); // last daily-login-bonus day
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN login_streak INT NOT NULL DEFAULT 0'); // consecutive-day streak
// additive columns (MySQL 8 has no IF NOT EXISTS for columns)
const alterSafe = async sql => {
try { await q(sql); }
catch (e) { if (!['ER_DUP_FIELDNAME', 'ER_DUP_KEYNAME'].includes(e.code)) throw e; }
};
await alterSafe('ALTER TABLE promo_codes ADD COLUMN funder VARCHAR(190) NULL'); // member-funded partner codes (Nexus), 2026-09-14
await alterSafe('ALTER TABLE accounts ADD COLUMN username VARCHAR(30) NULL');
await alterSafe('ALTER TABLE accounts ADD UNIQUE KEY uq_username (username)');
await alterSafe('ALTER TABLE accounts ADD COLUMN member_id INT NULL');
await alterSafe('ALTER TABLE accounts ADD KEY idx_member (member_id)');
await alterSafe('ALTER TABLE accounts ADD COLUMN avatar_url VARCHAR(500) NULL');
await alterSafe('ALTER TABLE accounts ADD COLUMN bio VARCHAR(600) NULL');
await alterSafe('ALTER TABLE accounts ADD COLUMN socials VARCHAR(1200) NULL'); // JSON {platform:url}
await alterSafe('ALTER TABLE accounts ADD COLUMN line_banner_url VARCHAR(500) NULL');
await alterSafe('ALTER TABLE accounts ADD COLUMN line_target_url VARCHAR(500) NULL');
await q(`CREATE TABLE IF NOT EXISTS daily_views (
email VARCHAR(190) NOT NULL,
day CHAR(10) NOT NULL,
views INT NOT NULL DEFAULT 0,
claimed TINYINT NOT NULL DEFAULT 0,
last_ts BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (email, day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await alterSafe('ALTER TABLE daily_views ADD COLUMN video_count INT NOT NULL DEFAULT 0'); // watch-to-earn videos/day
await q(`CREATE TABLE IF NOT EXISTS solo_inbox (
id INT AUTO_INCREMENT PRIMARY KEY,
campaign_id INT NOT NULL,
email VARCHAR(190) NOT NULL,
delivered BIGINT NOT NULL,
read_ts BIGINT NULL,
visited_ts BIGINT NULL,
rewarded TINYINT NOT NULL DEFAULT 0,
rewarded_day CHAR(10) NULL,
UNIQUE KEY uq_solo (campaign_id, email),
INDEX (email), INDEX (email, rewarded, rewarded_day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await alterSafe('ALTER TABLE solo_inbox ADD COLUMN visited_ts BIGINT NULL');
// solo message bodies are sanitized rich text; TEXT gives them room
await alterSafe('ALTER TABLE campaigns MODIFY body TEXT NULL');
await alterSafe('ALTER TABLE campaigns ADD COLUMN cta_label VARCHAR(40) NULL');
await alterSafe('ALTER TABLE campaigns ADD COLUMN last_shown_day VARCHAR(10) NULL'); // login ads: charge only days they were actually shown (2026-09-15)
await alterSafe('ALTER TABLE campaigns ADD COLUMN width INT NULL'); // banner size (IAB) → also NAS width
await alterSafe('ALTER TABLE campaigns ADD COLUMN height INT NULL'); // banner size (IAB) → also NAS height
await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_ad_id INT NULL'); // syndicated NAS sponsorads.ID
await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_served INT NOT NULL DEFAULT 0'); // NAS impressions already reconciled into spend
await alterSafe('ALTER TABLE campaigns ADD COLUMN drip_id INT NULL'); // syndicated DripOffers campaigns.id (2026-09-18)
await alterSafe('ALTER TABLE campaigns ADD COLUMN expires BIGINT NULL'); // featured rotation end time
await alterSafe('ALTER TABLE campaigns ADD COLUMN starts BIGINT NULL'); // featured run start (booked day); any type: scheduled start
await q(`CREATE TABLE IF NOT EXISTS camp_hours (
campaign_id INT NOT NULL, day CHAR(10) NOT NULL, hour TINYINT NOT NULL, n INT NOT NULL DEFAULT 0,
PRIMARY KEY (campaign_id, day, hour)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // on-site views per UTC hour, for the by-hour chart
await alterSafe('ALTER TABLE campaigns ADD COLUMN house TINYINT NOT NULL DEFAULT 0'); // admin house ad: free, never charged
await alterSafe('ALTER TABLE campaigns ADD COLUMN daily_cap INT NULL'); // optional credits/day pacing (banner + text)
await alterSafe('ALTER TABLE campaigns ADD COLUMN geo VARCHAR(16) NULL'); // country tiers the campaign shows to ('1,2'); NULL = everyone
await q(`CREATE TABLE IF NOT EXISTS camp_geo (
campaign_id INT NOT NULL, cc CHAR(2) NOT NULL, n INT NOT NULL DEFAULT 0,
PRIMARY KEY (campaign_id, cc)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // on-site serves per viewer country
await alterSafe('ALTER TABLE campaigns ADD COLUMN day_spent INT NOT NULL DEFAULT 0');
await alterSafe("ALTER TABLE campaigns ADD COLUMN day_key CHAR(10) NULL");
// linked positions: extra wallets owned by one email account (Qualified Start).
// Each is its own on-chain member sponsored by the account's main member.
await q(`CREATE TABLE IF NOT EXISTS positions (
address VARCHAR(64) PRIMARY KEY,
email VARCHAR(190) NOT NULL,
member_id INT NOT NULL DEFAULT 0,
created BIGINT NOT NULL,
INDEX (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS nudges (email VARCHAR(190) PRIMARY KEY, rung INT NOT NULL, ts BIGINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS digests (email VARCHAR(190) PRIMARY KEY, ts BIGINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS promo_codes (
code VARCHAR(24) NOT NULL PRIMARY KEY, credits INT NOT NULL, partner VARCHAR(80) NULL, note VARCHAR(200) NULL,
max_uses INT NOT NULL DEFAULT 0, expires BIGINT NOT NULL DEFAULT 0, active TINYINT NOT NULL DEFAULT 1, created BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // partner promo codes -> free ad credits
await q(`CREATE TABLE IF NOT EXISTS promo_redemptions (
code VARCHAR(24) NOT NULL, email VARCHAR(190) NOT NULL, credits INT NOT NULL, via VARCHAR(12) NOT NULL, ts BIGINT NOT NULL,
PRIMARY KEY (code, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS page_hits (
day CHAR(10) NOT NULL, host VARCHAR(80) NOT NULL, path VARCHAR(40) NOT NULL, n INT NOT NULL DEFAULT 0,
PRIMARY KEY (day, host, path)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // admin Traffic tab: public page views by referring domain
await q(`CREATE TABLE IF NOT EXISTS blog_posts (
slug VARCHAR(80) NOT NULL PRIMARY KEY, title VARCHAR(140) NOT NULL, excerpt VARCHAR(300) NULL, body MEDIUMTEXT NULL, cover VARCHAR(300) NULL,
tags VARCHAR(200) NULL, status VARCHAR(12) NOT NULL DEFAULT 'draft', author VARCHAR(80) NULL, created BIGINT NOT NULL, updated BIGINT NOT NULL,
published_at BIGINT NULL, views INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // public blog articles written in Admin > Blog
await q(`CREATE TABLE IF NOT EXISTS adoptions (
id INT AUTO_INCREMENT PRIMARY KEY,
adoptee VARCHAR(190) NOT NULL, adopter VARCHAR(190) NOT NULL,
ts BIGINT NOT NULL, expires BIGINT NOT NULL, status VARCHAR(12) NOT NULL DEFAULT 'open',
note VARCHAR(600) NULL, closed BIGINT NULL,
INDEX (adoptee), INDEX (adopter), INDEX (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // holding-tank adoptions
await q(`CREATE TABLE IF NOT EXISTS lead_marks (
\`lead\` VARCHAR(190) PRIMARY KEY, sponsor VARCHAR(190) NULL, contacted_ts BIGINT NULL, warned_ts BIGINT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); // dormant-lead rescue: manual "contacted" marks + warning sent
await q(`CREATE TABLE IF NOT EXISTS prospects (
id INT AUTO_INCREMENT PRIMARY KEY,
owner_email VARCHAR(190) NOT NULL,
name VARCHAR(120) NOT NULL,
contact VARCHAR(120) NULL,
status VARCHAR(20) NOT NULL DEFAULT 'new',
note VARCHAR(400) NULL,
next_ts BIGINT NULL,
created BIGINT NOT NULL,
updated BIGINT NOT NULL,
INDEX (owner_email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS pipeline_notes (
owner_email VARCHAR(190) NOT NULL,
person VARCHAR(190) NOT NULL,
note VARCHAR(1000) NULL,
follow_up BIGINT NULL,
tag VARCHAR(20) NULL,
updated BIGINT NOT NULL,
PRIMARY KEY (owner_email, person)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS join_views (
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(40) NOT NULL,
angle VARCHAR(20) NOT NULL DEFAULT '',
ts BIGINT NOT NULL,
INDEX (token, ts)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await alterSafe('ALTER TABLE join_views ADD COLUMN ref VARCHAR(80) NULL'); // referring domain
await q(`CREATE TABLE IF NOT EXISTS click_sources (
id INT AUTO_INCREMENT PRIMARY KEY,
campaign_id INT NOT NULL,
src VARCHAR(80) NOT NULL,
ts BIGINT NOT NULL,
INDEX (campaign_id, ts)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS visit_seen (
campaign_id INT NOT NULL,
email VARCHAR(190) NOT NULL,
day CHAR(10) NOT NULL,
ts BIGINT NOT NULL,
UNIQUE KEY uq_visit (campaign_id, email),
INDEX (email, day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS sponsor_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
from_member INT NOT NULL,
from_email VARCHAR(190) NOT NULL,
to_email VARCHAR(190) NOT NULL,
subject VARCHAR(160) NOT NULL,
body TEXT NULL,
sent BIGINT NOT NULL,
read_ts BIGINT NULL,
INDEX (to_email, read_ts), INDEX (from_email, sent)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
// sponsor CHAT (two-way) rides the same table: kind='chat' vs the default 'broadcast'
await alterSafe(`ALTER TABLE sponsor_messages ADD COLUMN kind VARCHAR(12) NOT NULL DEFAULT 'broadcast'`);
await alterSafe('ALTER TABLE sponsor_messages ADD INDEX idx_pair (to_email, from_email)');
// 2026-09-18: system notices used to be stored as 'broadcast', which is the kind the
// sign-in modal pops for — so a member with a backlog of payout notices got a fresh
// popup after every ad view. Retag the machine-generated ones so they stay in the inbox.
// Matched on subject, NOT on sender: these come from member 1 at ADMIN_EMAIL, which is
// also Marty's own member address, so his genuine broadcasts sit under the same sender.
// Idempotent — after the first pass nothing matches.
for (const like of ['You just got paid %POL on InstantAdPay',
'You missed %POL on InstantAdPay',
'%is trying to buy. %',
'You lost %payouts were not switched on%',
'% is now in your line for good%']) {
try { await q("UPDATE sponsor_messages SET kind='notice' WHERE kind='broadcast' AND subject LIKE ?", [like]); }
catch (e) {}
}
// ...but two of those DO need the member to act, and they lose money by not acting: a sale
// blocked or already lost because payouts are off, and a payout that passed them by. Those
// keep interrupting. The dividing line is "does this need you to do something", not
// "who sent it".
for (const like of ['You missed %POL on InstantAdPay',
'%is trying to buy. %',
'You lost %payouts were not switched on%']) {
try { await q("UPDATE sponsor_messages SET kind='alert' WHERE kind='notice' AND subject LIKE ?", [like]); }
catch (e) {}
}
// per-account chat settings: availability toggle (default on) + muted-member list (JSON emails)
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_available TINYINT NOT NULL DEFAULT 1');
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL');
await q(`CREATE TABLE IF NOT EXISTS video_seen (
email VARCHAR(190) NOT NULL,
campaign_id INT NOT NULL,
day CHAR(10) NOT NULL,
ts BIGINT NOT NULL,
UNIQUE KEY uq_vseen (email, campaign_id, day),
INDEX (email, day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS ad_reports (
id INT AUTO_INCREMENT PRIMARY KEY,
campaign_id INT NOT NULL,
reporter VARCHAR(190) NOT NULL DEFAULT '',
reason VARCHAR(20) NOT NULL,
note VARCHAR(600) NULL,
ts BIGINT NOT NULL,
resolved TINYINT NOT NULL DEFAULT 0,
INDEX (resolved, ts), INDEX (campaign_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS burns (
id VARCHAR(32) PRIMARY KEY,
member_id INT NOT NULL,
amount INT NOT NULL,
ref VARCHAR(64) NOT NULL,
ts BIGINT NOT NULL,
burned_tx VARCHAR(80) NULL,
burned_at BIGINT NULL,
INDEX (burned_tx)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
// credit activity ledger: one row per real credit movement (metered delivery rolls up per campaign per day)
await q(`CREATE TABLE IF NOT EXISTS credit_log (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL,
ts BIGINT NOT NULL,
day CHAR(10) NOT NULL,
delta INT NOT NULL,
kind VARCHAR(20) NOT NULL,
note VARCHAR(160) NOT NULL,
ref VARCHAR(40) NULL,
INDEX (email, ts), INDEX (email, kind, ref, day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
// follow-up email sequence queue (one row per free account)
await q(`CREATE TABLE IF NOT EXISTS drips (
email VARCHAR(190) PRIMARY KEY,
step INT NOT NULL DEFAULT 0,
next_at BIGINT NOT NULL,
started BIGINT NOT NULL,
stopped TINYINT NOT NULL DEFAULT 0,
ref VARCHAR(64) NULL,
angle VARCHAR(20) NULL,
INDEX (stopped, next_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await alterSafe('ALTER TABLE accounts ADD COLUMN joined_via VARCHAR(20) NULL'); // ?v= angle the lead came in on
await alterSafe('ALTER TABLE accounts ADD COLUMN joined_ref VARCHAR(80) NULL'); // referring domain of the first join-page visit
await alterSafe('ALTER TABLE accounts ADD COLUMN wall_offers VARCHAR(2000) NULL'); // JSON [{title,bannerUrl,targetUrl}] for wall positions 2-3 (unlock at 2 / 5 qualifying buyers)
}
// one-time import: only when the tables are empty and JSON files exist
async function importJsonOnce() {
const [{ n }] = await q('SELECT COUNT(*) n FROM accounts');
if (n > 0) return;
try {
const a = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'accounts.json'), 'utf8'));
for (const acc of Object.values(a.byEmail || {})) {
await q('INSERT IGNORE INTO accounts (email,pass,sponsor_ref,code,address,created) VALUES (?,?,?,?,?,?)',
[acc.email, acc.pass || null, String(acc.sponsorRef || acc.sponsorId || ''), acc.code, acc.address || null, acc.created || Date.now()]);
}
console.log('db: imported', Object.keys(a.byEmail || {}).length, 'account(s) from JSON');
} catch (e) {}
try {
const c = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'campaigns.json'), 'utf8'));
for (const cp of c.campaigns || []) {
await q(`INSERT IGNORE INTO campaigns (owner_email,member_id,type,name,target_url,image_url,title,body,
budget,spent,accrued,imps,clicks,batch_imps,status,last_day_charged,created)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
[cp.owner, cp.memberId, cp.type, cp.name, cp.targetUrl, cp.imageUrl || null, cp.title || null, cp.body || null,
cp.budget, cp.spent || 0, cp.accrued || 0, cp.imps || 0, cp.clicks || 0, cp.batchImps || 0,
cp.status, cp.lastDayCharged || null, cp.created || Date.now()]);
}
for (const b of c.burnsPending || []) {
await q('INSERT IGNORE INTO burns (id,member_id,amount,ref,ts,burned_tx,burned_at) VALUES (?,?,?,?,?,?,?)',
[b.id, b.memberId, b.amount, b.ref, b.ts, b.burnedTx || null, b.burnedAt || null]);
}
console.log('db: imported', (c.campaigns || []).length, 'campaign(s) from JSON');
} catch (e) {}
try {
const s = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'sessions.json'), 'utf8'));
for (const [tok, sess] of Object.entries(s)) {
if (sess.expires > Date.now()) {
await q('INSERT IGNORE INTO sessions (token,email,address,member_id,expires) VALUES (?,?,?,?,?)',
[tok, sess.email || null, sess.address || null, sess.memberId || 0, sess.expires]);
}
}
} catch (e) {}
}
module.exports = { init, enabled, q: (...a) => q(...a) };