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.
Files changed (2) hide show
  1. package/lib/grinder.js +149 -51
  2. 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
- try {
2408
- await fetch(`${API_URL}/api/grinder/status`, {
2409
- method: 'POST',
2410
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
2411
- body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl }),
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
- resolve(); return;
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
- if (redis) {
2443
- for (const cmd of ['daily', 'weekly', 'monthly', 'drops']) {
2444
- try {
2445
- const val = await redis.get(`dkg:done:${this.account.id}:${cmd}`);
2446
- if (val) {
2447
- const ttlSec = await redis.ttl(`dkg:done:${this.account.id}:${cmd}`);
2448
- if (ttlSec > 0) {
2449
- this.doneToday.set(cmd, Date.now() + ttlSec * 1000);
2450
- this.log('info', `${cmd} already claimed (${Math.round(ttlSec / 3600)}h remaining)`);
2451
- }
2452
- }
2453
- } catch {}
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
- // Load cached balance from Redis
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
- // Login batch in parallel
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
- // Phase 2: Run inventory on ALL accounts in parallel (must complete before grinding)
2637
- log('info', `${c.dim}Checking inventory for ${workers.length} accounts...${c.reset}`);
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 = workers.length;
2740
+ const total = activeWorkers.length;
2643
2741
 
2644
- await Promise.all(workers.map(async (w, i) => {
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}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
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}/${workers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
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 workers) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.1.0",
3
+ "version": "6.3.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"