dankgrinder 5.14.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.
- package/lib/commands/farm.js +1540 -0
- package/lib/commands/farmVision.js +437 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/shop.js +221 -73
- package/lib/commands/stream.js +102 -15
- package/lib/commands/utils.js +98 -9
- package/lib/grinder.js +1 -0
- package/package.json +1 -1
package/lib/commands/shop.js
CHANGED
|
@@ -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');
|
|
@@ -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
|
|
@@ -80,50 +101,56 @@ function isBuyFailText(text) {
|
|
|
80
101
|
|| lower.includes('missing items');
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const key = itemName.toLowerCase();
|
|
96
|
-
const searchTerm = ITEM_SEARCH_TERM[key] || key.split(' ').pop();
|
|
97
|
-
const targetTab = ITEM_SHOP_TAB[key] || 'Coin Shop';
|
|
104
|
+
function findBuyButtonInMessage(msg, searchTerm, key) {
|
|
105
|
+
const allBtns = getAllButtons(msg);
|
|
106
|
+
const normalizedSearch = String(searchTerm || '').toLowerCase().replace(/\s+/g, '');
|
|
107
|
+
const normalizedKey = String(key || '').toLowerCase().replace(/\s+/g, '');
|
|
108
|
+
return allBtns.find(b => {
|
|
109
|
+
const label = String(b.label || '').toLowerCase();
|
|
110
|
+
const id = String(b.customId || b.custom_id || '').toLowerCase().replace(/\s+/g, '');
|
|
111
|
+
return label.includes(searchTerm)
|
|
112
|
+
|| (normalizedSearch && id.includes(normalizedSearch))
|
|
113
|
+
|| (normalizedKey && id.includes(normalizedKey));
|
|
114
|
+
}) || null;
|
|
115
|
+
}
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
104
|
-
|
|
117
|
+
function findNextShopPageButton(msg) {
|
|
118
|
+
const btns = getAllButtons(msg).filter(b => !b.disabled);
|
|
119
|
+
return btns.find(b => {
|
|
120
|
+
const id = String(b.customId || b.custom_id || '').toLowerCase();
|
|
121
|
+
if (!id.includes('shop-view')) return false;
|
|
122
|
+
return id.endsWith(':1') || id.includes(':1:');
|
|
123
|
+
}) || null;
|
|
124
|
+
}
|
|
105
125
|
|
|
106
|
-
|
|
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
|
+
}
|
|
107
136
|
|
|
108
|
-
|
|
137
|
+
async function openShopView({ channel, waitForDankMemer }) {
|
|
109
138
|
await channel.send('pls shop view');
|
|
110
139
|
let response = await waitForDankMemer(10000);
|
|
111
140
|
|
|
112
|
-
if (!response) {
|
|
113
|
-
LOG.warn('[shop] No response to pls shop view');
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
141
|
+
if (!response) return { ok: false, reason: 'no-response', response: null };
|
|
116
142
|
if (isHoldTight(response)) {
|
|
117
143
|
LOG.warn('[shop] Hold tight — waiting 30s');
|
|
118
144
|
await sleep(30000);
|
|
119
|
-
return false;
|
|
145
|
+
return { ok: false, reason: 'hold-tight', response: null };
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
if (isCV2(response)) await ensureCV2(response);
|
|
123
|
-
|
|
124
149
|
logMsg(response, 'shop');
|
|
150
|
+
return { ok: true, reason: 'ok', response };
|
|
151
|
+
}
|
|
125
152
|
|
|
126
|
-
|
|
153
|
+
async function ensureShopTab({ response, waitForDankMemer, targetTab }) {
|
|
127
154
|
const selectInfo = findSelectMenuOption(response, targetTab);
|
|
128
155
|
if (selectInfo) {
|
|
129
156
|
const isAlreadySelected = (selectInfo.component.options || [])
|
|
@@ -137,7 +164,7 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
137
164
|
const row = response.components[i];
|
|
138
165
|
for (const comp of row?.components || []) {
|
|
139
166
|
if ((comp.type === 'STRING_SELECT' || comp.type === 3) &&
|
|
140
|
-
comp.customId === selectInfo.menuCustomId) {
|
|
167
|
+
(comp.customId || comp.custom_id) === selectInfo.menuCustomId) {
|
|
141
168
|
menuRowIdx = i;
|
|
142
169
|
break;
|
|
143
170
|
}
|
|
@@ -148,46 +175,62 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
148
175
|
menuRowIdx >= 0 ? menuRowIdx : selectInfo.menuCustomId,
|
|
149
176
|
[selectInfo.option.value]
|
|
150
177
|
);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
LOG.success(`Switched to ${targetTab}`);
|
|
155
|
-
}
|
|
178
|
+
response = result || (await waitForDankMemer(6000)) || response;
|
|
179
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
180
|
+
LOG.success(`Switched to ${targetTab}`);
|
|
156
181
|
await sleep(300);
|
|
157
182
|
} catch (e) {
|
|
158
183
|
LOG.error(`[shop] Tab switch failed: ${e.message}`);
|
|
159
|
-
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
const menus = getAllSelectMenus(response);
|
|
188
|
+
const menu = menus.find(m => (m.options || []).length > 0);
|
|
189
|
+
if (menu) {
|
|
190
|
+
const desired = (menu.options || []).find(o => (o.label || '').toLowerCase().includes(targetTab.toLowerCase()));
|
|
191
|
+
if (desired) {
|
|
192
|
+
try {
|
|
193
|
+
await response.selectMenu(menu.customId || menu.custom_id, [desired.value]);
|
|
194
|
+
const maybeUpdated = await waitForDankMemer(6000);
|
|
195
|
+
if (maybeUpdated) {
|
|
196
|
+
response = maybeUpdated;
|
|
197
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
198
|
+
LOG.success(`Switched to ${targetTab}`);
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
160
201
|
}
|
|
161
202
|
}
|
|
162
203
|
}
|
|
204
|
+
return response;
|
|
205
|
+
}
|
|
163
206
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
});
|
|
207
|
+
async function findBuyButtonPaged({ response, waitForDankMemer, searchTerm, key }) {
|
|
208
|
+
let buyBtn = null;
|
|
209
|
+
for (let page = 0; page < 10; page++) {
|
|
210
|
+
buyBtn = findBuyButtonInMessage(response, searchTerm, key);
|
|
211
|
+
if (buyBtn) break;
|
|
175
212
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
const nextPageBtn = findNextShopPageButton(response);
|
|
214
|
+
if (!nextPageBtn) break;
|
|
215
|
+
try {
|
|
216
|
+
const moved = await safeClickButton(response, nextPageBtn);
|
|
217
|
+
response = moved || (await waitForDankMemer(5000)) || response;
|
|
218
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
219
|
+
await sleep(200);
|
|
220
|
+
} catch {
|
|
221
|
+
break;
|
|
181
222
|
}
|
|
182
|
-
return false;
|
|
183
223
|
}
|
|
224
|
+
return { response, buyBtn };
|
|
225
|
+
}
|
|
184
226
|
|
|
227
|
+
async function performBuyFromButton({ response, buyBtn, waitForDankMemer, itemName, quantity, key }) {
|
|
228
|
+
if (!buyBtn) return { ok: false, reason: 'not-found', response };
|
|
185
229
|
if (buyBtn.disabled) {
|
|
186
230
|
LOG.warn(`[shop] "${buyBtn.label}" is DISABLED — not enough coins for ${itemName}`);
|
|
187
|
-
return false;
|
|
231
|
+
return { ok: false, reason: 'disabled', response };
|
|
188
232
|
}
|
|
189
233
|
|
|
190
|
-
// Step 4: Click the Buy button → Modal with quantity input
|
|
191
234
|
LOG.buy(`Clicking "${buyBtn.label}"...`);
|
|
192
235
|
const btnId = buyBtn.customId || buyBtn.custom_id;
|
|
193
236
|
let clickResult;
|
|
@@ -195,11 +238,9 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
195
238
|
clickResult = await response.clickButton(btnId);
|
|
196
239
|
} catch (e) {
|
|
197
240
|
LOG.error(`[shop] Button click failed: ${e.message}`);
|
|
198
|
-
return false;
|
|
241
|
+
return { ok: false, reason: 'click-failed', response };
|
|
199
242
|
}
|
|
200
243
|
|
|
201
|
-
// Some shop buttons buy immediately (no quantity modal).
|
|
202
|
-
// If we received a message-like response, parse it directly.
|
|
203
244
|
if (clickResult && !clickResult.components?.[0]?.components?.[0]?.setValue) {
|
|
204
245
|
if (isCV2(clickResult)) await ensureCV2(clickResult);
|
|
205
246
|
const directText = getFullText(clickResult);
|
|
@@ -207,14 +248,13 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
207
248
|
if (isBuySuccessText(directText)) {
|
|
208
249
|
LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
|
|
209
250
|
recentPurchases.set(key, Date.now());
|
|
210
|
-
return true;
|
|
251
|
+
return { ok: true, reason: 'direct-success', response: clickResult };
|
|
211
252
|
}
|
|
212
253
|
if (isBuyFailText(directText)) {
|
|
213
254
|
LOG.warn(`[shop] Direct buy failed for ${itemName}: ${directText.substring(0, 120)}`);
|
|
214
|
-
return false;
|
|
255
|
+
return { ok: false, reason: 'direct-failed', response: clickResult };
|
|
215
256
|
}
|
|
216
257
|
|
|
217
|
-
// Fallback: wait one follow-up message for purchase outcome text.
|
|
218
258
|
const follow = await waitForDankMemer(6000);
|
|
219
259
|
if (follow) {
|
|
220
260
|
if (isCV2(follow)) await ensureCV2(follow);
|
|
@@ -223,27 +263,26 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
223
263
|
if (isBuySuccessText(fText)) {
|
|
224
264
|
LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
|
|
225
265
|
recentPurchases.set(key, Date.now());
|
|
226
|
-
return true;
|
|
266
|
+
return { ok: true, reason: 'followup-success', response: follow };
|
|
227
267
|
}
|
|
228
268
|
if (isBuyFailText(fText)) {
|
|
229
269
|
LOG.warn(`[shop] Buy failed for ${itemName}: ${fText.substring(0, 120)}`);
|
|
230
|
-
return false;
|
|
270
|
+
return { ok: false, reason: 'followup-failed', response: follow };
|
|
231
271
|
}
|
|
272
|
+
return { ok: true, reason: 'followup-unknown', response: follow };
|
|
232
273
|
}
|
|
233
274
|
|
|
234
275
|
LOG.success(`Buy click sent for ${itemName} (no modal path)`);
|
|
235
276
|
recentPurchases.set(key, Date.now());
|
|
236
|
-
return true;
|
|
277
|
+
return { ok: true, reason: 'direct-unknown', response: clickResult };
|
|
237
278
|
}
|
|
238
279
|
|
|
239
280
|
const modal = clickResult;
|
|
240
|
-
|
|
241
281
|
if (!modal || !modal.components) {
|
|
242
282
|
LOG.warn('[shop] No modal returned from button click');
|
|
243
|
-
return false;
|
|
283
|
+
return { ok: false, reason: 'no-modal', response };
|
|
244
284
|
}
|
|
245
285
|
|
|
246
|
-
// Step 5: Set quantity and submit the modal
|
|
247
286
|
LOG.buy(`Modal "${modal.title}" — qty=${quantity}`);
|
|
248
287
|
try {
|
|
249
288
|
modal.components[0].components[0].setValue(String(quantity));
|
|
@@ -256,20 +295,129 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
256
295
|
if (isBuySuccessText(text)) {
|
|
257
296
|
LOG.success(`${c.green}${c.bold}Purchased ${quantity}x ${itemName}!${c.reset}`);
|
|
258
297
|
recentPurchases.set(key, Date.now());
|
|
259
|
-
return true;
|
|
298
|
+
return { ok: true, reason: 'modal-success', response: result };
|
|
260
299
|
}
|
|
261
300
|
if (isBuyFailText(text)) {
|
|
262
301
|
LOG.warn(`Not enough coins to buy ${itemName}`);
|
|
263
|
-
return false;
|
|
302
|
+
return { ok: false, reason: 'modal-failed', response: result };
|
|
264
303
|
}
|
|
304
|
+
return { ok: true, reason: 'modal-unknown', response: result };
|
|
265
305
|
}
|
|
266
306
|
|
|
267
307
|
LOG.success(`Modal submitted for ${quantity}x ${itemName} (assuming success)`);
|
|
268
|
-
return true;
|
|
308
|
+
return { ok: true, reason: 'modal-submitted', response };
|
|
269
309
|
} catch (e) {
|
|
270
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');
|
|
271
342
|
return false;
|
|
272
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 };
|
|
273
421
|
}
|
|
274
422
|
|
|
275
|
-
module.exports = { buyItem, ITEM_COSTS };
|
|
423
|
+
module.exports = { buyItem, buyItemsBatch, ITEM_COSTS };
|
package/lib/commands/stream.js
CHANGED
|
@@ -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,27 @@ 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
|
+
|
|
33
55
|
function messageStateSignature(msg) {
|
|
34
56
|
const text = normalizeLower(stripAnsi(getFullText(msg))).slice(0, 220);
|
|
35
57
|
const btnSig = getAllButtons(msg)
|
|
@@ -40,10 +62,16 @@ function messageStateSignature(msg) {
|
|
|
40
62
|
return `${text}||${btnSig}`;
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, timeoutMs = 12000 }) {
|
|
65
|
+
async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, client, timeoutMs = 12000 }) {
|
|
44
66
|
const start = Date.now();
|
|
45
67
|
const prevSig = messageStateSignature(prevMsg);
|
|
46
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
|
+
|
|
47
75
|
const evt = await waitForDankMemer(1500);
|
|
48
76
|
if (evt) {
|
|
49
77
|
await hydrate(evt);
|
|
@@ -75,11 +103,14 @@ function isLiveDashboard(msg, lowerText) {
|
|
|
75
103
|
return labels.some(l => l.includes('end stream')) && labels.some(l => STREAM_ACTION_LABELS.some(a => l.includes(a)));
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
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'));
|
|
79
109
|
return lowerText.includes('stream manager')
|
|
80
110
|
|| lowerText.includes('what game do you want to stream')
|
|
81
111
|
|| lowerText.includes('view setup')
|
|
82
|
-
|| lowerText.includes('go live')
|
|
112
|
+
|| lowerText.includes('go live')
|
|
113
|
+
|| hasManagerButtons;
|
|
83
114
|
}
|
|
84
115
|
|
|
85
116
|
function isActionResultText(lowerText) {
|
|
@@ -90,6 +121,16 @@ function isActionResultText(lowerText) {
|
|
|
90
121
|
|| lowerText.includes('received');
|
|
91
122
|
}
|
|
92
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
|
+
|
|
93
134
|
async function selectRandomStreamOption(msg) {
|
|
94
135
|
const menus = getAllSelectMenus(msg);
|
|
95
136
|
if (menus.length === 0) return false;
|
|
@@ -142,6 +183,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
142
183
|
logMsg(response, 'stream');
|
|
143
184
|
let text = getFullText(response);
|
|
144
185
|
let lower = normalizeLower(stripAnsi(text));
|
|
186
|
+
let nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
145
187
|
|
|
146
188
|
// Missing items — buy keyboard + mouse
|
|
147
189
|
const missing = needsItem(lower);
|
|
@@ -169,15 +211,18 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
169
211
|
logMsg(response, 'stream-retry');
|
|
170
212
|
text = getFullText(response);
|
|
171
213
|
lower = normalizeLower(stripAnsi(text));
|
|
214
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
172
215
|
}
|
|
173
216
|
|
|
174
217
|
// Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
|
|
218
|
+
let lastGoLiveCustomId = null;
|
|
175
219
|
for (let step = 0; step < 6; step++) {
|
|
176
220
|
text = getFullText(response);
|
|
177
221
|
lower = normalizeLower(stripAnsi(text));
|
|
222
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
178
223
|
|
|
179
|
-
|
|
180
|
-
|
|
224
|
+
if (isLiveDashboard(response, lower)) break;
|
|
225
|
+
if (!isStreamManagerScreen(response, lower)) break;
|
|
181
226
|
|
|
182
227
|
const selected = await selectRandomStreamOption(response);
|
|
183
228
|
if (selected) {
|
|
@@ -191,7 +236,34 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
191
236
|
}
|
|
192
237
|
|
|
193
238
|
const goLiveBtn = getGoLiveButton(response);
|
|
239
|
+
if (goLiveBtn?.customId || goLiveBtn?.custom_id) {
|
|
240
|
+
lastGoLiveCustomId = goLiveBtn.customId || goLiveBtn.custom_id;
|
|
241
|
+
}
|
|
194
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
|
+
|
|
195
267
|
await sleep(300);
|
|
196
268
|
const fresh = await refetchMsg(channel, response.id);
|
|
197
269
|
if (fresh) {
|
|
@@ -207,12 +279,22 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
207
279
|
try {
|
|
208
280
|
const before = response;
|
|
209
281
|
const liveResult = await safeClickButton(response, goLiveBtn);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
216
298
|
if (next) {
|
|
217
299
|
response = next;
|
|
218
300
|
await hydrate(response);
|
|
@@ -234,6 +316,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
234
316
|
const afterFallback = fallbackRes || (await waitForStreamTransition({
|
|
235
317
|
channel,
|
|
236
318
|
waitForDankMemer,
|
|
319
|
+
client,
|
|
237
320
|
prevMsg: before,
|
|
238
321
|
timeoutMs: 8000,
|
|
239
322
|
}));
|
|
@@ -255,11 +338,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
255
338
|
// Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
|
|
256
339
|
text = getFullText(response);
|
|
257
340
|
lower = normalizeLower(stripAnsi(text));
|
|
341
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
258
342
|
|
|
259
343
|
if (isLiveDashboard(response, lower)) {
|
|
260
344
|
const actions = getLiveActionButtons(response);
|
|
261
345
|
if (actions.length > 0) {
|
|
262
346
|
const action = pickRandom(actions);
|
|
347
|
+
const actionAt = new Date();
|
|
263
348
|
LOG.info(`[stream] Live action: "${action.label}"`);
|
|
264
349
|
await humanDelay(120, 320);
|
|
265
350
|
try {
|
|
@@ -292,10 +377,12 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
292
377
|
}
|
|
293
378
|
text = bestText;
|
|
294
379
|
lower = normalizeLower(stripAnsi(text));
|
|
380
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
295
381
|
}
|
|
296
382
|
} catch (e) {
|
|
297
383
|
LOG.error(`[stream] Action click failed: ${e.message}`);
|
|
298
384
|
}
|
|
385
|
+
LOG.info(`[stream] Action timestamp: ${actionAt.toISOString()} | next interaction in ${Math.ceil(nextCooldownSec / 60)}m`);
|
|
299
386
|
} else {
|
|
300
387
|
LOG.info('[stream] Live dashboard found but no action buttons available yet');
|
|
301
388
|
}
|
|
@@ -305,13 +392,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
305
392
|
const coins = parseCoins(text);
|
|
306
393
|
if (coins > 0) {
|
|
307
394
|
LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
308
|
-
return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec
|
|
395
|
+
return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec };
|
|
309
396
|
}
|
|
310
397
|
|
|
311
|
-
if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec
|
|
398
|
+
if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec };
|
|
312
399
|
|
|
313
400
|
const short = normalizeLower(stripAnsi(text)).substring(0, 60);
|
|
314
|
-
return { result: short || 'streamed', coins: 0, nextCooldownSec
|
|
401
|
+
return { result: short || 'streamed', coins: 0, nextCooldownSec };
|
|
315
402
|
}
|
|
316
403
|
|
|
317
404
|
module.exports = { runStream };
|