dankgrinder 6.46.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.
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,103 +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') || cv2Text.includes('wumpus gives you') || (cv2Text.includes('you received') && !cv2Text.includes('search') && !cv2Text.includes('hunt') && !cv2Text.includes('dig'))) 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') || (cv2Text.includes('brought back') && (cv2Text.includes('deer') || cv2Text.includes('wolf') || cv2Text.includes('bear') || cv2Text.includes('boar') || cv2Text.includes('lion') || cv2Text.includes('rabbit') || cv2Text.includes('squirrel') || cv2Text.includes('moose') || cv2Text.includes('bird') || cv2Text.includes('elk') || cv2Text.includes('hunting') || cv2Text.includes('capybara')))) 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
- // Quest completions (before generic daily/weekly)
159
- if (cv2Text.includes('locations') && (cv2Text.includes('inventory locations') || cv2Text.includes('found locations') || cv2Text.includes('completed your') || cv2Text.includes('locations remaining'))) return 'search';
160
- if (cv2Text.includes('weekly')) return 'weekly';
161
- if (cv2Text.includes('daily')) return 'daily';
162
- // Only match inventory if it looks like an actual inventory display (header format or sellable indicator)
163
- if (cv2Text.includes('inventory') && (cv2Text.includes('###') || cv2Text.includes('sellable') || cv2Text.includes('<:reply:'))) return 'inventory';
164
- if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
165
- if (cv2Text.includes('balances') && cv2Text.includes('global rank')) return 'balance';
166
-
167
- // Check content text (plain message content)
168
- const contentText = (d.content || '').toLowerCase();
169
- if (contentText.includes('balances') && contentText.includes('global rank')) return 'balance';
170
- if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed') || contentText.includes('imagine going into the woods')) return 'hunt';
171
- if (contentText.includes('you ran an ad for') && contentText.includes('received')) return 'stream';
172
- if (contentText.includes('you can\'t interact with your stream')) return 'stream';
173
- if (contentText.includes('you dug in the dirt') || (contentText.includes('found nothing while digging') && (contentText.includes('dug') || contentText.includes('dirt')))) return 'dig';
174
-
175
- // Check embed text
176
- const embedText = extractEmbedText(d.embeds).toLowerCase();
177
- // Gambling
178
- if (embedText.includes('high') && embedText.includes('low') && embedText.includes('secret number')) return 'highlow';
179
- if (embedText.includes('you won') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
180
- if (embedText.includes('you lost') && embedText.includes('your hint was') && embedText.includes('the hidden number was')) return 'highlow';
181
- if (embedText.includes('blackjack') || embedText.includes('dealer')) return 'blackjack';
182
- if (embedText.includes('roulette')) return 'roulette';
183
- if (embedText.includes('spinning') && embedText.includes('slots')) return 'slots';
184
- if (embedText.includes('snakeeyes') || embedText.includes('snake eyes') || embedText.includes('dice')) return 'snakeeyes';
185
- if (embedText.includes('scratch')) return 'scratch';
186
- // Adventure
187
- if (embedText.includes('adventure') || embedText.includes('choose an adventure')) return 'adventure';
188
- // Crime / search
189
- if (embedText.includes('what crime do you want')) return 'crime';
190
- if (embedText.includes('where do you want to search')) return 'search';
191
- if (embedText.includes('you searched') || embedText.includes('searched the')) return 'search';
192
- 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';
193
- if (embedText.includes('you committed') || embedText.includes('went outside')) return 'crime';
194
- if (embedText.includes('stole a developer') || embedText.includes('got confused about what trespassing')) return 'crime';
195
- // Search results (person names) - also check for beg results
196
- 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'))) {
197
- // Check if it's a beg result (has life saver or specific beg text)
198
- 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';
199
254
  return 'search';
200
255
  }
201
- // Hunt / dig
202
- 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') || (embedText.includes('brought back') && (embedText.includes('deer') || embedText.includes('wolf') || embedText.includes('bear') || embedText.includes('boar') || embedText.includes('lion') || embedText.includes('rabbit') || embedText.includes('squirrel') || embedText.includes('moose') || embedText.includes('bird') || embedText.includes('elk') || embedText.includes('capybara')))) return 'hunt';
203
- 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';
204
- // Work match both minigame prompt AND completion
205
- 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';
206
- if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
207
- if (embedText.includes('working as') || embedText.includes('for your shift')) return 'work';
208
- 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';
209
- // Postmemes
210
- if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
211
- // Stream
212
- 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';
213
- if (embedText.includes('you can\'t interact with your stream') || embedText.includes('stream can last')) return 'stream';
214
- // Deposit
215
- if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
216
- // Balance
217
- if (embedText.includes('balances') && embedText.includes('global rank') && embedText.includes('net worth')) return 'balance';
218
- // Trivia
219
- 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';
220
- if (embedText.includes('who in pulp fiction') || embedText.includes('what was') || embedText.includes('which of')) return 'trivia';
221
- // Cooldown messages
222
- if (embedText.includes('you can work again at') || embedText.includes('you can use this command again')) return 'cooldown';
223
- if (embedText.includes('amount needs to be greater than 0')) return 'cooldown';
224
- // Premium/upgrade messages
225
- if (embedText.includes('you can buy the ability to use this command')) return 'premium';
226
- // Profile / level
227
- if (embedText.includes('level:') && embedText.includes('experience:')) return 'profile';
228
- // Shop
229
- if (embedText.includes('dank memer shop') || embedText.includes('successful purchase')) return 'shop';
230
- // Farm
231
- if (embedText.includes('farm') && (embedText.includes('harvest') || embedText.includes('plant') || embedText.includes('hoe') || embedText.includes('water'))) return 'farm';
232
- // Beg
233
- if (embedText.includes('begging') || embedText.includes('wumpus gives you') || embedText.includes('life saver') || embedText.includes('lifesaver')) return 'beg';
234
- // Daily/weekly quest
235
- if (embedText.includes('daily quest')) return 'daily';
236
- // Fish
237
- if (embedText.includes('fishing') || embedText.includes('fish')) return 'fish';
238
- // Hold tight
239
- 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
+ }
240
344
 
241
345
  return 'unknown';
242
346
  }
@@ -303,7 +407,7 @@ async function store(d, event) {
303
407
  channelLast.set(d.channel_id, d.id);
304
408
 
305
409
  // Redis (non-blocking, fire-and-forget)
306
- if (redisReady && redis) {
410
+ if (redisReady && redis && redis.status === 'ready') {
307
411
  try {
308
412
  const key = `raw:msg:${d.id}`;
309
413
  const histKey = `raw:msg:${d.id}:history`;
@@ -318,8 +422,8 @@ async function store(d, event) {
318
422
  // History (all versions)
319
423
  pipe.rpush(histKey, json);
320
424
  pipe.expire(histKey, MSG_TTL);
321
- // Per-command log
322
- if (parsed.command && parsed.command !== 'unknown') {
425
+ // Per-command log (including 'unknown' so we can track detection gaps)
426
+ if (parsed.command) {
323
427
  const cmdKey = `raw:cmd:${parsed.command}:log`;
324
428
  pipe.lpush(cmdKey, `${d.id}:${parsed._capturedAt}:${event}`);
325
429
  pipe.ltrim(cmdKey, 0, 4999);
@@ -418,7 +522,7 @@ function attachDmLogger(client, opts = {}) {
418
522
  }
419
523
 
420
524
  // Store in Redis
421
- if (redisReady && redis && dmEvent) {
525
+ if (redisReady && redis && redis.status === 'ready' && dmEvent) {
422
526
  const json = JSON.stringify({
423
527
  id: d.id,
424
528
  channelId: d.channel_id,
package/lib/structures.js CHANGED
@@ -443,25 +443,6 @@ class MinHeap {
443
443
 
444
444
  peek() { return this.heap[0]; }
445
445
 
446
- /**
447
- * Remove a specific node from the heap by reference.
448
- * Used by CommandScheduler.unschedule() to cancel a pending scheduled command.
449
- * O(n) — linear scan since we store node references in a Map.
450
- */
451
- remove(node) {
452
- const idx = this.heap.indexOf(node);
453
- if (idx === -1) return false;
454
- const last = this.heap.pop();
455
- if (idx < this.heap.length) {
456
- this.heap[idx] = last;
457
- // Bubble up or sink down depending on where the removed item was
458
- if (this._bubbleUp(idx) === idx) {
459
- this._sinkDown(idx);
460
- }
461
- }
462
- return true;
463
- }
464
-
465
446
  _bubbleUp(i) {
466
447
  while (i > 0) {
467
448
  const parent = (i - 1) >>> 1;
@@ -699,6 +680,31 @@ class AsyncBatchQueue {
699
680
  destroy() { if (this._timer) clearTimeout(this._timer); this.queue.length = 0; }
700
681
  }
701
682
 
683
+ // ═══════════════════════════════════════════════════════════════
684
+ // JitterBackoff – Decorrelated jitter (AWS-style), O(1)
685
+ // Standard exponential backoff causes "thundering herd" when 10K
686
+ // accounts retry simultaneously. Decorrelated jitter spreads them:
687
+ // sleep = min(cap, random_between(base, sleep_prev * 3))
688
+ // This is the recommended strategy from AWS Architecture Blog.
689
+ // ═══════════════════════════════════════════════════════════════
690
+ class JitterBackoff {
691
+ constructor(baseMs = 1000, capMs = 30000) {
692
+ this.baseMs = baseMs;
693
+ this.capMs = capMs;
694
+ this._sleep = baseMs;
695
+ this.attempt = 0;
696
+ }
697
+
698
+ next() {
699
+ this._sleep = Math.min(this.capMs, this.baseMs + Math.random() * (this._sleep * 3 - this.baseMs));
700
+ this.attempt++;
701
+ return this._sleep;
702
+ }
703
+
704
+ reset() { this._sleep = this.baseMs; this.attempt = 0; }
705
+ get current() { return this._sleep; }
706
+ }
707
+
702
708
  module.exports = {
703
709
  BloomFilter,
704
710
  LRUCache,
@@ -715,4 +721,5 @@ module.exports = {
715
721
  ObjectPool,
716
722
  TimerWheel,
717
723
  AsyncBatchQueue,
724
+ JitterBackoff,
718
725
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.46.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"