dankgrinder 5.24.0 → 5.260.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.
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Advanced Anti-Detection System
3
+ *
4
+ * Features:
5
+ * - Adaptive human-like timing with context-aware delays
6
+ * - Micro-pause injection for natural interaction patterns
7
+ * - Activity-based variance (tired vs alert simulation)
8
+ * - Session behavior drift (mimics human consistency changes)
9
+ * - Randomized click timing within safe bounds
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ // ── Configuration ─────────────────────────────────────────────
15
+ const CONFIG = Object.freeze({
16
+ // Base delay ranges in ms (tuned for natural human timing)
17
+ MIN_DELAY: 500, // 0.5s minimum - natural human speed
18
+ MAX_DELAY: 1000, // 1.0s maximum - not too slow
19
+
20
+ // Context multipliers (subtle variations)
21
+ MULT_FIRST_ACTION: 1.2, // Slight hesitation on first action
22
+ MULT_REPEATED_ACTION: 0.85, // Slightly faster on repeated actions
23
+ MULT_HIGH_VALUE: 1.1, // Marginally slower for high-value actions
24
+ MULT_LOW_VALUE: 0.9, // Slightly faster for low-value actions
25
+
26
+ // Micro-pause settings (subtle human-like hesitation)
27
+ MICRO_PAUSE_CHANCE: 0.25, // 25% chance of micro-pause
28
+ MICRO_PAUSE_MIN: 50,
29
+ MICRO_PAUSE_MAX: 150,
30
+
31
+ // Minimal drift to avoid detectable patterns
32
+ DRIFT_FACTOR: 0.01,
33
+ DRIFT_MAX: 0.08, // Max 8% drift
34
+
35
+ // Pattern randomization
36
+ PATTERN_RESET_CHANCE: 0.2, // 20% chance to reset timing pattern
37
+ });
38
+
39
+ // ── Session State ─────────────────────────────────────────────
40
+ const sessionState = {
41
+ actionCount: 0,
42
+ lastActionTime: 0,
43
+ currentDrift: 0,
44
+ patternSeed: Math.random(),
45
+ activityLevel: 1.0, // Starts neutral
46
+ };
47
+
48
+ // ── Seeded Random for Reproducible Patterns ──────────────────
49
+ function seededRandom(seed) {
50
+ const x = Math.sin(seed) * 10000;
51
+ return x - Math.floor(x);
52
+ }
53
+
54
+ // ── Adaptive Delay Calculation ────────────────────────────────
55
+ /**
56
+ * Calculate human-like delay based on context.
57
+ * @param {Object} options
58
+ * @param {string} options.context - Action context ('first', 'repeated', 'high-value', 'low-value', 'normal')
59
+ * @param {number} options.baseMin - Minimum delay override
60
+ * @param {number} options.baseMax - Maximum delay override
61
+ * @param {boolean} options.skipMicroPause - Force skip micro-pause
62
+ * @returns {Promise<number>} - Delay in ms
63
+ */
64
+ async function calcAdaptiveDelay(options = {}) {
65
+ const {
66
+ context = 'normal',
67
+ baseMin = CONFIG.MIN_DELAY,
68
+ baseMax = CONFIG.MAX_DELAY,
69
+ skipMicroPause = false,
70
+ } = options;
71
+
72
+ sessionState.actionCount++;
73
+ const now = Date.now();
74
+
75
+ // Time since last action affects speed (rush vs relaxed)
76
+ const timeSinceLast = now - sessionState.lastActionTime;
77
+ const rushFactor = timeSinceLast < 500 ? 0.8 : timeSinceLast > 5000 ? 1.15 : 1.0;
78
+
79
+ // Apply context multiplier
80
+ let multiplier = 1.0;
81
+ switch (context) {
82
+ case 'first':
83
+ multiplier = CONFIG.MULT_FIRST_ACTION;
84
+ break;
85
+ case 'repeated':
86
+ multiplier = CONFIG.MULT_REPEATED_ACTION;
87
+ break;
88
+ case 'high-value':
89
+ multiplier = CONFIG.MULT_HIGH_VALUE;
90
+ break;
91
+ case 'low-value':
92
+ multiplier = CONFIG.MULT_LOW_VALUE;
93
+ break;
94
+ }
95
+
96
+ // Apply drift (session behavior change)
97
+ sessionState.currentDrift += (Math.random() - 0.5) * CONFIG.DRIFT_FACTOR;
98
+ sessionState.currentDrift = Math.max(
99
+ -CONFIG.DRIFT_MAX,
100
+ Math.min(CONFIG.DRIFT_MAX, sessionState.currentDrift)
101
+ );
102
+ multiplier *= (1 + sessionState.currentDrift);
103
+
104
+ // Pattern reset for unpredictability
105
+ if (Math.random() < CONFIG.PATTERN_RESET_CHANCE) {
106
+ sessionState.patternSeed = Math.random();
107
+ }
108
+
109
+ // Calculate base delay with seeded randomness
110
+ const seed = sessionState.patternSeed + (sessionState.actionCount * 0.001);
111
+ const randomFactor = 0.5 + seededRandom(seed); // 0.5 to 1.5 range
112
+ const baseDelay = baseMin + (baseMax - baseMin) * randomFactor;
113
+
114
+ // Apply all modifiers
115
+ let finalDelay = baseDelay * multiplier * rushFactor;
116
+
117
+ // Add micro-pause (simulates human hesitation)
118
+ if (!skipMicroPause && Math.random() < CONFIG.MICRO_PAUSE_CHANCE) {
119
+ const microPause = CONFIG.MICRO_PAUSE_MIN +
120
+ (CONFIG.MICRO_PAUSE_MAX - CONFIG.MICRO_PAUSE_MIN) * seededRandom(seed + 0.5);
121
+ finalDelay += microPause;
122
+ }
123
+
124
+ // Clamp to reasonable bounds
125
+ finalDelay = Math.max(50, Math.min(800, finalDelay));
126
+
127
+ sessionState.lastActionTime = Date.now();
128
+
129
+ return Math.round(finalDelay);
130
+ }
131
+
132
+ // ── Main Human Delay Function ─────────────────────────────────
133
+ /**
134
+ * Wait with human-like timing.
135
+ * @param {Object} options - Same as calcAdaptiveDelay
136
+ */
137
+ async function humanDelay(options = {}) {
138
+ const delay = await calcAdaptiveDelay(options);
139
+ return new Promise(resolve => setTimeout(resolve, delay));
140
+ }
141
+
142
+ // ── Reset Session State ───────────────────────────────────────
143
+ function resetSession() {
144
+ sessionState.actionCount = 0;
145
+ sessionState.lastActionTime = 0;
146
+ sessionState.currentDrift = 0;
147
+ sessionState.patternSeed = Math.random();
148
+ sessionState.activityLevel = 1.0;
149
+ }
150
+
151
+ // ── Get Session Stats (for debugging) ─────────────────────────
152
+ function getSessionStats() {
153
+ return {
154
+ actionCount: sessionState.actionCount,
155
+ currentDrift: sessionState.currentDrift,
156
+ timeSinceLast: Date.now() - sessionState.lastActionTime,
157
+ };
158
+ }
159
+
160
+ // ── Smart Cooldown Calculator ─────────────────────────────────
161
+ /**
162
+ * Calculate smart cooldown with safety buffer and variance.
163
+ * @param {number} baseCooldown - Base cooldown in seconds
164
+ * @param {Object} options
165
+ * @param {boolean} options.addBuffer - Add safety buffer
166
+ * @param {boolean} options.addVariance - Add random variance
167
+ * @returns {number} - Adjusted cooldown in seconds
168
+ */
169
+ function calcSmartCooldown(baseCooldown, options = {}) {
170
+ const { addBuffer = true, addVariance = true } = options;
171
+
172
+ let adjusted = baseCooldown;
173
+
174
+ // Add safety buffer
175
+ if (addBuffer) {
176
+ adjusted += CONFIG.COOLDOWN_BUFFER_SEC;
177
+ }
178
+
179
+ // Add small variance (±2%) to avoid detectable patterns
180
+ if (addVariance && baseCooldown > 10) {
181
+ const variance = (Math.random() - 0.5) * 0.04 * baseCooldown;
182
+ adjusted += variance;
183
+ }
184
+
185
+ return Math.max(5, Math.round(adjusted));
186
+ }
187
+
188
+ // ── Exponential Backoff with Jitter ───────────────────────────
189
+ /**
190
+ * Calculate backoff delay for retries.
191
+ * @param {number} attempt - Attempt number (0-indexed)
192
+ * @param {number} baseMs - Base delay in ms
193
+ * @param {number} maxMs - Maximum delay in ms
194
+ * @returns {number} - Delay in ms
195
+ */
196
+ function calcBackoff(attempt, baseMs = 1000, maxMs = 30000) {
197
+ const exponential = baseMs * Math.pow(2, attempt);
198
+ const jitter = exponential * 0.2 * (Math.random() - 0.5);
199
+ return Math.min(maxMs, Math.round(exponential + jitter));
200
+ }
201
+
202
+ // ── Exports ───────────────────────────────────────────────────
203
+ module.exports = {
204
+ humanDelay,
205
+ calcAdaptiveDelay,
206
+ calcSmartCooldown,
207
+ calcBackoff,
208
+ resetSession,
209
+ getSessionStats,
210
+ CONFIG,
211
+ };
@@ -39,8 +39,7 @@ const {
39
39
  safeClickButton, isHoldTight, logMsg,
40
40
  } = require('./utils');
41
41
 
42
- const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/;
43
- const RE_ADVENTURE_AGAIN_LABEL = /adventure again in (\d+)\s*(minute|min|hour|second)/;
42
+ const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/g;
44
43
 
45
44
  // ── Adventure type rotation (cycle through all types each run) ────
46
45
  let lastAdventureIndex = -1;
@@ -270,7 +269,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
270
269
 
271
270
  if (!response) {
272
271
  LOG.warn('[adventure] No response from Dank Memer');
273
- return { result: 'no response', coins: 0, nextCooldownSec: null };
272
+ return { result: 'no response', coins: 0, nextCooldownSec: 180 };
274
273
  }
275
274
 
276
275
  // Check for Hold Tight
@@ -439,33 +438,45 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
439
438
  function buildResult(finalText, coins, interactions, rewards, msg) {
440
439
  let nextCooldownSec = null;
441
440
 
442
- // 1) Best: Unix timestamp <t:UNIX:t> in final text or embed
443
- const allText = msg ? getFullText(msg) : finalText;
444
- const tsMatch = allText.match(RE_DISCORD_TIMESTAMP);
445
- if (tsMatch) {
446
- const unixTarget = parseInt(tsMatch[1]);
447
- const nowUnix = Math.floor(Date.now() / 1000);
448
- nextCooldownSec = Math.max(5, unixTarget - nowUnix);
449
- LOG.info(`[adventure] Next at ${new Date(unixTarget * 1000).toLocaleTimeString()} (${c.yellow}${nextCooldownSec}s${c.reset})`);
450
- }
451
-
452
- // 2) Fallback: "Adventure again in X minutes" button label
453
- if (!nextCooldownSec && msg) {
441
+ // 1) Best: button label after adventure ends (e.g. "Adventure again in 57m")
442
+ if (msg) {
454
443
  const rows = msg.components || [];
455
444
  for (let ri = 0; ri < rows.length; ri++) {
456
445
  const comps = rows[ri].components || [];
457
446
  for (let ci = 0; ci < comps.length; ci++) {
458
- const comp = comps[ci];
459
- const label = (comp.label || '').toLowerCase();
460
- const btnMatch = label.match(RE_ADVENTURE_AGAIN_LABEL);
461
- if (btnMatch) {
462
- nextCooldownSec = parseInt(btnMatch[1]);
463
- const unit = btnMatch[2].toLowerCase();
464
- if (unit.startsWith('min')) nextCooldownSec *= 60;
465
- if (unit.startsWith('hour')) nextCooldownSec *= 3600;
466
- LOG.info(`[adventure] Next in ${c.yellow}${btnMatch[1]} ${btnMatch[2]}s${c.reset} (from button)`);
467
- }
447
+ const label = String(comps[ci]?.label || '').toLowerCase();
448
+ if (!label.includes('adventure') || !label.includes('again')) continue;
449
+ const unitMatch = label.match(/(\d+)\s*(h|hr|hrs|hour|hours|m|min|mins|minute|minutes|s|sec|secs|second|seconds)\b/i);
450
+ if (!unitMatch) continue;
451
+ const n = parseInt(unitMatch[1], 10);
452
+ const u = unitMatch[2].toLowerCase();
453
+ if (!Number.isFinite(n) || n <= 0) continue;
454
+ if (u.startsWith('h')) nextCooldownSec = n * 3600;
455
+ else if (u.startsWith('m')) nextCooldownSec = n * 60;
456
+ else nextCooldownSec = n;
457
+ LOG.info(`[adventure] Next cooldown from button: ${c.yellow}${nextCooldownSec}s${c.reset}`);
458
+ break;
468
459
  }
460
+ if (nextCooldownSec) break;
461
+ }
462
+ }
463
+
464
+ // 2) Fallback: Unix timestamp <t:UNIX:*> in final text or embed
465
+ const allText = msg ? getFullText(msg) : finalText;
466
+ if (!nextCooldownSec) {
467
+ const nowUnix = Math.floor(Date.now() / 1000);
468
+ const tsMatches = Array.from(String(allText || '').matchAll(RE_DISCORD_TIMESTAMP));
469
+ let best = null;
470
+ for (const m of tsMatches) {
471
+ const unixTarget = parseInt(m[1], 10);
472
+ if (!Number.isFinite(unixTarget)) continue;
473
+ const diff = unixTarget - nowUnix;
474
+ if (diff <= 0) continue;
475
+ if (best == null || diff < best) best = diff;
476
+ }
477
+ if (best != null) {
478
+ nextCooldownSec = Math.max(5, best);
479
+ LOG.info(`[adventure] Next cooldown from timestamp: ${c.yellow}${nextCooldownSec}s${c.reset}`);
469
480
  }
470
481
  }
471
482
 
@@ -4,11 +4,88 @@
4
4
  */
5
5
 
6
6
  const {
7
- LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
7
+ LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton, humanDelay,
8
+ logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
8
9
  isCV2, ensureCV2, stripAnsi,
9
10
  } = require('./utils');
10
11
  const { buyItem } = require('./shop');
11
12
 
13
+ const DIG_LANE_LABELS = new Set(['left', 'middle', 'right']);
14
+
15
+ async function waitForDigUpdate(channel, messageId, baselineText, waitForDankMemer, timeoutMs = 9000) {
16
+ const started = Date.now();
17
+ while (Date.now() - started < timeoutMs) {
18
+ const msg = await waitForDankMemer(2200);
19
+ if (msg && msg.id === messageId) {
20
+ if (isCV2(msg)) await ensureCV2(msg, true);
21
+ const txt = stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim();
22
+ if (!baselineText || (txt && txt !== baselineText)) return msg;
23
+ }
24
+
25
+ await sleep(350);
26
+ try {
27
+ const fresh = await channel.messages.fetch(messageId);
28
+ if (!fresh) continue;
29
+ if (isCV2(fresh)) await ensureCV2(fresh, true);
30
+ const txt = stripAnsi(getFullText(fresh)).replace(/\s+/g, ' ').trim();
31
+ if (!baselineText || (txt && txt !== baselineText)) return fresh;
32
+ } catch {}
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function pickDigLaneButton(msg) {
38
+ const buttons = getAllButtons(msg).filter((b) => !b.disabled && DIG_LANE_LABELS.has(String(b.label || '').toLowerCase()));
39
+ if (buttons.length === 0) return null;
40
+
41
+ const textLower = stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim().toLowerCase();
42
+
43
+ // If prompt text explicitly mentions a lane, follow it.
44
+ for (const lane of DIG_LANE_LABELS) {
45
+ if (new RegExp(`\\b${lane}\\b`, 'i').test(textLower)) {
46
+ const matched = buttons.find((b) => String(b.label || '').toLowerCase() === lane);
47
+ if (matched) return matched;
48
+ }
49
+ }
50
+
51
+ // Fallback: random lane (still better than skipping the minigame).
52
+ return buttons[Math.floor(Math.random() * buttons.length)];
53
+ }
54
+
55
+ async function resolveDigFlow({ channel, waitForDankMemer, response }) {
56
+ let current = response;
57
+
58
+ for (let round = 0; round < 3; round++) {
59
+ if (isCV2(current)) await ensureCV2(current, true);
60
+ const laneBtn = pickDigLaneButton(current);
61
+ if (!laneBtn) break;
62
+
63
+ LOG.info(`[dig] Minigame: clicking "${laneBtn.label}"`);
64
+ const baseline = stripAnsi(getFullText(current)).replace(/\s+/g, ' ').trim();
65
+ await humanDelay(110, 240);
66
+
67
+ let clicked = null;
68
+ try { clicked = await safeClickButton(current, laneBtn); } catch {}
69
+ if (clicked) current = clicked;
70
+
71
+ const updated = await waitForDigUpdate(channel, current.id, baseline, waitForDankMemer, 10000);
72
+ if (updated) {
73
+ current = updated;
74
+ logMsg(current, `dig-minigame-${round + 1}`);
75
+ } else {
76
+ break;
77
+ }
78
+ }
79
+
80
+ const cleanText = stripAnsi(getFullText(current)).replace(/\s+/g, ' ').trim();
81
+ const coins = parseCoins(cleanText);
82
+ if (coins > 0) {
83
+ LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
84
+ return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
85
+ }
86
+ return { result: cleanText.substring(0, 90) || 'done', coins: 0 };
87
+ }
88
+
12
89
  /**
13
90
  * @param {object} opts
14
91
  * @param {object} opts.channel
@@ -66,25 +143,13 @@ async function runDig({ channel, waitForDankMemer, client }) {
66
143
  if (r2) {
67
144
  if (isCV2(r2)) await ensureCV2(r2);
68
145
  logMsg(r2, 'dig-retry');
69
- const t2 = getFullText(r2);
70
- const coins = parseCoins(t2);
71
- if (coins > 0) {
72
- LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
73
- return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
74
- }
75
- return { result: stripAnsi(t2).replace(/\s+/g, ' ').trim().substring(0, 60), coins: 0 };
146
+ return await resolveDigFlow({ channel, waitForDankMemer, response: r2 });
76
147
  }
77
148
  }
78
149
  return { result: 'need shovel (buy failed)', coins: 0 };
79
150
  }
80
151
 
81
- const coins = parseCoins(cleanText);
82
- if (coins > 0) {
83
- LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
84
- return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
85
- }
86
-
87
- return { result: cleanText.substring(0, 60) || 'done', coins: 0 };
152
+ return await resolveDigFlow({ channel, waitForDankMemer, response });
88
153
  }
89
154
 
90
155
  module.exports = { runDig };
@@ -62,6 +62,13 @@ function parseFarmGrowReadySec(text) {
62
62
  if (tsNearReady.length > 0) return Math.max(5, Math.min(...tsNearReady));
63
63
 
64
64
  // Fallback textual patterns.
65
+ const willReadyH = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*hour/i);
66
+ if (willReadyH) return Math.max(5, parseInt(willReadyH[1], 10) * 3600);
67
+ const willReadyM = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*minute/i);
68
+ if (willReadyM) return Math.max(5, parseInt(willReadyM[1], 10) * 60);
69
+ const willReadyS = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*second/i);
70
+ if (willReadyS) return Math.max(5, parseInt(willReadyS[1], 10));
71
+
65
72
  const h = clean.match(/ready\s+in\s+(\d+)\s*hour/i);
66
73
  if (h) return Math.max(5, parseInt(h[1], 10) * 3600);
67
74
  const mi = clean.match(/ready\s+in\s+(\d+)\s*minute/i);
@@ -977,7 +984,7 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
977
984
 
978
985
  if (!response) {
979
986
  LOG.warn('[farm] No response');
980
- return { result: 'no response', coins: 0 };
987
+ return { result: 'no response', coins: 0, nextCooldownSec: 90 };
981
988
  }
982
989
 
983
990
  if (isHoldTight(response)) {
@@ -1022,7 +1029,7 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
1022
1029
  LOG.warn('[farm] Subcommand required response detected; retrying with "pls farm view"');
1023
1030
  await channel.send('pls farm view');
1024
1031
  const retry = await waitForDankMemer(12000);
1025
- if (!retry) return { result: 'no response after farm view retry', coins: 0 };
1032
+ if (!retry) return { result: 'no response after farm view retry', coins: 0, nextCooldownSec: 120 };
1026
1033
  response = retry;
1027
1034
  if (isCV2(response)) await ensureCV2(response);
1028
1035
  logMsg(response, 'farm-retry-view');
@@ -1505,7 +1512,11 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
1505
1512
  }
1506
1513
 
1507
1514
  const coins = parseCoins(text);
1508
- let nextCd = parseFarmCooldownSec(text) || 10;
1515
+ let nextCd = parseFarmCooldownSec(text) || 30;
1516
+ const growReadyEnd = parseFarmGrowReadySec(text);
1517
+ if (Number.isFinite(growReadyEnd) && growReadyEnd > 0) {
1518
+ nextCd = Math.max(nextCd, Math.min(6 * 3600, growReadyEnd + 2));
1519
+ }
1509
1520
  const missingEnd = parseMissingFarmItem(text);
1510
1521
  if (missingEnd) {
1511
1522
  LOG.warn(`[farm] Missing ${c.bold}${missingEnd}${c.reset} after action — will retry in 1h`);
@@ -8,6 +8,7 @@ const { buyItem } = require('./shop');
8
8
  const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
9
9
  const STREAM_ACTION_LABELS = Object.freeze(['run ad', 'read chat', 'collect donations']);
10
10
  const RE_STREAM_INTERACT_MIN = /interact\s+with\s+your\s+stream\s+every\s+`?(\d+)`?\s*minutes?/i;
11
+ const RE_STREAM_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
11
12
 
12
13
  function normalizeLower(text) {
13
14
  return String(text || '')
@@ -123,11 +124,40 @@ function isActionResultText(lowerText) {
123
124
 
124
125
  function parseStreamInteractCooldownSec(text) {
125
126
  const clean = String(stripAnsi(text || '')).replace(/\s+/g, ' ').trim();
127
+
128
+ // Primary: explicit interaction cadence from dashboard text.
126
129
  const mm = clean.match(RE_STREAM_INTERACT_MIN);
127
130
  if (mm) {
128
131
  const mins = parseInt(mm[1], 10);
129
132
  if (Number.isFinite(mins) && mins > 0) return mins * 60;
130
133
  }
134
+
135
+ // Secondary: derive from "last streamed" marker (10m base cooldown).
136
+ const lower = normalizeLower(clean);
137
+ const now = Math.floor(Date.now() / 1000);
138
+ const tsMatches = Array.from(clean.matchAll(RE_STREAM_TS));
139
+ for (const m of tsMatches) {
140
+ const ts = parseInt(m[1], 10);
141
+ if (!Number.isFinite(ts) || ts <= 0) continue;
142
+ const idx = m.index ?? 0;
143
+ const left = lower.slice(Math.max(0, idx - 70), idx);
144
+ if (!left.includes('last streamed')) continue;
145
+ const elapsed = Math.max(0, now - ts);
146
+ const remain = Math.max(5, 600 - elapsed);
147
+ return remain;
148
+ }
149
+
150
+ // Textual fallback: "last streamed X minutes ago".
151
+ const ago = lower.match(/last\s+streamed[^\d]{0,24}(\d+)\s*(hour|hr|hrs|minute|min|mins|second|sec|secs)\s+ago/i);
152
+ if (ago) {
153
+ const n = parseInt(ago[1], 10);
154
+ const unit = ago[2].toLowerCase();
155
+ if (Number.isFinite(n) && n >= 0) {
156
+ const elapsed = unit.startsWith('h') ? n * 3600 : unit.startsWith('m') ? n * 60 : n;
157
+ return Math.max(5, 600 - elapsed);
158
+ }
159
+ }
160
+
131
161
  return 600;
132
162
  }
133
163
 
@@ -171,7 +201,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
171
201
 
172
202
  if (!response) {
173
203
  LOG.warn('[stream] No response');
174
- return { result: 'no response', coins: 0 };
204
+ return { result: 'no response', coins: 0, nextCooldownSec: 120 };
175
205
  }
176
206
 
177
207
  if (isHoldTight(response)) {
@@ -199,14 +229,14 @@ async function runStream({ channel, waitForDankMemer, client }) {
199
229
 
200
230
  for (const item of itemsToBuy) {
201
231
  const bought = await buyItem({ channel, waitForDankMemer, client, itemName: item, quantity: 1 });
202
- if (!bought) return { result: `need ${item} (buy failed)`, coins: 0 };
232
+ if (!bought) return { result: `need ${item} (buy failed)`, coins: 0, nextCooldownSec: 1800 };
203
233
  await humanDelay(500, 1000);
204
234
  }
205
235
 
206
236
  await sleep(2000);
207
237
  await channel.send('pls stream');
208
238
  response = await waitForDankMemer(12000);
209
- if (!response) return { result: 'no response after buy', coins: 0 };
239
+ if (!response) return { result: 'no response after buy', coins: 0, nextCooldownSec: 180 };
210
240
  await hydrate(response);
211
241
  logMsg(response, 'stream-retry');
212
242
  text = getFullText(response);
@@ -94,8 +94,26 @@ const RE_HOLD_TIGHT_REASON = /Reason:\s*\/(\w+)/i;
94
94
  // ── Sleep ────────────────────────────────────────────────────
95
95
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
96
96
 
97
- function humanDelay(min = 50, max = 200) {
98
- return new Promise(r => setTimeout(r, min + Math.random() * (max - min)));
97
+ // ── Advanced Human Delay (Anti-Detection) ────────────────────
98
+ // Natural human-like timing: 0.5-1.0s base with subtle variations
99
+ let _antiDetect = null;
100
+ try {
101
+ _antiDetect = require('../antiDetect');
102
+ } catch {}
103
+
104
+ function humanDelay(contextOrMin, max) {
105
+ // Backward compatibility: if called with numeric args, use simple delay
106
+ if (typeof contextOrMin === 'number') {
107
+ const min = contextOrMin;
108
+ const maxVal = typeof max === 'number' ? max : 1000;
109
+ return new Promise(r => setTimeout(r, min + Math.random() * (maxVal - min)));
110
+ }
111
+ // New API: context-based adaptive delay (uses 0.5-1s range)
112
+ if (_antiDetect && typeof contextOrMin === 'object') {
113
+ return _antiDetect.humanDelay(contextOrMin);
114
+ }
115
+ // Default: natural human timing (0.5-1.0s)
116
+ return new Promise(r => setTimeout(r, 500 + Math.random() * 500));
99
117
  }
100
118
 
101
119
  // ── Message Parsing ──────────────────────────────────────────
@@ -23,7 +23,7 @@ const {
23
23
 
24
24
  const RE_MEMORY_BACKTICK_CHUNK = /`([^`]+)`/g;
25
25
  const RE_BACKTICK_STRIP = /`/g;
26
- const RE_WORK_COOLDOWN_TS = /<t:(\d+):R>/;
26
+ const RE_WORK_COOLDOWN_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
27
27
  const RE_WORK_COOLDOWN_MINUTES = /(\d+)\s*minute/i;
28
28
  const RE_WORK_COOLDOWN_HOURS = /(\d+)\s*hour/i;
29
29
 
@@ -78,18 +78,42 @@ function parseMemoryOrder(text) {
78
78
  * Patterns: "next shift <t:TIMESTAMP:R>", "X minutes", "X hours"
79
79
  */
80
80
  function parseWorkCooldown(text) {
81
- // Unix timestamp pattern: <t:TIMESTAMP:R>
82
- const tsMatch = text.match(RE_WORK_COOLDOWN_TS);
83
- if (tsMatch) {
84
- const ts = parseInt(tsMatch[1]);
85
- const now = Math.floor(Date.now() / 1000);
86
- const diff = ts - now;
87
- return diff > 0 ? diff : 3600;
81
+ const clean = String(stripAnsi(text || ''));
82
+ const lower = normalizeLower(clean).replace(/\s+/g, ' ').trim();
83
+
84
+ // Unix timestamp pattern: <t:TIMESTAMP:*>
85
+ const now = Math.floor(Date.now() / 1000);
86
+ const tsMatches = Array.from(clean.matchAll(RE_WORK_COOLDOWN_TS));
87
+ if (tsMatches.length > 0) {
88
+ let best = null;
89
+ for (const m of tsMatches) {
90
+ const ts = parseInt(m[1], 10);
91
+ if (!Number.isFinite(ts)) continue;
92
+ const diff = ts - now;
93
+ if (diff <= 0) continue;
94
+ if (best == null || diff < best) best = diff;
95
+ }
96
+ if (best != null) return Math.max(5, best);
97
+ }
98
+
99
+ // Compact forms frequently seen in buttons/messages: "1h", "54m", "30s"
100
+ if (/(next shift|work again|cooldown|already done|shift)/i.test(lower)) {
101
+ const short = lower.match(/(\d+)\s*(h|hr|hrs|m|min|mins|s|sec|secs)\b/i);
102
+ if (short) {
103
+ const n = parseInt(short[1], 10);
104
+ const u = short[2].toLowerCase();
105
+ if (Number.isFinite(n) && n > 0) {
106
+ if (u.startsWith('h')) return n * 3600;
107
+ if (u.startsWith('m')) return n * 60;
108
+ return n;
109
+ }
110
+ }
88
111
  }
112
+
89
113
  // "X minutes" / "X hours" pattern
90
- const minMatch = text.match(RE_WORK_COOLDOWN_MINUTES);
114
+ const minMatch = clean.match(RE_WORK_COOLDOWN_MINUTES);
91
115
  if (minMatch) return parseInt(minMatch[1]) * 60;
92
- const hrMatch = text.match(RE_WORK_COOLDOWN_HOURS);
116
+ const hrMatch = clean.match(RE_WORK_COOLDOWN_HOURS);
93
117
  if (hrMatch) return parseInt(hrMatch[1]) * 3600;
94
118
  return null;
95
119
  }
@@ -311,7 +335,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
311
335
 
312
336
  if (!current) {
313
337
  LOG.warn('[work] No response');
314
- return { result: 'no response', coins: 0 };
338
+ return { result: 'no response', coins: 0, nextCooldownSec: 120 };
315
339
  }
316
340
 
317
341
  if (isHoldTight(current)) {
@@ -351,7 +375,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
351
375
 
352
376
  await channel.send('pls work shift');
353
377
  current = await waitForDankMemer(10000);
354
- if (!current) return { result: 'no response after apply', coins: 0 };
378
+ if (!current) return { result: 'no response after apply', coins: 0, nextCooldownSec: 180 };
355
379
  if (isCV2(current)) await ensureCV2(current);
356
380
  logMsg(current, 'work-after-apply');
357
381
  text = getFullText(current);
@@ -425,7 +449,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
425
449
  return { result: 'work shift completed', coins: 0, nextCooldownSec: finalCd || 3600 };
426
450
  }
427
451
 
428
- return { result: `work done`, coins: 0, nextCooldownSec: finalCd || null };
452
+ return { result: `work done`, coins: 0, nextCooldownSec: finalCd || 1800 };
429
453
  }
430
454
 
431
455
  module.exports = { runWorkShift, autoApplyForJob, resignFromJob, JOBS };