dankgrinder 4.0.0 → 4.1.1

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/grinder.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
2
  const Redis = require('ioredis');
3
+ const commands = require('./commands');
3
4
 
4
5
  // ── Terminal Colors & ANSI ───────────────────────────────────
5
6
  const c = {
@@ -36,12 +37,12 @@ const SAFE_CRIME_OPTIONS = [
36
37
 
37
38
  let API_KEY = '';
38
39
  let API_URL = '';
39
- let REDIS_URL = process.env.REDIS_URL || 'redis://default:qXcezFjDHlCDtakRUZJmvsEzHoBgdLHi@shortline.proxy.rlwy.net:32007';
40
+ let REDIS_URL = process.env.REDIS_URL || '';
40
41
  let redis = null;
41
42
  const workers = [];
42
43
 
43
44
  function initRedis() {
44
- if (!redis) {
45
+ if (!redis && REDIS_URL) {
45
46
  try {
46
47
  redis = new Redis(REDIS_URL, { maxRetriesPerRequest: null, lazyConnect: true });
47
48
  redis.connect().catch(() => {});
@@ -51,37 +52,78 @@ function initRedis() {
51
52
  }
52
53
  }
53
54
 
55
+ // ── Truecolor gradient helpers ───────────────────────────────
56
+ function rgb(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
57
+ function bgRgb(r, g, b) { return `\x1b[48;2;${r};${g};${b}m`; }
58
+ function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
59
+
60
+ function gradientLine(text, from, to) {
61
+ const chars = [...text];
62
+ const visible = chars.filter(ch => ch !== ' ').length;
63
+ let ci = 0, out = '';
64
+ for (const ch of chars) {
65
+ if (ch === ' ') { out += ' '; continue; }
66
+ const t = visible > 1 ? ci / (visible - 1) : 0;
67
+ out += rgb(lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)) + ch;
68
+ ci++;
69
+ }
70
+ return out + c.reset;
71
+ }
72
+
73
+ const BANNER_RAW = [
74
+ ' ██████╗ █████╗ ███╗ ██╗██╗ ██╗',
75
+ ' ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝',
76
+ ' ██║ ██║███████║██╔██╗ ██║█████╔╝ ',
77
+ ' ██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ',
78
+ ' ██████╔╝██║ ██║██║ ╚████║██║ ██╗',
79
+ ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝',
80
+ ' ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗ ',
81
+ ' ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗',
82
+ ' ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝',
83
+ ' ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗',
84
+ ' ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║',
85
+ ' ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝',
86
+ ];
87
+
88
+ function colorBanner() {
89
+ const topColor = [192, 132, 252];
90
+ const midColor = [139, 92, 246];
91
+ const botColor = [34, 211, 238];
92
+ const n = BANNER_RAW.length;
93
+ let out = '\n';
94
+ for (let i = 0; i < n; i++) {
95
+ const t = i / (n - 1);
96
+ const from = t < 0.5
97
+ ? [lerp(topColor[0], midColor[0], t * 2), lerp(topColor[1], midColor[1], t * 2), lerp(topColor[2], midColor[2], t * 2)]
98
+ : [lerp(midColor[0], botColor[0], (t - 0.5) * 2), lerp(midColor[1], botColor[1], (t - 0.5) * 2), lerp(midColor[2], botColor[2], (t - 0.5) * 2)];
99
+ const to = t < 0.5
100
+ ? [lerp(236, 168, t * 2), lerp(72, 85, t * 2), lerp(153, 247, t * 2)]
101
+ : [lerp(168, 6, (t - 0.5) * 2), lerp(85, 182, (t - 0.5) * 2), lerp(247, 212, (t - 0.5) * 2)];
102
+ out += c.bold + gradientLine(BANNER_RAW[i], from, to) + '\n';
103
+ }
104
+ return out;
105
+ }
106
+
54
107
  // ── Live Dashboard State ─────────────────────────────────────
55
108
  let dashboardLines = 0;
56
109
  let dashboardStarted = false;
110
+ let dashboardRendering = false;
57
111
  let totalBalance = 0;
58
112
  let totalCoins = 0;
59
113
  let totalCommands = 0;
60
114
  let startTime = Date.now();
61
- const recentLogs = []; // Ring buffer of last N log entries
62
- const MAX_LOGS = 6;
63
-
64
- const BANNER = `
65
- ${c.magenta}${c.bold} ██████╗ █████╗ ███╗ ██╗██╗ ██╗${c.reset}
66
- ${c.magenta}${c.bold} ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝${c.reset}
67
- ${c.magenta}${c.bold} ██║ ██║███████║██╔██╗ ██║█████╔╝ ${c.reset}
68
- ${c.magenta}${c.bold} ██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ${c.reset}
69
- ${c.magenta}${c.bold} ██████╔╝██║ ██║██║ ╚████║██║ ██╗${c.reset}
70
- ${c.magenta}${c.bold} ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝${c.reset}
71
- ${c.cyan}${c.bold} ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗${c.reset}
72
- ${c.cyan}${c.bold} ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗${c.reset}
73
- ${c.cyan}${c.bold} ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝${c.reset}
74
- ${c.cyan}${c.bold} ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗${c.reset}
75
- ${c.cyan}${c.bold} ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║${c.reset}
76
- ${c.cyan}${c.bold} ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝${c.reset}
77
- `;
115
+ let shutdownCalled = false;
116
+ const recentLogs = [];
117
+ const MAX_LOGS = 4;
78
118
 
79
119
  function formatUptime() {
80
120
  const s = Math.floor((Date.now() - startTime) / 1000);
81
121
  const h = Math.floor(s / 3600);
82
122
  const m = Math.floor((s % 3600) / 60);
83
123
  const sec = s % 60;
84
- return `${h}h ${m}m ${sec}s`;
124
+ if (h > 0) return `${h}h ${m}m`;
125
+ if (m > 0) return `${m}m ${sec}s`;
126
+ return `${sec}s`;
85
127
  }
86
128
 
87
129
  function formatCoins(n) {
@@ -91,9 +133,9 @@ function formatCoins(n) {
91
133
  }
92
134
 
93
135
  function renderDashboard() {
94
- if (!dashboardStarted || workers.length === 0) return;
136
+ if (!dashboardStarted || workers.length === 0 || dashboardRendering) return;
137
+ dashboardRendering = true;
95
138
 
96
- // Compute totals
97
139
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
98
140
  for (const w of workers) {
99
141
  totalBalance += w.stats.balance || 0;
@@ -102,42 +144,49 @@ function renderDashboard() {
102
144
  }
103
145
 
104
146
  const lines = [];
105
- const width = Math.min(process.stdout.columns || 80, 90);
106
- const sep = c.dim + '─'.repeat(width) + c.reset;
147
+ const tw = Math.min(process.stdout.columns || 80, 78);
148
+ const thinBar = c.dim + '─'.repeat(tw) + c.reset;
149
+ const thickTop = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
150
+ const thickBot = rgb(34, 211, 238) + c.bold + '═'.repeat(tw) + c.reset;
107
151
 
108
- // Header bar
109
- lines.push(sep);
152
+ lines.push(thickTop);
110
153
  lines.push(
111
- ` ${c.yellow}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}` +
112
- ` ${c.dim}│${c.reset} ${c.green}+⏣ ${formatCoins(totalCoins)}${c.reset} earned` +
113
- ` ${c.dim}│${c.reset} ${c.white}${totalCommands}${c.reset} cmds` +
114
- ` ${c.dim}│${c.reset} ${c.dim}⏱ ${formatUptime()}${c.reset}`
154
+ ` ${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}` +
155
+ ` ${c.dim}│${c.reset}` +
156
+ ` ${rgb(52, 211, 153)}${c.bold}+${formatCoins(totalCoins)}${c.reset} ${c.dim}earned${c.reset}` +
157
+ ` ${c.dim}│${c.reset}` +
158
+ ` ${c.white}${c.bold}${totalCommands}${c.reset} ${c.dim}cmds${c.reset}` +
159
+ ` ${c.dim}│${c.reset}` +
160
+ ` ${rgb(251, 191, 36)}⏱ ${formatUptime()}${c.reset}` +
161
+ ` ${c.dim}│${c.reset}` +
162
+ ` ${rgb(52, 211, 153)}●${c.reset} ${c.dim}LIVE${c.reset}`
115
163
  );
116
- lines.push(sep);
117
-
118
- // Per-account rows
119
- for (const w of workers) {
120
- const status = w.lastStatus || 'idle';
121
- const statusColor = w.running ? (w.busy ? c.yellow : c.green) : c.red;
122
- const dot = w.running ? (w.busy ? '◉' : '●') : '○';
123
- const bal = w.stats.balance > 0 ? `${c.dim}⏣${c.reset} ${c.white}${formatCoins(w.stats.balance)}${c.reset}` : `${c.dim}⏣ ---${c.reset}`;
124
- const earned = w.stats.coins > 0 ? `${c.green}+${formatCoins(w.stats.coins)}${c.reset}` : `${c.dim}+0${c.reset}`;
125
- const cmds = `${c.dim}${w.stats.commands}cmd${c.reset}`;
126
- const name = `${w.color}${c.bold}${(w.username || 'Account').substring(0, 14).padEnd(14)}${c.reset}`;
127
-
128
- lines.push(
129
- ` ${statusColor}${dot}${c.reset} ${name} ${bal} ${earned} ${cmds} ${c.dim}→${c.reset} ${status.substring(0, 35)}`
130
- );
164
+ lines.push(thinBar);
165
+
166
+ for (const wk of workers) {
167
+ const last = (wk.lastStatus || 'idle').substring(0, 32);
168
+ const dot = wk.running
169
+ ? (wk.paused ? `${rgb(251, 191, 36)}⏸${c.reset}` : wk.busy ? `${rgb(251, 191, 36)}◉${c.reset}` : `${rgb(52, 211, 153)}●${c.reset}`)
170
+ : `${rgb(239, 68, 68)}○${c.reset}`;
171
+ const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, 16).padEnd(16)}${c.reset}`;
172
+ const bal = wk.stats.balance > 0
173
+ ? `${rgb(251, 191, 36)}⏣${c.reset}${c.white}${c.bold}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
174
+ : `${c.dim}⏣ -${c.reset}`;
175
+ const earned = wk.stats.coins > 0
176
+ ? `${rgb(52, 211, 153)}+${formatCoins(wk.stats.coins).padStart(6)}${c.reset}`
177
+ : `${c.dim} +0${c.reset}`;
178
+ lines.push(` ${dot} ${name} ${bal} ${earned} ${c.dim}${last}${c.reset}`);
131
179
  }
132
- lines.push(sep);
133
180
 
134
- // Scrolling log section (last 6 events)
135
- for (const entry of recentLogs) {
136
- lines.push(` ${c.dim}${entry}${c.reset}`);
181
+ if (recentLogs.length > 0) {
182
+ lines.push(thinBar);
183
+ for (const entry of recentLogs) {
184
+ lines.push(` ${c.dim}${entry}${c.reset}`);
185
+ }
137
186
  }
138
- if (recentLogs.length > 0) lines.push(sep);
139
187
 
140
- // Move cursor up to overwrite previous dashboard
188
+ lines.push(thickBot);
189
+
141
190
  if (dashboardLines > 0) {
142
191
  process.stdout.write(c.cursorUp(dashboardLines));
143
192
  }
@@ -145,30 +194,31 @@ function renderDashboard() {
145
194
  process.stdout.write(c.clearLine + '\r' + line + '\n');
146
195
  }
147
196
  dashboardLines = lines.length;
197
+ dashboardRendering = false;
148
198
  }
149
199
 
150
200
  function log(type, msg, label) {
151
- const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
201
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
152
202
  const icons = {
153
- info: `│`, success: `✓`, error: `✗`, warn: `!`,
154
- cmd: `▸`, coin: `$`, buy: `♦`, bal: `◈`, debug: `⊙`,
203
+ info: '·', success: '✓', error: '✗', warn: '!',
204
+ cmd: '▸', coin: '$', buy: '♦', bal: '◈', debug: '·',
155
205
  };
156
- const tag = label ? `${label} ` : '';
157
- const stripped = msg.replace(/\x1b\[[0-9;]*m/g, ''); // Strip ANSI for log buffer
206
+ const tag = label ? (label.replace(/\x1b\[[0-9;]*m/g, '') + ' ') : '';
207
+ const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
158
208
  if (dashboardStarted) {
159
- // Add to scrolling log buffer
160
- recentLogs.push(`${time} ${icons[type] || '│'} ${tag}${stripped}`.substring(0, 80));
209
+ recentLogs.push(`${time} ${icons[type] || '·'} ${tag}${stripped}`.substring(0, 72));
161
210
  while (recentLogs.length > MAX_LOGS) recentLogs.shift();
162
211
  renderDashboard();
163
212
  } else {
164
213
  const colorIcons = {
165
- info: `${c.cyan}│${c.reset}`, success: `${c.green}✓${c.reset}`,
166
- error: `${c.red}✗${c.reset}`, warn: `${c.yellow}!${c.reset}`,
167
- cmd: `${c.magenta}▸${c.reset}`, coin: `${c.yellow}$${c.reset}`,
168
- buy: `${c.blue}♦${c.reset}`, bal: `${c.green}◈${c.reset}`,
169
- debug: `${c.dim}⊙${c.reset}`,
214
+ info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
215
+ error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
216
+ cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
217
+ buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
218
+ debug: `${c.dim}·${c.reset}`,
170
219
  };
171
- console.log(` ${c.dim}${time}${c.reset} ${colorIcons[type] || colorIcons.info} ${tag}${msg}`);
220
+ const tagCol = label ? `${label} ` : '';
221
+ console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
172
222
  }
173
223
  }
174
224
 
@@ -193,6 +243,17 @@ async function sendLog(accountName, command, response, status) {
193
243
  } catch { /* silent */ }
194
244
  }
195
245
 
246
+ async function reportEarnings(accountId, accountName, earned, spent, command) {
247
+ if (earned <= 0 && spent <= 0) return;
248
+ try {
249
+ await fetch(`${API_URL}/api/grinder/earnings`, {
250
+ method: 'POST',
251
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({ account_id: accountId, account_name: accountName, earned, spent, command }),
253
+ });
254
+ } catch { /* silent */ }
255
+ }
256
+
196
257
  function randomDelay(min, max) {
197
258
  return new Promise((r) => setTimeout(r, (Math.random() * (max - min) + min) * 1000));
198
259
  }
@@ -203,31 +264,21 @@ function safeParseJSON(str, fallback = []) {
203
264
  try { return JSON.parse(str || '[]'); } catch { return fallback; }
204
265
  }
205
266
 
206
- // ── Coin Parser ──────────────────────────────────────────────
267
+ // ── Coin Parser (conservative — only ⏣ amounts) ─────────────
207
268
  function parseCoins(text) {
208
269
  if (!text) return 0;
209
- const patterns = [
210
- /[⏣💰]\s*[\d,]+/g,
211
- /\+\s*[\d,]+\s*coins?/gi,
212
- /\*\*([\d,]+)\*\*/g,
213
- /take\s*[⏣💰]?\s*([\d,]+)/gi,
214
- /earned\s*[⏣💰]?\s*([\d,]+)/gi,
215
- /found\s*[⏣💰]?\s*([\d,]+)/gi,
216
- /got\s*[⏣💰]?\s*([\d,]+)/gi,
217
- /won\s*[⏣💰]?\s*([\d,]+)/gi,
218
- /gained\s*[⏣💰]?\s*([\d,]+)/gi,
219
- ];
220
- let total = 0;
221
- for (const pat of patterns) {
222
- const matches = text.match(pat);
223
- if (matches) {
224
- for (const m of matches) {
225
- const numStr = m.replace(/[^\d]/g, '');
226
- if (numStr) total = Math.max(total, parseInt(numStr));
227
- }
270
+ // Only match coins that are clearly ⏣ prefixed
271
+ const matches = text.match(/⏣\s*([\d,]+)/g);
272
+ if (!matches) return 0;
273
+ let best = 0;
274
+ for (const m of matches) {
275
+ const numStr = m.replace(/[^\d]/g, '');
276
+ if (numStr) {
277
+ const val = parseInt(numStr);
278
+ if (val > 0 && val < 10000000) best = Math.max(best, val); // Cap at 10M sanity
228
279
  }
229
280
  }
230
- return total;
281
+ return best;
231
282
  }
232
283
 
233
284
  function getFullText(msg) {
@@ -253,7 +304,7 @@ function getAllButtons(msg) {
253
304
  for (const row of msg.components) {
254
305
  if (row.components) {
255
306
  for (const comp of row.components) {
256
- if (comp.type === 2) buttons.push(comp);
307
+ if (comp.type === 2 || comp.type === 'BUTTON') buttons.push(comp);
257
308
  }
258
309
  }
259
310
  }
@@ -267,7 +318,7 @@ function getAllSelectMenus(msg) {
267
318
  for (const row of msg.components) {
268
319
  if (row.components) {
269
320
  for (const comp of row.components) {
270
- if (comp.type === 3) menus.push(comp);
321
+ if (comp.type === 3 || comp.type === 'SELECT_MENU' || comp.type === 'STRING_SELECT') menus.push(comp);
271
322
  }
272
323
  }
273
324
  }
@@ -275,6 +326,18 @@ function getAllSelectMenus(msg) {
275
326
  return menus;
276
327
  }
277
328
 
329
+ // Safe button click — tries .click() first, falls back to msg.clickButton()
330
+ async function safeClickButton(msg, button) {
331
+ if (typeof button.click === 'function') {
332
+ return button.click();
333
+ }
334
+ // Fallback: use message.clickButton with customId
335
+ if (button.customId && typeof msg.clickButton === 'function') {
336
+ return msg.clickButton(button.customId);
337
+ }
338
+ throw new Error('No click method available on button');
339
+ }
340
+
278
341
  function pickSafeButton(buttons, safeList) {
279
342
  if (!buttons || buttons.length === 0) return null;
280
343
  if (safeList && safeList.length > 0) {
@@ -309,6 +372,68 @@ function debugMessage(msg, label) {
309
372
  }
310
373
  }
311
374
 
375
+ // ══════════════════════════════════════════════════════════════
376
+ // ═ Min-Heap Priority Queue for Command Scheduling
377
+ // ══════════════════════════════════════════════════════════════
378
+
379
+ class MinHeap {
380
+ constructor() { this.heap = []; }
381
+
382
+ push(item) {
383
+ this.heap.push(item);
384
+ this._bubbleUp(this.heap.length - 1);
385
+ }
386
+
387
+ pop() {
388
+ if (this.heap.length === 0) return null;
389
+ const top = this.heap[0];
390
+ const last = this.heap.pop();
391
+ if (this.heap.length > 0) {
392
+ this.heap[0] = last;
393
+ this._sinkDown(0);
394
+ }
395
+ return top;
396
+ }
397
+
398
+ peek() {
399
+ return this.heap.length > 0 ? this.heap[0] : null;
400
+ }
401
+
402
+ get size() { return this.heap.length; }
403
+
404
+ _bubbleUp(i) {
405
+ while (i > 0) {
406
+ const parent = (i - 1) >> 1;
407
+ if (this.heap[i].nextRunAt < this.heap[parent].nextRunAt ||
408
+ (this.heap[i].nextRunAt === this.heap[parent].nextRunAt && this.heap[i].priority > this.heap[parent].priority)) {
409
+ [this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
410
+ i = parent;
411
+ } else break;
412
+ }
413
+ }
414
+
415
+ _sinkDown(i) {
416
+ const n = this.heap.length;
417
+ while (true) {
418
+ let smallest = i;
419
+ const left = 2 * i + 1;
420
+ const right = 2 * i + 2;
421
+ if (left < n && (this.heap[left].nextRunAt < this.heap[smallest].nextRunAt ||
422
+ (this.heap[left].nextRunAt === this.heap[smallest].nextRunAt && this.heap[left].priority > this.heap[smallest].priority))) {
423
+ smallest = left;
424
+ }
425
+ if (right < n && (this.heap[right].nextRunAt < this.heap[smallest].nextRunAt ||
426
+ (this.heap[right].nextRunAt === this.heap[smallest].nextRunAt && this.heap[right].priority > this.heap[smallest].priority))) {
427
+ smallest = right;
428
+ }
429
+ if (smallest !== i) {
430
+ [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
431
+ i = smallest;
432
+ } else break;
433
+ }
434
+ }
435
+ }
436
+
312
437
  // ══════════════════════════════════════════════════════════════
313
438
  // ═ Worker: one per Discord account
314
439
  // ══════════════════════════════════════════════════════════════
@@ -328,15 +453,23 @@ class AccountWorker {
328
453
  this.lastRunTime = {};
329
454
  this.cycleCount = 0;
330
455
  this.lastCommandRun = 0;
456
+ this.paused = false;
457
+ this.globalCooldownUntil = 0;
458
+ this.commandQueue = null;
459
+ this.lastHealthCheck = Date.now();
331
460
  }
332
461
 
333
462
  get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
334
- log(type, msg) { log(type, msg, this.tag); }
335
463
 
336
- // Update the live dashboard status for this worker
464
+ log(type, msg) {
465
+ const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
466
+ this.lastStatus = stripped.substring(0, 40);
467
+ log(type, msg, this.tag);
468
+ }
469
+
337
470
  setStatus(text) {
338
471
  this.lastStatus = text;
339
- renderDashboard();
472
+ if (dashboardStarted) renderDashboard();
340
473
  }
341
474
 
342
475
  waitForDankMemer(timeout = 15000) {
@@ -347,19 +480,39 @@ class AccountWorker {
347
480
  resolve(null);
348
481
  }, timeout);
349
482
  const self = this;
483
+ function cleanup() {
484
+ clearTimeout(timer);
485
+ self.client.removeListener('messageCreate', handler);
486
+ self.client.removeListener('messageUpdate', updateHandler);
487
+ }
350
488
  function handler(msg) {
351
489
  if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
352
- clearTimeout(timer);
353
- self.client.removeListener('messageCreate', handler);
354
- self.client.removeListener('messageUpdate', updateHandler);
490
+ // If message has no content and no embeds, Dank Memer may populate via edit
491
+ const hasContent = (msg.content && msg.content.length > 0) || (msg.embeds && msg.embeds.length > 0) || (msg.components && msg.components.length > 0);
492
+ if (!hasContent) {
493
+ // Wait for the edit with actual content (up to 3s)
494
+ const editTimer = setTimeout(() => { cleanup(); resolve(msg); }, 3000);
495
+ function editHandler(oldMsg, newMsg) {
496
+ if (newMsg.id === msg.id) {
497
+ clearTimeout(editTimer);
498
+ self.client.removeListener('messageUpdate', editHandler);
499
+ cleanup();
500
+ resolve(newMsg);
501
+ }
502
+ }
503
+ self.client.on('messageUpdate', editHandler);
504
+ // Remove original handlers since we're now specifically looking for this msg's edit
505
+ self.client.removeListener('messageCreate', handler);
506
+ self.client.removeListener('messageUpdate', updateHandler);
507
+ return;
508
+ }
509
+ cleanup();
355
510
  resolve(msg);
356
511
  }
357
512
  }
358
513
  function updateHandler(oldMsg, newMsg) {
359
514
  if (newMsg.author?.id === DANK_MEMER_ID && newMsg.channel?.id === self.channel.id) {
360
- clearTimeout(timer);
361
- self.client.removeListener('messageCreate', handler);
362
- self.client.removeListener('messageUpdate', updateHandler);
515
+ cleanup();
363
516
  resolve(newMsg);
364
517
  }
365
518
  }
@@ -473,7 +626,7 @@ class AccountWorker {
473
626
  }
474
627
 
475
628
  this.log('buy', `Clicking Buy ${itemName}...`);
476
- try { await buyBtn.click(); } catch (e) {
629
+ try { await safeClickButton(response, buyBtn); } catch (e) {
477
630
  this.log('error', `Buy click failed: ${e.message}`);
478
631
  if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
479
632
  return false;
@@ -560,383 +713,13 @@ class AccountWorker {
560
713
  }
561
714
  }
562
715
 
563
- // ══════════════════════════════════════════════════════════════
564
- // ═ COMMAND HANDLERS
565
- // ══════════════════════════════════════════════════════════════
566
-
567
- async handleSearch(response) {
568
- if (!response) return null;
569
- debugMessage(response, 'SEARCH');
570
- const buttons = getAllButtons(response);
571
- if (buttons.length === 0) return getFullText(response).substring(0, 80);
572
- const userSafe = safeParseJSON(this.account.search_answers, []);
573
- const btn = pickSafeButton(buttons, userSafe);
574
- if (btn) {
575
- await humanDelay();
576
- try {
577
- await btn.click();
578
- const followUp = await this.waitForDankMemer(10000);
579
- if (followUp) {
580
- const text = getFullText(followUp);
581
- const coins = parseCoins(text);
582
- if (coins > 0) { this.stats.coins += coins; return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
583
- if (text.toLowerCase().includes('nothing')) return `${btn.label} → found nothing`;
584
- return `${btn.label} → done`;
585
- }
586
- return `Clicked: ${btn.label}`;
587
- } catch { return null; }
588
- }
589
- return 'No safe option found';
590
- }
591
-
592
- async handleCrime(response) {
593
- if (!response) return null;
594
- debugMessage(response, 'CRIME');
595
- const buttons = getAllButtons(response);
596
- if (buttons.length === 0) return getFullText(response).substring(0, 80);
597
- const userSafe = safeParseJSON(this.account.crime_answers, []);
598
- const btn = pickSafeButton(buttons, userSafe);
599
- if (btn) {
600
- await humanDelay();
601
- try {
602
- await btn.click();
603
- const followUp = await this.waitForDankMemer(10000);
604
- if (followUp) {
605
- const text = getFullText(followUp);
606
- const coins = parseCoins(text);
607
- if (coins > 0) { this.stats.coins += coins; return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
608
- return `${btn.label} → done`;
609
- }
610
- return `Clicked: ${btn.label}`;
611
- } catch { return null; }
612
- }
613
- return 'No safe option found';
614
- }
615
-
616
- async handleHighLow(response) {
617
- if (!response) return null;
618
- debugMessage(response, 'HIGHLOW');
619
- const text = getFullText(response);
620
- const match = text.match(/number.*?(\d+)/i) || text.match(/(\d+)/);
621
- const buttons = getAllButtons(response);
622
- if (match && buttons.length >= 2) {
623
- const num = parseInt(match[1]);
624
- await humanDelay();
625
- let targetBtn;
626
- if (num > 50) targetBtn = buttons.find((b) => (b.label || '').toLowerCase().includes('lower')) || buttons[1];
627
- else if (num < 50) targetBtn = buttons.find((b) => (b.label || '').toLowerCase().includes('higher')) || buttons[0];
628
- else {
629
- const jackpot = buttons.find((b) => (b.label || '').toLowerCase().includes('jackpot'));
630
- targetBtn = jackpot || buttons[Math.floor(Math.random() * 2)];
631
- }
632
- try {
633
- await targetBtn.click();
634
- const followUp = await this.waitForDankMemer(10000);
635
- if (followUp) {
636
- const ftText = getFullText(followUp);
637
- const coins = parseCoins(ftText);
638
- if (coins > 0) { this.stats.coins += coins; return `${num} → ${targetBtn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
639
- const moreButtons = getAllButtons(followUp);
640
- if (moreButtons.length >= 2) return await this.handleHighLow(followUp);
641
- return `${num} → ${targetBtn.label}`;
642
- }
643
- return `${num} → ${targetBtn.label}`;
644
- } catch { return null; }
645
- }
646
- if (buttons.length > 0) {
647
- const btn = buttons[Math.floor(Math.random() * buttons.length)];
648
- await humanDelay();
649
- try { await btn.click(); } catch {}
650
- return `Clicked: ${btn.label || 'button'}`;
651
- }
652
- return null;
653
- }
654
-
655
- async handleScratch(response) {
656
- if (!response) return null;
657
- debugMessage(response, 'SCRATCH');
658
- const buttons = getAllButtons(response);
659
- if (buttons.length === 0) {
660
- const text = getFullText(response);
661
- const coins = parseCoins(text);
662
- if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
663
- return text.substring(0, 60) || 'done';
664
- }
665
- let lastResponse = response;
666
- for (let i = 0; i < Math.min(buttons.length, 9); i++) {
667
- const btn = buttons[i];
668
- if (btn && !btn.disabled) {
669
- await humanDelay(300, 700);
670
- try {
671
- await btn.click();
672
- const followUp = await this.waitForDankMemer(5000);
673
- if (followUp) lastResponse = followUp;
674
- } catch { break; }
675
- }
676
- }
677
- const finalText = getFullText(lastResponse);
678
- const coins = parseCoins(finalText);
679
- if (coins > 0) { this.stats.coins += coins; return `scratch → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
680
- return 'scratch done';
681
- }
682
-
683
- async handleAdventure(response) {
684
- if (!response) return null;
685
- debugMessage(response, 'ADVENTURE');
686
- let current = response;
687
- let rounds = 0;
688
- const MAX_ROUNDS = 15;
689
- while (current && rounds < MAX_ROUNDS) {
690
- const buttons = getAllButtons(current);
691
- if (buttons.length === 0) break;
692
- const safeButtons = buttons.filter((b) => !b.disabled && b.style !== 4);
693
- const btn = safeButtons.length > 0 ? safeButtons[Math.floor(Math.random() * safeButtons.length)] : buttons.find((b) => !b.disabled);
694
- if (!btn) break;
695
- await humanDelay(500, 1500);
696
- try {
697
- await btn.click();
698
- const followUp = await this.waitForDankMemer(10000);
699
- if (followUp) { current = followUp; rounds++; } else break;
700
- } catch { break; }
701
- }
702
- const finalText = getFullText(current);
703
- // Parse next adventure time from footer
704
- const nextMatch = finalText.match(/next adventure.*?(\d+)/i);
705
- if (nextMatch) {
706
- const nextSec = parseInt(nextMatch[1]) * 60;
707
- this.log('info', `Next adventure in ${c.yellow}${nextMatch[1]}m${c.reset}`);
708
- await this.setCooldown('adventure', nextSec);
709
- }
710
- const coins = parseCoins(finalText);
711
- if (coins > 0) { this.stats.coins += coins; return `adventure (${rounds} rounds) → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
712
- return `adventure done (${rounds} rounds)`;
713
- }
714
-
715
- // ── BlackJack Handler ─────────────────────────────────────
716
- async handleBlackjack(response) {
717
- if (!response) return null;
718
- debugMessage(response, 'BLACKJACK');
719
- let current = response;
720
- const MAX_ROUNDS = 10;
721
- for (let i = 0; i < MAX_ROUNDS; i++) {
722
- const text = getFullText(current);
723
- const buttons = getAllButtons(current);
724
- if (buttons.length === 0) break;
725
- // Parse player total from embed - look for patterns like "Total: 15" or "Value: **15**"
726
- const totalMatch = text.match(/total[:\s]*\**(\d+)\**/i) || text.match(/value[:\s]*\**(\d+)\**/i);
727
- const playerTotal = totalMatch ? parseInt(totalMatch[1]) : 0;
728
- let targetBtn;
729
- if (playerTotal >= 17 || playerTotal === 0) {
730
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('stand')) || buttons[1];
731
- } else {
732
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('hit')) || buttons[0];
733
- }
734
- if (!targetBtn || targetBtn.disabled) break;
735
- await humanDelay(500, 1200);
736
- try {
737
- await targetBtn.click();
738
- const followUp = await this.waitForDankMemer(8000);
739
- if (followUp) {
740
- current = followUp;
741
- const fButtons = getAllButtons(current);
742
- if (fButtons.length === 0 || fButtons.every(b => b.disabled)) break;
743
- } else break;
744
- } catch { break; }
745
- }
746
- const finalText = getFullText(current);
747
- const coins = parseCoins(finalText);
748
- if (coins > 0) { this.stats.coins += coins; return `blackjack → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
749
- if (finalText.toLowerCase().includes('won')) return 'blackjack → won';
750
- if (finalText.toLowerCase().includes('lost') || finalText.toLowerCase().includes('bust')) return 'blackjack → lost';
751
- return 'blackjack done';
752
- }
753
-
754
- // ── Trivia Handler ────────────────────────────────────────
755
- async handleTrivia(response) {
756
- if (!response) return null;
757
- debugMessage(response, 'TRIVIA');
758
- const buttons = getAllButtons(response);
759
- if (buttons.length === 0) return getFullText(response).substring(0, 80);
760
- // Pick a random answer (we don't have a trivia DB - random has ~25% chance)
761
- const clickable = buttons.filter(b => !b.disabled);
762
- const btn = clickable[Math.floor(Math.random() * clickable.length)];
763
- if (btn) {
764
- await humanDelay(1000, 3000);
765
- try {
766
- await btn.click();
767
- const followUp = await this.waitForDankMemer(8000);
768
- if (followUp) {
769
- const text = getFullText(followUp);
770
- const coins = parseCoins(text);
771
- if (coins > 0) { this.stats.coins += coins; return `trivia → ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
772
- if (text.toLowerCase().includes('correct')) return `trivia → ${btn.label} → correct`;
773
- return `trivia → ${btn.label} → wrong`;
774
- }
775
- } catch {}
776
- }
777
- return 'trivia done';
778
- }
779
-
780
- // ── Work Shift Handler ────────────────────────────────────
781
- async handleWorkShift(response) {
782
- if (!response) return null;
783
- debugMessage(response, 'WORK');
784
- let current = response;
785
- const MAX_ROUNDS = 5;
786
- for (let i = 0; i < MAX_ROUNDS; i++) {
787
- const buttons = getAllButtons(current);
788
- const text = getFullText(current);
789
- // Work shift mini-games: word scramble, memory, typing, etc.
790
- // Try to click any available non-disabled button
791
- if (buttons.length === 0) break;
792
- const clickable = buttons.filter(b => !b.disabled);
793
- if (clickable.length === 0) break;
794
- // For word-type games, look for the correct button
795
- const btn = clickable[0]; // Just pick first available
796
- await humanDelay(500, 1500);
797
- try {
798
- await btn.click();
799
- const followUp = await this.waitForDankMemer(8000);
800
- if (followUp) { current = followUp; } else break;
801
- } catch { break; }
802
- }
803
- const finalText = getFullText(current);
804
- const coins = parseCoins(finalText);
805
- if (coins > 0) { this.stats.coins += coins; return `work → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
806
- return 'work done';
807
- }
808
-
809
- // ── Generic Command Handler (beg, hunt, dig, farm, tidy, etc.) ──
810
- async handleGenericCommand(response) {
811
- if (!response) return null;
812
- debugMessage(response, 'GENERIC');
813
- const text = getFullText(response);
814
- const coins = parseCoins(text);
815
- const neededItem = this.needsItem(text);
816
- if (neededItem) {
817
- this.log('warn', `Missing ${c.bold}${neededItem}${c.reset} — auto-buying...`);
818
- const bought = await this.buyItem(neededItem);
819
- if (bought) return `auto-bought ${neededItem}`;
820
- return `need ${neededItem} (couldn't buy)`;
821
- }
822
- // Handle buttons if present (e.g., postmemes, farm choices)
823
- const buttons = getAllButtons(response);
824
- if (buttons.length > 0) {
825
- const btn = buttons.find((b) => !b.disabled) || buttons[0];
826
- if (btn) {
827
- await humanDelay();
828
- try {
829
- await btn.click();
830
- const followUp = await this.waitForDankMemer(8000);
831
- if (followUp) {
832
- const fText = getFullText(followUp);
833
- const fCoins = parseCoins(fText);
834
- if (fCoins > 0) { this.stats.coins += fCoins; return `${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
835
- // Check if followUp also has buttons (multi-step)
836
- const nextButtons = getAllButtons(followUp);
837
- if (nextButtons.length > 0) {
838
- const nextBtn = nextButtons.find(b => !b.disabled);
839
- if (nextBtn) {
840
- await humanDelay();
841
- try { await nextBtn.click(); } catch {}
842
- }
843
- }
844
- }
845
- } catch {}
846
- }
847
- }
848
- // Handle select menus (some commands use dropdowns)
849
- const menus = getAllSelectMenus(response);
850
- if (menus.length > 0) {
851
- const menu = menus[0];
852
- const options = menu.options || [];
853
- if (options.length > 0) {
854
- const opt = options[Math.floor(Math.random() * options.length)];
855
- try {
856
- await response.selectMenu(menu.customId, [opt.value]);
857
- const followUp = await this.waitForDankMemer(8000);
858
- if (followUp) {
859
- const fText = getFullText(followUp);
860
- const fCoins = parseCoins(fText);
861
- if (fCoins > 0) { this.stats.coins += fCoins; return `${opt.label} → ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
862
- }
863
- } catch {}
864
- }
865
- }
866
- if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
867
- return text.substring(0, 60) || 'done';
868
- }
869
-
870
- // ── Simple Gambling (cointoss, roulette, slots, snakeeyes) ──
871
- async handleSimpleGamble(response) {
872
- if (!response) return null;
873
- debugMessage(response, 'GAMBLE');
874
- const text = getFullText(response);
875
- const coins = parseCoins(text);
876
- // Check buttons (some gambling has buttons)
877
- const buttons = getAllButtons(response);
878
- if (buttons.length > 0) {
879
- const btn = buttons.find(b => !b.disabled);
880
- if (btn) {
881
- await humanDelay();
882
- try {
883
- await btn.click();
884
- const followUp = await this.waitForDankMemer(8000);
885
- if (followUp) {
886
- const fText = getFullText(followUp);
887
- const fCoins = parseCoins(fText);
888
- if (fCoins > 0) { this.stats.coins += fCoins; return `${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
889
- if (fText.toLowerCase().includes('won')) return `${c.green}won${c.reset}`;
890
- if (fText.toLowerCase().includes('lost')) return `${c.red}lost${c.reset}`;
891
- }
892
- } catch {}
893
- }
894
- }
895
- if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
896
- if (text.toLowerCase().includes('won')) return `${c.green}won${c.reset}`;
897
- if (text.toLowerCase().includes('lost')) return `${c.red}lost${c.reset}`;
898
- return text.substring(0, 60) || 'done';
899
- }
900
-
901
- // ── Deposit Handler ────────────────────────────────────────
902
- async handleDeposit(response) {
903
- if (!response) return null;
904
- const text = getFullText(response);
905
- const coins = parseCoins(text);
906
- if (text.toLowerCase().includes('deposited')) {
907
- return `deposited ${c.green}⏣ ${coins.toLocaleString()}${c.reset}`;
908
- }
909
- return text.substring(0, 60) || 'deposit done';
910
- }
911
-
912
- // ── Alert Handler ─────────────────────────────────────────
913
- async handleAlert(response) {
914
- if (!response) return null;
915
- debugMessage(response, 'ALERT');
916
- const buttons = getAllButtons(response);
917
- // Click any dismiss/accept/ok button
918
- const btn = buttons.find(b => !b.disabled && ['ok', 'dismiss', 'accept', 'got it', 'continue'].some(s => (b.label || '').toLowerCase().includes(s)));
919
- if (btn) {
920
- await humanDelay();
921
- try { await btn.click(); } catch {}
922
- return `alert dismissed`;
923
- }
924
- if (buttons.length > 0) {
925
- const first = buttons.find(b => !b.disabled);
926
- if (first) {
927
- await humanDelay();
928
- try { await first.click(); } catch {}
929
- }
930
- }
931
- return 'alert handled';
932
- }
933
-
934
716
  // ── Run Single Command ──────────────────────────────────────
717
+ // Each modular command handler sends the command, waits for response,
718
+ // handles Hold Tight / cooldowns / item-buying internally.
935
719
  async runCommand(cmdName, prefix) {
936
720
  let cmdString;
937
721
  const betAmount = this.account.bet_amount || 1000;
938
722
 
939
- // Build the command string
940
723
  switch (cmdName) {
941
724
  case 'dep max': cmdString = `${prefix} dep max`; break;
942
725
  case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
@@ -948,65 +731,85 @@ class AccountWorker {
948
731
  default: cmdString = `${prefix} ${cmdName}`;
949
732
  }
950
733
 
951
- this.log('cmd', `${c.white}${c.bold}${cmdString}${c.reset}`);
734
+ if (shutdownCalled || !this.running) return;
952
735
  this.stats.commands++;
953
736
 
737
+ const cmdOpts = {
738
+ channel: this.channel,
739
+ waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
740
+ client: this.client,
741
+ safeAnswers: cmdName === 'search' ? safeParseJSON(this.account.search_answers, []) :
742
+ cmdName === 'crime' ? safeParseJSON(this.account.crime_answers, []) : [],
743
+ betAmount,
744
+ accountId: this.account.id,
745
+ redis,
746
+ };
747
+
954
748
  try {
955
- await this.channel.send(cmdString);
956
- const response = await this.waitForDankMemer();
749
+ let cmdResult;
750
+ switch (cmdName) {
751
+ case 'beg': cmdResult = await commands.runBeg(cmdOpts); break;
752
+ case 'search': cmdResult = await commands.runSearch(cmdOpts); break;
753
+ case 'crime': cmdResult = await commands.runCrime(cmdOpts); break;
754
+ case 'hl': cmdResult = await commands.runHighLow(cmdOpts); break;
755
+ case 'pm': cmdResult = await commands.runPostMemes(cmdOpts); break;
756
+ case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
757
+ case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
758
+ case 'fish': cmdResult = await commands.runFish(cmdOpts); break;
759
+ case 'scratch': cmdResult = await commands.runScratch(cmdOpts); break;
760
+ case 'adventure': cmdResult = await commands.runAdventure(cmdOpts); break;
761
+ case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
762
+ case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
763
+ case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
764
+ case 'coinflip': cmdResult = await commands.runCoinflip(cmdOpts); break;
765
+ case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
766
+ case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
767
+ case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
768
+ case 'dep max': cmdResult = await commands.runDeposit(cmdOpts); break;
769
+ case 'alert': cmdResult = await commands.runAlert(cmdOpts); break;
770
+ default: cmdResult = await commands.runGeneric({ ...cmdOpts, cmdString, cmdName }); break;
771
+ }
957
772
 
958
- if (!response) {
959
- this.log('warn', `No response for ${cmdString}`);
960
- this.stats.errors++;
961
- await sendLog(this.username, cmdString, 'timeout', 'timeout');
773
+ const result = cmdResult.result || 'done';
774
+ const resultLower = result.toLowerCase();
775
+
776
+ // Rate limit detection
777
+ if (resultLower.includes('slow down') || resultLower.includes('rate limit') || resultLower.includes('too fast')) {
778
+ this.log('warn', `${c.yellow}⚠ Rate limited!${c.reset} Setting 60s global cooldown for ${this.username}`);
779
+ this.globalCooldownUntil = Date.now() + 60000;
780
+ await this.setCooldown(cmdName, 60);
962
781
  return;
963
782
  }
964
783
 
965
- // Check for cooldown messages
966
- const responseText = getFullText(response).toLowerCase();
967
- if (responseText.includes('already claimed') ||
968
- (responseText.includes('wait') && responseText.includes('before')) ||
969
- responseText.includes('cooldown') || responseText.includes('slow down') ||
970
- responseText.includes('come back') || responseText.includes('already used') ||
971
- responseText.includes('try again in')) {
972
- this.log('warn', `${cmdName} on cooldown`);
973
- // Try to parse remaining cooldown from text
974
- const timeMatch = responseText.match(/(\d+)\s*(second|minute|hour|day)/i);
975
- if (timeMatch) {
976
- let secs = parseInt(timeMatch[1]);
977
- const unit = timeMatch[2].toLowerCase();
978
- if (unit.startsWith('minute')) secs *= 60;
979
- if (unit.startsWith('hour')) secs *= 3600;
980
- if (unit.startsWith('day')) secs *= 86400;
981
- await this.setCooldown(cmdName, secs + 5);
982
- this.log('info', `Set cooldown for ${cmdName}: ${secs}s`);
983
- }
984
- await sendLog(this.username, cmdString, 'on cooldown', 'cooldown');
784
+ // Captcha/verification detection pause immediately
785
+ if (resultLower.includes('captcha') || resultLower.includes('verification') ||
786
+ resultLower.includes('are you human') || resultLower.includes("prove you're not a bot")) {
787
+ this.log('error', `${c.red}${c.bold}🚨 CAPTCHA/VERIFICATION DETECTED for ${this.username}!${c.reset}`);
788
+ this.log('error', `${c.red}URGENT: Worker PAUSED. Re-enable manually from the dashboard.${c.reset}`);
789
+ this.paused = true;
790
+ await sendLog(this.username, cmdString, 'CAPTCHA DETECTED — worker paused', 'error');
985
791
  return;
986
792
  }
987
793
 
988
- let result;
989
- switch (cmdName) {
990
- case 'search': result = await this.handleSearch(response); break;
991
- case 'crime': result = await this.handleCrime(response); break;
992
- case 'hl': result = await this.handleHighLow(response); break;
993
- case 'scratch': result = await this.handleScratch(response); break;
994
- case 'adventure': result = await this.handleAdventure(response); break;
995
- case 'blackjack': result = await this.handleBlackjack(response); break;
996
- case 'trivia': result = await this.handleTrivia(response); break;
997
- case 'work shift': result = await this.handleWorkShift(response); break;
998
- case 'coinflip':
999
- case 'roulette':
1000
- case 'slots':
1001
- case 'snakeeyes': result = await this.handleSimpleGamble(response); break;
1002
- case 'dep max': result = await this.handleDeposit(response); break;
1003
- case 'alert': result = await this.handleAlert(response); break;
1004
- default: result = await this.handleGenericCommand(response); break;
794
+ const earned = Math.max(0, cmdResult.coins || 0);
795
+ const spent = Math.max(0, cmdResult.lost || 0);
796
+ if (earned > 0) this.stats.coins += earned;
797
+ if (cmdResult.nextCooldownSec) await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
798
+
799
+ if (cmdResult.holdTightReason) {
800
+ const reason = cmdResult.holdTightReason;
801
+ this.log('warn', `Hold Tight caused by /${reason} — setting 35s cooldown on "${reason}"`);
802
+ const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
803
+ const mappedCmd = reasonMap[reason] || reason;
804
+ await this.setCooldown(mappedCmd, 35);
805
+ await this.setCooldown(cmdName, 35);
1005
806
  }
1006
807
 
1007
808
  this.stats.successes++;
1008
- this.log('success', `${c.dim}${cmdString}${c.reset} ${result || 'done'}`);
1009
- await sendLog(this.username, cmdString, result || 'done', 'success');
809
+ const shortResult = result.substring(0, 50).replace(/\n/g, ' ');
810
+ this.setStatus(`${cmdName} ${shortResult}`);
811
+ await sendLog(this.username, cmdString, result, 'success');
812
+ reportEarnings(this.account.id, this.username, earned, spent, cmdName);
1010
813
  } catch (err) {
1011
814
  this.stats.errors++;
1012
815
  this.log('error', `${cmdString} failed: ${err.message}`);
@@ -1032,130 +835,215 @@ class AccountWorker {
1032
835
  // Stats are shown in the live dashboard, no-op here
1033
836
  }
1034
837
 
838
+ // ── Command Map (shared across ticks, used to build the heap) ──
839
+ static COMMAND_MAP = [
840
+ { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
841
+ { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 4 },
842
+ { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 3 },
843
+ { key: 'cmd_pm', cmd: 'pm', cdKey: 'cd_pm', defaultCd: 20, priority: 3 },
844
+ { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 2 },
845
+ { key: 'cmd_hunt', cmd: 'hunt', cdKey: 'cd_hunt', defaultCd: 20, priority: 1 },
846
+ { key: 'cmd_dig', cmd: 'dig', cdKey: 'cd_dig', defaultCd: 20, priority: 1 },
847
+ { key: 'cmd_fish', cmd: 'fish', cdKey: 'cd_fish', defaultCd: 20, priority: 1 },
848
+ { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 2 },
849
+ { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 1 },
850
+ { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
851
+ { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
852
+ { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
853
+ { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
854
+ { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
855
+ { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
856
+ { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 1 },
857
+ { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 60, priority: 2 },
858
+ { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 8 },
859
+ { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 60, priority: 7 },
860
+ { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
861
+ { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
862
+ { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
863
+ { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 3600, priority: 8 },
864
+ { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 3600, priority: 6 },
865
+ { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 6 },
866
+ { key: 'cmd_alert', cmd: 'alert', cdKey: 'cd_alert', defaultCd: 300, priority: 9 },
867
+ ];
868
+
869
+ buildCommandQueue() {
870
+ const heap = new MinHeap();
871
+ const now = Date.now();
872
+ const enabled = AccountWorker.COMMAND_MAP.filter(
873
+ ci => this.account[ci.key] === true || this.account[ci.key] === 1
874
+ );
875
+ for (const info of enabled) {
876
+ heap.push({ cmd: info.cmd, nextRunAt: now, priority: info.priority, info });
877
+ }
878
+ return heap;
879
+ }
880
+
881
+ // ── Health Check: verify Discord client is still connected ──
882
+ async healthCheck() {
883
+ if (!this.running || shutdownCalled) return;
884
+ const now = Date.now();
885
+ if (now - this.lastHealthCheck < 60000) return;
886
+ this.lastHealthCheck = now;
887
+
888
+ if (!this.client.ws || this.client.ws.status !== 0) {
889
+ this.log('warn', `${c.yellow}⚠ Discord client disconnected. Attempting reconnect...${c.reset}`);
890
+ try {
891
+ this.client.destroy();
892
+ await this.client.login(this.account.discord_token);
893
+ this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
894
+ if (this.channel) {
895
+ this.log('success', 'Reconnected successfully.');
896
+ } else {
897
+ this.log('error', 'Reconnected but channel not found.');
898
+ }
899
+ } catch (e) {
900
+ this.log('error', `Reconnect failed: ${e.message}`);
901
+ }
902
+ }
903
+ }
904
+
1035
905
  // ── Main Non-Blocking Grind Scheduler ───────────────────────
1036
906
  async tick() {
1037
- if (!this.running) return;
907
+ if (!this.running || shutdownCalled) return;
908
+ if (this.paused) {
909
+ this.setStatus(`${c.red}${c.bold}PAUSED (captcha/verification)${c.reset}`);
910
+ this.tickTimeout = setTimeout(() => this.tick(), 5000);
911
+ return;
912
+ }
1038
913
  if (this.busy) {
1039
- this.tickTimeout = setTimeout(() => this.tick(), 1000);
914
+ this.tickTimeout = setTimeout(() => this.tick(), 2000);
915
+ return;
916
+ }
917
+
918
+ // Global rate-limit cooldown
919
+ const now = Date.now();
920
+ if (now < this.globalCooldownUntil) {
921
+ const waitSec = Math.ceil((this.globalCooldownUntil - now) / 1000);
922
+ this.setStatus(`${c.yellow}rate limited (${waitSec}s)${c.reset}`);
923
+ this.tickTimeout = setTimeout(() => this.tick(), 2000);
1040
924
  return;
1041
925
  }
1042
926
 
1043
- const commandMap = [
1044
- // Grinding commands (high frequency)
1045
- { key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
1046
- { key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 4 },
1047
- { key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 3 },
1048
- { key: 'cmd_pm', cmd: 'pm', cdKey: 'cd_pm', defaultCd: 20, priority: 3 },
1049
- { key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 2 },
1050
- { key: 'cmd_hunt', cmd: 'hunt', cdKey: 'cd_hunt', defaultCd: 20, priority: 1 },
1051
- { key: 'cmd_dig', cmd: 'dig', cdKey: 'cd_dig', defaultCd: 20, priority: 1 },
1052
- { key: 'cmd_fish', cmd: 'fish', cdKey: 'cd_fish', defaultCd: 20, priority: 1 },
1053
- { key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 10, priority: 2 },
1054
- { key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 1 },
1055
- // Gambling
1056
- { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
1057
- { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
1058
- { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
1059
- { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
1060
- { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
1061
- // Medium cooldown
1062
- { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
1063
- { key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 1 },
1064
- { key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 60, priority: 2 },
1065
- { key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 8 },
1066
- // Economy
1067
- { key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 60, priority: 7 },
1068
- // Long cooldown / special
1069
- { key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
1070
- { key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
1071
- { key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
1072
- { key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 3600, priority: 8 },
1073
- { key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 3600, priority: 6 },
1074
- { key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 6 },
1075
- // System
1076
- { key: 'cmd_alert', cmd: 'alert', cdKey: 'cd_alert', defaultCd: 300, priority: 9 },
1077
- ];
1078
-
1079
- const enabledCommands = commandMap.filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
1080
-
1081
- if (enabledCommands.length === 0) {
927
+ // Periodic health check
928
+ try { await this.healthCheck(); } catch (e) {
929
+ this.log('error', `Health check error: ${e.message}`);
930
+ }
931
+
932
+ // Rebuild queue if it doesn't exist or is empty (e.g. after config refresh)
933
+ if (!this.commandQueue || this.commandQueue.size === 0) {
934
+ this.commandQueue = this.buildCommandQueue();
935
+ }
936
+
937
+ if (this.commandQueue.size === 0) {
1082
938
  this.tickTimeout = setTimeout(() => this.tick(), 15000);
1083
939
  return;
1084
940
  }
1085
941
 
1086
- let nextCmdToRun = null;
1087
- let highestPriority = -1;
942
+ // Peek the top item — is it ready?
943
+ const top = this.commandQueue.peek();
944
+ if (top.nextRunAt > now) {
945
+ const waitMs = Math.min(top.nextRunAt - now, 2000);
946
+ this.setStatus(`${c.dim}waiting for cooldowns...${c.reset}`);
947
+ this.tickTimeout = setTimeout(() => this.tick(), waitMs);
948
+ return;
949
+ }
1088
950
 
1089
- for (const info of enabledCommands) {
1090
- const ready = await this.isCooldownReady(info.cmd);
1091
- if (ready && info.priority > highestPriority) {
1092
- nextCmdToRun = info;
1093
- highestPriority = info.priority;
1094
- }
951
+ // Pop the command, check Redis cooldown as a secondary gate
952
+ const item = this.commandQueue.pop();
953
+ const ready = await this.isCooldownReady(item.cmd);
954
+ if (!ready) {
955
+ const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
956
+ item.nextRunAt = now + cd * 1000;
957
+ this.commandQueue.push(item);
958
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
959
+ return;
1095
960
  }
1096
961
 
1097
- if (nextCmdToRun) {
1098
- this.busy = true;
1099
- const cd = (this.account[nextCmdToRun.cdKey] || nextCmdToRun.defaultCd);
1100
- const jitter = 1 + Math.random() * 3;
1101
- const totalWait = cd + jitter;
1102
-
1103
- // Optimistic lock via Redis
1104
- await this.setCooldown(nextCmdToRun.cmd, totalWait);
1105
-
1106
- // Global Jitter between commands
1107
- const now = Date.now();
1108
- const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1109
- const globalJitter = 1500 + Math.random() * 1500;
1110
- if (timeSinceLastCmd < globalJitter) {
1111
- await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1112
- }
962
+ this.busy = true;
963
+ const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
964
+ const jitter = 1 + Math.random() * 3;
965
+ const totalWait = cd + jitter;
1113
966
 
1114
- const prefix = this.account.use_slash ? '/' : 'pls';
1115
- this.setStatus(`${c.white}pls ${nextCmdToRun.cmd}${c.reset}`);
1116
- await this.runCommand(nextCmdToRun.cmd, prefix);
1117
-
1118
- this.lastCommandRun = Date.now();
1119
- // Re-apply cooldown based on actual finish time
1120
- await this.setCooldown(nextCmdToRun.cmd, totalWait);
1121
-
1122
- this.busy = false;
1123
- this.cycleCount++;
967
+ await this.setCooldown(item.cmd, totalWait);
1124
968
 
1125
- if (this.cycleCount > 0 && this.cycleCount % 10 === 0) this.printStats();
1126
- if (this.cycleCount > 0 && this.cycleCount % 20 === 0) {
1127
- this.busy = true;
1128
- await this.checkBalance();
1129
- this.busy = false;
1130
- }
969
+ // Global jitter between commands
970
+ const timeSinceLastCmd = now - (this.lastCommandRun || 0);
971
+ const globalJitter = 1500 + Math.random() * 1500;
972
+ if (timeSinceLastCmd < globalJitter) {
973
+ await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
974
+ }
1131
975
 
1132
- this.tickTimeout = setTimeout(() => this.tick(), 100);
1133
- } else {
1134
- this.setStatus(`${c.dim}waiting for cooldowns...${c.reset}`);
1135
- this.tickTimeout = setTimeout(() => this.tick(), 1000);
976
+ const prefix = this.account.use_slash ? '/' : 'pls';
977
+ this.setStatus(`${c.white}pls ${item.cmd}${c.reset}`);
978
+ await this.runCommand(item.cmd, prefix);
979
+
980
+ this.lastCommandRun = Date.now();
981
+ await this.setCooldown(item.cmd, totalWait);
982
+
983
+ // Push the command back into the heap with its next available time
984
+ item.nextRunAt = Date.now() + totalWait * 1000;
985
+ this.commandQueue.push(item);
986
+
987
+ this.busy = false;
988
+ this.cycleCount++;
989
+
990
+ if (this.cycleCount > 0 && this.cycleCount % 10 === 0) this.printStats();
991
+ if (this.cycleCount > 0 && this.cycleCount % 20 === 0) {
992
+ this.busy = true;
993
+ await this.checkBalance();
994
+ this.busy = false;
1136
995
  }
996
+
997
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1137
998
  }
1138
999
 
1139
1000
  async grindLoop() {
1140
1001
  if (this.running) return;
1141
1002
  this.running = true;
1142
1003
  this.busy = false;
1004
+ this.paused = false;
1143
1005
  this.cycleCount = 0;
1144
1006
  this.lastCommandRun = 0;
1007
+ this.commandQueue = this.buildCommandQueue();
1008
+ this.lastHealthCheck = Date.now();
1145
1009
 
1146
1010
  this.configInterval = setInterval(async () => {
1147
1011
  if (!this.running) return;
1148
1012
  await this.refreshConfig();
1013
+
1014
+ // Dashboard can un-pause a captcha-paused worker by re-activating it
1015
+ if (this.account.active && this.paused) {
1016
+ this.log('success', 'Account re-activated from dashboard! Resuming from captcha pause...');
1017
+ this.paused = false;
1018
+ this.busy = false;
1019
+ }
1020
+
1149
1021
  if (!this.account.active && !this.busy) {
1150
1022
  this.log('warn', 'Account deactivated from dashboard. Pausing...');
1151
1023
  this.busy = true;
1152
- } else if (this.account.active && this.busy) {
1024
+ } else if (this.account.active && this.busy && !this.paused) {
1153
1025
  this.log('success', 'Account re-activated! Resuming...');
1154
1026
  this.busy = false;
1155
1027
  }
1028
+
1029
+ // Rebuild the command queue on config refresh so newly enabled/disabled commands take effect
1030
+ this.commandQueue = this.buildCommandQueue();
1156
1031
  }, 15000);
1157
1032
 
1158
- this.tick();
1033
+ const safeTickLoop = async () => {
1034
+ try {
1035
+ await this.tick();
1036
+ } catch (err) {
1037
+ this.log('error', `Unhandled tick error: ${err.message}`);
1038
+ this.busy = false;
1039
+ await new Promise(r => setTimeout(r, 5000));
1040
+ if (this.running && !shutdownCalled) {
1041
+ this.tickTimeout = setTimeout(() => safeTickLoop(), 100);
1042
+ }
1043
+ }
1044
+ };
1045
+
1046
+ safeTickLoop();
1159
1047
  }
1160
1048
 
1161
1049
  async refreshConfig() {
@@ -1172,10 +1060,8 @@ class AccountWorker {
1172
1060
  }
1173
1061
 
1174
1062
  async start() {
1175
- if (!this.account.discord_token) { this.log('error', 'No Discord token.'); return; }
1176
- if (!this.account.channel_id) { this.log('error', 'No channel ID.'); return; }
1177
-
1178
- this.log('info', 'Connecting...');
1063
+ if (!this.account.discord_token) { this.log('error', 'No token'); return; }
1064
+ if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
1179
1065
 
1180
1066
  return new Promise((resolve) => {
1181
1067
  this.client.on('ready', async () => {
@@ -1188,41 +1074,33 @@ class AccountWorker {
1188
1074
  });
1189
1075
  } catch { /* silent */ }
1190
1076
 
1191
- this.log('success', `Logged in! ${c.dim}(${this.client.guilds.cache.size} servers)${c.reset}`);
1192
1077
  this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
1193
-
1194
1078
  if (!this.channel) {
1195
- this.log('error', `Channel ${this.account.channel_id} not found`);
1079
+ this.log('error', `Channel not found`);
1196
1080
  resolve(); return;
1197
1081
  }
1198
1082
 
1199
- this.log('info', `Channel: ${c.white}#${this.channel.name || this.account.channel_id}${c.reset}`);
1200
-
1201
- const ALL_CMDS = [
1202
- { key: 'cmd_hunt', label: 'hunt' }, { key: 'cmd_dig', label: 'dig' },
1203
- { key: 'cmd_fish', label: 'fish' }, { key: 'cmd_beg', label: 'beg' },
1204
- { key: 'cmd_search', label: 'search' }, { key: 'cmd_hl', label: 'hl' },
1205
- { key: 'cmd_crime', label: 'crime' }, { key: 'cmd_pm', label: 'pm' },
1206
- { key: 'cmd_daily', label: 'daily' }, { key: 'cmd_weekly', label: 'weekly' },
1207
- { key: 'cmd_monthly', label: 'monthly' }, { key: 'cmd_work', label: 'work' },
1208
- { key: 'cmd_stream', label: 'stream' }, { key: 'cmd_scratch', label: 'scratch' },
1209
- { key: 'cmd_adventure', label: 'adventure' }, { key: 'cmd_farm', label: 'farm' },
1210
- { key: 'cmd_tidy', label: 'tidy' }, { key: 'cmd_blackjack', label: 'bj' },
1211
- { key: 'cmd_cointoss', label: 'coinflip' }, { key: 'cmd_roulette', label: 'roulette' },
1212
- { key: 'cmd_slots', label: 'slots' }, { key: 'cmd_snakeeyes', label: 'snakeeyes' },
1213
- { key: 'cmd_trivia', label: 'trivia' }, { key: 'cmd_use', label: 'use' },
1214
- { key: 'cmd_deposit', label: 'deposit' }, { key: 'cmd_drops', label: 'drops' },
1215
- { key: 'cmd_alert', label: 'alert' },
1083
+ const enabledCmds = [
1084
+ { key: 'cmd_hunt', l: 'hunt' }, { key: 'cmd_dig', l: 'dig' },
1085
+ { key: 'cmd_fish', l: 'fish' }, { key: 'cmd_beg', l: 'beg' },
1086
+ { key: 'cmd_search', l: 'search' }, { key: 'cmd_hl', l: 'hl' },
1087
+ { key: 'cmd_crime', l: 'crime' }, { key: 'cmd_pm', l: 'pm' },
1088
+ { key: 'cmd_daily', l: 'daily' }, { key: 'cmd_weekly', l: 'weekly' },
1089
+ { key: 'cmd_monthly', l: 'monthly' }, { key: 'cmd_work', l: 'work' },
1090
+ { key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
1091
+ { key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
1092
+ { key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
1093
+ { key: 'cmd_cointoss', l: 'flip' }, { key: 'cmd_roulette', l: 'roul' },
1094
+ { key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
1095
+ { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1096
+ { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
1097
+ { key: 'cmd_alert', l: 'alert' },
1216
1098
  ].filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
1217
1099
 
1218
- const cmdStr = ALL_CMDS.map(ci => ci.label).join(', ');
1219
- this.log('info', `Commands: ${c.white}${cmdStr || 'none'}${c.reset}`);
1220
- if (this.account.bet_amount) {
1221
- this.log('info', `Bet amount: ${c.yellow}⏣ ${this.account.bet_amount.toLocaleString()}${c.reset}`);
1222
- }
1100
+ const cmdList = enabledCmds.map(ci => ci.l).join(' ');
1101
+ this.log('success', `${this.tag} ${c.dim}#${(this.channel.name || '?').substring(0, 12)}${c.reset} ${c.dim}[${enabledCmds.length} cmds: ${cmdList}]${c.reset}`);
1223
1102
 
1224
1103
  await this.checkBalance();
1225
- console.log('');
1226
1104
  this.grindLoop();
1227
1105
  resolve();
1228
1106
  });
@@ -1233,8 +1111,10 @@ class AccountWorker {
1233
1111
 
1234
1112
  stop() {
1235
1113
  this.running = false;
1114
+ this.paused = false;
1236
1115
  if (this.tickTimeout) clearTimeout(this.tickTimeout);
1237
1116
  if (this.configInterval) clearInterval(this.configInterval);
1117
+ this.commandQueue = null;
1238
1118
  try { this.client.destroy(); } catch {}
1239
1119
  }
1240
1120
  }
@@ -1245,21 +1125,37 @@ class AccountWorker {
1245
1125
 
1246
1126
  async function start(apiKey, apiUrl) {
1247
1127
  API_KEY = apiKey;
1248
- API_URL = apiUrl;
1128
+ API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
1129
+ REDIS_URL = process.env.REDIS_URL || '';
1249
1130
  initRedis();
1250
1131
 
1251
- // Clear screen & show big banner
1252
1132
  process.stdout.write('\x1b[2J\x1b[H');
1253
- console.log(BANNER);
1254
- console.log(` ${c.dim}v4.0${c.reset} ${c.dim}•${c.reset} ${c.white}30 Commands${c.reset} ${c.dim}•${c.reset} ${c.cyan}Redis Cooldowns${c.reset} ${c.dim}•${c.reset} ${c.green}Smart AI${c.reset}`);
1255
- console.log('');
1133
+ const tw = Math.min(process.stdout.columns || 80, 78);
1134
+ const bar = c.dim + '─'.repeat(tw) + c.reset;
1135
+
1136
+ console.log(colorBanner());
1137
+ console.log(
1138
+ ` ${rgb(139, 92, 246)}v4.1${c.reset}` +
1139
+ ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1140
+ ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1141
+ ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
1142
+ ` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Smart AI${c.reset}`
1143
+ );
1144
+ console.log(bar);
1145
+
1146
+ const checks = [];
1147
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
1148
+ checks.push(REDIS_URL ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${c.dim}○ Redis${c.reset}`);
1149
+ console.log(` ${checks.join(' ')}`);
1256
1150
 
1257
- log('info', `API: ${c.dim}${API_URL}${c.reset}`);
1258
- log('info', `Redis: ${c.dim}${REDIS_URL.replace(/:[^:]+@/, ':***@')}${c.reset}`);
1259
- log('info', 'Fetching accounts...');
1151
+ log('info', `${c.dim}Fetching accounts...${c.reset}`);
1260
1152
 
1261
1153
  const data = await fetchConfig();
1262
- if (!data) { log('error', 'Failed to fetch config.'); process.exit(1); }
1154
+ if (!data) {
1155
+ log('error', `Cannot connect to API`);
1156
+ log('error', `Pass ${c.white}--url${c.reset} with your Railway URL or set ${c.white}DANKGRINDER_URL${c.reset}`);
1157
+ process.exit(1);
1158
+ }
1263
1159
 
1264
1160
  const { accounts } = data;
1265
1161
  if (!accounts || accounts.length === 0) {
@@ -1267,7 +1163,10 @@ async function start(apiKey, apiUrl) {
1267
1163
  process.exit(1);
1268
1164
  }
1269
1165
 
1270
- log('success', `${c.bold}${accounts.length}${c.reset} active account(s) found`);
1166
+ checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
1167
+ process.stdout.write(c.cursorUp(1));
1168
+ process.stdout.write(c.clearLine + '\r');
1169
+ console.log(` ${checks.join(' ')}`);
1271
1170
  console.log('');
1272
1171
 
1273
1172
  for (let i = 0; i < accounts.length; i++) {
@@ -1279,26 +1178,31 @@ async function start(apiKey, apiUrl) {
1279
1178
  console.log('');
1280
1179
  startTime = Date.now();
1281
1180
  dashboardStarted = true;
1282
- process.stdout.write(c.hide); // Hide cursor for clean UI
1181
+ process.stdout.write(c.hide);
1283
1182
 
1284
- // Start the live dashboard refresh loop
1285
1183
  setInterval(() => renderDashboard(), 1000);
1286
- renderDashboard(); // Initial render
1184
+ renderDashboard();
1287
1185
 
1288
1186
  process.on('SIGINT', () => {
1289
- process.stdout.write(c.show); // Show cursor again
1187
+ shutdownCalled = true;
1188
+ process.stdout.write(c.show);
1290
1189
  dashboardStarted = false;
1291
- console.log('');
1292
- console.log('');
1293
- log('warn', 'Shutting down...');
1294
- console.log('');
1295
- for (const w of workers) {
1296
- const rate = w.stats.commands > 0 ? ((w.stats.successes / w.stats.commands) * 100).toFixed(0) : 0;
1297
- console.log(` ${w.color}${c.bold}${w.username}${c.reset} ${c.green}⏣ ${w.stats.coins.toLocaleString()}${c.reset} earned ${c.dim}│${c.reset} ${w.stats.commands} cmds ${c.dim}│${c.reset} ${rate}% success`);
1298
- w.stop();
1190
+ const sepBar = rgb(139, 92, 246) + c.bold + ''.repeat(tw) + c.reset;
1191
+ console.log('\n');
1192
+ console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
1193
+ console.log(sepBar);
1194
+ for (const wk of workers) {
1195
+ const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
1196
+ console.log(
1197
+ ` ${wk.color}${c.bold}${(wk.username || '?').padEnd(18)}${c.reset}` +
1198
+ ` ${rgb(52, 211, 153)}+⏣ ${wk.stats.coins.toLocaleString().padStart(8)}${c.reset}` +
1199
+ ` ${c.dim}${wk.stats.commands.toString().padStart(4)} cmds${c.reset}` +
1200
+ ` ${c.dim}${rate}% success${c.reset}`
1201
+ );
1202
+ wk.stop();
1299
1203
  }
1300
- console.log('');
1301
- console.log(` ${c.yellow}${c.bold}Total: ${totalCoins.toLocaleString()}${c.reset} earned in ${formatUptime()}`);
1204
+ console.log(sepBar);
1205
+ console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${totalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()}${c.reset}`);
1302
1206
  console.log('');
1303
1207
  setTimeout(() => process.exit(0), 2000);
1304
1208
  });