dankgrinder 5.13.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,62 @@ 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
+
55
+ function messageStateSignature(msg) {
56
+ const text = normalizeLower(stripAnsi(getFullText(msg))).slice(0, 220);
57
+ const btnSig = getAllButtons(msg)
58
+ .map(b => `${(b.label || '').toLowerCase()}|${(b.customId || b.custom_id || '').toLowerCase()}|${b.disabled ? '1' : '0'}`)
59
+ .sort()
60
+ .join(';')
61
+ .slice(0, 300);
62
+ return `${text}||${btnSig}`;
63
+ }
64
+
65
+ async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, client, timeoutMs = 12000 }) {
66
+ const start = Date.now();
67
+ const prevSig = messageStateSignature(prevMsg);
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
+
75
+ const evt = await waitForDankMemer(1500);
76
+ if (evt) {
77
+ await hydrate(evt);
78
+ if (messageStateSignature(evt) !== prevSig) return evt;
79
+ }
80
+
81
+ const fresh = await refetchMsg(channel, prevMsg.id);
82
+ if (fresh) {
83
+ await hydrate(fresh);
84
+ if (messageStateSignature(fresh) !== prevSig) return fresh;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
33
90
  function getGoLiveButton(msg) {
34
91
  const buttons = getAllButtons(msg);
35
92
  return buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('go live')) || null;
@@ -46,11 +103,14 @@ function isLiveDashboard(msg, lowerText) {
46
103
  return labels.some(l => l.includes('end stream')) && labels.some(l => STREAM_ACTION_LABELS.some(a => l.includes(a)));
47
104
  }
48
105
 
49
- 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'));
50
109
  return lowerText.includes('stream manager')
51
110
  || lowerText.includes('what game do you want to stream')
52
111
  || lowerText.includes('view setup')
53
- || lowerText.includes('go live');
112
+ || lowerText.includes('go live')
113
+ || hasManagerButtons;
54
114
  }
55
115
 
56
116
  function isActionResultText(lowerText) {
@@ -61,6 +121,16 @@ function isActionResultText(lowerText) {
61
121
  || lowerText.includes('received');
62
122
  }
63
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
+
64
134
  async function selectRandomStreamOption(msg) {
65
135
  const menus = getAllSelectMenus(msg);
66
136
  if (menus.length === 0) return false;
@@ -113,6 +183,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
113
183
  logMsg(response, 'stream');
114
184
  let text = getFullText(response);
115
185
  let lower = normalizeLower(stripAnsi(text));
186
+ let nextCooldownSec = parseStreamInteractCooldownSec(text);
116
187
 
117
188
  // Missing items — buy keyboard + mouse
118
189
  const missing = needsItem(lower);
@@ -140,15 +211,18 @@ async function runStream({ channel, waitForDankMemer, client }) {
140
211
  logMsg(response, 'stream-retry');
141
212
  text = getFullText(response);
142
213
  lower = normalizeLower(stripAnsi(text));
214
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
143
215
  }
144
216
 
145
217
  // Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
218
+ let lastGoLiveCustomId = null;
146
219
  for (let step = 0; step < 6; step++) {
147
220
  text = getFullText(response);
148
221
  lower = normalizeLower(stripAnsi(text));
222
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
149
223
 
150
- if (isLiveDashboard(response, lower)) break;
151
- if (!isStreamManagerScreen(lower)) break;
224
+ if (isLiveDashboard(response, lower)) break;
225
+ if (!isStreamManagerScreen(response, lower)) break;
152
226
 
153
227
  const selected = await selectRandomStreamOption(response);
154
228
  if (selected) {
@@ -162,7 +236,34 @@ async function runStream({ channel, waitForDankMemer, client }) {
162
236
  }
163
237
 
164
238
  const goLiveBtn = getGoLiveButton(response);
239
+ if (goLiveBtn?.customId || goLiveBtn?.custom_id) {
240
+ lastGoLiveCustomId = goLiveBtn.customId || goLiveBtn.custom_id;
241
+ }
165
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
+
166
267
  await sleep(300);
167
268
  const fresh = await refetchMsg(channel, response.id);
168
269
  if (fresh) {
@@ -176,14 +277,57 @@ async function runStream({ channel, waitForDankMemer, client }) {
176
277
  LOG.info('[stream] Clicking "Go Live"');
177
278
  await humanDelay(100, 250);
178
279
  try {
280
+ const before = response;
179
281
  const liveResult = await safeClickButton(response, goLiveBtn);
180
- const next = liveResult || (await waitForDankMemer(7000)) || (await refetchMsg(channel, response.id));
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
+ }
181
298
  if (next) {
182
299
  response = next;
183
300
  await hydrate(response);
184
301
  logMsg(response, `stream-go-live-${step}`);
185
302
  continue;
186
303
  }
304
+
305
+ // Some accounts open setup first; try a fallback non-destructive button once.
306
+ const fallbackBtn = getAllButtons(response).find(b =>
307
+ !b.disabled &&
308
+ b.label &&
309
+ ((b.label || '').toLowerCase().includes('view setup') || (b.label || '').toLowerCase().includes('setup'))
310
+ );
311
+ if (fallbackBtn) {
312
+ LOG.info('[stream] Go Live transition not detected; trying setup fallback');
313
+ await humanDelay(80, 180);
314
+ try {
315
+ const fallbackRes = await safeClickButton(response, fallbackBtn);
316
+ const afterFallback = fallbackRes || (await waitForStreamTransition({
317
+ channel,
318
+ waitForDankMemer,
319
+ client,
320
+ prevMsg: before,
321
+ timeoutMs: 8000,
322
+ }));
323
+ if (afterFallback) {
324
+ response = afterFallback;
325
+ await hydrate(response);
326
+ logMsg(response, `stream-setup-fallback-${step}`);
327
+ continue;
328
+ }
329
+ } catch {}
330
+ }
187
331
  } catch (e) {
188
332
  LOG.error(`[stream] Go Live click failed: ${e.message}`);
189
333
  }
@@ -194,11 +338,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
194
338
  // Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
195
339
  text = getFullText(response);
196
340
  lower = normalizeLower(stripAnsi(text));
341
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
197
342
 
198
343
  if (isLiveDashboard(response, lower)) {
199
344
  const actions = getLiveActionButtons(response);
200
345
  if (actions.length > 0) {
201
346
  const action = pickRandom(actions);
347
+ const actionAt = new Date();
202
348
  LOG.info(`[stream] Live action: "${action.label}"`);
203
349
  await humanDelay(120, 320);
204
350
  try {
@@ -231,10 +377,12 @@ async function runStream({ channel, waitForDankMemer, client }) {
231
377
  }
232
378
  text = bestText;
233
379
  lower = normalizeLower(stripAnsi(text));
380
+ nextCooldownSec = parseStreamInteractCooldownSec(text);
234
381
  }
235
382
  } catch (e) {
236
383
  LOG.error(`[stream] Action click failed: ${e.message}`);
237
384
  }
385
+ LOG.info(`[stream] Action timestamp: ${actionAt.toISOString()} | next interaction in ${Math.ceil(nextCooldownSec / 60)}m`);
238
386
  } else {
239
387
  LOG.info('[stream] Live dashboard found but no action buttons available yet');
240
388
  }
@@ -244,13 +392,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
244
392
  const coins = parseCoins(text);
245
393
  if (coins > 0) {
246
394
  LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
247
- return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
395
+ return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec };
248
396
  }
249
397
 
250
- if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
398
+ if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec };
251
399
 
252
400
  const short = normalizeLower(stripAnsi(text)).substring(0, 60);
253
- return { result: short || 'streamed', coins: 0, nextCooldownSec: 600 };
401
+ return { result: short || 'streamed', coins: 0, nextCooldownSec };
254
402
  }
255
403
 
256
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.13.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"