dankgrinder 6.16.0 → 6.19.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.
@@ -64,7 +64,7 @@ async function clickAndRefetch(channel, msg, btn) {
64
64
  LOG.error(`[adventure] Click error: ${e.message}`);
65
65
  return null;
66
66
  }
67
- await sleep(250);
67
+ await sleep(100);
68
68
  return await refetchMsg(channel, msg.id);
69
69
  }
70
70
 
@@ -174,7 +174,7 @@ async function playAdventureRounds(channel, msg) {
174
174
  const choice = pickSafeChoice(choices);
175
175
  if (choice) {
176
176
  LOG.info(`[adventure] → Choosing: "${choice.label}"`);
177
- await sleep(100);
177
+ await sleep(50);
178
178
  const afterChoice = await clickAndRefetch(channel, current, choice);
179
179
  if (afterChoice) {
180
180
  current = afterChoice;
@@ -208,7 +208,7 @@ async function playAdventureRounds(channel, msg) {
208
208
  } else if (nextBtnNow && nextBtnNow.disabled) {
209
209
  // Next is disabled but no choices found — might be loading
210
210
  LOG.debug(`[adventure] Next disabled, no choices — waiting...`);
211
- await sleep(300);
211
+ await sleep(150);
212
212
  const refreshed = await refetchMsg(channel, current.id);
213
213
  if (refreshed) {
214
214
  current = refreshed;
@@ -221,7 +221,7 @@ async function playAdventureRounds(channel, msg) {
221
221
  // No next button at all
222
222
  LOG.debug(`[adventure] No Next button — checking if done`);
223
223
  if (isAdventureDone(current)) break;
224
- await sleep(500);
224
+ await sleep(200);
225
225
  const refreshed = await refetchMsg(channel, current.id);
226
226
  if (refreshed) {
227
227
  current = refreshed;
@@ -374,7 +374,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
374
374
  } catch (e) {
375
375
  LOG.error(`[adventure] Select error: ${e.message}`);
376
376
  }
377
- await sleep(300);
377
+ await sleep(150);
378
378
  }
379
379
  }
380
380
 
@@ -63,7 +63,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
63
63
  if (initLower.includes('cannot post another meme') || initLower.includes('dead meme') ||
64
64
  initLower.includes('another meme for another')) {
65
65
  const minMatch = initText.match(RE_COOLDOWN_MIN);
66
- const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
66
+ const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 150;
67
67
  LOG.warn(`[pm] Cooldown: ${cdSec}s`);
68
68
  return { result: `pm cooldown ${cdSec}s`, coins: 0, nextCooldownSec: cdSec };
69
69
  }
@@ -110,7 +110,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
110
110
  try {
111
111
  await response.selectMenu(locRowIdx, [opt.value]);
112
112
  } catch (e) { LOG.error(`[pm] Platform select error: ${e.message}`); }
113
- await sleep(300);
113
+ await sleep(100);
114
114
  const updated = await refetchMsg(channel, msgId);
115
115
  if (updated) { response = updated; logMsg(response, 'pm-after-platform'); }
116
116
  }
@@ -123,7 +123,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
123
123
  try {
124
124
  await response.selectMenu(kindRowIdx, [opt.value]);
125
125
  } catch (e) { LOG.error(`[pm] Kind select error: ${e.message}`); }
126
- await sleep(300);
126
+ await sleep(100);
127
127
  const updated = await refetchMsg(channel, msgId);
128
128
  if (updated) { response = updated; logMsg(response, 'pm-after-kind'); }
129
129
  }
@@ -136,7 +136,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
136
136
  LOG.info(`[pm] Clicking "${postBtn.label}"...`);
137
137
  try {
138
138
  await safeClickButton(response, postBtn);
139
- await sleep(300);
139
+ await sleep(100);
140
140
  const final = await refetchMsg(channel, msgId);
141
141
  if (final) {
142
142
  logMsg(final, 'pm-result');
@@ -230,10 +230,10 @@ async function runStream({ channel, waitForDankMemer, client }) {
230
230
  for (const item of itemsToBuy) {
231
231
  const bought = await buyItem({ channel, waitForDankMemer, client, itemName: item, quantity: 1 });
232
232
  if (!bought) return { result: `need ${item} (buy failed)`, coins: 0, nextCooldownSec: 1800 };
233
- await humanDelay(500, 1000);
233
+ await humanDelay(200, 400);
234
234
  }
235
235
 
236
- await sleep(2000);
236
+ await sleep(800);
237
237
  await channel.send('pls stream');
238
238
  response = await waitForDankMemer(12000);
239
239
  if (!response) return { result: 'no response after buy', coins: 0, nextCooldownSec: 180 };
@@ -256,7 +256,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
256
256
 
257
257
  const selected = await selectRandomStreamOption(response);
258
258
  if (selected) {
259
- await humanDelay(150, 350);
259
+ await humanDelay(80, 180);
260
260
  const updatedAfterSelect = (await waitForDankMemer(5000)) || (await refetchMsg(channel, response.id));
261
261
  if (updatedAfterSelect) {
262
262
  response = updatedAfterSelect;
@@ -294,7 +294,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
294
294
  }
295
295
  }
296
296
 
297
- await sleep(300);
297
+ await sleep(150);
298
298
  const fresh = await refetchMsg(channel, response.id);
299
299
  if (fresh) {
300
300
  response = fresh;
@@ -305,7 +305,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
305
305
  }
306
306
 
307
307
  LOG.info('[stream] Clicking "Go Live"');
308
- await humanDelay(100, 250);
308
+ await humanDelay(50, 120);
309
309
  try {
310
310
  const before = response;
311
311
  const liveResult = await safeClickButton(response, goLiveBtn);
@@ -340,7 +340,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
340
340
  );
341
341
  if (fallbackBtn) {
342
342
  LOG.info('[stream] Go Live transition not detected; trying setup fallback');
343
- await humanDelay(80, 180);
343
+ await humanDelay(40, 100);
344
344
  try {
345
345
  const fallbackRes = await safeClickButton(response, fallbackBtn);
346
346
  const afterFallback = fallbackRes || (await waitForStreamTransition({
@@ -376,7 +376,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
376
376
  const action = pickRandom(actions);
377
377
  const actionAt = new Date();
378
378
  LOG.info(`[stream] Live action: "${action.label}"`);
379
- await humanDelay(120, 320);
379
+ await humanDelay(60, 150);
380
380
  try {
381
381
  const clicked = await safeClickButton(response, action);
382
382
  let updated = clicked || (await waitForDankMemer(6000)) || (await refetchMsg(channel, response.id));
@@ -311,49 +311,66 @@ function findSelectMenuOption(msg, label) {
311
311
  // Safe button click — tries library methods first, falls back to raw HTTP for CV2.
312
312
  // When CV2 fallback is used, waits for the message to update so callers always
313
313
  // get the updated message back (instead of null, which broke multi-round games).
314
- async function safeClickButton(msg, button) {
314
+ async function safeClickButton(msg, button, retries = 2) {
315
315
  // Skip LINK buttons (external URLs) — they have style=LINK/5 and no customId
316
316
  if (button.style === 'LINK' || button.style === 5 || (!button.customId && !button.custom_id && typeof button.click !== 'function')) {
317
317
  return null;
318
318
  }
319
- if (typeof button.click === 'function') {
320
- return button.click();
321
- }
322
- const id = button.customId || button.custom_id;
323
- if (id && typeof msg.clickButton === 'function') {
319
+
320
+ for (let attempt = 0; attempt <= retries; attempt++) {
324
321
  try {
325
- return await msg.clickButton(id);
326
- } catch {
327
- // Fall through to CV2 raw interaction fallback.
328
- }
329
- }
330
- // CV2 fallback: send interaction via raw HTTP, then wait for the message
331
- // to update so we can return the updated message to the caller.
332
- if (id) {
333
- const interactionAck = await clickCV2Button(msg, id);
334
- if (interactionAck) msg._lastInteractionAck = interactionAck;
335
- // Wait for Dank Memer to process the interaction and update the message
336
- const updatedMsg = await new Promise((resolve) => {
337
- const timeout = setTimeout(() => {
338
- msg.client?.removeListener?.('messageUpdate', handler);
339
- resolve(null);
340
- }, 8000);
341
- const handler = (_, newMsg) => {
342
- if (newMsg.id === msg.id) {
343
- clearTimeout(timeout);
344
- msg.client?.removeListener?.('messageUpdate', handler);
345
- resolve(newMsg);
322
+ if (typeof button.click === 'function') {
323
+ return await button.click();
324
+ }
325
+ const id = button.customId || button.custom_id;
326
+ if (id && typeof msg.clickButton === 'function') {
327
+ try {
328
+ return await msg.clickButton(id);
329
+ } catch {
330
+ // Fall through to CV2 raw interaction fallback.
346
331
  }
347
- };
348
- msg.client?.on?.('messageUpdate', handler);
349
- });
350
- if (updatedMsg) {
351
- await ensureCV2(updatedMsg);
352
- return updatedMsg;
332
+ }
333
+ // CV2 fallback: send interaction via raw HTTP, then wait for the message
334
+ // to update so we can return the updated message to the caller.
335
+ if (id) {
336
+ const interactionAck = await clickCV2Button(msg, id);
337
+ if (interactionAck) msg._lastInteractionAck = interactionAck;
338
+ // Wait for Dank Memer to process the interaction and update the message
339
+ const updatedMsg = await new Promise((resolve) => {
340
+ const timeout = setTimeout(() => {
341
+ msg.client?.removeListener?.('messageUpdate', handler);
342
+ resolve(null);
343
+ }, 6000);
344
+ const handler = (_, newMsg) => {
345
+ if (newMsg.id === msg.id) {
346
+ clearTimeout(timeout);
347
+ msg.client?.removeListener?.('messageUpdate', handler);
348
+ resolve(newMsg);
349
+ }
350
+ };
351
+ msg.client?.on?.('messageUpdate', handler);
352
+ });
353
+ if (updatedMsg) {
354
+ await ensureCV2(updatedMsg);
355
+ return updatedMsg;
356
+ }
357
+ // Retry if we got null
358
+ if (attempt < retries) {
359
+ await new Promise(r => setTimeout(r, 300 + attempt * 200));
360
+ continue;
361
+ }
362
+ return null;
363
+ }
364
+ throw new Error('No click method available on button');
365
+ } catch (err) {
366
+ if (attempt < retries) {
367
+ await new Promise(r => setTimeout(r, 300 + attempt * 200));
368
+ continue;
369
+ }
370
+ throw err;
353
371
  }
354
- return null;
355
372
  }
356
- throw new Error('No click method available on button');
373
+ return null;
357
374
  }
358
375
 
359
376
  // ── Hold Tight Detection ─────────────────────────────────────
package/lib/grinder.js CHANGED
@@ -1507,210 +1507,116 @@ class AccountWorker {
1507
1507
  if (!t) return false;
1508
1508
  const lower = t.toLowerCase();
1509
1509
  return lower.includes('balance') || lower.includes('balances') || lower.includes('global rank')
1510
- || lower.includes('wallet') || /<a?:coin:\d+>/.test(t) || /<a?:bank:\d+>/.test(t);
1511
- };
1512
-
1513
- const readBalanceText = async (msg, forceCV2 = false) => {
1514
- if (!msg) return '';
1515
- const needsCv2 = forceCV2
1516
- || isCV2(msg)
1517
- || (Array.isArray(msg.components) && msg.components.length > 0
1518
- && (!msg.content || msg.content.length === 0)
1519
- && (!msg.embeds || msg.embeds.length === 0));
1520
- if (needsCv2) await ensureCV2(msg, forceCV2);
1521
- return stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim();
1522
- };
1523
-
1524
- const findRecentBalanceMessage = async () => {
1525
- if (!this.channel?.messages?.fetch) return null;
1526
- for (let attempt = 0; attempt < 6; attempt++) {
1527
- try {
1528
- const recent = await this.channel.messages.fetch({ limit: 12 });
1529
- const candidates = [...recent.values()].filter((m) =>
1530
- m?.author?.id === DANK_MEMER_ID && (m.createdTimestamp || 0) >= sentAt - 10000
1531
- );
1532
- for (const m of candidates) {
1533
- const t = await readBalanceText(m, true);
1534
- if (looksLikeBalance(t)) return m;
1535
- }
1536
- } catch {}
1537
- await new Promise((r) => setTimeout(r, 700));
1538
- }
1539
- return null;
1510
+ || lower.includes('wallet') || /<a?:coin:\d+>/i.test(t) || /<a?:bank:\d+>/i.test(t);
1540
1511
  };
1541
1512
 
1513
+ // Fast path: send command, wait 3s, read from rawLogger
1542
1514
  if (this.account.use_slash && this.channel?.sendSlash) {
1543
1515
  await this.channel.sendSlash(DANK_MEMER_ID, 'balance').catch(() => this.channel.send('/balance'));
1544
1516
  } else {
1545
1517
  await this.channel.send(`${prefix} bal`);
1546
1518
  }
1547
- let response = await this.waitForDankMemer(12000);
1519
+ await new Promise(r => setTimeout(r, 3000));
1548
1520
 
1549
- // Fallback for slash setup: try legacy prefix if no slash response.
1550
- if (!response && this.account.use_slash) {
1551
- await this.channel.send('pls bal');
1552
- response = await this.waitForDankMemer(12000);
1521
+ // Try rawLogger first it captures CV2 text from gateway instantly
1522
+ let text = '';
1523
+ const rawData = rawLogger.getLastRaw(this.channel?.id);
1524
+ if (rawData && rawData.cv2Text && looksLikeBalance(rawData.cv2Text)) {
1525
+ text = rawData.cv2Text;
1526
+ } else if (rawData && rawData.allText && looksLikeBalance(rawData.allText)) {
1527
+ text = rawData.allText;
1553
1528
  }
1554
1529
 
1555
- if (response) {
1556
- let text = await readBalanceText(response);
1557
-
1558
- // Dank Memer sometimes sends empty first payload then edits in the full card.
1559
- if ((!text || !looksLikeBalance(text)) && response.id) {
1560
- const edited = await this.waitForMessageUpdate(response.id, 8000);
1561
- if (edited) {
1562
- text = await readBalanceText(edited, true);
1563
- response = edited;
1564
- }
1530
+ // If rawLogger didn't capture it, fall back to waitForDankMemer
1531
+ if (!text || !looksLikeBalance(text)) {
1532
+ let response = await this.waitForDankMemer(8000);
1533
+ if (!response && this.account.use_slash) {
1534
+ await this.channel.send('pls bal');
1535
+ response = await this.waitForDankMemer(8000);
1565
1536
  }
1566
-
1567
- // If we received a stale/irrelevant update, fetch same message fresh.
1568
- if ((!text || !looksLikeBalance(text)) && response.id && this.channel?.messages?.fetch) {
1569
- const fetched = await Promise.resolve(this.channel.messages.fetch(response.id)).catch(() => null);
1570
- if (fetched) {
1571
- const fetchedText = await readBalanceText(fetched, true);
1572
- if (fetchedText) {
1573
- text = fetchedText;
1574
- response = fetched;
1575
- }
1576
- }
1537
+ if (response) {
1538
+ if (isCV2(response)) await ensureCV2(response);
1539
+ text = stripAnsi(getFullText(response)).replace(/\s+/g, ' ').trim();
1577
1540
  }
1578
-
1579
- // Fallback: scan latest Dank messages right after command send.
1580
- if (!text || !looksLikeBalance(text)) {
1581
- const recentBalance = await findRecentBalanceMessage();
1582
- if (recentBalance) {
1583
- text = await readBalanceText(recentBalance, true);
1584
- response = recentBalance;
1585
- }
1586
- }
1587
-
1588
- // Last resort: wait for CV2 content propagation then re-fetch
1589
- if ((!text || !looksLikeBalance(text)) && response.id && this.channel?.messages?.fetch) {
1590
- await new Promise(r => setTimeout(r, 3000));
1591
- try {
1592
- const fresh = await this.channel.messages.fetch(response.id);
1593
- if (fresh) {
1594
- const freshText = await readBalanceText(fresh, true);
1595
- if (freshText && looksLikeBalance(freshText)) {
1596
- text = freshText;
1597
- response = fresh;
1598
- }
1599
- }
1600
- } catch {}
1601
- }
1602
-
1603
- // Absolute last: re-scan channel messages after the extra wait
1541
+ // One more rawLogger check after the wait
1604
1542
  if (!text || !looksLikeBalance(text)) {
1605
- const recentBalance2 = await findRecentBalanceMessage();
1606
- if (recentBalance2) {
1607
- text = await readBalanceText(recentBalance2, true);
1608
- response = recentBalance2;
1609
- }
1610
- }
1611
-
1612
- // Raw logger fallback — CV2 text is captured directly from gateway
1613
- if (!text || !looksLikeBalance(text)) {
1614
- const rawData = rawLogger.getLastRaw(this.channel?.id);
1615
- if (rawData && rawData.cv2Text) {
1616
- const rawText = rawData.cv2Text;
1617
- if (looksLikeBalance(rawText)) {
1618
- text = rawText;
1619
- this.log('debug', 'Balance: using rawLogger CV2 text fallback');
1620
- }
1621
- }
1622
- // Also try from Redis raw message
1623
- if ((!text || !looksLikeBalance(text)) && response?.id) {
1624
- try {
1625
- const rawMsg = await rawLogger.getMsg(response.id);
1626
- if (rawMsg?.allText && looksLikeBalance(rawMsg.allText)) {
1627
- text = rawMsg.allText;
1628
- this.log('debug', 'Balance: using rawLogger Redis fallback');
1629
- }
1630
- } catch {}
1631
- }
1632
- }
1633
-
1634
- if (!text) {
1635
- this.log('warn', 'Balance response was empty after waiting for update');
1636
- return;
1637
- }
1638
-
1639
- if (!looksLikeBalance(text)) {
1640
- this.log('warn', `Balance response did not look like balance card: "${text.substring(0, 140)}"`);
1641
- return;
1543
+ const raw2 = rawLogger.getLastRaw(this.channel?.id);
1544
+ if (raw2?.cv2Text && looksLikeBalance(raw2.cv2Text)) text = raw2.cv2Text;
1545
+ else if (raw2?.allText && looksLikeBalance(raw2.allText)) text = raw2.allText;
1642
1546
  }
1547
+ }
1643
1548
 
1644
- let wallet = 0;
1645
- let bank = 0;
1646
- let matched = '';
1549
+ if (!text || !looksLikeBalance(text)) {
1550
+ this.log('warn', 'Balance: no data after all attempts');
1551
+ return;
1552
+ }
1647
1553
 
1648
- // CV2 format: <:Coin:ID> 3,272,896 and <:Bank:ID> 275,000 / 275,000
1554
+ // Parse wallet and bank
1555
+ let wallet = 0, bank = 0, matched = '';
1649
1556
  const coinMatch = text.match(/<a?:Coin:\d+>\s*([\d,]+)/i);
1650
1557
  const bankEmojiMatch = text.match(/<a?:Bank:\d+>\s*([\d,]+)/i);
1651
1558
  const bankSlashMatch = text.match(/(?:<a?:Bank:\d+>\s*)?([\d,]+)\s*\/\s*[\d,]+/i);
1652
1559
 
1653
- // Legacy embed format: Wallet: ⏣ 1,234,567
1654
- const walletMatch = text.match(/wallet[:\s]*\*{0,2}\s*[⏣💰]?\s*\*{0,2}\s*([\d,]+)/i);
1655
- const bankTextMatch = text.match(/bank[:\s]*\*{0,2}\s*[⏣💰]?\s*\*{0,2}\s*([\d,]+)/i);
1560
+ // Legacy embed format: Wallet: ⏣ 1,234,567
1561
+ const walletMatch = text.match(/wallet[:\s]*\*{0,2}\s*[⏣💰]?\s*\*{0,2}\s*([\d,]+)/i);
1562
+ const bankTextMatch = text.match(/bank[:\s]*\*{0,2}\s*[⏣💰]?\s*\*{0,2}\s*([\d,]+)/i);
1656
1563
 
1657
- // Fallback: any numbers near ⏣ or just plain numbers in CV2 text
1658
- const allNums = [...text.matchAll(/(?:⏣\s*)?(\d[\d,]*\d)/g)].map(m => parseInt(m[1].replace(/,/g, ''), 10)).filter(n => n > 0);
1564
+ // Fallback: any numbers near ⏣ or just plain numbers in CV2 text
1565
+ const allNums = [...text.matchAll(/(?:⏣\s*)?(\d[\d,]*\d)/g)].map(m => parseInt(m[1].replace(/,/g, ''), 10)).filter(n => n > 0);
1659
1566
 
1660
- if (coinMatch) {
1661
- wallet = parseInt(coinMatch[1].replace(/,/g, ''), 10);
1662
- matched = 'cv2-emoji';
1663
- } else if (walletMatch) {
1664
- wallet = parseInt(walletMatch[1].replace(/,/g, ''), 10);
1665
- matched = 'legacy-wallet';
1666
- } else if (allNums.length > 0) {
1667
- wallet = Math.max(...allNums);
1668
- matched = 'fallback-nums';
1669
- }
1567
+ if (coinMatch) {
1568
+ wallet = parseInt(coinMatch[1].replace(/,/g, ''), 10);
1569
+ matched = 'cv2-emoji';
1570
+ } else if (walletMatch) {
1571
+ wallet = parseInt(walletMatch[1].replace(/,/g, ''), 10);
1572
+ matched = 'legacy-wallet';
1573
+ } else if (allNums.length > 0) {
1574
+ wallet = Math.max(...allNums);
1575
+ matched = 'fallback-nums';
1576
+ }
1670
1577
 
1671
- if (bankEmojiMatch) {
1672
- bank = parseInt(bankEmojiMatch[1].replace(/,/g, ''), 10);
1673
- matched += matched ? '+bank-emoji' : 'bank-emoji';
1674
- } else if (bankTextMatch) {
1675
- bank = parseInt(bankTextMatch[1].replace(/,/g, ''), 10);
1676
- matched += matched ? '+bank-text' : 'bank-text';
1677
- } else if (bankSlashMatch) {
1678
- bank = parseInt(bankSlashMatch[1].replace(/,/g, ''), 10);
1679
- matched += matched ? '+bank-slash' : 'bank-slash';
1680
- }
1578
+ if (bankEmojiMatch) {
1579
+ bank = parseInt(bankEmojiMatch[1].replace(/,/g, ''), 10);
1580
+ matched += matched ? '+bank-emoji' : 'bank-emoji';
1581
+ } else if (bankTextMatch) {
1582
+ bank = parseInt(bankTextMatch[1].replace(/,/g, ''), 10);
1583
+ matched += matched ? '+bank-text' : 'bank-text';
1584
+ } else if (bankSlashMatch) {
1585
+ bank = parseInt(bankSlashMatch[1].replace(/,/g, ''), 10);
1586
+ matched += matched ? '+bank-slash' : 'bank-slash';
1587
+ }
1681
1588
 
1682
- if (wallet === 0 && bank === 0) {
1683
- this.log('warn', `Balance parse returned 0 — raw text: "${text.substring(0, 200)}"`);
1684
- // Don't overwrite a known-good balance with 0
1685
- if (this.stats.balance > 0 || this.stats.bankBalance > 0) return;
1686
- }
1589
+ if (wallet === 0 && bank === 0) {
1590
+ this.log('warn', `Balance parse returned 0 — raw text: "${text.substring(0, 200)}"`);
1591
+ // Don't overwrite a known-good balance with 0
1592
+ if (this.stats.balance > 0 || this.stats.bankBalance > 0) return;
1593
+ }
1687
1594
 
1688
1595
  this.stats.balance = wallet;
1689
1596
  this.stats.bankBalance = bank;
1690
1597
  this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} ${c.dim}(${matched || 'none'})${c.reset}`);
1691
1598
 
1692
- // Store in Redis for persistence
1693
- if (redis) {
1694
- try {
1695
- await redis.set(`dkg:bal:${this.account.id}`, JSON.stringify({ wallet, bank, ts: Date.now() }));
1696
- } catch {}
1697
- }
1698
-
1699
- // Always report to dashboard API
1599
+ // Store in Redis for persistence
1600
+ if (redis) {
1700
1601
  try {
1701
- await fetch(`${API_URL}/api/grinder/status`, {
1702
- method: 'POST',
1703
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1704
- body: JSON.stringify({
1705
- account_id: this.account.id,
1706
- balance: wallet,
1707
- bank_balance: bank,
1708
- total_balance: wallet + bank,
1709
- lifesavers: this._lifesavers ?? null,
1710
- }),
1711
- });
1712
- } catch { /* silent */ }
1602
+ await redis.set(`dkg:bal:${this.account.id}`, JSON.stringify({ wallet, bank, ts: Date.now() }));
1603
+ } catch {}
1713
1604
  }
1605
+
1606
+ // Always report to dashboard API
1607
+ try {
1608
+ await fetch(`${API_URL}/api/grinder/status`, {
1609
+ method: 'POST',
1610
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1611
+ body: JSON.stringify({
1612
+ account_id: this.account.id,
1613
+ balance: wallet,
1614
+ bank_balance: bank,
1615
+ total_balance: wallet + bank,
1616
+ lifesavers: this._lifesavers ?? null,
1617
+ }),
1618
+ });
1619
+ } catch { /* silent */ }
1714
1620
  }
1715
1621
 
1716
1622
  // ── Check DM History for deaths/level-ups ──────────────────
@@ -1805,20 +1711,68 @@ class AccountWorker {
1805
1711
  if (shutdownCalled || !this.running) return;
1806
1712
  this.stats.commands++;
1807
1713
 
1714
+ // ── Monthly: only run if balance ≥ 18M (advancements requirement) ──
1715
+ if (cmdName === 'monthly') {
1716
+ const totalBal = (this.stats.balance || 0) + (this.stats.bankBalance || 0);
1717
+ if (totalBal < 18_000_000) {
1718
+ this.log('warn', `[monthly] SKIPPED — balance ${(totalBal / 1e6).toFixed(1)}M < 18M`);
1719
+ await this.setCooldown(cmdName, 86400);
1720
+ return;
1721
+ }
1722
+ // Check if disabled (not premium / no advancement)
1723
+ if (redis) {
1724
+ try {
1725
+ const disabled = await redis.get(`dkg:disabled:${this.account.id}:monthly`);
1726
+ if (disabled) {
1727
+ this.log('warn', `[monthly] SKIPPED — disabled (needs advancement purchase)`);
1728
+ return;
1729
+ }
1730
+ } catch {}
1731
+ }
1732
+ }
1733
+
1734
+ // ── Daily/Monthly: check if already claimed today (avoid wasting a command) ──
1735
+ if (cmdName === 'daily' || cmdName === 'monthly') {
1736
+ if (redis) {
1737
+ try {
1738
+ const done = await redis.get(`dkg:done:${this.account.id}:${cmdName}`);
1739
+ if (done) {
1740
+ const ttl = await redis.ttl(`dkg:done:${this.account.id}:${cmdName}`);
1741
+ this.log('info', `[${cmdName}] already claimed — ${Math.ceil(ttl / 60)}min left`);
1742
+ await this.setCooldown(cmdName, Math.max(60, ttl));
1743
+ return;
1744
+ }
1745
+ } catch {}
1746
+ }
1747
+ }
1748
+
1749
+ // ── Deposit: max once per hour, only when enabled ──
1750
+ if (cmdName === 'dep max') {
1751
+ if (this._lastDepositAt && Date.now() - this._lastDepositAt < 3600_000) {
1752
+ return; // silently skip — too soon
1753
+ }
1754
+ }
1755
+
1808
1756
  // ── Lifesaver protection: skip crime/search if 0 lifesavers ──
1809
1757
  if (cmdName === 'crime' || cmdName === 'search') {
1758
+ // Fast path: check in-memory lifesaver count (set from inv + DM check)
1759
+ if (this._lifesavers === 0) {
1760
+ this.log('warn', `[${cmdName}] SKIPPED — 0 lifesavers (in-memory)`);
1761
+ await this.setCooldown(cmdName, 3600);
1762
+ return;
1763
+ }
1810
1764
  const noLifesaver = await rawLogger.hasNoLifesaverAlert(this.channel?.id);
1811
1765
  if (noLifesaver) {
1812
1766
  this.log('warn', `[${cmdName}] SKIPPED — no lifesavers! (death detected in DMs)`);
1813
- await this.setCooldown(cmdName, 3600); // block for 1 hour
1767
+ await this.setCooldown(cmdName, 3600);
1814
1768
  return;
1815
1769
  }
1816
- // Also check Redis key for lifesaver count
1817
1770
  if (redis) {
1818
1771
  try {
1819
1772
  const lsCount = await redis.get(`dkg:lifesavers:${this.account.id}`);
1820
1773
  if (lsCount === '0') {
1821
- this.log('warn', `[${cmdName}] SKIPPED — 0 lifesavers cached`);
1774
+ this._lifesavers = 0;
1775
+ this.log('warn', `[${cmdName}] SKIPPED — 0 lifesavers (Redis)`);
1822
1776
  await this.setCooldown(cmdName, 3600);
1823
1777
  return;
1824
1778
  }
@@ -1885,7 +1839,7 @@ class AccountWorker {
1885
1839
  // PostMemes / command-specific cooldown from response
1886
1840
  if (resultLower.includes('cannot post another meme') || resultLower.includes('dead meme')) {
1887
1841
  const minMatch = result.match(/(\d+)\s*minute/i);
1888
- const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
1842
+ const cdSec = minMatch ? parseInt(minMatch[1]) * 60 + 30 : 150; // dead meme = N min + 30s buffer
1889
1843
  this.log('warn', `${cmdName} on cooldown: ${cdSec}s`);
1890
1844
  await this.setCooldown(cmdName, cdSec);
1891
1845
  return;
@@ -1965,11 +1919,13 @@ class AccountWorker {
1965
1919
  return;
1966
1920
  }
1967
1921
 
1968
- // Premium-only command detection — disable for 24h
1922
+ // Premium-only command detection — disable permanently
1969
1923
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
1970
- resultLower.includes('buy the ability to use this command')) {
1971
- this.log('warn', `${cmdName} requires premium — skipping for 24h`);
1972
- await this.setCooldown(cmdName, 86400);
1924
+ resultLower.includes('buy the ability to use this command') ||
1925
+ resultLower.includes('advancements upgrades')) {
1926
+ this.log('warn', `${cmdName} requires premium/advancement — DISABLED`);
1927
+ await this.setCooldown(cmdName, 2592000); // 30 days
1928
+ if (redis) try { await redis.set(`dkg:disabled:${this.account.id}:${cmdName}`, '1', 'EX', 2592000); } catch {}
1973
1929
  return;
1974
1930
  }
1975
1931
 
@@ -2076,9 +2032,12 @@ class AccountWorker {
2076
2032
  }
2077
2033
  }
2078
2034
 
2079
- // Smart auto-deposit: when wallet exceeds threshold, deposit to protect from robbery
2080
- if (earned > 0 && this.stats.balance > this._autoDepositThreshold) {
2035
+ // Smart auto-deposit: max once per hour, only if deposit is enabled
2036
+ const depositEnabled = this.account.cmd_deposit !== false;
2037
+ const depositCooldownOk = !this._lastDepositAt || Date.now() - this._lastDepositAt >= 3600_000;
2038
+ if (depositEnabled && depositCooldownOk && earned > 0 && this.stats.balance > this._autoDepositThreshold) {
2081
2039
  this.log('info', `Wallet ⏣ ${this.stats.balance.toLocaleString()} exceeds threshold — auto-depositing`);
2040
+ this._lastDepositAt = Date.now();
2082
2041
  try {
2083
2042
  await this.channel.send('pls dep max');
2084
2043
  await this.waitForDankMemer(6000);
@@ -2172,10 +2131,10 @@ class AccountWorker {
2172
2131
  { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
2173
2132
  // Time-gated (run ASAP when available)
2174
2133
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
2175
- { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
2134
+ // weekly removed premium only, not available for free users
2176
2135
  { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000,priority: 10 },
2177
2136
  // Financial safety
2178
- { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 120, priority: 8 },
2137
+ { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
2179
2138
  { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
2180
2139
  // Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
2181
2140
  ].map(Object.freeze);
@@ -3171,16 +3130,16 @@ async function start(apiKey, apiUrl) {
3171
3130
  let balDone = 0;
3172
3131
  const balProgressInterval = setInterval(() => {
3173
3132
  const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3174
- 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} `);
3133
+ 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}${activeWorkers.length}${c.reset} `);
3175
3134
  }, 80);
3176
3135
 
3177
- // Run sequentially parallel causes CV2 text to be empty (raw logger timing)
3178
- for (const w of activeWorkers) {
3136
+ // Run in parallel
3137
+ await Promise.all(activeWorkers.map(async w => {
3179
3138
  try {
3180
3139
  await w.checkBalance();
3140
+ balDone++;
3181
3141
  } catch {}
3182
- balDone++;
3183
- }
3142
+ }));
3184
3143
 
3185
3144
  clearInterval(balProgressInterval);
3186
3145
  process.stdout.write(`\r${c.clearLine}`);
@@ -3213,6 +3172,9 @@ async function start(apiKey, apiUrl) {
3213
3172
  if (dm.deaths > 0) dmDeaths += dm.deaths;
3214
3173
  if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
3215
3174
  if (dm.lifesavers === 0) dmNoLs.push(w.username);
3175
+ // Store level and lifesaver for dashboard
3176
+ if (dm.currentLevel > 0) w._level = dm.currentLevel;
3177
+ if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
3216
3178
  const parts = [];
3217
3179
  if (dm.currentLevel > 0) parts.push(`Lv${dm.currentLevel}`);
3218
3180
  if (dm.deaths > 0) parts.push(`${rgb(239, 68, 68)}${dm.deaths} deaths${c.reset}`);
@@ -3227,6 +3189,15 @@ async function start(apiKey, apiUrl) {
3227
3189
  }
3228
3190
  if (dmNoLs.length > 0) {
3229
3191
  console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}DM confirms 0 lifesavers:${c.reset} ${dmNoLs.join(', ')}`);
3192
+ // Set Redis keys to block crime/search
3193
+ for (const w of activeWorkers) {
3194
+ if (dmNoLs.includes(w.username) && redis) {
3195
+ try {
3196
+ await redis.set(`dkg:lifesavers:${w.account.id}`, '0', 'EX', 86400);
3197
+ await redis.set(`raw:alert:no-lifesaver:${w.channel?.id}`, '1', 'EX', 86400);
3198
+ } catch {}
3199
+ }
3200
+ }
3230
3201
  }
3231
3202
  console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}DM check${c.reset} ${c.dim}${dmDeaths} deaths, ${dmLevelUps} level-ups found${c.reset}`);
3232
3203
  console.log('');
package/lib/rawLogger.js CHANGED
@@ -152,6 +152,9 @@ function detectCommand(d) {
152
152
  if (cv2Text.includes('fishing') || cv2Text.includes('fisherfolk')) return 'fish';
153
153
  if (cv2Text.includes('deposit') || cv2Text.includes('bank account')) return 'deposit';
154
154
  if (cv2Text.includes('begging') || cv2Text.includes('imagine begging')) return 'beg';
155
+ if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle')) return 'hunt';
156
+ if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig')) return 'dig';
157
+ if (cv2Text.includes('great work') || cv2Text.includes('for your shift') || cv2Text.includes('working as') || cv2Text.includes('work shift') || cv2Text.includes('what color was') || cv2Text.includes('remember words order') || cv2Text.includes('remember the colors') || cv2Text.includes('remember the emojis') || cv2Text.includes('what word was repeated') || cv2Text.includes('unscramble') || cv2Text.includes('remember the number') || cv2Text.includes('click the buttons in correct order') || cv2Text.includes('babysitter') || cv2Text.includes('click the matching')) return 'work';
155
158
  if (cv2Text.includes('weekly')) return 'weekly';
156
159
  if (cv2Text.includes('daily')) return 'daily';
157
160
  if (cv2Text.includes('inventory')) return 'inventory';
@@ -174,11 +177,13 @@ function detectCommand(d) {
174
177
  if (embedText.includes('you searched') || embedText.includes('searched the')) return 'search';
175
178
  if (embedText.includes('you committed') || embedText.includes('went outside')) return 'crime';
176
179
  // Hunt / dig
177
- if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the')) return 'hunt';
178
- if (embedText.includes('you dig') || embedText.includes('found a') && embedText.includes('digging') || embedText.includes('shovel')) return 'dig';
179
- // Work
180
- if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color'))) return 'work';
180
+ if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('hunt') && (embedText.includes('caught') || embedText.includes('brought back') || embedText.includes('attacked') || embedText.includes('nothing') || embedText.includes('laughed') || embedText.includes('escaped') || embedText.includes('fell asleep'))) return 'hunt';
181
+ if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('found a') && embedText.includes('dig') || embedText.includes('shovel') || embedText.includes('found nothing while') || embedText.includes('you found') && !embedText.includes('search')) return 'dig';
182
+ // Work — match both minigame prompt AND completion
183
+ if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color') || embedText.includes('babysitter') || embedText.includes('great work') || embedText.includes('for your shift'))) return 'work';
181
184
  if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
185
+ if (embedText.includes('working as') || embedText.includes('for your shift')) return 'work';
186
+ if (embedText.includes('remember words order') || embedText.includes('remember the colors') || embedText.includes('remember the emojis') || embedText.includes('what word was repeated') || embedText.includes('unscramble the word') || embedText.includes('remember the number') || embedText.includes('click the buttons in correct order') || embedText.includes('click the matching')) return 'work';
182
187
  // Postmemes
183
188
  if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
184
189
  // Stream
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.16.0",
3
+ "version": "6.19.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"