dankgrinder 6.8.1 → 6.14.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 CHANGED
@@ -2,6 +2,7 @@ const { Client, Options } = require('discord.js-selfbot-v13');
2
2
  const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
4
  const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
5
+ const rawLogger = require('./rawLogger');
5
6
  const {
6
7
  BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
7
8
  AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
@@ -597,6 +598,14 @@ function renderDashboard() {
597
598
  balStr = `${D}⏣${' '.repeat(colBal - 3)}-${c.reset}`;
598
599
  }
599
600
 
601
+ // ── Lifesaver indicator ──
602
+ const ls = wk._lifesavers;
603
+ let lsStr;
604
+ if (ls === 0) lsStr = `${R}♥0${c.reset}`;
605
+ else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${c.reset}`;
606
+ else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
607
+ else lsStr = `${D}♥?${c.reset}`;
608
+
600
609
  // ── Earned (fixed visible width) ──
601
610
  const earnNum = wk.stats.coins || 0;
602
611
  let earnStr;
@@ -612,7 +621,7 @@ function renderDashboard() {
612
621
  const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
613
622
  const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
614
623
 
615
- lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${earnStr} ${earnBar} ${actLabel}`));
624
+ lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
616
625
  }
617
626
 
618
627
  // Overflow summary
@@ -1400,6 +1409,24 @@ class AccountWorker {
1400
1409
  // Final result on same line
1401
1410
  const resultLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.green}${result.items?.length || 0} items${c.reset}, ⏣ ${c.green}${(result.totalValue || 0).toLocaleString()}${c.reset} net${attemptLabel}`;
1402
1411
  process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1412
+
1413
+ // Extract lifesaver count from inventory and cache in Redis
1414
+ if (result.items && redis) {
1415
+ const lsItem = result.items.find(i =>
1416
+ /life\s*saver/i.test(i.name) || /lifesaver/i.test(i.name)
1417
+ );
1418
+ const lsCount = lsItem ? lsItem.qty : 0;
1419
+ this._lifesavers = lsCount;
1420
+ try {
1421
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
1422
+ if (lsCount === 0) {
1423
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1424
+ } else {
1425
+ await redis.del(`raw:alert:no-lifesaver:${this.channel?.id}`);
1426
+ }
1427
+ } catch {}
1428
+ }
1429
+
1403
1430
  try {
1404
1431
  await fetch(`${API_URL}/api/grinder/inventory`, {
1405
1432
  method: 'POST',
@@ -1629,6 +1656,7 @@ class AccountWorker {
1629
1656
  balance: wallet,
1630
1657
  bank_balance: bank,
1631
1658
  total_balance: wallet + bank,
1659
+ lifesavers: this._lifesavers ?? null,
1632
1660
  }),
1633
1661
  });
1634
1662
  } catch { /* silent */ }
@@ -1660,6 +1688,27 @@ class AccountWorker {
1660
1688
  if (shutdownCalled || !this.running) return;
1661
1689
  this.stats.commands++;
1662
1690
 
1691
+ // ── Lifesaver protection: skip crime/search if 0 lifesavers ──
1692
+ if (cmdName === 'crime' || cmdName === 'search') {
1693
+ const noLifesaver = await rawLogger.hasNoLifesaverAlert(this.channel?.id);
1694
+ if (noLifesaver) {
1695
+ this.log('warn', `[${cmdName}] SKIPPED — no lifesavers! (death detected in DMs)`);
1696
+ await this.setCooldown(cmdName, 3600); // block for 1 hour
1697
+ return;
1698
+ }
1699
+ // Also check Redis key for lifesaver count
1700
+ if (redis) {
1701
+ try {
1702
+ const lsCount = await redis.get(`dkg:lifesavers:${this.account.id}`);
1703
+ if (lsCount === '0') {
1704
+ this.log('warn', `[${cmdName}] SKIPPED — 0 lifesavers cached`);
1705
+ await this.setCooldown(cmdName, 3600);
1706
+ return;
1707
+ }
1708
+ } catch {}
1709
+ }
1710
+ }
1711
+
1663
1712
  const cmdOpts = {
1664
1713
  channel: this.channel,
1665
1714
  waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
@@ -1763,6 +1812,42 @@ class AccountWorker {
1763
1812
  return;
1764
1813
  }
1765
1814
 
1815
+ // ── Death / lifesaver detection in command responses ──
1816
+ if (resultLower.includes('you died') || resultLower.includes('lifesaver protected')) {
1817
+ this.log('error', `DEATH DETECTED during ${cmdName}!`);
1818
+ // Check for lifesaver count in the response
1819
+ const lsMatch = result.match(/(\d+)\s*life\s*saver/i);
1820
+ const lsCount = lsMatch ? parseInt(lsMatch[1]) : -1;
1821
+ if (redis) {
1822
+ if (lsCount === 0) {
1823
+ // 0 lifesavers — DISABLE crime/search immediately
1824
+ await redis.set(`dkg:lifesavers:${this.account.id}`, '0', 'EX', 86400);
1825
+ await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1826
+ this.log('error', `0 LIFESAVERS! Disabling crime/search for this account!`);
1827
+ await this.setCooldown('crime', 86400); // 24h
1828
+ await this.setCooldown('search', 86400);
1829
+ sendWebhook('DEATH ALERT', `**${this.username}** died during \`${cmdName}\`!\n**0 lifesavers remaining!**\nCrime/search disabled for 24h.`, 0xef4444);
1830
+ } else if (lsCount > 0) {
1831
+ await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
1832
+ this.log('warn', `Lifesaver used! ${lsCount} remaining.`);
1833
+ if (lsCount <= 2) {
1834
+ sendWebhook('LOW LIFESAVERS', `**${this.username}** has only **${lsCount}** lifesaver(s) left!`, 0xfbbf24);
1835
+ }
1836
+ }
1837
+ }
1838
+ return;
1839
+ }
1840
+
1841
+ // Died flag from crime/search handler (death detected in the command response)
1842
+ if (cmdResult.died) {
1843
+ this.log('error', `${cmdName} → DIED! Checking lifesaver count...`);
1844
+ // The DM will come separately with the actual death details
1845
+ // For now, be cautious — set a short cooldown and let DM listener handle the rest
1846
+ await this.setCooldown('crime', 300); // 5 min cooldown to check DMs
1847
+ await this.setCooldown('search', 300);
1848
+ return;
1849
+ }
1850
+
1766
1851
  // Premium-only command detection — disable for 24h
1767
1852
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
1768
1853
  resultLower.includes('buy the ability to use this command')) {
@@ -2101,6 +2186,8 @@ class AccountWorker {
2101
2186
 
2102
2187
  // Set up error/disconnect handlers for auto-recovery
2103
2188
  this._attachRecoveryListeners();
2189
+ rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
2190
+ rawLogger.attachDmLogger(this.client);
2104
2191
 
2105
2192
  await this.client.login(this.account.discord_token);
2106
2193
  this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
@@ -2554,6 +2641,11 @@ class AccountWorker {
2554
2641
  clearTimeout(timeoutId);
2555
2642
  this.username = this.client.user.tag || this.username;
2556
2643
  this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
2644
+
2645
+ // Attach raw gateway logger for CV2 component capture
2646
+ rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
2647
+ rawLogger.attachDmLogger(this.client);
2648
+
2557
2649
  // Report status non-blocking
2558
2650
  fetch(`${API_URL}/api/grinder/status`, {
2559
2651
  method: 'POST',
@@ -2795,6 +2887,27 @@ async function start(apiKey, apiUrl) {
2795
2887
  const checks = [];
2796
2888
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
2797
2889
  if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
2890
+
2891
+ // Init rawLogger Redis (uses same URL — logs all raw gateway data)
2892
+ if (REDIS_URL) {
2893
+ rawLogger.init(REDIS_URL).catch(() => {});
2894
+ // Listen for DM death events across all accounts
2895
+ rawLogger.onDmEvent((event, raw) => {
2896
+ if (event.type === 'death' && event.lifesaversLeft === 0) {
2897
+ const channelId = raw.channel_id;
2898
+ // Find which worker uses this DM channel and disable their crime/search
2899
+ for (const w of workers) {
2900
+ if (w.client?.user?.dmChannel?.id === channelId || w.channel?.id) {
2901
+ w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
2902
+ w.setCooldown?.('crime', 86400);
2903
+ w.setCooldown?.('search', 86400);
2904
+ }
2905
+ }
2906
+ sendWebhook?.('DEATH ALERT (DM)', `Account died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
2907
+ }
2908
+ });
2909
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
2910
+ }
2798
2911
  if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
2799
2912
  if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
2800
2913
  if (CLUSTER_ENABLED) {
@@ -2934,6 +3047,45 @@ async function start(apiKey, apiUrl) {
2934
3047
 
2935
3048
  console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${total}${c.reset} ${c.dim}all clear${c.reset}`);
2936
3049
  console.log('');
3050
+
3051
+ // Phase 2.5: Check balance for ALL accounts + show lifesaver status
3052
+ console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking balance for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${c.dim} accounts...${c.reset}`);
3053
+
3054
+ let balDone = 0;
3055
+ const balProgressInterval = setInterval(() => {
3056
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3057
+ process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${c.bold}${rgb(52, 211, 153)}${balDone}${c.reset}${c.dim}/${c.reset}${c.white}${total}${c.reset} `);
3058
+ }, 80);
3059
+
3060
+ await Promise.all(activeWorkers.map(async (w) => {
3061
+ try {
3062
+ await w.checkBalance();
3063
+ } catch {}
3064
+ balDone++;
3065
+ }));
3066
+
3067
+ clearInterval(balProgressInterval);
3068
+ process.stdout.write(`\r${c.clearLine}`);
3069
+
3070
+ // Show balance + lifesaver summary for each account
3071
+ let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3072
+ for (const w of activeWorkers) {
3073
+ const wallet = w.stats?.balance || 0;
3074
+ const bank = w.stats?.bankBalance || 0;
3075
+ const ls = w._lifesavers ?? '?';
3076
+ totalWallet += wallet;
3077
+ totalBank += bank;
3078
+ const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3079
+ console.log(` ${c.dim}├${c.reset} ${c.bold}${w.username}${c.reset} Wallet: ${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} LS: ${lsColor}${ls}${c.reset}`);
3080
+ if (ls === 0) noLifesaverAccounts.push(w.username);
3081
+ }
3082
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Balance${c.reset} Total: ${c.bold}${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
3083
+
3084
+ if (noLifesaverAccounts.length > 0) {
3085
+ console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}WARNING: ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS!${c.reset} Crime/Search disabled for: ${noLifesaverAccounts.join(', ')}`);
3086
+ }
3087
+ console.log('');
3088
+
2937
3089
  console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
2938
3090
  console.log('');
2939
3091
 
@@ -3015,7 +3167,8 @@ async function start(apiKey, apiUrl) {
3015
3167
  }
3016
3168
 
3017
3169
  const before = workers.length;
3018
- workers = workers.filter(w => w.running || freshIds.has(w.account.id));
3170
+ // Keep ALL workers visible — never remove from array (user wants to see gaps)
3171
+ // Only clean up workerMap entries for accounts fully removed from API
3019
3172
  if (workers.length !== before) scheduleRender();
3020
3173
  } catch {}
3021
3174
  }, 10_000);
@@ -3124,6 +3277,7 @@ function setupKeyboardShortcuts() {
3124
3277
 
3125
3278
  // Ctrl+C or q = quit
3126
3279
  if (k === '\u0003' || k === 'q') {
3280
+ process.stdout.write(c.show);
3127
3281
  console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
3128
3282
  process.emit('SIGINT');
3129
3283
  return;
@@ -3131,51 +3285,40 @@ function setupKeyboardShortcuts() {
3131
3285
 
3132
3286
  // p = pause all accounts
3133
3287
  if (k === 'p') {
3134
- let paused = 0;
3135
- workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; paused++; } });
3136
- console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
3137
- console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.yellow}⏸ Paused ${paused} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
3138
- console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
3288
+ let count = 0;
3289
+ workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
3290
+ recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
3291
+ scheduleRender();
3139
3292
  return;
3140
3293
  }
3141
3294
 
3142
3295
  // r = resume all accounts
3143
3296
  if (k === 'r') {
3144
- let resumed = 0;
3145
- workers.forEach(w => { if (w.paused) { w.paused = false; resumed++; } });
3146
- console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
3147
- console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.green}● Resumed ${resumed} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
3148
- console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
3297
+ let count = 0;
3298
+ workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
3299
+ recentLogs.push(`>> RESUMED ${count} accounts`);
3300
+ scheduleRender();
3149
3301
  return;
3150
3302
  }
3151
3303
 
3152
- // s = show status summary
3304
+ // s = show status summary (pushed to log feed)
3153
3305
  if (k === 's') {
3154
3306
  const active = workers.filter(w => w.running && !w.paused).length;
3155
3307
  const paused = workers.filter(w => w.paused).length;
3156
- const offline = workers.filter(w => !w.running).length;
3308
+ const invalid = workers.filter(w => w._tokenInvalid).length;
3309
+ const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
3157
3310
  const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
3158
3311
  const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
3159
- console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
3160
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Status Summary${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
3161
- console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
3162
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.green}● ${active} active${c.reset} ${c.yellow}⏸ ${paused} paused${c.reset} ${c.red}○ ${offline} offline${c.reset} ${c.yellow}↻ ${recovering} recovering${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
3163
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.dim}Total earnings:${c.reset} ${rgb(52, 211, 153)}⏣ ${totalEarn.toLocaleString()}${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
3164
- console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
3312
+ recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
3313
+ recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
3314
+ scheduleRender();
3165
3315
  return;
3166
3316
  }
3167
3317
 
3168
- // ? = show help
3318
+ // ? or h = show help
3169
3319
  if (k === '?' || k === 'h') {
3170
- console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
3171
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Keyboard Shortcuts${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
3172
- console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
3173
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}p${c.reset} Pause all accounts ${rgb(139, 92, 246)}║${c.reset}`);
3174
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}r${c.reset} Resume all accounts ${rgb(139, 92, 246)}║${c.reset}`);
3175
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}s${c.reset} Show status summary ${rgb(139, 92, 246)}║${c.reset}`);
3176
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}q${c.reset} Quit gracefully ${rgb(139, 92, 246)}║${c.reset}`);
3177
- console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}?${c.reset} Show this help ${rgb(139, 92, 246)}║${c.reset}`);
3178
- console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
3320
+ recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
3321
+ scheduleRender();
3179
3322
  return;
3180
3323
  }
3181
3324
  });