dankgrinder 6.1.0 → 6.3.0
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/lib/grinder.js +149 -51
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -2400,22 +2400,37 @@ class AccountWorker {
|
|
|
2400
2400
|
if (!this.account.discord_token) { this.log('error', 'No token'); return; }
|
|
2401
2401
|
if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
|
|
2402
2402
|
|
|
2403
|
+
const LOGIN_TIMEOUT_MS = 30_000; // 30s max per account login
|
|
2404
|
+
|
|
2403
2405
|
return new Promise((resolve) => {
|
|
2406
|
+
let resolved = false;
|
|
2407
|
+
const done = () => { if (!resolved) { resolved = true; resolve(); } };
|
|
2408
|
+
|
|
2409
|
+
// Timeout guard — don't let a single account hang the batch
|
|
2410
|
+
const timeoutId = setTimeout(() => {
|
|
2411
|
+
if (!resolved) {
|
|
2412
|
+
this.log('warn', 'Login timed out after 30s — will retry in background');
|
|
2413
|
+
done();
|
|
2414
|
+
// Retry login in background after timeout
|
|
2415
|
+
this._retryLoginBackground();
|
|
2416
|
+
}
|
|
2417
|
+
}, LOGIN_TIMEOUT_MS);
|
|
2418
|
+
|
|
2404
2419
|
this.client.on('ready', async () => {
|
|
2420
|
+
clearTimeout(timeoutId);
|
|
2405
2421
|
this.username = this.client.user.tag || this.username;
|
|
2406
2422
|
this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
} catch { /* silent */ }
|
|
2423
|
+
// Report status non-blocking
|
|
2424
|
+
fetch(`${API_URL}/api/grinder/status`, {
|
|
2425
|
+
method: 'POST',
|
|
2426
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2427
|
+
body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl }),
|
|
2428
|
+
}).catch(() => {});
|
|
2414
2429
|
|
|
2415
2430
|
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2416
2431
|
if (!this.channel) {
|
|
2417
2432
|
this.log('error', `Channel not found`);
|
|
2418
|
-
|
|
2433
|
+
done(); return;
|
|
2419
2434
|
}
|
|
2420
2435
|
|
|
2421
2436
|
const enabledCmds = [
|
|
@@ -2438,49 +2453,108 @@ class AccountWorker {
|
|
|
2438
2453
|
this.log('success', `#${chName} · ${enabledCmds.length} cmds`);
|
|
2439
2454
|
this.setStatus('starting...');
|
|
2440
2455
|
|
|
2441
|
-
// Load daily/weekly/monthly done state from Redis
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2456
|
+
// Load daily/weekly/monthly done state from Redis (non-blocking for login speed)
|
|
2457
|
+
this._loadRedisState().catch(() => {});
|
|
2458
|
+
|
|
2459
|
+
// Reduced settle time — 200ms is enough for most gateways
|
|
2460
|
+
await new Promise(r => setTimeout(r, 200));
|
|
2461
|
+
done();
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
// Handle login errors so they don't hang
|
|
2465
|
+
this.client.on('error', (err) => {
|
|
2466
|
+
if (!resolved) {
|
|
2467
|
+
clearTimeout(timeoutId);
|
|
2468
|
+
const msg = (err?.message || '').toLowerCase();
|
|
2469
|
+
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401')) {
|
|
2470
|
+
this.log('error', `✗ TOKEN INVALID — this token is no longer valid`);
|
|
2471
|
+
this.paused = true;
|
|
2472
|
+
this._tokenInvalid = true;
|
|
2473
|
+
} else {
|
|
2474
|
+
this.log('error', `Login error: ${err?.message || err}`);
|
|
2454
2475
|
}
|
|
2455
|
-
|
|
2456
|
-
try {
|
|
2457
|
-
const balData = await redis.get(`dkg:bal:${this.account.id}`);
|
|
2458
|
-
if (balData) {
|
|
2459
|
-
const { wallet, bank } = JSON.parse(balData);
|
|
2460
|
-
if (wallet > 0 || bank > 0) {
|
|
2461
|
-
this.stats.balance = wallet;
|
|
2462
|
-
this.stats.bankBalance = bank;
|
|
2463
|
-
await fetch(`${API_URL}/api/grinder/status`, {
|
|
2464
|
-
method: 'POST',
|
|
2465
|
-
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2466
|
-
body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
|
|
2467
|
-
}).catch(() => {});
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
} catch {}
|
|
2476
|
+
done();
|
|
2471
2477
|
}
|
|
2472
|
-
|
|
2473
|
-
// Let Discord gateway settle (reduced for faster startup)
|
|
2474
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2475
|
-
resolve();
|
|
2476
2478
|
});
|
|
2477
2479
|
|
|
2478
2480
|
// Attach auto-recovery event listeners before login
|
|
2479
2481
|
this._attachRecoveryListeners();
|
|
2480
|
-
this.client.login(this.account.discord_token)
|
|
2482
|
+
this.client.login(this.account.discord_token).catch((err) => {
|
|
2483
|
+
clearTimeout(timeoutId);
|
|
2484
|
+
const msg = (err?.message || '').toLowerCase();
|
|
2485
|
+
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401') || msg.includes('incorrect login')) {
|
|
2486
|
+
this.log('error', `✗ TOKEN INVALID — check this account's token`);
|
|
2487
|
+
this.paused = true;
|
|
2488
|
+
this._tokenInvalid = true;
|
|
2489
|
+
// Report invalid status to API
|
|
2490
|
+
fetch(`${API_URL}/api/grinder/status`, {
|
|
2491
|
+
method: 'POST',
|
|
2492
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2493
|
+
body: JSON.stringify({ account_id: this.account.id, active: false, status: 'token_invalid' }),
|
|
2494
|
+
}).catch(() => {});
|
|
2495
|
+
sendWebhook('Token Invalid', `**${this.account.label || this.account.id}** has an invalid Discord token.`, 0xef4444);
|
|
2496
|
+
} else {
|
|
2497
|
+
this.log('error', `Login failed: ${err?.message || 'unknown error'}`);
|
|
2498
|
+
}
|
|
2499
|
+
done();
|
|
2500
|
+
});
|
|
2481
2501
|
});
|
|
2482
2502
|
}
|
|
2483
2503
|
|
|
2504
|
+
/** Load Redis cached state in background (non-blocking) */
|
|
2505
|
+
async _loadRedisState() {
|
|
2506
|
+
if (!redis) return;
|
|
2507
|
+
for (const cmd of ['daily', 'weekly', 'monthly', 'drops']) {
|
|
2508
|
+
try {
|
|
2509
|
+
const val = await redis.get(`dkg:done:${this.account.id}:${cmd}`);
|
|
2510
|
+
if (val) {
|
|
2511
|
+
const ttlSec = await redis.ttl(`dkg:done:${this.account.id}:${cmd}`);
|
|
2512
|
+
if (ttlSec > 0) {
|
|
2513
|
+
this.doneToday.set(cmd, Date.now() + ttlSec * 1000);
|
|
2514
|
+
this.log('info', `${cmd} already claimed (${Math.round(ttlSec / 3600)}h remaining)`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
} catch {}
|
|
2518
|
+
}
|
|
2519
|
+
// Load cached balance from Redis
|
|
2520
|
+
try {
|
|
2521
|
+
const balData = await redis.get(`dkg:bal:${this.account.id}`);
|
|
2522
|
+
if (balData) {
|
|
2523
|
+
const { wallet, bank } = JSON.parse(balData);
|
|
2524
|
+
if (wallet > 0 || bank > 0) {
|
|
2525
|
+
this.stats.balance = wallet;
|
|
2526
|
+
this.stats.bankBalance = bank;
|
|
2527
|
+
await fetch(`${API_URL}/api/grinder/status`, {
|
|
2528
|
+
method: 'POST',
|
|
2529
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2530
|
+
body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
|
|
2531
|
+
}).catch(() => {});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
} catch {}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/** Retry login in background after a timeout */
|
|
2538
|
+
async _retryLoginBackground() {
|
|
2539
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
2540
|
+
if (this.client && !this.running) {
|
|
2541
|
+
this.log('info', 'Retrying login in background...');
|
|
2542
|
+
try {
|
|
2543
|
+
this.client.destroy();
|
|
2544
|
+
this.client = createLeanClient();
|
|
2545
|
+
this._attachRecoveryListeners();
|
|
2546
|
+
await this.client.login(this.account.discord_token);
|
|
2547
|
+
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2548
|
+
if (this.channel) {
|
|
2549
|
+
this.username = this.client.user?.tag || this.username;
|
|
2550
|
+
this.log('success', `Background login OK`);
|
|
2551
|
+
}
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
this.log('error', `Background login failed: ${err?.message || err}`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2484
2558
|
stop() {
|
|
2485
2559
|
this.running = false;
|
|
2486
2560
|
this.paused = false;
|
|
@@ -2610,13 +2684,19 @@ async function start(apiKey, apiUrl) {
|
|
|
2610
2684
|
};
|
|
2611
2685
|
|
|
2612
2686
|
// Parallel login in batches of 10 to avoid rate limits while being fast
|
|
2687
|
+
// Within each batch, stagger logins by 100-600ms to avoid gateway flood
|
|
2613
2688
|
const BATCH_SIZE = 10;
|
|
2614
2689
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2615
2690
|
if (shutdownCalled) break;
|
|
2616
2691
|
const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
|
|
2617
2692
|
|
|
2618
|
-
//
|
|
2693
|
+
// Staggered parallel login: fire each login with a small jitter delay
|
|
2619
2694
|
await Promise.all(batch.map(async (acc, idx) => {
|
|
2695
|
+
// Stagger within batch: 0ms for first, 100-600ms for subsequent
|
|
2696
|
+
if (idx > 0) {
|
|
2697
|
+
const jitter = 100 + Math.floor(Math.random() * 500);
|
|
2698
|
+
await new Promise(r => setTimeout(r, jitter));
|
|
2699
|
+
}
|
|
2620
2700
|
const worker = new AccountWorker(acc, i + idx);
|
|
2621
2701
|
workers.push(worker);
|
|
2622
2702
|
workerMap.set(acc.id, worker);
|
|
@@ -2633,15 +2713,33 @@ async function start(apiKey, apiUrl) {
|
|
|
2633
2713
|
hintGC();
|
|
2634
2714
|
}
|
|
2635
2715
|
|
|
2636
|
-
//
|
|
2637
|
-
|
|
2716
|
+
// Login summary: show invalid tokens clearly
|
|
2717
|
+
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2718
|
+
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2719
|
+
if (invalidWorkers.length > 0) {
|
|
2720
|
+
console.log('');
|
|
2721
|
+
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
|
|
2722
|
+
for (const w of invalidWorkers) {
|
|
2723
|
+
log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
|
|
2724
|
+
}
|
|
2725
|
+
console.log('');
|
|
2726
|
+
}
|
|
2727
|
+
if (timedOutWorkers.length > 0) {
|
|
2728
|
+
log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Filter out workers with invalid tokens from grinding
|
|
2732
|
+
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2733
|
+
|
|
2734
|
+
// Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
|
|
2735
|
+
log('info', `${c.dim}Checking inventory for ${activeWorkers.length} accounts...${c.reset}`);
|
|
2638
2736
|
|
|
2639
2737
|
// Parallel inventory checks with single-line progress
|
|
2640
2738
|
let invDone = 0;
|
|
2641
2739
|
let invFailed = 0;
|
|
2642
|
-
const total =
|
|
2740
|
+
const total = activeWorkers.length;
|
|
2643
2741
|
|
|
2644
|
-
await Promise.all(
|
|
2742
|
+
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
2645
2743
|
try {
|
|
2646
2744
|
const invRes = await w.checkInventory({
|
|
2647
2745
|
force: true,
|
|
@@ -2660,15 +2758,15 @@ async function start(apiKey, apiUrl) {
|
|
|
2660
2758
|
log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
|
|
2661
2759
|
|
|
2662
2760
|
if (invFailed > 0) {
|
|
2663
|
-
log('error', `${c.red}Inventory phase incomplete: ${invDone}/${
|
|
2761
|
+
log('error', `${c.red}Inventory phase incomplete: ${invDone}/${activeWorkers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
|
|
2664
2762
|
return;
|
|
2665
2763
|
}
|
|
2666
2764
|
|
|
2667
2765
|
const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
|
|
2668
|
-
log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${
|
|
2766
|
+
log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${activeWorkers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
|
|
2669
2767
|
|
|
2670
|
-
// Phase 3: Start all grind loops
|
|
2671
|
-
for (const w of
|
|
2768
|
+
// Phase 3: Start all grind loops (only for valid workers)
|
|
2769
|
+
for (const w of activeWorkers) {
|
|
2672
2770
|
if (!shutdownCalled) w.grindLoop();
|
|
2673
2771
|
}
|
|
2674
2772
|
|