dankgrinder 5.11.0 → 5.13.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.
@@ -16,7 +16,7 @@
16
16
  const {
17
17
  LOG, c, sleep, humanDelay, getFullText,
18
18
  getAllButtons, findSelectMenuOption,
19
- logMsg, isHoldTight, isCV2, ensureCV2,
19
+ logMsg, isHoldTight, isCV2, ensureCV2, stripAnsi,
20
20
  } = require('./utils');
21
21
  const { LRUCache, Trie } = require('../structures');
22
22
 
@@ -61,6 +61,25 @@ for (const [item, searchTerm] of Object.entries(ITEM_SEARCH_TERM)) {
61
61
  // commands both detect "need shovel" within seconds of each other)
62
62
  const recentPurchases = new LRUCache(16);
63
63
 
64
+ function isBuySuccessText(text) {
65
+ const lower = String(stripAnsi(text || '')).toLowerCase();
66
+ return lower.includes('success')
67
+ || lower.includes('successful purchase')
68
+ || lower.includes('bought')
69
+ || lower.includes('purchased')
70
+ || (lower.includes('you bought') && lower.includes('for'));
71
+ }
72
+
73
+ function isBuyFailText(text) {
74
+ const lower = String(stripAnsi(text || '')).toLowerCase();
75
+ return lower.includes('not enough')
76
+ || lower.includes("can't afford")
77
+ || lower.includes('insufficient')
78
+ || lower.includes('you need')
79
+ || lower.includes('missing item')
80
+ || lower.includes('missing items');
81
+ }
82
+
64
83
  /**
65
84
  * Buy an item from the Dank Memer shop.
66
85
  *
@@ -171,14 +190,54 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
171
190
  // Step 4: Click the Buy button → Modal with quantity input
172
191
  LOG.buy(`Clicking "${buyBtn.label}"...`);
173
192
  const btnId = buyBtn.customId || buyBtn.custom_id;
174
- let modal;
193
+ let clickResult;
175
194
  try {
176
- modal = await response.clickButton(btnId);
195
+ clickResult = await response.clickButton(btnId);
177
196
  } catch (e) {
178
197
  LOG.error(`[shop] Button click failed: ${e.message}`);
179
198
  return false;
180
199
  }
181
200
 
201
+ // Some shop buttons buy immediately (no quantity modal).
202
+ // If we received a message-like response, parse it directly.
203
+ if (clickResult && !clickResult.components?.[0]?.components?.[0]?.setValue) {
204
+ if (isCV2(clickResult)) await ensureCV2(clickResult);
205
+ const directText = getFullText(clickResult);
206
+ logMsg(clickResult, 'buy-direct-result');
207
+ if (isBuySuccessText(directText)) {
208
+ LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
209
+ recentPurchases.set(key, Date.now());
210
+ return true;
211
+ }
212
+ if (isBuyFailText(directText)) {
213
+ LOG.warn(`[shop] Direct buy failed for ${itemName}: ${directText.substring(0, 120)}`);
214
+ return false;
215
+ }
216
+
217
+ // Fallback: wait one follow-up message for purchase outcome text.
218
+ const follow = await waitForDankMemer(6000);
219
+ if (follow) {
220
+ if (isCV2(follow)) await ensureCV2(follow);
221
+ const fText = getFullText(follow);
222
+ logMsg(follow, 'buy-direct-followup');
223
+ if (isBuySuccessText(fText)) {
224
+ LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
225
+ recentPurchases.set(key, Date.now());
226
+ return true;
227
+ }
228
+ if (isBuyFailText(fText)) {
229
+ LOG.warn(`[shop] Buy failed for ${itemName}: ${fText.substring(0, 120)}`);
230
+ return false;
231
+ }
232
+ }
233
+
234
+ LOG.success(`Buy click sent for ${itemName} (no modal path)`);
235
+ recentPurchases.set(key, Date.now());
236
+ return true;
237
+ }
238
+
239
+ const modal = clickResult;
240
+
182
241
  if (!modal || !modal.components) {
183
242
  LOG.warn('[shop] No modal returned from button click');
184
243
  return false;
@@ -194,12 +253,12 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
194
253
  const text = getFullText(result);
195
254
  logMsg(result, 'buy-result');
196
255
 
197
- if (text.toLowerCase().includes('success') || text.toLowerCase().includes('bought') || text.toLowerCase().includes('purchased')) {
256
+ if (isBuySuccessText(text)) {
198
257
  LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
199
258
  recentPurchases.set(key, Date.now());
200
259
  return true;
201
260
  }
202
- if (text.toLowerCase().includes('not enough') || text.toLowerCase().includes("can't afford") || text.toLowerCase().includes('insufficient')) {
261
+ if (isBuyFailText(text)) {
203
262
  LOG.warn(`Not enough coins to buy ${itemName}`);
204
263
  return false;
205
264
  }
@@ -1,10 +1,97 @@
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
5
  } = require('./utils');
5
6
  const { buyItem } = require('./shop');
6
7
 
7
8
  const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
9
+ const STREAM_ACTION_LABELS = Object.freeze(['run ad', 'read chat', 'collect donations']);
10
+
11
+ function normalizeLower(text) {
12
+ return String(text || '')
13
+ .normalize('NFKC')
14
+ .replace(/[’‘`´]/g, "'")
15
+ .replace(/\s+/g, ' ')
16
+ .trim()
17
+ .toLowerCase();
18
+ }
19
+
20
+ function pickRandom(arr) {
21
+ return arr[Math.floor(Math.random() * arr.length)];
22
+ }
23
+
24
+ async function hydrate(msg) {
25
+ if (isCV2(msg)) await ensureCV2(msg);
26
+ return msg;
27
+ }
28
+
29
+ async function refetchMsg(channel, msgId) {
30
+ try { return await channel.messages.fetch(msgId); } catch { return null; }
31
+ }
32
+
33
+ function getGoLiveButton(msg) {
34
+ const buttons = getAllButtons(msg);
35
+ return buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('go live')) || null;
36
+ }
37
+
38
+ function getLiveActionButtons(msg) {
39
+ const buttons = getAllButtons(msg).filter(b => !b.disabled && b.label);
40
+ return buttons.filter(b => STREAM_ACTION_LABELS.some(lbl => (b.label || '').toLowerCase().includes(lbl)));
41
+ }
42
+
43
+ function isLiveDashboard(msg, lowerText) {
44
+ if (lowerText.includes('your stream can last') || lowerText.includes('you can interact with your stream every')) return true;
45
+ const labels = getAllButtons(msg).map(b => (b.label || '').toLowerCase());
46
+ return labels.some(l => l.includes('end stream')) && labels.some(l => STREAM_ACTION_LABELS.some(a => l.includes(a)));
47
+ }
48
+
49
+ function isStreamManagerScreen(lowerText) {
50
+ return lowerText.includes('stream manager')
51
+ || lowerText.includes('what game do you want to stream')
52
+ || lowerText.includes('view setup')
53
+ || lowerText.includes('go live');
54
+ }
55
+
56
+ function isActionResultText(lowerText) {
57
+ return lowerText.includes('you ran an ad')
58
+ || lowerText.includes('read chat')
59
+ || lowerText.includes('collect')
60
+ || lowerText.includes('donation')
61
+ || lowerText.includes('received');
62
+ }
63
+
64
+ async function selectRandomStreamOption(msg) {
65
+ const menus = getAllSelectMenus(msg);
66
+ if (menus.length === 0) return false;
67
+
68
+ const menu = menus[0];
69
+ const options = (menu.options || []).filter(o => !o.default);
70
+ if (options.length === 0) return false;
71
+
72
+ const pick = pickRandom(options);
73
+ LOG.info(`[stream] Selecting option: "${pick.label}"`);
74
+
75
+ let menuRowIdx = -1;
76
+ for (let i = 0; i < (msg.components || []).length; i++) {
77
+ for (const comp of (msg.components[i]?.components || [])) {
78
+ if ((comp.type === 'STRING_SELECT' || comp.type === 3) &&
79
+ ((comp.customId || comp.custom_id) === (menu.customId || menu.custom_id))) {
80
+ menuRowIdx = i;
81
+ break;
82
+ }
83
+ }
84
+ if (menuRowIdx >= 0) break;
85
+ }
86
+
87
+ try {
88
+ await msg.selectMenu(menuRowIdx >= 0 ? menuRowIdx : (menu.customId || menu.custom_id), [pick.value]);
89
+ return true;
90
+ } catch (e) {
91
+ LOG.error(`[stream] Select option failed: ${e.message}`);
92
+ return false;
93
+ }
94
+ }
8
95
 
9
96
  async function runStream({ channel, waitForDankMemer, client }) {
10
97
  LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
@@ -22,16 +109,21 @@ async function runStream({ channel, waitForDankMemer, client }) {
22
109
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
23
110
  }
24
111
 
112
+ await hydrate(response);
25
113
  logMsg(response, 'stream');
26
114
  let text = getFullText(response);
27
- let lower = text.toLowerCase();
115
+ let lower = normalizeLower(stripAnsi(text));
28
116
 
29
117
  // Missing items — buy keyboard + mouse
30
- if (lower.includes('missing items') || lower.includes('need following items') ||
118
+ const missing = needsItem(lower);
119
+ if (missing === 'keyboard' || missing === 'mouse' ||
120
+ lower.includes('missing items') || lower.includes('need following items') ||
121
+ lower.includes('need the following items') ||
31
122
  lower.includes('need a keyboard') || lower.includes('need a mouse')) {
32
123
  const itemsToBuy = [];
33
124
  if (lower.includes('keyboard')) itemsToBuy.push('keyboard');
34
125
  if (lower.includes('mouse')) itemsToBuy.push('mouse');
126
+ if ((missing === 'keyboard' || missing === 'mouse') && !itemsToBuy.includes(missing)) itemsToBuy.push(missing);
35
127
  if (itemsToBuy.length === 0) itemsToBuy.push(...STREAM_ITEMS);
36
128
 
37
129
  for (const item of itemsToBuy) {
@@ -44,108 +136,121 @@ async function runStream({ channel, waitForDankMemer, client }) {
44
136
  await channel.send('pls stream');
45
137
  response = await waitForDankMemer(12000);
46
138
  if (!response) return { result: 'no response after buy', coins: 0 };
139
+ await hydrate(response);
47
140
  logMsg(response, 'stream-retry');
48
141
  text = getFullText(response);
49
- lower = text.toLowerCase();
142
+ lower = normalizeLower(stripAnsi(text));
50
143
  }
51
144
 
52
- // Stream Manager select a game from dropdown, then click "Go Live"
53
- if (lower.includes('stream manager') || lower.includes('what game do you want to stream')) {
54
- const menus = getAllSelectMenus(response);
55
- if (menus.length > 0) {
56
- const menu = menus[0];
57
- const options = menu.options || [];
58
- if (options.length > 0) {
59
- // Pick a random game (variety is better for Dank Memer)
60
- const pick = options[Math.floor(Math.random() * options.length)];
61
- LOG.info(`[stream] Selecting game: "${pick.label}"`);
62
-
63
- let menuRowIdx = -1;
64
- for (let i = 0; i < (response.components || []).length; i++) {
65
- for (const comp of (response.components[i].components || [])) {
66
- if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
67
- }
68
- if (menuRowIdx >= 0) break;
69
- }
145
+ // Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
146
+ for (let step = 0; step < 6; step++) {
147
+ text = getFullText(response);
148
+ lower = normalizeLower(stripAnsi(text));
70
149
 
71
- if (menuRowIdx >= 0) {
72
- try {
73
- const selectResult = await response.selectMenu(menuRowIdx, [pick.value]);
74
- if (selectResult) {
75
- response = selectResult;
76
- logMsg(response, 'stream-game-selected');
77
- }
78
- } catch (e) {
79
- LOG.error(`[stream] Select game failed: ${e.message}`);
80
- }
81
- await humanDelay(200, 500);
82
- }
150
+ if (isLiveDashboard(response, lower)) break;
151
+ if (!isStreamManagerScreen(lower)) break;
152
+
153
+ const selected = await selectRandomStreamOption(response);
154
+ if (selected) {
155
+ await humanDelay(150, 350);
156
+ const updatedAfterSelect = (await waitForDankMemer(5000)) || (await refetchMsg(channel, response.id));
157
+ if (updatedAfterSelect) {
158
+ response = updatedAfterSelect;
159
+ await hydrate(response);
160
+ logMsg(response, `stream-selected-${step}`);
83
161
  }
84
162
  }
85
163
 
86
- // Click "Go Live" button
87
- const buttons = getAllButtons(response);
88
- const goLiveBtn = buttons.find(b => !b.disabled && b.label && b.label.toLowerCase().includes('go live'));
89
- if (goLiveBtn) {
90
- LOG.info('[stream] Clicking "Go Live"');
91
- await humanDelay(100, 300);
92
- try {
93
- const liveResult = await safeClickButton(response, goLiveBtn);
94
- if (liveResult) {
95
- response = liveResult;
96
- logMsg(response, 'stream-live');
97
- text = getFullText(response);
98
- lower = text.toLowerCase();
99
- }
100
- } catch (e) {
101
- LOG.error(`[stream] Go Live click failed: ${e.message}`);
102
- }
103
- } else {
104
- const anyBtn = buttons.find(b => !b.disabled && b.label && !b.label.toLowerCase().includes('back'));
105
- if (anyBtn) {
106
- try { await safeClickButton(response, anyBtn); } catch {}
164
+ const goLiveBtn = getGoLiveButton(response);
165
+ if (!goLiveBtn) {
166
+ await sleep(300);
167
+ const fresh = await refetchMsg(channel, response.id);
168
+ if (fresh) {
169
+ response = fresh;
170
+ await hydrate(response);
171
+ continue;
107
172
  }
173
+ break;
108
174
  }
109
- }
110
175
 
111
- // After going live, there might be interactive stream events
112
- // Keep clicking non-end buttons for up to 5 rounds
113
- for (let round = 0; round < 5; round++) {
114
- const followUp = await waitForDankMemer(15000);
115
- if (!followUp) break;
116
-
117
- logMsg(followUp, `stream-round-${round}`);
118
- const fText = getFullText(followUp);
119
- const fLower = fText.toLowerCase();
120
- const fCoins = parseCoins(fText);
121
-
122
- if (fLower.includes('stream ended') || fLower.includes('stream is over') || fLower.includes('you earned')) {
123
- if (fCoins > 0) {
124
- LOG.coin(`[stream] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
125
- return { result: `stream → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins, nextCooldownSec: 600 };
176
+ LOG.info('[stream] Clicking "Go Live"');
177
+ await humanDelay(100, 250);
178
+ try {
179
+ const liveResult = await safeClickButton(response, goLiveBtn);
180
+ const next = liveResult || (await waitForDankMemer(7000)) || (await refetchMsg(channel, response.id));
181
+ if (next) {
182
+ response = next;
183
+ await hydrate(response);
184
+ logMsg(response, `stream-go-live-${step}`);
185
+ continue;
126
186
  }
127
- return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
187
+ } catch (e) {
188
+ LOG.error(`[stream] Go Live click failed: ${e.message}`);
128
189
  }
190
+ break;
191
+ }
129
192
 
130
- const btns = getAllButtons(followUp);
131
- const actionBtn = btns.find(b => !b.disabled && b.label &&
132
- !b.label.toLowerCase().includes('end') && !b.label.toLowerCase().includes('stop') && !b.label.toLowerCase().includes('back'));
133
- if (actionBtn) {
134
- LOG.info(`[stream] Clicking "${actionBtn.label}"`);
135
- await humanDelay(100, 300);
136
- try { await safeClickButton(followUp, actionBtn); } catch {}
137
- } else if (btns.length > 0) {
138
- const first = btns.find(b => !b.disabled);
139
- if (first) { try { await safeClickButton(followUp, first); } catch {} }
193
+ // Live phase: click exactly one random interaction button every run.
194
+ // Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
195
+ text = getFullText(response);
196
+ lower = normalizeLower(stripAnsi(text));
197
+
198
+ if (isLiveDashboard(response, lower)) {
199
+ const actions = getLiveActionButtons(response);
200
+ if (actions.length > 0) {
201
+ const action = pickRandom(actions);
202
+ LOG.info(`[stream] Live action: "${action.label}"`);
203
+ await humanDelay(120, 320);
204
+ try {
205
+ const clicked = await safeClickButton(response, action);
206
+ let updated = clicked || (await waitForDankMemer(6000)) || (await refetchMsg(channel, response.id));
207
+ if (updated) {
208
+ response = updated;
209
+ await hydrate(response);
210
+ logMsg(response, 'stream-action');
211
+ text = getFullText(response);
212
+ lower = normalizeLower(stripAnsi(text));
213
+
214
+ // Action payouts may appear as a second follow-up message.
215
+ // Keep checking briefly so earnings are not missed.
216
+ let bestText = text;
217
+ let bestCoins = parseCoins(bestText);
218
+ for (let i = 0; i < 2; i++) {
219
+ const nextMsg = await waitForDankMemer(5000);
220
+ if (!nextMsg) break;
221
+ await hydrate(nextMsg);
222
+ logMsg(nextMsg, `stream-action-followup-${i}`);
223
+ const nextText = getFullText(nextMsg);
224
+ const nextLower = normalizeLower(stripAnsi(nextText));
225
+ const nextCoins = parseCoins(nextText);
226
+ if (nextCoins > bestCoins || (bestCoins === 0 && isActionResultText(nextLower))) {
227
+ bestText = nextText;
228
+ bestCoins = nextCoins;
229
+ response = nextMsg;
230
+ }
231
+ }
232
+ text = bestText;
233
+ lower = normalizeLower(stripAnsi(text));
234
+ }
235
+ } catch (e) {
236
+ LOG.error(`[stream] Action click failed: ${e.message}`);
237
+ }
140
238
  } else {
141
- break;
239
+ LOG.info('[stream] Live dashboard found but no action buttons available yet');
142
240
  }
143
241
  }
144
242
 
243
+ const ended = lower.includes('stream ended') || lower.includes('stream is over') || lower.includes('you earned');
145
244
  const coins = parseCoins(text);
146
- if (coins > 0) return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
245
+ if (coins > 0) {
246
+ LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
247
+ return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
248
+ }
249
+
250
+ if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
147
251
 
148
- return { result: text.substring(0, 50) || 'streamed', coins: 0, nextCooldownSec: 600 };
252
+ const short = normalizeLower(stripAnsi(text)).substring(0, 60);
253
+ return { result: short || 'streamed', coins: 0, nextCooldownSec: 600 };
149
254
  }
150
255
 
151
256
  module.exports = { runStream };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.11.0",
3
+ "version": "5.13.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"