dankgrinder 5.13.0 → 5.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/shop.js +64 -19
- package/lib/commands/stream.js +157 -9
- package/lib/commands/utils.js +28 -7
- package/package.json +1 -1
package/lib/commands/shop.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const {
|
|
17
17
|
LOG, c, sleep, humanDelay, getFullText,
|
|
18
|
-
getAllButtons, findSelectMenuOption,
|
|
18
|
+
getAllButtons, findSelectMenuOption, getAllSelectMenus, safeClickButton,
|
|
19
19
|
logMsg, isHoldTight, isCV2, ensureCV2, stripAnsi,
|
|
20
20
|
} = require('./utils');
|
|
21
21
|
const { LRUCache, Trie } = require('../structures');
|
|
@@ -80,6 +80,28 @@ function isBuyFailText(text) {
|
|
|
80
80
|
|| lower.includes('missing items');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function findBuyButtonInMessage(msg, searchTerm, key) {
|
|
84
|
+
const allBtns = getAllButtons(msg);
|
|
85
|
+
const normalizedSearch = String(searchTerm || '').toLowerCase().replace(/\s+/g, '');
|
|
86
|
+
const normalizedKey = String(key || '').toLowerCase().replace(/\s+/g, '');
|
|
87
|
+
return allBtns.find(b => {
|
|
88
|
+
const label = String(b.label || '').toLowerCase();
|
|
89
|
+
const id = String(b.customId || b.custom_id || '').toLowerCase().replace(/\s+/g, '');
|
|
90
|
+
return label.includes(searchTerm)
|
|
91
|
+
|| (normalizedSearch && id.includes(normalizedSearch))
|
|
92
|
+
|| (normalizedKey && id.includes(normalizedKey));
|
|
93
|
+
}) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findNextShopPageButton(msg) {
|
|
97
|
+
const btns = getAllButtons(msg).filter(b => !b.disabled);
|
|
98
|
+
return btns.find(b => {
|
|
99
|
+
const id = String(b.customId || b.custom_id || '').toLowerCase();
|
|
100
|
+
if (!id.includes('shop-view')) return false;
|
|
101
|
+
return id.endsWith(':1') || id.includes(':1:');
|
|
102
|
+
}) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
/**
|
|
84
106
|
* Buy an item from the Dank Memer shop.
|
|
85
107
|
*
|
|
@@ -137,7 +159,7 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
137
159
|
const row = response.components[i];
|
|
138
160
|
for (const comp of row?.components || []) {
|
|
139
161
|
if ((comp.type === 'STRING_SELECT' || comp.type === 3) &&
|
|
140
|
-
comp.customId === selectInfo.menuCustomId) {
|
|
162
|
+
(comp.customId || comp.custom_id) === selectInfo.menuCustomId) {
|
|
141
163
|
menuRowIdx = i;
|
|
142
164
|
break;
|
|
143
165
|
}
|
|
@@ -148,34 +170,57 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
|
|
|
148
170
|
menuRowIdx >= 0 ? menuRowIdx : selectInfo.menuCustomId,
|
|
149
171
|
[selectInfo.option.value]
|
|
150
172
|
);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
LOG.success(`Switched to ${targetTab}`);
|
|
155
|
-
}
|
|
173
|
+
response = result || (await waitForDankMemer(6000)) || response;
|
|
174
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
175
|
+
LOG.success(`Switched to ${targetTab}`);
|
|
156
176
|
await sleep(300);
|
|
157
177
|
} catch (e) {
|
|
158
178
|
LOG.error(`[shop] Tab switch failed: ${e.message}`);
|
|
159
179
|
return false;
|
|
160
180
|
}
|
|
161
181
|
}
|
|
182
|
+
} else {
|
|
183
|
+
// If select menu exists but option matching failed due label variations,
|
|
184
|
+
// try selecting by value from any menu option containing coin/fishing/etc.
|
|
185
|
+
const menus = getAllSelectMenus(response);
|
|
186
|
+
const menu = menus.find(m => (m.options || []).length > 0);
|
|
187
|
+
if (menu) {
|
|
188
|
+
const desired = (menu.options || []).find(o => (o.label || '').toLowerCase().includes(targetTab.toLowerCase()));
|
|
189
|
+
if (desired) {
|
|
190
|
+
try {
|
|
191
|
+
await response.selectMenu(menu.customId || menu.custom_id, [desired.value]);
|
|
192
|
+
const maybeUpdated = await waitForDankMemer(6000);
|
|
193
|
+
if (maybeUpdated) {
|
|
194
|
+
response = maybeUpdated;
|
|
195
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
196
|
+
LOG.success(`Switched to ${targetTab}`);
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
162
201
|
}
|
|
163
202
|
|
|
164
|
-
// Step 3: Find the Buy button for our item
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
// Step 3: Find the Buy button for our item (scan pages in current tab)
|
|
204
|
+
let buyBtn = null;
|
|
205
|
+
for (let page = 0; page < 10; page++) {
|
|
206
|
+
buyBtn = findBuyButtonInMessage(response, searchTerm, key);
|
|
207
|
+
if (buyBtn) break;
|
|
208
|
+
|
|
209
|
+
const nextPageBtn = findNextShopPageButton(response);
|
|
210
|
+
if (!nextPageBtn) break;
|
|
211
|
+
try {
|
|
212
|
+
const moved = await safeClickButton(response, nextPageBtn);
|
|
213
|
+
response = moved || (await waitForDankMemer(5000)) || response;
|
|
214
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
215
|
+
await sleep(200);
|
|
216
|
+
} catch {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
175
220
|
|
|
176
221
|
if (!buyBtn) {
|
|
177
222
|
LOG.warn(`[shop] No button for "${itemName}" (search: "${searchTerm}")`);
|
|
178
|
-
const named =
|
|
223
|
+
const named = getAllButtons(response).filter(b => b.label);
|
|
179
224
|
if (named.length > 0) {
|
|
180
225
|
LOG.debug(`[shop] Available: ${named.map(b => `"${b.label}"(${b.disabled ? 'DIS' : 'EN'})`).join(', ')}`);
|
|
181
226
|
}
|
package/lib/commands/stream.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const {
|
|
2
2
|
LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
|
|
3
3
|
safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
|
|
4
|
-
isCV2, ensureCV2, stripAnsi,
|
|
4
|
+
isCV2, ensureCV2, stripAnsi, clickCV2Button,
|
|
5
5
|
} = require('./utils');
|
|
6
6
|
const { buyItem } = require('./shop');
|
|
7
7
|
|
|
8
8
|
const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
|
|
9
9
|
const STREAM_ACTION_LABELS = Object.freeze(['run ad', 'read chat', 'collect donations']);
|
|
10
|
+
const RE_STREAM_INTERACT_MIN = /interact\s+with\s+your\s+stream\s+every\s+`?(\d+)`?\s*minutes?/i;
|
|
10
11
|
|
|
11
12
|
function normalizeLower(text) {
|
|
12
13
|
return String(text || '')
|
|
@@ -30,6 +31,62 @@ async function refetchMsg(channel, msgId) {
|
|
|
30
31
|
try { return await channel.messages.fetch(msgId); } catch { return null; }
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
async function waitForMessageEditById(client, msgId, channelId, timeoutMs = 6000) {
|
|
35
|
+
if (!client?.on) return null;
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
try { client.removeListener('messageUpdate', onUpdate); } catch {}
|
|
39
|
+
resolve(null);
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
|
|
42
|
+
function onUpdate(_oldMsg, newMsg) {
|
|
43
|
+
if (!newMsg) return;
|
|
44
|
+
if (newMsg.id !== msgId) return;
|
|
45
|
+
if (channelId && newMsg.channel?.id !== channelId && newMsg.channelId !== channelId) return;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
try { client.removeListener('messageUpdate', onUpdate); } catch {}
|
|
48
|
+
resolve(newMsg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
client.on('messageUpdate', onUpdate);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function messageStateSignature(msg) {
|
|
56
|
+
const text = normalizeLower(stripAnsi(getFullText(msg))).slice(0, 220);
|
|
57
|
+
const btnSig = getAllButtons(msg)
|
|
58
|
+
.map(b => `${(b.label || '').toLowerCase()}|${(b.customId || b.custom_id || '').toLowerCase()}|${b.disabled ? '1' : '0'}`)
|
|
59
|
+
.sort()
|
|
60
|
+
.join(';')
|
|
61
|
+
.slice(0, 300);
|
|
62
|
+
return `${text}||${btnSig}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, client, timeoutMs = 12000 }) {
|
|
66
|
+
const start = Date.now();
|
|
67
|
+
const prevSig = messageStateSignature(prevMsg);
|
|
68
|
+
while (Date.now() - start < timeoutMs) {
|
|
69
|
+
const byIdUpdate = await waitForMessageEditById(client, prevMsg.id, prevMsg.channel?.id || prevMsg.channelId, 1800);
|
|
70
|
+
if (byIdUpdate) {
|
|
71
|
+
await hydrate(byIdUpdate);
|
|
72
|
+
if (messageStateSignature(byIdUpdate) !== prevSig) return byIdUpdate;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const evt = await waitForDankMemer(1500);
|
|
76
|
+
if (evt) {
|
|
77
|
+
await hydrate(evt);
|
|
78
|
+
if (messageStateSignature(evt) !== prevSig) return evt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fresh = await refetchMsg(channel, prevMsg.id);
|
|
82
|
+
if (fresh) {
|
|
83
|
+
await hydrate(fresh);
|
|
84
|
+
if (messageStateSignature(fresh) !== prevSig) return fresh;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
33
90
|
function getGoLiveButton(msg) {
|
|
34
91
|
const buttons = getAllButtons(msg);
|
|
35
92
|
return buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('go live')) || null;
|
|
@@ -46,11 +103,14 @@ function isLiveDashboard(msg, lowerText) {
|
|
|
46
103
|
return labels.some(l => l.includes('end stream')) && labels.some(l => STREAM_ACTION_LABELS.some(a => l.includes(a)));
|
|
47
104
|
}
|
|
48
105
|
|
|
49
|
-
function isStreamManagerScreen(lowerText) {
|
|
106
|
+
function isStreamManagerScreen(msg, lowerText) {
|
|
107
|
+
const labels = getAllButtons(msg).map(b => (b.label || '').toLowerCase());
|
|
108
|
+
const hasManagerButtons = labels.some(l => l.includes('go live')) || labels.some(l => l.includes('view setup'));
|
|
50
109
|
return lowerText.includes('stream manager')
|
|
51
110
|
|| lowerText.includes('what game do you want to stream')
|
|
52
111
|
|| lowerText.includes('view setup')
|
|
53
|
-
|| lowerText.includes('go live')
|
|
112
|
+
|| lowerText.includes('go live')
|
|
113
|
+
|| hasManagerButtons;
|
|
54
114
|
}
|
|
55
115
|
|
|
56
116
|
function isActionResultText(lowerText) {
|
|
@@ -61,6 +121,16 @@ function isActionResultText(lowerText) {
|
|
|
61
121
|
|| lowerText.includes('received');
|
|
62
122
|
}
|
|
63
123
|
|
|
124
|
+
function parseStreamInteractCooldownSec(text) {
|
|
125
|
+
const clean = String(stripAnsi(text || '')).replace(/\s+/g, ' ').trim();
|
|
126
|
+
const mm = clean.match(RE_STREAM_INTERACT_MIN);
|
|
127
|
+
if (mm) {
|
|
128
|
+
const mins = parseInt(mm[1], 10);
|
|
129
|
+
if (Number.isFinite(mins) && mins > 0) return mins * 60;
|
|
130
|
+
}
|
|
131
|
+
return 600;
|
|
132
|
+
}
|
|
133
|
+
|
|
64
134
|
async function selectRandomStreamOption(msg) {
|
|
65
135
|
const menus = getAllSelectMenus(msg);
|
|
66
136
|
if (menus.length === 0) return false;
|
|
@@ -113,6 +183,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
113
183
|
logMsg(response, 'stream');
|
|
114
184
|
let text = getFullText(response);
|
|
115
185
|
let lower = normalizeLower(stripAnsi(text));
|
|
186
|
+
let nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
116
187
|
|
|
117
188
|
// Missing items — buy keyboard + mouse
|
|
118
189
|
const missing = needsItem(lower);
|
|
@@ -140,15 +211,18 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
140
211
|
logMsg(response, 'stream-retry');
|
|
141
212
|
text = getFullText(response);
|
|
142
213
|
lower = normalizeLower(stripAnsi(text));
|
|
214
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
143
215
|
}
|
|
144
216
|
|
|
145
217
|
// Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
|
|
218
|
+
let lastGoLiveCustomId = null;
|
|
146
219
|
for (let step = 0; step < 6; step++) {
|
|
147
220
|
text = getFullText(response);
|
|
148
221
|
lower = normalizeLower(stripAnsi(text));
|
|
222
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
149
223
|
|
|
150
|
-
|
|
151
|
-
|
|
224
|
+
if (isLiveDashboard(response, lower)) break;
|
|
225
|
+
if (!isStreamManagerScreen(response, lower)) break;
|
|
152
226
|
|
|
153
227
|
const selected = await selectRandomStreamOption(response);
|
|
154
228
|
if (selected) {
|
|
@@ -162,7 +236,34 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
162
236
|
}
|
|
163
237
|
|
|
164
238
|
const goLiveBtn = getGoLiveButton(response);
|
|
239
|
+
if (goLiveBtn?.customId || goLiveBtn?.custom_id) {
|
|
240
|
+
lastGoLiveCustomId = goLiveBtn.customId || goLiveBtn.custom_id;
|
|
241
|
+
}
|
|
165
242
|
if (!goLiveBtn) {
|
|
243
|
+
// Parser fallback: game prompt text detected but buttons/selects may be hidden by library.
|
|
244
|
+
// Use previously known stream-game custom_id directly against current message.
|
|
245
|
+
if (lower.includes('what game do you want to stream') && lastGoLiveCustomId) {
|
|
246
|
+
LOG.info('[stream] Go Live button not visible; trying raw stream-game fallback');
|
|
247
|
+
try {
|
|
248
|
+
await clickCV2Button(response, lastGoLiveCustomId);
|
|
249
|
+
const transitioned = await waitForStreamTransition({
|
|
250
|
+
channel,
|
|
251
|
+
waitForDankMemer,
|
|
252
|
+
client,
|
|
253
|
+
prevMsg: response,
|
|
254
|
+
timeoutMs: 12000,
|
|
255
|
+
});
|
|
256
|
+
if (transitioned) {
|
|
257
|
+
response = transitioned;
|
|
258
|
+
await hydrate(response);
|
|
259
|
+
logMsg(response, `stream-go-live-raw-${step}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
LOG.error(`[stream] Raw Go Live fallback failed: ${e.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
166
267
|
await sleep(300);
|
|
167
268
|
const fresh = await refetchMsg(channel, response.id);
|
|
168
269
|
if (fresh) {
|
|
@@ -176,14 +277,57 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
176
277
|
LOG.info('[stream] Clicking "Go Live"');
|
|
177
278
|
await humanDelay(100, 250);
|
|
178
279
|
try {
|
|
280
|
+
const before = response;
|
|
179
281
|
const liveResult = await safeClickButton(response, goLiveBtn);
|
|
180
|
-
const
|
|
282
|
+
const liveLooksLikeMessage = Boolean(liveResult?.id && (liveResult?.channel?.id || liveResult?.channelId));
|
|
283
|
+
let next = liveLooksLikeMessage ? liveResult : null;
|
|
284
|
+
if (!next) {
|
|
285
|
+
next = await waitForStreamTransition({
|
|
286
|
+
channel,
|
|
287
|
+
waitForDankMemer,
|
|
288
|
+
client,
|
|
289
|
+
prevMsg: before,
|
|
290
|
+
timeoutMs: 12000,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (!next) {
|
|
294
|
+
// Last fallback: explicit refetch of the original message after click.
|
|
295
|
+
const fetched = await refetchMsg(channel, before.id);
|
|
296
|
+
if (fetched) next = fetched;
|
|
297
|
+
}
|
|
181
298
|
if (next) {
|
|
182
299
|
response = next;
|
|
183
300
|
await hydrate(response);
|
|
184
301
|
logMsg(response, `stream-go-live-${step}`);
|
|
185
302
|
continue;
|
|
186
303
|
}
|
|
304
|
+
|
|
305
|
+
// Some accounts open setup first; try a fallback non-destructive button once.
|
|
306
|
+
const fallbackBtn = getAllButtons(response).find(b =>
|
|
307
|
+
!b.disabled &&
|
|
308
|
+
b.label &&
|
|
309
|
+
((b.label || '').toLowerCase().includes('view setup') || (b.label || '').toLowerCase().includes('setup'))
|
|
310
|
+
);
|
|
311
|
+
if (fallbackBtn) {
|
|
312
|
+
LOG.info('[stream] Go Live transition not detected; trying setup fallback');
|
|
313
|
+
await humanDelay(80, 180);
|
|
314
|
+
try {
|
|
315
|
+
const fallbackRes = await safeClickButton(response, fallbackBtn);
|
|
316
|
+
const afterFallback = fallbackRes || (await waitForStreamTransition({
|
|
317
|
+
channel,
|
|
318
|
+
waitForDankMemer,
|
|
319
|
+
client,
|
|
320
|
+
prevMsg: before,
|
|
321
|
+
timeoutMs: 8000,
|
|
322
|
+
}));
|
|
323
|
+
if (afterFallback) {
|
|
324
|
+
response = afterFallback;
|
|
325
|
+
await hydrate(response);
|
|
326
|
+
logMsg(response, `stream-setup-fallback-${step}`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
187
331
|
} catch (e) {
|
|
188
332
|
LOG.error(`[stream] Go Live click failed: ${e.message}`);
|
|
189
333
|
}
|
|
@@ -194,11 +338,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
194
338
|
// Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
|
|
195
339
|
text = getFullText(response);
|
|
196
340
|
lower = normalizeLower(stripAnsi(text));
|
|
341
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
197
342
|
|
|
198
343
|
if (isLiveDashboard(response, lower)) {
|
|
199
344
|
const actions = getLiveActionButtons(response);
|
|
200
345
|
if (actions.length > 0) {
|
|
201
346
|
const action = pickRandom(actions);
|
|
347
|
+
const actionAt = new Date();
|
|
202
348
|
LOG.info(`[stream] Live action: "${action.label}"`);
|
|
203
349
|
await humanDelay(120, 320);
|
|
204
350
|
try {
|
|
@@ -231,10 +377,12 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
231
377
|
}
|
|
232
378
|
text = bestText;
|
|
233
379
|
lower = normalizeLower(stripAnsi(text));
|
|
380
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
234
381
|
}
|
|
235
382
|
} catch (e) {
|
|
236
383
|
LOG.error(`[stream] Action click failed: ${e.message}`);
|
|
237
384
|
}
|
|
385
|
+
LOG.info(`[stream] Action timestamp: ${actionAt.toISOString()} | next interaction in ${Math.ceil(nextCooldownSec / 60)}m`);
|
|
238
386
|
} else {
|
|
239
387
|
LOG.info('[stream] Live dashboard found but no action buttons available yet');
|
|
240
388
|
}
|
|
@@ -244,13 +392,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
244
392
|
const coins = parseCoins(text);
|
|
245
393
|
if (coins > 0) {
|
|
246
394
|
LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
247
|
-
return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec
|
|
395
|
+
return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec };
|
|
248
396
|
}
|
|
249
397
|
|
|
250
|
-
if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec
|
|
398
|
+
if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec };
|
|
251
399
|
|
|
252
400
|
const short = normalizeLower(stripAnsi(text)).substring(0, 60);
|
|
253
|
-
return { result: short || 'streamed', coins: 0, nextCooldownSec
|
|
401
|
+
return { result: short || 'streamed', coins: 0, nextCooldownSec };
|
|
254
402
|
}
|
|
255
403
|
|
|
256
404
|
module.exports = { runStream };
|
package/lib/commands/utils.js
CHANGED
|
@@ -249,6 +249,17 @@ function flattenComponents(components) {
|
|
|
249
249
|
_accessoryOf: item,
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
|
+
if (acc.type === 3 || acc.type === 'SELECT_MENU' || acc.type === 'STRING_SELECT') {
|
|
253
|
+
flat.push({
|
|
254
|
+
type: 'STRING_SELECT',
|
|
255
|
+
customId: acc.custom_id || acc.customId,
|
|
256
|
+
options: acc.options || [],
|
|
257
|
+
disabled: acc.disabled || false,
|
|
258
|
+
placeholder: acc.placeholder,
|
|
259
|
+
data: acc,
|
|
260
|
+
_accessoryOf: item,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
252
263
|
}
|
|
253
264
|
}
|
|
254
265
|
return flat;
|
|
@@ -273,7 +284,7 @@ function findSelectMenuOption(msg, label) {
|
|
|
273
284
|
const menus = getAllSelectMenus(msg);
|
|
274
285
|
for (const comp of menus) {
|
|
275
286
|
const opt = (comp.options || []).find(o => o.label?.toLowerCase().includes(label.toLowerCase()));
|
|
276
|
-
if (opt) return { menuCustomId: comp.customId, option: opt, component: comp };
|
|
287
|
+
if (opt) return { menuCustomId: comp.customId || comp.custom_id, option: opt, component: comp };
|
|
277
288
|
}
|
|
278
289
|
return null;
|
|
279
290
|
}
|
|
@@ -429,13 +440,15 @@ function isCV2(msg) {
|
|
|
429
440
|
|
|
430
441
|
async function ensureCV2(msg, force = false) {
|
|
431
442
|
if (!msg) return msg;
|
|
432
|
-
|
|
443
|
+
const msgEditedTs = msg.editedTimestamp || msg.editedAt?.getTime?.() || null;
|
|
444
|
+
if (!force && msg._cv2 && msg._cv2EditedTs === msgEditedTs) return msg;
|
|
433
445
|
if (!isCV2(msg)) return msg;
|
|
434
446
|
try {
|
|
435
447
|
if (force) {
|
|
436
448
|
delete msg._cv2;
|
|
437
449
|
delete msg._cv2text;
|
|
438
450
|
delete msg._cv2buttons;
|
|
451
|
+
delete msg._cv2EditedTs;
|
|
439
452
|
cv2Cache.delete(msg.id);
|
|
440
453
|
}
|
|
441
454
|
const token = msg.client?.token;
|
|
@@ -445,10 +458,17 @@ async function ensureCV2(msg, force = false) {
|
|
|
445
458
|
// LRU cache hit — O(1) lookup avoids redundant HTTP fetches
|
|
446
459
|
const cached = cv2Cache.get(msg.id);
|
|
447
460
|
if (cached && !force) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
461
|
+
const cachedComponents = Array.isArray(cached) ? cached : cached.components;
|
|
462
|
+
const cachedEditedTs = Array.isArray(cached) ? null : (cached.editedTimestamp || null);
|
|
463
|
+
|
|
464
|
+
// If message has been edited since cache snapshot, ignore stale cache.
|
|
465
|
+
if (!msgEditedTs || cachedEditedTs === msgEditedTs) {
|
|
466
|
+
msg._cv2 = cachedComponents;
|
|
467
|
+
msg._cv2text = _extractCV2Text(cachedComponents).trim();
|
|
468
|
+
msg._cv2buttons = _extractCV2Buttons(cachedComponents);
|
|
469
|
+
msg._cv2EditedTs = cachedEditedTs;
|
|
470
|
+
return msg;
|
|
471
|
+
}
|
|
452
472
|
}
|
|
453
473
|
|
|
454
474
|
let raw = null;
|
|
@@ -470,10 +490,11 @@ async function ensureCV2(msg, force = false) {
|
|
|
470
490
|
}
|
|
471
491
|
|
|
472
492
|
if (raw?.components) {
|
|
473
|
-
cv2Cache.set(msg.id, raw.components);
|
|
493
|
+
cv2Cache.set(msg.id, { components: raw.components, editedTimestamp: msgEditedTs });
|
|
474
494
|
msg._cv2 = raw.components;
|
|
475
495
|
msg._cv2text = _extractCV2Text(raw.components).trim();
|
|
476
496
|
msg._cv2buttons = _extractCV2Buttons(raw.components);
|
|
497
|
+
msg._cv2EditedTs = msgEditedTs;
|
|
477
498
|
}
|
|
478
499
|
} catch (e) { LOG.debug(`[cv2] fetch error: ${e.message}`); }
|
|
479
500
|
return msg;
|