dankgrinder 5.14.0 → 5.16.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.
@@ -15,7 +15,7 @@
15
15
 
16
16
  const {
17
17
  LOG, c, sleep, humanDelay, getFullText,
18
- getAllButtons, findSelectMenuOption,
18
+ getAllButtons, findSelectMenuOption, getAllSelectMenus, safeClickButton,
19
19
  logMsg, isHoldTight, isCV2, ensureCV2, stripAnsi,
20
20
  } = require('./utils');
21
21
  const { LRUCache, Trie } = require('../structures');
@@ -80,6 +80,28 @@ function isBuyFailText(text) {
80
80
  || lower.includes('missing items');
81
81
  }
82
82
 
83
+ function findBuyButtonInMessage(msg, searchTerm, key) {
84
+ const allBtns = getAllButtons(msg);
85
+ const normalizedSearch = String(searchTerm || '').toLowerCase().replace(/\s+/g, '');
86
+ const normalizedKey = String(key || '').toLowerCase().replace(/\s+/g, '');
87
+ return allBtns.find(b => {
88
+ const label = String(b.label || '').toLowerCase();
89
+ const id = String(b.customId || b.custom_id || '').toLowerCase().replace(/\s+/g, '');
90
+ return label.includes(searchTerm)
91
+ || (normalizedSearch && id.includes(normalizedSearch))
92
+ || (normalizedKey && id.includes(normalizedKey));
93
+ }) || null;
94
+ }
95
+
96
+ function findNextShopPageButton(msg) {
97
+ const btns = getAllButtons(msg).filter(b => !b.disabled);
98
+ return btns.find(b => {
99
+ const id = String(b.customId || b.custom_id || '').toLowerCase();
100
+ if (!id.includes('shop-view')) return false;
101
+ return id.endsWith(':1') || id.includes(':1:');
102
+ }) || null;
103
+ }
104
+
83
105
  /**
84
106
  * Buy an item from the Dank Memer shop.
85
107
  *
@@ -137,7 +159,7 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
137
159
  const row = response.components[i];
138
160
  for (const comp of row?.components || []) {
139
161
  if ((comp.type === 'STRING_SELECT' || comp.type === 3) &&
140
- comp.customId === selectInfo.menuCustomId) {
162
+ (comp.customId || comp.custom_id) === selectInfo.menuCustomId) {
141
163
  menuRowIdx = i;
142
164
  break;
143
165
  }
@@ -148,34 +170,57 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
148
170
  menuRowIdx >= 0 ? menuRowIdx : selectInfo.menuCustomId,
149
171
  [selectInfo.option.value]
150
172
  );
151
- if (result) {
152
- response = result;
153
- if (isCV2(response)) await ensureCV2(response);
154
- LOG.success(`Switched to ${targetTab}`);
155
- }
173
+ response = result || (await waitForDankMemer(6000)) || response;
174
+ if (isCV2(response)) await ensureCV2(response);
175
+ LOG.success(`Switched to ${targetTab}`);
156
176
  await sleep(300);
157
177
  } catch (e) {
158
178
  LOG.error(`[shop] Tab switch failed: ${e.message}`);
159
179
  return false;
160
180
  }
161
181
  }
182
+ } else {
183
+ // If select menu exists but option matching failed due label variations,
184
+ // try selecting by value from any menu option containing coin/fishing/etc.
185
+ const menus = getAllSelectMenus(response);
186
+ const menu = menus.find(m => (m.options || []).length > 0);
187
+ if (menu) {
188
+ const desired = (menu.options || []).find(o => (o.label || '').toLowerCase().includes(targetTab.toLowerCase()));
189
+ if (desired) {
190
+ try {
191
+ await response.selectMenu(menu.customId || menu.custom_id, [desired.value]);
192
+ const maybeUpdated = await waitForDankMemer(6000);
193
+ if (maybeUpdated) {
194
+ response = maybeUpdated;
195
+ if (isCV2(response)) await ensureCV2(response);
196
+ LOG.success(`Switched to ${targetTab}`);
197
+ }
198
+ } catch {}
199
+ }
200
+ }
162
201
  }
163
202
 
164
- // Step 3: Find the Buy button for our item
165
- const allBtns = getAllButtons(response);
166
- const normalizedSearch = String(searchTerm || '').toLowerCase().replace(/\s+/g, '');
167
- const normalizedKey = String(key || '').toLowerCase().replace(/\s+/g, '');
168
- const buyBtn = allBtns.find(b => {
169
- const label = String(b.label || '').toLowerCase();
170
- const id = String(b.customId || b.custom_id || '').toLowerCase().replace(/\s+/g, '');
171
- return label.includes(searchTerm)
172
- || (normalizedSearch && id.includes(normalizedSearch))
173
- || (normalizedKey && id.includes(normalizedKey));
174
- });
203
+ // Step 3: Find the Buy button for our item (scan pages in current tab)
204
+ let buyBtn = null;
205
+ for (let page = 0; page < 10; page++) {
206
+ buyBtn = findBuyButtonInMessage(response, searchTerm, key);
207
+ if (buyBtn) break;
208
+
209
+ const nextPageBtn = findNextShopPageButton(response);
210
+ if (!nextPageBtn) break;
211
+ try {
212
+ const moved = await safeClickButton(response, nextPageBtn);
213
+ response = moved || (await waitForDankMemer(5000)) || response;
214
+ if (isCV2(response)) await ensureCV2(response);
215
+ await sleep(200);
216
+ } catch {
217
+ break;
218
+ }
219
+ }
175
220
 
176
221
  if (!buyBtn) {
177
222
  LOG.warn(`[shop] No button for "${itemName}" (search: "${searchTerm}")`);
178
- const named = allBtns.filter(b => b.label);
223
+ const named = getAllButtons(response).filter(b => b.label);
179
224
  if (named.length > 0) {
180
225
  LOG.debug(`[shop] Available: ${named.map(b => `"${b.label}"(${b.disabled ? 'DIS' : 'EN'})`).join(', ')}`);
181
226
  }
@@ -1,12 +1,13 @@
1
1
  const {
2
2
  LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
3
3
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
4
- isCV2, ensureCV2, stripAnsi,
4
+ isCV2, ensureCV2, stripAnsi, clickCV2Button,
5
5
  } = require('./utils');
6
6
  const { buyItem } = require('./shop');
7
7
 
8
8
  const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
9
9
  const STREAM_ACTION_LABELS = Object.freeze(['run ad', 'read chat', 'collect donations']);
10
+ const RE_STREAM_INTERACT_MIN = /interact\s+with\s+your\s+stream\s+every\s+`?(\d+)`?\s*minutes?/i;
10
11
 
11
12
  function normalizeLower(text) {
12
13
  return String(text || '')
@@ -30,6 +31,27 @@ async function refetchMsg(channel, msgId) {
30
31
  try { return await channel.messages.fetch(msgId); } catch { return null; }
31
32
  }
32
33
 
34
+ async function waitForMessageEditById(client, msgId, channelId, timeoutMs = 6000) {
35
+ if (!client?.on) return null;
36
+ return new Promise((resolve) => {
37
+ const timer = setTimeout(() => {
38
+ try { client.removeListener('messageUpdate', onUpdate); } catch {}
39
+ resolve(null);
40
+ }, timeoutMs);
41
+
42
+ function onUpdate(_oldMsg, newMsg) {
43
+ if (!newMsg) return;
44
+ if (newMsg.id !== msgId) return;
45
+ if (channelId && newMsg.channel?.id !== channelId && newMsg.channelId !== channelId) return;
46
+ clearTimeout(timer);
47
+ try { client.removeListener('messageUpdate', onUpdate); } catch {}
48
+ resolve(newMsg);
49
+ }
50
+
51
+ client.on('messageUpdate', onUpdate);
52
+ });
53
+ }
54
+
33
55
  function messageStateSignature(msg) {
34
56
  const text = normalizeLower(stripAnsi(getFullText(msg))).slice(0, 220);
35
57
  const btnSig = getAllButtons(msg)
@@ -40,10 +62,16 @@ function messageStateSignature(msg) {
40
62
  return `${text}||${btnSig}`;
41
63
  }
42
64
 
43
- async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, timeoutMs = 12000 }) {
65
+ async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, client, timeoutMs = 12000 }) {
44
66
  const start = Date.now();
45
67
  const prevSig = messageStateSignature(prevMsg);
46
68
  while (Date.now() - start < timeoutMs) {
69
+ const byIdUpdate = await waitForMessageEditById(client, prevMsg.id, prevMsg.channel?.id || prevMsg.channelId, 1800);
70
+ if (byIdUpdate) {
71
+ await hydrate(byIdUpdate);
72
+ if (messageStateSignature(byIdUpdate) !== prevSig) return byIdUpdate;
73
+ }
74
+
47
75
  const evt = await waitForDankMemer(1500);
48
76
  if (evt) {
49
77
  await hydrate(evt);
@@ -75,11 +103,14 @@ function isLiveDashboard(msg, lowerText) {
75
103
  return labels.some(l => l.includes('end stream')) && labels.some(l => STREAM_ACTION_LABELS.some(a => l.includes(a)));
76
104
  }
77
105
 
78
- function isStreamManagerScreen(lowerText) {
106
+ function isStreamManagerScreen(msg, lowerText) {
107
+ const labels = getAllButtons(msg).map(b => (b.label || '').toLowerCase());
108
+ const hasManagerButtons = labels.some(l => l.includes('go live')) || labels.some(l => l.includes('view setup'));
79
109
  return lowerText.includes('stream manager')
80
110
  || lowerText.includes('what game do you want to stream')
81
111
  || lowerText.includes('view setup')
82
- || lowerText.includes('go live');
112
+ || lowerText.includes('go live')
113
+ || hasManagerButtons;
83
114
  }
84
115
 
85
116
  function isActionResultText(lowerText) {
@@ -90,6 +121,16 @@ function isActionResultText(lowerText) {
90
121
  || lowerText.includes('received');
91
122
  }
92
123
 
124
+ function parseStreamInteractCooldownSec(text) {
125
+ const clean = String(stripAnsi(text || '')).replace(/\s+/g, ' ').trim();
126
+ const mm = clean.match(RE_STREAM_INTERACT_MIN);
127
+ if (mm) {
128
+ const mins = parseInt(mm[1], 10);
129
+ if (Number.isFinite(mins) && mins > 0) return mins * 60;
130
+ }
131
+ return 600;
132
+ }
133
+
93
134
  async function selectRandomStreamOption(msg) {
94
135
  const menus = getAllSelectMenus(msg);
95
136
  if (menus.length === 0) return false;
@@ -142,6 +183,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
142
183
  logMsg(response, 'stream');
143
184
  let text = getFullText(response);
144
185
  let lower = normalizeLower(stripAnsi(text));
186
+ let nextCooldownSec = parseStreamInteractCooldownSec(text);
145
187
 
146
188
  // Missing items — buy keyboard + mouse
147
189
  const missing = needsItem(lower);
@@ -169,15 +211,18 @@ async function runStream({ channel, waitForDankMemer, client }) {
169
211
  logMsg(response, 'stream-retry');
170
212
  text = getFullText(response);
171
213
  lower = normalizeLower(stripAnsi(text));
214
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
172
215
  }
173
216
 
174
217
  // Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
218
+ let lastGoLiveCustomId = null;
175
219
  for (let step = 0; step < 6; step++) {
176
220
  text = getFullText(response);
177
221
  lower = normalizeLower(stripAnsi(text));
222
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
178
223
 
179
- if (isLiveDashboard(response, lower)) break;
180
- if (!isStreamManagerScreen(lower)) break;
224
+ if (isLiveDashboard(response, lower)) break;
225
+ if (!isStreamManagerScreen(response, lower)) break;
181
226
 
182
227
  const selected = await selectRandomStreamOption(response);
183
228
  if (selected) {
@@ -191,7 +236,34 @@ async function runStream({ channel, waitForDankMemer, client }) {
191
236
  }
192
237
 
193
238
  const goLiveBtn = getGoLiveButton(response);
239
+ if (goLiveBtn?.customId || goLiveBtn?.custom_id) {
240
+ lastGoLiveCustomId = goLiveBtn.customId || goLiveBtn.custom_id;
241
+ }
194
242
  if (!goLiveBtn) {
243
+ // Parser fallback: game prompt text detected but buttons/selects may be hidden by library.
244
+ // Use previously known stream-game custom_id directly against current message.
245
+ if (lower.includes('what game do you want to stream') && lastGoLiveCustomId) {
246
+ LOG.info('[stream] Go Live button not visible; trying raw stream-game fallback');
247
+ try {
248
+ await clickCV2Button(response, lastGoLiveCustomId);
249
+ const transitioned = await waitForStreamTransition({
250
+ channel,
251
+ waitForDankMemer,
252
+ client,
253
+ prevMsg: response,
254
+ timeoutMs: 12000,
255
+ });
256
+ if (transitioned) {
257
+ response = transitioned;
258
+ await hydrate(response);
259
+ logMsg(response, `stream-go-live-raw-${step}`);
260
+ continue;
261
+ }
262
+ } catch (e) {
263
+ LOG.error(`[stream] Raw Go Live fallback failed: ${e.message}`);
264
+ }
265
+ }
266
+
195
267
  await sleep(300);
196
268
  const fresh = await refetchMsg(channel, response.id);
197
269
  if (fresh) {
@@ -207,12 +279,22 @@ async function runStream({ channel, waitForDankMemer, client }) {
207
279
  try {
208
280
  const before = response;
209
281
  const liveResult = await safeClickButton(response, goLiveBtn);
210
- const next = liveResult || (await waitForStreamTransition({
211
- channel,
212
- waitForDankMemer,
213
- prevMsg: before,
214
- timeoutMs: 12000,
215
- }));
282
+ const liveLooksLikeMessage = Boolean(liveResult?.id && (liveResult?.channel?.id || liveResult?.channelId));
283
+ let next = liveLooksLikeMessage ? liveResult : null;
284
+ if (!next) {
285
+ next = await waitForStreamTransition({
286
+ channel,
287
+ waitForDankMemer,
288
+ client,
289
+ prevMsg: before,
290
+ timeoutMs: 12000,
291
+ });
292
+ }
293
+ if (!next) {
294
+ // Last fallback: explicit refetch of the original message after click.
295
+ const fetched = await refetchMsg(channel, before.id);
296
+ if (fetched) next = fetched;
297
+ }
216
298
  if (next) {
217
299
  response = next;
218
300
  await hydrate(response);
@@ -234,6 +316,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
234
316
  const afterFallback = fallbackRes || (await waitForStreamTransition({
235
317
  channel,
236
318
  waitForDankMemer,
319
+ client,
237
320
  prevMsg: before,
238
321
  timeoutMs: 8000,
239
322
  }));
@@ -255,11 +338,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
255
338
  // Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
256
339
  text = getFullText(response);
257
340
  lower = normalizeLower(stripAnsi(text));
341
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
258
342
 
259
343
  if (isLiveDashboard(response, lower)) {
260
344
  const actions = getLiveActionButtons(response);
261
345
  if (actions.length > 0) {
262
346
  const action = pickRandom(actions);
347
+ const actionAt = new Date();
263
348
  LOG.info(`[stream] Live action: "${action.label}"`);
264
349
  await humanDelay(120, 320);
265
350
  try {
@@ -292,10 +377,12 @@ async function runStream({ channel, waitForDankMemer, client }) {
292
377
  }
293
378
  text = bestText;
294
379
  lower = normalizeLower(stripAnsi(text));
380
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
295
381
  }
296
382
  } catch (e) {
297
383
  LOG.error(`[stream] Action click failed: ${e.message}`);
298
384
  }
385
+ LOG.info(`[stream] Action timestamp: ${actionAt.toISOString()} | next interaction in ${Math.ceil(nextCooldownSec / 60)}m`);
299
386
  } else {
300
387
  LOG.info('[stream] Live dashboard found but no action buttons available yet');
301
388
  }
@@ -305,13 +392,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
305
392
  const coins = parseCoins(text);
306
393
  if (coins > 0) {
307
394
  LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
308
- return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
395
+ return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec };
309
396
  }
310
397
 
311
- if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
398
+ if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec };
312
399
 
313
400
  const short = normalizeLower(stripAnsi(text)).substring(0, 60);
314
- return { result: short || 'streamed', coins: 0, nextCooldownSec: 600 };
401
+ return { result: short || 'streamed', coins: 0, nextCooldownSec };
315
402
  }
316
403
 
317
404
  module.exports = { runStream };
@@ -249,6 +249,17 @@ function flattenComponents(components) {
249
249
  _accessoryOf: item,
250
250
  });
251
251
  }
252
+ if (acc.type === 3 || acc.type === 'SELECT_MENU' || acc.type === 'STRING_SELECT') {
253
+ flat.push({
254
+ type: 'STRING_SELECT',
255
+ customId: acc.custom_id || acc.customId,
256
+ options: acc.options || [],
257
+ disabled: acc.disabled || false,
258
+ placeholder: acc.placeholder,
259
+ data: acc,
260
+ _accessoryOf: item,
261
+ });
262
+ }
252
263
  }
253
264
  }
254
265
  return flat;
@@ -273,7 +284,7 @@ function findSelectMenuOption(msg, label) {
273
284
  const menus = getAllSelectMenus(msg);
274
285
  for (const comp of menus) {
275
286
  const opt = (comp.options || []).find(o => o.label?.toLowerCase().includes(label.toLowerCase()));
276
- if (opt) return { menuCustomId: comp.customId, option: opt, component: comp };
287
+ if (opt) return { menuCustomId: comp.customId || comp.custom_id, option: opt, component: comp };
277
288
  }
278
289
  return null;
279
290
  }
@@ -429,13 +440,15 @@ function isCV2(msg) {
429
440
 
430
441
  async function ensureCV2(msg, force = false) {
431
442
  if (!msg) return msg;
432
- if (!force && msg._cv2) return msg;
443
+ const msgEditedTs = msg.editedTimestamp || msg.editedAt?.getTime?.() || null;
444
+ if (!force && msg._cv2 && msg._cv2EditedTs === msgEditedTs) return msg;
433
445
  if (!isCV2(msg)) return msg;
434
446
  try {
435
447
  if (force) {
436
448
  delete msg._cv2;
437
449
  delete msg._cv2text;
438
450
  delete msg._cv2buttons;
451
+ delete msg._cv2EditedTs;
439
452
  cv2Cache.delete(msg.id);
440
453
  }
441
454
  const token = msg.client?.token;
@@ -445,10 +458,17 @@ async function ensureCV2(msg, force = false) {
445
458
  // LRU cache hit — O(1) lookup avoids redundant HTTP fetches
446
459
  const cached = cv2Cache.get(msg.id);
447
460
  if (cached && !force) {
448
- msg._cv2 = cached;
449
- msg._cv2text = _extractCV2Text(cached).trim();
450
- msg._cv2buttons = _extractCV2Buttons(cached);
451
- return msg;
461
+ const cachedComponents = Array.isArray(cached) ? cached : cached.components;
462
+ const cachedEditedTs = Array.isArray(cached) ? null : (cached.editedTimestamp || null);
463
+
464
+ // If message has been edited since cache snapshot, ignore stale cache.
465
+ if (!msgEditedTs || cachedEditedTs === msgEditedTs) {
466
+ msg._cv2 = cachedComponents;
467
+ msg._cv2text = _extractCV2Text(cachedComponents).trim();
468
+ msg._cv2buttons = _extractCV2Buttons(cachedComponents);
469
+ msg._cv2EditedTs = cachedEditedTs;
470
+ return msg;
471
+ }
452
472
  }
453
473
 
454
474
  let raw = null;
@@ -470,10 +490,11 @@ async function ensureCV2(msg, force = false) {
470
490
  }
471
491
 
472
492
  if (raw?.components) {
473
- cv2Cache.set(msg.id, raw.components);
493
+ cv2Cache.set(msg.id, { components: raw.components, editedTimestamp: msgEditedTs });
474
494
  msg._cv2 = raw.components;
475
495
  msg._cv2text = _extractCV2Text(raw.components).trim();
476
496
  msg._cv2buttons = _extractCV2Buttons(raw.components);
497
+ msg._cv2EditedTs = msgEditedTs;
477
498
  }
478
499
  } catch (e) { LOG.debug(`[cv2] fetch error: ${e.message}`); }
479
500
  return msg;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.14.0",
3
+ "version": "5.16.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"