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.
@@ -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 coins = parseCoins(text);
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(text);
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
- if (followUp) {
81
- logMsg(followUp, `${cmdName}-followup`);
82
- const fText = getFullText(followUp);
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(followUp);
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(followUp, nextBtn); } catch {}
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) { LOG.error(`[${cmdName}] Click error: ${e.message}`); }
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) { LOG.error(`[${cmdName}] Select error: ${e.message}`); }
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
- return { result: text.substring(0, 60) || 'done', coins: 0 };
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
  /**
@@ -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 = text.match(/Net:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([+-]?[\d,]+)/i);
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 = text.match(/Winnings:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([\d,]+)/i);
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 = text.match(/⏣\s*([\d,]+)\s*was placed/i);
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 = text.match(/⏣\s*([\d,]+)/g);
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(/<:Coin:\d+>\s*([\d,]+)/);
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' && !isCV2(msg)) {
210
- return msg.clickButton(id);
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 || msg._cv2) return 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
- // Use listing endpoint — single-message endpoint is bot-only
377
- const msgs = await _httpGet(
378
- `https://discord.com/api/v9/channels/${chId}/messages?limit=5&around=${msg.id}`,
379
- { Authorization: token }
380
- );
381
- const raw = Array.isArray(msgs) ? msgs.find(m => m.id === msg.id) : null;
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.ws?.shards?.first()?.sessionId || 'unknown';
448
+ const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
397
449
  const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
398
- const payload = JSON.stringify({
399
- type: 3, application_id: DANK_MEMER_ID, nonce,
400
- channel_id: chId, guild_id: gId,
401
- message_id: msg.id, message_flags: msg.flags?.bitfield || 0,
402
- session_id: sessionId,
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
- const response = await this.waitForDankMemer(10000);
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
- const text = getFullText(response);
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
- // CV2 format: <:Coin:ID> 3,272,896 and <:Bank:ID> 275,000 / 275,000
841
- const coinMatch = text.match(/<[a:]?:Coin:\d+>\s*([\d,]+)/i);
842
- const bankEmojiMatch = text.match(/<[a:]?:Bank:\d+>\s*([\d,]+)/i);
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
- this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${wallet.toLocaleString()}${c.reset} Bank: ${c.bold}${c.cyan}⏣ ${bank.toLocaleString()}${c.reset} ${c.dim}(${matched || 'none'})${c.reset}`);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.9.3",
3
+ "version": "4.9.4",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"