dankgrinder 7.1.0 → 7.6.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.
@@ -474,8 +474,8 @@ function getVisionRepairPlan(actionName, score) {
474
474
  }
475
475
  if (actionName === 'hoe') {
476
476
  const planted = score.ratios?.planted || 0;
477
- const wet = score.ratios?.wet || 0;
478
- if ((planted + wet) >= 0.45) {
477
+ const tilled = score.ratios?.tilled || 0;
478
+ if (planted >= 0.30 || tilled >= 0.30) {
479
479
  // Farm likely already progressed beyond hoe stage.
480
480
  return null;
481
481
  }
@@ -488,10 +488,9 @@ function getVisionRepairPlan(actionName, score) {
488
488
  function inferNextActionFromScore(actionName, score) {
489
489
  if (!score) return null;
490
490
  const planted = score.ratios?.planted || 0;
491
- const wet = score.ratios?.wet || 0;
492
491
  const tilled = score.ratios?.tilled || 0;
493
492
 
494
- if (actionName === 'hoe' && (planted + wet) >= 0.45) {
493
+ if (actionName === 'hoe' && (planted + tilled) >= 0.45) {
495
494
  return 'water';
496
495
  }
497
496
  if (actionName === 'water' && planted >= 0.45) {
@@ -515,16 +514,14 @@ async function inferPreferredActionFromImage(msg) {
515
514
  const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
516
515
  const ratios = {
517
516
  tilled: (analysis?.counts?.tilled || 0) / slots,
518
- wet: (analysis?.counts?.wet || 0) / slots,
519
517
  planted: (analysis?.counts?.planted || 0) / slots,
520
518
  unknown: (analysis?.counts?.unknown || 0) / slots,
521
519
  };
522
520
 
523
521
  let suggested = null;
524
- // Image-first phase routing.
525
- if (ratios.planted >= 0.45) suggested = 'plant';
526
- else if (ratios.wet >= 0.35) suggested = 'plant';
527
- else if (ratios.tilled >= 0.35) suggested = 'water';
522
+ // Image-first phase routing (3-state: tilled/planted/unknown, no separate wet state).
523
+ if (ratios.planted >= 0.30) suggested = 'plant';
524
+ else if (ratios.tilled >= 0.30) suggested = 'water';
528
525
  else suggested = 'hoe';
529
526
 
530
527
  let dbg = null;
@@ -541,7 +538,6 @@ async function inferPreferredActionFromImage(msg) {
541
538
 
542
539
  LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} suggested=${suggested} ratios=${JSON.stringify({
543
540
  tilled: +ratios.tilled.toFixed(3),
544
- wet: +ratios.wet.toFixed(3),
545
541
  planted: +ratios.planted.toFixed(3),
546
542
  unknown: +ratios.unknown.toFixed(3),
547
543
  })}`);
@@ -1117,31 +1113,30 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
1117
1113
  const slots = Math.max(1, (imageAnalysis.rows || 3) * (imageAnalysis.cols || 3));
1118
1114
  const ratios = {
1119
1115
  tilled: (imageAnalysis.counts?.tilled || 0) / slots,
1120
- wet: (imageAnalysis.counts?.wet || 0) / slots,
1121
1116
  planted: (imageAnalysis.counts?.planted || 0) / slots,
1122
1117
  unknown: (imageAnalysis.counts?.unknown || 0) / slots,
1123
1118
  };
1124
1119
 
1125
- // Phase-aware selection: pick the earliest incomplete phase.
1126
- // Harvest when planted >= 45%.
1127
- if (ratios.planted >= 0.45) {
1120
+ // Phase-aware selection: pick the earliest incomplete phase (3-state model).
1121
+ // Harvest when planted >= 30%.
1122
+ if (ratios.planted >= 0.30) {
1128
1123
  const btn = managedActions.harvest || btns.find(b => hasAny(b, ['harvest', 'reap', 'collect']));
1129
1124
  if (btn) return { action: 'harvest', button: btn, reason: `harvest-ready-vision(planted=${ratios.planted.toFixed(2)})` };
1130
1125
  }
1131
- // Plant when wet >= 45% or tilled >= 45%.
1132
- if (ratios.wet >= 0.45 || ratios.tilled >= 0.45) {
1126
+ // Plant when tilled >= 30%.
1127
+ if (ratios.tilled >= 0.30) {
1133
1128
  const btn = managedActions.plant || btns.find(b => hasAny(b, ['plant', 'seed', 'sow']));
1134
- if (btn) return { action: 'plant', button: btn, reason: `plant-vision(wet=${ratios.wet.toFixed(2)},tilled=${ratios.tilled.toFixed(2)})` };
1129
+ if (btn) return { action: 'plant', button: btn, reason: `plant-vision(tilled=${ratios.tilled.toFixed(2)})` };
1135
1130
  }
1136
- // Water when we have tilled tiles but not enough wet.
1137
- if (ratios.tilled >= 0.35 && ratios.wet < 0.35) {
1131
+ // Water when we have tilled tiles.
1132
+ if (ratios.tilled >= 0.15) {
1138
1133
  const btn = managedActions.water || btns.find(b => hasAny(b, ['water', 'watering']));
1139
- if (btn) return { action: 'water', button: btn, reason: `water-vision(tilled=${ratios.tilled.toFixed(2)},wet=${ratios.wet.toFixed(2)})` };
1134
+ if (btn) return { action: 'water', button: btn, reason: `water-vision(tilled=${ratios.tilled.toFixed(2)})` };
1140
1135
  }
1141
1136
  // Hoe when farm is mostly unknown/empty.
1142
- if (ratios.tilled < 0.35 && ratios.wet < 0.35 && ratios.planted < 0.35) {
1137
+ if (ratios.tilled < 0.15 && ratios.planted < 0.30) {
1143
1138
  const btn = managedActions.hoe || btns.find(b => hasAny(b, ['hoe', 'till']));
1144
- if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},w=${ratios.wet.toFixed(2)},p=${ratios.planted.toFixed(2)})` };
1139
+ if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},p=${ratios.planted.toFixed(2)})` };
1145
1140
  }
1146
1141
  }
1147
1142
 
@@ -82,12 +82,16 @@ function extractFarmImageUrl(msg) {
82
82
  function colorStatsForPixel(r, g, b) {
83
83
  const sum = r + g + b;
84
84
  const brightness = sum / 3;
85
- const greenStrong = g > r * 1.12 && g > b * 1.1;
86
- const blueStrong = b > r * 1.08 && b > g * 1.02;
87
- const brownish = r > g * 1.03 && g > b * 1.03 && r > 45 && g > 28 && b < 95;
88
- const dark = brightness < 56;
85
+ // Crop green harvest phase: large bright leaves, G significantly > R and B
86
+ const greenStrong = g > r * 1.25 && g > b * 1.25 && g > 38;
87
+ // Weeds/grass small scattered green, less intense than crop green
88
+ const weedGreen = g > r * 1.12 && g > b * 1.1 && g <= 38;
89
+ // Wet soil — rich dark brown, no blue, higher R than typical dry soil
90
+ const wetSoil = r > 50 && r > g * 1.5 && b < 20 && brightness < 45;
91
+ // Dry tilled soil — medium brown, debris textures
92
+ const drySoil = r > 35 && r > g * 1.2 && b < 30;
89
93
  const whiteish = brightness > 210 && Math.abs(r - g) < 20 && Math.abs(g - b) < 20;
90
- return { brightness, greenStrong, blueStrong, brownish, dark, whiteish };
94
+ return { brightness, greenStrong, weedGreen, wetSoil, drySoil, whiteish };
91
95
  }
92
96
 
93
97
  function clamp01(n) {
@@ -97,52 +101,34 @@ function clamp01(n) {
97
101
  function classifyCell(features) {
98
102
  const { greenPct, bluePct, brownPct, darkPct, avgBrightness } = features;
99
103
 
100
- // Score bands tuned for Dank farm palette.
101
- const scorePlanted =
102
- (greenPct * 1.9) +
103
- (clamp01((120 - avgBrightness) / 120) * 0.2) -
104
- (bluePct * 0.4);
105
-
106
- const scoreWet =
107
- (bluePct * 1.7) +
108
- (darkPct * 0.7) +
109
- (clamp01((105 - avgBrightness) / 105) * 0.15) -
110
- (greenPct * 0.35);
111
-
112
- const scoreTilled =
113
- (brownPct * 1.9) +
114
- (clamp01((125 - avgBrightness) / 125) * 0.1) -
115
- (greenPct * 0.25) -
116
- (bluePct * 0.2);
117
-
118
- const scored = [
119
- ['planted', scorePlanted],
120
- ['wet', scoreWet],
121
- ['tilled', scoreTilled],
122
- ].sort((a, b) => b[1] - a[1]);
123
-
124
- const [bestState, bestScore] = scored[0];
125
- const secondScore = scored[1][1];
126
- const margin = bestScore - secondScore;
127
-
128
- // Confidence from score margin + minimum signal level.
129
- const signal = Math.max(greenPct, bluePct, brownPct);
130
- const confidence = clamp01((margin * 2.1) + (signal * 0.6));
104
+ // ── HARVEST: Bright green mature crops (G >> R,B, G>38) ──
105
+ // Detected by greenStrong pixel percentage — crop leaves are vivid green
106
+ if (greenPct >= 0.30) {
107
+ const confidence = clamp01(0.5 + (greenPct - 0.30) * 2.0);
108
+ return { state: 'planted', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
109
+ }
131
110
 
132
- // Low-signal cells become unknown.
133
- if (signal < 0.085 || confidence < 0.20) {
134
- return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { planted: +scorePlanted.toFixed(3), wet: +scoreWet.toFixed(3), tilled: +scoreTilled.toFixed(3) } };
111
+ // ── TILLED / WET: Brown soil — no meaningful green or blue ──
112
+ if (brownPct >= 0.15) {
113
+ // High brown percentage = definitely soil. Both hoe (dry) and water (wet)
114
+ // phases look like brown soil. We classify as 'tilled' — the farming
115
+ // logic handles water vs hoe state transition by phase context.
116
+ const confidence = clamp01((brownPct - 0.15) * 2.5 + (1 - darkPct) * 0.3);
117
+ return { state: 'tilled', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
135
118
  }
136
119
 
137
- return {
138
- state: bestState,
139
- confidence: +confidence.toFixed(3),
140
- scores: {
141
- planted: +scorePlanted.toFixed(3),
142
- wet: +scoreWet.toFixed(3),
143
- tilled: +scoreTilled.toFixed(3),
144
- },
145
- };
120
+ // ── PLANTED (early stage): Soil with green weeds ──
121
+ // Cells with 5-30% green are likely soil with weeds/crops starting.
122
+ // This is distinct from harvest (30%+) which is handled above.
123
+ if (greenPct >= 0.05 && greenPct < 0.30) {
124
+ const confidence = clamp01((greenPct - 0.05) * 3.0);
125
+ return { state: 'planted', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
126
+ }
127
+
128
+ // ── UNKNOWN: Low signal — grass borders, artifacts, etc. ──
129
+ const signal = Math.max(greenPct, bluePct, brownPct);
130
+ const confidence = signal * 1.5;
131
+ return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
146
132
  }
147
133
 
148
134
  async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
@@ -160,18 +146,17 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
160
146
  const ey = Math.min(sy + cellH, height);
161
147
 
162
148
  // Sample center region only (ignore grass borders around each tile).
163
- const padX = Math.floor((ex - sx) * 0.18);
164
- const padY = Math.floor((ey - sy) * 0.18);
149
+ const padX = Math.floor((ex - sx) * 0.20); // 20% padding to cut more grass
150
+ const padY = Math.floor((ey - sy) * 0.20);
165
151
  const csx = sx + padX;
166
152
  const csy = sy + padY;
167
153
  const cex = ex - padX;
168
154
  const cey = ey - padY;
169
155
 
170
156
  let total = 0;
171
- let greenPx = 0;
172
- let bluePx = 0;
173
- let brownPx = 0;
174
- let darkPx = 0;
157
+ let greenPx = 0; // crop green: G > R*1.25 && G > B*1.25 && G > 38
158
+ let brownPx = 0; // soil brown: R > G*1.2 && B < 30
159
+ let darkPx = 0; // very dark: brightness < 56
175
160
  let whitePx = 0;
176
161
  let brightSum = 0;
177
162
 
@@ -184,22 +169,20 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
184
169
  const s = colorStatsForPixel(r, g, b);
185
170
  total++;
186
171
  brightSum += s.brightness;
187
- if (s.greenStrong) greenPx++;
188
- if (s.blueStrong) bluePx++;
189
- if (s.brownish) brownPx++;
172
+ if (s.greenStrong) greenPx++; // harvest green (vivid crop leaves)
173
+ if (s.drySoil || s.wetSoil) brownPx++; // soil — dry or wet
190
174
  if (s.dark) darkPx++;
191
175
  if (s.whiteish) whitePx++;
192
176
  }
193
177
  }
194
178
 
195
179
  const greenPct = greenPx / total;
196
- const bluePct = bluePx / total;
197
180
  const brownPct = brownPx / total;
198
181
  const darkPct = darkPx / total;
199
182
  const whitePct = whitePx / total;
200
183
  const avgBrightness = brightSum / total;
201
184
 
202
- const classified = classifyCell({ greenPct, bluePct, brownPct, darkPct, avgBrightness, whitePct });
185
+ const classified = classifyCell({ greenPct, brownPct, darkPct, avgBrightness });
203
186
 
204
187
  cells.push({
205
188
  row,
@@ -208,16 +191,14 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
208
191
  confidence: classified.confidence,
209
192
  scores: classified.scores,
210
193
  greenPct: +greenPct.toFixed(3),
211
- bluePct: +bluePct.toFixed(3),
212
194
  brownPct: +brownPct.toFixed(3),
213
195
  darkPct: +darkPct.toFixed(3),
214
- whitePct: +whitePct.toFixed(3),
215
196
  avgBrightness: +avgBrightness.toFixed(1),
216
197
  });
217
198
  }
218
199
  }
219
200
 
220
- const counts = { tilled: 0, wet: 0, planted: 0, unknown: 0 };
201
+ const counts = { tilled: 0, planted: 0, unknown: 0 };
221
202
  for (const c of cells) counts[c.state] = (counts[c.state] || 0) + 1;
222
203
 
223
204
  const avgConfidence = cells.length > 0
@@ -228,7 +209,7 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
228
209
  }
229
210
 
230
211
  function gridToString(analysis) {
231
- const icon = { tilled: 'T', wet: 'W', planted: 'P', unknown: '?' };
212
+ const icon = { tilled: 'T', planted: 'P', unknown: '?' };
232
213
  const rows = [];
233
214
  for (let r = 0; r < (analysis?.rows || 0); r++) {
234
215
  const line = [];
@@ -251,25 +232,23 @@ function evaluateActionNeed(actionName, analysis) {
251
232
  const slots = 9;
252
233
 
253
234
  // Avoid aggressive repeats when confidence is too low.
254
- if (conf < 0.22) return false;
235
+ if (conf < 0.20) return false;
255
236
 
256
- // Conservative rules: only request repeat if we have clear evidence.
237
+ // Simple rules:
238
+ // - hoe: repeat until farm has enough tilled/planted soil
239
+ // - water: skip (we don't distinguish wet vs tilled visually — use phase context instead)
240
+ // - plant: repeat until most cells show green crops
241
+ // - harvest: repeat if any planted crops visible
257
242
  if (actionName === 'hoe') {
258
- return (tilled + wet + planted) < slots && unknown < 6;
259
- }
260
- if (actionName === 'water') {
261
- // If still many plain-tilled (not watered/planted), water likely incomplete.
262
- return tilled > 0 && (wet + planted) < slots;
243
+ return (tilled + planted) < slots && unknown < 6;
263
244
  }
264
245
  if (actionName === 'plant') {
265
- return planted < slots && (tilled + wet) > 0;
246
+ return planted < slots && (tilled) > 0;
266
247
  }
267
248
  if (actionName === 'harvest') {
268
- // If many planted remain, harvest may still be pending.
269
249
  return planted > 0;
270
250
  }
271
-
272
- // Fertilizer optional; do not force repeats.
251
+ // water and others: don't force repeats
273
252
  return false;
274
253
  }
275
254
 
@@ -277,59 +256,45 @@ function evaluateActionScores(actionName, analysis) {
277
256
  const c = analysis?.counts || {};
278
257
  const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
279
258
  const tilled = c.tilled || 0;
280
- const wet = c.wet || 0;
281
259
  const planted = c.planted || 0;
282
260
  const unknown = c.unknown || 0;
283
261
  const conf = analysis?.avgConfidence || 0;
284
262
 
285
263
  const ratios = {
286
264
  tilled: tilled / slots,
287
- wet: wet / slots,
288
265
  planted: planted / slots,
289
266
  unknown: unknown / slots,
290
267
  };
291
268
 
292
269
  const clamp = (n) => Math.max(0, Math.min(1, n));
293
- const withConf = (base, mult = 0.25) => clamp(base * (0.82 + (conf * mult)));
270
+ const withConf = (base) => clamp(base * (0.85 + conf * 0.3));
294
271
 
295
272
  let score = 0;
296
- let threshold = 0.8;
273
+ let threshold = 0.7;
297
274
  let reason = 'ok';
298
275
 
299
276
  if (actionName === 'hoe') {
300
- // Hoe is considered good if farm has mostly actionable soil/crops and little unknown noise.
301
- const base = (ratios.tilled * 0.9) + (ratios.wet * 0.52) + (ratios.planted * 0.4) - (ratios.unknown * 0.32);
302
- score = withConf(base);
303
- threshold = 0.68;
304
- if (ratios.unknown > 0.45) reason = 'too_many_unknown_cells';
305
- else if (ratios.tilled < 0.45 && (ratios.wet + ratios.planted) < 0.35) reason = 'not_enough_tilled_or_progressed_tiles';
306
- } else if (actionName === 'water') {
307
- // After watering, we want wet/planted dominance and minimal plain-tilled.
308
- const base = (ratios.wet * 0.92) + (ratios.planted * 0.34) - (ratios.tilled * 0.48) - (ratios.unknown * 0.22);
277
+ // Hoe is good if farm has mostly soil (tilled or planted)
278
+ const base = (ratios.tilled * 0.9) + (ratios.planted * 0.5) - (ratios.unknown * 0.4);
309
279
  score = withConf(base);
310
- threshold = 0.7;
311
- if (ratios.planted >= 0.45) {
312
- // If many planted tiles are already visible, watering phase is effectively done.
313
- score = Math.max(score, withConf(0.86));
314
- reason = 'progressed_to_plant_phase';
315
- }
316
- if (ratios.wet < 0.45 && ratios.planted < 0.2) reason = 'not_enough_wet_tiles';
317
- else if (ratios.tilled > 0.4) reason = 'too_many_dry_tilled_tiles';
280
+ threshold = 0.55;
281
+ if (ratios.unknown > 0.5) reason = 'too_many_unknown_cells';
282
+ else if (ratios.tilled + ratios.planted < 0.4) reason = 'farm_not_ready';
318
283
  } else if (actionName === 'plant') {
319
- // Plant stage should end with mostly planted tiles.
320
- const base = (ratios.planted * 0.96) + (ratios.wet * 0.12) - (ratios.tilled * 0.48) - (ratios.unknown * 0.2);
284
+ // Plant should end with mostly planted cells
285
+ const base = (ratios.planted * 0.98) - (ratios.tilled * 0.3) - (ratios.unknown * 0.2);
321
286
  score = withConf(base);
322
- threshold = 0.72;
323
- if (ratios.planted < 0.6) reason = 'not_enough_planted_tiles';
287
+ threshold = 0.65;
288
+ if (ratios.planted < 0.5) reason = 'not_enough_planted_tiles';
324
289
  } else if (actionName === 'harvest') {
325
- // Harvest should reduce planted tiles; tilled/wet should dominate after collection.
326
- const base = ((ratios.tilled + ratios.wet) * 0.86) - (ratios.planted * 0.76) - (ratios.unknown * 0.18);
290
+ // Harvest is good if planted tiles dominate
291
+ const base = (ratios.planted * 0.9) - (ratios.tilled * 0.1) - (ratios.unknown * 0.2);
327
292
  score = withConf(base);
328
- threshold = 0.68;
329
- if (ratios.planted > 0.34) reason = 'too_many_unharvested_plants';
293
+ threshold = 0.5;
294
+ if (ratios.planted < 0.3) reason = 'no_plants_to_harvest';
330
295
  } else {
331
- score = withConf(0.7 - (ratios.unknown * 0.2));
332
- threshold = 0.7;
296
+ score = withConf(0.6 - ratios.unknown * 0.3);
297
+ threshold = 0.6;
333
298
  }
334
299
 
335
300
  const matched = conf >= 0.2 && score >= threshold;
@@ -341,10 +306,9 @@ function evaluateActionScores(actionName, analysis) {
341
306
  threshold: +threshold.toFixed(3),
342
307
  confidence: +conf.toFixed(3),
343
308
  reason,
344
- counts: { tilled, wet, planted, unknown, slots },
309
+ counts: { tilled, planted, unknown, slots },
345
310
  ratios: {
346
311
  tilled: +ratios.tilled.toFixed(3),
347
- wet: +ratios.wet.toFixed(3),
348
312
  planted: +ratios.planted.toFixed(3),
349
313
  unknown: +ratios.unknown.toFixed(3),
350
314
  },
@@ -558,7 +558,7 @@ async function clickCV2Button(msg, customId) {
558
558
  const gId = msg.guildId || msg.guild?.id;
559
559
  if (!token) throw new Error('No token for CV2 click');
560
560
  const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
561
- const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
561
+ const nonce = `${(BigInt(Date.now() - 1420070400000) << 22n)}`;
562
562
  const payloadObj = {
563
563
  type: 3,
564
564
  application_id: String(msg.applicationId || DANK_MEMER_ID),
package/lib/rawLogger.js CHANGED
@@ -31,16 +31,41 @@ let memIdx = 0;
31
31
 
32
32
  // ── Redis init ──
33
33
  async function init(redisUrl) {
34
- if (!redisUrl) return;
34
+ if (!redisUrl) {
35
+ console.log('[rawLogger] No Redis URL — raw logging disabled');
36
+ return;
37
+ }
35
38
  try {
36
39
  const Redis = require('ioredis');
37
40
  redis = new Redis(redisUrl, {
38
41
  maxRetriesPerRequest: 2,
39
42
  retryStrategy: (times) => times > 3 ? null : Math.min(times * 500, 3000),
40
- lazyConnect: true,
43
+ lazyConnect: false,
44
+ });
45
+ // Wait for actual connection before marking ready
46
+ await new Promise((resolve, reject) => {
47
+ const timeout = setTimeout(() => {
48
+ reject(new Error('Redis connection timeout (5s)'));
49
+ }, 5000);
50
+ redis.once('ready', () => { clearTimeout(timeout); resolve(); });
51
+ redis.once('error', (e) => { clearTimeout(timeout); reject(e); });
52
+ redis.connect().catch(reject);
41
53
  });
42
- await redis.connect();
43
54
  redisReady = true;
55
+ console.log('[rawLogger] Redis connected');
56
+ redis.on('error', (e) => {
57
+ console.error(`[rawLogger] Redis error: ${e.message}`);
58
+ redisReady = false;
59
+ });
60
+ redis.on('close', () => {
61
+ redisReady = false;
62
+ });
63
+ redis.on('reconnecting', () => {
64
+ redisReady = false;
65
+ });
66
+ redis.on('ready', () => {
67
+ redisReady = true;
68
+ });
44
69
  } catch (e) {
45
70
  console.error(`[rawLogger] Redis connect failed: ${e.message}`);
46
71
  redis = null;
@@ -105,8 +130,19 @@ function extractEmbedText(embeds) {
105
130
 
106
131
  // ── Detect command from components/embeds ──
107
132
  function detectCommand(d) {
133
+ // Build a combined text source that works even when d.components is a JSON string
134
+ // (stored in Redis and retrieved as a string breaks for-of iteration over objects).
135
+ // Prefer pre-extracted allText (stored in parseRawPacket) over re-extracting.
136
+ const cv2Texts = extractTexts(d.components);
137
+ const cv2Text = cv2Texts.join(' ').toLowerCase();
138
+ const contentText = (d.content || '').toLowerCase();
139
+ const embedText = extractEmbedText(d.embeds).toLowerCase();
140
+ // allText combines all text fields — used as a fallback when components can't be parsed
141
+ const allText = [contentText, cv2Text, embedText].join(' ').trim();
142
+
108
143
  // Check button custom_ids
109
144
  const walk = (items) => {
145
+ if (typeof items === 'string') return null;
110
146
  for (const c of (items || [])) {
111
147
  const cid = c.custom_id || '';
112
148
  // Gambling
@@ -140,101 +176,171 @@ function detectCommand(d) {
140
176
  const fromBtn = walk(d.components);
141
177
  if (fromBtn) return fromBtn;
142
178
 
143
- // Check CV2 text content
144
- const cv2Text = extractTexts(d.components).join(' ').toLowerCase();
145
- if (cv2Text.includes('coin toss')) return 'cointoss';
146
- if (cv2Text.includes('blackjack')) return 'blackjack';
147
- if (cv2Text.includes('roulette')) return 'roulette';
148
- if (cv2Text.includes('slots')) return 'slots';
149
- if (cv2Text.includes('snakeeyes') || cv2Text.includes('snake eyes')) return 'snakeeyes';
150
- if (cv2Text.includes('cooldown') || cv2Text.includes('again <t:')) return 'cooldown';
151
- // Non-gambling CV2
152
- if (cv2Text.includes('fishing') || cv2Text.includes('fisherfolk')) return 'fish';
153
- if (cv2Text.includes('deposit') || cv2Text.includes('bank account')) return 'deposit';
154
- if (cv2Text.includes('begging') || cv2Text.includes('imagine begging')) return 'beg';
155
- if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle') || cv2Text.includes('your aim was so bad') || cv2Text.includes('animals laughed') || cv2Text.includes('animals attacked') || cv2Text.includes('barely escaped') || cv2Text.includes('fell asleep in a tree') || cv2Text.includes('caught nothing') || cv2Text.includes('brought back literally nothing') || cv2Text.includes('rifle broke')) return 'hunt';
156
- if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig') || cv2Text.includes('dug in the dirt') || cv2Text.includes('brought back') && (cv2Text.includes('ant') || cv2Text.includes('worm') || cv2Text.includes('stickbug') || cv2Text.includes('ladybug'))) return 'dig';
157
- if (cv2Text.includes('great work') || cv2Text.includes('for your shift') || cv2Text.includes('working as') || cv2Text.includes('work shift') || cv2Text.includes('what color was') || cv2Text.includes('remember words order') || cv2Text.includes('remember the colors') || cv2Text.includes('remember the emojis') || cv2Text.includes('what word was repeated') || cv2Text.includes('unscramble') || cv2Text.includes('remember the number') || cv2Text.includes('click the buttons in correct order') || cv2Text.includes('babysitter') || cv2Text.includes('click the matching')) return 'work';
158
- if (cv2Text.includes('weekly')) return 'weekly';
159
- if (cv2Text.includes('daily')) return 'daily';
160
- if (cv2Text.includes('inventory')) return 'inventory';
161
- if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
162
- if (cv2Text.includes('balances') && cv2Text.includes('global rank')) return 'balance';
163
-
164
- // Check content text (plain message content)
165
- const contentText = (d.content || '').toLowerCase();
166
- if (contentText.includes('balances') && contentText.includes('global rank')) return 'balance';
167
- if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed')) return 'hunt';
168
- if (contentText.includes('imagine going into the woods')) return 'hunt';
169
- if (contentText.includes('you ran an ad for') && contentText.includes('received')) return 'stream';
170
- if (contentText.includes('you can\'t interact with your stream')) return 'stream';
171
- if (contentText.includes('you dug in the dirt') || contentText.includes('found nothing while digging')) return 'dig';
172
-
173
- // Check embed text
174
- const embedText = extractEmbedText(d.embeds).toLowerCase();
175
- // Gambling
176
- if (embedText.includes('high') && embedText.includes('low') && embedText.includes('secret number')) return 'highlow';
177
- if (embedText.includes('you won') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
178
- if (embedText.includes('you lost') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
179
- if (embedText.includes('blackjack') || embedText.includes('dealer')) return 'blackjack';
180
- if (embedText.includes('roulette')) return 'roulette';
181
- if (embedText.includes('spinning') && embedText.includes('slots')) return 'slots';
182
- if (embedText.includes('snakeeyes') || embedText.includes('snake eyes') || embedText.includes('dice')) return 'snakeeyes';
183
- if (embedText.includes('scratch')) return 'scratch';
184
- // Adventure
185
- if (embedText.includes('adventure') || embedText.includes('choose an adventure')) return 'adventure';
186
- // Crime / search
187
- if (embedText.includes('what crime do you want')) return 'crime';
188
- if (embedText.includes('where do you want to search')) return 'search';
189
- if (embedText.includes('you searched') || embedText.includes('searched the')) return 'search';
190
- if (embedText.includes('committed') && (embedText.includes('trespassing') || embedText.includes('identity theft') || embedText.includes('fraud') || embedText.includes('shoplifting') || embedText.includes('dui') || embedText.includes('tax evasion') || embedText.includes('littering') || embedText.includes('cyber bullying') || embedText.includes('grand theft auto') || embedText.includes('drug distribution') || embedText.includes('bank robbing') || embedText.includes('arson') || embedText.includes('murder') || embedText.includes('vandalism') || embedText.includes('jaywalking') || embedText.includes('piracy') || embedText.includes('breaking and entering'))) return 'crime';
191
- if (embedText.includes('you committed') || embedText.includes('went outside')) return 'crime';
192
- if (embedText.includes('stole a developer') || embedText.includes('got confused about what trespassing')) return 'crime';
193
- // Search results (person names) - also check for beg results
194
- if ((embedText.includes('oh you poor soul') || embedText.includes('take this') || embedText.includes('sure take') || embedText.includes('here\'s a thought') || embedText.includes('nope, nothing') || embedText.includes('no u') || embedText.includes('coins? in this economy')) && (embedText.includes('###') || embedText.includes('charlie chaplin') || embedText.includes('shrek') || embedText.includes('elton john') || embedText.includes('alexa') || embedText.includes('confucius') || embedText.includes('doctor strange') || embedText.includes('rick astley') || embedText.includes('toby turner') || embedText.includes('oprah') || embedText.includes('bruce lee') || embedText.includes('david attenborough') || embedText.includes('honey badger'))) {
195
- // Check if it's a beg result (has life saver or specific beg text)
196
- if (embedText.includes('life saver') || embedText.includes('lifesaver')) return 'beg';
179
+ // ── Shared helpers ──
180
+ const has = (text, needle) => text.includes(needle);
181
+ const anyOf = (text, needles) => needles.some(n => text.includes(n));
182
+ const hasAll = (text, needles) => needles.every(n => text.includes(n));
183
+
184
+ // ── CV2 text content ──
185
+ if (anyOf(cv2Text, ['coin toss'])) return 'cointoss';
186
+ if (has(cv2Text, 'blackjack')) return 'blackjack';
187
+ if (has(cv2Text, 'roulette')) return 'roulette';
188
+ if (has(cv2Text, 'slots')) return 'slots';
189
+ if (anyOf(cv2Text, ['snakeeyes', 'snake eyes'])) return 'snakeeyes';
190
+ if (anyOf(cv2Text, ['fishing', 'fisherfolk'])) return 'fish';
191
+ if (anyOf(cv2Text, ['deposit', 'bank account'])) return 'deposit';
192
+ // Stream success (ephemeral DM with ad results) catch early before other checks
193
+ if (anyOf(cv2Text, ['ran an ad', 'received', 'for an ad', 'sponsor'])) return 'stream';
194
+ // Beg detection — check beg phrases FIRST (before person name check)
195
+ // CV2 text may not have ### header, so match beg phrases directly
196
+ if (anyOf(cv2Text, ['oh you poor soul', 'life saver', 'lifesaver', 'sure take this', "here's a thought", 'take this'])) return 'beg';
197
+ if (anyOf(cv2Text, ['begging', 'imagine begging'])) return 'beg';
198
+ if (anyOf(cv2Text, ['you must be fun at parties', 'guess ', ' hates u', 'hates u', 'you were denied', 'get away', 'piss poor attempt', 'too poor', "wouldn't have given"])) return 'beg';
199
+ if (anyOf(cv2Text, ['hunting', 'went hunting', 'hunting rifle', 'your aim was so bad', 'animals laughed', 'animals attacked', 'barely escaped', 'fell asleep in a tree', 'caught nothing', 'brought back literally nothing', 'rifle broke', 'imagine going into the woods to hunt', 'laughed you out of the forest', 'brought back ant', 'brought back worm', 'brought back stickbug', 'brought back ladybug'])) return 'hunt';
200
+ if (anyOf(cv2Text, ['digging', 'found nothing while', 'you dug', 'dug in the dirt', 'what are the odds lol'])) return 'dig';
201
+ if (anyOf(cv2Text, ['brought back']) && anyOf(cv2Text, ['ant', 'worm', 'stickbug', 'ladybug'])) return 'dig';
202
+ // Work minigame messages color/word, basketball, and emoji variants
203
+ if (anyOf(cv2Text, ['great work', 'for your shift', 'working as', 'work shift', 'what color was', 'remember words order', 'remember the colors', 'remember the emojis', 'what word was repeated', 'unscramble', 'remember the number', 'click the buttons in correct order', 'babysitter', 'click the matching', 'dunk the ball', 'wastebasket', 'look at the emoji', 'what was the emoji'])) return 'work';
204
+ if (has(cv2Text, 'weekly')) return 'weekly';
205
+ if (has(cv2Text, 'daily')) return 'daily'; // must be BEFORE cooldown (also matches "again <t:")
206
+ if (has(cv2Text, 'inventory')) return 'inventory';
207
+ if (has(cv2Text, 'level:')) return 'profile';
208
+ // Premium / ability upgrade message
209
+ if (anyOf(cv2Text, ['you can buy the ability', 'premium feature', 'upgrade to premium'])) return 'premium';
210
+ if (hasAll(cv2Text, ['balances', 'global rank'])) return 'balance';
211
+ // Balance: "username's Balances" format — less strict check
212
+ if (anyOf(cv2Text, ["'s balances", 'balances', 'global rank', 'net worth'])) return 'balance';
213
+ // Work cooldown message specific check before generic cooldown
214
+ if (has(cv2Text, 'you can work again at')) return 'work';
215
+ // Deposit validation errors (CV2) catch these before cooldown
216
+ if (anyOf(cv2Text, ['amount needs to be greater than 0'])) return 'deposit';
217
+ // Gambling validation errors (CV2) catch these before unknown
218
+ if (anyOf(cv2Text, ["can't bet less", 'bet less than', 'minimum bet', 'insufficient', 'not enough'])) return 'cointoss';
219
+ // Generic cooldown — must be AFTER specific command checks
220
+ if (anyOf(cv2Text, ['cooldown', 'again <t:'])) return 'cooldown';
221
+ if (has(cv2Text, 'you must specify a subcommand')) return 'shop';
222
+
223
+ // ── Content text ──
224
+ if (hasAll(contentText, ['balances', 'global rank'])) return 'balance';
225
+ if (anyOf(contentText, ["'s balances", 'balances', 'global rank', 'net worth'])) return 'balance';
226
+ if (anyOf(contentText, ['your aim was so bad', 'animals laughed', 'imagine going into the woods', 'laughed you out of', 'hunt', 'hunting', 'brought back'])) return 'hunt';
227
+ if (anyOf(contentText, ['you dug', 'dug in the dirt', 'found nothing while digging'])) return 'dig';
228
+ if (anyOf(contentText, ['you ran an ad for', 'received'])) return 'stream';
229
+ if (has(contentText, "can't interact with your stream")) return 'stream';
230
+ // Beg failure person denied the request (plain content, not CV2)
231
+ if (anyOf(contentText, ['you must be fun at parties', 'guess ', ' hates u', 'get away', 'piss poor attempt', 'too poor', "wouldn't have given"])) return 'beg';
232
+ // Work minigame messages — these arrive as plain text (d.content), not embeds
233
+ if (anyOf(contentText, ['what color was', 'terrible work', 'lost the mini-game', 'you lost the mini-game', 'lost because you didn', 'what word was repeated', 'remember words in order', 'remember the word', 'look at each color', 'unscramble the word', 'dunk the ball', 'wastebasket', 'look at the emoji', 'what was the emoji', 'emoji closely'])) return 'work';
234
+
235
+ // ── Embed text ──
236
+ // Highlow — check "the hidden number was" which appears in ALL result messages
237
+ if (anyOf(embedText, ['you won', 'you lost']) && has(embedText, 'the hidden number was')) return 'highlow';
238
+ if (hasAll(embedText, ['high', 'low', 'secret number'])) return 'highlow';
239
+ if (anyOf(embedText, ['blackjack', 'dealer'])) return 'blackjack';
240
+ if (has(embedText, 'roulette')) return 'roulette';
241
+ if (hasAll(embedText, ['spinning', 'slots'])) return 'slots';
242
+ if (anyOf(embedText, ['snakeeyes', 'snake eyes', 'dice'])) return 'snakeeyes';
243
+ if (has(embedText, 'scratch')) return 'scratch';
244
+ if (anyOf(embedText, ['adventure', 'choose an adventure'])) return 'adventure';
245
+ if (has(embedText, 'what crime do you want')) return 'crime';
246
+ if (has(embedText, 'where do you want to search')) return 'search';
247
+ if (anyOf(embedText, ['you searched', 'searched the'])) return 'search';
248
+ if (has(embedText, 'what crime do you want')) return 'crime';
249
+ if (has(embedText, 'committed') && anyOf(embedText, ['trespassing', 'identity theft', 'fraud', 'shoplifting', 'dui', 'tax evasion', 'littering', 'cyber bullying', 'grand theft auto', 'drug distribution', 'bank robbing', 'arson', 'murder', 'vandalism', 'jaywalking', 'piracy', 'breaking and entering'])) return 'crime';
250
+ if (anyOf(embedText, ['you committed', 'went outside', 'stole a developer', 'got confused about what trespassing'])) return 'crime';
251
+ // Search / beg result: person names in ### header
252
+ if (has(embedText, '###') && anyOf(embedText, ['charlie chaplin', 'shrek', 'elton john', 'alexa', 'confucius', 'doctor strange', 'rick astley', 'toby turner', 'oprah', 'bruce lee', 'david attenborough', 'honey badger', 'a honey badger', 'nope, nothing', 'no u', "here's a thought"])) {
253
+ if (anyOf(embedText, ['life saver', 'lifesaver', 'oh you poor soul', 'take this', 'sure take', 'coins? in this economy'])) return 'beg';
197
254
  return 'search';
198
255
  }
199
- // Hunt / dig
200
- if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('your aim was so bad') || embedText.includes('animals laughed') || embedText.includes('animals attacked') || embedText.includes('barely escaped') || embedText.includes('fell asleep in a tree') || embedText.includes('caught nothing') || embedText.includes('brought back literally nothing') || embedText.includes('rifle broke') || embedText.includes('imagine going into the woods')) return 'hunt';
201
- if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('dug in the dirt') || embedText.includes('found nothing while') || embedText.includes('what are the odds lol') || embedText.includes('brought back') && (embedText.includes('ant') || embedText.includes('worm') || embedText.includes('stickbug') || embedText.includes('ladybug'))) return 'dig';
202
- // Work match both minigame prompt AND completion
203
- if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color') || embedText.includes('babysitter') || embedText.includes('great work') || embedText.includes('for your shift'))) return 'work';
204
- if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
205
- if (embedText.includes('working as') || embedText.includes('for your shift')) return 'work';
206
- if (embedText.includes('remember words order') || embedText.includes('remember the colors') || embedText.includes('remember the emojis') || embedText.includes('what word was repeated') || embedText.includes('unscramble the word') || embedText.includes('remember the number') || embedText.includes('click the buttons in correct order') || embedText.includes('click the matching')) return 'work';
207
- // Postmemes
208
- if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
209
- // Stream
210
- if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream') || embedText.includes('you ran an ad for') || embedText.includes('you received') && embedText.includes('from your sponsors') || embedText.includes('### chat') && embedText.includes('hasanbabi')) return 'stream';
211
- if (embedText.includes('you can\'t interact with your stream') || embedText.includes('stream can last')) return 'stream';
212
- // Deposit
213
- if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
214
- // Balance
215
- if (embedText.includes('balances') && embedText.includes('global rank') && embedText.includes('net worth')) return 'balance';
216
- // Trivia
217
- if (embedText.includes('you have 10 seconds to answer') || embedText.includes('you have 12 seconds to answer') || embedText.includes('you have 15 seconds to answer') || embedText.includes('trivia') || embedText.includes('difficulty') && embedText.includes('category') && (embedText.includes('correct answer was') || embedText.includes('you got that answer correct'))) return 'trivia';
218
- if (embedText.includes('who in pulp fiction') || embedText.includes('what was') || embedText.includes('which of')) return 'trivia';
219
- // Cooldown messages
220
- if (embedText.includes('you can work again at') || embedText.includes('you can use this command again')) return 'cooldown';
221
- if (embedText.includes('amount needs to be greater than 0')) return 'cooldown';
222
- // Premium/upgrade messages
223
- if (embedText.includes('you can buy the ability to use this command')) return 'premium';
224
- // Profile / level
225
- if (embedText.includes('level:') && embedText.includes('experience:')) return 'profile';
226
- // Shop
227
- if (embedText.includes('dank memer shop') || embedText.includes('successful purchase')) return 'shop';
228
- // Farm
229
- if (embedText.includes('farm') && (embedText.includes('harvest') || embedText.includes('plant') || embedText.includes('hoe') || embedText.includes('water'))) return 'farm';
230
- // Beg
231
- if (embedText.includes('begging')) return 'beg';
232
- // Daily/weekly quest
233
- if (embedText.includes('daily quest')) return 'daily';
234
- // Fish
235
- if (embedText.includes('fishing') || embedText.includes('fish')) return 'fish';
236
- // Hold tight
237
- if (embedText.includes('hold tight')) return 'holdtight';
256
+ if (anyOf(embedText, ['hunting', 'came back with', 'hunting rifle', "dragon's fireball", 'dodge the', 'went hunting', 'your aim was so bad', 'animals laughed', 'animals attacked', 'barely escaped', 'fell asleep in a tree', 'caught nothing', 'brought back literally nothing', 'rifle broke', 'imagine going into the woods'])) return 'hunt';
257
+ if (anyOf(embedText, ['digging', 'you dug', 'dug in the dirt', 'found nothing while', 'what are the odds lol'])) return 'dig';
258
+ if (anyOf(embedText, ['brought back']) && anyOf(embedText, ['ant', 'worm', 'stickbug', 'ladybug'])) return 'dig';
259
+ if (hasAll(embedText, ['work', 'shift'])) return 'work';
260
+ if (anyOf(embedText, ['great work', 'you were given', 'working as', 'for your shift'])) return 'work';
261
+ if (anyOf(embedText, ['remember words order', 'remember the colors', 'remember the emojis', 'what word was repeated', 'unscramble the word', 'remember the number', 'click the buttons in correct order', 'click the matching'])) return 'work';
262
+ if (anyOf(embedText, ['pick a meme', 'meme posting'])) return 'postmemes';
263
+ if (anyOf(embedText, ['stream manager', 'go live', 'what game do you want to stream', 'you ran an ad for', 'you received', 'from your sponsors'])) return 'stream';
264
+ if (has(embedText, "can't interact with your stream")) return 'stream';
265
+ if (hasAll(embedText, ['deposited', 'bank balance'])) return 'deposit';
266
+ if (hasAll(embedText, ['balances', 'global rank', 'net worth'])) return 'balance';
267
+ if (anyOf(embedText, ['you have 10 seconds to answer', 'you have 12 seconds to answer', 'you have 15 seconds to answer', 'trivia'])) return 'trivia';
268
+ if (anyOf(embedText, ['correct answer was', 'you got that answer correct'])) return 'trivia';
269
+ if (anyOf(embedText, ['you can work again at', 'you can use this command again', 'amount needs to be greater than 0'])) return 'cooldown';
270
+ if (has(embedText, 'you can buy the ability to use this command')) return 'premium';
271
+ if (hasAll(embedText, ['level:', 'experience:'])) return 'profile';
272
+ if (anyOf(embedText, ['dank memer shop', 'successful purchase'])) return 'shop';
273
+ if (has(embedText, 'farm') && anyOf(embedText, ['harvest', 'plant', 'hoe', 'water'])) return 'farm';
274
+ if (has(embedText, 'begging')) return 'beg';
275
+ if (has(embedText, 'daily quest')) return 'daily';
276
+ if (anyOf(embedText, ["'s daily coins", 'daily coins', 'streak bonus', 'daily bonus'])) return 'daily';
277
+ if (anyOf(embedText, ['fishing', 'fish'])) return 'fish';
278
+ if (has(embedText, 'hold tight')) return 'holdtight';
279
+
280
+ // ── allText fallback catches CV2 messages where d.components is a JSON string ──
281
+ if (anyOf(allText, ['hunting', 'went hunting', 'hunting rifle', 'your aim was so bad', 'animals laughed', 'animals attacked', 'barely escaped', 'fell asleep in a tree', 'caught nothing', 'brought back literally nothing', 'rifle broke', 'imagine going into the woods'])) return 'hunt';
282
+ if (anyOf(allText, ['digging', 'you dug', 'dug in the dirt', 'found nothing while', 'what are the odds lol'])) return 'dig';
283
+ if (anyOf(allText, ['brought back']) && anyOf(allText, ['ant', 'worm', 'stickbug', 'ladybug'])) return 'dig';
284
+ if (anyOf(allText, ['great work', 'you were given', 'for your shift', 'working as', 'dunk the ball', 'wastebasket', 'look at the emoji', 'what was the emoji'])) return 'work';
285
+ if (has(allText, "can't interact with your stream")) return 'stream';
286
+ if (anyOf(allText, ['you won', 'you lost']) && has(allText, 'the hidden number was')) return 'highlow';
287
+ if (anyOf(allText, ['you can work again at', 'amount needs to be greater than 0'])) return 'cooldown';
288
+ if (anyOf(allText, ["'s daily coins", 'daily coins', 'streak bonus'])) return 'daily';
289
+ if (anyOf(allText, ['you must be fun at parties', 'get away', 'piss poor attempt', 'too poor', "wouldn't have given"])) return 'beg';
290
+ if (anyOf(allText, ['charlie chaplin', 'shrek', 'elton john', 'alexa', 'confucius', 'doctor strange', 'rick astley', 'a honey badger'])) {
291
+ if (anyOf(allText, ['life saver', 'lifesaver', 'oh you poor soul', 'take this'])) return 'beg';
292
+ return 'search';
293
+ }
294
+
295
+ // ── Last-resort Dank Memer catch-all — ANY message from Dank Memer that got
296
+ // this far has recognizable game keywords even if format is unusual.
297
+ // Avoids dropping valid Dank Memer messages as 'unknown'.
298
+ if (anyOf(allText, [
299
+ // Dank Memer currency / items
300
+ '⏣', 'coins', 'bank', 'balance', 'wallet', 'pocket',
301
+ // Game mechanics
302
+ 'cooldown', 'work shift', 'shift', 'hunting', 'digging', 'fishing', 'search', 'crime',
303
+ 'beg', 'daily', 'weekly', 'hourly', 'quest', 'adventure', 'stream', 'postmemes',
304
+ // Gamble keywords
305
+ 'gamble', 'bet', 'poker', 'blackjack', 'roulette', 'slots', 'cointoss', 'coin toss',
306
+ // Work minigame variants (color/word/emoji/basketball)
307
+ 'color', 'emoji', 'word', 'dunk', 'basketball', 'wastebasket', 'mini-game', 'minigame',
308
+ 'lost', 'won', 'correct', 'wrong', 'answer', 'click',
309
+ // Command responses
310
+ 'you can work', 'you can use', 'again', 'try again', 'already',
311
+ // Item names (common drops)
312
+ 'fossil', 'worm', 'ant', 'bug', 'fish', 'coin', 'gem', 'lore', 'crate', 'box',
313
+ // Button labels
314
+ 'pls ', 'command', 'subcommand', 'specify',
315
+ // Failure/success
316
+ 'nothing', 'found', 'brought back', 'got', 'received', 'earned',
317
+ ])) {
318
+ // Try to narrow it down by what's most distinctive in allText
319
+ if (anyOf(allText, ['you can work again', 'work shift', 'for your shift', 'working as', 'great work'])) return 'work';
320
+ if (anyOf(allText, ['⏣', 'pocket', 'winnings', 'spinning', 'net:'])) return 'slots';
321
+ if (anyOf(allText, ['dealer', 'blackjack'])) return 'blackjack';
322
+ if (anyOf(allText, ['coin toss', 'cointoss', "can't bet"])) return 'cointoss';
323
+ if (anyOf(allText, ['roulette'])) return 'roulette';
324
+ if (anyOf(allText, ['trivia', 'seconds to answer', 'correct answer'])) return 'trivia';
325
+ if (anyOf(allText, ['daily', 'quest', 'streak', 'already got'])) return 'daily';
326
+ if (anyOf(allText, ['bank', 'deposit', 'deposited'])) return 'deposit';
327
+ if (anyOf(allText, ['hunt', 'hunting', 'rifle', 'caught', 'animals'])) return 'hunt';
328
+ if (anyOf(allText, ['dig', 'digging', 'fossil', 'worm', 'ant', 'bug', 'brought back'])) return 'dig';
329
+ if (anyOf(allText, ['fish', 'fishing', 'fisherfolk'])) return 'fish';
330
+ if (anyOf(allText, ['search', 'searched'])) return 'search';
331
+ if (anyOf(allText, ['crime', 'commit', 'trespassing', 'fraud', 'rob'])) return 'crime';
332
+ if (anyOf(allText, ['stream', 'live', 'sponsor'])) return 'stream';
333
+ if (anyOf(allText, ['beg', 'beggar', 'begging'])) return 'beg';
334
+ if (anyOf(allText, ['adventure', 'adventure ticket'])) return 'adventure';
335
+ if (anyOf(allText, ['shop', 'buy', 'purchase', 'item'])) return 'shop';
336
+ if (anyOf(allText, ['postmeme', 'meme'])) return 'postmemes';
337
+ if (anyOf(allText, ['balance', 'net worth', 'global rank'])) return 'balance';
338
+ if (anyOf(allText, ['emoji', 'color', 'word', 'dunk', 'basketball', 'minigame', 'mini-game', 'lost', 'won'])) return 'work';
339
+ if (anyOf(allText, ['high', 'low', 'secret number', 'higher or lower'])) return 'highlow';
340
+ if (anyOf(allText, ['cooldown', 'again <t:', 'try again', 'already'])) return 'cooldown';
341
+ // Generic fallback — it's definitely from Dank Memer, return a best guess
342
+ return 'holdtight';
343
+ }
238
344
 
239
345
  return 'unknown';
240
346
  }
@@ -301,7 +407,7 @@ async function store(d, event) {
301
407
  channelLast.set(d.channel_id, d.id);
302
408
 
303
409
  // Redis (non-blocking, fire-and-forget)
304
- if (redisReady && redis) {
410
+ if (redisReady && redis && redis.status === 'ready') {
305
411
  try {
306
412
  const key = `raw:msg:${d.id}`;
307
413
  const histKey = `raw:msg:${d.id}:history`;
@@ -316,8 +422,8 @@ async function store(d, event) {
316
422
  // History (all versions)
317
423
  pipe.rpush(histKey, json);
318
424
  pipe.expire(histKey, MSG_TTL);
319
- // Per-command log
320
- if (parsed.command && parsed.command !== 'unknown') {
425
+ // Per-command log (including 'unknown' so we can track detection gaps)
426
+ if (parsed.command) {
321
427
  const cmdKey = `raw:cmd:${parsed.command}:log`;
322
428
  pipe.lpush(cmdKey, `${d.id}:${parsed._capturedAt}:${event}`);
323
429
  pipe.ltrim(cmdKey, 0, 4999);
@@ -416,7 +522,7 @@ function attachDmLogger(client, opts = {}) {
416
522
  }
417
523
 
418
524
  // Store in Redis
419
- if (redisReady && redis && dmEvent) {
525
+ if (redisReady && redis && redis.status === 'ready' && dmEvent) {
420
526
  const json = JSON.stringify({
421
527
  id: d.id,
422
528
  channelId: d.channel_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.1.0",
3
+ "version": "7.6.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"