dankgrinder 5.0.1 → 5.0.3
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/utils.js +24 -2
- package/lib/grinder.js +52 -60
- package/package.json +1 -1
package/lib/commands/utils.js
CHANGED
|
@@ -282,7 +282,9 @@ function findSelectMenuOption(msg, label) {
|
|
|
282
282
|
return null;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
// Safe button click — tries library methods first, falls back to raw HTTP for CV2
|
|
285
|
+
// Safe button click — tries library methods first, falls back to raw HTTP for CV2.
|
|
286
|
+
// When CV2 fallback is used, waits for the message to update so callers always
|
|
287
|
+
// get the updated message back (instead of null, which broke multi-round games).
|
|
286
288
|
async function safeClickButton(msg, button) {
|
|
287
289
|
if (typeof button.click === 'function') {
|
|
288
290
|
return button.click();
|
|
@@ -295,9 +297,29 @@ async function safeClickButton(msg, button) {
|
|
|
295
297
|
// Fall through to CV2 raw interaction fallback.
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
|
-
// CV2 fallback: send interaction via raw HTTP
|
|
300
|
+
// CV2 fallback: send interaction via raw HTTP, then wait for the message
|
|
301
|
+
// to update so we can return the updated message to the caller.
|
|
299
302
|
if (id) {
|
|
300
303
|
await clickCV2Button(msg, id);
|
|
304
|
+
// Wait for Dank Memer to process the interaction and update the message
|
|
305
|
+
const updatedMsg = await new Promise((resolve) => {
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
msg.client?.removeListener?.('messageUpdate', handler);
|
|
308
|
+
resolve(null);
|
|
309
|
+
}, 8000);
|
|
310
|
+
const handler = (_, newMsg) => {
|
|
311
|
+
if (newMsg.id === msg.id) {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
msg.client?.removeListener?.('messageUpdate', handler);
|
|
314
|
+
resolve(newMsg);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
msg.client?.on?.('messageUpdate', handler);
|
|
318
|
+
});
|
|
319
|
+
if (updatedMsg) {
|
|
320
|
+
await ensureCV2(updatedMsg);
|
|
321
|
+
return updatedMsg;
|
|
322
|
+
}
|
|
301
323
|
return null;
|
|
302
324
|
}
|
|
303
325
|
throw new Error('No click method available on button');
|
package/lib/grinder.js
CHANGED
|
@@ -453,19 +453,16 @@ function renderDashboard() {
|
|
|
453
453
|
|
|
454
454
|
lines.push(bar);
|
|
455
455
|
|
|
456
|
+
// Use absolute cursor positioning (row 1, col 1) to avoid ghost bar drift
|
|
457
|
+
process.stdout.write('\x1b[H');
|
|
456
458
|
const prevLines = dashboardLines;
|
|
457
|
-
if (prevLines > 0) {
|
|
458
|
-
process.stdout.write(c.cursorUp(prevLines));
|
|
459
|
-
}
|
|
460
459
|
for (const line of lines) {
|
|
461
460
|
process.stdout.write(c.clearLine + '\r' + line + '\n');
|
|
462
461
|
}
|
|
463
|
-
// Clear trailing
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
process.stdout.write(c.cursorUp(prevLines - lines.length));
|
|
462
|
+
// Clear any trailing lines from previous (larger) render
|
|
463
|
+
const maxClear = Math.max(prevLines - lines.length, 0) + 3;
|
|
464
|
+
for (let i = 0; i < maxClear; i++) {
|
|
465
|
+
process.stdout.write(c.clearLine + '\r\n');
|
|
469
466
|
}
|
|
470
467
|
dashboardLines = lines.length;
|
|
471
468
|
dashboardRendering = false;
|
|
@@ -568,32 +565,20 @@ async function reportEarnings(accountId, accountName, earned, spent, command) {
|
|
|
568
565
|
earningsBatch.push({ account_id: accountId, account_name: accountName, earned, spent, command });
|
|
569
566
|
}
|
|
570
567
|
|
|
571
|
-
|
|
568
|
+
// Command feed sends directly (no batching) for real-time dashboard SSE updates.
|
|
569
|
+
// The dashboard live queue depends on instant delivery via eventBus.emit("sse").
|
|
570
|
+
async function reportCommandFeed(accountId, accountName, data) {
|
|
572
571
|
if (!API_URL) return;
|
|
572
|
+
const normalized = { ...data };
|
|
573
|
+
if (typeof normalized.command === 'string') normalized.command = stripAnsi(normalized.command).replace(/\s+/g, ' ').trim();
|
|
574
|
+
if (typeof normalized.result === 'string') normalized.result = stripAnsi(normalized.result).replace(/\s+/g, ' ').trim();
|
|
573
575
|
try {
|
|
574
|
-
await fetch(`${API_URL}/api/grinder/command-feed
|
|
576
|
+
await fetch(`${API_URL}/api/grinder/command-feed`, {
|
|
575
577
|
method: 'POST',
|
|
576
578
|
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
577
|
-
body: JSON.stringify({
|
|
579
|
+
body: JSON.stringify({ account_id: accountId, account_name: accountName, ...normalized }),
|
|
578
580
|
});
|
|
579
|
-
} catch {
|
|
580
|
-
for (const item of batch) {
|
|
581
|
-
try {
|
|
582
|
-
await fetch(`${API_URL}/api/grinder/command-feed`, {
|
|
583
|
-
method: 'POST',
|
|
584
|
-
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
585
|
-
body: JSON.stringify(item),
|
|
586
|
-
});
|
|
587
|
-
} catch {}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}, { maxSize: 100, flushMs: 2000 });
|
|
591
|
-
|
|
592
|
-
async function reportCommandFeed(accountId, accountName, data) {
|
|
593
|
-
const normalized = { ...data };
|
|
594
|
-
if (typeof normalized.command === 'string') normalized.command = stripAnsi(normalized.command).replace(/\s+/g, ' ').trim();
|
|
595
|
-
if (typeof normalized.result === 'string') normalized.result = stripAnsi(normalized.result).replace(/\s+/g, ' ').trim();
|
|
596
|
-
feedBatch.push({ account_id: accountId, account_name: accountName, ...normalized });
|
|
581
|
+
} catch { /* silent — dashboard just won't see this update */ }
|
|
597
582
|
}
|
|
598
583
|
|
|
599
584
|
function randomDelay(min, max) {
|
|
@@ -1535,11 +1520,13 @@ class AccountWorker {
|
|
|
1535
1520
|
|
|
1536
1521
|
if (cmdResult.holdTightReason) {
|
|
1537
1522
|
const reason = cmdResult.holdTightReason;
|
|
1538
|
-
|
|
1523
|
+
const holdSec = 35;
|
|
1524
|
+
this.log('warn', `Hold Tight: /${reason} — ${holdSec}s global cooldown`);
|
|
1539
1525
|
const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
|
|
1540
1526
|
const mappedCmd = reasonMap[reason] || reason;
|
|
1541
|
-
await this.setCooldown(mappedCmd,
|
|
1542
|
-
await this.setCooldown(cmdName,
|
|
1527
|
+
await this.setCooldown(mappedCmd, holdSec);
|
|
1528
|
+
await this.setCooldown(cmdName, holdSec);
|
|
1529
|
+
this.globalCooldownUntil = Math.max(this.globalCooldownUntil, Date.now() + holdSec * 1000);
|
|
1543
1530
|
}
|
|
1544
1531
|
|
|
1545
1532
|
this.stats.successes++;
|
|
@@ -1887,7 +1874,7 @@ class AccountWorker {
|
|
|
1887
1874
|
this.tickTimeout = setTimeout(() => this.tick(), 5000);
|
|
1888
1875
|
return;
|
|
1889
1876
|
}
|
|
1890
|
-
if (this.busy) {
|
|
1877
|
+
if (this.busy || this._invRunning) {
|
|
1891
1878
|
this.tickTimeout = setTimeout(() => this.tick(), 2000);
|
|
1892
1879
|
return;
|
|
1893
1880
|
}
|
|
@@ -2025,6 +2012,14 @@ class AccountWorker {
|
|
|
2025
2012
|
await this.runCommand(item.cmd, prefix);
|
|
2026
2013
|
const earned = this.stats.coins - beforeCoins;
|
|
2027
2014
|
|
|
2015
|
+
// Grace period for interactive (button-click) commands — Dank Memer
|
|
2016
|
+
// needs time to process the interaction before accepting the next command.
|
|
2017
|
+
// Without this, the next command gets "Hold Tight" errors.
|
|
2018
|
+
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish']);
|
|
2019
|
+
if (INTERACTIVE_CMDS.has(item.cmd)) {
|
|
2020
|
+
await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2028
2023
|
// EMA: track smoothed earnings per command for adaptive scheduling
|
|
2029
2024
|
if (earned > 0) {
|
|
2030
2025
|
this._earningsEMA.update(earned);
|
|
@@ -2075,14 +2070,8 @@ class AccountWorker {
|
|
|
2075
2070
|
if (this.cycleCount > 0 && this.cycleCount % 10 === 0) this.printStats();
|
|
2076
2071
|
// Rebuild BloomFilter every 50 cycles to clear expired entries
|
|
2077
2072
|
if (this.cycleCount > 0 && this.cycleCount % 50 === 0) this._rebuildBloom();
|
|
2078
|
-
if (this.cycleCount > 0 && this.cycleCount % 5 === 0) {
|
|
2079
|
-
this.busy = true;
|
|
2080
|
-
await this.checkBalance();
|
|
2081
|
-
this.busy = false;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
2073
|
if (this.running && !shutdownCalled) {
|
|
2085
|
-
const nextDelay = this.failStreak > 0 ?
|
|
2074
|
+
const nextDelay = this.failStreak > 0 ? 3000 : 1500;
|
|
2086
2075
|
this.tickTimeout = setTimeout(() => this.tick(), nextDelay);
|
|
2087
2076
|
}
|
|
2088
2077
|
}
|
|
@@ -2090,7 +2079,6 @@ class AccountWorker {
|
|
|
2090
2079
|
async grindLoop() {
|
|
2091
2080
|
if (this.running) return;
|
|
2092
2081
|
this.running = true;
|
|
2093
|
-
this.busy = false;
|
|
2094
2082
|
this.paused = false;
|
|
2095
2083
|
this.dashboardPaused = false;
|
|
2096
2084
|
this.failStreak = 0;
|
|
@@ -2189,9 +2177,9 @@ class AccountWorker {
|
|
|
2189
2177
|
// Handle pending actions from dashboard
|
|
2190
2178
|
if (Array.isArray(data.pendingActions)) {
|
|
2191
2179
|
for (const action of data.pendingActions) {
|
|
2192
|
-
if (action.action === 'check_inventory' && !this.busy) {
|
|
2180
|
+
if (action.action === 'check_inventory' && !this.busy && !this._invRunning) {
|
|
2193
2181
|
this.log('info', 'Dashboard requested inventory check');
|
|
2194
|
-
this.checkInventory().catch(() => {});
|
|
2182
|
+
await this.checkInventory().catch(() => {});
|
|
2195
2183
|
try {
|
|
2196
2184
|
await fetch(`${API_URL}/api/grinder/actions`, {
|
|
2197
2185
|
method: 'DELETE',
|
|
@@ -2280,8 +2268,8 @@ class AccountWorker {
|
|
|
2280
2268
|
|
|
2281
2269
|
// Let Discord gateway settle before sending first command
|
|
2282
2270
|
await new Promise(r => setTimeout(r, 2500));
|
|
2283
|
-
|
|
2284
|
-
this.checkInventory().catch(() => {});
|
|
2271
|
+
// Run initial inventory check (awaited) before grind loop starts
|
|
2272
|
+
await this.checkInventory().catch(() => {});
|
|
2285
2273
|
this.grindLoop();
|
|
2286
2274
|
resolve();
|
|
2287
2275
|
});
|
|
@@ -2343,7 +2331,6 @@ async function start(apiKey, apiUrl) {
|
|
|
2343
2331
|
API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
|
|
2344
2332
|
REDIS_URL = process.env.REDIS_URL || '';
|
|
2345
2333
|
WEBHOOK_URL = process.env.WEBHOOK_URL || '';
|
|
2346
|
-
initRedis();
|
|
2347
2334
|
|
|
2348
2335
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
2349
2336
|
const tw = Math.min(process.stdout.columns || 80, 78);
|
|
@@ -2363,16 +2350,6 @@ async function start(apiKey, apiUrl) {
|
|
|
2363
2350
|
);
|
|
2364
2351
|
console.log(bar);
|
|
2365
2352
|
|
|
2366
|
-
const checks = [];
|
|
2367
|
-
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
|
|
2368
|
-
checks.push(REDIS_URL ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${c.dim}○ Redis${c.reset}`);
|
|
2369
|
-
checks.push(hasZlib ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}zlib (npm i zlib-sync)${c.reset}`);
|
|
2370
|
-
checks.push(WEBHOOK_URL ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}` : `${c.dim}○ Webhook${c.reset}`);
|
|
2371
|
-
if (CLUSTER_ENABLED) {
|
|
2372
|
-
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
|
|
2373
|
-
}
|
|
2374
|
-
console.log(` ${checks.join(' ')}`);
|
|
2375
|
-
|
|
2376
2353
|
log('info', `${c.dim}Fetching accounts...${c.reset}`);
|
|
2377
2354
|
|
|
2378
2355
|
let data = await fetchConfig(4, 2000);
|
|
@@ -2383,6 +2360,13 @@ async function start(apiKey, apiUrl) {
|
|
|
2383
2360
|
data = await fetchConfig(4, 2000);
|
|
2384
2361
|
}
|
|
2385
2362
|
|
|
2363
|
+
// Pull Redis/Webhook URLs from API config if not in env
|
|
2364
|
+
if (!REDIS_URL && data.redis_url) REDIS_URL = data.redis_url;
|
|
2365
|
+
if (!REDIS_URL && data.redisUrl) REDIS_URL = data.redisUrl;
|
|
2366
|
+
if (!WEBHOOK_URL && data.webhook_url) WEBHOOK_URL = data.webhook_url;
|
|
2367
|
+
if (!WEBHOOK_URL && data.webhookUrl) WEBHOOK_URL = data.webhookUrl;
|
|
2368
|
+
initRedis();
|
|
2369
|
+
|
|
2386
2370
|
let { accounts } = data;
|
|
2387
2371
|
if (!accounts || accounts.length === 0) {
|
|
2388
2372
|
log('error', 'No active accounts. Add them in the dashboard.');
|
|
@@ -2399,9 +2383,15 @@ async function start(apiKey, apiUrl) {
|
|
|
2399
2383
|
}
|
|
2400
2384
|
}
|
|
2401
2385
|
|
|
2386
|
+
const checks = [];
|
|
2387
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
|
|
2388
|
+
if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
|
|
2389
|
+
if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
|
|
2390
|
+
if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
|
|
2391
|
+
if (CLUSTER_ENABLED) {
|
|
2392
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
|
|
2393
|
+
}
|
|
2402
2394
|
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
|
|
2403
|
-
process.stdout.write(c.cursorUp(1));
|
|
2404
|
-
process.stdout.write(c.clearLine + '\r');
|
|
2405
2395
|
console.log(` ${checks.join(' ')}`);
|
|
2406
2396
|
console.log('');
|
|
2407
2397
|
|
|
@@ -2421,11 +2411,13 @@ async function start(apiKey, apiUrl) {
|
|
|
2421
2411
|
}
|
|
2422
2412
|
}
|
|
2423
2413
|
|
|
2424
|
-
console.log('');
|
|
2425
2414
|
startTime = Date.now();
|
|
2426
2415
|
dashboardStarted = true;
|
|
2427
2416
|
setDashboardActive(true);
|
|
2417
|
+
// Clear entire screen so startup logs don't create ghost bars
|
|
2418
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
2428
2419
|
process.stdout.write(c.hide);
|
|
2420
|
+
dashboardLines = 0;
|
|
2429
2421
|
|
|
2430
2422
|
setInterval(() => scheduleRender(), 1000);
|
|
2431
2423
|
scheduleRender();
|