dankgrinder 6.8.1 → 6.14.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,541 @@
1
+ /**
2
+ * Raw Gateway Logger — intercepts ALL Dank Memer messages and stores to Redis.
3
+ * Captures: CV2 components, embeds, buttons, text, edits, ephemeral, cooldowns.
4
+ *
5
+ * Redis Keys:
6
+ * raw:msg:{msgId} — full parsed message (latest version), TTL 24h
7
+ * raw:msg:{msgId}:history — list of all versions (create + updates), TTL 24h
8
+ * raw:cmd:{command}:log — list of recent message IDs for that command, TTL 7d
9
+ * raw:account:{discordUserId} — list of recent message IDs for that account, TTL 7d
10
+ * raw:ephemeral:log — list of all ephemeral messages, TTL 7d
11
+ * raw:all:log — list of ALL message IDs (global), TTL 7d, capped at 10000
12
+ *
13
+ * Usage:
14
+ * const rawLogger = require('./rawLogger');
15
+ * await rawLogger.init('redis://...');
16
+ * rawLogger.attachRawLogger(client);
17
+ * const msg = await rawLogger.getMsg('123456');
18
+ * const history = await rawLogger.getMsgHistory('123456');
19
+ * const recent = await rawLogger.getRecentForCommand('cointoss', 20);
20
+ */
21
+
22
+ const LRU_SIZE = 256;
23
+ let redis = null;
24
+ let redisReady = false;
25
+
26
+ // ── In-memory LRU (always available, even without Redis) ──
27
+ const memStore = new Map();
28
+ const channelLast = new Map();
29
+ const memRing = [];
30
+ let memIdx = 0;
31
+
32
+ // ── Redis init ──
33
+ async function init(redisUrl) {
34
+ if (!redisUrl) return;
35
+ try {
36
+ const Redis = require('ioredis');
37
+ redis = new Redis(redisUrl, {
38
+ maxRetriesPerRequest: 2,
39
+ retryStrategy: (times) => times > 3 ? null : Math.min(times * 500, 3000),
40
+ lazyConnect: true,
41
+ });
42
+ await redis.connect();
43
+ redisReady = true;
44
+ } catch (e) {
45
+ console.error(`[rawLogger] Redis connect failed: ${e.message}`);
46
+ redis = null;
47
+ redisReady = false;
48
+ }
49
+ }
50
+
51
+ // ── CV2 component walkers ──
52
+ function extractTexts(components, out = []) {
53
+ for (const c of (components || [])) {
54
+ if (c.type === 10 && c.content) out.push(c.content);
55
+ if (c.components) extractTexts(c.components, out);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function extractButtons(components, out = []) {
61
+ for (const c of (components || [])) {
62
+ if (c.type === 2) {
63
+ out.push({
64
+ type: 2,
65
+ label: c.label || '',
66
+ customId: c.custom_id || '',
67
+ style: c.style,
68
+ disabled: !!c.disabled,
69
+ emoji: c.emoji || null,
70
+ });
71
+ }
72
+ if (c.components) extractButtons(c.components, out);
73
+ if (c.accessory?.type === 2) {
74
+ const a = c.accessory;
75
+ out.push({ type: 2, label: a.label || '', customId: a.custom_id || '', style: a.style, disabled: !!a.disabled, emoji: a.emoji || null });
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+
81
+ function extractSelectMenus(components, out = []) {
82
+ for (const c of (components || [])) {
83
+ if (c.type === 3) {
84
+ out.push({ type: 3, customId: c.custom_id || '', options: c.options || [], disabled: !!c.disabled });
85
+ }
86
+ if (c.components) extractSelectMenus(c.components, out);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function extractEmbedText(embeds) {
92
+ const parts = [];
93
+ for (const e of (embeds || [])) {
94
+ if (e.title) parts.push(e.title);
95
+ if (e.description) parts.push(e.description);
96
+ if (e.footer?.text) parts.push(e.footer.text);
97
+ if (e.author?.name) parts.push(e.author.name);
98
+ for (const f of (e.fields || [])) {
99
+ if (f.name) parts.push(f.name);
100
+ if (f.value) parts.push(f.value);
101
+ }
102
+ }
103
+ return parts.join('\n');
104
+ }
105
+
106
+ // ── Detect command from components/embeds ──
107
+ function detectCommand(d) {
108
+ // Check button custom_ids
109
+ const walk = (items) => {
110
+ for (const c of (items || [])) {
111
+ const cid = c.custom_id || '';
112
+ // Gambling
113
+ if (cid.startsWith('cointoss')) return 'cointoss';
114
+ if (cid.startsWith('blackjack')) return 'blackjack';
115
+ if (cid.startsWith('roulette')) return 'roulette';
116
+ if (cid.startsWith('slots')) return 'slots';
117
+ if (cid.startsWith('snakeeyes')) return 'snakeeyes';
118
+ if (cid.includes(':low') || cid.includes(':high') || cid.includes(':jackpot')) return 'highlow';
119
+ if (cid.startsWith('scratch')) return 'scratch';
120
+ // Adventure / trivia
121
+ if (cid.startsWith('adventure') || cid.startsWith('adv')) return 'adventure';
122
+ if (cid.startsWith('trivia')) return 'trivia';
123
+ // Grind commands
124
+ if (cid.startsWith('fish')) return 'fish';
125
+ if (cid.startsWith('hunt')) return 'hunt';
126
+ if (cid.startsWith('dig')) return 'dig';
127
+ if (cid.startsWith('crime')) return 'crime';
128
+ if (cid.startsWith('search')) return 'search';
129
+ if (cid.startsWith('postmemes') || cid.startsWith('pm')) return 'postmemes';
130
+ // Stream
131
+ if (cid.startsWith('stream')) return 'stream';
132
+ // Shop
133
+ if (cid.startsWith('shop')) return 'shop';
134
+ // Farm
135
+ if (cid.startsWith('farm')) return 'farm';
136
+ if (c.components) { const r = walk(c.components); if (r) return r; }
137
+ }
138
+ return null;
139
+ };
140
+ const fromBtn = walk(d.components);
141
+ if (fromBtn) return fromBtn;
142
+
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('weekly')) return 'weekly';
156
+ if (cv2Text.includes('daily')) return 'daily';
157
+ if (cv2Text.includes('inventory')) return 'inventory';
158
+ if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
159
+
160
+ // Check embed text
161
+ const embedText = extractEmbedText(d.embeds).toLowerCase();
162
+ // Gambling
163
+ if (embedText.includes('high') && embedText.includes('low') && embedText.includes('secret number')) return 'highlow';
164
+ if (embedText.includes('blackjack') || embedText.includes('dealer')) return 'blackjack';
165
+ if (embedText.includes('roulette')) return 'roulette';
166
+ if (embedText.includes('spinning') && embedText.includes('slots')) return 'slots';
167
+ if (embedText.includes('snakeeyes') || embedText.includes('snake eyes') || embedText.includes('dice')) return 'snakeeyes';
168
+ if (embedText.includes('scratch')) return 'scratch';
169
+ // Adventure
170
+ if (embedText.includes('adventure') || embedText.includes('choose an adventure')) return 'adventure';
171
+ // Crime / search
172
+ if (embedText.includes('what crime do you want')) return 'crime';
173
+ if (embedText.includes('where do you want to search')) return 'search';
174
+ if (embedText.includes('you searched') || embedText.includes('searched the')) return 'search';
175
+ if (embedText.includes('you committed') || embedText.includes('went outside')) return 'crime';
176
+ // Hunt / dig
177
+ if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the')) return 'hunt';
178
+ if (embedText.includes('you dig') || embedText.includes('found a') && embedText.includes('digging') || embedText.includes('shovel')) return 'dig';
179
+ // Work
180
+ if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color'))) return 'work';
181
+ if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
182
+ // Postmemes
183
+ if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
184
+ // Stream
185
+ if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream')) return 'stream';
186
+ // Deposit
187
+ if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
188
+ // Trivia
189
+ if (embedText.includes('you have 10 seconds to answer') || embedText.includes('trivia')) return 'trivia';
190
+ // Profile / level
191
+ if (embedText.includes('level:') && embedText.includes('experience:')) return 'profile';
192
+ // Shop
193
+ if (embedText.includes('dank memer shop') || embedText.includes('successful purchase')) return 'shop';
194
+ // Farm
195
+ if (embedText.includes('farm') && (embedText.includes('harvest') || embedText.includes('plant') || embedText.includes('hoe') || embedText.includes('water'))) return 'farm';
196
+ // Beg
197
+ if (embedText.includes('begging')) return 'beg';
198
+ // Daily/weekly quest
199
+ if (embedText.includes('daily quest')) return 'daily';
200
+ // Fish
201
+ if (embedText.includes('fishing') || embedText.includes('fish')) return 'fish';
202
+ // Hold tight
203
+ if (embedText.includes('hold tight')) return 'holdtight';
204
+
205
+ return 'unknown';
206
+ }
207
+
208
+ // ── Parse raw gateway packet ──
209
+ function parseRawPacket(d, event) {
210
+ const cv2Texts = extractTexts(d.components);
211
+ const buttons = extractButtons(d.components);
212
+ const selectMenus = extractSelectMenus(d.components);
213
+ const embedText = extractEmbedText(d.embeds);
214
+ const isCV2 = !!(d.flags & 32768);
215
+ const isEphemeral = !!(d.flags & 64);
216
+ const command = detectCommand(d);
217
+
218
+ return {
219
+ id: d.id,
220
+ channelId: d.channel_id,
221
+ guildId: d.guild_id || null,
222
+ authorId: d.author?.id,
223
+ authorTag: d.author?.username || d.author?.id,
224
+ flags: d.flags,
225
+ isCV2,
226
+ isEphemeral,
227
+ event,
228
+ command,
229
+ content: d.content || '',
230
+ embeds: d.embeds || [],
231
+ components: d.components || [],
232
+ cv2Text: cv2Texts.join('\n'),
233
+ allText: [d.content || '', ...cv2Texts, embedText].join('\n').trim(),
234
+ buttons,
235
+ clickableButtons: buttons.filter(b => !b.disabled && b.customId && !b.customId.includes('warning')),
236
+ selectMenus,
237
+ embedText,
238
+ timestamp: d.timestamp || null,
239
+ editedTimestamp: d.edited_timestamp || null,
240
+ _capturedAt: Date.now(),
241
+ _raw: d,
242
+ };
243
+ }
244
+
245
+ // ── Store to memory + Redis ──
246
+ async function store(d, event) {
247
+ const parsed = parseRawPacket(d, event);
248
+
249
+ // Memory LRU
250
+ if (memRing.length >= LRU_SIZE) {
251
+ const old = memRing[memIdx];
252
+ if (old) memStore.delete(old);
253
+ memRing[memIdx] = d.id;
254
+ } else {
255
+ memRing.push(d.id);
256
+ }
257
+ memIdx = (memIdx + 1) % LRU_SIZE;
258
+ memStore.set(d.id, parsed);
259
+ channelLast.set(d.channel_id, d.id);
260
+
261
+ // Redis (non-blocking, fire-and-forget)
262
+ if (redisReady && redis) {
263
+ try {
264
+ const key = `raw:msg:${d.id}`;
265
+ const histKey = `raw:msg:${d.id}:history`;
266
+ const json = JSON.stringify(parsed, (k, v) => k === '_raw' ? undefined : v);
267
+
268
+ const MSG_TTL = 2592000; // 30 days
269
+ const LOG_TTL = 2592000; // 30 days
270
+
271
+ const pipe = redis.pipeline();
272
+ // Latest version
273
+ pipe.set(key, json, 'EX', MSG_TTL);
274
+ // History (all versions)
275
+ pipe.rpush(histKey, json);
276
+ pipe.expire(histKey, MSG_TTL);
277
+ // Per-command log
278
+ if (parsed.command && parsed.command !== 'unknown') {
279
+ const cmdKey = `raw:cmd:${parsed.command}:log`;
280
+ pipe.lpush(cmdKey, `${d.id}:${parsed._capturedAt}:${event}`);
281
+ pipe.ltrim(cmdKey, 0, 4999);
282
+ pipe.expire(cmdKey, LOG_TTL);
283
+ }
284
+ // Per-account log
285
+ if (parsed.authorId) {
286
+ const accKey = `raw:channel:${d.channel_id}:log`;
287
+ pipe.lpush(accKey, `${d.id}:${parsed._capturedAt}:${event}:${parsed.command}`);
288
+ pipe.ltrim(accKey, 0, 4999);
289
+ pipe.expire(accKey, LOG_TTL);
290
+ }
291
+ // Ephemeral log
292
+ if (parsed.isEphemeral || (d.flags & 32832)) {
293
+ pipe.lpush('raw:ephemeral:log', `${d.id}:${parsed._capturedAt}:${parsed.command}:${d.channel_id}`);
294
+ pipe.ltrim('raw:ephemeral:log', 0, 4999);
295
+ pipe.expire('raw:ephemeral:log', LOG_TTL);
296
+ }
297
+ // Global log
298
+ pipe.lpush('raw:all:log', `${d.id}:${parsed._capturedAt}:${event}:${parsed.command}:${d.channel_id}`);
299
+ pipe.ltrim('raw:all:log', 0, 49999);
300
+ pipe.expire('raw:all:log', LOG_TTL);
301
+
302
+ pipe.exec().catch(() => {}); // fire and forget
303
+ } catch {}
304
+ }
305
+
306
+ return parsed;
307
+ }
308
+
309
+ // ── Verbose console log ──
310
+ let verboseMode = false;
311
+ function setVerbose(v) { verboseMode = v; }
312
+
313
+ function verboseLog(event, parsed) {
314
+ if (!verboseMode) return;
315
+ const tag = parsed.isCV2 ? 'CV2' : parsed.isEphemeral ? 'EPH' : 'STD';
316
+ console.log(` [RAW ${event}] [${tag}] [${parsed.command}] id=${parsed.id} flags=${parsed.flags}`);
317
+ if (parsed.cv2Text) console.log(` cv2: "${parsed.cv2Text.substring(0, 150)}"`);
318
+ if (parsed.embedText) console.log(` embed: "${parsed.embedText.substring(0, 150)}"`);
319
+ for (const b of parsed.buttons) console.log(` btn: [${b.label}] id="${b.customId}" disabled=${b.disabled}`);
320
+ }
321
+
322
+ // ── DM event listeners ──
323
+ const dmListeners = [];
324
+ function onDmEvent(fn) { dmListeners.push(fn); }
325
+
326
+ /**
327
+ * Attach DM logger — monitors Dank Memer DMs for:
328
+ * - Level ups: "You leveled up from level X to Y"
329
+ * - Deaths: "Your lifesaver protected you!" / "you died"
330
+ * - Lifesaver warnings: "you have 0 lifesavers"
331
+ * - Robbery: "you were robbed"
332
+ * - Trade notifications
333
+ *
334
+ * Stores to Redis under raw:dm:{channelId}:log
335
+ * Emits events via onDmEvent() callback
336
+ */
337
+ function attachDmLogger(client, opts = {}) {
338
+ const targetAuthorId = opts.authorId || '270904126974590976'; // Dank Memer
339
+
340
+ client.on('raw', (packet) => {
341
+ if (packet.t !== 'MESSAGE_CREATE') return;
342
+ const d = packet.d;
343
+ if (!d?.id) return;
344
+ // DMs have no guild_id
345
+ if (d.guild_id) return;
346
+ if (targetAuthorId && d.author?.id !== targetAuthorId) return;
347
+
348
+ const content = d.content || '';
349
+ const embedText = extractEmbedText(d.embeds).toLowerCase();
350
+ const allText = (content + '\n' + embedText).toLowerCase();
351
+
352
+ // Detect event type
353
+ let dmEvent = null;
354
+ if (allText.includes('leveled up') || allText.includes('level up')) {
355
+ const m = allText.match(/level\s+(\d+)\s+to\s+(\d+)/i);
356
+ dmEvent = { type: 'levelup', from: m ? parseInt(m[1]) : 0, to: m ? parseInt(m[2]) : 0 };
357
+ } else if (allText.includes('lifesaver protected') || allText.includes('you died')) {
358
+ const ls = allText.match(/(\d+)\s*lifesaver/i);
359
+ dmEvent = { type: 'death', lifesaversLeft: ls ? parseInt(ls[1]) : -1 };
360
+ } else if (allText.includes('you were robbed') || allText.includes('just robbed you')) {
361
+ const coins = allText.match(/[⏣]\s*([\d,]+)/);
362
+ dmEvent = { type: 'robbed', amount: coins ? parseInt(coins[1].replace(/,/g, '')) : 0 };
363
+ } else if (allText.includes('trade')) {
364
+ dmEvent = { type: 'trade' };
365
+ }
366
+
367
+ // Store in Redis
368
+ if (redisReady && redis && dmEvent) {
369
+ const json = JSON.stringify({
370
+ id: d.id,
371
+ channelId: d.channel_id,
372
+ event: dmEvent,
373
+ text: allText.substring(0, 500),
374
+ ts: Date.now(),
375
+ });
376
+ const pipe = redis.pipeline();
377
+ pipe.lpush(`raw:dm:${d.channel_id}:log`, json);
378
+ pipe.ltrim(`raw:dm:${d.channel_id}:log`, 0, 999);
379
+ pipe.expire(`raw:dm:${d.channel_id}:log`, 2592000); // 30 days
380
+ // Also log globally
381
+ pipe.lpush('raw:dm:all:log', `${d.id}:${Date.now()}:${dmEvent.type}:${d.channel_id}`);
382
+ pipe.ltrim('raw:dm:all:log', 0, 4999);
383
+ pipe.expire('raw:dm:all:log', 2592000);
384
+ // If death with 0 lifesavers — store alert
385
+ if (dmEvent.type === 'death' && dmEvent.lifesaversLeft === 0) {
386
+ pipe.set(`raw:alert:no-lifesaver:${d.channel_id}`, '1', 'EX', 86400);
387
+ }
388
+ // If level up — update cached level
389
+ if (dmEvent.type === 'levelup' && dmEvent.to > 0) {
390
+ pipe.set(`dkg:level:dm:${d.channel_id}`, String(dmEvent.to), 'EX', 2592000);
391
+ }
392
+ pipe.exec().catch(() => {});
393
+ }
394
+
395
+ // Emit to listeners
396
+ if (dmEvent) {
397
+ for (const fn of dmListeners) {
398
+ try { fn(dmEvent, d); } catch {}
399
+ }
400
+ }
401
+ });
402
+ }
403
+
404
+ async function getDmLog(channelId, count = 50) {
405
+ if (!redisReady || !redis) return [];
406
+ try {
407
+ const items = await redis.lrange(`raw:dm:${channelId}:log`, 0, count - 1);
408
+ return items.map(i => { try { return JSON.parse(i); } catch { return null; } }).filter(Boolean);
409
+ } catch { return []; }
410
+ }
411
+
412
+ async function hasNoLifesaverAlert(channelId) {
413
+ if (!redisReady || !redis) return false;
414
+ try { return await redis.get(`raw:alert:no-lifesaver:${channelId}`) === '1'; } catch { return false; }
415
+ }
416
+
417
+ // ── Attach to discord.js-selfbot-v13 client ──
418
+ function attachRawLogger(client, opts = {}) {
419
+ const targetAuthorId = opts.authorId || '270904126974590976'; // Dank Memer
420
+ const targetChannelId = opts.channelId || null;
421
+
422
+ client.on('raw', (packet) => {
423
+ if (packet.t !== 'MESSAGE_CREATE' && packet.t !== 'MESSAGE_UPDATE') return;
424
+ const d = packet.d;
425
+ if (!d?.id) return;
426
+ if (targetAuthorId && d.author?.id !== targetAuthorId) return;
427
+ if (targetChannelId && d.channel_id !== targetChannelId) return;
428
+
429
+ const event = packet.t === 'MESSAGE_CREATE' ? 'CREATE' : 'UPDATE';
430
+ const parsed = store(d, event); // async but we don't await (fire-and-forget)
431
+ if (parsed.then) parsed.then(p => verboseLog(event, p)).catch(() => {});
432
+ else verboseLog(event, parsed);
433
+ });
434
+ }
435
+
436
+ // ── Read API (memory) ──
437
+ function getRawMessage(msgId) { return memStore.get(msgId) || null; }
438
+ function getLastRaw(channelId) {
439
+ const id = channelLast.get(channelId);
440
+ return id ? memStore.get(id) || null : null;
441
+ }
442
+
443
+ // ── Read API (Redis) ──
444
+ async function getMsg(msgId) {
445
+ // Memory first
446
+ const mem = memStore.get(msgId);
447
+ if (mem) return mem;
448
+ // Redis fallback
449
+ if (!redisReady || !redis) return null;
450
+ try {
451
+ const json = await redis.get(`raw:msg:${msgId}`);
452
+ return json ? JSON.parse(json) : null;
453
+ } catch { return null; }
454
+ }
455
+
456
+ async function getMsgHistory(msgId) {
457
+ if (!redisReady || !redis) return [];
458
+ try {
459
+ const items = await redis.lrange(`raw:msg:${msgId}:history`, 0, -1);
460
+ return items.map(i => { try { return JSON.parse(i); } catch { return null; } }).filter(Boolean);
461
+ } catch { return []; }
462
+ }
463
+
464
+ async function getRecentForCommand(command, count = 20) {
465
+ if (!redisReady || !redis) return [];
466
+ try {
467
+ const entries = await redis.lrange(`raw:cmd:${command}:log`, 0, count - 1);
468
+ const results = [];
469
+ for (const entry of entries) {
470
+ const msgId = entry.split(':')[0];
471
+ const msg = await getMsg(msgId);
472
+ if (msg) results.push(msg);
473
+ }
474
+ return results;
475
+ } catch { return []; }
476
+ }
477
+
478
+ async function getRecentAll(count = 50) {
479
+ if (!redisReady || !redis) return [];
480
+ try {
481
+ return await redis.lrange('raw:all:log', 0, count - 1);
482
+ } catch { return []; }
483
+ }
484
+
485
+ async function getRecentEphemeral(count = 50) {
486
+ if (!redisReady || !redis) return [];
487
+ try {
488
+ return await redis.lrange('raw:ephemeral:log', 0, count - 1);
489
+ } catch { return []; }
490
+ }
491
+
492
+ async function getRecentForChannel(channelId, count = 50) {
493
+ if (!redisReady || !redis) return [];
494
+ try {
495
+ return await redis.lrange(`raw:channel:${channelId}:log`, 0, count - 1);
496
+ } catch { return []; }
497
+ }
498
+
499
+ async function getStats() {
500
+ if (!redisReady || !redis) return { redis: false };
501
+ try {
502
+ const pipe = redis.pipeline();
503
+ pipe.llen('raw:all:log');
504
+ pipe.llen('raw:ephemeral:log');
505
+ const cmds = ['cointoss', 'blackjack', 'highlow', 'roulette', 'slots', 'snakeeyes', 'cooldown', 'unknown'];
506
+ for (const cmd of cmds) pipe.llen(`raw:cmd:${cmd}:log`);
507
+ const results = await pipe.exec();
508
+ const stats = { redis: true, total: results[0][1], ephemeral: results[1][1], commands: {} };
509
+ cmds.forEach((cmd, i) => { stats.commands[cmd] = results[i + 2][1]; });
510
+ return stats;
511
+ } catch { return { redis: true, error: true }; }
512
+ }
513
+
514
+ module.exports = {
515
+ init,
516
+ attachRawLogger,
517
+ attachDmLogger,
518
+ onDmEvent,
519
+ setVerbose,
520
+ // Memory reads
521
+ getRawMessage,
522
+ getLastRaw,
523
+ // Redis reads
524
+ getMsg,
525
+ getMsgHistory,
526
+ getRecentForCommand,
527
+ getRecentAll,
528
+ getRecentEphemeral,
529
+ getRecentForChannel,
530
+ getStats,
531
+ // DM reads
532
+ getDmLog,
533
+ hasNoLifesaverAlert,
534
+ // Component helpers
535
+ extractTexts,
536
+ extractButtons,
537
+ extractSelectMenus,
538
+ extractEmbedText,
539
+ parseRawPacket,
540
+ detectCommand,
541
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.8.1",
3
+ "version": "6.14.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"