dankgrinder 5.14.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 +102 -15
- 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,27 @@ 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
|
+
|
|
33
55
|
function messageStateSignature(msg) {
|
|
34
56
|
const text = normalizeLower(stripAnsi(getFullText(msg))).slice(0, 220);
|
|
35
57
|
const btnSig = getAllButtons(msg)
|
|
@@ -40,10 +62,16 @@ function messageStateSignature(msg) {
|
|
|
40
62
|
return `${text}||${btnSig}`;
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, timeoutMs = 12000 }) {
|
|
65
|
+
async function waitForStreamTransition({ channel, waitForDankMemer, prevMsg, client, timeoutMs = 12000 }) {
|
|
44
66
|
const start = Date.now();
|
|
45
67
|
const prevSig = messageStateSignature(prevMsg);
|
|
46
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
|
+
|
|
47
75
|
const evt = await waitForDankMemer(1500);
|
|
48
76
|
if (evt) {
|
|
49
77
|
await hydrate(evt);
|
|
@@ -75,11 +103,14 @@ function isLiveDashboard(msg, lowerText) {
|
|
|
75
103
|
return labels.some(l => l.includes('end stream')) && labels.some(l => STREAM_ACTION_LABELS.some(a => l.includes(a)));
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
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'));
|
|
79
109
|
return lowerText.includes('stream manager')
|
|
80
110
|
|| lowerText.includes('what game do you want to stream')
|
|
81
111
|
|| lowerText.includes('view setup')
|
|
82
|
-
|| lowerText.includes('go live')
|
|
112
|
+
|| lowerText.includes('go live')
|
|
113
|
+
|| hasManagerButtons;
|
|
83
114
|
}
|
|
84
115
|
|
|
85
116
|
function isActionResultText(lowerText) {
|
|
@@ -90,6 +121,16 @@ function isActionResultText(lowerText) {
|
|
|
90
121
|
|| lowerText.includes('received');
|
|
91
122
|
}
|
|
92
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
|
+
|
|
93
134
|
async function selectRandomStreamOption(msg) {
|
|
94
135
|
const menus = getAllSelectMenus(msg);
|
|
95
136
|
if (menus.length === 0) return false;
|
|
@@ -142,6 +183,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
142
183
|
logMsg(response, 'stream');
|
|
143
184
|
let text = getFullText(response);
|
|
144
185
|
let lower = normalizeLower(stripAnsi(text));
|
|
186
|
+
let nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
145
187
|
|
|
146
188
|
// Missing items — buy keyboard + mouse
|
|
147
189
|
const missing = needsItem(lower);
|
|
@@ -169,15 +211,18 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
169
211
|
logMsg(response, 'stream-retry');
|
|
170
212
|
text = getFullText(response);
|
|
171
213
|
lower = normalizeLower(stripAnsi(text));
|
|
214
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
172
215
|
}
|
|
173
216
|
|
|
174
217
|
// Setup flow: manager -> option select (game/platform) -> Go Live -> live dashboard
|
|
218
|
+
let lastGoLiveCustomId = null;
|
|
175
219
|
for (let step = 0; step < 6; step++) {
|
|
176
220
|
text = getFullText(response);
|
|
177
221
|
lower = normalizeLower(stripAnsi(text));
|
|
222
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
178
223
|
|
|
179
|
-
|
|
180
|
-
|
|
224
|
+
if (isLiveDashboard(response, lower)) break;
|
|
225
|
+
if (!isStreamManagerScreen(response, lower)) break;
|
|
181
226
|
|
|
182
227
|
const selected = await selectRandomStreamOption(response);
|
|
183
228
|
if (selected) {
|
|
@@ -191,7 +236,34 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
191
236
|
}
|
|
192
237
|
|
|
193
238
|
const goLiveBtn = getGoLiveButton(response);
|
|
239
|
+
if (goLiveBtn?.customId || goLiveBtn?.custom_id) {
|
|
240
|
+
lastGoLiveCustomId = goLiveBtn.customId || goLiveBtn.custom_id;
|
|
241
|
+
}
|
|
194
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
|
+
|
|
195
267
|
await sleep(300);
|
|
196
268
|
const fresh = await refetchMsg(channel, response.id);
|
|
197
269
|
if (fresh) {
|
|
@@ -207,12 +279,22 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
207
279
|
try {
|
|
208
280
|
const before = response;
|
|
209
281
|
const liveResult = await safeClickButton(response, goLiveBtn);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
216
298
|
if (next) {
|
|
217
299
|
response = next;
|
|
218
300
|
await hydrate(response);
|
|
@@ -234,6 +316,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
234
316
|
const afterFallback = fallbackRes || (await waitForStreamTransition({
|
|
235
317
|
channel,
|
|
236
318
|
waitForDankMemer,
|
|
319
|
+
client,
|
|
237
320
|
prevMsg: before,
|
|
238
321
|
timeoutMs: 8000,
|
|
239
322
|
}));
|
|
@@ -255,11 +338,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
255
338
|
// Scheduler uses nextCooldownSec=600, so this executes every 10 minutes.
|
|
256
339
|
text = getFullText(response);
|
|
257
340
|
lower = normalizeLower(stripAnsi(text));
|
|
341
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
258
342
|
|
|
259
343
|
if (isLiveDashboard(response, lower)) {
|
|
260
344
|
const actions = getLiveActionButtons(response);
|
|
261
345
|
if (actions.length > 0) {
|
|
262
346
|
const action = pickRandom(actions);
|
|
347
|
+
const actionAt = new Date();
|
|
263
348
|
LOG.info(`[stream] Live action: "${action.label}"`);
|
|
264
349
|
await humanDelay(120, 320);
|
|
265
350
|
try {
|
|
@@ -292,10 +377,12 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
292
377
|
}
|
|
293
378
|
text = bestText;
|
|
294
379
|
lower = normalizeLower(stripAnsi(text));
|
|
380
|
+
nextCooldownSec = parseStreamInteractCooldownSec(text);
|
|
295
381
|
}
|
|
296
382
|
} catch (e) {
|
|
297
383
|
LOG.error(`[stream] Action click failed: ${e.message}`);
|
|
298
384
|
}
|
|
385
|
+
LOG.info(`[stream] Action timestamp: ${actionAt.toISOString()} | next interaction in ${Math.ceil(nextCooldownSec / 60)}m`);
|
|
299
386
|
} else {
|
|
300
387
|
LOG.info('[stream] Live dashboard found but no action buttons available yet');
|
|
301
388
|
}
|
|
@@ -305,13 +392,13 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
305
392
|
const coins = parseCoins(text);
|
|
306
393
|
if (coins > 0) {
|
|
307
394
|
LOG.coin(`[stream] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
308
|
-
return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec
|
|
395
|
+
return { result: `stream → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec };
|
|
309
396
|
}
|
|
310
397
|
|
|
311
|
-
if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec
|
|
398
|
+
if (ended) return { result: 'stream ended', coins: 0, nextCooldownSec };
|
|
312
399
|
|
|
313
400
|
const short = normalizeLower(stripAnsi(text)).substring(0, 60);
|
|
314
|
-
return { result: short || 'streamed', coins: 0, nextCooldownSec
|
|
401
|
+
return { result: short || 'streamed', coins: 0, nextCooldownSec };
|
|
315
402
|
}
|
|
316
403
|
|
|
317
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;
|