dankgrinder 4.9.3 → 4.9.4
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/generic.js +56 -13
- package/lib/commands/utils.js +81 -23
- package/lib/grinder.js +33 -6
- package/package.json +1 -1
package/lib/commands/generic.js
CHANGED
|
@@ -7,10 +7,26 @@
|
|
|
7
7
|
const {
|
|
8
8
|
LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
|
|
9
9
|
safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
|
|
10
|
-
isCV2, ensureCV2,
|
|
10
|
+
isCV2, ensureCV2, stripAnsi,
|
|
11
11
|
} = require('./utils');
|
|
12
12
|
const { buyItem } = require('./shop');
|
|
13
13
|
|
|
14
|
+
async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs = 12000) {
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
while (Date.now() - start < timeoutMs) {
|
|
17
|
+
await sleep(700);
|
|
18
|
+
try {
|
|
19
|
+
if (!channel?.messages?.fetch) continue;
|
|
20
|
+
const fresh = await channel.messages.fetch(messageId);
|
|
21
|
+
if (!fresh) continue;
|
|
22
|
+
if (isCV2(fresh)) await ensureCV2(fresh, true);
|
|
23
|
+
const next = stripAnsi(getFullText(fresh)).replace(/\s+/g, ' ').trim();
|
|
24
|
+
if (next && next !== baselineText) return fresh;
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
/**
|
|
15
31
|
* @param {object} opts
|
|
16
32
|
* @param {object} opts.channel
|
|
@@ -41,10 +57,11 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
41
57
|
if (isCV2(response)) await ensureCV2(response);
|
|
42
58
|
logMsg(response, cmdName);
|
|
43
59
|
const text = getFullText(response);
|
|
44
|
-
const
|
|
60
|
+
const cleanText = stripAnsi(text).replace(/\s+/g, ' ').trim();
|
|
61
|
+
const coins = parseCoins(cleanText);
|
|
45
62
|
|
|
46
63
|
// Check if we need an item
|
|
47
|
-
const missing = needsItem(
|
|
64
|
+
const missing = needsItem(cleanText);
|
|
48
65
|
if (missing) {
|
|
49
66
|
LOG.warn(`[${cmdName}] Missing ${c.bold}${missing}${c.reset} — auto-buying...`);
|
|
50
67
|
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 1 });
|
|
@@ -55,7 +72,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
55
72
|
const r2 = await waitForDankMemer(10000);
|
|
56
73
|
if (r2) {
|
|
57
74
|
logMsg(r2, `${cmdName}-retry`);
|
|
58
|
-
const t2 = getFullText(r2);
|
|
75
|
+
const t2 = stripAnsi(getFullText(r2)).replace(/\s+/g, ' ').trim();
|
|
59
76
|
const c2 = parseCoins(t2);
|
|
60
77
|
if (c2 > 0) {
|
|
61
78
|
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${c2.toLocaleString()}${c.reset}`);
|
|
@@ -77,25 +94,43 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
77
94
|
await humanDelay();
|
|
78
95
|
try {
|
|
79
96
|
const followUp = await safeClickButton(response, btn);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
|
|
98
|
+
let postClickMsg = followUp || null;
|
|
99
|
+
if (!postClickMsg && response.id) {
|
|
100
|
+
postClickMsg = await waitForEditedMessage(channel, response.id, cleanText, 12000);
|
|
101
|
+
}
|
|
102
|
+
if (!postClickMsg) {
|
|
103
|
+
postClickMsg = await waitForDankMemer(9000);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (postClickMsg) {
|
|
107
|
+
if (isCV2(postClickMsg)) await ensureCV2(postClickMsg);
|
|
108
|
+
logMsg(postClickMsg, `${cmdName}-followup`);
|
|
109
|
+
const fText = stripAnsi(getFullText(postClickMsg)).replace(/\s+/g, ' ').trim();
|
|
83
110
|
const fCoins = parseCoins(fText);
|
|
111
|
+
|
|
84
112
|
if (fCoins > 0) {
|
|
85
113
|
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
|
|
86
114
|
return { result: `+⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
|
|
87
115
|
}
|
|
116
|
+
|
|
88
117
|
// Multi-step: click next button too
|
|
89
|
-
const nextButtons = getAllButtons(
|
|
118
|
+
const nextButtons = getAllButtons(postClickMsg);
|
|
90
119
|
if (nextButtons.length > 0) {
|
|
91
120
|
const nextBtn = nextButtons.find(b => !b.disabled);
|
|
92
121
|
if (nextBtn) {
|
|
93
122
|
await humanDelay();
|
|
94
|
-
try { await safeClickButton(
|
|
123
|
+
try { await safeClickButton(postClickMsg, nextBtn); } catch {}
|
|
95
124
|
}
|
|
96
125
|
}
|
|
126
|
+
|
|
127
|
+
// For XP-only outputs (like tidy) return concise title text.
|
|
128
|
+
const firstLine = fText.split(/\s*-#\s*/)[0].trim() || fText;
|
|
129
|
+
return { result: firstLine.substring(0, 90), coins: 0 };
|
|
97
130
|
}
|
|
98
|
-
} catch (e) {
|
|
131
|
+
} catch (e) {
|
|
132
|
+
LOG.error(`[${cmdName}] Click error: ${e.message}`);
|
|
133
|
+
}
|
|
99
134
|
}
|
|
100
135
|
}
|
|
101
136
|
|
|
@@ -108,6 +143,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
108
143
|
if (freshMsg) response = freshMsg;
|
|
109
144
|
}
|
|
110
145
|
} catch { /* proceed with original */ }
|
|
146
|
+
|
|
111
147
|
// Find row index of first select menu
|
|
112
148
|
let menuRowIdx = -1;
|
|
113
149
|
for (let i = 0; i < (response.components || []).length; i++) {
|
|
@@ -118,6 +154,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
118
154
|
}
|
|
119
155
|
if (menuRowIdx >= 0) break;
|
|
120
156
|
}
|
|
157
|
+
|
|
121
158
|
const menu = menuRowIdx >= 0 ? response.components?.[menuRowIdx]?.components?.[0] : null;
|
|
122
159
|
const options = menu?.options || [];
|
|
123
160
|
if (options.length > 0 && menuRowIdx >= 0) {
|
|
@@ -127,14 +164,16 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
127
164
|
await response.selectMenu(menuRowIdx, [opt.value]);
|
|
128
165
|
const followUp = await waitForDankMemer(8000);
|
|
129
166
|
if (followUp) {
|
|
130
|
-
const fText = getFullText(followUp);
|
|
167
|
+
const fText = stripAnsi(getFullText(followUp)).replace(/\s+/g, ' ').trim();
|
|
131
168
|
const fCoins = parseCoins(fText);
|
|
132
169
|
if (fCoins > 0) {
|
|
133
170
|
LOG.coin(`[${cmdName}] ${opt.label} → ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
|
|
134
171
|
return { result: `${opt.label} → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
|
|
135
172
|
}
|
|
136
173
|
}
|
|
137
|
-
} catch (e) {
|
|
174
|
+
} catch (e) {
|
|
175
|
+
LOG.error(`[${cmdName}] Select error: ${e.message}`);
|
|
176
|
+
}
|
|
138
177
|
}
|
|
139
178
|
}
|
|
140
179
|
|
|
@@ -143,7 +182,11 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
|
|
|
143
182
|
return { result: `+⏣ ${coins.toLocaleString()}`, coins };
|
|
144
183
|
}
|
|
145
184
|
|
|
146
|
-
|
|
185
|
+
if (cmdName === 'tidy') {
|
|
186
|
+
return { result: 'tidy → no coins (xp only)', coins: 0 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { result: cleanText.substring(0, 90) || 'done', coins: 0 };
|
|
147
190
|
}
|
|
148
191
|
|
|
149
192
|
/**
|
package/lib/commands/utils.js
CHANGED
|
@@ -68,24 +68,49 @@ function getFullText(msg) {
|
|
|
68
68
|
return text;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function stripAnsi(text) {
|
|
72
|
+
if (!text) return '';
|
|
73
|
+
return String(text).replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
function parseCoins(text) {
|
|
72
77
|
if (!text) return 0;
|
|
78
|
+
const cleanText = stripAnsi(text);
|
|
73
79
|
// Prefer "Net:" if present (accurate earned/lost from Dank Memer)
|
|
74
|
-
const netMatch =
|
|
80
|
+
const netMatch = cleanText.match(/Net:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([+-]?[\d,]+)/i);
|
|
75
81
|
if (netMatch) {
|
|
76
82
|
const net = parseInt(netMatch[1].replace(/,/g, ''));
|
|
77
83
|
return net > 0 ? net : 0;
|
|
78
84
|
}
|
|
79
|
-
const winMatch =
|
|
85
|
+
const winMatch = cleanText.match(/Winnings:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([\d,]+)/i);
|
|
80
86
|
if (winMatch) {
|
|
81
87
|
const w = parseInt(winMatch[1].replace(/,/g, ''));
|
|
82
88
|
if (w > 0) return w;
|
|
83
89
|
}
|
|
90
|
+
|
|
91
|
+
// Tidy/CV2 reward blocks often use "You received:" with non-⏣ symbols.
|
|
92
|
+
const receivedMatch = cleanText.match(/you\s+received\s*:\s*[\s\S]{0,80}?([\d,]+)/i);
|
|
93
|
+
if (receivedMatch) {
|
|
94
|
+
const r = parseInt(receivedMatch[1].replace(/,/g, ''), 10);
|
|
95
|
+
if (r > 0) return r;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// CV2 pattern: <:Coin:ID> NUMBER
|
|
99
|
+
const coinEmojiMatches = [...cleanText.matchAll(/<a?:Coin:\d+>\s*([\d,]+)/gi)];
|
|
100
|
+
if (coinEmojiMatches.length > 0) {
|
|
101
|
+
let best = 0;
|
|
102
|
+
for (const m of coinEmojiMatches) {
|
|
103
|
+
const v = parseInt((m[1] || '0').replace(/,/g, ''), 10) || 0;
|
|
104
|
+
if (v > best) best = v;
|
|
105
|
+
}
|
|
106
|
+
if (best > 0) return best;
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
// Prefer "placed in your wallet" pattern (daily, beg, etc.)
|
|
85
|
-
const walletMatch =
|
|
110
|
+
const walletMatch = cleanText.match(/⏣\s*([\d,]+)\s*was placed/i);
|
|
86
111
|
if (walletMatch) return parseInt(walletMatch[1].replace(/,/g, ''));
|
|
87
112
|
// Fallback: max ⏣ number
|
|
88
|
-
const matches =
|
|
113
|
+
const matches = cleanText.match(/⏣\s*([\d,]+)/g);
|
|
89
114
|
if (!matches) return 0;
|
|
90
115
|
let best = 0;
|
|
91
116
|
for (const m of matches) {
|
|
@@ -119,16 +144,19 @@ function parseNetCoins(text) {
|
|
|
119
144
|
*/
|
|
120
145
|
function parseBalance(msg) {
|
|
121
146
|
if (!msg) return 0;
|
|
122
|
-
const text = getFullText(msg);
|
|
147
|
+
const text = stripAnsi(getFullText(msg));
|
|
123
148
|
|
|
124
149
|
// Try standard embed wallet pattern
|
|
125
150
|
const walletMatch = text.match(/wallet[:\s]*[⏣💰]?\s*([\d,]+)/i);
|
|
126
151
|
if (walletMatch) return parseInt(walletMatch[1].replace(/,/g, ''));
|
|
127
152
|
|
|
128
153
|
// CV2 pattern: <:Coin:ID> NUMBER (first number after Coin emoji = wallet)
|
|
129
|
-
const coinEmojiMatch = text.match(
|
|
154
|
+
const coinEmojiMatch = text.match(/<a?:Coin:\d+>\s*([\d,]+)/i);
|
|
130
155
|
if (coinEmojiMatch) return parseInt(coinEmojiMatch[1].replace(/,/g, ''));
|
|
131
156
|
|
|
157
|
+
const bankEmojiMatch = text.match(/<a?:Bank:\d+>\s*([\d,]+)/i);
|
|
158
|
+
if (bankEmojiMatch) return parseInt(bankEmojiMatch[1].replace(/,/g, ''));
|
|
159
|
+
|
|
132
160
|
// Fallback: ⏣ prefixed
|
|
133
161
|
const coins = parseCoins(text);
|
|
134
162
|
if (coins > 0) return coins;
|
|
@@ -206,8 +234,12 @@ async function safeClickButton(msg, button) {
|
|
|
206
234
|
return button.click();
|
|
207
235
|
}
|
|
208
236
|
const id = button.customId || button.custom_id;
|
|
209
|
-
if (id && typeof msg.clickButton === 'function'
|
|
210
|
-
|
|
237
|
+
if (id && typeof msg.clickButton === 'function') {
|
|
238
|
+
try {
|
|
239
|
+
return await msg.clickButton(id);
|
|
240
|
+
} catch {
|
|
241
|
+
// Fall through to CV2 raw interaction fallback.
|
|
242
|
+
}
|
|
211
243
|
}
|
|
212
244
|
// CV2 fallback: send interaction via raw HTTP
|
|
213
245
|
if (id) {
|
|
@@ -366,19 +398,39 @@ function isCV2(msg) {
|
|
|
366
398
|
return msg.components?.length > 0 && msg.components.every(c => !c);
|
|
367
399
|
}
|
|
368
400
|
|
|
369
|
-
async function ensureCV2(msg) {
|
|
370
|
-
if (!msg
|
|
401
|
+
async function ensureCV2(msg, force = false) {
|
|
402
|
+
if (!msg) return msg;
|
|
403
|
+
if (!force && msg._cv2) return msg;
|
|
371
404
|
if (!isCV2(msg)) return msg;
|
|
372
405
|
try {
|
|
406
|
+
if (force) {
|
|
407
|
+
delete msg._cv2;
|
|
408
|
+
delete msg._cv2text;
|
|
409
|
+
delete msg._cv2buttons;
|
|
410
|
+
}
|
|
373
411
|
const token = msg.client?.token;
|
|
374
412
|
const chId = msg.channelId || msg.channel?.id;
|
|
375
413
|
if (!token || !chId) return msg;
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
414
|
+
// Try single-message endpoint first (usually freshest for edited CV2 messages).
|
|
415
|
+
let raw = null;
|
|
416
|
+
try {
|
|
417
|
+
raw = await _httpGet(
|
|
418
|
+
`https://discord.com/api/v9/channels/${chId}/messages/${msg.id}`,
|
|
419
|
+
{ Authorization: token }
|
|
420
|
+
);
|
|
421
|
+
} catch {
|
|
422
|
+
raw = null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Fallback to listing endpoint when single fetch is unavailable.
|
|
426
|
+
if (!raw || !raw.components) {
|
|
427
|
+
const msgs = await _httpGet(
|
|
428
|
+
`https://discord.com/api/v9/channels/${chId}/messages?limit=5&around=${msg.id}`,
|
|
429
|
+
{ Authorization: token }
|
|
430
|
+
);
|
|
431
|
+
raw = Array.isArray(msgs) ? msgs.find(m => m.id === msg.id) : null;
|
|
432
|
+
}
|
|
433
|
+
|
|
382
434
|
if (raw?.components) {
|
|
383
435
|
msg._cv2 = raw.components;
|
|
384
436
|
msg._cv2text = _extractCV2Text(raw.components).trim();
|
|
@@ -393,15 +445,20 @@ async function clickCV2Button(msg, customId) {
|
|
|
393
445
|
const chId = msg.channelId || msg.channel?.id;
|
|
394
446
|
const gId = msg.guildId || msg.guild?.id;
|
|
395
447
|
if (!token) throw new Error('No token for CV2 click');
|
|
396
|
-
const sessionId = msg.client
|
|
448
|
+
const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
|
|
397
449
|
const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
|
|
398
|
-
const
|
|
399
|
-
type: 3,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
450
|
+
const payloadObj = {
|
|
451
|
+
type: 3,
|
|
452
|
+
application_id: String(msg.applicationId || DANK_MEMER_ID),
|
|
453
|
+
nonce,
|
|
454
|
+
channel_id: String(chId),
|
|
455
|
+
message_id: String(msg.id),
|
|
403
456
|
data: { component_type: 2, custom_id: customId },
|
|
404
|
-
}
|
|
457
|
+
};
|
|
458
|
+
if (gId) payloadObj.guild_id = String(gId);
|
|
459
|
+
if (sessionId) payloadObj.session_id = sessionId;
|
|
460
|
+
|
|
461
|
+
const payload = JSON.stringify(payloadObj);
|
|
405
462
|
const resp = await _httpPost('https://discord.com/api/v9/interactions', {
|
|
406
463
|
Authorization: token, 'Content-Type': 'application/json',
|
|
407
464
|
}, payload);
|
|
@@ -441,6 +498,7 @@ module.exports = {
|
|
|
441
498
|
sleep,
|
|
442
499
|
humanDelay,
|
|
443
500
|
getFullText,
|
|
501
|
+
stripAnsi,
|
|
444
502
|
parseCoins,
|
|
445
503
|
parseNetCoins,
|
|
446
504
|
parseBalance,
|
package/lib/grinder.js
CHANGED
|
@@ -828,18 +828,40 @@ class AccountWorker {
|
|
|
828
828
|
async checkBalance() {
|
|
829
829
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
830
830
|
await this.channel.send(`${prefix} bal`);
|
|
831
|
-
|
|
831
|
+
let response = await this.waitForDankMemer(10000);
|
|
832
|
+
|
|
833
|
+
// Fallback for slash setup: sometimes "/ bal" misses and only "/balance" works.
|
|
834
|
+
if (!response && this.account.use_slash) {
|
|
835
|
+
await this.channel.send('/balance');
|
|
836
|
+
response = await this.waitForDankMemer(10000);
|
|
837
|
+
}
|
|
838
|
+
|
|
832
839
|
if (response) {
|
|
833
840
|
if (isCV2(response)) await ensureCV2(response);
|
|
834
|
-
|
|
841
|
+
let text = stripAnsi(getFullText(response)).replace(/\s+/g, ' ').trim();
|
|
842
|
+
|
|
843
|
+
// Dank Memer sometimes sends empty first payload then edits in the full card.
|
|
844
|
+
if (!text && response.id) {
|
|
845
|
+
const edited = await this.waitForMessageUpdate(response.id, 5000);
|
|
846
|
+
if (edited) {
|
|
847
|
+
if (isCV2(edited)) await ensureCV2(edited);
|
|
848
|
+
text = stripAnsi(getFullText(edited)).replace(/\s+/g, ' ').trim();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (!text) {
|
|
853
|
+
this.log('warn', 'Balance response was empty after waiting for update');
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
835
856
|
|
|
836
857
|
let wallet = 0;
|
|
837
858
|
let bank = 0;
|
|
838
859
|
let matched = '';
|
|
839
860
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
861
|
+
// CV2 format: <:Coin:ID> 3,272,896 and <:Bank:ID> 275,000 / 275,000
|
|
862
|
+
const coinMatch = text.match(/<a?:Coin:\d+>\s*([\d,]+)/i);
|
|
863
|
+
const bankEmojiMatch = text.match(/<a?:Bank:\d+>\s*([\d,]+)/i);
|
|
864
|
+
const bankSlashMatch = text.match(/(?:<a?:Bank:\d+>\s*)?([\d,]+)\s*\/\s*[\d,]+/i);
|
|
843
865
|
|
|
844
866
|
// Legacy embed format: Wallet: ⏣ 1,234,567
|
|
845
867
|
const walletMatch = text.match(/wallet[:\s]*\*{0,2}\s*[⏣💰]?\s*\*{0,2}\s*([\d,]+)/i);
|
|
@@ -861,8 +883,13 @@ class AccountWorker {
|
|
|
861
883
|
|
|
862
884
|
if (bankEmojiMatch) {
|
|
863
885
|
bank = parseInt(bankEmojiMatch[1].replace(/,/g, ''), 10);
|
|
886
|
+
matched += matched ? '+bank-emoji' : 'bank-emoji';
|
|
864
887
|
} else if (bankTextMatch) {
|
|
865
888
|
bank = parseInt(bankTextMatch[1].replace(/,/g, ''), 10);
|
|
889
|
+
matched += matched ? '+bank-text' : 'bank-text';
|
|
890
|
+
} else if (bankSlashMatch) {
|
|
891
|
+
bank = parseInt(bankSlashMatch[1].replace(/,/g, ''), 10);
|
|
892
|
+
matched += matched ? '+bank-slash' : 'bank-slash';
|
|
866
893
|
}
|
|
867
894
|
|
|
868
895
|
if (wallet === 0 && bank === 0) {
|
|
@@ -873,7 +900,7 @@ class AccountWorker {
|
|
|
873
900
|
|
|
874
901
|
this.stats.balance = wallet;
|
|
875
902
|
this.stats.bankBalance = bank;
|
|
876
|
-
|
|
903
|
+
this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} Total: ${c.bold}⏣ ${(wallet + bank).toLocaleString()}${c.reset} ${c.dim}(${matched || 'none'})${c.reset}`);
|
|
877
904
|
|
|
878
905
|
// Store in Redis for persistence
|
|
879
906
|
if (redis) {
|