dankgrinder 1.0.1 → 3.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 +451 -185
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -1,35 +1,48 @@
1
1
  const { Client } = require('discord.js-selfbot-v13');
2
2
 
3
+ // ── Terminal Colors ──────────────────────────────────────────
3
4
  const c = {
4
- reset: '\x1b[0m',
5
- green: '\x1b[32m',
6
- red: '\x1b[31m',
7
- yellow: '\x1b[33m',
8
- cyan: '\x1b[36m',
9
- magenta: '\x1b[35m',
10
- dim: '\x1b[2m',
11
- bold: '\x1b[1m',
12
- white: '\x1b[37m',
13
- blue: '\x1b[34m',
5
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
6
+ green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m',
7
+ magenta: '\x1b[35m', white: '\x1b[37m', blue: '\x1b[34m',
8
+ bgGreen: '\x1b[42m', bgRed: '\x1b[41m', bgYellow: '\x1b[43m', bgCyan: '\x1b[46m',
9
+ bgMagenta: '\x1b[45m', bgBlue: '\x1b[44m',
14
10
  };
15
11
 
16
- const ACCOUNT_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
12
+ const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
13
+ const DANK_MEMER_ID = '270904126974590976';
14
+
15
+ // ── Safe options for search/crime ──────────────────────────
16
+ const SAFE_SEARCH_LOCATIONS = [
17
+ 'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
18
+ 'couch', 'pantry', 'fridge', 'kitchen', 'bathroom', 'attic',
19
+ 'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
20
+ 'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
21
+ ];
22
+
23
+ const SAFE_CRIME_OPTIONS = [
24
+ 'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
25
+ 'money laundering', 'tax fraud', 'insurance fraud', 'scam',
26
+ ];
17
27
 
18
28
  let API_KEY = '';
19
29
  let API_URL = '';
20
30
  const workers = [];
21
31
 
22
32
  function log(type, msg, label) {
23
- const time = new Date().toLocaleTimeString();
24
- const prefix = {
25
- info: `${c.cyan}ℹ${c.reset}`,
33
+ const time = new Date().toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit', second: '2-digit' });
34
+ const icons = {
35
+ info: `${c.cyan}│${c.reset}`,
26
36
  success: `${c.green}✓${c.reset}`,
27
37
  error: `${c.red}✗${c.reset}`,
28
- warn: `${c.yellow}⚠${c.reset}`,
38
+ warn: `${c.yellow}!${c.reset}`,
29
39
  cmd: `${c.magenta}▸${c.reset}`,
40
+ coin: `${c.yellow}$${c.reset}`,
41
+ buy: `${c.blue}♦${c.reset}`,
42
+ bal: `${c.green}◈${c.reset}`,
30
43
  };
31
- const tag = label ? `${c.bold}[${label}]${c.reset} ` : '';
32
- console.log(` ${c.dim}${time}${c.reset} ${prefix[type] || prefix.info} ${tag}${msg}`);
44
+ const tag = label ? `${label} ` : '';
45
+ console.log(` ${c.dim}${time}${c.reset} ${icons[type] || icons.info} ${tag}${msg}`);
33
46
  }
34
47
 
35
48
  async function fetchConfig() {
@@ -38,178 +51,362 @@ async function fetchConfig() {
38
51
  headers: { Authorization: `Bearer ${API_KEY}` },
39
52
  });
40
53
  const data = await res.json();
41
- if (data.error) {
42
- log('error', `Config fetch failed: ${data.error}`);
43
- return null;
44
- }
54
+ if (data.error) { log('error', `Config fetch failed: ${data.error}`); return null; }
45
55
  return data;
46
- } catch (err) {
47
- log('error', `Cannot reach API: ${err.message}`);
48
- return null;
49
- }
56
+ } catch (err) { log('error', `Cannot reach API: ${err.message}`); return null; }
50
57
  }
51
58
 
52
59
  async function sendLog(command, response, status) {
53
60
  try {
54
61
  await fetch(`${API_URL}/api/grinder/log`, {
55
62
  method: 'POST',
56
- headers: {
57
- Authorization: `Bearer ${API_KEY}`,
58
- 'Content-Type': 'application/json',
59
- },
63
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
60
64
  body: JSON.stringify({ command, response, status }),
61
65
  });
62
- } catch {
63
- // silent fail for logging
64
- }
66
+ } catch { /* silent */ }
65
67
  }
66
68
 
67
69
  function randomDelay(min, max) {
68
- const ms = (Math.random() * (max - min) + min) * 1000;
69
- return new Promise((resolve) => setTimeout(resolve, ms));
70
+ return new Promise((r) => setTimeout(r, (Math.random() * (max - min) + min) * 1000));
71
+ }
72
+ function humanDelay(min = 300, max = 800) {
73
+ return new Promise((r) => setTimeout(r, min + Math.random() * (max - min)));
74
+ }
75
+ function safeParseJSON(str, fallback = []) {
76
+ try { return JSON.parse(str || '[]'); } catch { return fallback; }
70
77
  }
71
78
 
72
- function humanDelay() {
73
- return new Promise((resolve) => setTimeout(resolve, 200 + Math.random() * 600));
79
+ // ── Coin Parser ──────────────────────────────────────────────
80
+ function parseCoins(text) {
81
+ if (!text) return 0;
82
+ // Match patterns like "⏣ 1,250" or "+1,250 coins" or "**1,250**"
83
+ const patterns = [
84
+ /[⏣💰]\s*[\d,]+/g,
85
+ /\+\s*[\d,]+\s*coins?/gi,
86
+ /\*\*([\d,]+)\*\*/g,
87
+ /take\s*[⏣💰]?\s*([\d,]+)/gi,
88
+ /earned\s*[⏣💰]?\s*([\d,]+)/gi,
89
+ /found\s*[⏣💰]?\s*([\d,]+)/gi,
90
+ /got\s*[⏣💰]?\s*([\d,]+)/gi,
91
+ ];
92
+ let total = 0;
93
+ for (const pat of patterns) {
94
+ const matches = text.match(pat);
95
+ if (matches) {
96
+ for (const m of matches) {
97
+ const numStr = m.replace(/[^\d]/g, '');
98
+ if (numStr) total = Math.max(total, parseInt(numStr));
99
+ }
100
+ }
101
+ }
102
+ return total;
74
103
  }
75
104
 
76
- function safeParseJSON(str, fallback) {
77
- try {
78
- return JSON.parse(str || '[]');
79
- } catch {
80
- return fallback;
105
+ function getFullText(msg) {
106
+ let text = msg.content || '';
107
+ if (msg.embeds) {
108
+ for (const embed of msg.embeds) {
109
+ if (embed.title) text += ' ' + embed.title;
110
+ if (embed.description) text += ' ' + embed.description;
111
+ if (embed.fields) {
112
+ for (const f of embed.fields) {
113
+ text += ' ' + (f.name || '') + ' ' + (f.value || '');
114
+ }
115
+ }
116
+ if (embed.footer?.text) text += ' ' + embed.footer.text;
117
+ }
118
+ }
119
+ return text;
120
+ }
121
+
122
+ // ── Smart Button Picker ──────────────────────────────────────
123
+ function pickSafeButton(buttons, safeList) {
124
+ if (!buttons || buttons.length === 0) return null;
125
+
126
+ // First check user-configured safe answers
127
+ if (safeList && safeList.length > 0) {
128
+ for (const btn of buttons) {
129
+ const label = (btn.label || '').toLowerCase();
130
+ if (safeList.some((s) => label.includes(s.toLowerCase()))) return btn;
131
+ }
132
+ }
133
+
134
+ // Then check built-in safe list
135
+ for (const btn of buttons) {
136
+ const label = (btn.label || '').toLowerCase();
137
+ if (SAFE_SEARCH_LOCATIONS.some((s) => label.includes(s))) return btn;
138
+ if (SAFE_CRIME_OPTIONS.some((s) => label.includes(s))) return btn;
139
+ }
140
+
141
+ // Fallback: pick random
142
+ const clickable = buttons.filter((b) => !b.disabled);
143
+ return clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
144
+ }
145
+
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
+ }
155
+ }
81
156
  }
157
+ return buttons;
82
158
  }
83
159
 
84
- // ── Worker: one per Discord account ──────────────────────────────
160
+ // ══════════════════════════════════════════════════════════════
161
+ // ═ Worker: one per Discord account
162
+ // ══════════════════════════════════════════════════════════════
85
163
 
86
164
  class AccountWorker {
87
165
  constructor(account, idx) {
88
166
  this.account = account;
89
- this.label = account.label || `Account ${idx + 1}`;
90
- this.color = ACCOUNT_COLORS[idx % ACCOUNT_COLORS.length];
167
+ this.idx = idx;
168
+ this.color = WORKER_COLORS[idx % WORKER_COLORS.length];
91
169
  this.client = new Client();
92
170
  this.channel = null;
93
171
  this.running = false;
172
+ this.username = account.label || `Account ${idx + 1}`;
173
+
174
+ // Session stats
175
+ this.stats = { coins: 0, commands: 0, successes: 0, errors: 0, balance: 0 };
94
176
  }
95
177
 
96
- log(type, msg) {
97
- log(type, msg, `${this.color}${this.label}${c.reset}`);
178
+ get tag() {
179
+ return `${this.color}${c.bold}${this.username}${c.reset}`;
98
180
  }
99
181
 
182
+ log(type, msg) { log(type, msg, this.tag); }
183
+
100
184
  waitForDankMemer(timeout = 15000) {
101
185
  return new Promise((resolve) => {
102
186
  const timer = setTimeout(() => {
103
187
  this.client.removeListener('messageCreate', handler);
104
188
  resolve(null);
105
189
  }, timeout);
106
-
107
190
  const self = this;
108
191
  function handler(msg) {
109
- if (msg.author.id === '270904126974590976' && msg.channel.id === self.channel.id) {
192
+ if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
110
193
  clearTimeout(timer);
111
194
  self.client.removeListener('messageCreate', handler);
112
195
  resolve(msg);
113
196
  }
114
197
  }
115
-
116
198
  this.client.on('messageCreate', handler);
117
199
  });
118
200
  }
119
201
 
120
- async handleInteraction(msg) {
121
- if (!msg) return null;
122
-
123
- if (msg.components && msg.components.length > 0) {
124
- for (const row of msg.components) {
125
- if (row.components) {
126
- for (const component of row.components) {
127
- if (component.type === 2) {
128
- const label = component.label?.toLowerCase() || '';
129
- const searchAnswers = safeParseJSON(this.account.search_answers, []);
130
- const crimeAnswers = safeParseJSON(this.account.crime_answers, []);
131
- const allSafe = [...searchAnswers, ...crimeAnswers];
132
-
133
- if (allSafe.length > 0) {
134
- const match = allSafe.find((a) => label.includes(a.toLowerCase()));
135
- if (match) {
136
- await humanDelay();
137
- try {
138
- await component.click();
139
- return `Clicked: ${label}`;
140
- } catch {
141
- return null;
142
- }
143
- }
144
- }
145
- }
146
- }
202
+ // ── Needs Item Detection ────────────────────────────────────
203
+ needsItem(text) {
204
+ const lower = text.toLowerCase();
205
+ if (lower.includes("don't have a shovel") || lower.includes('need to go buy') && lower.includes('shovel'))
206
+ return 'shovel';
207
+ if (lower.includes("don't have a fishing") || lower.includes('need a fishing'))
208
+ return 'fishing pole';
209
+ if (lower.includes("don't have a hunting rifle") || lower.includes('need a hunting rifle'))
210
+ return 'hunting rifle';
211
+ return null;
212
+ }
147
213
 
148
- const buttons = row.components.filter((comp) => comp.type === 2 && !comp.disabled);
149
- if (buttons.length > 0) {
150
- const btn = buttons[Math.floor(Math.random() * buttons.length)];
151
- await humanDelay();
152
- try {
153
- await btn.click();
154
- return `Clicked: ${btn.label || 'button'}`;
155
- } catch {
156
- return null;
157
- }
158
- }
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);
159
231
  }
160
232
  }
233
+ if (text.toLowerCase().includes('bought') || text.toLowerCase().includes('purchased')) {
234
+ this.log('success', `Bought ${c.bold}${itemName}${c.reset}!`);
235
+ return true;
236
+ }
161
237
  }
238
+ this.log('warn', `Could not buy ${itemName}`);
239
+ return false;
240
+ }
162
241
 
163
- return msg.content?.substring(0, 100) || 'Response received';
242
+ // ── Check Balance ───────────────────────────────────────────
243
+ async checkBalance() {
244
+ await this.channel.send('pls bal');
245
+ const response = await this.waitForDankMemer(10000);
246
+ if (response) {
247
+ const text = getFullText(response);
248
+ // Try to parse wallet balance
249
+ const walletMatch = text.match(/wallet[:\s]*[⏣💰]?\s*([\d,]+)/i);
250
+ if (walletMatch) {
251
+ this.stats.balance = parseInt(walletMatch[1].replace(/,/g, ''));
252
+ this.log('bal', `Wallet: ${c.bold}${c.green}⏣ ${this.stats.balance.toLocaleString()}${c.reset}`);
253
+ }
254
+ }
164
255
  }
165
256
 
166
- async handleHighLow(msg) {
167
- if (!msg) return null;
168
-
169
- if (msg.embeds && msg.embeds.length > 0) {
170
- const embed = msg.embeds[0];
171
- const desc = embed.description || '';
172
- const match = desc.match(/(\d+)/);
173
- if (match) {
174
- const num = parseInt(match[1]);
175
- const buttons = [];
176
- if (msg.components) {
177
- for (const row of msg.components) {
178
- if (row.components) {
179
- for (const comp of row.components) {
180
- if (comp.type === 2) buttons.push(comp);
181
- }
182
- }
257
+ // ── Command Handlers ────────────────────────────────────────
258
+
259
+ async handleSearch(response) {
260
+ if (!response) return null;
261
+ const buttons = getAllButtons(response);
262
+ if (buttons.length === 0) return getFullText(response).substring(0, 80);
263
+
264
+ const userSafe = safeParseJSON(this.account.search_answers, []);
265
+ const btn = pickSafeButton(buttons, userSafe);
266
+ if (btn) {
267
+ await humanDelay();
268
+ try {
269
+ await btn.click();
270
+ const followUp = await this.waitForDankMemer(10000);
271
+ if (followUp) {
272
+ const text = getFullText(followUp);
273
+ const coins = parseCoins(text);
274
+ if (coins > 0) {
275
+ this.stats.coins += coins;
276
+ return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
183
277
  }
278
+ if (text.toLowerCase().includes('nothing')) return `${btn.label} → found nothing`;
279
+ return `${btn.label} → done`;
184
280
  }
281
+ return `Clicked: ${btn.label}`;
282
+ } catch { return null; }
283
+ }
284
+ return 'No safe option found';
285
+ }
185
286
 
186
- if (buttons.length >= 2) {
187
- await humanDelay();
188
- let targetBtn;
189
- if (num > 50) {
190
- targetBtn = buttons.find((b) => b.label?.toLowerCase().includes('lower')) || buttons[1];
191
- } else if (num < 50) {
192
- targetBtn = buttons.find((b) => b.label?.toLowerCase().includes('higher')) || buttons[0];
193
- } else {
194
- targetBtn = buttons[Math.floor(Math.random() * 2)];
287
+ async handleCrime(response) {
288
+ if (!response) return null;
289
+ const buttons = getAllButtons(response);
290
+ if (buttons.length === 0) return getFullText(response).substring(0, 80);
291
+
292
+ const userSafe = safeParseJSON(this.account.crime_answers, []);
293
+ const btn = pickSafeButton(buttons, userSafe);
294
+ if (btn) {
295
+ await humanDelay();
296
+ try {
297
+ await btn.click();
298
+ const followUp = await this.waitForDankMemer(10000);
299
+ if (followUp) {
300
+ const text = getFullText(followUp);
301
+ const coins = parseCoins(text);
302
+ if (coins > 0) {
303
+ this.stats.coins += coins;
304
+ return `${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
195
305
  }
306
+ return `${btn.label} → done`;
307
+ }
308
+ return `Clicked: ${btn.label}`;
309
+ } catch { return null; }
310
+ }
311
+ return 'No safe option found';
312
+ }
313
+
314
+ async handleHighLow(response) {
315
+ if (!response) return null;
316
+ const text = getFullText(response);
317
+ const match = text.match(/number.*?(\d+)/i) || text.match(/(\d+)/);
318
+ const buttons = getAllButtons(response);
319
+
320
+ if (match && buttons.length >= 2) {
321
+ const num = parseInt(match[1]);
322
+ await humanDelay();
323
+ 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
330
+ const jackpot = buttons.find((b) => (b.label || '').toLowerCase().includes('jackpot'));
331
+ targetBtn = jackpot || buttons[Math.floor(Math.random() * 2)];
332
+ }
196
333
 
197
- try {
198
- await targetBtn.click();
199
- return `HL: number was ${num}, clicked ${targetBtn.label}`;
200
- } catch {
201
- return null;
334
+ try {
335
+ await targetBtn.click();
336
+ const followUp = await this.waitForDankMemer(10000);
337
+ if (followUp) {
338
+ const ftText = getFullText(followUp);
339
+ 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
345
+ const moreButtons = getAllButtons(followUp);
346
+ if (moreButtons.length >= 2) {
347
+ return await this.handleHighLow(followUp);
202
348
  }
349
+ return `${num} → ${targetBtn.label}`;
203
350
  }
204
- }
351
+ return `${num} → ${targetBtn.label}`;
352
+ } catch { return null; }
205
353
  }
206
354
 
355
+ // Fallback: click random
356
+ if (buttons.length > 0) {
357
+ const btn = buttons[Math.floor(Math.random() * buttons.length)];
358
+ await humanDelay();
359
+ try { await btn.click(); } catch {}
360
+ return `Clicked: ${btn.label || 'button'}`;
361
+ }
207
362
  return null;
208
363
  }
209
364
 
365
+ async handleGenericCommand(response) {
366
+ if (!response) return null;
367
+ const text = getFullText(response);
368
+ const coins = parseCoins(text);
369
+
370
+ // Check if we need to buy something
371
+ const neededItem = this.needsItem(text);
372
+ if (neededItem) {
373
+ this.log('warn', `Missing ${c.bold}${neededItem}${c.reset} — auto-buying...`);
374
+ const bought = await this.buyItem(neededItem);
375
+ if (bought) return `auto-bought ${neededItem}`;
376
+ return `need ${neededItem} (couldn't buy)`;
377
+ }
378
+
379
+ // Handle buttons if present
380
+ const buttons = getAllButtons(response);
381
+ if (buttons.length > 0) {
382
+ const btn = buttons.find((b) => !b.disabled) || buttons[0];
383
+ if (btn) {
384
+ await humanDelay();
385
+ try {
386
+ await btn.click();
387
+ const followUp = await this.waitForDankMemer(8000);
388
+ if (followUp) {
389
+ const fText = getFullText(followUp);
390
+ const fCoins = parseCoins(fText);
391
+ if (fCoins > 0) { this.stats.coins += fCoins; return `${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`; }
392
+ }
393
+ } catch {}
394
+ }
395
+ }
396
+
397
+ if (coins > 0) {
398
+ this.stats.coins += coins;
399
+ return `${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`;
400
+ }
401
+
402
+ return text.substring(0, 60) || 'done';
403
+ }
404
+
405
+ // ── Run Single Command ──────────────────────────────────────
210
406
  async runCommand(cmdName, prefix) {
211
407
  const cmdString = `${prefix} ${cmdName}`;
212
- this.log('cmd', `${c.white}${cmdString}${c.reset}`);
408
+ this.log('cmd', `${c.white}${c.bold}${cmdString}${c.reset}`);
409
+ this.stats.commands++;
213
410
 
214
411
  try {
215
412
  await this.channel.send(cmdString);
@@ -217,53 +414,78 @@ class AccountWorker {
217
414
 
218
415
  if (!response) {
219
416
  this.log('warn', `No response for ${cmdString}`);
220
- await sendLog(cmdString, 'No response (timeout)', 'timeout');
417
+ this.stats.errors++;
418
+ await sendLog(cmdString, 'timeout', 'timeout');
221
419
  return;
222
420
  }
223
421
 
224
422
  let result;
225
- if (cmdName === 'hl') {
226
- result = await this.handleHighLow(response);
227
- } else if (['search', 'crime'].includes(cmdName)) {
228
- result = await this.handleInteraction(response);
229
- } else {
230
- result = response.content?.substring(0, 100) || 'Embed response';
231
- if (response.components && response.components.length > 0) {
232
- result = await this.handleInteraction(response);
233
- }
423
+ 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;
234
428
  }
235
429
 
236
- this.log('success', `${cmdString} ${c.dim}→${c.reset} ${c.green}${result || 'done'}${c.reset}`);
430
+ this.stats.successes++;
431
+ this.log('success', `${c.dim}${cmdString}${c.reset} → ${result || 'done'}`);
237
432
  await sendLog(cmdString, result || 'done', 'success');
238
433
  } catch (err) {
434
+ this.stats.errors++;
239
435
  this.log('error', `${cmdString} failed: ${err.message}`);
240
436
  await sendLog(cmdString, err.message, 'error');
241
437
  }
242
438
  }
243
439
 
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}`);
449
+ }
450
+
451
+ // ── Main Grind Loop ─────────────────────────────────────────
244
452
  async grindLoop() {
245
453
  if (this.running) return;
246
454
  this.running = true;
247
455
 
248
456
  const commandMap = {
249
- cmd_hunt: 'hunt',
250
- cmd_dig: 'dig',
251
- cmd_beg: 'beg',
252
- cmd_search: 'search',
253
- cmd_hl: 'hl',
254
- cmd_crime: 'crime',
255
- cmd_pm: 'pm',
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 },
256
464
  };
257
465
 
466
+ let cycleCount = 0;
467
+
258
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
+ }
482
+
259
483
  const acc = this.account;
260
484
  const prefix = acc.use_slash ? '/' : 'pls';
261
- const minCD = acc.cooldown_min || 3;
262
- const maxCD = acc.cooldown_max || 8;
263
485
 
264
486
  const enabledCommands = Object.entries(commandMap)
265
487
  .filter(([key]) => acc[key] === true || acc[key] === 1)
266
- .map(([, cmd]) => cmd);
488
+ .map(([, val]) => val);
267
489
 
268
490
  if (enabledCommands.length === 0) {
269
491
  this.log('warn', 'No commands enabled. Waiting 30s...');
@@ -271,50 +493,95 @@ class AccountWorker {
271
493
  continue;
272
494
  }
273
495
 
274
- for (const cmd of enabledCommands) {
496
+ for (const { cmd, cdKey, defaultCd } of enabledCommands) {
275
497
  if (!this.running) break;
276
498
  await this.runCommand(cmd, prefix);
277
- await randomDelay(minCD, maxCD);
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));
506
+ }
507
+
508
+ cycleCount++;
509
+
510
+ // Every 5 cycles: print stats + check balance
511
+ if (cycleCount % 5 === 0) {
512
+ this.printStats();
513
+ await humanDelay(500, 1500);
514
+ await this.checkBalance();
515
+ await humanDelay(2000, 4000);
278
516
  }
279
517
 
280
518
  const cycleDelay = 2 + Math.random() * 3;
281
- this.log('info', `${c.dim}Cycle done. Next in ${cycleDelay.toFixed(1)}s${c.reset}`);
519
+ this.log('info', `${c.dim}Cycle ${cycleCount} done. Next in ${cycleDelay.toFixed(1)}s${c.reset}`);
282
520
  await new Promise((r) => setTimeout(r, cycleDelay * 1000));
283
521
  }
284
522
  }
285
523
 
524
+ // ── Re-fetch Config from API ─────────────────────────────────
525
+ async refreshConfig() {
526
+ try {
527
+ const res = await fetch(`${API_URL}/api/grinder/status`, {
528
+ headers: { Authorization: `Bearer ${API_KEY}` },
529
+ });
530
+ const data = await res.json();
531
+ if (data.accounts) {
532
+ const updated = data.accounts.find((a) => a.id === this.account.id);
533
+ if (updated) this.account = updated;
534
+ }
535
+ } catch { /* silent */ }
536
+ }
537
+
538
+ // ── Start Worker ────────────────────────────────────────────
286
539
  async start() {
287
- if (!this.account.discord_token) {
288
- this.log('error', 'No Discord token configured.');
289
- return;
290
- }
291
- if (!this.account.channel_id) {
292
- this.log('error', 'No channel ID configured.');
293
- return;
294
- }
540
+ if (!this.account.discord_token) { this.log('error', 'No Discord token.'); return; }
541
+ if (!this.account.channel_id) { this.log('error', 'No channel ID.'); return; }
295
542
 
296
- this.log('info', 'Connecting to Discord...');
543
+ this.log('info', `Connecting...`);
297
544
 
298
545
  return new Promise((resolve) => {
299
546
  this.client.on('ready', async () => {
300
- this.log('success', `Logged in as ${c.bold}${this.client.user.tag}${c.reset}`);
301
- this.log('info', `Servers: ${c.white}${this.client.guilds.cache.size}${c.reset}`);
302
-
547
+ // Use the actual Discord username
548
+ this.username = this.client.user.tag || this.username;
549
+
550
+ // Report username back to API
551
+ try {
552
+ await fetch(`${API_URL}/api/grinder/status`, {
553
+ method: 'POST',
554
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
555
+ body: JSON.stringify({ account_id: this.account.id, discord_username: this.username }),
556
+ });
557
+ } catch { /* silent */ }
558
+
559
+ this.log('success', `Logged in! ${c.dim}(${this.client.guilds.cache.size} servers)${c.reset}`);
303
560
  this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
304
561
 
305
562
  if (!this.channel) {
306
- this.log('error', `Cannot find channel ${this.account.channel_id}`);
307
- resolve();
308
- return;
563
+ this.log('error', `Channel ${this.account.channel_id} not found`);
564
+ resolve(); return;
309
565
  }
310
566
 
311
567
  this.log('info', `Channel: ${c.white}#${this.channel.name || this.account.channel_id}${c.reset}`);
312
568
 
313
- const enabled = Object.keys({ cmd_hunt: 1, cmd_dig: 1, cmd_beg: 1, cmd_search: 1, cmd_hl: 1, cmd_crime: 1, cmd_pm: 1 })
314
- .filter((k) => this.account[k] === true || this.account[k] === 1)
315
- .map((k) => k.replace('cmd_', ''));
316
- this.log('info', `Commands: ${c.white}${enabled.join(', ') || 'none'}${c.reset}`);
317
- this.log('info', `Cooldown: ${c.white}${this.account.cooldown_min || 3}-${this.account.cooldown_max || 8}s${c.reset}`);
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' },
578
+ ].filter((ci) => this.account[ci.key] === true || this.account[ci.key] === 1);
579
+
580
+ const cmdStr = cmdInfo.map((ci) => `${ci.label}(${this.account[ci.cd] || 20}s)`).join(', ');
581
+ this.log('info', `Commands: ${c.white}${cmdStr || 'none'}${c.reset}`);
582
+
583
+ // Initial balance check
584
+ await this.checkBalance();
318
585
  console.log('');
319
586
 
320
587
  this.grindLoop();
@@ -331,55 +598,54 @@ class AccountWorker {
331
598
  }
332
599
  }
333
600
 
334
- // ── Main Entry ──────────────────────────────────
601
+ // ══════════════════════════════════════════════════════════════
602
+ // ═ Main Entry
603
+ // ══════════════════════════════════════════════════════════════
335
604
 
336
605
  async function start(apiKey, apiUrl) {
337
606
  API_KEY = apiKey;
338
607
  API_URL = apiUrl;
339
608
 
340
609
  console.log('');
341
- console.log(` ${c.magenta}${c.bold}╔══════════════════════════════════════════╗${c.reset}`);
342
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
343
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.white}${c.bold}DankGrinder${c.reset} ${c.dim}v2.0${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
344
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.dim}Multi-Account Automation Engine${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
345
- console.log(` ${c.magenta}${c.bold}║${c.reset} ${c.magenta}${c.bold}║${c.reset}`);
346
- console.log(` ${c.magenta}${c.bold}╚══════════════════════════════════════════╝${c.reset}`);
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}`);
347
616
  console.log('');
348
617
 
349
618
  log('info', `API: ${c.dim}${API_URL}${c.reset}`);
350
- log('info', 'Fetching config & accounts...');
619
+ log('info', 'Fetching accounts...');
351
620
 
352
621
  const data = await fetchConfig();
353
-
354
- if (!data) {
355
- log('error', 'Failed to fetch config. Check your API key and server.');
356
- process.exit(1);
357
- }
622
+ if (!data) { log('error', 'Failed to fetch config.'); process.exit(1); }
358
623
 
359
624
  const { accounts } = data;
360
-
361
625
  if (!accounts || accounts.length === 0) {
362
- log('error', 'No active accounts found. Add accounts in the dashboard.');
626
+ log('error', 'No active accounts. Add them in the dashboard.');
363
627
  process.exit(1);
364
628
  }
365
629
 
366
- log('success', `Found ${c.bold}${accounts.length}${c.reset} active account(s)`);
630
+ log('success', `${c.bold}${accounts.length}${c.reset} active account(s) found`);
367
631
  console.log('');
368
632
 
369
- // Spawn a worker per account
370
633
  for (let i = 0; i < accounts.length; i++) {
371
634
  const worker = new AccountWorker(accounts[i], i);
372
635
  workers.push(worker);
373
636
  await worker.start();
374
637
  }
375
638
 
376
- log('info', `${c.bold}All workers running. Press Ctrl+C to stop.${c.reset}`);
639
+ log('info', `${c.bold}All workers running.${c.reset} ${c.dim}Ctrl+C to stop.${c.reset}`);
377
640
  console.log('');
378
641
 
379
642
  process.on('SIGINT', () => {
380
643
  console.log('');
381
- log('warn', 'Shutting down all workers...');
382
- for (const w of workers) w.stop();
644
+ log('warn', 'Shutting down...');
645
+ for (const w of workers) {
646
+ w.printStats();
647
+ w.stop();
648
+ }
383
649
  setTimeout(() => process.exit(0), 2000);
384
650
  });
385
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "1.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "./bin/dankgrinder.js"