dankgrinder 4.3.0 → 4.5.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.
@@ -365,9 +365,12 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
365
365
 
366
366
  // ── Select adventure type from dropdown ─────────────────────
367
367
  if (menus.length > 0) {
368
- // Re-fetch message to get hydrated components (minValues/maxValues)
369
- const freshMsg = await channel.messages.fetch(response.id).catch(() => null);
370
- if (freshMsg) response = freshMsg;
368
+ try {
369
+ if (channel.messages && typeof channel.messages.fetch === 'function') {
370
+ const freshMsg = await channel.messages.fetch(response.id);
371
+ if (freshMsg) response = freshMsg;
372
+ }
373
+ } catch { /* proceed with original */ }
371
374
 
372
375
  // Find the select menu row index
373
376
  let menuRowIdx = -1;
@@ -0,0 +1,116 @@
1
+ const {
2
+ LOG, c, getFullText, parseCoins, getAllButtons,
3
+ safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
4
+ } = require('./utils');
5
+
6
+ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
7
+ LOG.cmd(`${c.white}${c.bold}pls drops${c.reset}`);
8
+
9
+ await channel.send('pls drops');
10
+ const response = await waitForDankMemer(10000);
11
+
12
+ if (!response) return { result: 'no response', coins: 0 };
13
+
14
+ if (isHoldTight(response)) {
15
+ const reason = getHoldTightReason(response);
16
+ return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
17
+ }
18
+
19
+ logMsg(response, 'drops');
20
+ const text = getFullText(response);
21
+ const lower = text.toLowerCase();
22
+
23
+ const drops = [];
24
+
25
+ for (const embed of response.embeds || []) {
26
+ const desc = embed.description || '';
27
+ const items = desc.split(/\n\n+/);
28
+ for (const block of items) {
29
+ const nameMatch = block.match(/\*\*(.+?)\*\*/);
30
+ const costMatch = block.match(/Cost:\s*[⏣💰]?\s*([\d,]+)/i);
31
+ const stockMatch = block.match(/Stock:\s*(\d+|Infinite)/i);
32
+ const dateMatch = block.match(/<t:(\d+):[tTdDfFR]>/);
33
+ if (nameMatch) {
34
+ drops.push({
35
+ name: nameMatch[1],
36
+ cost: costMatch ? parseInt(costMatch[1].replace(/,/g, '')) : 0,
37
+ stock: stockMatch ? (stockMatch[1] === 'Infinite' ? Infinity : parseInt(stockMatch[1])) : 0,
38
+ dropTimestamp: dateMatch ? parseInt(dateMatch[1]) : 0,
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ // Also parse from text lines
45
+ const textBlocks = text.split(/\n/).filter(l => l.trim());
46
+ for (let i = 0; i < textBlocks.length; i++) {
47
+ const line = textBlocks[i];
48
+ if (line.includes('Cost:') && i > 0) {
49
+ const prevLine = textBlocks[i - 1];
50
+ const nameMatch = prevLine.match(/\*\*(.+?)\*\*/) || prevLine.match(/(\w[\w\s']+)/);
51
+ const costMatch = line.match(/Cost:\s*[⏣💰o]?\s*([\d,]+)/i);
52
+ if (nameMatch && costMatch) {
53
+ const cost = parseInt(costMatch[1].replace(/,/g, ''));
54
+ const existing = drops.find(d => d.name === nameMatch[1]);
55
+ if (!existing && cost > 0) {
56
+ drops.push({ name: nameMatch[1], cost, stock: 0, dropTimestamp: 0 });
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ // Store drop schedule in Redis: track dates so we can auto-buy
63
+ if (redis && drops.length > 0) {
64
+ try {
65
+ await redis.set('dkg:drops:latest', JSON.stringify(drops), 'EX', 172800);
66
+ // Schedule individual drops with their timestamps
67
+ for (const drop of drops) {
68
+ if (drop.dropTimestamp > 0) {
69
+ const key = `dkg:drops:schedule:${drop.name.replace(/\s+/g, '_').toLowerCase()}`;
70
+ await redis.set(key, JSON.stringify(drop), 'EX', 172800);
71
+ }
72
+ }
73
+ } catch {}
74
+ }
75
+
76
+ // Try to buy any drops that are currently available (stock > 0, no future date)
77
+ const now = Math.floor(Date.now() / 1000);
78
+ const buttons = getAllButtons(response);
79
+ let boughtCount = 0;
80
+ for (const drop of drops) {
81
+ if (drop.stock > 0 && (drop.dropTimestamp === 0 || drop.dropTimestamp <= now)) {
82
+ const buyBtn = buttons.find(b =>
83
+ !b.disabled && b.label && (
84
+ b.label.toLowerCase().includes('buy') ||
85
+ b.label.toLowerCase().includes(drop.name.toLowerCase().split(' ')[0])
86
+ )
87
+ );
88
+ if (buyBtn) {
89
+ try {
90
+ await humanDelay(100, 300);
91
+ await safeClickButton(response, buyBtn);
92
+ boughtCount++;
93
+ LOG.coin(`[drops] ${c.green}Bought ${drop.name}!${c.reset}`);
94
+ } catch {}
95
+ }
96
+ }
97
+ }
98
+
99
+ if (drops.length > 0) {
100
+ const summary = drops.slice(0, 3).map(d => {
101
+ const ts = d.dropTimestamp > 0 ? ` @${new Date(d.dropTimestamp * 1000).toLocaleDateString()}` : '';
102
+ return `${d.name}(⏣${d.cost > 0 ? d.cost.toLocaleString() : '?'}${ts})`;
103
+ }).join(', ');
104
+ const boughtMsg = boughtCount > 0 ? ` — bought ${boughtCount}` : '';
105
+ LOG.info(`[drops] ${drops.length} drops: ${summary}${boughtMsg}`);
106
+ return { result: `${drops.length} drops: ${summary}${boughtMsg}`, coins: 0 };
107
+ }
108
+
109
+ if (lower.includes('no drops') || lower.includes('nothing')) {
110
+ return { result: 'no drops available', coins: 0 };
111
+ }
112
+
113
+ return { result: text.substring(0, 50) || 'drops checked', coins: 0 };
114
+ }
115
+
116
+ module.exports = { runDrops };
@@ -70,26 +70,29 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
70
70
  }
71
71
 
72
72
  const lower = text.toLowerCase();
73
- if (lower.includes('won')) return { result: `${cmdName} → ${c.green}won${c.reset}`, coins: 0 };
74
- if (lower.includes('lost')) return { result: `${cmdName} → ${c.red}lost${c.reset}`, coins: 0 };
73
+ if (lower.includes('won')) return { result: `${cmdName} → won`, coins };
74
+ if (lower.includes('lost')) {
75
+ const lostCoins = parseCoins(text);
76
+ return { result: `${cmdName} → lost`, coins: 0, lost: lostCoins };
77
+ }
75
78
 
76
79
  return { result: `${cmdName} done`, coins: 0 };
77
80
  }
78
81
 
79
82
  // Convenience wrappers
80
- async function runCoinflip({ channel, waitForDankMemer, betAmount = 1000 }) {
83
+ async function runCoinflip({ channel, waitForDankMemer, betAmount = 5000 }) {
81
84
  return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
82
85
  }
83
86
 
84
- async function runRoulette({ channel, waitForDankMemer, betAmount = 1000 }) {
87
+ async function runRoulette({ channel, waitForDankMemer, betAmount = 5000 }) {
85
88
  return runGamble({ channel, waitForDankMemer, cmdName: 'roulette', cmdString: `pls roulette ${betAmount} red` });
86
89
  }
87
90
 
88
- async function runSlots({ channel, waitForDankMemer, betAmount = 1000 }) {
91
+ async function runSlots({ channel, waitForDankMemer, betAmount = 5000 }) {
89
92
  return runGamble({ channel, waitForDankMemer, cmdName: 'slots', cmdString: `pls slots ${betAmount}` });
90
93
  }
91
94
 
92
- async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 1000 }) {
95
+ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
93
96
  return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
94
97
  }
95
98
 
@@ -99,9 +99,12 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
99
99
  // Handle select menus
100
100
  const menus = getAllSelectMenus(response);
101
101
  if (menus.length > 0) {
102
- // Re-fetch for hydrated components (minValues/maxValues)
103
- const freshMsg = await channel.messages.fetch(response.id).catch(() => null);
104
- if (freshMsg) response = freshMsg;
102
+ try {
103
+ if (channel.messages && typeof channel.messages.fetch === 'function') {
104
+ const freshMsg = await channel.messages.fetch(response.id);
105
+ if (freshMsg) response = freshMsg;
106
+ }
107
+ } catch { /* proceed with original */ }
105
108
  // Find row index of first select menu
106
109
  let menuRowIdx = -1;
107
110
  for (let i = 0; i < (response.components || []).length; i++) {
@@ -20,6 +20,7 @@ const { runCoinflip, runRoulette, runSlots, runSnakeeyes, runGamble } = require(
20
20
  const { runDeposit } = require('./deposit');
21
21
  const { runGeneric, runAlert } = require('./generic');
22
22
  const { runStream } = require('./stream');
23
+ const { runDrops } = require('./drops');
23
24
  const { buyItem, ITEM_COSTS } = require('./shop');
24
25
  const { getPlayerLevel, meetsLevelRequirement } = require('./profile');
25
26
 
@@ -47,6 +48,7 @@ module.exports = {
47
48
  runGeneric,
48
49
  runAlert,
49
50
  runStream,
51
+ runDrops,
50
52
  buyItem,
51
53
 
52
54
  // Profile / Level
@@ -79,9 +79,13 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
79
79
  }
80
80
 
81
81
  // Step 2: Navigate to Coin Shop tab
82
- // Re-fetch message to get hydrated components (minValues/maxValues for selectMenu)
83
- const freshShopMsg = await channel.messages.fetch(response.id).catch(() => null);
84
- if (freshShopMsg) response = freshShopMsg;
82
+ // Try re-fetch for hydrated components; skip if unavailable
83
+ try {
84
+ if (channel.messages && typeof channel.messages.fetch === 'function') {
85
+ const freshShopMsg = await channel.messages.fetch(response.id);
86
+ if (freshShopMsg) response = freshShopMsg;
87
+ }
88
+ } catch { /* proceed with original response */ }
85
89
 
86
90
  const csInfo = findSelectMenuOption(response, 'Coin Shop');
87
91
  if (csInfo) {
@@ -1,5 +1,5 @@
1
1
  const {
2
- LOG, c, getFullText, parseCoins, getAllButtons,
2
+ LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
3
3
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
4
4
  } = require('./utils');
5
5
  const { buyItem } = require('./shop');
@@ -10,7 +10,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
10
10
  LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
11
11
 
12
12
  await channel.send('pls stream');
13
- let response = await waitForDankMemer(10000);
13
+ let response = await waitForDankMemer(12000);
14
14
 
15
15
  if (!response) {
16
16
  LOG.warn('[stream] No response');
@@ -19,78 +19,133 @@ async function runStream({ channel, waitForDankMemer, client }) {
19
19
 
20
20
  if (isHoldTight(response)) {
21
21
  const reason = getHoldTightReason(response);
22
- LOG.warn(`[stream] Hold Tight${reason ? ` (reason: /${reason})` : ''}`);
23
- await sleep(30000);
24
22
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
25
23
  }
26
24
 
27
25
  logMsg(response, 'stream');
28
26
  let text = getFullText(response);
27
+ let lower = text.toLowerCase();
29
28
 
30
- const lower = text.toLowerCase();
31
- if (lower.includes('missing items') || lower.includes('need following items') || lower.includes('need a keyboard') || lower.includes('need a mouse')) {
29
+ // Missing items — buy keyboard + mouse
30
+ if (lower.includes('missing items') || lower.includes('need following items') ||
31
+ lower.includes('need a keyboard') || lower.includes('need a mouse')) {
32
32
  const itemsToBuy = [];
33
33
  if (lower.includes('keyboard')) itemsToBuy.push('keyboard');
34
34
  if (lower.includes('mouse')) itemsToBuy.push('mouse');
35
-
36
35
  if (itemsToBuy.length === 0) itemsToBuy.push(...STREAM_ITEMS);
37
36
 
38
- LOG.warn(`[stream] Missing items: ${c.bold}${itemsToBuy.join(', ')}${c.reset} — auto-buying...`);
39
-
40
37
  for (const item of itemsToBuy) {
41
38
  const bought = await buyItem({ channel, waitForDankMemer, client, itemName: item, quantity: 1 });
42
- if (bought) {
43
- LOG.success(`[stream] Bought ${c.bold}${item}${c.reset}`);
44
- } else {
45
- LOG.error(`[stream] Failed to buy ${item}`);
46
- return { result: `need ${item} (buy failed)`, coins: 0 };
47
- }
48
- await humanDelay(1500, 2500);
39
+ if (!bought) return { result: `need ${item} (buy failed)`, coins: 0 };
40
+ await humanDelay(500, 1000);
49
41
  }
50
42
 
51
- LOG.info('[stream] Retrying stream after buying items...');
52
- await sleep(3000);
43
+ await sleep(2000);
53
44
  await channel.send('pls stream');
54
- response = await waitForDankMemer(10000);
45
+ response = await waitForDankMemer(12000);
55
46
  if (!response) return { result: 'no response after buy', coins: 0 };
56
47
  logMsg(response, 'stream-retry');
57
48
  text = getFullText(response);
49
+ lower = text.toLowerCase();
58
50
  }
59
51
 
60
- const coins = parseCoins(text);
52
+ // Stream Manager — select a game from dropdown, then click "Go Live"
53
+ if (lower.includes('stream manager') || lower.includes('what game do you want to stream')) {
54
+ const menus = getAllSelectMenus(response);
55
+ if (menus.length > 0) {
56
+ const menu = menus[0];
57
+ const options = menu.options || [];
58
+ if (options.length > 0) {
59
+ // Pick a random game (variety is better for Dank Memer)
60
+ const pick = options[Math.floor(Math.random() * options.length)];
61
+ LOG.info(`[stream] Selecting game: "${pick.label}"`);
62
+
63
+ let menuRowIdx = -1;
64
+ for (let i = 0; i < (response.components || []).length; i++) {
65
+ for (const comp of (response.components[i].components || [])) {
66
+ if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
67
+ }
68
+ if (menuRowIdx >= 0) break;
69
+ }
61
70
 
62
- const buttons = getAllButtons(response);
63
- if (buttons.length > 0) {
64
- const actionBtn = buttons.find(b => !b.disabled && b.label &&
65
- !b.label.toLowerCase().includes('end') && !b.label.toLowerCase().includes('stop'));
66
- const btn = actionBtn || buttons.find(b => !b.disabled);
67
- if (btn) {
68
- LOG.info(`[stream] Clicking "${btn.label || '?'}"`);
69
- await humanDelay();
70
- try {
71
- await safeClickButton(response, btn);
72
- const followUp = await waitForDankMemer(8000);
73
- if (followUp) {
74
- logMsg(followUp, 'stream-action');
75
- const fText = getFullText(followUp);
76
- const fCoins = parseCoins(fText);
77
- if (fCoins > 0) {
78
- LOG.coin(`[stream] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
79
- return { result: `+⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
71
+ if (menuRowIdx >= 0) {
72
+ try {
73
+ const selectResult = await response.selectMenu(menuRowIdx, [pick.value]);
74
+ if (selectResult) {
75
+ response = selectResult;
76
+ logMsg(response, 'stream-game-selected');
77
+ }
78
+ } catch (e) {
79
+ LOG.error(`[stream] Select game failed: ${e.message}`);
80
80
  }
81
+ await humanDelay(200, 500);
82
+ }
83
+ }
84
+ }
85
+
86
+ // Click "Go Live" button
87
+ const buttons = getAllButtons(response);
88
+ const goLiveBtn = buttons.find(b => !b.disabled && b.label && b.label.toLowerCase().includes('go live'));
89
+ if (goLiveBtn) {
90
+ LOG.info('[stream] Clicking "Go Live"');
91
+ await humanDelay(100, 300);
92
+ try {
93
+ const liveResult = await safeClickButton(response, goLiveBtn);
94
+ if (liveResult) {
95
+ response = liveResult;
96
+ logMsg(response, 'stream-live');
97
+ text = getFullText(response);
98
+ lower = text.toLowerCase();
81
99
  }
82
100
  } catch (e) {
83
- LOG.error(`[stream] Click error: ${e.message}`);
101
+ LOG.error(`[stream] Go Live click failed: ${e.message}`);
102
+ }
103
+ } else {
104
+ const anyBtn = buttons.find(b => !b.disabled && b.label && !b.label.toLowerCase().includes('back'));
105
+ if (anyBtn) {
106
+ try { await safeClickButton(response, anyBtn); } catch {}
84
107
  }
85
108
  }
86
109
  }
87
110
 
88
- if (coins > 0) {
89
- LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
90
- return { result: `+⏣ ${coins.toLocaleString()}`, coins };
111
+ // After going live, there might be interactive stream events
112
+ // Keep clicking non-end buttons for up to 5 rounds
113
+ for (let round = 0; round < 5; round++) {
114
+ const followUp = await waitForDankMemer(15000);
115
+ if (!followUp) break;
116
+
117
+ logMsg(followUp, `stream-round-${round}`);
118
+ const fText = getFullText(followUp);
119
+ const fLower = fText.toLowerCase();
120
+ const fCoins = parseCoins(fText);
121
+
122
+ if (fLower.includes('stream ended') || fLower.includes('stream is over') || fLower.includes('you earned')) {
123
+ if (fCoins > 0) {
124
+ LOG.coin(`[stream] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
125
+ return { result: `stream → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins, nextCooldownSec: 600 };
126
+ }
127
+ return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
128
+ }
129
+
130
+ const btns = getAllButtons(followUp);
131
+ const actionBtn = btns.find(b => !b.disabled && b.label &&
132
+ !b.label.toLowerCase().includes('end') && !b.label.toLowerCase().includes('stop') && !b.label.toLowerCase().includes('back'));
133
+ if (actionBtn) {
134
+ LOG.info(`[stream] Clicking "${actionBtn.label}"`);
135
+ await humanDelay(100, 300);
136
+ try { await safeClickButton(followUp, actionBtn); } catch {}
137
+ } else if (btns.length > 0) {
138
+ const first = btns.find(b => !b.disabled);
139
+ if (first) { try { await safeClickButton(followUp, first); } catch {} }
140
+ } else {
141
+ break;
142
+ }
91
143
  }
92
144
 
93
- return { result: text.substring(0, 60) || 'streamed', coins: 0, nextCooldownSec: 600 };
145
+ const coins = parseCoins(text);
146
+ if (coins > 0) return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
147
+
148
+ return { result: text.substring(0, 50) || 'streamed', coins: 0, nextCooldownSec: 600 };
94
149
  }
95
150
 
96
151
  module.exports = { runStream };
@@ -13,7 +13,13 @@ const c = {
13
13
  };
14
14
 
15
15
  // ── Logging ──────────────────────────────────────────────────
16
+ // When dashboard is active, suppress direct console output from command handlers.
17
+ // grinder.js sets this to true once the live dashboard starts rendering.
18
+ let _dashboardActive = false;
19
+ function setDashboardActive(val) { _dashboardActive = val; }
20
+
16
21
  function log(label, msg) {
22
+ if (_dashboardActive) return;
17
23
  const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
18
24
  console.log(` ${c.dim}${time}${c.reset} ${label} ${msg}`);
19
25
  }
@@ -32,7 +38,7 @@ const LOG = {
32
38
  // ── Sleep ────────────────────────────────────────────────────
33
39
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
34
40
 
35
- function humanDelay(min = 100, max = 350) {
41
+ function humanDelay(min = 50, max = 200) {
36
42
  return new Promise(r => setTimeout(r, min + Math.random() * (max - min)));
37
43
  }
38
44
 
@@ -277,6 +283,7 @@ module.exports = {
277
283
  DANK_MEMER_ID,
278
284
  c,
279
285
  LOG,
286
+ setDashboardActive,
280
287
  sleep,
281
288
  humanDelay,
282
289
  getFullText,
package/lib/grinder.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
2
  const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
+ const { setDashboardActive, getFullText } = require('./commands/utils');
4
5
 
5
6
  // ── Terminal Colors & ANSI ───────────────────────────────────
6
7
  const c = {
@@ -283,7 +284,7 @@ async function reportEarnings(accountId, accountName, earned, spent, command) {
283
284
  function randomDelay(min, max) {
284
285
  return new Promise((r) => setTimeout(r, (Math.random() * (max - min) + min) * 1000));
285
286
  }
286
- function humanDelay(min = 300, max = 800) {
287
+ function humanDelay(min = 80, max = 250) {
287
288
  return new Promise((r) => setTimeout(r, min + Math.random() * (max - min)));
288
289
  }
289
290
  function safeParseJSON(str, fallback = []) {
@@ -758,10 +759,11 @@ class AccountWorker {
758
759
  // handles Hold Tight / cooldowns / item-buying internally.
759
760
  async runCommand(cmdName, prefix) {
760
761
  let cmdString;
761
- const betAmount = this.account.bet_amount || 1000;
762
+ const betAmount = Math.max(5000, this.account.bet_amount || 5000);
762
763
 
763
764
  switch (cmdName) {
764
765
  case 'dep max': cmdString = `${prefix} dep max`; break;
766
+ case 'with max': cmdString = `${prefix} with max`; break;
765
767
  case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
766
768
  case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
767
769
  case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
@@ -808,6 +810,7 @@ class AccountWorker {
808
810
  case 'dep max': cmdResult = await commands.runDeposit(cmdOpts); break;
809
811
  case 'alert': cmdResult = await commands.runAlert(cmdOpts); break;
810
812
  case 'stream': cmdResult = await commands.runStream(cmdOpts); break;
813
+ case 'drops': cmdResult = await commands.runDrops(cmdOpts); break;
811
814
  default: cmdResult = await commands.runGeneric({ ...cmdOpts, cmdString, cmdName }); break;
812
815
  }
813
816
 
@@ -822,12 +825,37 @@ class AccountWorker {
822
825
  return;
823
826
  }
824
827
 
825
- // Captcha/verification detection — pause immediately
826
- if (resultLower.includes('captcha') || resultLower.includes('verification') ||
827
- resultLower.includes('are you human') || resultLower.includes("prove you're not a bot")) {
828
- this.log('error', `CAPTCHA DETECTED! Worker PAUSED.`);
828
+ // Captcha/verification detection — deactivate account and stop
829
+ if (resultLower.includes('captcha') || resultLower.includes('verification required') ||
830
+ resultLower.includes('verify your account') || resultLower.includes('pass verification') ||
831
+ resultLower.includes('are you human') || resultLower.includes("prove you're not a bot") ||
832
+ resultLower.includes('complete the captcha') || resultLower.includes('continue playing')) {
833
+ this.log('error', `VERIFICATION REQUIRED! Deactivating account.`);
834
+ this.log('error', `Solve it in Discord, then re-enable from dashboard.`);
829
835
  this.paused = true;
830
- await sendLog(this.username, cmdString, 'CAPTCHA DETECTED — worker paused', 'error');
836
+ this.account.active = false;
837
+ // Deactivate in DB so dashboard shows it as paused
838
+ try {
839
+ await fetch(`${API_URL}/api/grinder/status`, {
840
+ method: 'POST',
841
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
842
+ body: JSON.stringify({ account_id: this.account.id, active: false }),
843
+ });
844
+ } catch {}
845
+ await sendLog(this.username, cmdString, 'VERIFICATION — account deactivated', 'error');
846
+ return;
847
+ }
848
+
849
+ // Min bet detection — raise bet amount
850
+ if (resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
851
+ const betMatch = result.match(/⏣\s*([\d,]+)/);
852
+ if (betMatch) {
853
+ const minBet = parseInt(betMatch[1].replace(/,/g, ''));
854
+ if (minBet > 0) {
855
+ this.account.bet_amount = minBet;
856
+ this.log('info', `${cmdName} min bet: ⏣ ${minBet.toLocaleString()}`);
857
+ }
858
+ }
831
859
  return;
832
860
  }
833
861
 
@@ -853,6 +881,13 @@ class AccountWorker {
853
881
  return;
854
882
  }
855
883
 
884
+ // Scratch level-gate: if level too low, disable for 24h
885
+ if (cmdResult.skipReason === 'level') {
886
+ this.log('warn', `${cmdName} level too low — retry in 24h`);
887
+ await this.setCooldown(cmdName, 86400);
888
+ return;
889
+ }
890
+
856
891
  const earned = Math.max(0, cmdResult.coins || 0);
857
892
  const spent = Math.max(0, cmdResult.lost || 0);
858
893
  if (earned > 0) this.stats.coins += earned;
@@ -899,6 +934,7 @@ class AccountWorker {
899
934
 
900
935
  // ── Command Map (shared across ticks, used to build the heap) ──
901
936
  static COMMAND_MAP = [
937
+ // Core grinding commands (short cooldowns, run frequently)
902
938
  { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
903
939
  { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 4 },
904
940
  { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 3 },
@@ -909,23 +945,25 @@ class AccountWorker {
909
945
  { key: 'cmd_fish', cmd: 'fish', cdKey: 'cd_fish', defaultCd: 20, priority: 1 },
910
946
  { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 2 },
911
947
  { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 1 },
948
+ { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
949
+ // Gambling (fast cycle)
912
950
  { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
913
951
  { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
914
952
  { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
915
953
  { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
916
954
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
917
- { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
955
+ // Utility
918
956
  { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 1 },
919
- { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 60, priority: 2 },
920
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 8 },
921
- { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 60, priority: 7 },
957
+ // Interactive commands
958
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 6 },
959
+ { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 21600, priority: 4 },
960
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 4 },
961
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 3600, priority: 6 },
962
+ // Time-gated
922
963
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
923
- { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
924
- { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
925
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 3600, priority: 8 },
926
- { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 3600, priority: 6 },
927
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 6 },
928
- { key: 'cmd_alert', cmd: 'alert', cdKey: 'cd_alert', defaultCd: 300, priority: 9 },
964
+ { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 300, priority: 7 },
965
+ { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
966
+ // Alert is NOT scheduled it's reactive (listener-based, see grindLoop)
929
967
  ];
930
968
 
931
969
  buildCommandQueue() {
@@ -948,6 +986,38 @@ class AccountWorker {
948
986
  return heap;
949
987
  }
950
988
 
989
+ // Merge config changes into existing queue without resetting cooldown timings.
990
+ // Uses a HashMap to drain old items, then rebuilds preserving nextRunAt.
991
+ mergeCommandQueue() {
992
+ const enabled = new Set(
993
+ AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key])).map(ci => ci.cmd)
994
+ );
995
+ if (enabled.size === 0) return;
996
+
997
+ const existing = new Map();
998
+ if (this.commandQueue) {
999
+ while (this.commandQueue.size > 0) {
1000
+ const item = this.commandQueue.pop();
1001
+ existing.set(item.cmd, item);
1002
+ }
1003
+ }
1004
+
1005
+ const heap = new MinHeap();
1006
+ const now = Date.now();
1007
+ for (const info of AccountWorker.COMMAND_MAP) {
1008
+ if (!enabled.has(info.cmd)) continue;
1009
+ const old = existing.get(info.cmd);
1010
+ if (old) {
1011
+ old.info = info;
1012
+ old.priority = info.priority;
1013
+ heap.push(old);
1014
+ } else {
1015
+ heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
1016
+ }
1017
+ }
1018
+ this.commandQueue = heap;
1019
+ }
1020
+
951
1021
  // ── Health Check: verify Discord client is still connected ──
952
1022
  async healthCheck() {
953
1023
  if (!this.running || shutdownCalled) return;
@@ -1042,7 +1112,7 @@ class AccountWorker {
1042
1112
  await this.setCooldown(item.cmd, totalWait);
1043
1113
 
1044
1114
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1045
- const globalJitter = 1500 + Math.random() * 1500;
1115
+ const globalJitter = 500 + Math.random() * 1000;
1046
1116
  if (timeSinceLastCmd < globalJitter) {
1047
1117
  await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1048
1118
  }
@@ -1098,14 +1168,33 @@ class AccountWorker {
1098
1168
  this.commandQueue = this.buildCommandQueue();
1099
1169
  this.lastHealthCheck = Date.now();
1100
1170
 
1171
+ // Reactive alert listener: run `pls alert` only when Dank Memer
1172
+ // sends an unsolicited message containing alert-like keywords
1173
+ this._alertHandler = (msg) => {
1174
+ if (!this.running || this.paused || this.dashboardPaused || shutdownCalled) return;
1175
+ if (msg.author?.id !== DANK_MEMER_ID || msg.channel?.id !== this.channel?.id) return;
1176
+ const text = getFullText(msg).toLowerCase();
1177
+ if (text.includes('alert') || text.includes('notification') ||
1178
+ text.includes('you have a pending') || text.includes('check your alerts')) {
1179
+ if (!this.busy) {
1180
+ this.log('info', 'Alert detected → running pls alert');
1181
+ this.busy = true;
1182
+ const prefix = this.account.use_slash ? '/' : 'pls';
1183
+ this.runCommand('alert', prefix).finally(() => { this.busy = false; });
1184
+ }
1185
+ }
1186
+ };
1187
+ this.client.on('messageCreate', this._alertHandler);
1188
+
1101
1189
  this.configInterval = setInterval(async () => {
1102
1190
  if (!this.running) return;
1103
1191
  await this.refreshConfig();
1104
1192
 
1105
1193
  if (this.account.active && this.paused) {
1106
- this.log('success', 'Captcha cleared! Resuming...');
1194
+ this.log('success', 'Account re-enabled! Resuming...');
1107
1195
  this.paused = false;
1108
1196
  this.dashboardPaused = false;
1197
+ if (this.commandQueue) this.mergeCommandQueue();
1109
1198
  }
1110
1199
 
1111
1200
  if (!this.account.active && !this.dashboardPaused) {
@@ -1117,7 +1206,7 @@ class AccountWorker {
1117
1206
  }
1118
1207
 
1119
1208
  if (this.commandQueue && !shutdownCalled) {
1120
- this.commandQueue = this.buildCommandQueue();
1209
+ this.mergeCommandQueue();
1121
1210
  }
1122
1211
  }, 15000);
1123
1212
 
@@ -1184,8 +1273,7 @@ class AccountWorker {
1184
1273
  { key: 'cmd_cointoss', l: 'flip' }, { key: 'cmd_roulette', l: 'roul' },
1185
1274
  { key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
1186
1275
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1187
- { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1188
- { key: 'cmd_alert', l: 'alert' },
1276
+ { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1189
1277
  ].filter((ci) => Boolean(this.account[ci.key]));
1190
1278
 
1191
1279
  const chName = (this.channel.name || '?').substring(0, 12);
@@ -1208,6 +1296,10 @@ class AccountWorker {
1208
1296
  this.busy = false;
1209
1297
  if (this.tickTimeout) { clearTimeout(this.tickTimeout); this.tickTimeout = null; }
1210
1298
  if (this.configInterval) { clearInterval(this.configInterval); this.configInterval = null; }
1299
+ if (this._alertHandler) {
1300
+ try { this.client.removeListener('messageCreate', this._alertHandler); } catch {}
1301
+ this._alertHandler = null;
1302
+ }
1211
1303
  this.commandQueue = null;
1212
1304
  try { this.client.destroy(); } catch {}
1213
1305
  }
@@ -1229,7 +1321,7 @@ async function start(apiKey, apiUrl) {
1229
1321
 
1230
1322
  console.log(colorBanner());
1231
1323
  console.log(
1232
- ` ${rgb(139, 92, 246)}v4.3${c.reset}` +
1324
+ ` ${rgb(139, 92, 246)}v4.5${c.reset}` +
1233
1325
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1234
1326
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1235
1327
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
@@ -1272,6 +1364,7 @@ async function start(apiKey, apiUrl) {
1272
1364
  console.log('');
1273
1365
  startTime = Date.now();
1274
1366
  dashboardStarted = true;
1367
+ setDashboardActive(true);
1275
1368
  process.stdout.write(c.hide);
1276
1369
 
1277
1370
  setInterval(() => scheduleRender(), 1000);
@@ -1283,6 +1376,7 @@ async function start(apiKey, apiUrl) {
1283
1376
  sigintHandled = true;
1284
1377
  shutdownCalled = true;
1285
1378
  dashboardStarted = false;
1379
+ setDashboardActive(false);
1286
1380
  process.stdout.write(c.show);
1287
1381
 
1288
1382
  // Clear the dashboard area before printing summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"