dankgrinder 5.16.0 → 5.19.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.
@@ -29,6 +29,13 @@ const ITEM_COSTS = Object.freeze({
29
29
  'adventure ticket': 250000,
30
30
  'keyboard': 10000,
31
31
  'mouse': 10000,
32
+ 'hoe': 25000,
33
+ 'watering can': 25000,
34
+ 'water can': 25000,
35
+ 'water bucket': 100000,
36
+ 'seeds': 5000,
37
+ 'seed': 5000,
38
+ 'corn seeds': 5000,
32
39
  });
33
40
 
34
41
  const ITEM_SEARCH_TERM = Object.freeze({
@@ -39,6 +46,13 @@ const ITEM_SEARCH_TERM = Object.freeze({
39
46
  'adventure ticket': 'ticket',
40
47
  'keyboard': 'keyboard',
41
48
  'mouse': 'mouse',
49
+ 'hoe': 'hoe',
50
+ 'watering can': 'water can',
51
+ 'water can': 'water can',
52
+ 'water bucket': 'water bucket',
53
+ 'seeds': 'seeds',
54
+ 'seed': 'seed',
55
+ 'corn seeds': 'corn seeds',
42
56
  });
43
57
 
44
58
  const ITEM_SHOP_TAB = Object.freeze({
@@ -49,6 +63,13 @@ const ITEM_SHOP_TAB = Object.freeze({
49
63
  'adventure ticket': 'Coin Shop',
50
64
  'keyboard': 'Coin Shop',
51
65
  'mouse': 'Coin Shop',
66
+ 'hoe': 'Coin Shop',
67
+ 'watering can': 'Coin Shop',
68
+ 'water can': 'Coin Shop',
69
+ 'water bucket': 'Coin Shop',
70
+ 'seeds': 'Coin Shop',
71
+ 'seed': 'Coin Shop',
72
+ 'corn seeds': 'Coin Shop',
52
73
  });
53
74
 
54
75
  // Trie for item button label matching — O(k) vs O(n) linear scan
@@ -102,50 +123,34 @@ function findNextShopPageButton(msg) {
102
123
  }) || null;
103
124
  }
104
125
 
105
- /**
106
- * Buy an item from the Dank Memer shop.
107
- *
108
- * @param {object} opts
109
- * @param {object} opts.channel
110
- * @param {function} opts.waitForDankMemer
111
- * @param {string} opts.itemName - e.g. 'Hunting Rifle', 'Shovel'
112
- * @param {number} [opts.quantity=1]
113
- * @param {object} [opts.client] - Discord client
114
- * @returns {Promise<boolean>} true if purchase succeeded
115
- */
116
- async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, client }) {
117
- const key = itemName.toLowerCase();
118
- const searchTerm = ITEM_SEARCH_TERM[key] || key.split(' ').pop();
119
- const targetTab = ITEM_SHOP_TAB[key] || 'Coin Shop';
120
-
121
- // LRU dedup: skip if we bought this item in the last 10 seconds
122
- const lastBuy = recentPurchases.get(key);
123
- if (lastBuy && Date.now() - lastBuy < 10000) {
124
- LOG.info(`[shop] Skipping ${itemName} — just purchased ${Math.round((Date.now() - lastBuy) / 1000)}s ago`);
125
- return true;
126
- }
127
-
128
- LOG.buy(`Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}`);
126
+ function normalizeShopRequest(itemName, quantity = 1) {
127
+ const key = String(itemName || '').toLowerCase();
128
+ return {
129
+ itemName,
130
+ quantity,
131
+ key,
132
+ searchTerm: ITEM_SEARCH_TERM[key] || key.split(' ').pop(),
133
+ targetTab: ITEM_SHOP_TAB[key] || 'Coin Shop',
134
+ };
135
+ }
129
136
 
130
- // Step 1: Open shop
137
+ async function openShopView({ channel, waitForDankMemer }) {
131
138
  await channel.send('pls shop view');
132
139
  let response = await waitForDankMemer(10000);
133
140
 
134
- if (!response) {
135
- LOG.warn('[shop] No response to pls shop view');
136
- return false;
137
- }
141
+ if (!response) return { ok: false, reason: 'no-response', response: null };
138
142
  if (isHoldTight(response)) {
139
143
  LOG.warn('[shop] Hold tight — waiting 30s');
140
144
  await sleep(30000);
141
- return false;
145
+ return { ok: false, reason: 'hold-tight', response: null };
142
146
  }
143
147
 
144
148
  if (isCV2(response)) await ensureCV2(response);
145
-
146
149
  logMsg(response, 'shop');
150
+ return { ok: true, reason: 'ok', response };
151
+ }
147
152
 
148
- // Step 2: Switch shop tab if needed (e.g. Fishing Shop for fishing pole)
153
+ async function ensureShopTab({ response, waitForDankMemer, targetTab }) {
149
154
  const selectInfo = findSelectMenuOption(response, targetTab);
150
155
  if (selectInfo) {
151
156
  const isAlreadySelected = (selectInfo.component.options || [])
@@ -176,12 +181,9 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
176
181
  await sleep(300);
177
182
  } catch (e) {
178
183
  LOG.error(`[shop] Tab switch failed: ${e.message}`);
179
- return false;
180
184
  }
181
185
  }
182
186
  } 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
187
  const menus = getAllSelectMenus(response);
186
188
  const menu = menus.find(m => (m.options || []).length > 0);
187
189
  if (menu) {
@@ -199,8 +201,10 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
199
201
  }
200
202
  }
201
203
  }
204
+ return response;
205
+ }
202
206
 
203
- // Step 3: Find the Buy button for our item (scan pages in current tab)
207
+ async function findBuyButtonPaged({ response, waitForDankMemer, searchTerm, key }) {
204
208
  let buyBtn = null;
205
209
  for (let page = 0; page < 10; page++) {
206
210
  buyBtn = findBuyButtonInMessage(response, searchTerm, key);
@@ -217,22 +221,16 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
217
221
  break;
218
222
  }
219
223
  }
224
+ return { response, buyBtn };
225
+ }
220
226
 
221
- if (!buyBtn) {
222
- LOG.warn(`[shop] No button for "${itemName}" (search: "${searchTerm}")`);
223
- const named = getAllButtons(response).filter(b => b.label);
224
- if (named.length > 0) {
225
- LOG.debug(`[shop] Available: ${named.map(b => `"${b.label}"(${b.disabled ? 'DIS' : 'EN'})`).join(', ')}`);
226
- }
227
- return false;
228
- }
229
-
227
+ async function performBuyFromButton({ response, buyBtn, waitForDankMemer, itemName, quantity, key }) {
228
+ if (!buyBtn) return { ok: false, reason: 'not-found', response };
230
229
  if (buyBtn.disabled) {
231
230
  LOG.warn(`[shop] "${buyBtn.label}" is DISABLED — not enough coins for ${itemName}`);
232
- return false;
231
+ return { ok: false, reason: 'disabled', response };
233
232
  }
234
233
 
235
- // Step 4: Click the Buy button → Modal with quantity input
236
234
  LOG.buy(`Clicking "${buyBtn.label}"...`);
237
235
  const btnId = buyBtn.customId || buyBtn.custom_id;
238
236
  let clickResult;
@@ -240,11 +238,9 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
240
238
  clickResult = await response.clickButton(btnId);
241
239
  } catch (e) {
242
240
  LOG.error(`[shop] Button click failed: ${e.message}`);
243
- return false;
241
+ return { ok: false, reason: 'click-failed', response };
244
242
  }
245
243
 
246
- // Some shop buttons buy immediately (no quantity modal).
247
- // If we received a message-like response, parse it directly.
248
244
  if (clickResult && !clickResult.components?.[0]?.components?.[0]?.setValue) {
249
245
  if (isCV2(clickResult)) await ensureCV2(clickResult);
250
246
  const directText = getFullText(clickResult);
@@ -252,14 +248,13 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
252
248
  if (isBuySuccessText(directText)) {
253
249
  LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
254
250
  recentPurchases.set(key, Date.now());
255
- return true;
251
+ return { ok: true, reason: 'direct-success', response: clickResult };
256
252
  }
257
253
  if (isBuyFailText(directText)) {
258
254
  LOG.warn(`[shop] Direct buy failed for ${itemName}: ${directText.substring(0, 120)}`);
259
- return false;
255
+ return { ok: false, reason: 'direct-failed', response: clickResult };
260
256
  }
261
257
 
262
- // Fallback: wait one follow-up message for purchase outcome text.
263
258
  const follow = await waitForDankMemer(6000);
264
259
  if (follow) {
265
260
  if (isCV2(follow)) await ensureCV2(follow);
@@ -268,27 +263,26 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
268
263
  if (isBuySuccessText(fText)) {
269
264
  LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
270
265
  recentPurchases.set(key, Date.now());
271
- return true;
266
+ return { ok: true, reason: 'followup-success', response: follow };
272
267
  }
273
268
  if (isBuyFailText(fText)) {
274
269
  LOG.warn(`[shop] Buy failed for ${itemName}: ${fText.substring(0, 120)}`);
275
- return false;
270
+ return { ok: false, reason: 'followup-failed', response: follow };
276
271
  }
272
+ return { ok: true, reason: 'followup-unknown', response: follow };
277
273
  }
278
274
 
279
275
  LOG.success(`Buy click sent for ${itemName} (no modal path)`);
280
276
  recentPurchases.set(key, Date.now());
281
- return true;
277
+ return { ok: true, reason: 'direct-unknown', response: clickResult };
282
278
  }
283
279
 
284
280
  const modal = clickResult;
285
-
286
281
  if (!modal || !modal.components) {
287
282
  LOG.warn('[shop] No modal returned from button click');
288
- return false;
283
+ return { ok: false, reason: 'no-modal', response };
289
284
  }
290
285
 
291
- // Step 5: Set quantity and submit the modal
292
286
  LOG.buy(`Modal "${modal.title}" — qty=${quantity}`);
293
287
  try {
294
288
  modal.components[0].components[0].setValue(String(quantity));
@@ -301,20 +295,129 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
301
295
  if (isBuySuccessText(text)) {
302
296
  LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
303
297
  recentPurchases.set(key, Date.now());
304
- return true;
298
+ return { ok: true, reason: 'modal-success', response: result };
305
299
  }
306
300
  if (isBuyFailText(text)) {
307
301
  LOG.warn(`Not enough coins to buy ${itemName}`);
308
- return false;
302
+ return { ok: false, reason: 'modal-failed', response: result };
309
303
  }
304
+ return { ok: true, reason: 'modal-unknown', response: result };
310
305
  }
311
306
 
312
307
  LOG.success(`Modal submitted for ${quantity}x ${itemName} (assuming success)`);
313
- return true;
308
+ return { ok: true, reason: 'modal-submitted', response };
314
309
  } catch (e) {
315
310
  LOG.error(`[shop] Modal submit failed: ${e.message}`);
311
+ return { ok: false, reason: 'modal-submit-failed', response };
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Buy an item from the Dank Memer shop.
317
+ *
318
+ * @param {object} opts
319
+ * @param {object} opts.channel
320
+ * @param {function} opts.waitForDankMemer
321
+ * @param {string} opts.itemName - e.g. 'Hunting Rifle', 'Shovel'
322
+ * @param {number} [opts.quantity=1]
323
+ * @param {object} [opts.client] - Discord client
324
+ * @returns {Promise<boolean>} true if purchase succeeded
325
+ */
326
+ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, client }) {
327
+ const { key, searchTerm, targetTab } = normalizeShopRequest(itemName, quantity);
328
+
329
+ // LRU dedup: skip if we bought this item in the last 10 seconds
330
+ const lastBuy = recentPurchases.get(key);
331
+ if (lastBuy && Date.now() - lastBuy < 10000) {
332
+ LOG.info(`[shop] Skipping ${itemName} — just purchased ${Math.round((Date.now() - lastBuy) / 1000)}s ago`);
333
+ return true;
334
+ }
335
+
336
+ LOG.buy(`Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}`);
337
+
338
+ // Step 1: Open shop
339
+ const opened = await openShopView({ channel, waitForDankMemer });
340
+ if (!opened.ok) {
341
+ LOG.warn('[shop] No response to pls shop view');
316
342
  return false;
317
343
  }
344
+ let response = opened.response;
345
+
346
+ // Step 2: Switch shop tab if needed
347
+ response = await ensureShopTab({ response, waitForDankMemer, targetTab });
348
+
349
+ // Step 3: Find the Buy button for our item (scan pages in current tab)
350
+ const located = await findBuyButtonPaged({ response, waitForDankMemer, searchTerm, key });
351
+ response = located.response;
352
+ const buyBtn = located.buyBtn;
353
+
354
+ if (!buyBtn) {
355
+ LOG.warn(`[shop] No button for "${itemName}" (search: "${searchTerm}")`);
356
+ const named = getAllButtons(response).filter(b => b.label);
357
+ if (named.length > 0) {
358
+ LOG.debug(`[shop] Available: ${named.map(b => `"${b.label}"(${b.disabled ? 'DIS' : 'EN'})`).join(', ')}`);
359
+ }
360
+ return false;
361
+ }
362
+
363
+ if (buyBtn.disabled) {
364
+ LOG.warn(`[shop] "${buyBtn.label}" is DISABLED — not enough coins for ${itemName}`);
365
+ return false;
366
+ }
367
+
368
+ // Step 4/5: purchase
369
+ const bought = await performBuyFromButton({ response, buyBtn, waitForDankMemer, itemName, quantity, key });
370
+ return !!bought.ok;
371
+ }
372
+
373
+ async function buyItemsBatch({ channel, waitForDankMemer, client, items = [] }) {
374
+ const requests = items
375
+ .map(i => normalizeShopRequest(i.itemName || i.item || '', i.quantity || i.qty || 1))
376
+ .filter(r => r.itemName && r.quantity > 0);
377
+
378
+ if (requests.length === 0) return { ok: true, results: [] };
379
+
380
+ const opened = await openShopView({ channel, waitForDankMemer });
381
+ if (!opened.ok) return { ok: false, results: requests.map(r => ({ itemName: r.itemName, success: false, reason: opened.reason })) };
382
+
383
+ let response = opened.response;
384
+ const results = [];
385
+
386
+ for (const req of requests) {
387
+ const lastBuy = recentPurchases.get(req.key);
388
+ if (lastBuy && Date.now() - lastBuy < 10000) {
389
+ results.push({ itemName: req.itemName, success: true, reason: 'recent' });
390
+ continue;
391
+ }
392
+
393
+ response = await ensureShopTab({ response, waitForDankMemer, targetTab: req.targetTab });
394
+ const located = await findBuyButtonPaged({ response, waitForDankMemer, searchTerm: req.searchTerm, key: req.key });
395
+ response = located.response;
396
+
397
+ if (!located.buyBtn) {
398
+ const named = getAllButtons(response).filter(b => b.label);
399
+ if (named.length > 0) {
400
+ LOG.debug(`[shop] Available: ${named.map(b => `"${b.label}"(${b.disabled ? 'DIS' : 'EN'})`).join(', ')}`);
401
+ }
402
+ results.push({ itemName: req.itemName, success: false, reason: 'not-found' });
403
+ continue;
404
+ }
405
+
406
+ const bought = await performBuyFromButton({
407
+ response,
408
+ buyBtn: located.buyBtn,
409
+ waitForDankMemer,
410
+ itemName: req.itemName,
411
+ quantity: req.quantity,
412
+ key: req.key,
413
+ });
414
+ response = bought.response || response;
415
+ results.push({ itemName: req.itemName, success: !!bought.ok, reason: bought.reason });
416
+
417
+ await sleep(300);
418
+ }
419
+
420
+ return { ok: results.every(r => r.success), results };
318
421
  }
319
422
 
320
- module.exports = { buyItem, ITEM_COSTS };
423
+ module.exports = { buyItem, buyItemsBatch, ITEM_COSTS };
@@ -311,7 +311,8 @@ async function safeClickButton(msg, button) {
311
311
  // CV2 fallback: send interaction via raw HTTP, then wait for the message
312
312
  // to update so we can return the updated message to the caller.
313
313
  if (id) {
314
- await clickCV2Button(msg, id);
314
+ const interactionAck = await clickCV2Button(msg, id);
315
+ if (interactionAck) msg._lastInteractionAck = interactionAck;
315
316
  // Wait for Dank Memer to process the interaction and update the message
316
317
  const updatedMsg = await new Promise((resolve) => {
317
318
  const timeout = setTimeout(() => {
@@ -523,7 +524,73 @@ async function clickCV2Button(msg, customId) {
523
524
  Authorization: token, 'Content-Type': 'application/json',
524
525
  }, payload);
525
526
  if (resp.status >= 400) throw new Error(`CV2 click ${resp.status}: ${resp.body.substring(0, 200)}`);
526
- return null;
527
+
528
+ let parsed = null;
529
+ try {
530
+ parsed = resp.body ? JSON.parse(resp.body) : null;
531
+ } catch {
532
+ parsed = null;
533
+ }
534
+
535
+ const data = parsed?.data || null;
536
+ if (!data && !parsed) return null;
537
+
538
+ return {
539
+ interactionStatus: resp.status,
540
+ interactionType: parsed?.type ?? null,
541
+ flags: data?.flags ?? parsed?.flags ?? 0,
542
+ content: data?.content ?? parsed?.content ?? '',
543
+ embeds: data?.embeds ?? parsed?.embeds ?? [],
544
+ components: data?.components ?? parsed?.components ?? [],
545
+ raw: parsed || resp.body,
546
+ };
547
+ }
548
+
549
+ async function clickCV2SelectMenu(msg, customId, values = []) {
550
+ const token = msg.client?.token;
551
+ const chId = msg.channelId || msg.channel?.id;
552
+ const gId = msg.guildId || msg.guild?.id;
553
+ if (!token) throw new Error('No token for CV2 select');
554
+ const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
555
+ const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
556
+ const payloadObj = {
557
+ type: 3,
558
+ application_id: String(msg.applicationId || DANK_MEMER_ID),
559
+ nonce,
560
+ channel_id: String(chId),
561
+ message_id: String(msg.id),
562
+ data: {
563
+ component_type: 3,
564
+ custom_id: customId,
565
+ values: Array.isArray(values) ? values.map(v => String(v)) : [],
566
+ },
567
+ };
568
+ if (gId) payloadObj.guild_id = String(gId);
569
+ if (sessionId) payloadObj.session_id = sessionId;
570
+
571
+ const resp = await _httpPost('https://discord.com/api/v9/interactions', {
572
+ Authorization: token, 'Content-Type': 'application/json',
573
+ }, JSON.stringify(payloadObj));
574
+ if (resp.status >= 400) throw new Error(`CV2 select ${resp.status}: ${resp.body.substring(0, 200)}`);
575
+
576
+ let parsed = null;
577
+ try {
578
+ parsed = resp.body ? JSON.parse(resp.body) : null;
579
+ } catch {
580
+ parsed = null;
581
+ }
582
+
583
+ const data = parsed?.data || null;
584
+ if (!data && !parsed) return null;
585
+ return {
586
+ interactionStatus: resp.status,
587
+ interactionType: parsed?.type ?? null,
588
+ flags: data?.flags ?? parsed?.flags ?? 0,
589
+ content: data?.content ?? parsed?.content ?? '',
590
+ embeds: data?.embeds ?? parsed?.embeds ?? [],
591
+ components: data?.components ?? parsed?.components ?? [],
592
+ raw: parsed || resp.body,
593
+ };
527
594
  }
528
595
 
529
596
  // ── Item Detection (Aho-Corasick Automaton) ──────────────────
@@ -596,6 +663,7 @@ module.exports = {
596
663
  isCV2,
597
664
  ensureCV2,
598
665
  clickCV2Button,
666
+ clickCV2SelectMenu,
599
667
  // Shared structures and optimized constants
600
668
  strings,
601
669
  cv2Cache,
package/lib/grinder.js CHANGED
@@ -1353,6 +1353,7 @@ class AccountWorker {
1353
1353
  case 'search': cmdResult = await commands.runSearch(cmdOpts); break;
1354
1354
  case 'crime': cmdResult = await commands.runCrime(cmdOpts); break;
1355
1355
  case 'hl': cmdResult = await commands.runHighLow(cmdOpts); break;
1356
+ case 'farm': cmdResult = await commands.runFarm(cmdOpts); break;
1356
1357
  case 'pm': cmdResult = await commands.runPostMemes(cmdOpts); break;
1357
1358
  case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
1358
1359
  case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.16.0",
3
+ "version": "5.19.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"