dankgrinder 6.8.2 → 6.16.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/commands/blackjack.js +110 -51
- package/lib/commands/crime.js +8 -0
- package/lib/commands/fish.js +7 -0
- package/lib/commands/gamble.js +70 -54
- package/lib/commands/highlow.js +48 -28
- package/lib/commands/hunt.js +153 -32
- package/lib/commands/profile.js +5 -4
- package/lib/commands/search.js +8 -0
- package/lib/commands/utils.js +24 -8
- package/lib/commands/work.js +92 -4
- package/lib/grinder.js +302 -5
- package/lib/rawLogger.js +550 -0
- package/package.json +1 -1
package/lib/rawLogger.js
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
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
|
+
// Parse lifesaver count from button labels: "You have 0 Life Saver left"
|
|
359
|
+
let lsLeft = -1;
|
|
360
|
+
const btnText = extractButtons(d.components).map(b => (b.label || '').toLowerCase()).join(' ');
|
|
361
|
+
const btnMatch = btnText.match(/you have (\d+) life\s*saver/i);
|
|
362
|
+
if (btnMatch) lsLeft = parseInt(btnMatch[1]);
|
|
363
|
+
// Fallback to embed text
|
|
364
|
+
if (lsLeft === -1) {
|
|
365
|
+
const ls = allText.match(/(\d+)\s*life\s*saver/i);
|
|
366
|
+
if (ls) lsLeft = parseInt(ls[1]);
|
|
367
|
+
}
|
|
368
|
+
dmEvent = { type: 'death', lifesaversLeft: lsLeft };
|
|
369
|
+
} else if (allText.includes('you were robbed') || allText.includes('just robbed you')) {
|
|
370
|
+
const coins = allText.match(/[⏣]\s*([\d,]+)/);
|
|
371
|
+
dmEvent = { type: 'robbed', amount: coins ? parseInt(coins[1].replace(/,/g, '')) : 0 };
|
|
372
|
+
} else if (allText.includes('trade')) {
|
|
373
|
+
dmEvent = { type: 'trade' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Store in Redis
|
|
377
|
+
if (redisReady && redis && dmEvent) {
|
|
378
|
+
const json = JSON.stringify({
|
|
379
|
+
id: d.id,
|
|
380
|
+
channelId: d.channel_id,
|
|
381
|
+
event: dmEvent,
|
|
382
|
+
text: allText.substring(0, 500),
|
|
383
|
+
ts: Date.now(),
|
|
384
|
+
});
|
|
385
|
+
const pipe = redis.pipeline();
|
|
386
|
+
pipe.lpush(`raw:dm:${d.channel_id}:log`, json);
|
|
387
|
+
pipe.ltrim(`raw:dm:${d.channel_id}:log`, 0, 999);
|
|
388
|
+
pipe.expire(`raw:dm:${d.channel_id}:log`, 2592000); // 30 days
|
|
389
|
+
// Also log globally
|
|
390
|
+
pipe.lpush('raw:dm:all:log', `${d.id}:${Date.now()}:${dmEvent.type}:${d.channel_id}`);
|
|
391
|
+
pipe.ltrim('raw:dm:all:log', 0, 4999);
|
|
392
|
+
pipe.expire('raw:dm:all:log', 2592000);
|
|
393
|
+
// If death with 0 lifesavers — store alert
|
|
394
|
+
if (dmEvent.type === 'death' && dmEvent.lifesaversLeft === 0) {
|
|
395
|
+
pipe.set(`raw:alert:no-lifesaver:${d.channel_id}`, '1', 'EX', 86400);
|
|
396
|
+
}
|
|
397
|
+
// If level up — update cached level
|
|
398
|
+
if (dmEvent.type === 'levelup' && dmEvent.to > 0) {
|
|
399
|
+
pipe.set(`dkg:level:dm:${d.channel_id}`, String(dmEvent.to), 'EX', 2592000);
|
|
400
|
+
}
|
|
401
|
+
pipe.exec().catch(() => {});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Emit to listeners
|
|
405
|
+
if (dmEvent) {
|
|
406
|
+
for (const fn of dmListeners) {
|
|
407
|
+
try { fn(dmEvent, d); } catch {}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function getDmLog(channelId, count = 50) {
|
|
414
|
+
if (!redisReady || !redis) return [];
|
|
415
|
+
try {
|
|
416
|
+
const items = await redis.lrange(`raw:dm:${channelId}:log`, 0, count - 1);
|
|
417
|
+
return items.map(i => { try { return JSON.parse(i); } catch { return null; } }).filter(Boolean);
|
|
418
|
+
} catch { return []; }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function hasNoLifesaverAlert(channelId) {
|
|
422
|
+
if (!redisReady || !redis) return false;
|
|
423
|
+
try { return await redis.get(`raw:alert:no-lifesaver:${channelId}`) === '1'; } catch { return false; }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Attach to discord.js-selfbot-v13 client ──
|
|
427
|
+
function attachRawLogger(client, opts = {}) {
|
|
428
|
+
const targetAuthorId = opts.authorId || '270904126974590976'; // Dank Memer
|
|
429
|
+
const targetChannelId = opts.channelId || null;
|
|
430
|
+
|
|
431
|
+
client.on('raw', (packet) => {
|
|
432
|
+
if (packet.t !== 'MESSAGE_CREATE' && packet.t !== 'MESSAGE_UPDATE') return;
|
|
433
|
+
const d = packet.d;
|
|
434
|
+
if (!d?.id) return;
|
|
435
|
+
if (targetAuthorId && d.author?.id !== targetAuthorId) return;
|
|
436
|
+
if (targetChannelId && d.channel_id !== targetChannelId) return;
|
|
437
|
+
|
|
438
|
+
const event = packet.t === 'MESSAGE_CREATE' ? 'CREATE' : 'UPDATE';
|
|
439
|
+
const parsed = store(d, event); // async but we don't await (fire-and-forget)
|
|
440
|
+
if (parsed.then) parsed.then(p => verboseLog(event, p)).catch(() => {});
|
|
441
|
+
else verboseLog(event, parsed);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Read API (memory) ──
|
|
446
|
+
function getRawMessage(msgId) { return memStore.get(msgId) || null; }
|
|
447
|
+
function getLastRaw(channelId) {
|
|
448
|
+
const id = channelLast.get(channelId);
|
|
449
|
+
return id ? memStore.get(id) || null : null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Read API (Redis) ──
|
|
453
|
+
async function getMsg(msgId) {
|
|
454
|
+
// Memory first
|
|
455
|
+
const mem = memStore.get(msgId);
|
|
456
|
+
if (mem) return mem;
|
|
457
|
+
// Redis fallback
|
|
458
|
+
if (!redisReady || !redis) return null;
|
|
459
|
+
try {
|
|
460
|
+
const json = await redis.get(`raw:msg:${msgId}`);
|
|
461
|
+
return json ? JSON.parse(json) : null;
|
|
462
|
+
} catch { return null; }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function getMsgHistory(msgId) {
|
|
466
|
+
if (!redisReady || !redis) return [];
|
|
467
|
+
try {
|
|
468
|
+
const items = await redis.lrange(`raw:msg:${msgId}:history`, 0, -1);
|
|
469
|
+
return items.map(i => { try { return JSON.parse(i); } catch { return null; } }).filter(Boolean);
|
|
470
|
+
} catch { return []; }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function getRecentForCommand(command, count = 20) {
|
|
474
|
+
if (!redisReady || !redis) return [];
|
|
475
|
+
try {
|
|
476
|
+
const entries = await redis.lrange(`raw:cmd:${command}:log`, 0, count - 1);
|
|
477
|
+
const results = [];
|
|
478
|
+
for (const entry of entries) {
|
|
479
|
+
const msgId = entry.split(':')[0];
|
|
480
|
+
const msg = await getMsg(msgId);
|
|
481
|
+
if (msg) results.push(msg);
|
|
482
|
+
}
|
|
483
|
+
return results;
|
|
484
|
+
} catch { return []; }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function getRecentAll(count = 50) {
|
|
488
|
+
if (!redisReady || !redis) return [];
|
|
489
|
+
try {
|
|
490
|
+
return await redis.lrange('raw:all:log', 0, count - 1);
|
|
491
|
+
} catch { return []; }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function getRecentEphemeral(count = 50) {
|
|
495
|
+
if (!redisReady || !redis) return [];
|
|
496
|
+
try {
|
|
497
|
+
return await redis.lrange('raw:ephemeral:log', 0, count - 1);
|
|
498
|
+
} catch { return []; }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function getRecentForChannel(channelId, count = 50) {
|
|
502
|
+
if (!redisReady || !redis) return [];
|
|
503
|
+
try {
|
|
504
|
+
return await redis.lrange(`raw:channel:${channelId}:log`, 0, count - 1);
|
|
505
|
+
} catch { return []; }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function getStats() {
|
|
509
|
+
if (!redisReady || !redis) return { redis: false };
|
|
510
|
+
try {
|
|
511
|
+
const pipe = redis.pipeline();
|
|
512
|
+
pipe.llen('raw:all:log');
|
|
513
|
+
pipe.llen('raw:ephemeral:log');
|
|
514
|
+
const cmds = ['cointoss', 'blackjack', 'highlow', 'roulette', 'slots', 'snakeeyes', 'cooldown', 'unknown'];
|
|
515
|
+
for (const cmd of cmds) pipe.llen(`raw:cmd:${cmd}:log`);
|
|
516
|
+
const results = await pipe.exec();
|
|
517
|
+
const stats = { redis: true, total: results[0][1], ephemeral: results[1][1], commands: {} };
|
|
518
|
+
cmds.forEach((cmd, i) => { stats.commands[cmd] = results[i + 2][1]; });
|
|
519
|
+
return stats;
|
|
520
|
+
} catch { return { redis: true, error: true }; }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
module.exports = {
|
|
524
|
+
init,
|
|
525
|
+
attachRawLogger,
|
|
526
|
+
attachDmLogger,
|
|
527
|
+
onDmEvent,
|
|
528
|
+
setVerbose,
|
|
529
|
+
// Memory reads
|
|
530
|
+
getRawMessage,
|
|
531
|
+
getLastRaw,
|
|
532
|
+
// Redis reads
|
|
533
|
+
getMsg,
|
|
534
|
+
getMsgHistory,
|
|
535
|
+
getRecentForCommand,
|
|
536
|
+
getRecentAll,
|
|
537
|
+
getRecentEphemeral,
|
|
538
|
+
getRecentForChannel,
|
|
539
|
+
getStats,
|
|
540
|
+
// DM reads
|
|
541
|
+
getDmLog,
|
|
542
|
+
hasNoLifesaverAlert,
|
|
543
|
+
// Component helpers
|
|
544
|
+
extractTexts,
|
|
545
|
+
extractButtons,
|
|
546
|
+
extractSelectMenus,
|
|
547
|
+
extractEmbedText,
|
|
548
|
+
parseRawPacket,
|
|
549
|
+
detectCommand,
|
|
550
|
+
};
|