Achievement badge image rendered on the server with ffmpeg (a phone canvas sent a blank card); admin re-post route
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ const updates = require('./updates');
|
||||
const audit = require('./audit'); // counter audit: views vs delivery logs, charges vs shows (Marty, 2026-09-15) // member update emails from Admin > Releases (Marty, 2026-09-14)
|
||||
const leaderboard = require('./leaderboard');
|
||||
const toolkit = require('./toolkit');
|
||||
const badge = require('./badge'); // achievement badge image drawn on the server with ffmpeg (2026-09-16)
|
||||
const snapshot = require('./snapshot'); // daily growth snapshot -> Telegram payments feed (Marty, 2026-09-15)
|
||||
const pipeline = require('./pipeline'); // sponsor follow-up board (coming soon until site setting pipelineMode = on) (Marty, 2026-09-15)
|
||||
const videomaker = require('./videomaker'); // Circuit tool: promo videos with the member's own end card (ffmpeg in the image) // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
||||
@@ -372,6 +373,7 @@ async function boot() {
|
||||
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
|
||||
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer, ads, tank, lb: () => leaderboard, siteConfig });
|
||||
pipeline.init({ dataDir: DATA_DIR, accounts, coach, chain });
|
||||
badge.init({ publicDir: PUBLIC_DIR });
|
||||
snapshot.init({ dataDir: DATA_DIR, db, chain, siteConfig, send: async (c, t, th) => { await telegramSend(c, t, th); return true; } });
|
||||
setInterval(() => snapshot.tick().catch(e => console.error('snapshot', e.message)), 10 * 60 * 1000);
|
||||
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
|
||||
@@ -1548,6 +1550,8 @@ const server = http.createServer(async (req, res) => {
|
||||
const link = a && a.username ? 'instantadpay.com/join/' + a.username : 'instantadpay.com';
|
||||
const [label, sub] = BADGE_META[key];
|
||||
const caption = '\u{1F3C6} <b>InstantAdPay</b> \u00b7 <b>' + who.replace(/[<>&]/g, '') + '</b> unlocked <b>' + label + '</b>: ' + sub + '\n' + link;
|
||||
// the server draws the card (a phone's canvas can come out blank: cryptomonk, 2026-09-16); the upload is the fallback
|
||||
try { const srv = await badge.render(key, a && a.username ? a.username : (a && a.memberId ? 'member #' + a.memberId : '')); if (srv) jpeg = srv; } catch (e) {}
|
||||
const sc = siteConfig(); let sent = 0;
|
||||
if (sc.telegramBotToken && sc.telegramEchoChatId) {
|
||||
if (await telegramSendPhoto(sc.telegramEchoChatId, jpeg, caption, sc.telegramEchoTopicId)) sent++; // payments topic
|
||||
@@ -1569,9 +1573,27 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!(await ads.milestonesOf(s.email)).includes(key)) return json(res, 400, { error: 'You have not unlocked that badge yet.' });
|
||||
const a = await accounts.byEmail(s.email);
|
||||
if (!a || !a.username) return json(res, 400, { error: 'Pick a username first; the share page carries it.' });
|
||||
try { const srv = await badge.render(key, a.username); if (srv) jpeg = srv; } catch (e) {}
|
||||
fs.writeFileSync(path.join(UPLOADS_DIR, 'badge-' + a.username + '-' + key + '.jpg'), jpeg);
|
||||
return json(res, 200, { ok: true, page: 'https://instantadpay.com/b/' + a.username + '/' + key, image: 'https://instantadpay.com/badge-img/' + a.username + '/' + key + '.jpg' });
|
||||
}
|
||||
if (p === '/api/admin/badge-repost' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req); const key = String(b.key || ''); const a = await accounts.byEmail(String(b.email || '').toLowerCase());
|
||||
if (!a || !BADGE_META[key]) return json(res, 400, { error: 'Unknown member or badge.' });
|
||||
if (!(await ads.milestonesOf(a.email)).includes(key)) return json(res, 400, { error: 'That member has not unlocked that badge.' });
|
||||
const jpeg = await badge.render(key, a.username || ('member #' + a.memberId)); if (!jpeg) return json(res, 500, { error: 'Render failed.' });
|
||||
const who = a.username ? '@' + a.username : 'member #' + a.memberId; const link = a.username ? 'instantadpay.com/join/' + a.username : 'instantadpay.com';
|
||||
const [label, sub] = BADGE_META[key];
|
||||
const caption = '\u{1F3C6} <b>InstantAdPay</b> \u00b7 <b>' + who.replace(/[<>&]/g, '') + '</b> unlocked <b>' + label + '</b>: ' + sub + '\n' + link;
|
||||
const sc = siteConfig(); let sent = 0;
|
||||
if (sc.telegramBotToken && sc.telegramEchoChatId) {
|
||||
if (await telegramSendPhoto(sc.telegramEchoChatId, jpeg, caption, sc.telegramEchoTopicId)) sent++;
|
||||
if (String(sc.telegramBadgeGeneral || '1') !== '0' && b.general !== false && await telegramSendPhoto(sc.telegramEchoChatId, jpeg, caption, null)) sent++;
|
||||
}
|
||||
const log = badgeLog(); const mine = log[a.email] || {}; mine[key] = { ts: Date.now(), sent, repost: true }; log[a.email] = mine; try { fs.writeFileSync(BADGE_LOG(), JSON.stringify(log)); } catch (e) {}
|
||||
return json(res, 200, { ok: true, sent, bytes: jpeg.length });
|
||||
}
|
||||
if (p === '/api/my/badge-posted' && req.method === 'GET') { // which of my badges are already on Telegram
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
|
||||
Reference in New Issue
Block a user