dankgrinder 4.6.0 → 4.8.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.
- package/bin/dankgrinder.js +18 -0
- package/lib/commands/adventure.js +11 -46
- package/lib/commands/dig.js +1 -1
- package/lib/commands/gamble.js +49 -34
- package/lib/commands/generic.js +5 -4
- package/lib/commands/hunt.js +1 -1
- package/lib/commands/index.js +2 -2
- package/lib/commands/shop.js +13 -22
- package/lib/grinder.js +107 -55
- package/package.json +2 -2
package/bin/dankgrinder.js
CHANGED
|
@@ -46,6 +46,24 @@ for (let i = 0; i < args.length; i++) {
|
|
|
46
46
|
apiUrl = apiUrl || process.env.DANKGRINDER_URL || DEFAULT_URL;
|
|
47
47
|
if (redisUrl) process.env.REDIS_URL = redisUrl;
|
|
48
48
|
|
|
49
|
+
// Keep process alive on transient discord interaction fetch failures.
|
|
50
|
+
process.on('uncaughtException', (err) => {
|
|
51
|
+
const msg = String(err?.message || err || '');
|
|
52
|
+
const stack = String(err?.stack || '');
|
|
53
|
+
if (msg.toLowerCase().includes('fetch failed') && stack.includes('discord.js-selfbot-v13')) {
|
|
54
|
+
console.error(`\n ${C.red}✗ Discord interaction fetch failed (transient). Continuing...${C.r}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
});
|
|
59
|
+
process.on('unhandledRejection', (reason) => {
|
|
60
|
+
const msg = String(reason?.message || reason || '');
|
|
61
|
+
if (msg.toLowerCase().includes('fetch failed')) {
|
|
62
|
+
console.error(`\n ${C.red}✗ Unhandled fetch failure (network/transient). Continuing...${C.r}\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
49
67
|
if (!apiKey) {
|
|
50
68
|
console.error(`\n ${C.red}✗ Missing API key.${C.r}\n`);
|
|
51
69
|
console.error(` ${C.b}Usage:${C.r} npx dankgrinder --key <YOUR_API_KEY>\n`);
|
|
@@ -34,11 +34,10 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
const {
|
|
37
|
-
LOG, c, sleep, humanDelay, getFullText, parseCoins,
|
|
38
|
-
getAllButtons, getAllSelectMenus, findButton,
|
|
39
|
-
safeClickButton, isHoldTight, logMsg,
|
|
37
|
+
LOG, c, sleep, humanDelay, getFullText, parseCoins,
|
|
38
|
+
getAllButtons, getAllSelectMenus, findButton,
|
|
39
|
+
safeClickButton, isHoldTight, logMsg,
|
|
40
40
|
} = require('./utils');
|
|
41
|
-
const { buyItem } = require('./shop');
|
|
42
41
|
|
|
43
42
|
// ── Adventure type rotation (cycle through all types each run) ────
|
|
44
43
|
let lastAdventureIndex = -1;
|
|
@@ -63,8 +62,7 @@ async function clickAndRefetch(channel, msg, btn) {
|
|
|
63
62
|
LOG.error(`[adventure] Click error: ${e.message}`);
|
|
64
63
|
return null;
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
await sleep(500);
|
|
65
|
+
await sleep(250);
|
|
68
66
|
return await refetchMsg(channel, msg.id);
|
|
69
67
|
}
|
|
70
68
|
|
|
@@ -172,7 +170,7 @@ async function playAdventureRounds(channel, msg) {
|
|
|
172
170
|
const choice = pickSafeChoice(choices);
|
|
173
171
|
if (choice) {
|
|
174
172
|
LOG.info(`[adventure] → Choosing: "${choice.label}"`);
|
|
175
|
-
await sleep(
|
|
173
|
+
await sleep(100);
|
|
176
174
|
const afterChoice = await clickAndRefetch(channel, current, choice);
|
|
177
175
|
if (afterChoice) {
|
|
178
176
|
current = afterChoice;
|
|
@@ -194,7 +192,7 @@ async function playAdventureRounds(channel, msg) {
|
|
|
194
192
|
const nextBtnNow = getNextButton(current);
|
|
195
193
|
if (nextBtnNow && !nextBtnNow.disabled) {
|
|
196
194
|
LOG.debug(`[adventure] Clicking Next arrow...`);
|
|
197
|
-
await sleep(
|
|
195
|
+
await sleep(100);
|
|
198
196
|
const afterNext = await clickAndRefetch(channel, current, nextBtnNow);
|
|
199
197
|
if (afterNext) {
|
|
200
198
|
current = afterNext;
|
|
@@ -206,7 +204,7 @@ async function playAdventureRounds(channel, msg) {
|
|
|
206
204
|
} else if (nextBtnNow && nextBtnNow.disabled) {
|
|
207
205
|
// Next is disabled but no choices found — might be loading
|
|
208
206
|
LOG.debug(`[adventure] Next disabled, no choices — waiting...`);
|
|
209
|
-
await sleep(
|
|
207
|
+
await sleep(300);
|
|
210
208
|
const refreshed = await refetchMsg(channel, current.id);
|
|
211
209
|
if (refreshed) {
|
|
212
210
|
current = refreshed;
|
|
@@ -219,8 +217,7 @@ async function playAdventureRounds(channel, msg) {
|
|
|
219
217
|
// No next button at all
|
|
220
218
|
LOG.debug(`[adventure] No Next button — checking if done`);
|
|
221
219
|
if (isAdventureDone(current)) break;
|
|
222
|
-
|
|
223
|
-
await sleep(1500);
|
|
220
|
+
await sleep(500);
|
|
224
221
|
const refreshed = await refetchMsg(channel, current.id);
|
|
225
222
|
if (refreshed) {
|
|
226
223
|
current = refreshed;
|
|
@@ -314,42 +311,10 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
314
311
|
return { result: 'cooldown', coins: 0, nextCooldownSec: cooldownSec + 3 };
|
|
315
312
|
}
|
|
316
313
|
|
|
317
|
-
// ── 4) If we need a ticket,
|
|
314
|
+
// ── 4) If we need a ticket, skip (too expensive to auto-buy) ──
|
|
318
315
|
if (needsTicket) {
|
|
319
|
-
LOG.warn(`[adventure]
|
|
320
|
-
|
|
321
|
-
// Ticket costs 250,000 coins — check balance first to avoid wasting time in shop
|
|
322
|
-
const TICKET_COST = 250000;
|
|
323
|
-
let currentBalance = 0;
|
|
324
|
-
await channel.send('pls bal');
|
|
325
|
-
const balMsg = await waitForDankMemer(8000);
|
|
326
|
-
if (balMsg) {
|
|
327
|
-
currentBalance = parseBalance(balMsg);
|
|
328
|
-
LOG.info(`[adventure] Balance: ${c.yellow}⏣ ${currentBalance.toLocaleString()}${c.reset} (ticket costs ⏣ ${TICKET_COST.toLocaleString()})`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (currentBalance < TICKET_COST) {
|
|
332
|
-
LOG.warn(`[adventure] Not enough coins for ticket (⏣ ${currentBalance.toLocaleString()} < ⏣ ${TICKET_COST.toLocaleString()}). Grind more first.`);
|
|
333
|
-
return { result: `need ticket (⏣ ${currentBalance.toLocaleString()}/${TICKET_COST.toLocaleString()})`, coins: 0, nextCooldownSec: 120 };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const bought = await buyItem({
|
|
337
|
-
channel, waitForDankMemer, client,
|
|
338
|
-
itemName: 'Adventure Ticket', quantity: 1,
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
if (!bought) {
|
|
342
|
-
LOG.error('[adventure] Could not buy adventure ticket from shop.');
|
|
343
|
-
return { result: 'need ticket (buy failed)', coins: 0, nextCooldownSec: 120 };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
LOG.success('[adventure] Tickets purchased! Re-running adventure...');
|
|
347
|
-
await sleep(1500);
|
|
348
|
-
|
|
349
|
-
await channel.send('pls adventure');
|
|
350
|
-
response = await waitForDankMemer(12000);
|
|
351
|
-
if (!response) return { result: 'no response after ticket buy', coins: 0, nextCooldownSec: null };
|
|
352
|
-
logMsg(response, 'adventure-after-buy');
|
|
316
|
+
LOG.warn(`[adventure] No ticket — skipping (too expensive to auto-buy)`);
|
|
317
|
+
return { result: 'no ticket', coins: 0, nextCooldownSec: 3600, skipReason: 'no_ticket' };
|
|
353
318
|
}
|
|
354
319
|
|
|
355
320
|
// ── Check if we're already mid-adventure (no select menu) ──
|
package/lib/commands/dig.js
CHANGED
package/lib/commands/gamble.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Covers:
|
|
2
|
+
* Gambling command handlers.
|
|
3
|
+
* Covers: cointoss, roulette, slots, snakeeyes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const {
|
|
@@ -8,6 +8,29 @@ const {
|
|
|
8
8
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
9
9
|
} = require('./utils');
|
|
10
10
|
|
|
11
|
+
function parseGambleResult(text, cmdName) {
|
|
12
|
+
const net = parseNetCoins(text);
|
|
13
|
+
const lower = text.toLowerCase();
|
|
14
|
+
|
|
15
|
+
if (net > 0) {
|
|
16
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
|
|
17
|
+
return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
|
|
18
|
+
}
|
|
19
|
+
if (net < 0) {
|
|
20
|
+
LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
|
|
21
|
+
return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
|
|
22
|
+
}
|
|
23
|
+
const coins = parseCoins(text);
|
|
24
|
+
if (coins > 0) {
|
|
25
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
26
|
+
return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
|
|
27
|
+
}
|
|
28
|
+
if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
|
|
29
|
+
if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
|
|
30
|
+
|
|
31
|
+
return { result: `${cmdName} done`, coins: 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
|
|
12
35
|
LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
|
|
13
36
|
|
|
@@ -26,16 +49,29 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
|
|
|
26
49
|
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
27
50
|
}
|
|
28
51
|
|
|
52
|
+
// Check for min bet error
|
|
53
|
+
const initText = getFullText(response);
|
|
54
|
+
const initLower = initText.toLowerCase();
|
|
55
|
+
if (initLower.includes("can't bet less than") || initLower.includes('cannot bet less than') || initLower.includes('minimum bet')) {
|
|
56
|
+
const betMatch = initText.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || initText.match(/(\d[\d,]+)/);
|
|
57
|
+
if (betMatch) {
|
|
58
|
+
const minBet = parseInt(betMatch[1].replace(/,/g, ''));
|
|
59
|
+
if (minBet > 0) return { result: `min bet ${minBet}`, coins: 0, newMinBet: minBet };
|
|
60
|
+
}
|
|
61
|
+
return { result: 'min bet error', coins: 0 };
|
|
62
|
+
}
|
|
63
|
+
|
|
29
64
|
logMsg(response, cmdName);
|
|
30
65
|
const text = getFullText(response);
|
|
31
66
|
|
|
32
|
-
//
|
|
67
|
+
// For cointoss/gambles with buttons: click a random non-disabled button
|
|
33
68
|
const buttons = getAllButtons(response);
|
|
34
69
|
if (buttons.length > 0) {
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
70
|
+
const clickable = buttons.filter(b => !b.disabled);
|
|
71
|
+
if (clickable.length > 0) {
|
|
72
|
+
const btn = clickable[Math.floor(Math.random() * clickable.length)];
|
|
73
|
+
LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
|
|
74
|
+
await humanDelay(50, 200);
|
|
39
75
|
try {
|
|
40
76
|
const followUp = await safeClickButton(response, btn);
|
|
41
77
|
if (followUp) {
|
|
@@ -50,32 +86,11 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
|
|
|
50
86
|
return parseGambleResult(text, cmdName);
|
|
51
87
|
}
|
|
52
88
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
|
|
59
|
-
return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
|
|
60
|
-
}
|
|
61
|
-
if (net < 0) {
|
|
62
|
-
LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
|
|
63
|
-
return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
|
|
64
|
-
}
|
|
65
|
-
// Fallback to parseCoins
|
|
66
|
-
const coins = parseCoins(text);
|
|
67
|
-
if (coins > 0) {
|
|
68
|
-
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
69
|
-
return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
|
|
70
|
-
}
|
|
71
|
-
if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
|
|
72
|
-
if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
|
|
73
|
-
|
|
74
|
-
return { result: `${cmdName} done`, coins: 0 };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function runCoinflip({ channel, waitForDankMemer, betAmount = 5000 }) {
|
|
78
|
-
return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
|
|
89
|
+
/**
|
|
90
|
+
* Cointoss: send "pls cointoss <bet>", then click Heads or Tails randomly.
|
|
91
|
+
*/
|
|
92
|
+
async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
|
|
93
|
+
return runGamble({ channel, waitForDankMemer, cmdName: 'cointoss', cmdString: `pls cointoss ${betAmount}` });
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
async function runRoulette({ channel, waitForDankMemer, betAmount = 5000 }) {
|
|
@@ -90,4 +105,4 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
|
|
|
90
105
|
return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
module.exports = { runGamble,
|
|
108
|
+
module.exports = { runGamble, runCointoss, runRoulette, runSlots, runSnakeeyes };
|
package/lib/commands/generic.js
CHANGED
|
@@ -45,7 +45,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
45
45
|
const missing = needsItem(text);
|
|
46
46
|
if (missing) {
|
|
47
47
|
LOG.warn(`[${cmdName}] Missing ${c.bold}${missing}${c.reset} — auto-buying...`);
|
|
48
|
-
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity:
|
|
48
|
+
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 1 });
|
|
49
49
|
if (bought) {
|
|
50
50
|
LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
|
|
51
51
|
await sleep(3000);
|
|
@@ -65,11 +65,12 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
65
65
|
return { result: `need ${missing} (buy failed)`, coins: 0 };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// Handle buttons if present
|
|
68
|
+
// Handle buttons if present — pick a random non-disabled button
|
|
69
69
|
const buttons = getAllButtons(response);
|
|
70
70
|
if (buttons.length > 0) {
|
|
71
|
-
const
|
|
72
|
-
|
|
71
|
+
const clickable = buttons.filter(b => !b.disabled);
|
|
72
|
+
const btn = clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
|
|
73
|
+
if (btn) {
|
|
73
74
|
LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
|
|
74
75
|
await humanDelay();
|
|
75
76
|
try {
|
package/lib/commands/hunt.js
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const { runScratch } = require('./scratch');
|
|
|
16
16
|
const { runBlackjack } = require('./blackjack');
|
|
17
17
|
const { runTrivia, triviaDB } = require('./trivia');
|
|
18
18
|
const { runWorkShift } = require('./work');
|
|
19
|
-
const {
|
|
19
|
+
const { runCointoss, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
|
|
20
20
|
const { runDeposit } = require('./deposit');
|
|
21
21
|
const { runGeneric, runAlert } = require('./generic');
|
|
22
22
|
const { runStream } = require('./stream');
|
|
@@ -39,7 +39,7 @@ module.exports = {
|
|
|
39
39
|
runBlackjack,
|
|
40
40
|
runTrivia,
|
|
41
41
|
runWorkShift,
|
|
42
|
-
|
|
42
|
+
runCointoss,
|
|
43
43
|
runRoulette,
|
|
44
44
|
runSlots,
|
|
45
45
|
runSnakeeyes,
|
package/lib/commands/shop.js
CHANGED
|
@@ -32,8 +32,12 @@ const ITEM_COSTS = {
|
|
|
32
32
|
* @returns {Promise<boolean>} true if purchase succeeded
|
|
33
33
|
*/
|
|
34
34
|
async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, client }) {
|
|
35
|
-
const MAX_RETRIES =
|
|
36
|
-
const
|
|
35
|
+
const MAX_RETRIES = 1;
|
|
36
|
+
const searchNames = [
|
|
37
|
+
itemName.toLowerCase(),
|
|
38
|
+
itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', ''),
|
|
39
|
+
itemName.toLowerCase().split(' ').pop(),
|
|
40
|
+
];
|
|
37
41
|
|
|
38
42
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
39
43
|
LOG.buy(`Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset} (attempt ${attempt}/${MAX_RETRIES})`);
|
|
@@ -120,30 +124,17 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
120
124
|
logMsg(response, 'shop-after-nav');
|
|
121
125
|
|
|
122
126
|
// Step 3: Find the Buy button for our item
|
|
123
|
-
let buyBtn = getAllButtons(response).find(b =>
|
|
127
|
+
let buyBtn = getAllButtons(response).find(b => {
|
|
128
|
+
if (!b.label) return false;
|
|
129
|
+
const label = b.label.toLowerCase();
|
|
130
|
+
return searchNames.some(s => label.includes(s) || s.includes(label));
|
|
131
|
+
});
|
|
124
132
|
|
|
125
133
|
if (!buyBtn) {
|
|
126
|
-
LOG.warn(`No Buy button found for "${itemName}"
|
|
127
|
-
// Log all available buttons for debugging
|
|
134
|
+
LOG.warn(`No Buy button found for "${itemName}"`);
|
|
128
135
|
const allBtns = getAllButtons(response);
|
|
129
136
|
if (allBtns.length > 0) {
|
|
130
|
-
LOG.debug(`Available
|
|
131
|
-
}
|
|
132
|
-
// Maybe we need to scroll/paginate?
|
|
133
|
-
const nextBtn = findButton(response, 'next') || findButton(response, '▶') || findButton(response, '→');
|
|
134
|
-
if (nextBtn && !nextBtn.disabled) {
|
|
135
|
-
LOG.buy('Clicking next page to find item...');
|
|
136
|
-
try {
|
|
137
|
-
const nextPage = await safeClickButton(response, nextBtn);
|
|
138
|
-
if (nextPage) {
|
|
139
|
-
response = nextPage;
|
|
140
|
-
logMsg(response, 'shop-page2');
|
|
141
|
-
// Try finding button again
|
|
142
|
-
buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
|
|
143
|
-
}
|
|
144
|
-
} catch (e) {
|
|
145
|
-
LOG.error(`Page nav failed: ${e.message}`);
|
|
146
|
-
}
|
|
137
|
+
LOG.debug(`Available: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
|
|
147
138
|
}
|
|
148
139
|
|
|
149
140
|
if (!buyBtn) {
|
package/lib/grinder.js
CHANGED
|
@@ -270,15 +270,32 @@ function log(type, msg, label) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
async function fetchConfig() {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
273
|
+
async function fetchConfig(retries = 3, delayMs = 1500) {
|
|
274
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
275
|
+
try {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const t = setTimeout(() => controller.abort(), 10000);
|
|
278
|
+
const res = await fetch(`${API_URL}/api/grinder/config`, {
|
|
279
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
280
|
+
signal: controller.signal,
|
|
281
|
+
});
|
|
282
|
+
clearTimeout(t);
|
|
283
|
+
const data = await res.json();
|
|
284
|
+
if (data.error) {
|
|
285
|
+
log('error', `Config fetch failed: ${data.error}`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return data;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (attempt < retries) {
|
|
291
|
+
log('warn', `API fetch failed (attempt ${attempt}/${retries}) — retrying...`);
|
|
292
|
+
await new Promise((r) => setTimeout(r, delayMs * attempt));
|
|
293
|
+
} else {
|
|
294
|
+
log('error', `Cannot reach API: ${err.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
282
299
|
}
|
|
283
300
|
|
|
284
301
|
async function sendLog(accountName, command, response, status) {
|
|
@@ -509,6 +526,7 @@ class AccountWorker {
|
|
|
509
526
|
this.globalCooldownUntil = 0;
|
|
510
527
|
this.commandQueue = null;
|
|
511
528
|
this.lastHealthCheck = Date.now();
|
|
529
|
+
this.doneToday = new Map(); // in-memory dedup: cmd → expiry timestamp
|
|
512
530
|
}
|
|
513
531
|
|
|
514
532
|
get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
|
|
@@ -616,8 +634,8 @@ class AccountWorker {
|
|
|
616
634
|
return null;
|
|
617
635
|
}
|
|
618
636
|
|
|
619
|
-
async buyItem(itemName, quantity =
|
|
620
|
-
const MAX_RETRIES =
|
|
637
|
+
async buyItem(itemName, quantity = 1) {
|
|
638
|
+
const MAX_RETRIES = 1;
|
|
621
639
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
622
640
|
this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
|
|
623
641
|
if (this.account.use_slash) {
|
|
@@ -671,12 +689,18 @@ class AccountWorker {
|
|
|
671
689
|
}
|
|
672
690
|
await humanDelay(1000, 2000);
|
|
673
691
|
|
|
674
|
-
// Find Buy button
|
|
692
|
+
// Find Buy button — match by full name or partial name
|
|
675
693
|
let buyBtn = null;
|
|
676
|
-
const
|
|
694
|
+
const searchNames = [
|
|
695
|
+
itemName.toLowerCase(),
|
|
696
|
+
itemName.toLowerCase().replace('hunting ', '').replace('fishing ', ''),
|
|
697
|
+
itemName.toLowerCase().split(' ')[0],
|
|
698
|
+
];
|
|
677
699
|
for (const row of response.components || []) {
|
|
678
700
|
for (const comp of row.components || []) {
|
|
679
|
-
if (comp.type
|
|
701
|
+
if (comp.type !== 2 || !comp.label) continue;
|
|
702
|
+
const label = comp.label.toLowerCase();
|
|
703
|
+
if (searchNames.some(s => label.includes(s) || s.includes(label))) {
|
|
680
704
|
buyBtn = comp; break;
|
|
681
705
|
}
|
|
682
706
|
}
|
|
@@ -788,7 +812,7 @@ class AccountWorker {
|
|
|
788
812
|
case 'dep max': cmdString = `${prefix} dep max`; break;
|
|
789
813
|
case 'with max': cmdString = `${prefix} with max`; break;
|
|
790
814
|
case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
|
|
791
|
-
case '
|
|
815
|
+
case 'cointoss': cmdString = `${prefix} cointoss ${betAmount}`; break;
|
|
792
816
|
case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
|
|
793
817
|
case 'slots': cmdString = `${prefix} slots ${betAmount}`; break;
|
|
794
818
|
case 'snakeeyes': cmdString = `${prefix} snakeeyes ${betAmount}`; break;
|
|
@@ -826,7 +850,7 @@ class AccountWorker {
|
|
|
826
850
|
case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
|
|
827
851
|
case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
|
|
828
852
|
case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
|
|
829
|
-
case '
|
|
853
|
+
case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
|
|
830
854
|
case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
|
|
831
855
|
case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
|
|
832
856
|
case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
|
|
@@ -878,18 +902,24 @@ class AccountWorker {
|
|
|
878
902
|
return;
|
|
879
903
|
}
|
|
880
904
|
|
|
881
|
-
// Min bet detection —
|
|
882
|
-
if (resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
|
|
883
|
-
const betMatch = result.match(
|
|
905
|
+
// Min bet detection — "You can't bet less than 10,000" or "cannot bet less than ⏣ 5,000"
|
|
906
|
+
if (resultLower.includes("can't bet less than") || resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
|
|
907
|
+
const betMatch = result.match(/less than\s*\*?\*?\s*[⏣o]?\s*([\d,]+)/i) || result.match(/(\d[\d,]+)/);
|
|
884
908
|
if (betMatch) {
|
|
885
909
|
const minBet = parseInt(betMatch[1].replace(/,/g, ''));
|
|
886
910
|
if (minBet > 0) {
|
|
887
911
|
this.account.bet_amount = minBet;
|
|
888
|
-
this.log('info', `${cmdName} min bet
|
|
912
|
+
this.log('info', `${cmdName} min bet raised → ⏣ ${minBet.toLocaleString()}`);
|
|
889
913
|
}
|
|
890
914
|
}
|
|
891
915
|
return;
|
|
892
916
|
}
|
|
917
|
+
// Also handle newMinBet from gamble handler
|
|
918
|
+
if (cmdResult.newMinBet && cmdResult.newMinBet > (this.account.bet_amount || 0)) {
|
|
919
|
+
this.account.bet_amount = cmdResult.newMinBet;
|
|
920
|
+
this.log('info', `${cmdName} min bet raised → ⏣ ${cmdResult.newMinBet.toLocaleString()}`);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
893
923
|
|
|
894
924
|
// Premium-only command detection — disable for 24h
|
|
895
925
|
if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
|
|
@@ -899,41 +929,50 @@ class AccountWorker {
|
|
|
899
929
|
return;
|
|
900
930
|
}
|
|
901
931
|
|
|
902
|
-
// Already claimed today (daily/weekly) — set long cooldown + mark
|
|
932
|
+
// Already claimed today (daily/weekly) — set long cooldown + mark done
|
|
903
933
|
if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
|
|
904
934
|
this.log('info', `${cmdName} already claimed — waiting`);
|
|
905
935
|
const timeMatch = result.match(/<t:(\d+):R>/);
|
|
936
|
+
let waitSec;
|
|
906
937
|
if (timeMatch) {
|
|
907
938
|
const nextAvail = parseInt(timeMatch[1]) * 1000;
|
|
908
|
-
|
|
909
|
-
await this.setCooldown(cmdName, waitSec);
|
|
910
|
-
if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
|
|
939
|
+
waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
|
|
911
940
|
} else {
|
|
912
|
-
|
|
913
|
-
await this.setCooldown(cmdName, fallbackSec);
|
|
914
|
-
if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', fallbackSec); } catch {}
|
|
941
|
+
waitSec = cmdName === 'daily' ? 86400 : 604800;
|
|
915
942
|
}
|
|
943
|
+
await this.setCooldown(cmdName, waitSec);
|
|
944
|
+
this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
|
|
945
|
+
if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
|
|
916
946
|
return;
|
|
917
947
|
}
|
|
918
948
|
|
|
919
|
-
//
|
|
949
|
+
// Skip reasons: level too low, no ticket, etc.
|
|
920
950
|
if (cmdResult.skipReason === 'level') {
|
|
921
951
|
this.log('warn', `${cmdName} level too low — retry in 24h`);
|
|
922
952
|
await this.setCooldown(cmdName, 86400);
|
|
923
953
|
return;
|
|
924
954
|
}
|
|
955
|
+
if (cmdResult.skipReason === 'no_ticket') {
|
|
956
|
+
this.log('warn', `${cmdName} no ticket — retry in 1h`);
|
|
957
|
+
await this.setCooldown(cmdName, 3600);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
925
960
|
|
|
926
961
|
const earned = Math.max(0, cmdResult.coins || 0);
|
|
927
962
|
const spent = Math.max(0, cmdResult.lost || 0);
|
|
928
963
|
if (earned > 0) this.stats.coins += earned;
|
|
929
964
|
if (cmdResult.nextCooldownSec) await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
|
|
930
965
|
|
|
931
|
-
// Mark daily/drops as done
|
|
932
|
-
if (
|
|
933
|
-
|
|
966
|
+
// Mark daily/drops as done so we don't re-run this session
|
|
967
|
+
if (cmdName === 'daily' && earned > 0) {
|
|
968
|
+
const expiry = Date.now() + 86400 * 1000;
|
|
969
|
+
this.doneToday.set('daily', expiry);
|
|
970
|
+
if (redis) try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
|
|
934
971
|
}
|
|
935
|
-
if (
|
|
936
|
-
|
|
972
|
+
if (cmdName === 'drops') {
|
|
973
|
+
const expiry = Date.now() + 86400 * 1000;
|
|
974
|
+
this.doneToday.set('drops', expiry);
|
|
975
|
+
if (redis) try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
|
|
937
976
|
}
|
|
938
977
|
|
|
939
978
|
if (cmdResult.holdTightReason) {
|
|
@@ -991,7 +1030,7 @@ class AccountWorker {
|
|
|
991
1030
|
{ key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
|
|
992
1031
|
// Gambling (fast cycle)
|
|
993
1032
|
{ key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
|
|
994
|
-
{ key: 'cmd_cointoss', cmd: '
|
|
1033
|
+
{ key: 'cmd_cointoss', cmd: 'cointoss', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
|
|
995
1034
|
{ key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
|
|
996
1035
|
{ key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
|
|
997
1036
|
{ key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
|
|
@@ -1147,17 +1186,28 @@ class AccountWorker {
|
|
|
1147
1186
|
return;
|
|
1148
1187
|
}
|
|
1149
1188
|
|
|
1150
|
-
// Skip daily/drops if already done today (Redis
|
|
1151
|
-
if (
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1189
|
+
// Skip daily/drops if already done today (in-memory + Redis)
|
|
1190
|
+
if (item.cmd === 'daily' || item.cmd === 'drops') {
|
|
1191
|
+
const memExpiry = this.doneToday.get(item.cmd);
|
|
1192
|
+
if (memExpiry && Date.now() < memExpiry) {
|
|
1193
|
+
item.nextRunAt = memExpiry;
|
|
1194
|
+
if (this.commandQueue) this.commandQueue.push(item);
|
|
1195
|
+
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (redis) {
|
|
1199
|
+
try {
|
|
1200
|
+
const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
|
|
1201
|
+
if (done) {
|
|
1202
|
+
const expiry = now + 86400 * 1000;
|
|
1203
|
+
this.doneToday.set(item.cmd, expiry);
|
|
1204
|
+
item.nextRunAt = expiry;
|
|
1205
|
+
if (this.commandQueue) this.commandQueue.push(item);
|
|
1206
|
+
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
} catch {}
|
|
1210
|
+
}
|
|
1161
1211
|
}
|
|
1162
1212
|
|
|
1163
1213
|
this.busy = true;
|
|
@@ -1167,10 +1217,11 @@ class AccountWorker {
|
|
|
1167
1217
|
|
|
1168
1218
|
await this.setCooldown(item.cmd, totalWait);
|
|
1169
1219
|
|
|
1220
|
+
// Inter-command delay: 1-3s random (human-like spacing)
|
|
1170
1221
|
const timeSinceLastCmd = now - (this.lastCommandRun || 0);
|
|
1171
|
-
const
|
|
1172
|
-
if (timeSinceLastCmd <
|
|
1173
|
-
await new Promise(r => setTimeout(r,
|
|
1222
|
+
const minGap = 1000 + Math.random() * 2000; // 1-3s
|
|
1223
|
+
if (timeSinceLastCmd < minGap) {
|
|
1224
|
+
await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
|
|
1174
1225
|
}
|
|
1175
1226
|
|
|
1176
1227
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
@@ -1326,7 +1377,7 @@ class AccountWorker {
|
|
|
1326
1377
|
{ key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
|
|
1327
1378
|
{ key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
|
|
1328
1379
|
{ key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
|
|
1329
|
-
{ key: 'cmd_cointoss', l: '
|
|
1380
|
+
{ key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
|
|
1330
1381
|
{ key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
|
|
1331
1382
|
{ key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
|
|
1332
1383
|
{ key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
|
|
@@ -1377,7 +1428,7 @@ async function start(apiKey, apiUrl) {
|
|
|
1377
1428
|
|
|
1378
1429
|
console.log(colorBanner());
|
|
1379
1430
|
console.log(
|
|
1380
|
-
` ${rgb(139, 92, 246)}v4.
|
|
1431
|
+
` ${rgb(139, 92, 246)}v4.8.1${c.reset}` +
|
|
1381
1432
|
` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
|
|
1382
1433
|
` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
|
|
1383
1434
|
` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
|
|
@@ -1392,17 +1443,18 @@ async function start(apiKey, apiUrl) {
|
|
|
1392
1443
|
|
|
1393
1444
|
log('info', `${c.dim}Fetching accounts...${c.reset}`);
|
|
1394
1445
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1446
|
+
let data = await fetchConfig(4, 2000);
|
|
1447
|
+
while (!data) {
|
|
1397
1448
|
log('error', `Cannot connect to API`);
|
|
1398
|
-
log('
|
|
1399
|
-
|
|
1449
|
+
log('warn', `Will retry in 10s (check internet/API URL if this repeats).`);
|
|
1450
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
1451
|
+
data = await fetchConfig(4, 2000);
|
|
1400
1452
|
}
|
|
1401
1453
|
|
|
1402
1454
|
const { accounts } = data;
|
|
1403
1455
|
if (!accounts || accounts.length === 0) {
|
|
1404
1456
|
log('error', 'No active accounts. Add them in the dashboard.');
|
|
1405
|
-
|
|
1457
|
+
return;
|
|
1406
1458
|
}
|
|
1407
1459
|
|
|
1408
1460
|
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dankgrinder",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.1",
|
|
4
4
|
"description": "Dank Memer automation engine — grind coins while you sleep",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dankgrinder": "bin/dankgrinder.js"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"debug": "^4.4.0",
|
|
24
|
-
"discord.js-selfbot-v13": "
|
|
24
|
+
"discord.js-selfbot-v13": "3.5.0",
|
|
25
25
|
"ioredis": "^5.10.1",
|
|
26
26
|
"sharp": "^0.34.5"
|
|
27
27
|
},
|