dankgrinder 5.0.0 → 5.0.2

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.
@@ -12,6 +12,20 @@
12
12
  const https = require('https');
13
13
  const { AhoCorasick, LRUCache, StringPool, ObjectPool } = require('../structures');
14
14
 
15
+ // ── HTTPS Keep-Alive Agent ───────────────────────────────────
16
+ // At 10K accounts, every CV2 fetch without keep-alive creates a new
17
+ // TCP+TLS handshake (~150ms). With keep-alive, connections are reused.
18
+ // maxSockets=150 prevents file descriptor exhaustion; scheduling='fifo'
19
+ // distributes load evenly across connections.
20
+ const httpsAgent = new https.Agent({
21
+ keepAlive: true,
22
+ keepAliveMsecs: 30000,
23
+ maxSockets: 150,
24
+ maxFreeSockets: 30,
25
+ timeout: 30000,
26
+ scheduling: 'fifo',
27
+ });
28
+
15
29
  // ── String Interning ─────────────────────────────────────────
16
30
  // Discord sends repeated IDs millions of times. Intern them to share references.
17
31
  const strings = new StringPool();
@@ -62,6 +76,25 @@ const LOG = {
62
76
  debug: (msg) => log(`${c.dim}⊙${c.reset}`, msg),
63
77
  };
64
78
 
79
+ // ── Pre-compiled Regex (avoid recompilation in hot paths) ────
80
+ // V8 recompiles inline /regex/ on every call site. Module-level
81
+ // compilation runs once and shares the compiled automaton.
82
+ const RE_ANSI = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
83
+ const RE_NET_COINS = /Net:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([+-]?[\d,]+)/i;
84
+ const RE_WINNINGS = /Winnings:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([\d,]+)/i;
85
+ const RE_RECEIVED = /you\s+received\s*:\s*[\s\S]{0,80}?([\d,]+)/i;
86
+ const RE_COIN_EMOJI = /<a?:Coin:\d+>\s*([\d,]+)/gi;
87
+ const RE_WALLET_PLACED = /⏣\s*([\d,]+)\s*was placed/i;
88
+ const RE_COINS_PLAIN = /⏣\s*([\d,]+)/g;
89
+ const RE_WALLET_TEXT = /wallet[:\s]*[⏣💰]?\s*([\d,]+)/i;
90
+ const RE_BANK_TEXT = /bank[:\s]*[⏣💰]?\s*([\d,]+)/i;
91
+ const RE_BANK_EMOJI = /<a?:Bank:\d+>\s*([\d,]+)/i;
92
+ const RE_BANK_SLASH = /[\d,]+\s*\/\s*[\d,]+/g;
93
+ const RE_LARGE_NUM = /([\d,]{2,})/;
94
+ const RE_NET_SIGNED = /Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
95
+ const RE_WIN_SIGNED = /Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
96
+ const RE_HOLD_TIGHT_REASON = /Reason:\s*\/(\w+)/i;
97
+
65
98
  // ── Sleep ────────────────────────────────────────────────────
66
99
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
67
100
 
@@ -72,76 +105,83 @@ function humanDelay(min = 50, max = 200) {
72
105
  // ── Message Parsing ──────────────────────────────────────────
73
106
  function getFullText(msg) {
74
107
  if (!msg) return '';
75
- // If CV2 text was pre-fetched via ensureCV2(), use it
76
108
  if (msg._cv2text) return msg._cv2text;
77
- let text = msg.content || '';
78
- for (const embed of msg.embeds || []) {
79
- if (embed.title) text += ' ' + embed.title;
80
- if (embed.description) text += ' ' + embed.description;
81
- for (const f of embed.fields || []) text += ' ' + (f.name || '') + ' ' + (f.value || '');
82
- if (embed.footer?.text) text += ' ' + embed.footer.text;
109
+ // Array-join pattern: O(n) vs O(n²) from repeated string concatenation.
110
+ // At 10K accounts processing messages concurrently, this avoids
111
+ // creating intermediate string copies on every += operation.
112
+ const parts = [msg.content || ''];
113
+ const embeds = msg.embeds;
114
+ if (embeds) {
115
+ for (let i = 0; i < embeds.length; i++) {
116
+ const e = embeds[i];
117
+ if (e.title) parts.push(e.title);
118
+ if (e.description) parts.push(e.description);
119
+ const fields = e.fields;
120
+ if (fields) for (let j = 0; j < fields.length; j++) {
121
+ parts.push(fields[j].name || '', fields[j].value || '');
122
+ }
123
+ if (e.footer?.text) parts.push(e.footer.text);
124
+ }
83
125
  }
84
- function extractText(components) {
85
- for (const comp of components || []) {
126
+ const _extract = (components) => {
127
+ if (!components) return;
128
+ for (let i = 0; i < components.length; i++) {
129
+ const comp = components[i];
86
130
  if (!comp) continue;
87
- if (comp.content) text += ' ' + comp.content;
88
- if (comp.components) extractText(comp.components);
131
+ if (comp.content) parts.push(comp.content);
132
+ if (comp.components) _extract(comp.components);
89
133
  }
90
- }
91
- extractText(msg.components);
92
- return text;
134
+ };
135
+ _extract(msg.components);
136
+ return parts.join(' ');
93
137
  }
94
138
 
95
139
  function stripAnsi(text) {
96
140
  if (!text) return '';
97
- return String(text).replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
141
+ return String(text).replace(RE_ANSI, '');
98
142
  }
99
143
 
100
144
  function parseCoins(text) {
101
145
  if (!text) return 0;
102
146
  const cleanText = stripAnsi(text);
103
- // Prefer "Net:" if present (accurate earned/lost from Dank Memer)
104
- const netMatch = cleanText.match(/Net:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([+-]?[\d,]+)/i);
147
+ RE_NET_COINS.lastIndex = 0;
148
+ const netMatch = cleanText.match(RE_NET_COINS);
105
149
  if (netMatch) {
106
150
  const net = parseInt(netMatch[1].replace(/,/g, ''));
107
151
  return net > 0 ? net : 0;
108
152
  }
109
- const winMatch = cleanText.match(/Winnings:\s*\*{0,2}\s*[⏣o]\s*\*{0,2}([\d,]+)/i);
153
+ RE_WINNINGS.lastIndex = 0;
154
+ const winMatch = cleanText.match(RE_WINNINGS);
110
155
  if (winMatch) {
111
156
  const w = parseInt(winMatch[1].replace(/,/g, ''));
112
157
  if (w > 0) return w;
113
158
  }
114
-
115
- // Tidy/CV2 reward blocks often use "You received:" with non-⏣ symbols.
116
- const receivedMatch = cleanText.match(/you\s+received\s*:\s*[\s\S]{0,80}?([\d,]+)/i);
159
+ const receivedMatch = cleanText.match(RE_RECEIVED);
117
160
  if (receivedMatch) {
118
161
  const r = parseInt(receivedMatch[1].replace(/,/g, ''), 10);
119
162
  if (r > 0) return r;
120
163
  }
121
-
122
- // CV2 pattern: <:Coin:ID> NUMBER
123
- const coinEmojiMatches = [...cleanText.matchAll(/<a?:Coin:\d+>\s*([\d,]+)/gi)];
164
+ RE_COIN_EMOJI.lastIndex = 0;
165
+ const coinEmojiMatches = [...cleanText.matchAll(RE_COIN_EMOJI)];
124
166
  if (coinEmojiMatches.length > 0) {
125
167
  let best = 0;
126
- for (const m of coinEmojiMatches) {
127
- const v = parseInt((m[1] || '0').replace(/,/g, ''), 10) || 0;
168
+ for (let i = 0; i < coinEmojiMatches.length; i++) {
169
+ const v = parseInt((coinEmojiMatches[i][1] || '0').replace(/,/g, ''), 10) || 0;
128
170
  if (v > best) best = v;
129
171
  }
130
172
  if (best > 0) return best;
131
173
  }
132
-
133
- // Prefer "placed in your wallet" pattern (daily, beg, etc.)
134
- const walletMatch = cleanText.match(/⏣\s*([\d,]+)\s*was placed/i);
174
+ const walletMatch = cleanText.match(RE_WALLET_PLACED);
135
175
  if (walletMatch) return parseInt(walletMatch[1].replace(/,/g, ''));
136
- // Fallback: max ⏣ number
137
- const matches = cleanText.match(/⏣\s*([\d,]+)/g);
176
+ RE_COINS_PLAIN.lastIndex = 0;
177
+ const matches = cleanText.match(RE_COINS_PLAIN);
138
178
  if (!matches) return 0;
139
179
  let best = 0;
140
- for (const m of matches) {
141
- const numStr = m.replace(/[^\d]/g, '');
180
+ for (let i = 0; i < matches.length; i++) {
181
+ const numStr = matches[i].replace(/[^\d]/g, '');
142
182
  if (numStr) {
143
183
  const val = parseInt(numStr);
144
- if (val > 0 && val < 1_000_000_000) best = Math.max(best, val);
184
+ if (val > 0 && val < 1_000_000_000 && val > best) best = val;
145
185
  }
146
186
  }
147
187
  return best;
@@ -149,11 +189,9 @@ function parseCoins(text) {
149
189
 
150
190
  function parseNetCoins(text) {
151
191
  if (!text) return 0;
152
- // "Net: **⏣ -5,000**" or "Net: **⏣ +0**" (draw)
153
- const netMatch = text.match(/Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i);
192
+ const netMatch = text.match(RE_NET_SIGNED);
154
193
  if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
155
- // Snakeeyes: "Winnings: **⏣ -10,000**", CV2: "Winnings: ⏣ **10,000**"
156
- const winMatch = text.match(/Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i);
194
+ const winMatch = text.match(RE_WIN_SIGNED);
157
195
  if (winMatch) {
158
196
  const v = parseInt(winMatch[1].replace(/,/g, ''));
159
197
  if (v !== 0) return v;
@@ -169,27 +207,19 @@ function parseNetCoins(text) {
169
207
  function parseBalance(msg) {
170
208
  if (!msg) return 0;
171
209
  const text = stripAnsi(getFullText(msg));
172
-
173
- // Try standard embed wallet pattern
174
- const walletMatch = text.match(/wallet[:\s]*[⏣💰]?\s*([\d,]+)/i);
210
+ const walletMatch = text.match(RE_WALLET_TEXT);
175
211
  if (walletMatch) return parseInt(walletMatch[1].replace(/,/g, ''));
176
-
177
- // CV2 pattern: <:Coin:ID> NUMBER (first number after Coin emoji = wallet)
178
- const coinEmojiMatch = text.match(/<a?:Coin:\d+>\s*([\d,]+)/i);
212
+ RE_COIN_EMOJI.lastIndex = 0;
213
+ const coinEmojiMatch = text.match(RE_COIN_EMOJI);
179
214
  if (coinEmojiMatch) return parseInt(coinEmojiMatch[1].replace(/,/g, ''));
180
-
181
- const bankEmojiMatch = text.match(/<a?:Bank:\d+>\s*([\d,]+)/i);
215
+ const bankEmojiMatch = text.match(RE_BANK_EMOJI);
182
216
  if (bankEmojiMatch) return parseInt(bankEmojiMatch[1].replace(/,/g, ''));
183
-
184
- // Fallback: ⏣ prefixed
185
217
  const coins = parseCoins(text);
186
218
  if (coins > 0) return coins;
187
-
188
- // Last resort: first large number not in "X / Y" bank pattern
189
- const cleaned = text.replace(/[\d,]+\s*\/\s*[\d,]+/g, '');
190
- const numMatch = cleaned.match(/([\d,]{2,})/);
219
+ RE_BANK_SLASH.lastIndex = 0;
220
+ const cleaned = text.replace(RE_BANK_SLASH, '');
221
+ const numMatch = cleaned.match(RE_LARGE_NUM);
191
222
  if (numMatch) return parseInt(numMatch[1].replace(/,/g, ''));
192
-
193
223
  return 0;
194
224
  }
195
225
 
@@ -252,7 +282,9 @@ function findSelectMenuOption(msg, label) {
252
282
  return null;
253
283
  }
254
284
 
255
- // Safe button click — tries library methods first, falls back to raw HTTP for CV2
285
+ // Safe button click — tries library methods first, falls back to raw HTTP for CV2.
286
+ // When CV2 fallback is used, waits for the message to update so callers always
287
+ // get the updated message back (instead of null, which broke multi-round games).
256
288
  async function safeClickButton(msg, button) {
257
289
  if (typeof button.click === 'function') {
258
290
  return button.click();
@@ -265,9 +297,29 @@ async function safeClickButton(msg, button) {
265
297
  // Fall through to CV2 raw interaction fallback.
266
298
  }
267
299
  }
268
- // CV2 fallback: send interaction via raw HTTP
300
+ // CV2 fallback: send interaction via raw HTTP, then wait for the message
301
+ // to update so we can return the updated message to the caller.
269
302
  if (id) {
270
303
  await clickCV2Button(msg, id);
304
+ // Wait for Dank Memer to process the interaction and update the message
305
+ const updatedMsg = await new Promise((resolve) => {
306
+ const timeout = setTimeout(() => {
307
+ msg.client?.removeListener?.('messageUpdate', handler);
308
+ resolve(null);
309
+ }, 8000);
310
+ const handler = (_, newMsg) => {
311
+ if (newMsg.id === msg.id) {
312
+ clearTimeout(timeout);
313
+ msg.client?.removeListener?.('messageUpdate', handler);
314
+ resolve(newMsg);
315
+ }
316
+ };
317
+ msg.client?.on?.('messageUpdate', handler);
318
+ });
319
+ if (updatedMsg) {
320
+ await ensureCV2(updatedMsg);
321
+ return updatedMsg;
322
+ }
271
323
  return null;
272
324
  }
273
325
  throw new Error('No click method available on button');
@@ -288,7 +340,7 @@ function isHoldTight(msg) {
288
340
  function getHoldTightReason(msg) {
289
341
  if (!msg) return null;
290
342
  const text = getFullText(msg);
291
- const match = text.match(/Reason:\s*\/(\w+)/i);
343
+ const match = text.match(RE_HOLD_TIGHT_REASON);
292
344
  return match ? match[1].toLowerCase() : null;
293
345
  }
294
346
 
@@ -359,10 +411,13 @@ function dumpMessage(msg, label) {
359
411
 
360
412
  function _httpGet(url, headers) {
361
413
  return new Promise((resolve, reject) => {
362
- https.get(url, { headers }, res => {
363
- let d = '';
364
- res.on('data', c => d += c);
365
- res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } });
414
+ https.get(url, { headers, agent: httpsAgent }, res => {
415
+ const chunks = [];
416
+ res.on('data', c => chunks.push(c));
417
+ res.on('end', () => {
418
+ const d = chunks.join('');
419
+ try { resolve(JSON.parse(d)); } catch (e) { reject(e); }
420
+ });
366
421
  }).on('error', reject);
367
422
  });
368
423
  }
@@ -371,11 +426,11 @@ function _httpPost(url, headers, body) {
371
426
  return new Promise((resolve, reject) => {
372
427
  const u = new URL(url);
373
428
  const req = https.request({
374
- hostname: u.hostname, path: u.pathname, method: 'POST', headers,
429
+ hostname: u.hostname, path: u.pathname, method: 'POST', headers, agent: httpsAgent,
375
430
  }, res => {
376
- let d = '';
377
- res.on('data', c => d += c);
378
- res.on('end', () => resolve({ status: res.statusCode, body: d }));
431
+ const chunks = [];
432
+ res.on('data', c => chunks.push(c));
433
+ res.on('end', () => resolve({ status: res.statusCode, body: chunks.join('') }));
379
434
  });
380
435
  req.on('error', reject);
381
436
  req.write(body);
@@ -564,8 +619,12 @@ module.exports = {
564
619
  isCV2,
565
620
  ensureCV2,
566
621
  clickCV2Button,
567
- // Expose shared structures for other command files
622
+ // Shared structures and optimized constants
568
623
  strings,
569
624
  cv2Cache,
570
625
  itemDetector,
626
+ httpsAgent,
627
+ RE_NET_SIGNED,
628
+ RE_WIN_SIGNED,
629
+ RE_COIN_EMOJI,
571
630
  };
@@ -20,14 +20,20 @@ const {
20
20
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
21
21
  } = require('./utils');
22
22
 
23
+ const RE_MEMORY_BACKTICK_CHUNK = /`([^`]+)`/g;
24
+ const RE_BACKTICK_STRIP = /`/g;
25
+ const RE_WORK_COOLDOWN_TS = /<t:(\d+):R>/;
26
+ const RE_WORK_COOLDOWN_MINUTES = /(\d+)\s*minute/i;
27
+ const RE_WORK_COOLDOWN_HOURS = /(\d+)\s*hour/i;
28
+
23
29
  // Job progression list (order matters — first is easiest to get)
24
- const JOBS = [
30
+ const JOBS = Object.freeze([
25
31
  'babysitter', 'dog walker', 'fast food worker', 'youtuber',
26
32
  'twitch streamer', 'professional fisherman', 'robber',
27
33
  'veterinarian', 'musician', 'manager', 'politician',
28
34
  'detective', 'santa claus', 'discord mod', 'professional hunter',
29
35
  'scientist', 'pro gamer', 'ghost',
30
- ];
36
+ ]);
31
37
 
32
38
  /**
33
39
  * Re-fetch a message to get updated state.
@@ -41,9 +47,9 @@ async function refetchMsg(channel, msgId) {
41
47
  * Looks for backtick-wrapped words: `word1` `word2` `word3`
42
48
  */
43
49
  function parseMemoryOrder(text) {
44
- const matches = text.match(/`([^`]+)`/g);
50
+ const matches = text.match(RE_MEMORY_BACKTICK_CHUNK);
45
51
  if (matches && matches.length > 0) {
46
- return matches.map(m => m.replace(/`/g, '').trim().toLowerCase());
52
+ return matches.map(m => m.replace(RE_BACKTICK_STRIP, '').trim().toLowerCase());
47
53
  }
48
54
  return [];
49
55
  }
@@ -54,7 +60,7 @@ function parseMemoryOrder(text) {
54
60
  */
55
61
  function parseWorkCooldown(text) {
56
62
  // Unix timestamp pattern: <t:TIMESTAMP:R>
57
- const tsMatch = text.match(/<t:(\d+):R>/);
63
+ const tsMatch = text.match(RE_WORK_COOLDOWN_TS);
58
64
  if (tsMatch) {
59
65
  const ts = parseInt(tsMatch[1]);
60
66
  const now = Math.floor(Date.now() / 1000);
@@ -62,9 +68,9 @@ function parseWorkCooldown(text) {
62
68
  return diff > 0 ? diff : 3600;
63
69
  }
64
70
  // "X minutes" / "X hours" pattern
65
- const minMatch = text.match(/(\d+)\s*minute/i);
71
+ const minMatch = text.match(RE_WORK_COOLDOWN_MINUTES);
66
72
  if (minMatch) return parseInt(minMatch[1]) * 60;
67
- const hrMatch = text.match(/(\d+)\s*hour/i);
73
+ const hrMatch = text.match(RE_WORK_COOLDOWN_HOURS);
68
74
  if (hrMatch) return parseInt(hrMatch[1]) * 3600;
69
75
  return null;
70
76
  }
@@ -214,7 +220,10 @@ async function handleWordMemory({ channel, current, wordOrder, waitForDankMemer
214
220
  async function handleRepeatWord({ current, wordOrder }) {
215
221
  // Find which word appears more than once
216
222
  const counts = {};
217
- for (const w of wordOrder) counts[w] = (counts[w] || 0) + 1;
223
+ for (let wi = 0; wi < wordOrder.length; wi++) {
224
+ const w = wordOrder[wi];
225
+ counts[w] = (counts[w] || 0) + 1;
226
+ }
218
227
  const repeated = Object.entries(counts).find(([, count]) => count > 1);
219
228
  if (repeated) {
220
229
  const [word] = repeated;