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.
- package/lib/commands/adventure.js +6 -3
- package/lib/commands/drops.js +116 -0
- package/lib/commands/gamble.js +9 -6
- package/lib/commands/generic.js +6 -3
- package/lib/commands/index.js +2 -0
- package/lib/commands/shop.js +7 -3
- package/lib/commands/stream.js +98 -43
- package/lib/commands/utils.js +8 -1
- package/lib/grinder.js +117 -23
- package/package.json +1 -1
|
@@ -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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 };
|
package/lib/commands/gamble.js
CHANGED
|
@@ -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} →
|
|
74
|
-
if (lower.includes('lost'))
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
95
|
+
async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
|
|
93
96
|
return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
|
|
94
97
|
}
|
|
95
98
|
|
package/lib/commands/generic.js
CHANGED
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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++) {
|
package/lib/commands/index.js
CHANGED
|
@@ -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
|
package/lib/commands/shop.js
CHANGED
|
@@ -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
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
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) {
|
package/lib/commands/stream.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
31
|
-
if (lower.includes('missing items') || lower.includes('need following items') ||
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
await sleep(3000);
|
|
43
|
+
await sleep(2000);
|
|
53
44
|
await channel.send('pls stream');
|
|
54
|
-
response = await waitForDankMemer(
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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]
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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 };
|
package/lib/commands/utils.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 ||
|
|
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 —
|
|
826
|
-
if (resultLower.includes('captcha') || resultLower.includes('verification') ||
|
|
827
|
-
resultLower.includes('
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
955
|
+
// Utility
|
|
918
956
|
{ key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 1 },
|
|
919
|
-
|
|
920
|
-
{ key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority:
|
|
921
|
-
{ key: '
|
|
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: '
|
|
924
|
-
{ key: '
|
|
925
|
-
|
|
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 =
|
|
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', '
|
|
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.
|
|
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' },
|
|
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.
|
|
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
|