dankgrinder 3.0.0 → 4.0.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.
Files changed (2) hide show
  1. package/lib/grinder.js +856 -202
  2. package/package.json +3 -2
package/lib/grinder.js CHANGED
@@ -1,12 +1,21 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
+ const Redis = require('ioredis');
2
3
 
3
- // ── Terminal Colors ──────────────────────────────────────────
4
+ // ── Terminal Colors & ANSI ───────────────────────────────────
4
5
  const c = {
5
6
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
6
7
  green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m',
7
8
  magenta: '\x1b[35m', white: '\x1b[37m', blue: '\x1b[34m',
8
9
  bgGreen: '\x1b[42m', bgRed: '\x1b[41m', bgYellow: '\x1b[43m', bgCyan: '\x1b[46m',
9
- bgMagenta: '\x1b[45m', bgBlue: '\x1b[44m',
10
+ bgMagenta: '\x1b[45m', bgBlue: '\x1b[44m', bgWhite: '\x1b[47m',
11
+ // Cursor control
12
+ clearLine: '\x1b[2K',
13
+ cursorUp: (n) => `\x1b[${n}A`,
14
+ cursorTo: (col) => `\x1b[${col}G`,
15
+ hide: '\x1b[?25l',
16
+ show: '\x1b[?25h',
17
+ saveCursor: '\x1b7',
18
+ restoreCursor: '\x1b8',
10
19
  };
11
20
 
12
21
  const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
@@ -27,22 +36,140 @@ const SAFE_CRIME_OPTIONS = [
27
36
 
28
37
  let API_KEY = '';
29
38
  let API_URL = '';
39
+ let REDIS_URL = process.env.REDIS_URL || 'redis://default:qXcezFjDHlCDtakRUZJmvsEzHoBgdLHi@shortline.proxy.rlwy.net:32007';
40
+ let redis = null;
30
41
  const workers = [];
31
42
 
43
+ function initRedis() {
44
+ if (!redis) {
45
+ try {
46
+ redis = new Redis(REDIS_URL, { maxRetriesPerRequest: null, lazyConnect: true });
47
+ redis.connect().catch(() => {});
48
+ } catch (e) {
49
+ console.error('Redis connection failed', e);
50
+ }
51
+ }
52
+ }
53
+
54
+ // ── Live Dashboard State ─────────────────────────────────────
55
+ let dashboardLines = 0;
56
+ let dashboardStarted = false;
57
+ let totalBalance = 0;
58
+ let totalCoins = 0;
59
+ let totalCommands = 0;
60
+ 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
+ `;
78
+
79
+ function formatUptime() {
80
+ const s = Math.floor((Date.now() - startTime) / 1000);
81
+ const h = Math.floor(s / 3600);
82
+ const m = Math.floor((s % 3600) / 60);
83
+ const sec = s % 60;
84
+ return `${h}h ${m}m ${sec}s`;
85
+ }
86
+
87
+ function formatCoins(n) {
88
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
89
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
90
+ return n.toLocaleString();
91
+ }
92
+
93
+ function renderDashboard() {
94
+ if (!dashboardStarted || workers.length === 0) return;
95
+
96
+ // Compute totals
97
+ totalBalance = 0; totalCoins = 0; totalCommands = 0;
98
+ for (const w of workers) {
99
+ totalBalance += w.stats.balance || 0;
100
+ totalCoins += w.stats.coins || 0;
101
+ totalCommands += w.stats.commands || 0;
102
+ }
103
+
104
+ const lines = [];
105
+ const width = Math.min(process.stdout.columns || 80, 90);
106
+ const sep = c.dim + '─'.repeat(width) + c.reset;
107
+
108
+ // Header bar
109
+ lines.push(sep);
110
+ 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}`
115
+ );
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
+ );
131
+ }
132
+ lines.push(sep);
133
+
134
+ // Scrolling log section (last 6 events)
135
+ for (const entry of recentLogs) {
136
+ lines.push(` ${c.dim}${entry}${c.reset}`);
137
+ }
138
+ if (recentLogs.length > 0) lines.push(sep);
139
+
140
+ // Move cursor up to overwrite previous dashboard
141
+ if (dashboardLines > 0) {
142
+ process.stdout.write(c.cursorUp(dashboardLines));
143
+ }
144
+ for (const line of lines) {
145
+ process.stdout.write(c.clearLine + '\r' + line + '\n');
146
+ }
147
+ dashboardLines = lines.length;
148
+ }
149
+
32
150
  function log(type, msg, label) {
33
151
  const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
34
152
  const icons = {
35
- info: `${c.cyan}│${c.reset}`,
36
- success: `${c.green}✓${c.reset}`,
37
- error: `${c.red}✗${c.reset}`,
38
- warn: `${c.yellow}!${c.reset}`,
39
- cmd: `${c.magenta}▸${c.reset}`,
40
- coin: `${c.yellow}$${c.reset}`,
41
- buy: `${c.blue}♦${c.reset}`,
42
- bal: `${c.green}◈${c.reset}`,
153
+ info: `│`, success: `✓`, error: `✗`, warn: `!`,
154
+ cmd: `▸`, coin: `$`, buy: `♦`, bal: `◈`, debug: `⊙`,
43
155
  };
44
156
  const tag = label ? `${label} ` : '';
45
- console.log(` ${c.dim}${time}${c.reset} ${icons[type] || icons.info} ${tag}${msg}`);
157
+ const stripped = msg.replace(/\x1b\[[0-9;]*m/g, ''); // Strip ANSI for log buffer
158
+ if (dashboardStarted) {
159
+ // Add to scrolling log buffer
160
+ recentLogs.push(`${time} ${icons[type] || '│'} ${tag}${stripped}`.substring(0, 80));
161
+ while (recentLogs.length > MAX_LOGS) recentLogs.shift();
162
+ renderDashboard();
163
+ } else {
164
+ 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}`,
170
+ };
171
+ console.log(` ${c.dim}${time}${c.reset} ${colorIcons[type] || colorIcons.info} ${tag}${msg}`);
172
+ }
46
173
  }
47
174
 
48
175
  async function fetchConfig() {
@@ -56,12 +183,12 @@ async function fetchConfig() {
56
183
  } catch (err) { log('error', `Cannot reach API: ${err.message}`); return null; }
57
184
  }
58
185
 
59
- async function sendLog(command, response, status) {
186
+ async function sendLog(accountName, command, response, status) {
60
187
  try {
61
188
  await fetch(`${API_URL}/api/grinder/log`, {
62
189
  method: 'POST',
63
190
  headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
64
- body: JSON.stringify({ command, response, status }),
191
+ body: JSON.stringify({ account_name: accountName, command, response, status }),
65
192
  });
66
193
  } catch { /* silent */ }
67
194
  }
@@ -79,7 +206,6 @@ function safeParseJSON(str, fallback = []) {
79
206
  // ── Coin Parser ──────────────────────────────────────────────
80
207
  function parseCoins(text) {
81
208
  if (!text) return 0;
82
- // Match patterns like "⏣ 1,250" or "+1,250 coins" or "**1,250**"
83
209
  const patterns = [
84
210
  /[⏣💰]\s*[\d,]+/g,
85
211
  /\+\s*[\d,]+\s*coins?/gi,
@@ -88,6 +214,8 @@ function parseCoins(text) {
88
214
  /earned\s*[⏣💰]?\s*([\d,]+)/gi,
89
215
  /found\s*[⏣💰]?\s*([\d,]+)/gi,
90
216
  /got\s*[⏣💰]?\s*([\d,]+)/gi,
217
+ /won\s*[⏣💰]?\s*([\d,]+)/gi,
218
+ /gained\s*[⏣💰]?\s*([\d,]+)/gi,
91
219
  ];
92
220
  let total = 0;
93
221
  for (const pat of patterns) {
@@ -119,42 +247,66 @@ function getFullText(msg) {
119
247
  return text;
120
248
  }
121
249
 
122
- // ── Smart Button Picker ──────────────────────────────────────
250
+ function getAllButtons(msg) {
251
+ const buttons = [];
252
+ if (msg.components) {
253
+ for (const row of msg.components) {
254
+ if (row.components) {
255
+ for (const comp of row.components) {
256
+ if (comp.type === 2) buttons.push(comp);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ return buttons;
262
+ }
263
+
264
+ function getAllSelectMenus(msg) {
265
+ const menus = [];
266
+ if (msg.components) {
267
+ for (const row of msg.components) {
268
+ if (row.components) {
269
+ for (const comp of row.components) {
270
+ if (comp.type === 3) menus.push(comp);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ return menus;
276
+ }
277
+
123
278
  function pickSafeButton(buttons, safeList) {
124
279
  if (!buttons || buttons.length === 0) return null;
125
-
126
- // First check user-configured safe answers
127
280
  if (safeList && safeList.length > 0) {
128
281
  for (const btn of buttons) {
129
282
  const label = (btn.label || '').toLowerCase();
130
283
  if (safeList.some((s) => label.includes(s.toLowerCase()))) return btn;
131
284
  }
132
285
  }
133
-
134
- // Then check built-in safe list
135
286
  for (const btn of buttons) {
136
287
  const label = (btn.label || '').toLowerCase();
137
288
  if (SAFE_SEARCH_LOCATIONS.some((s) => label.includes(s))) return btn;
138
289
  if (SAFE_CRIME_OPTIONS.some((s) => label.includes(s))) return btn;
139
290
  }
140
-
141
- // Fallback: pick random
142
291
  const clickable = buttons.filter((b) => !b.disabled);
143
292
  return clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
144
293
  }
145
294
 
146
- function getAllButtons(msg) {
147
- const buttons = [];
148
- if (msg.components) {
149
- for (const row of msg.components) {
150
- if (row.components) {
151
- for (const comp of row.components) {
152
- if (comp.type === 2) buttons.push(comp);
153
- }
154
- }
295
+ // Debug: log all embed/component data for a message
296
+ function debugMessage(msg, label) {
297
+ if (!msg) return;
298
+ const text = getFullText(msg);
299
+ log('debug', `${label || 'MSG'}: ${c.dim}${text.substring(0, 120)}${c.reset}`);
300
+ const buttons = getAllButtons(msg);
301
+ if (buttons.length > 0) {
302
+ log('debug', ` Buttons: ${buttons.map(b => `[${b.label || b.emoji?.name || '?'}${b.disabled ? ' (disabled)' : ''}]`).join(' ')}`);
303
+ }
304
+ const menus = getAllSelectMenus(msg);
305
+ if (menus.length > 0) {
306
+ for (const m of menus) {
307
+ log('debug', ` SelectMenu: ${(m.options || []).map(o => o.label).join(', ')}`);
155
308
  }
156
309
  }
157
- return buttons;
158
310
  }
159
311
 
160
312
  // ══════════════════════════════════════════════════════════════
@@ -169,22 +321,29 @@ class AccountWorker {
169
321
  this.client = new Client();
170
322
  this.channel = null;
171
323
  this.running = false;
324
+ this.busy = false;
172
325
  this.username = account.label || `Account ${idx + 1}`;
173
-
174
- // Session stats
326
+ this.tickTimeout = null;
175
327
  this.stats = { coins: 0, commands: 0, successes: 0, errors: 0, balance: 0 };
328
+ this.lastRunTime = {};
329
+ this.cycleCount = 0;
330
+ this.lastCommandRun = 0;
176
331
  }
177
332
 
178
- get tag() {
179
- return `${this.color}${c.bold}${this.username}${c.reset}`;
180
- }
181
-
333
+ get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
182
334
  log(type, msg) { log(type, msg, this.tag); }
183
335
 
336
+ // Update the live dashboard status for this worker
337
+ setStatus(text) {
338
+ this.lastStatus = text;
339
+ renderDashboard();
340
+ }
341
+
184
342
  waitForDankMemer(timeout = 15000) {
185
343
  return new Promise((resolve) => {
186
344
  const timer = setTimeout(() => {
187
345
  this.client.removeListener('messageCreate', handler);
346
+ this.client.removeListener('messageUpdate', updateHandler);
188
347
  resolve(null);
189
348
  }, timeout);
190
349
  const self = this;
@@ -192,75 +351,224 @@ class AccountWorker {
192
351
  if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
193
352
  clearTimeout(timer);
194
353
  self.client.removeListener('messageCreate', handler);
354
+ self.client.removeListener('messageUpdate', updateHandler);
195
355
  resolve(msg);
196
356
  }
197
357
  }
358
+ function updateHandler(oldMsg, newMsg) {
359
+ 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);
363
+ resolve(newMsg);
364
+ }
365
+ }
198
366
  this.client.on('messageCreate', handler);
367
+ this.client.on('messageUpdate', updateHandler);
368
+ });
369
+ }
370
+
371
+ // Wait specifically for message edits (Dank Memer often edits messages)
372
+ waitForMessageUpdate(msgId, timeout = 10000) {
373
+ return new Promise((resolve) => {
374
+ const timer = setTimeout(() => {
375
+ this.client.removeListener('messageUpdate', handler);
376
+ resolve(null);
377
+ }, timeout);
378
+ const self = this;
379
+ function handler(oldMsg, newMsg) {
380
+ if (newMsg.id === msgId && newMsg.channel?.id === self.channel.id) {
381
+ clearTimeout(timer);
382
+ self.client.removeListener('messageUpdate', handler);
383
+ resolve(newMsg);
384
+ }
385
+ }
386
+ this.client.on('messageUpdate', handler);
199
387
  });
200
388
  }
201
389
 
202
390
  // ── Needs Item Detection ────────────────────────────────────
203
391
  needsItem(text) {
204
392
  const lower = text.toLowerCase();
205
- if (lower.includes("don't have a shovel") || lower.includes('need to go buy') && lower.includes('shovel'))
393
+ if (lower.includes("don't have a shovel") || (lower.includes('need') && lower.includes('shovel')) || lower.includes('you need a shovel'))
206
394
  return 'shovel';
207
- if (lower.includes("don't have a fishing") || lower.includes('need a fishing'))
395
+ if (lower.includes("don't have a fishing") || lower.includes('need a fishing') || lower.includes('you need a fishing pole'))
208
396
  return 'fishing pole';
209
- if (lower.includes("don't have a hunting rifle") || lower.includes('need a hunting rifle'))
397
+ if (lower.includes("don't have a hunting rifle") || lower.includes('need a hunting rifle') || lower.includes('you need a rifle'))
210
398
  return 'hunting rifle';
211
399
  return null;
212
400
  }
213
401
 
214
- async buyItem(itemName) {
215
- this.log('buy', `Buying ${c.bold}${itemName}${c.reset}...`);
216
- await this.channel.send(`pls buy ${itemName}`);
217
- const response = await this.waitForDankMemer(10000);
218
- if (response) {
219
- const text = getFullText(response);
220
- // Sometimes Dank Memer asks for confirmation
221
- const buttons = getAllButtons(response);
222
- if (buttons.length > 0) {
223
- const confirm = buttons.find((b) => {
224
- const label = (b.label || '').toLowerCase();
225
- return label.includes('yes') || label.includes('confirm') || label.includes('buy');
226
- });
227
- if (confirm) {
228
- await humanDelay();
229
- try { await confirm.click(); } catch {}
230
- await this.waitForDankMemer(5000);
402
+ async buyItem(itemName, quantity = 10) {
403
+ const MAX_RETRIES = 3;
404
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
405
+ this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
406
+ if (this.account.use_slash) {
407
+ await this.channel.sendSlash(DANK_MEMER_ID, 'shop', 'view').catch(() => this.channel.send('pls shop view'));
408
+ } else {
409
+ await this.channel.send('pls shop view');
410
+ }
411
+ let response = await this.waitForDankMemer(10000);
412
+ if (!response) {
413
+ this.log('warn', 'No response to shop view command.');
414
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
415
+ return false;
416
+ }
417
+
418
+ const responseText = getFullText(response).toLowerCase();
419
+ const hasShopComponents = (response.components || []).some(row =>
420
+ (row.components || []).some(comp => comp.type === 3 || (comp.type === 2 && comp.label && comp.label.toLowerCase().includes('buy')))
421
+ );
422
+
423
+ if (!hasShopComponents && (responseText.includes('lucky') || responseText.includes('event') || responseText.includes('for the rest of the day'))) {
424
+ this.log('warn', 'Got event response instead of shop. Retrying...');
425
+ await humanDelay(3000, 5000);
426
+ continue;
427
+ }
428
+ if (!hasShopComponents && responseText.includes('shop')) {
429
+ const shopUI = await this.waitForDankMemer(8000);
430
+ if (shopUI) response = shopUI;
431
+ }
432
+
433
+ // Navigate to Coin Shop
434
+ let coinShopMenuId = null;
435
+ let coinShopOption = null;
436
+ for (const row of response.components || []) {
437
+ for (const comp of row.components || []) {
438
+ if (comp.type === 3) {
439
+ const opt = (comp.options || []).find(o => o.label && o.label.includes('Coin Shop'));
440
+ if (opt) { coinShopMenuId = comp.customId; coinShopOption = opt; }
441
+ }
231
442
  }
232
443
  }
233
- if (text.toLowerCase().includes('bought') || text.toLowerCase().includes('purchased')) {
234
- this.log('success', `Bought ${c.bold}${itemName}${c.reset}!`);
235
- return true;
444
+
445
+ if (coinShopMenuId && coinShopOption) {
446
+ this.log('buy', 'Navigating to Coin Shop...');
447
+ try {
448
+ await response.selectMenu(coinShopMenuId, [coinShopOption.value]);
449
+ const updatedMsg = await this.waitForDankMemer(8000);
450
+ if (updatedMsg) response = updatedMsg;
451
+ } catch (e) {
452
+ this.log('error', `Failed to open Coin Shop: ${e.message}`);
453
+ }
454
+ }
455
+ await humanDelay(1000, 2000);
456
+
457
+ // Find Buy button
458
+ let buyBtn = null;
459
+ const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '');
460
+ for (const row of response.components || []) {
461
+ for (const comp of row.components || []) {
462
+ if (comp.type === 2 && comp.label && comp.label.toLowerCase().includes(searchName)) {
463
+ buyBtn = comp; break;
464
+ }
465
+ }
466
+ if (buyBtn) break;
467
+ }
468
+
469
+ if (!buyBtn) {
470
+ this.log('warn', `Could not find Buy button for ${itemName} (attempt ${attempt})`);
471
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
472
+ return false;
236
473
  }
474
+
475
+ this.log('buy', `Clicking Buy ${itemName}...`);
476
+ try { await buyBtn.click(); } catch (e) {
477
+ this.log('error', `Buy click failed: ${e.message}`);
478
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
479
+ return false;
480
+ }
481
+
482
+ // Handle Modal
483
+ const modal = await new Promise((resolve) => {
484
+ const timer = setTimeout(() => resolve(null), 8000);
485
+ const handler = (m) => {
486
+ clearTimeout(timer);
487
+ this.client.removeListener('interactionModalCreate', handler);
488
+ resolve(m);
489
+ };
490
+ this.client.on('interactionModalCreate', handler);
491
+ });
492
+
493
+ if (modal) {
494
+ this.log('buy', `Submitting quantity ${c.bold}${quantity}${c.reset} in modal...`);
495
+ try {
496
+ const quantityInputId = modal.components[0].components[0].customId;
497
+ await fetch('https://discord.com/api/v9/interactions', {
498
+ method: 'POST',
499
+ headers: { 'Authorization': this.client.token, 'Content-Type': 'application/json' },
500
+ body: JSON.stringify({
501
+ type: 5, application_id: modal.applicationId,
502
+ channel_id: this.channel.id, guild_id: this.channel.guild?.id,
503
+ data: {
504
+ id: modal.id, custom_id: modal.customId,
505
+ components: [{ type: 1, components: [{ type: 4, custom_id: quantityInputId, value: String(quantity) }] }]
506
+ },
507
+ session_id: this.client.sessionId || "dummy_session",
508
+ nonce: Date.now().toString()
509
+ })
510
+ });
511
+ } catch (e) {
512
+ this.log('error', `Modal submit failed: ${e.message}`);
513
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
514
+ return false;
515
+ }
516
+ } else {
517
+ this.log('warn', 'No modal appeared after clicking buy.');
518
+ if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
519
+ return false;
520
+ }
521
+
522
+ const confirmMsg = await this.waitForDankMemer(8000);
523
+ if (confirmMsg) {
524
+ const text = getFullText(confirmMsg).toLowerCase();
525
+ if (text.includes('bought') || text.includes('purchased') || text.includes('success')) {
526
+ this.log('success', `Bought ${c.bold}${quantity}x ${itemName}${c.reset}!`);
527
+ return true;
528
+ }
529
+ if (text.includes('not enough') || text.includes("can't afford") || text.includes('insufficient')) {
530
+ this.log('warn', `Not enough coins to buy ${itemName}.`);
531
+ return false;
532
+ }
533
+ }
534
+ this.log('success', `Submitted purchase for ${quantity}x ${itemName}.`);
535
+ return true;
237
536
  }
238
- this.log('warn', `Could not buy ${itemName}`);
537
+ this.log('error', `Failed to buy ${itemName} after ${MAX_RETRIES} attempts.`);
239
538
  return false;
240
539
  }
241
540
 
242
541
  // ── Check Balance ───────────────────────────────────────────
243
542
  async checkBalance() {
244
- await this.channel.send('pls bal');
543
+ const prefix = this.account.use_slash ? '/' : 'pls';
544
+ await this.channel.send(`${prefix} bal`);
245
545
  const response = await this.waitForDankMemer(10000);
246
546
  if (response) {
247
547
  const text = getFullText(response);
248
- // Try to parse wallet balance
249
548
  const walletMatch = text.match(/wallet[:\s]*[⏣💰]?\s*([\d,]+)/i);
250
549
  if (walletMatch) {
251
550
  this.stats.balance = parseInt(walletMatch[1].replace(/,/g, ''));
252
551
  this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${this.stats.balance.toLocaleString()}${c.reset}`);
552
+ try {
553
+ await fetch(`${API_URL}/api/grinder/status`, {
554
+ method: 'POST',
555
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
556
+ body: JSON.stringify({ account_id: this.account.id, balance: this.stats.balance }),
557
+ });
558
+ } catch { /* silent */ }
253
559
  }
254
560
  }
255
561
  }
256
562
 
257
- // ── Command Handlers ────────────────────────────────────────
563
+ // ══════════════════════════════════════════════════════════════
564
+ // ═ COMMAND HANDLERS
565
+ // ══════════════════════════════════════════════════════════════
258
566
 
259
567
  async handleSearch(response) {
260
568
  if (!response) return null;
569
+ debugMessage(response, 'SEARCH');
261
570
  const buttons = getAllButtons(response);
262
571
  if (buttons.length === 0) return getFullText(response).substring(0, 80);
263
-
264
572
  const userSafe = safeParseJSON(this.account.search_answers, []);
265
573
  const btn = pickSafeButton(buttons, userSafe);
266
574
  if (btn) {
@@ -271,10 +579,7 @@ class AccountWorker {
271
579
  if (followUp) {
272
580
  const text = getFullText(followUp);
273
581
  const coins = parseCoins(text);
274
- if (coins > 0) {
275
- this.stats.coins += coins;
276
- return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
277
- }
582
+ if (coins > 0) { this.stats.coins += coins; return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
278
583
  if (text.toLowerCase().includes('nothing')) return `${btn.label} → found nothing`;
279
584
  return `${btn.label} → done`;
280
585
  }
@@ -286,9 +591,9 @@ class AccountWorker {
286
591
 
287
592
  async handleCrime(response) {
288
593
  if (!response) return null;
594
+ debugMessage(response, 'CRIME');
289
595
  const buttons = getAllButtons(response);
290
596
  if (buttons.length === 0) return getFullText(response).substring(0, 80);
291
-
292
597
  const userSafe = safeParseJSON(this.account.crime_answers, []);
293
598
  const btn = pickSafeButton(buttons, userSafe);
294
599
  if (btn) {
@@ -299,10 +604,7 @@ class AccountWorker {
299
604
  if (followUp) {
300
605
  const text = getFullText(followUp);
301
606
  const coins = parseCoins(text);
302
- if (coins > 0) {
303
- this.stats.coins += coins;
304
- return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
305
- }
607
+ if (coins > 0) { this.stats.coins += coins; return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
306
608
  return `${btn.label} → done`;
307
609
  }
308
610
  return `Clicked: ${btn.label}`;
@@ -313,46 +615,34 @@ class AccountWorker {
313
615
 
314
616
  async handleHighLow(response) {
315
617
  if (!response) return null;
618
+ debugMessage(response, 'HIGHLOW');
316
619
  const text = getFullText(response);
317
620
  const match = text.match(/number.*?(\d+)/i) || text.match(/(\d+)/);
318
621
  const buttons = getAllButtons(response);
319
-
320
622
  if (match && buttons.length >= 2) {
321
623
  const num = parseInt(match[1]);
322
624
  await humanDelay();
323
625
  let targetBtn;
324
- if (num > 50) {
325
- targetBtn = buttons.find((b) => (b.label || '').toLowerCase().includes('lower')) || buttons[1];
326
- } else if (num < 50) {
327
- targetBtn = buttons.find((b) => (b.label || '').toLowerCase().includes('higher')) || buttons[0];
328
- } else {
329
- // Jackpot hint
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 {
330
629
  const jackpot = buttons.find((b) => (b.label || '').toLowerCase().includes('jackpot'));
331
630
  targetBtn = jackpot || buttons[Math.floor(Math.random() * 2)];
332
631
  }
333
-
334
632
  try {
335
633
  await targetBtn.click();
336
634
  const followUp = await this.waitForDankMemer(10000);
337
635
  if (followUp) {
338
636
  const ftText = getFullText(followUp);
339
637
  const coins = parseCoins(ftText);
340
- if (coins > 0) {
341
- this.stats.coins += coins;
342
- return `${num} → ${targetBtn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
343
- }
344
- // Multi-round: keep playing
638
+ if (coins > 0) { this.stats.coins += coins; return `${num} → ${targetBtn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
345
639
  const moreButtons = getAllButtons(followUp);
346
- if (moreButtons.length >= 2) {
347
- return await this.handleHighLow(followUp);
348
- }
640
+ if (moreButtons.length >= 2) return await this.handleHighLow(followUp);
349
641
  return `${num} → ${targetBtn.label}`;
350
642
  }
351
643
  return `${num} → ${targetBtn.label}`;
352
644
  } catch { return null; }
353
645
  }
354
-
355
- // Fallback: click random
356
646
  if (buttons.length > 0) {
357
647
  const btn = buttons[Math.floor(Math.random() * buttons.length)];
358
648
  await humanDelay();
@@ -362,12 +652,166 @@ class AccountWorker {
362
652
  return null;
363
653
  }
364
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.) ──
365
810
  async handleGenericCommand(response) {
366
811
  if (!response) return null;
812
+ debugMessage(response, 'GENERIC');
367
813
  const text = getFullText(response);
368
814
  const coins = parseCoins(text);
369
-
370
- // Check if we need to buy something
371
815
  const neededItem = this.needsItem(text);
372
816
  if (neededItem) {
373
817
  this.log('warn', `Missing ${c.bold}${neededItem}${c.reset} — auto-buying...`);
@@ -375,8 +819,7 @@ class AccountWorker {
375
819
  if (bought) return `auto-bought ${neededItem}`;
376
820
  return `need ${neededItem} (couldn't buy)`;
377
821
  }
378
-
379
- // Handle buttons if present
822
+ // Handle buttons if present (e.g., postmemes, farm choices)
380
823
  const buttons = getAllButtons(response);
381
824
  if (buttons.length > 0) {
382
825
  const btn = buttons.find((b) => !b.disabled) || buttons[0];
@@ -389,22 +832,122 @@ class AccountWorker {
389
832
  const fText = getFullText(followUp);
390
833
  const fCoins = parseCoins(fText);
391
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
+ }
392
844
  }
393
845
  } catch {}
394
846
  }
395
847
  }
396
-
397
- if (coins > 0) {
398
- this.stats.coins += coins;
399
- return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
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
+ }
400
865
  }
866
+ if (coins > 0) { this.stats.coins += coins; return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`; }
867
+ return text.substring(0, 60) || 'done';
868
+ }
401
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}`;
402
898
  return text.substring(0, 60) || 'done';
403
899
  }
404
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
+
405
934
  // ── Run Single Command ──────────────────────────────────────
406
935
  async runCommand(cmdName, prefix) {
407
- const cmdString = `${prefix} ${cmdName}`;
936
+ let cmdString;
937
+ const betAmount = this.account.bet_amount || 1000;
938
+
939
+ // Build the command string
940
+ switch (cmdName) {
941
+ case 'dep max': cmdString = `${prefix} dep max`; break;
942
+ case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
943
+ case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
944
+ case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
945
+ case 'slots': cmdString = `${prefix} slots ${betAmount}`; break;
946
+ case 'snakeeyes': cmdString = `${prefix} snakeeyes ${betAmount}`; break;
947
+ case 'work shift': cmdString = `${prefix} work shift`; break;
948
+ default: cmdString = `${prefix} ${cmdName}`;
949
+ }
950
+
408
951
  this.log('cmd', `${c.white}${c.bold}${cmdString}${c.reset}`);
409
952
  this.stats.commands++;
410
953
 
@@ -415,113 +958,206 @@ class AccountWorker {
415
958
  if (!response) {
416
959
  this.log('warn', `No response for ${cmdString}`);
417
960
  this.stats.errors++;
418
- await sendLog(cmdString, 'timeout', 'timeout');
961
+ await sendLog(this.username, cmdString, 'timeout', 'timeout');
962
+ return;
963
+ }
964
+
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');
419
985
  return;
420
986
  }
421
987
 
422
988
  let result;
423
989
  switch (cmdName) {
424
- case 'search': result = await this.handleSearch(response); break;
425
- case 'crime': result = await this.handleCrime(response); break;
426
- case 'hl': result = await this.handleHighLow(response); break;
427
- default: result = await this.handleGenericCommand(response); break;
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;
428
1005
  }
429
1006
 
430
1007
  this.stats.successes++;
431
1008
  this.log('success', `${c.dim}${cmdString}${c.reset} → ${result || 'done'}`);
432
- await sendLog(cmdString, result || 'done', 'success');
1009
+ await sendLog(this.username, cmdString, result || 'done', 'success');
433
1010
  } catch (err) {
434
1011
  this.stats.errors++;
435
1012
  this.log('error', `${cmdString} failed: ${err.message}`);
436
- await sendLog(cmdString, err.message, 'error');
1013
+ await sendLog(this.username, cmdString, err.message, 'error');
437
1014
  }
438
1015
  }
439
1016
 
440
- // ── Print Session Stats ─────────────────────────────────────
441
- printStats() {
442
- const rate = this.stats.commands > 0 ? ((this.stats.successes / this.stats.commands) * 100).toFixed(0) : 0;
443
- this.log('info', `${c.dim}──────────────────────────────────────${c.reset}`);
444
- this.log('coin', `Session: ${c.bold}${c.green}⏣ ${this.stats.coins.toLocaleString()}${c.reset} earned ${c.dim}│${c.reset} ${c.white}${this.stats.commands}${c.reset} cmds ${c.dim}│${c.reset} ${c.green}${rate}%${c.reset} success`);
445
- if (this.stats.balance > 0) {
446
- this.log('bal', `Wallet: ${c.bold}⏣ ${this.stats.balance.toLocaleString()}${c.reset}`);
447
- }
448
- this.log('info', `${c.dim}──────────────────────────────────────${c.reset}`);
1017
+ // ── Redis Cooldown Management ───────────────────────────────
1018
+ async setCooldown(cmdName, durationSeconds) {
1019
+ if (!redis) return;
1020
+ const key = `dkg:cd:${this.account.id}:${cmdName}`;
1021
+ await redis.set(key, '1', 'EX', Math.ceil(durationSeconds));
449
1022
  }
450
1023
 
451
- // ── Main Grind Loop ─────────────────────────────────────────
452
- async grindLoop() {
453
- if (this.running) return;
454
- this.running = true;
1024
+ async isCooldownReady(cmdName) {
1025
+ if (!redis) return true;
1026
+ const key = `dkg:cd:${this.account.id}:${cmdName}`;
1027
+ const val = await redis.get(key);
1028
+ return !val;
1029
+ }
455
1030
 
456
- const commandMap = {
457
- cmd_hunt: { cmd: 'hunt', cdKey: 'cd_hunt', defaultCd: 20 },
458
- cmd_dig: { cmd: 'dig', cdKey: 'cd_dig', defaultCd: 20 },
459
- cmd_beg: { cmd: 'beg', cdKey: 'cd_beg', defaultCd: 20 },
460
- cmd_search: { cmd: 'search', cdKey: 'cd_search', defaultCd: 20 },
461
- cmd_hl: { cmd: 'hl', cdKey: 'cd_hl', defaultCd: 20 },
462
- cmd_crime: { cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40 },
463
- cmd_pm: { cmd: 'pm', cdKey: 'cd_pm', defaultCd: 20 },
464
- };
1031
+ printStats() {
1032
+ // Stats are shown in the live dashboard, no-op here
1033
+ }
465
1034
 
466
- let cycleCount = 0;
467
-
468
- while (this.running) {
469
- // Re-fetch config from API every cycle for live dashboard control
470
- if (cycleCount > 0 && cycleCount % 3 === 0) {
471
- await this.refreshConfig();
472
- if (!this.account.active) {
473
- this.log('warn', 'Account deactivated from dashboard. Pausing...');
474
- while (!this.account.active && this.running) {
475
- await new Promise((r) => setTimeout(r, 10000));
476
- await this.refreshConfig();
477
- }
478
- if (!this.running) break;
479
- this.log('success', 'Account re-activated! Resuming...');
480
- }
481
- }
1035
+ // ── Main Non-Blocking Grind Scheduler ───────────────────────
1036
+ async tick() {
1037
+ if (!this.running) return;
1038
+ if (this.busy) {
1039
+ this.tickTimeout = setTimeout(() => this.tick(), 1000);
1040
+ return;
1041
+ }
482
1042
 
483
- const acc = this.account;
484
- const prefix = acc.use_slash ? '/' : 'pls';
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) {
1082
+ this.tickTimeout = setTimeout(() => this.tick(), 15000);
1083
+ return;
1084
+ }
485
1085
 
486
- const enabledCommands = Object.entries(commandMap)
487
- .filter(([key]) => acc[key] === true || acc[key] === 1)
488
- .map(([, val]) => val);
1086
+ let nextCmdToRun = null;
1087
+ let highestPriority = -1;
489
1088
 
490
- if (enabledCommands.length === 0) {
491
- this.log('warn', 'No commands enabled. Waiting 30s...');
492
- await new Promise((r) => setTimeout(r, 30000));
493
- continue;
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;
494
1094
  }
1095
+ }
495
1096
 
496
- for (const { cmd, cdKey, defaultCd } of enabledCommands) {
497
- if (!this.running) break;
498
- await this.runCommand(cmd, prefix);
499
-
500
- // Per-command cooldown with jitter
501
- const cd = (acc[cdKey] || defaultCd);
502
- const jitter = 1 + Math.random() * 3; // 1-4s random jitter for safety
503
- const totalWait = cd + jitter;
504
- this.log('info', `${c.dim}⏳ cd ${cd}s + ${jitter.toFixed(1)}s jitter = ${totalWait.toFixed(1)}s${c.reset}`);
505
- await new Promise((r) => setTimeout(r, totalWait * 1000));
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));
506
1112
  }
507
1113
 
508
- cycleCount++;
509
-
510
- // Every 5 cycles: print stats + check balance
511
- if (cycleCount % 5 === 0) {
512
- this.printStats();
513
- await humanDelay(500, 1500);
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++;
1124
+
1125
+ if (this.cycleCount > 0 && this.cycleCount % 10 === 0) this.printStats();
1126
+ if (this.cycleCount > 0 && this.cycleCount % 20 === 0) {
1127
+ this.busy = true;
514
1128
  await this.checkBalance();
515
- await humanDelay(2000, 4000);
1129
+ this.busy = false;
516
1130
  }
517
1131
 
518
- const cycleDelay = 2 + Math.random() * 3;
519
- this.log('info', `${c.dim}Cycle ${cycleCount} done. Next in ${cycleDelay.toFixed(1)}s${c.reset}`);
520
- await new Promise((r) => setTimeout(r, cycleDelay * 1000));
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);
521
1136
  }
522
1137
  }
523
1138
 
524
- // ── Re-fetch Config from API ─────────────────────────────────
1139
+ async grindLoop() {
1140
+ if (this.running) return;
1141
+ this.running = true;
1142
+ this.busy = false;
1143
+ this.cycleCount = 0;
1144
+ this.lastCommandRun = 0;
1145
+
1146
+ this.configInterval = setInterval(async () => {
1147
+ if (!this.running) return;
1148
+ await this.refreshConfig();
1149
+ if (!this.account.active && !this.busy) {
1150
+ this.log('warn', 'Account deactivated from dashboard. Pausing...');
1151
+ this.busy = true;
1152
+ } else if (this.account.active && this.busy) {
1153
+ this.log('success', 'Account re-activated! Resuming...');
1154
+ this.busy = false;
1155
+ }
1156
+ }, 15000);
1157
+
1158
+ this.tick();
1159
+ }
1160
+
525
1161
  async refreshConfig() {
526
1162
  try {
527
1163
  const res = await fetch(`${API_URL}/api/grinder/status`, {
@@ -535,19 +1171,15 @@ class AccountWorker {
535
1171
  } catch { /* silent */ }
536
1172
  }
537
1173
 
538
- // ── Start Worker ────────────────────────────────────────────
539
1174
  async start() {
540
1175
  if (!this.account.discord_token) { this.log('error', 'No Discord token.'); return; }
541
1176
  if (!this.account.channel_id) { this.log('error', 'No channel ID.'); return; }
542
1177
 
543
- this.log('info', `Connecting...`);
1178
+ this.log('info', 'Connecting...');
544
1179
 
545
1180
  return new Promise((resolve) => {
546
1181
  this.client.on('ready', async () => {
547
- // Use the actual Discord username
548
1182
  this.username = this.client.user.tag || this.username;
549
-
550
- // Report username back to API
551
1183
  try {
552
1184
  await fetch(`${API_URL}/api/grinder/status`, {
553
1185
  method: 'POST',
@@ -566,24 +1198,31 @@ class AccountWorker {
566
1198
 
567
1199
  this.log('info', `Channel: ${c.white}#${this.channel.name || this.account.channel_id}${c.reset}`);
568
1200
 
569
- // Show per-command cooldowns
570
- const cmdInfo = [
571
- { key: 'cmd_hunt', cd: 'cd_hunt', label: 'hunt' },
572
- { key: 'cmd_dig', cd: 'cd_dig', label: 'dig' },
573
- { key: 'cmd_beg', cd: 'cd_beg', label: 'beg' },
574
- { key: 'cmd_search', cd: 'cd_search', label: 'search' },
575
- { key: 'cmd_hl', cd: 'cd_hl', label: 'hl' },
576
- { key: 'cmd_crime', cd: 'cd_crime', label: 'crime' },
577
- { key: 'cmd_pm', cd: 'cd_pm', label: 'pm' },
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' },
578
1216
  ].filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
579
1217
 
580
- const cmdStr = cmdInfo.map((ci) => `${ci.label}(${this.account[ci.cd] || 20}s)`).join(', ');
1218
+ const cmdStr = ALL_CMDS.map(ci => ci.label).join(', ');
581
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
+ }
582
1223
 
583
- // Initial balance check
584
1224
  await this.checkBalance();
585
1225
  console.log('');
586
-
587
1226
  this.grindLoop();
588
1227
  resolve();
589
1228
  });
@@ -594,6 +1233,8 @@ class AccountWorker {
594
1233
 
595
1234
  stop() {
596
1235
  this.running = false;
1236
+ if (this.tickTimeout) clearTimeout(this.tickTimeout);
1237
+ if (this.configInterval) clearInterval(this.configInterval);
597
1238
  try { this.client.destroy(); } catch {}
598
1239
  }
599
1240
  }
@@ -605,17 +1246,16 @@ class AccountWorker {
605
1246
  async function start(apiKey, apiUrl) {
606
1247
  API_KEY = apiKey;
607
1248
  API_URL = apiUrl;
1249
+ initRedis();
608
1250
 
609
- console.log('');
610
- console.log(` ${c.magenta}${c.bold}┌─────────────────────────────────────────┐${c.reset}`);
611
- console.log(` ${c.magenta}${c.bold}│${c.reset} ${c.magenta}${c.bold}│${c.reset}`);
612
- console.log(` ${c.magenta}${c.bold}│${c.reset} ${c.white}${c.bold} DankGrinder${c.reset} ${c.dim}v3.0${c.reset} ${c.magenta}${c.bold}│${c.reset}`);
613
- console.log(` ${c.magenta}${c.bold}│${c.reset} ${c.dim}Per-Cmd CD • Live Control • Smart${c.reset} ${c.magenta}${c.bold}│${c.reset}`);
614
- console.log(` ${c.magenta}${c.bold}│${c.reset} ${c.magenta}${c.bold}│${c.reset}`);
615
- console.log(` ${c.magenta}${c.bold}└─────────────────────────────────────────┘${c.reset}`);
1251
+ // Clear screen & show big banner
1252
+ 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}`);
616
1255
  console.log('');
617
1256
 
618
1257
  log('info', `API: ${c.dim}${API_URL}${c.reset}`);
1258
+ log('info', `Redis: ${c.dim}${REDIS_URL.replace(/:[^:]+@/, ':***@')}${c.reset}`);
619
1259
  log('info', 'Fetching accounts...');
620
1260
 
621
1261
  const data = await fetchConfig();
@@ -636,16 +1276,30 @@ async function start(apiKey, apiUrl) {
636
1276
  await worker.start();
637
1277
  }
638
1278
 
639
- log('info', `${c.bold}All workers running.${c.reset} ${c.dim}Ctrl+C to stop.${c.reset}`);
640
1279
  console.log('');
1280
+ startTime = Date.now();
1281
+ dashboardStarted = true;
1282
+ process.stdout.write(c.hide); // Hide cursor for clean UI
1283
+
1284
+ // Start the live dashboard refresh loop
1285
+ setInterval(() => renderDashboard(), 1000);
1286
+ renderDashboard(); // Initial render
641
1287
 
642
1288
  process.on('SIGINT', () => {
1289
+ process.stdout.write(c.show); // Show cursor again
1290
+ dashboardStarted = false;
1291
+ console.log('');
643
1292
  console.log('');
644
1293
  log('warn', 'Shutting down...');
1294
+ console.log('');
645
1295
  for (const w of workers) {
646
- w.printStats();
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`);
647
1298
  w.stop();
648
1299
  }
1300
+ console.log('');
1301
+ console.log(` ${c.yellow}${c.bold}Total: ⏣ ${totalCoins.toLocaleString()}${c.reset} earned in ${formatUptime()}`);
1302
+ console.log('');
649
1303
  setTimeout(() => process.exit(0), 2000);
650
1304
  });
651
1305
  }