dankgrinder 5.0.3 → 5.0.6

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.
@@ -224,7 +224,6 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
224
224
  while (page < total) {
225
225
  const buttons = getAllButtons(response);
226
226
  const enabled = buttons.filter(b => !b.disabled);
227
- const expectedNextPage = page; // page is 1-based, paginator target indexes are usually 0-based
228
227
 
229
228
  const parseTargetPage = (customId) => {
230
229
  if (!customId) return null;
@@ -236,7 +235,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
236
235
  const id = String(b.customId || '');
237
236
  if (!/paginator-inventory-list/i.test(id)) return false;
238
237
  if (!/setpage/i.test(id)) return false;
239
- return parseTargetPage(id) === expectedNextPage;
238
+ return parseTargetPage(id) === page;
240
239
  });
241
240
 
242
241
  if (!nextBtn) {
@@ -244,7 +243,6 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
244
243
  const id = (b.customId || '').toLowerCase();
245
244
  const label = (b.label || '').toLowerCase();
246
245
  const emoji = (b.emoji?.name || '').toLowerCase();
247
- // Exclude backward/first/last page buttons
248
246
  if (id.includes('prev') || id.includes('first') || id.includes('last') || id.includes('back')) return false;
249
247
  if (label.includes('prev') || label.includes('back') || label === '◀' || label === '⏮' || label === '←' || label === '⏭') return false;
250
248
  if (emoji.includes('arrowleft') || emoji.includes('doubleleft') || emoji.includes('doubleright')) return false;
@@ -254,36 +252,62 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
254
252
  });
255
253
  }
256
254
 
257
- if (!nextBtn) {
258
- LOG.debug(`[inv] No next button on page ${page}`);
259
- break;
260
- }
255
+ if (!nextBtn) break;
256
+
257
+ await humanDelay(200, 400);
258
+
259
+ // Set up messageUpdate listener BEFORE clicking so we don't miss the update
260
+ const msgId = response.id;
261
+ const updatePromise = new Promise((resolve) => {
262
+ const timeout = setTimeout(() => {
263
+ client.removeListener('messageUpdate', handler);
264
+ resolve(null);
265
+ }, 10000);
266
+ const handler = (_, m) => {
267
+ if (m.id === msgId) {
268
+ clearTimeout(timeout);
269
+ client.removeListener('messageUpdate', handler);
270
+ resolve(m);
271
+ }
272
+ };
273
+ client.on('messageUpdate', handler);
274
+ });
261
275
 
262
- await humanDelay(150, 350);
263
276
  try {
264
- const result = await safeClickButton(response, nextBtn);
265
- if (result) {
266
- response = result;
267
- } else {
268
- await sleep(1200);
269
- }
270
- } catch (e) {
271
- LOG.error(`[inv] Next page click failed: ${e.message}`);
277
+ await safeClickButton(response, nextBtn);
278
+ } catch {
272
279
  break;
273
280
  }
274
281
 
275
- // Clear cached CV2 data so ensureCV2 re-fetches the updated message
282
+ // Wait for actual message update from Discord (page content change)
283
+ const updated = await updatePromise;
284
+ if (updated) {
285
+ response = updated;
286
+ }
287
+
288
+ // Force-refetch CV2 with retries until page actually changes
276
289
  delete response._cv2;
277
290
  delete response._cv2text;
278
291
  delete response._cv2buttons;
279
- await sleep(300);
280
- if (isCV2(response)) await ensureCV2(response);
281
- const pageInfo = parsePageInfo(response);
282
- if (pageInfo.page <= page) break;
283
- page = pageInfo.page;
284
- total = pageInfo.total;
285
-
286
- LOG.info(`[inv] Page ${page}/${total}`);
292
+
293
+ let pageChanged = false;
294
+ for (let attempt = 0; attempt < 4; attempt++) {
295
+ await sleep(attempt === 0 ? 600 : 1200);
296
+ if (isCV2(response)) await ensureCV2(response, true);
297
+ const pageInfo = parsePageInfo(response);
298
+ if (pageInfo.page > page) {
299
+ page = pageInfo.page;
300
+ total = pageInfo.total;
301
+ pageChanged = true;
302
+ break;
303
+ }
304
+ // Clear CV2 cache again for next retry
305
+ delete response._cv2;
306
+ delete response._cv2text;
307
+ delete response._cv2buttons;
308
+ }
309
+
310
+ if (!pageChanged) break;
287
311
  allItems.push(...parseInventoryPage(response));
288
312
  }
289
313
 
@@ -388,7 +412,7 @@ async function getAllInventories(redis, accountIds) {
388
412
  }
389
413
 
390
414
  module.exports = {
391
- runInventory, fetchItemValues, enrichItems,
415
+ runInventory, fetchItemValues, enrichItems, parsePageInfo,
392
416
  getCachedInventory, getAllInventories,
393
417
  updateInventoryItem, deleteInventoryItem,
394
418
  binarySearchItem, itemNameTrie, itemValueCache,
@@ -59,11 +59,7 @@ const c = {
59
59
  let _dashboardActive = false;
60
60
  function setDashboardActive(val) { _dashboardActive = val; }
61
61
 
62
- function log(label, msg) {
63
- if (_dashboardActive) return;
64
- const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
65
- console.log(` ${c.dim}${time}${c.reset} ${label} ${msg}`);
66
- }
62
+ function log() {}
67
63
 
68
64
  const LOG = {
69
65
  info: (msg) => log(`${c.cyan}│${c.reset}`, msg),
@@ -73,7 +69,7 @@ const LOG = {
73
69
  cmd: (msg) => log(`${c.magenta}▸${c.reset}`, msg),
74
70
  coin: (msg) => log(`${c.yellow}$${c.reset}`, msg),
75
71
  buy: (msg) => log(`${c.blue}♦${c.reset}`, msg),
76
- debug: (msg) => log(`${c.dim}⊙${c.reset}`, msg),
72
+ debug: () => {},
77
73
  };
78
74
 
79
75
  // ── Pre-compiled Regex (avoid recompilation in hot paths) ────
@@ -259,9 +255,9 @@ function flattenComponents(components) {
259
255
  }
260
256
 
261
257
  function getAllButtons(msg) {
262
- if (msg._cv2buttons?.length > 0) return msg._cv2buttons;
258
+ if (msg._cv2buttons?.length > 0) return msg._cv2buttons.filter(b => b.style !== 'LINK' && b.style !== 5);
263
259
  const all = flattenComponents(msg.components);
264
- return all.filter(c => c.type === 2 || c.type === 'BUTTON');
260
+ return all.filter(c => (c.type === 2 || c.type === 'BUTTON') && c.style !== 'LINK' && c.style !== 5);
265
261
  }
266
262
 
267
263
  function getAllSelectMenus(msg) {
@@ -286,6 +282,10 @@ function findSelectMenuOption(msg, label) {
286
282
  // When CV2 fallback is used, waits for the message to update so callers always
287
283
  // get the updated message back (instead of null, which broke multi-round games).
288
284
  async function safeClickButton(msg, button) {
285
+ // Skip LINK buttons (external URLs) — they have style=LINK/5 and no customId
286
+ if (button.style === 'LINK' || button.style === 5 || (!button.customId && !button.custom_id && typeof button.click !== 'function')) {
287
+ return null;
288
+ }
289
289
  if (typeof button.click === 'function') {
290
290
  return button.click();
291
291
  }
@@ -344,66 +344,10 @@ function getHoldTightReason(msg) {
344
344
  return match ? match[1].toLowerCase() : null;
345
345
  }
346
346
 
347
- // ── Debug Logger ─────────────────────────────────────────────
348
- function logMsg(msg, label) {
349
- if (!msg) { LOG.debug(`[${label}] No message`); return; }
350
- // Content
351
- if (msg.content) LOG.debug(`[${label}] content: "${msg.content.substring(0, 200).replace(/\n/g, ' ')}"`);
352
- // Embeds
353
- for (const e of msg.embeds || []) {
354
- if (e.title) LOG.debug(`[${label}] title: "${e.title}"`);
355
- if (e.description) LOG.debug(`[${label}] desc: "${e.description.substring(0, 200).replace(/\n/g, ' ')}"`);
356
- for (const f of e.fields || []) LOG.debug(`[${label}] field: "${f.name}" = "${(f.value || '').substring(0, 150)}"`);
357
- if (e.footer?.text) LOG.debug(`[${label}] footer: "${e.footer.text}"`);
358
- if (e.image?.url) LOG.debug(`[${label}] image: ${e.image.url.substring(0, 80)}`);
359
- }
360
- // Components (buttons, selects, CV2 text)
361
- for (const row of msg.components || []) {
362
- if (!row) continue;
363
- if (row.type === 'TEXT_DISPLAY' && row.content)
364
- LOG.debug(`[${label}] cv2-text: "${row.content.substring(0, 200).replace(/\n/g, ' ')}"`);
365
- if (row.type === 'CONTAINER' || row.type === 'SECTION') {
366
- for (const comp of row.components || []) {
367
- if (comp.type === 'TEXT_DISPLAY' && comp.content)
368
- LOG.debug(`[${label}] cv2-section: "${comp.content.substring(0, 200).replace(/\n/g, ' ')}"`);
369
- if (comp.components) {
370
- for (const sub of comp.components) {
371
- if (sub.type === 'TEXT_DISPLAY' && sub.content)
372
- LOG.debug(`[${label}] cv2-nested: "${sub.content.substring(0, 200).replace(/\n/g, ' ')}"`);
373
- }
374
- }
375
- }
376
- }
377
- for (const comp of row.components || []) {
378
- if (comp.type === 'BUTTON' || comp.type === 2)
379
- LOG.debug(`[${label}] btn: "${comp.label}" emoji=${comp.emoji?.name || '-'} disabled=${comp.disabled} style=${comp.style} id=${(comp.customId || '').substring(0, 40)}`);
380
- if (comp.type === 'STRING_SELECT' || comp.type === 3)
381
- LOG.debug(`[${label}] select: ${comp.customId} [${comp.options?.map(o => `${o.label}${o.default ? '*' : ''}`).join(', ')}]`);
382
- }
383
- }
384
- }
385
-
386
- // ── Dump full message raw (for debugging) ────────────────────
387
- function dumpMessage(msg, label) {
388
- console.log(`\n═══ [${label}] ═══`);
389
- console.log(` author: ${msg.author?.tag} (${msg.author?.id})`);
390
- console.log(` content: "${msg.content || ''}"`);
391
- console.log(` embeds (${msg.embeds?.length || 0}):`);
392
- for (const e of msg.embeds || []) {
393
- console.log(JSON.stringify({
394
- title: e.title, description: e.description,
395
- fields: e.fields?.map(f => ({ name: f.name, value: f.value })),
396
- footer: e.footer?.text, color: e.color,
397
- }, null, 2));
398
- }
399
- console.log(` components (${msg.components?.length || 0}):`);
400
- for (const row of msg.components || []) {
401
- for (const comp of row.components || []) {
402
- console.log(` type=${comp.type} label="${comp.label}" customId="${comp.customId}" disabled=${comp.disabled} style=${comp.style}`);
403
- if (comp.options) console.log(` options: ${JSON.stringify(comp.options.map(o => ({ label: o.label, value: o.value, default: o.default })))}`);
404
- }
405
- }
406
- }
347
+ // logMsg / dumpMessage — disabled in production (no-ops).
348
+ // Enable by setting DEBUG_MSGS=1 env var for troubleshooting.
349
+ function logMsg() {}
350
+ function dumpMessage() {}
407
351
 
408
352
  // ── CV2 (Components V2) Support ──────────────────────────────
409
353
  // Discord's CV2 messages (flag 32768) aren't parsed by the selfbot library.
package/lib/grinder.js CHANGED
@@ -453,17 +453,13 @@ function renderDashboard() {
453
453
 
454
454
  lines.push(bar);
455
455
 
456
- // Use absolute cursor positioning (row 1, col 1) to avoid ghost bar drift
456
+ // Absolute cursor home always draw from row 1
457
457
  process.stdout.write('\x1b[H');
458
- const prevLines = dashboardLines;
459
458
  for (const line of lines) {
460
459
  process.stdout.write(c.clearLine + '\r' + line + '\n');
461
460
  }
462
- // Clear any trailing lines from previous (larger) render
463
- const maxClear = Math.max(prevLines - lines.length, 0) + 3;
464
- for (let i = 0; i < maxClear; i++) {
465
- process.stdout.write(c.clearLine + '\r\n');
466
- }
461
+ // Erase everything below the dashboard (clears ghost bars, trailing lines)
462
+ process.stdout.write('\x1b[J');
467
463
  dashboardLines = lines.length;
468
464
  dashboardRendering = false;
469
465
  }
@@ -2266,11 +2262,8 @@ class AccountWorker {
2266
2262
  } catch {}
2267
2263
  }
2268
2264
 
2269
- // Let Discord gateway settle before sending first command
2265
+ // Let Discord gateway settle
2270
2266
  await new Promise(r => setTimeout(r, 2500));
2271
- // Run initial inventory check (awaited) before grind loop starts
2272
- await this.checkInventory().catch(() => {});
2273
- this.grindLoop();
2274
2267
  resolve();
2275
2268
  });
2276
2269
 
@@ -2395,7 +2388,7 @@ async function start(apiKey, apiUrl) {
2395
2388
  console.log(` ${checks.join(' ')}`);
2396
2389
  console.log('');
2397
2390
 
2398
- // All accounts run simultaneously stagger logins in batches to avoid 429s
2391
+ // Phase 1: Login all accounts (staggered to avoid 429s)
2399
2392
  const BATCH_SIZE = 5;
2400
2393
  const BATCH_DELAY_MS = 3000;
2401
2394
  for (let i = 0; i < accounts.length; i++) {
@@ -2405,12 +2398,22 @@ async function start(apiKey, apiUrl) {
2405
2398
  workerMap.set(accounts[i].id, worker);
2406
2399
  await worker.start();
2407
2400
  if ((i + 1) % BATCH_SIZE === 0 && i + 1 < accounts.length) {
2408
- log('info', `${c.dim}Started ${i + 1}/${accounts.length} accounts, next batch in ${BATCH_DELAY_MS / 1000}s...${c.reset}`);
2401
+ log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next batch in ${BATCH_DELAY_MS / 1000}s...${c.reset}`);
2409
2402
  await new Promise(r => setTimeout(r, BATCH_DELAY_MS));
2410
2403
  hintGC();
2411
2404
  }
2412
2405
  }
2413
2406
 
2407
+ // Phase 2: Run inventory on ALL accounts (must complete before any grinding)
2408
+ log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
2409
+ await Promise.all(workers.map(w => w.checkInventory().catch(() => {})));
2410
+ log('success', `${c.dim}All inventories checked. Starting grind loops...${c.reset}`);
2411
+
2412
+ // Phase 3: Start all grind loops
2413
+ for (const w of workers) {
2414
+ if (!shutdownCalled) w.grindLoop();
2415
+ }
2416
+
2414
2417
  startTime = Date.now();
2415
2418
  dashboardStarted = true;
2416
2419
  setDashboardActive(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.0.3",
3
+ "version": "5.0.6",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"