dankgrinder 6.37.0 → 6.42.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/beg.js +26 -4
- package/lib/commands/farm.js +36 -10
- package/lib/commands/fishVision.js +27 -1
- package/lib/commands/index.js +2 -3
- package/lib/commands/profile.js +200 -52
- package/lib/commands/stream.js +1 -1
- package/lib/commands/utils.js +4 -2
- package/lib/commands/work.js +1 -1
- package/lib/grinder.js +390 -142
- package/lib/rawLogger.js +13 -11
- package/lib/structures.js +19 -26
- package/package.json +1 -1
- package/lib/commands/scratch.js +0 -83
package/lib/commands/beg.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Beg command handler.
|
|
3
3
|
* Simple command: send "pls beg", parse coins from response.
|
|
4
|
+
* Detects: positive coins, Life Saver drops, negative (no coins).
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep } = require('./utils');
|
|
7
8
|
|
|
8
9
|
const RE_NEWLINE = /\n/g;
|
|
10
|
+
const RE_LIFE_SAVER = /life\s*saver/i;
|
|
11
|
+
const RE_BEG_COINS = /you\s+received\s*:\s*[\s\S]{0,200}?([\d,]+)/i;
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* @param {object} opts
|
|
12
15
|
* @param {object} opts.channel
|
|
13
16
|
* @param {function} opts.waitForDankMemer
|
|
14
|
-
* @returns {Promise<{result: string, coins: number}>}
|
|
17
|
+
* @returns {Promise<{result: string, coins: number, lifeSaver?: boolean}>}
|
|
15
18
|
*/
|
|
16
19
|
async function runBeg({ channel, waitForDankMemer }) {
|
|
17
20
|
LOG.cmd(`${c.white}${c.bold}pls beg${c.reset}`);
|
|
@@ -33,11 +36,30 @@ async function runBeg({ channel, waitForDankMemer }) {
|
|
|
33
36
|
|
|
34
37
|
logMsg(response, 'beg');
|
|
35
38
|
const text = getFullText(response);
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
// Extract coins: prefer "You received" pattern for beg responses
|
|
41
|
+
let coins = 0;
|
|
42
|
+
const begMatch = text.match(RE_BEG_COINS);
|
|
43
|
+
if (begMatch) {
|
|
44
|
+
coins = parseInt(begMatch[1].replace(/,/g, ''), 10) || 0;
|
|
45
|
+
}
|
|
46
|
+
// Fallback to general parseCoins
|
|
47
|
+
if (coins === 0) {
|
|
48
|
+
coins = parseCoins(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Detect Life Saver drops
|
|
52
|
+
const lifeSaver = RE_LIFE_SAVER.test(text);
|
|
37
53
|
|
|
38
54
|
if (coins > 0) {
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
const lsNote = lifeSaver ? ` + ${c.cyan}Life Saver${c.reset}` : '';
|
|
56
|
+
LOG.coin(`[beg] ${c.green}+⏣ ${coins.toLocaleString()}${lsNote}${c.reset}`);
|
|
57
|
+
return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${lifeSaver ? ' + Life Saver' : ''}${c.reset}`, coins, lifeSaver };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (lifeSaver) {
|
|
61
|
+
LOG.coin(`[beg] ${c.cyan}Life Saver${c.reset} (no coins)`);
|
|
62
|
+
return { result: 'beg → Life Saver', coins: 0, lifeSaver };
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
LOG.info(`[beg] ${text.substring(0, 80).replace(RE_NEWLINE, ' ')}`);
|
package/lib/commands/farm.js
CHANGED
|
@@ -68,11 +68,30 @@ function parseFarmCooldownSec(text) {
|
|
|
68
68
|
const hasCooldownContext = /easy tiger|slow it down|already farmed|farm again|cooldown|can be used again|on cooldown/.test(lower);
|
|
69
69
|
if (!hasCooldownContext) return null;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// Handle all Discord timestamp formats: :R, :f, :F, :T, :t, :d, :D
|
|
72
|
+
// :R = relative seconds from now (already seconds since epoch difference)
|
|
73
|
+
// :f/:F = absolute datetime → diff from now
|
|
74
|
+
// :T/:t/:d/:D = not useful for cooldowns (no time component)
|
|
75
|
+
const RE_TS_ALL = /<t:(\d+):([tTdDfFR])>/g;
|
|
76
|
+
const now = Math.floor(Date.now() / 1000);
|
|
77
|
+
let best = null;
|
|
78
|
+
for (const m of clean.matchAll(RE_TS_ALL)) {
|
|
79
|
+
const ts = parseInt(m[1], 10);
|
|
80
|
+
const fmt = m[2];
|
|
81
|
+
if (!Number.isFinite(ts)) continue;
|
|
82
|
+
let diff;
|
|
83
|
+
if (fmt === 'R') {
|
|
84
|
+
diff = ts - now; // :R is already relative seconds
|
|
85
|
+
} else if (fmt === 'f' || fmt === 'F' || fmt === 'T') {
|
|
86
|
+
diff = ts - now; // :f/:F/:T are absolute → diff gives seconds remaining
|
|
87
|
+
} else {
|
|
88
|
+
// :t/:d/:D don't have enough info for cooldown calc — skip
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (diff > 0 && (best === null || diff < best)) best = diff;
|
|
75
92
|
}
|
|
93
|
+
if (best !== null) return Math.max(5, best);
|
|
94
|
+
|
|
76
95
|
const mm = clean.match(RE_MIN);
|
|
77
96
|
if (mm) return parseInt(mm[1], 10) * 60;
|
|
78
97
|
const hh = clean.match(RE_HR);
|
|
@@ -1589,16 +1608,23 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1589
1608
|
LOG.coin(`[farm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
1590
1609
|
// After a harvest, Dank Memer auto-plants new crops. Record harvest time
|
|
1591
1610
|
// in Redis so the next run knows when crops should be ready.
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1611
|
+
// Only force a short 30s re-check if there is NO known grow queue timer.
|
|
1612
|
+
// If growReadyEnd is set (e.g. "ready in 26m"), that takes priority.
|
|
1594
1613
|
const afterHarvestState = analyzeFarmState({ msg: cycleResponse, text });
|
|
1595
1614
|
const farmIsHarvested = afterHarvestState.stage === 'overview'
|
|
1596
1615
|
|| /seems? pretty empty|empty\.{0,3}|hoe|water|plant|harvest/i.test(text);
|
|
1597
1616
|
if (farmIsHarvested) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1617
|
+
const hasGrowTimer = Number.isFinite(growReadyEnd) && growReadyEnd > 0;
|
|
1618
|
+
if (hasGrowTimer) {
|
|
1619
|
+
// Crops were planted and a grow queue timer exists (e.g. "ready in 26m").
|
|
1620
|
+
// Respect that timer — the 30s cap should NOT override it.
|
|
1621
|
+
LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage}, grow in ${Math.ceil(growReadyEnd / 60)}m — waiting for grow queue`);
|
|
1622
|
+
} else {
|
|
1623
|
+
// No grow queue timer visible (farm is truly empty or manage menu without timestamp).
|
|
1624
|
+
// Short re-check to start the next hoe→water→plant→harvest cycle.
|
|
1625
|
+
nextCd = Math.min(nextCd, 30);
|
|
1626
|
+
LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage} — re-checking in 30s`);
|
|
1627
|
+
}
|
|
1602
1628
|
}
|
|
1603
1629
|
// Record in Redis for cross-instance awareness
|
|
1604
1630
|
let growDurMs = null;
|
|
@@ -11,10 +11,27 @@ const sharp = require('sharp');
|
|
|
11
11
|
const https = require('https');
|
|
12
12
|
const http = require('http');
|
|
13
13
|
|
|
14
|
+
// Allowed CDN hostnames for image downloads (prevents SSRF).
|
|
15
|
+
const ALLOWED_IMAGE_HOSTS = new Set([
|
|
16
|
+
'cdn.discordapp.com',
|
|
17
|
+
'media.discordapp.net',
|
|
18
|
+
'images.discordapp.net',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB cap — prevents memory exhaustion
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Download an image from a URL and return as Buffer.
|
|
25
|
+
* Only allows Discord CDN URLs and enforces a max size limit.
|
|
16
26
|
*/
|
|
17
27
|
function downloadImage(url) {
|
|
28
|
+
// ── SSRF check ──────────────────────────────────────────────
|
|
29
|
+
let hostname;
|
|
30
|
+
try { hostname = new URL(url).hostname; } catch { return Promise.reject(new Error('invalid URL')); }
|
|
31
|
+
if (!ALLOWED_IMAGE_HOSTS.has(hostname)) {
|
|
32
|
+
return Promise.reject(new Error(`disallowed host: ${hostname}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
return new Promise((resolve, reject) => {
|
|
19
36
|
const proto = url.startsWith('https') ? https : http;
|
|
20
37
|
const req = proto.get(url, res => {
|
|
@@ -22,7 +39,16 @@ function downloadImage(url) {
|
|
|
22
39
|
return downloadImage(res.headers.location).then(resolve, reject);
|
|
23
40
|
}
|
|
24
41
|
const chunks = [];
|
|
25
|
-
|
|
42
|
+
let bytesReceived = 0;
|
|
43
|
+
res.on('data', c => {
|
|
44
|
+
bytesReceived += c.length;
|
|
45
|
+
if (bytesReceived > MAX_IMAGE_BYTES) {
|
|
46
|
+
req.destroy();
|
|
47
|
+
reject(new Error(`image too large: ${bytesReceived} bytes (max ${MAX_IMAGE_BYTES})`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
chunks.push(c);
|
|
51
|
+
});
|
|
26
52
|
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
27
53
|
res.on('error', reject);
|
|
28
54
|
});
|
package/lib/commands/index.js
CHANGED
|
@@ -13,7 +13,6 @@ const { runHunt } = require('./hunt');
|
|
|
13
13
|
const { runDig } = require('./dig');
|
|
14
14
|
const { runFish, sellAllFish } = require('./fish');
|
|
15
15
|
const { runPostMemes } = require('./postmemes');
|
|
16
|
-
const { runScratch } = require('./scratch');
|
|
17
16
|
const { runBlackjack } = require('./blackjack');
|
|
18
17
|
const { runTrivia, triviaDB } = require('./trivia');
|
|
19
18
|
const { runWorkShift } = require('./work');
|
|
@@ -23,7 +22,7 @@ const { runGeneric, runAlert } = require('./generic');
|
|
|
23
22
|
const { runStream } = require('./stream');
|
|
24
23
|
const { runDrops } = require('./drops');
|
|
25
24
|
const { buyItem, ITEM_COSTS } = require('./shop');
|
|
26
|
-
const { getPlayerLevel, meetsLevelRequirement } = require('./profile');
|
|
25
|
+
const { getPlayerLevel, meetsLevelRequirement, runProfile } = require('./profile');
|
|
27
26
|
const { runInventory, fetchItemValues, enrichItems, getCachedInventory, getAllInventories, updateInventoryItem, deleteInventoryItem } = require('./inventory');
|
|
28
27
|
|
|
29
28
|
module.exports = {
|
|
@@ -39,7 +38,6 @@ module.exports = {
|
|
|
39
38
|
runFish,
|
|
40
39
|
sellAllFish,
|
|
41
40
|
runPostMemes,
|
|
42
|
-
runScratch,
|
|
43
41
|
runBlackjack,
|
|
44
42
|
runTrivia,
|
|
45
43
|
runWorkShift,
|
|
@@ -66,6 +64,7 @@ module.exports = {
|
|
|
66
64
|
// Profile / Level
|
|
67
65
|
getPlayerLevel,
|
|
68
66
|
meetsLevelRequirement,
|
|
67
|
+
runProfile,
|
|
69
68
|
|
|
70
69
|
// Constants
|
|
71
70
|
SAFE_SEARCH_LOCATIONS,
|
package/lib/commands/profile.js
CHANGED
|
@@ -1,49 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Profile
|
|
3
|
-
*
|
|
4
|
-
* Used to gate commands like scratch (requires level 25).
|
|
4
|
+
* Profile command — parses Dank Memer `pls profile` responses.
|
|
5
|
+
* Extracts: level, XP, badges (emoji IDs), wallet/bank/net, items, commands, pets, showcase.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const {
|
|
8
|
-
LOG, c, getFullText, logMsg, isHoldTight, sleep,
|
|
9
|
+
LOG, c, getFullText, logMsg, isHoldTight, sleep, stripAnsi,
|
|
9
10
|
} = require('./utils');
|
|
10
11
|
|
|
11
|
-
// In-memory cache: { accountId: { level: number, checkedAt: timestamp } }
|
|
12
|
-
const levelCache = {};
|
|
13
12
|
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
const RE_PROFILE_PRESTIGE_LEVEL = /prestige\s+\d+\s+level\s+`?(\d+)`?/i;
|
|
14
|
+
// ── Regex patterns for each profile field ────────────────────────────────────
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
// "Level: `73`" or "Level: Level: `73`"
|
|
17
|
+
const RE_LEVEL = /(?:global\s+)?level[:\s]+[`'"]?(\d+)[`'"]?/i;
|
|
18
|
+
|
|
19
|
+
// "Experience: `69/100`"
|
|
20
|
+
const RE_XP = /experience[:\s]+`?(\d+)\s*\/\s*(\d+)`?/i;
|
|
21
|
+
|
|
22
|
+
// Badges: "<:PB1C:123456789>..." (custom Discord emojis)
|
|
23
|
+
const RE_BADGE = /<a?:(\w+):(\d+)>/g;
|
|
24
|
+
|
|
25
|
+
// "Wallet: `⏣ 3.2M`"
|
|
26
|
+
const RE_WALLET = /wallet[:\s]+[`⏣]?\s*([\d,.KMB]+(?:[KMB])?(?:\.\d+[KMB])?)/i;
|
|
27
|
+
const RE_BANK = /bank[:\s]+[`⏣]?\s*([\d,.KMB]+(?:[KMB])?(?:\.\d+[KMB])?)/i;
|
|
28
|
+
const RE_NET = /net[:\s]+[`⏣]?\s*([\d,.KMB]+(?:[KMB])?(?:\.\d+[KMB])?)/i;
|
|
29
|
+
|
|
30
|
+
// "Items: Unique: `70`\nTotal: `766`\nWorth: `⏣ 31M`"
|
|
31
|
+
const RE_ITEMS_UNIQUE = /unique[:\s]+[`']?(\d+)[`']?/i;
|
|
32
|
+
const RE_ITEMS_TOTAL = /total[:\s]+[`']?(\d+)[`']?/i;
|
|
33
|
+
const RE_ITEMS_WORTH = /worth[:\s]+[`⏣]?\s*([\d,.KMB]+(?:[KMB])?(?:\.\d+[KMB])?)/i;
|
|
34
|
+
|
|
35
|
+
// "Commands: Total: `5.1K`\nFavorite: `hunt`"
|
|
36
|
+
const RE_CMDS_TOTAL = /(?:total\s+)?commands?[:\s]+[`']?([\d,.KMB]+(?:[KMB])?(?:\.\d+[KMB])?)[`']?/i;
|
|
37
|
+
const RE_FAVORITE = /favorite[:\s]+[`']?(\w+)[`']?/i;
|
|
38
|
+
|
|
39
|
+
// Pets: "Pets: <:Rock:861584585250308106> **Rock**\n`Level 1 Rock`"
|
|
40
|
+
const RE_PET_BLOCK = /pets[:\s]*((?:<a?:\w+:\d+>\s*\*+\*?\*?\*?\s*(?:[\w ]+)\*+\s*`[^`]+`\s*)+)/gi;
|
|
41
|
+
const RE_PET_ENTRY = /<a?:(\w+):(\d+)>[\s\*]*([\w ]+?)[\s\*]*`([^`]+)`/gi;
|
|
42
|
+
|
|
43
|
+
// Showcase: "Showcase: ..." or "Showcase: No showcase"
|
|
44
|
+
const RE_SHOWCASE = /showcase[:\s]*(.+?)(?:\n|$)/i;
|
|
45
|
+
|
|
46
|
+
// ── Coin number parser ───────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function parseCoinNum(str) {
|
|
49
|
+
if (!str) return 0;
|
|
50
|
+
str = str.replace(/[⏣,\s]/g, '').toUpperCase();
|
|
51
|
+
const m = parseFloat(str);
|
|
52
|
+
if (isNaN(m)) return 0;
|
|
53
|
+
if (str.includes('B')) return Math.round(m * 1_000_000_000);
|
|
54
|
+
if (str.includes('M')) return Math.round(m * 1_000_000);
|
|
55
|
+
if (str.includes('K')) return Math.round(m * 1_000);
|
|
56
|
+
return Math.round(m);
|
|
28
57
|
}
|
|
29
58
|
|
|
59
|
+
// ── Badge extraction ──────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function parseBadges(text) {
|
|
62
|
+
const badges = [];
|
|
63
|
+
let m;
|
|
64
|
+
const re = /<a?:([^>]+):(\d+)>/g;
|
|
65
|
+
while ((m = re.exec(text)) !== null) {
|
|
66
|
+
badges.push({ name: m[1], emojiId: m[2] });
|
|
67
|
+
}
|
|
68
|
+
return badges;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Pet extraction ────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function parsePets(text) {
|
|
74
|
+
const pets = [];
|
|
75
|
+
let m;
|
|
76
|
+
// Match: emoji + bold name + level line
|
|
77
|
+
// e.g. "<:Rock:861584585250308106> **Rock**\n`Level 1 Rock`"
|
|
78
|
+
const re = /<a?:(\w+):(\d+)>\s*\*\*([^\*]+)\*\*\s*`([^`]+)`/gi;
|
|
79
|
+
while ((m = re.exec(text)) !== null) {
|
|
80
|
+
pets.push({
|
|
81
|
+
emojiName: m[1],
|
|
82
|
+
emojiId: m[2],
|
|
83
|
+
name: m[3].trim(),
|
|
84
|
+
level: m[4].trim(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return pets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Main parse function ──────────────────────────────────────────────────────
|
|
91
|
+
|
|
30
92
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @param {object}
|
|
33
|
-
* @
|
|
34
|
-
* @param {function} opts.waitForDankMemer
|
|
35
|
-
* @param {string} [opts.accountId] - for cache key
|
|
36
|
-
* @param {object} [opts.redis] - Redis client for persistent cache
|
|
37
|
-
* @returns {Promise<number|null>} Level number or null if can't determine
|
|
93
|
+
* Parse all profile fields from Dank Memer `pls profile` response.
|
|
94
|
+
* @param {object} msg - Discord message (or partial with embeds)
|
|
95
|
+
* @returns {object} Parsed profile data
|
|
38
96
|
*/
|
|
97
|
+
function parseProfile(msg) {
|
|
98
|
+
const text = getFullText(msg);
|
|
99
|
+
|
|
100
|
+
// Level
|
|
101
|
+
let level = null;
|
|
102
|
+
const levelMatch = text.match(RE_LEVEL);
|
|
103
|
+
if (levelMatch) level = parseInt(levelMatch[1], 10);
|
|
104
|
+
|
|
105
|
+
// XP
|
|
106
|
+
let xpCurrent = null, xpMax = null;
|
|
107
|
+
const xpMatch = text.match(RE_XP);
|
|
108
|
+
if (xpMatch) {
|
|
109
|
+
xpCurrent = parseInt(xpMatch[1], 10);
|
|
110
|
+
xpMax = parseInt(xpMatch[2], 10);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Coins
|
|
114
|
+
const wallet = parseCoinNum((text.match(RE_WALLET) || [])[1]);
|
|
115
|
+
const bank = parseCoinNum((text.match(RE_BANK) || [])[1]);
|
|
116
|
+
const net = parseCoinNum((text.match(RE_NET) || [])[1]);
|
|
117
|
+
|
|
118
|
+
// Items
|
|
119
|
+
const itemsUnique = parseInt((text.match(RE_ITEMS_UNIQUE) || [])[1], 10) || 0;
|
|
120
|
+
const itemsTotal = parseInt((text.match(RE_ITEMS_TOTAL) || [])[1], 10) || 0;
|
|
121
|
+
const itemsWorth = parseCoinNum((text.match(RE_ITEMS_WORTH) || [])[1]);
|
|
122
|
+
|
|
123
|
+
// Commands
|
|
124
|
+
const cmdsTotal = (text.match(RE_CMDS_TOTAL) || [])[1] || '0';
|
|
125
|
+
const favorite = ((text.match(RE_FAVORITE) || [])[1] || '').trim().toLowerCase();
|
|
126
|
+
|
|
127
|
+
// Badges
|
|
128
|
+
const badges = parseBadges(text);
|
|
129
|
+
|
|
130
|
+
// Pets
|
|
131
|
+
const pets = parsePets(text);
|
|
132
|
+
|
|
133
|
+
// Showcase
|
|
134
|
+
const showcaseRaw = (text.match(RE_SHOWCASE) || [])[1] || '';
|
|
135
|
+
const showcase = showcaseRaw.toLowerCase().includes('no showcase')
|
|
136
|
+
? []
|
|
137
|
+
: showcaseRaw.split('\n').filter(l => l.trim()).map(l => l.trim().replace(/^\*\*|\*\*$/g, ''));
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
level,
|
|
141
|
+
xpCurrent,
|
|
142
|
+
xpMax,
|
|
143
|
+
wallet,
|
|
144
|
+
bank,
|
|
145
|
+
net,
|
|
146
|
+
itemsUnique,
|
|
147
|
+
itemsTotal,
|
|
148
|
+
itemsWorth,
|
|
149
|
+
cmdsTotal,
|
|
150
|
+
favorite,
|
|
151
|
+
badges,
|
|
152
|
+
pets,
|
|
153
|
+
showcase,
|
|
154
|
+
rawText: text.substring(0, 2000),
|
|
155
|
+
parsedAt: Date.now(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── In-memory cache ─────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
const levelCache = {};
|
|
162
|
+
|
|
39
163
|
async function getPlayerLevel({ channel, waitForDankMemer, accountId = 'default', redis }) {
|
|
40
|
-
// Check in-memory cache first
|
|
41
164
|
const cached = levelCache[accountId];
|
|
42
165
|
if (cached && (Date.now() - cached.checkedAt) < CACHE_TTL) {
|
|
43
166
|
return cached.level;
|
|
44
167
|
}
|
|
45
|
-
|
|
46
|
-
// Check Redis cache
|
|
47
168
|
if (redis) {
|
|
48
169
|
try {
|
|
49
170
|
const redisLevel = await redis.get(`dkg:level:${accountId}`);
|
|
@@ -54,48 +175,75 @@ async function getPlayerLevel({ channel, waitForDankMemer, accountId = 'default'
|
|
|
54
175
|
}
|
|
55
176
|
} catch {}
|
|
56
177
|
}
|
|
57
|
-
|
|
58
|
-
// Fetch from Discord
|
|
59
|
-
LOG.debug('[profile] Checking level...');
|
|
60
178
|
await channel.send('pls profile');
|
|
61
179
|
const response = await waitForDankMemer(8000);
|
|
180
|
+
if (!response) { LOG.warn('[profile] No response'); return null; }
|
|
181
|
+
if (isHoldTight(response)) { await sleep(5000); return null; }
|
|
182
|
+
logMsg(response, 'profile');
|
|
183
|
+
const parsed = parseProfile(response);
|
|
184
|
+
if (parsed.level !== null) {
|
|
185
|
+
levelCache[accountId] = { level: parsed.level, checkedAt: Date.now() };
|
|
186
|
+
if (redis) {
|
|
187
|
+
const ttl = parsed.level >= 25 ? 2592000 : 600;
|
|
188
|
+
try { await redis.set(`dkg:level:${accountId}`, String(parsed.level), 'EX', ttl); } catch {}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return parsed.level;
|
|
192
|
+
}
|
|
62
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Run full profile check, parse all fields, and return structured data.
|
|
196
|
+
* @param {object} opts
|
|
197
|
+
* @param {object} opts.channel
|
|
198
|
+
* @param {function} opts.waitForDankMemer
|
|
199
|
+
* @param {string} [opts.accountId]
|
|
200
|
+
* @param {object} [opts.redis]
|
|
201
|
+
* @returns {Promise<object>} Parsed profile data
|
|
202
|
+
*/
|
|
203
|
+
async function runProfile({ channel, waitForDankMemer, accountId = 'default', redis }) {
|
|
204
|
+
LOG.cmd(`${c.white}${c.bold}pls profile${c.reset}`);
|
|
205
|
+
await channel.send('pls profile');
|
|
206
|
+
const response = await waitForDankMemer(8000);
|
|
63
207
|
if (!response) {
|
|
64
208
|
LOG.warn('[profile] No response');
|
|
65
|
-
return
|
|
209
|
+
return { error: 'no_response' };
|
|
66
210
|
}
|
|
67
|
-
|
|
68
211
|
if (isHoldTight(response)) {
|
|
69
|
-
|
|
70
|
-
|
|
212
|
+
LOG.warn('[profile] Hold Tight');
|
|
213
|
+
await sleep(30000);
|
|
214
|
+
return { error: 'hold_tight' };
|
|
71
215
|
}
|
|
72
|
-
|
|
73
216
|
logMsg(response, 'profile');
|
|
74
|
-
const
|
|
75
|
-
const level = parseLevelFromText(text);
|
|
217
|
+
const parsed = parseProfile(response);
|
|
76
218
|
|
|
77
|
-
if (level !== null) {
|
|
78
|
-
|
|
79
|
-
levelCache[accountId] = { level, checkedAt: Date.now() };
|
|
80
|
-
// Persist to Redis — longer TTL for higher levels since they only go up
|
|
219
|
+
if (parsed.level !== null) {
|
|
220
|
+
levelCache[accountId] = { level: parsed.level, checkedAt: Date.now() };
|
|
81
221
|
if (redis) {
|
|
82
|
-
const ttl = level >= 25 ? 2592000 : 600;
|
|
83
|
-
try { await redis.set(`dkg:level:${accountId}`, String(level), 'EX', ttl); } catch {}
|
|
222
|
+
const ttl = parsed.level >= 25 ? 2592000 : 600;
|
|
223
|
+
try { await redis.set(`dkg:level:${accountId}`, String(parsed.level), 'EX', ttl); } catch {}
|
|
224
|
+
try {
|
|
225
|
+
await redis.set(`dkg:profile:${accountId}`, JSON.stringify(parsed), 'EX', CACHE_TTL / 1000);
|
|
226
|
+
} catch {}
|
|
84
227
|
}
|
|
85
|
-
} else {
|
|
86
|
-
LOG.warn(`[profile] Could not parse level from: ${text.substring(0, 100)}`);
|
|
87
228
|
}
|
|
88
229
|
|
|
89
|
-
|
|
230
|
+
LOG.info(`[profile] Level ${parsed.level} · ⏣ ${parsed.wallet?.toLocaleString() || 0} wallet · ⏣ ${parsed.net?.toLocaleString() || 0} net`);
|
|
231
|
+
return parsed;
|
|
90
232
|
}
|
|
91
233
|
|
|
92
|
-
/**
|
|
93
|
-
* Check if player meets minimum level requirement.
|
|
94
|
-
*/
|
|
95
234
|
async function meetsLevelRequirement(opts, minLevel) {
|
|
96
235
|
const level = await getPlayerLevel(opts);
|
|
97
|
-
if (level === null) return false;
|
|
236
|
+
if (level === null) return false;
|
|
98
237
|
return level >= minLevel;
|
|
99
238
|
}
|
|
100
239
|
|
|
101
|
-
module.exports = {
|
|
240
|
+
module.exports = {
|
|
241
|
+
getPlayerLevel,
|
|
242
|
+
meetsLevelRequirement,
|
|
243
|
+
parseLevelFromText: (text) => parseProfile({ content: text, embeds: [] }).level,
|
|
244
|
+
runProfile,
|
|
245
|
+
parseProfile,
|
|
246
|
+
parseCoinNum,
|
|
247
|
+
parseBadges,
|
|
248
|
+
parsePets,
|
|
249
|
+
};
|
package/lib/commands/stream.js
CHANGED
|
@@ -206,7 +206,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
206
206
|
|
|
207
207
|
if (isHoldTight(response)) {
|
|
208
208
|
const reason = getHoldTightReason(response);
|
|
209
|
-
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
209
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason, nextCooldownSec: 30 };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
await hydrate(response);
|
package/lib/commands/utils.js
CHANGED
|
@@ -558,7 +558,8 @@ async function clickCV2Button(msg, customId) {
|
|
|
558
558
|
const gId = msg.guildId || msg.guild?.id;
|
|
559
559
|
if (!token) throw new Error('No token for CV2 click');
|
|
560
560
|
const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
|
|
561
|
-
const
|
|
561
|
+
const { randomUUID } = require('crypto');
|
|
562
|
+
const nonce = randomUUID();
|
|
562
563
|
const payloadObj = {
|
|
563
564
|
type: 3,
|
|
564
565
|
application_id: String(msg.applicationId || DANK_MEMER_ID),
|
|
@@ -603,7 +604,8 @@ async function clickCV2SelectMenu(msg, customId, values = []) {
|
|
|
603
604
|
const gId = msg.guildId || msg.guild?.id;
|
|
604
605
|
if (!token) throw new Error('No token for CV2 select');
|
|
605
606
|
const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
|
|
606
|
-
const
|
|
607
|
+
const { randomUUID } = require('crypto');
|
|
608
|
+
const nonce = randomUUID();
|
|
607
609
|
const payloadObj = {
|
|
608
610
|
type: 3,
|
|
609
611
|
application_id: String(msg.applicationId || DANK_MEMER_ID),
|
package/lib/commands/work.js
CHANGED
|
@@ -418,7 +418,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
418
418
|
const reason = getHoldTightReason(current);
|
|
419
419
|
LOG.warn(`[work] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
420
420
|
await sleep(30000);
|
|
421
|
-
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
421
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason, nextCooldownSec: 30 };
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
if (isCV2(current)) await ensureCV2(current);
|