57bba0111a
- nas.js: direct-MySQL writer mirroring CTB's proven payments+sponsorads path (approved:1, pid 1/2, catid 5, remaining counts DOWN) - Create-time push for banner/text; pause/resume mirrors to NAS deactivate/topUp - reconcileNas() 5-min loop: reads served, charges delta credits to the shared budget, stops NAS ad when budget dry - Campaign row shows IAP + '+N net' network views (differentiated stats) - Inert unless NAS_DB_* env is set; zero prod impact until enabled - Also: rename Inbox sub-tab to 'Inbox Ads' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
5.4 KiB
JavaScript
115 lines
5.4 KiB
JavaScript
// NAS syndication — pushes IAP campaigns out to Network Ad Space (Marty's own
|
|
// EvolutionScript platform; no source, so its MySQL DB IS the API). Mirrors the
|
|
// CTB Rewards direct-write pattern: INSERT payments then sponsorads with
|
|
// approved:1 (bypasses NAS moderation), and reads `remaining` back to reconcile
|
|
// spend into IAP's unified credit pool.
|
|
//
|
|
// KNOWN NAS TRUTHS (measured across RM Circle + CTB, 1600+ ads):
|
|
// - sponsorads.remaining counts DOWN: served = assigned - remaining.
|
|
// - pid 1 = text, pid 2 = banner ONLY; banner size lives in width/height.
|
|
// - hits = clicks (not impressions). catid 5 = Cryptocurrencies.
|
|
// - NAS drifts its own counters upward post-insert → clamp served to assigned.
|
|
//
|
|
// FEATURE-FLAGGED: inert unless NAS_DB_HOST/USER/PASSWORD/NAME are all set.
|
|
// Nothing here runs (no connection, no writes) when the flag is off.
|
|
const crypto = require('crypto');
|
|
|
|
let pool = null;
|
|
function enabled() {
|
|
return !!(process.env.NAS_DB_HOST && process.env.NAS_DB_USER
|
|
&& process.env.NAS_DB_PASSWORD && process.env.NAS_DB_NAME);
|
|
}
|
|
function nasPool() {
|
|
if (pool) return pool;
|
|
const mysql = require('mysql2/promise');
|
|
pool = mysql.createPool({
|
|
host: process.env.NAS_DB_HOST,
|
|
port: Number(process.env.NAS_DB_PORT || 3306),
|
|
user: process.env.NAS_DB_USER,
|
|
password: process.env.NAS_DB_PASSWORD,
|
|
database: process.env.NAS_DB_NAME,
|
|
charset: 'latin1', // EvolutionScript is latin1
|
|
connectionLimit: 3,
|
|
connectTimeout: 10000
|
|
});
|
|
return pool;
|
|
}
|
|
async function q(sql, args) { const [r] = await nasPool().query(sql, args); return r; }
|
|
|
|
const CATID_CRYPTO = Number(process.env.NAS_CATID || 5);
|
|
// how many NAS impressions one IAP credit buys, per format. IAP charges credits
|
|
// on its own surfaces at these same rates, so NAS delivery draws the same pool.
|
|
function impressionsPerCredit(type) {
|
|
return type === 'banner' ? 5 : type === 'text' ? 10 : type === 'video' ? 0 : 0;
|
|
}
|
|
function nasKind(type) {
|
|
if (type === 'banner') return { pid: 2, adtype: 'banner' };
|
|
if (type === 'text') return { pid: 1, adtype: 'text' };
|
|
return null; // only banner/text syndicate to NAS in v1 (login/solo/video are IAP-native)
|
|
}
|
|
|
|
// push one IAP campaign into NAS. `c` is a pubC-shaped campaign. Returns
|
|
// { nasAdId, assigned } or { skipped } / throws on a real DB error.
|
|
async function pushCampaign(c, opts = {}) {
|
|
if (!enabled()) return { skipped: 'flag-off' };
|
|
const kind = nasKind(c.type);
|
|
if (!kind) return { skipped: 'type' };
|
|
const budgetLeft = c.budget - (c.spent || 0);
|
|
const assigned = Math.max(1, Math.floor(budgetLeft * impressionsPerCredit(c.type)));
|
|
const token = 'iap_' + crypto.randomBytes(8).toString('hex'); // manage token = Username
|
|
const now = new Date();
|
|
const days = Number(opts.days || 30);
|
|
const edate = new Date(now.getTime() + days * 86400000);
|
|
const payref = 'iap_campaign:' + c.id;
|
|
// payments first (MyISAM, no txn) — hand-rollback the row if sponsorads fails
|
|
const pay = await q(
|
|
'INSERT INTO payments (Username, Amount, Currency_code, status, Date, pay_address) VALUES (?,?,?,1,?,?)',
|
|
[token, 0, 'IAP_CREDIT', now, payref]);
|
|
try {
|
|
const ad = await q(
|
|
`INSERT INTO sponsorads
|
|
(Username, Subject, Body, WebsiteURL, assigned, remaining, hits, approved, Date, adtype,
|
|
Name, Email, PaymentDetails, EDate, sp, width, height, BannerURL, pid, ref_by, catid)
|
|
VALUES (?,?,?,?,?,?,0,1,?,?,?,?,?,?,'',?,?,?,?,0,?)`,
|
|
[token, c.title || null, c.type === 'text' ? (c.body || null) : null, c.targetUrl,
|
|
assigned, assigned, now, kind.adtype,
|
|
opts.name || 'InstantAdPay member', opts.email || 'ads@instantadpay.com',
|
|
'InstantAdPay campaign #' + c.id, edate,
|
|
c.width || '', c.height || '', c.type === 'banner' ? c.imageUrl : null, kind.pid, CATID_CRYPTO]);
|
|
return { nasAdId: ad.insertId, manageToken: token, assigned };
|
|
} catch (e) {
|
|
try { await q('DELETE FROM payments WHERE ID=?', [pay.insertId]); } catch (e2) {}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// read served count for a syndicated ad (assigned - remaining, clamped ≥0 and
|
|
// ≤ assigned because NAS drifts counters upward post-insert).
|
|
async function readServed(nasAdId) {
|
|
if (!enabled()) return null;
|
|
const rows = await q('SELECT assigned, remaining, hits FROM sponsorads WHERE ID=?', [Number(nasAdId)]);
|
|
if (!rows.length) return null;
|
|
const assigned = Number(rows[0].assigned) || 0;
|
|
const remaining = Number(rows[0].remaining) || 0;
|
|
const served = Math.max(0, Math.min(assigned, assigned - remaining));
|
|
return { assigned, remaining, served, clicks: Number(rows[0].hits) || 0 };
|
|
}
|
|
|
|
// stop a syndicated ad (budget spent, paused, or expired) — remaining:0 halts serving.
|
|
async function deactivate(nasAdId) {
|
|
if (!enabled()) return { skipped: 'flag-off' };
|
|
await q('UPDATE sponsorads SET remaining=0, EDate=NOW() WHERE ID=?', [Number(nasAdId)]);
|
|
return { ok: true };
|
|
}
|
|
|
|
// top up a syndicated ad with more impressions (buy-more-views / reactivate).
|
|
async function topUp(nasAdId, addImpressions, days) {
|
|
if (!enabled()) return { skipped: 'flag-off' };
|
|
await q(`UPDATE sponsorads SET assigned=assigned+?, remaining=remaining+?, approved=1,
|
|
EDate=DATE_ADD(NOW(), INTERVAL ? DAY) WHERE ID=?`,
|
|
[Number(addImpressions), Number(addImpressions), Number(days || 30), Number(nasAdId)]);
|
|
return { ok: true };
|
|
}
|
|
|
|
module.exports = { enabled, pushCampaign, readServed, deactivate, topUp, impressionsPerCredit, nasKind };
|