@xuda.io/account_module 1.2.2298 → 1.2.2300
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +93 -19
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -2,7 +2,8 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import _ from 'lodash';
|
|
5
|
-
import { execFile } from 'child_process';
|
|
5
|
+
import { execFile, spawn } from 'child_process';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
import { promisify } from 'util';
|
|
7
8
|
import { createRequire } from 'module';
|
|
8
9
|
import { parsePhoneNumber } from 'libphonenumber-js';
|
|
@@ -306,7 +307,6 @@ export const update_account_info = async function (req, job_id, headers) {
|
|
|
306
307
|
|
|
307
308
|
if (account_obj.account_info?.profile_picture) {
|
|
308
309
|
if (!account_obj.account_info?.profile_avatar && account_obj.account_info.profile_avatar_stat !== 2) {
|
|
309
|
-
debugger;
|
|
310
310
|
set_account_profile_picture(uid, uid, account_obj.account_info, job_id, headers, account_profile_info);
|
|
311
311
|
}
|
|
312
312
|
}
|
|
@@ -2529,17 +2529,61 @@ const _ops_validate_modules = (modules) => {
|
|
|
2529
2529
|
return { list, unknown: list.filter((m) => !universe.includes(m)) };
|
|
2530
2530
|
};
|
|
2531
2531
|
|
|
2532
|
-
//
|
|
2533
|
-
//
|
|
2534
|
-
//
|
|
2535
|
-
|
|
2536
|
-
|
|
2532
|
+
// Run `node <args>` DETACHED, with its own process group and its own log file (not
|
|
2533
|
+
// our stdio pipes). This account proc is the deploy orchestrator, and pm2 WATCHES
|
|
2534
|
+
// common/ + cpi/account_module, so a stray file change restarts it. A rollout spawned
|
|
2535
|
+
// as a normal child would then be torn down mid-flight, leaving boxes half-patched.
|
|
2536
|
+
// detached:true = the child leads its own session and survives a signal aimed at our
|
|
2537
|
+
// process group; the log file = a broken parent pipe cannot SIGPIPE it. We still await
|
|
2538
|
+
// its exit for the summary; if we get bounced mid-run the child finishes on its own,
|
|
2539
|
+
// and a re-run is a safe no-op (hotfix_prod skips already-current modules). Never
|
|
2540
|
+
// rejects: failures come back as { ok:false, exit, stderr }. stdout+stderr are merged
|
|
2541
|
+
// into the log, so a fatal error stays visible in the tail the UI shows.
|
|
2542
|
+
const _ops_run_node_detached = ({ args, timeout_ms, env }) => {
|
|
2543
|
+
const logfile = path.join(os.tmpdir(), `xuda_ops_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.log`);
|
|
2544
|
+
let fd;
|
|
2537
2545
|
try {
|
|
2538
|
-
|
|
2539
|
-
return { ok: true, exit: 0, stdout: String(stdout || ''), stderr: String(stderr || '') };
|
|
2546
|
+
fd = fs.openSync(logfile, 'a');
|
|
2540
2547
|
} catch (e) {
|
|
2541
|
-
return { ok: false, exit:
|
|
2542
|
-
}
|
|
2548
|
+
return Promise.resolve({ ok: false, exit: 1, stdout: '', stderr: 'could not open run log: ' + e.message });
|
|
2549
|
+
}
|
|
2550
|
+
return new Promise((resolve) => {
|
|
2551
|
+
let done = false;
|
|
2552
|
+
let timer = null;
|
|
2553
|
+
const settle = ({ exit, stderr }) => {
|
|
2554
|
+
if (done) return;
|
|
2555
|
+
done = true;
|
|
2556
|
+
if (timer) clearTimeout(timer);
|
|
2557
|
+
try { fs.closeSync(fd); } catch (e) {}
|
|
2558
|
+
let stdout = '';
|
|
2559
|
+
try { stdout = fs.readFileSync(logfile, 'utf8'); } catch (e) {}
|
|
2560
|
+
try { fs.unlinkSync(logfile); } catch (e) {}
|
|
2561
|
+
resolve({ ok: exit === 0, exit, stdout, stderr: stderr || '' });
|
|
2562
|
+
};
|
|
2563
|
+
let child;
|
|
2564
|
+
try {
|
|
2565
|
+
child = spawn('node', args, { detached: true, stdio: ['ignore', fd, fd], env: env || { ...process.env } });
|
|
2566
|
+
} catch (e) {
|
|
2567
|
+
settle({ exit: 1, stderr: 'spawn failed: ' + e.message });
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
timer = setTimeout(() => {
|
|
2571
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch (e) {}
|
|
2572
|
+
settle({ exit: 124, stderr: `timed out after ${Math.round((timeout_ms || 300000) / 1000)}s` });
|
|
2573
|
+
}, timeout_ms || 300000);
|
|
2574
|
+
child.on('error', (e) => settle({ exit: 1, stderr: e.message }));
|
|
2575
|
+
child.on('exit', (code, signal) => settle({ exit: code == null ? 1 : code, stderr: signal ? 'killed by ' + signal : '' }));
|
|
2576
|
+
child.unref();
|
|
2577
|
+
});
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// Spawn utils/hotfix_prod.mjs against explicit WG hosts, DETACHED (see above), so a
|
|
2581
|
+
// pm2 file-watch restart of this orchestrator cannot kill an in-flight rollout.
|
|
2582
|
+
// hotfix_prod exits 1 if ANY host failed; that becomes ok:false, keeping stdout for
|
|
2583
|
+
// the summary either way.
|
|
2584
|
+
const _ops_run_hotfix = ({ hosts, modules, dry_run, timeout_ms }) => {
|
|
2585
|
+
const args = [_HOTFIX_TOOL(), '--hosts', hosts.map((h) => h.ssh).join(','), ...(modules || []), ...(dry_run ? ['--dry-run'] : [])];
|
|
2586
|
+
return _ops_run_node_detached({ args, timeout_ms });
|
|
2543
2587
|
};
|
|
2544
2588
|
|
|
2545
2589
|
// Pull hotfix_prod's human summary block + a tail for the UI.
|
|
@@ -3177,7 +3221,6 @@ export const get_hosting_plan = function (req) {
|
|
|
3177
3221
|
if (_conf.PRICE_OBJ.server_slugs[app_obj.app_hosting.app_server_type]) {
|
|
3178
3222
|
return _conf.PRICE_OBJ.server_slugs[app_obj.app_hosting.app_server_type];
|
|
3179
3223
|
}
|
|
3180
|
-
debugger;
|
|
3181
3224
|
console.error('error: ' + app_obj.app_hosting.app_server_type + ' not found in PRICE_OBJ.server_slugs');
|
|
3182
3225
|
return { cpu: 0, price: 0 };
|
|
3183
3226
|
}
|
|
@@ -5898,13 +5941,26 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
5898
5941
|
if (account_info_ret.code < 0) {
|
|
5899
5942
|
return;
|
|
5900
5943
|
}
|
|
5901
|
-
|
|
5944
|
+
// The main profile IS the account's own face: always mirror the account
|
|
5945
|
+
// avatar/picture on it, so the main-profile card matches the account avatar.
|
|
5946
|
+
// A divergent value stored on the main profile doc (e.g. a stale AI-generated
|
|
5947
|
+
// avatar) must not win. Non-main profiles keep their own, falling back to the
|
|
5948
|
+
// account avatar only when they have none.
|
|
5949
|
+
if (doc.main || !doc.profile_avatar) {
|
|
5902
5950
|
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
5903
5951
|
}
|
|
5904
|
-
if (!doc.profile_picture) {
|
|
5952
|
+
if (doc.main || !doc.profile_picture) {
|
|
5905
5953
|
doc.profile_picture = account_info_ret.data.profile_picture;
|
|
5906
5954
|
}
|
|
5907
5955
|
|
|
5956
|
+
// The main profile is the account's OWN identity: it must never auto-respond
|
|
5957
|
+
// (you don't auto-reply to yourself). Force it off in the read model so the
|
|
5958
|
+
// card hides the control and no read-based consumer treats it as on. The
|
|
5959
|
+
// runtime (ai_module.auto_response) also hard-skips the main profile.
|
|
5960
|
+
if (doc.main) {
|
|
5961
|
+
doc.auto_respond = false;
|
|
5962
|
+
}
|
|
5963
|
+
|
|
5908
5964
|
if (!doc.account_type) {
|
|
5909
5965
|
doc.account_type = account_info_ret.data.account_type;
|
|
5910
5966
|
}
|
|
@@ -7047,7 +7103,6 @@ export const get_account_ai_usage_old = async function (req, job_id, headers) {
|
|
|
7047
7103
|
group_level: 999,
|
|
7048
7104
|
});
|
|
7049
7105
|
// console.log('ai_usage', ai_usage.rows);
|
|
7050
|
-
debugger;
|
|
7051
7106
|
let total_usage = 0;
|
|
7052
7107
|
let profile = {};
|
|
7053
7108
|
|
|
@@ -7455,8 +7510,10 @@ setTimeout(async () => {
|
|
|
7455
7510
|
// 10% monthly Stripe coupon, saves a NUMBERED draft issue to xuda_newsletter, and
|
|
7456
7511
|
// emails a sample to ops (info@xuda.ai) + a superuser notification for review.
|
|
7457
7512
|
// Nothing goes to subscribers until a superuser hits Publish in the dashboard
|
|
7458
|
-
// (newsletter_publish), which blasts to every
|
|
7459
|
-
//
|
|
7513
|
+
// (newsletter_publish), which blasts to every account that has NOT opted out
|
|
7514
|
+
// (preferences.newsletter_opt_in !== false, so a missing flag counts as opted
|
|
7515
|
+
// in), minus the xuda.network seed personas, with a per-recipient unsubscribe
|
|
7516
|
+
// link + List-Unsubscribe header. See _newsletter_blast for why both rules exist.
|
|
7460
7517
|
// Opt-out flips the same flag the router /newsletter/unsubscribe route reads.
|
|
7461
7518
|
// ============================================================================
|
|
7462
7519
|
const NEWSLETTER_DB = 'xuda_newsletter';
|
|
@@ -8089,8 +8146,25 @@ const _newsletter_blast = async (issue, by_uid) => {
|
|
|
8089
8146
|
const [pool, ppool] = await Promise.all([_nl_fashion_pool(), _nl_partner_pool()]);
|
|
8090
8147
|
let sent = 0, failed = 0, total = 0;
|
|
8091
8148
|
try {
|
|
8092
|
-
|
|
8093
|
-
|
|
8149
|
+
// Opt-in is OPT-OUT by design: the three signup surfaces stamp
|
|
8150
|
+
// preferences.newsletter_opt_in=true, but every account created before the
|
|
8151
|
+
// newsletter shipped has NO such field. Selecting on `=== true` therefore
|
|
8152
|
+
// matched 1 of 456 accounts on master and the audience was silently empty
|
|
8153
|
+
// (issue #4 reported "1 sent"). Treat a MISSING flag as opted in, and only
|
|
8154
|
+
// an explicit false as opted out.
|
|
8155
|
+
//
|
|
8156
|
+
// EXCLUDE the xuda.network personas. They are seed accounts on
|
|
8157
|
+
// @ambassadors.xuda.network / @mentors.xuda.network, domains with no mail
|
|
8158
|
+
// service, so mailing them is 420 guaranteed hard bounces in one blast,
|
|
8159
|
+
// which the bounce handler turns into 420 suspensions plus real damage to
|
|
8160
|
+
// the sending reputation. `source` starts with xuda_network_ on every one of
|
|
8161
|
+
// them and agrees with the address on all 420, so it is the reliable marker.
|
|
8162
|
+
// Filtering here rather than in the Mango selector: it is a few hundred docs,
|
|
8163
|
+
// and $ne against a MISSING field is exactly the semantic that caused this bug.
|
|
8164
|
+
const rret = await db_module.find_couch_query('xuda_accounts', { selector: { docType: 'account', stat: 3 }, limit: 100000 }, true);
|
|
8165
|
+
const recipients = (rret.docs || []).filter(
|
|
8166
|
+
(a) => !String(a.source || '').startsWith('xuda_network_') && a.preferences?.newsletter_opt_in !== false,
|
|
8167
|
+
);
|
|
8094
8168
|
total = recipients.length;
|
|
8095
8169
|
for (const acct of recipients) {
|
|
8096
8170
|
const email = acct.account_info && acct.account_info.email;
|