dankgrinder 5.12.0 → 5.14.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/shop.js +64 -5
- package/lib/commands/stream.js +92 -2
- package/package.json +1 -1
package/lib/commands/shop.js
CHANGED
|
@@ -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
|
|
193
|
+
let clickResult;
|
|
175
194
|
try {
|
|
176
|
-
|
|
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 (
|
|
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 (
|
|
261
|
+
if (isBuyFailText(text)) {
|
|
203
262
|
LOG.warn(`Not enough coins to buy ${itemName}`);
|
|
204
263
|
return false;
|
|
205
264
|
}
|
package/lib/commands/stream.js
CHANGED
|
@@ -30,6 +30,35 @@ async function refetchMsg(channel, msgId) {
|
|
|
30
30
|
try { return await channel.messages.fetch(msgId); } catch { return null; }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function messageStateSignature(msg) {
|
|
34
|
+
const text = normalizeLower(stripAnsi(getFullText(msg))).slice(0, 220);
|
|
35
|
+
const btnSig = getAllButtons(msg)
|
|
36
|
+
.map(b => `${(b.label || '').toLowerCase()}|${(b.customId || b.custom_id || '').toLowerCase()}|${b.disabled ? '1' : '0'}`)
|
|
37
|
+
.sort()
|
|
38
|
+
.join(';')
|
|
39
|
+
.slice(0, 300);
|
|
40
|
+
return `${text}||${btnSig}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, timeoutMs = 12000 }) {
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
const prevSig = messageStateSignature(prevMsg);
|
|
46
|
+
while (Date.now() - start < timeoutMs) {
|
|
47
|
+
const evt = await waitForDankMemer(1500);
|
|
48
|
+
if (evt) {
|
|
49
|
+
await hydrate(evt);
|
|
50
|
+
if (messageStateSignature(evt) !== prevSig) return evt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fresh = await refetchMsg(channel, prevMsg.id);
|
|
54
|
+
if (fresh) {
|
|
55
|
+
await hydrate(fresh);
|
|
56
|
+
if (messageStateSignature(fresh) !== prevSig) return fresh;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
function getGoLiveButton(msg) {
|
|
34
63
|
const buttons = getAllButtons(msg);
|
|
35
64
|
return buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('go live')) || null;
|
|
@@ -53,6 +82,14 @@ function isStreamManagerScreen(lowerText) {
|
|
|
53
82
|
|| lowerText.includes('go live');
|
|
54
83
|
}
|
|
55
84
|
|
|
85
|
+
function isActionResultText(lowerText) {
|
|
86
|
+
return lowerText.includes('you ran an ad')
|
|
87
|
+
|| lowerText.includes('read chat')
|
|
88
|
+
|| lowerText.includes('collect')
|
|
89
|
+
|| lowerText.includes('donation')
|
|
90
|
+
|| lowerText.includes('received');
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
async function selectRandomStreamOption(msg) {
|
|
57
94
|
const menus = getAllSelectMenus(msg);
|
|
58
95
|
if (menus.length === 0) return false;
|
|
@@ -168,14 +205,46 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
168
205
|
LOG.info('[stream] Clicking "Go Live"');
|
|
169
206
|
await humanDelay(100, 250);
|
|
170
207
|
try {
|
|
208
|
+
const before = response;
|
|
171
209
|
const liveResult = await safeClickButton(response, goLiveBtn);
|
|
172
|
-
const next = liveResult || (await
|
|
210
|
+
const next = liveResult || (await waitForStreamTransition({
|
|
211
|
+
channel,
|
|
212
|
+
waitForDankMemer,
|
|
213
|
+
prevMsg: before,
|
|
214
|
+
timeoutMs: 12000,
|
|
215
|
+
}));
|
|
173
216
|
if (next) {
|
|
174
217
|
response = next;
|
|
175
218
|
await hydrate(response);
|
|
176
219
|
logMsg(response, `stream-go-live-${step}`);
|
|
177
220
|
continue;
|
|
178
221
|
}
|
|
222
|
+
|
|
223
|
+
// Some accounts open setup first; try a fallback non-destructive button once.
|
|
224
|
+
const fallbackBtn = getAllButtons(response).find(b =>
|
|
225
|
+
!b.disabled &&
|
|
226
|
+
b.label &&
|
|
227
|
+
((b.label || '').toLowerCase().includes('view setup') || (b.label || '').toLowerCase().includes('setup'))
|
|
228
|
+
);
|
|
229
|
+
if (fallbackBtn) {
|
|
230
|
+
LOG.info('[stream] Go Live transition not detected; trying setup fallback');
|
|
231
|
+
await humanDelay(80, 180);
|
|
232
|
+
try {
|
|
233
|
+
const fallbackRes = await safeClickButton(response, fallbackBtn);
|
|
234
|
+
const afterFallback = fallbackRes || (await waitForStreamTransition({
|
|
235
|
+
channel,
|
|
236
|
+
waitForDankMemer,
|
|
237
|
+
prevMsg: before,
|
|
238
|
+
timeoutMs: 8000,
|
|
239
|
+
}));
|
|
240
|
+
if (afterFallback) {
|
|
241
|
+
response = afterFallback;
|
|
242
|
+
await hydrate(response);
|
|
243
|
+
logMsg(response, `stream-setup-fallback-${step}`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
179
248
|
} catch (e) {
|
|
180
249
|
LOG.error(`[stream] Go Live click failed: ${e.message}`);
|
|
181
250
|
}
|
|
@@ -195,13 +264,34 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
195
264
|
await humanDelay(120, 320);
|
|
196
265
|
try {
|
|
197
266
|
const clicked = await safeClickButton(response, action);
|
|
198
|
-
|
|
267
|
+
let updated = clicked || (await waitForDankMemer(6000)) || (await refetchMsg(channel, response.id));
|
|
199
268
|
if (updated) {
|
|
200
269
|
response = updated;
|
|
201
270
|
await hydrate(response);
|
|
202
271
|
logMsg(response, 'stream-action');
|
|
203
272
|
text = getFullText(response);
|
|
204
273
|
lower = normalizeLower(stripAnsi(text));
|
|
274
|
+
|
|
275
|
+
// Action payouts may appear as a second follow-up message.
|
|
276
|
+
// Keep checking briefly so earnings are not missed.
|
|
277
|
+
let bestText = text;
|
|
278
|
+
let bestCoins = parseCoins(bestText);
|
|
279
|
+
for (let i = 0; i < 2; i++) {
|
|
280
|
+
const nextMsg = await waitForDankMemer(5000);
|
|
281
|
+
if (!nextMsg) break;
|
|
282
|
+
await hydrate(nextMsg);
|
|
283
|
+
logMsg(nextMsg, `stream-action-followup-${i}`);
|
|
284
|
+
const nextText = getFullText(nextMsg);
|
|
285
|
+
const nextLower = normalizeLower(stripAnsi(nextText));
|
|
286
|
+
const nextCoins = parseCoins(nextText);
|
|
287
|
+
if (nextCoins > bestCoins || (bestCoins === 0 && isActionResultText(nextLower))) {
|
|
288
|
+
bestText = nextText;
|
|
289
|
+
bestCoins = nextCoins;
|
|
290
|
+
response = nextMsg;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
text = bestText;
|
|
294
|
+
lower = normalizeLower(stripAnsi(text));
|
|
205
295
|
}
|
|
206
296
|
} catch (e) {
|
|
207
297
|
LOG.error(`[stream] Action click failed: ${e.message}`);
|