dankgrinder 4.3.1 โ†’ 4.5.1

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.
@@ -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
 
@@ -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
@@ -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 };
@@ -38,7 +38,7 @@ const LOG = {
38
38
  // โ”€โ”€ Sleep โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
39
39
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
40
40
 
41
- function humanDelay(min = 100, max = 350) {
41
+ function humanDelay(min = 50, max = 200) {
42
42
  return new Promise(r => setTimeout(r, min + Math.random() * (max - min)));
43
43
  }
44
44
 
package/lib/grinder.js CHANGED
@@ -284,7 +284,7 @@ async function reportEarnings(accountId, accountName, earned, spent, command) {
284
284
  function randomDelay(min, max) {
285
285
  return new Promise((r) => setTimeout(r, (Math.random() * (max - min) + min) * 1000));
286
286
  }
287
- function humanDelay(min = 300, max = 800) {
287
+ function humanDelay(min = 80, max = 250) {
288
288
  return new Promise((r) => setTimeout(r, min + Math.random() * (max - min)));
289
289
  }
290
290
  function safeParseJSON(str, fallback = []) {
@@ -759,10 +759,11 @@ class AccountWorker {
759
759
  // handles Hold Tight / cooldowns / item-buying internally.
760
760
  async runCommand(cmdName, prefix) {
761
761
  let cmdString;
762
- const betAmount = this.account.bet_amount || 1000;
762
+ const betAmount = Math.max(5000, this.account.bet_amount || 5000);
763
763
 
764
764
  switch (cmdName) {
765
765
  case 'dep max': cmdString = `${prefix} dep max`; break;
766
+ case 'with max': cmdString = `${prefix} with max`; break;
766
767
  case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
767
768
  case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
768
769
  case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
@@ -809,6 +810,7 @@ class AccountWorker {
809
810
  case 'dep max': cmdResult = await commands.runDeposit(cmdOpts); break;
810
811
  case 'alert': cmdResult = await commands.runAlert(cmdOpts); break;
811
812
  case 'stream': cmdResult = await commands.runStream(cmdOpts); break;
813
+ case 'drops': cmdResult = await commands.runDrops(cmdOpts); break;
812
814
  default: cmdResult = await commands.runGeneric({ ...cmdOpts, cmdString, cmdName }); break;
813
815
  }
814
816
 
@@ -823,12 +825,37 @@ class AccountWorker {
823
825
  return;
824
826
  }
825
827
 
826
- // Captcha/verification detection โ€” pause immediately
827
- if (resultLower.includes('captcha') || resultLower.includes('verification') ||
828
- resultLower.includes('are you human') || resultLower.includes("prove you're not a bot")) {
829
- 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.`);
830
835
  this.paused = true;
831
- 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
+ }
832
859
  return;
833
860
  }
834
861
 
@@ -854,6 +881,13 @@ class AccountWorker {
854
881
  return;
855
882
  }
856
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
+
857
891
  const earned = Math.max(0, cmdResult.coins || 0);
858
892
  const spent = Math.max(0, cmdResult.lost || 0);
859
893
  if (earned > 0) this.stats.coins += earned;
@@ -900,6 +934,7 @@ class AccountWorker {
900
934
 
901
935
  // โ”€โ”€ Command Map (shared across ticks, used to build the heap) โ”€โ”€
902
936
  static COMMAND_MAP = [
937
+ // Core grinding commands (short cooldowns, run frequently)
903
938
  { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
904
939
  { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 4 },
905
940
  { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 3 },
@@ -910,23 +945,25 @@ class AccountWorker {
910
945
  { key: 'cmd_fish', cmd: 'fish', cdKey: 'cd_fish', defaultCd: 20, priority: 1 },
911
946
  { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 2 },
912
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)
913
950
  { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
914
951
  { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
915
952
  { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
916
953
  { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
917
954
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
918
- { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
955
+ // Utility
919
956
  { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 1 },
920
- { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 60, priority: 2 },
921
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 8 },
922
- { 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
923
963
  { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
924
- { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
925
- { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
926
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 3600, priority: 8 },
927
- { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 3600, priority: 6 },
928
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 6 },
929
- { 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)
930
967
  ];
931
968
 
932
969
  buildCommandQueue() {
@@ -1075,7 +1112,7 @@ class AccountWorker {
1075
1112
  await this.setCooldown(item.cmd, totalWait);
1076
1113
 
1077
1114
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1078
- const globalJitter = 1500 + Math.random() * 1500;
1115
+ const globalJitter = 500 + Math.random() * 1000;
1079
1116
  if (timeSinceLastCmd < globalJitter) {
1080
1117
  await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1081
1118
  }
@@ -1131,14 +1168,33 @@ class AccountWorker {
1131
1168
  this.commandQueue = this.buildCommandQueue();
1132
1169
  this.lastHealthCheck = Date.now();
1133
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
+
1134
1189
  this.configInterval = setInterval(async () => {
1135
1190
  if (!this.running) return;
1136
1191
  await this.refreshConfig();
1137
1192
 
1138
1193
  if (this.account.active && this.paused) {
1139
- this.log('success', 'Captcha cleared! Resuming...');
1194
+ this.log('success', 'Account re-enabled! Resuming...');
1140
1195
  this.paused = false;
1141
1196
  this.dashboardPaused = false;
1197
+ if (this.commandQueue) this.mergeCommandQueue();
1142
1198
  }
1143
1199
 
1144
1200
  if (!this.account.active && !this.dashboardPaused) {
@@ -1217,8 +1273,7 @@ class AccountWorker {
1217
1273
  { key: 'cmd_cointoss', l: 'flip' }, { key: 'cmd_roulette', l: 'roul' },
1218
1274
  { key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
1219
1275
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1220
- { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1221
- { key: 'cmd_alert', l: 'alert' },
1276
+ { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1222
1277
  ].filter((ci) => Boolean(this.account[ci.key]));
1223
1278
 
1224
1279
  const chName = (this.channel.name || '?').substring(0, 12);
@@ -1241,6 +1296,10 @@ class AccountWorker {
1241
1296
  this.busy = false;
1242
1297
  if (this.tickTimeout) { clearTimeout(this.tickTimeout); this.tickTimeout = null; }
1243
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
+ }
1244
1303
  this.commandQueue = null;
1245
1304
  try { this.client.destroy(); } catch {}
1246
1305
  }
@@ -1262,7 +1321,7 @@ async function start(apiKey, apiUrl) {
1262
1321
 
1263
1322
  console.log(colorBanner());
1264
1323
  console.log(
1265
- ` ${rgb(139, 92, 246)}v4.3${c.reset}` +
1324
+ ` ${rgb(139, 92, 246)}v4.5${c.reset}` +
1266
1325
  ` ${c.dim}ยท${c.reset} ${c.white}30 Commands${c.reset}` +
1267
1326
  ` ${c.dim}ยท${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1268
1327
  ` ${c.dim}ยท${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.3.1",
3
+ "version": "4.5.1",
4
4
  "description": "Dank Memer automation engine โ€” grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"