dankgrinder 5.10.0 → 5.12.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.
@@ -1,10 +1,89 @@
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
+ async function selectRandomStreamOption(msg) {
57
+ const menus = getAllSelectMenus(msg);
58
+ if (menus.length === 0) return false;
59
+
60
+ const menu = menus[0];
61
+ const options = (menu.options || []).filter(o => !o.default);
62
+ if (options.length === 0) return false;
63
+
64
+ const pick = pickRandom(options);
65
+ LOG.info(`[stream] Selecting option: "${pick.label}"`);
66
+
67
+ let menuRowIdx = -1;
68
+ for (let i = 0; i < (msg.components || []).length; i++) {
69
+ for (const comp of (msg.components[i]?.components || [])) {
70
+ if ((comp.type === 'STRING_SELECT' || comp.type === 3) &&
71
+ ((comp.customId || comp.custom_id) === (menu.customId || menu.custom_id))) {
72
+ menuRowIdx = i;
73
+ break;
74
+ }
75
+ }
76
+ if (menuRowIdx >= 0) break;
77
+ }
78
+
79
+ try {
80
+ await msg.selectMenu(menuRowIdx >= 0 ? menuRowIdx : (menu.customId || menu.custom_id), [pick.value]);
81
+ return true;
82
+ } catch (e) {
83
+ LOG.error(`[stream] Select option failed: ${e.message}`);
84
+ return false;
85
+ }
86
+ }
8
87
 
9
88
  async function runStream({ channel, waitForDankMemer, client }) {
10
89
  LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
@@ -22,16 +101,21 @@ async function runStream({ channel, waitForDankMemer, client }) {
22
101
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
23
102
  }
24
103
 
104
+ await hydrate(response);
25
105
  logMsg(response, 'stream');
26
106
  let text = getFullText(response);
27
- let lower = text.toLowerCase();
107
+ let lower = normalizeLower(stripAnsi(text));
28
108
 
29
109
  // Missing items — buy keyboard + mouse
30
- if (lower.includes('missing items') || lower.includes('need following items') ||
110
+ const missing = needsItem(lower);
111
+ if (missing === 'keyboard' || missing === 'mouse' ||
112
+ lower.includes('missing items') || lower.includes('need following items') ||
113
+ lower.includes('need the following items') ||
31
114
  lower.includes('need a keyboard') || lower.includes('need a mouse')) {
32
115
  const itemsToBuy = [];
33
116
  if (lower.includes('keyboard')) itemsToBuy.push('keyboard');
34
117
  if (lower.includes('mouse')) itemsToBuy.push('mouse');
118
+ if ((missing === 'keyboard' || missing === 'mouse') && !itemsToBuy.includes(missing)) itemsToBuy.push(missing);
35
119
  if (itemsToBuy.length === 0) itemsToBuy.push(...STREAM_ITEMS);
36
120
 
37
121
  for (const item of itemsToBuy) {
@@ -44,108 +128,100 @@ async function runStream({ channel, waitForDankMemer, client }) {
44
128
  await channel.send('pls stream');
45
129
  response = await waitForDankMemer(12000);
46
130
  if (!response) return { result: 'no response after buy', coins: 0 };
131
+ await hydrate(response);
47
132
  logMsg(response, 'stream-retry');
48
133
  text = getFullText(response);
49
- lower = text.toLowerCase();
134
+ lower = normalizeLower(stripAnsi(text));
50
135
  }
51
136
 
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
- }
137
+ // Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
138
+ for (let step = 0; step < 6; step++) {
139
+ text = getFullText(response);
140
+ lower = normalizeLower(stripAnsi(text));
141
+
142
+ if (isLiveDashboard(response, lower)) break;
143
+ if (!isStreamManagerScreen(lower)) break;
144
+
145
+ const selected = await selectRandomStreamOption(response);
146
+ if (selected) {
147
+ await humanDelay(150, 350);
148
+ const updatedAfterSelect = (await waitForDankMemer(5000)) || (await refetchMsg(channel, response.id));
149
+ if (updatedAfterSelect) {
150
+ response = updatedAfterSelect;
151
+ await hydrate(response);
152
+ logMsg(response, `stream-selected-${step}`);
153
+ }
154
+ }
70
155
 
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
- }
156
+ const goLiveBtn = getGoLiveButton(response);
157
+ if (!goLiveBtn) {
158
+ await sleep(300);
159
+ const fresh = await refetchMsg(channel, response.id);
160
+ if (fresh) {
161
+ response = fresh;
162
+ await hydrate(response);
163
+ continue;
164
+ }
165
+ break;
166
+ }
167
+
168
+ LOG.info('[stream] Clicking "Go Live"');
169
+ await humanDelay(100, 250);
170
+ try {
171
+ const liveResult = await safeClickButton(response, goLiveBtn);
172
+ const next = liveResult || (await waitForDankMemer(7000)) || (await refetchMsg(channel, response.id));
173
+ if (next) {
174
+ response = next;
175
+ await hydrate(response);
176
+ logMsg(response, `stream-go-live-${step}`);
177
+ continue;
83
178
  }
179
+ } catch (e) {
180
+ LOG.error(`[stream] Go Live click failed: ${e.message}`);
84
181
  }
182
+ break;
183
+ }
85
184
 
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);
185
+ // Live phase: click exactly one random interaction button every run.
186
+ // Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
187
+ text = getFullText(response);
188
+ lower = normalizeLower(stripAnsi(text));
189
+
190
+ if (isLiveDashboard(response, lower)) {
191
+ const actions = getLiveActionButtons(response);
192
+ if (actions.length > 0) {
193
+ const action = pickRandom(actions);
194
+ LOG.info(`[stream] Live action: "${action.label}"`);
195
+ await humanDelay(120, 320);
92
196
  try {
93
- const liveResult = await safeClickButton(response, goLiveBtn);
94
- if (liveResult) {
95
- response = liveResult;
96
- logMsg(response, 'stream-live');
197
+ const clicked = await safeClickButton(response, action);
198
+ const updated = clicked || (await waitForDankMemer(6000)) || (await refetchMsg(channel, response.id));
199
+ if (updated) {
200
+ response = updated;
201
+ await hydrate(response);
202
+ logMsg(response, 'stream-action');
97
203
  text = getFullText(response);
98
- lower = text.toLowerCase();
204
+ lower = normalizeLower(stripAnsi(text));
99
205
  }
100
206
  } catch (e) {
101
- LOG.error(`[stream] Go Live click failed: ${e.message}`);
207
+ LOG.error(`[stream] Action click failed: ${e.message}`);
102
208
  }
103
209
  } 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 {}
107
- }
210
+ LOG.info('[stream] Live dashboard found but no action buttons available yet');
108
211
  }
109
212
  }
110
213
 
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 };
126
- }
127
- return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
128
- }
129
-
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 {} }
140
- } else {
141
- break;
142
- }
214
+ const ended = lower.includes('stream ended') || lower.includes('stream is over') || lower.includes('you earned');
215
+ const coins = parseCoins(text);
216
+ if (coins > 0) {
217
+ LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
218
+ return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
143
219
  }
144
220
 
145
- const coins = parseCoins(text);
146
- if (coins > 0) return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: 600 };
221
+ if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec: 600 };
147
222
 
148
- return { result: text.substring(0, 50) || 'streamed', coins: 0, nextCooldownSec: 600 };
223
+ const short = normalizeLower(stripAnsi(text)).substring(0, 60);
224
+ return { result: short || 'streamed', coins: 0, nextCooldownSec: 600 };
149
225
  }
150
226
 
151
227
  module.exports = { runStream };
@@ -18,6 +18,7 @@
18
18
  const {
19
19
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
20
20
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
21
+ isCV2, ensureCV2, stripAnsi,
21
22
  } = require('./utils');
22
23
 
23
24
  const RE_MEMORY_BACKTICK_CHUNK = /`([^`]+)`/g;
@@ -26,6 +27,24 @@ const RE_WORK_COOLDOWN_TS = /<t:(\d+):R>/;
26
27
  const RE_WORK_COOLDOWN_MINUTES = /(\d+)\s*minute/i;
27
28
  const RE_WORK_COOLDOWN_HOURS = /(\d+)\s*hour/i;
28
29
 
30
+ function normalizeLower(text) {
31
+ return String(text || '')
32
+ .normalize('NFKC')
33
+ .replace(/[’‘`´]/g, "'")
34
+ .replace(/\bdon'?t\b/gi, "don't")
35
+ .toLowerCase();
36
+ }
37
+
38
+ function isNoJobText(text) {
39
+ const t = normalizeLower(stripAnsi(text)).replace(/\s+/g, ' ').trim();
40
+ return t.includes("don't currently have a job")
41
+ || t.includes("don't have a job")
42
+ || t.includes('no job to work at')
43
+ || t.includes('you need a job')
44
+ || t.includes('/work list')
45
+ || (t.includes('work list') && t.includes('available jobs'));
46
+ }
47
+
29
48
  // Job progression list (order matters — first is easiest to get)
30
49
  const JOBS = Object.freeze([
31
50
  'babysitter', 'dog walker', 'fast food worker', 'youtuber',
@@ -114,14 +133,15 @@ async function resignFromJob({ channel, waitForDankMemer }) {
114
133
  async function autoApplyForJob({ channel, waitForDankMemer }) {
115
134
  LOG.info('[work] No job! Applying for Babysitter...');
116
135
  await channel.send('pls work apply babysitter');
117
- const response = await waitForDankMemer(8000);
136
+ const response = await waitForDankMemer(10000);
118
137
  if (!response) {
119
138
  LOG.warn('[work] No response to apply');
120
139
  return { applied: false };
121
140
  }
141
+ if (isCV2(response)) await ensureCV2(response);
122
142
  logMsg(response, 'work-apply');
123
143
  const text = getFullText(response);
124
- const tl = text.toLowerCase();
144
+ const tl = normalizeLower(stripAnsi(text)).replace(/\s+/g, ' ').trim();
125
145
  if (tl.includes('congratulations') || tl.includes('now working')) {
126
146
  LOG.success('[work] Applied for Babysitter!');
127
147
  return { applied: true };
@@ -301,6 +321,8 @@ async function runWorkShift({ channel, waitForDankMemer }) {
301
321
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
302
322
  }
303
323
 
324
+ if (isCV2(current)) await ensureCV2(current);
325
+
304
326
  logMsg(current, 'work');
305
327
  let text = getFullText(current);
306
328
 
@@ -314,11 +336,10 @@ async function runWorkShift({ channel, waitForDankMemer }) {
314
336
  return { result: `work cooldown (${Math.ceil(cooldownSec / 60)}m)`, coins: 0, nextCooldownSec: cooldownSec };
315
337
  }
316
338
 
317
- const textLower = text.toLowerCase();
339
+ const textLower = normalizeLower(stripAnsi(text)).replace(/\s+/g, ' ').trim();
318
340
 
319
341
  // ── No job? Auto-apply ─────────────────────────────────────
320
- if (textLower.includes("don't currently have a job") || textLower.includes('don\'t have a job') ||
321
- textLower.includes('you need a job') || textLower.includes('work list')) {
342
+ if (isNoJobText(textLower)) {
322
343
  const applyResult = await autoApplyForJob({ channel, waitForDankMemer });
323
344
  if (!applyResult.applied) {
324
345
  return { result: 'no job (apply failed)', coins: 0, nextCooldownSec: applyResult.cooldownSec || 600 };
@@ -331,6 +352,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
331
352
  await channel.send('pls work shift');
332
353
  current = await waitForDankMemer(10000);
333
354
  if (!current) return { result: 'no response after apply', coins: 0 };
355
+ if (isCV2(current)) await ensureCV2(current);
334
356
  logMsg(current, 'work-after-apply');
335
357
  text = getFullText(current);
336
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.10.0",
3
+ "version": "5.12.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"