dankgrinder 6.39.0 → 6.45.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/fishVision.js +27 -1
- package/lib/commands/index.js +2 -3
- package/lib/commands/inventory.js +1 -1
- package/lib/commands/profile.js +200 -52
- package/lib/commands/utils.js +4 -2
- package/lib/grinder.js +270 -141
- 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, ' ')}`);
|
|
@@ -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,
|
|
@@ -198,7 +198,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
198
198
|
LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
|
|
199
199
|
|
|
200
200
|
await channel.send('pls inv');
|
|
201
|
-
let response = await waitForDankMemer(
|
|
201
|
+
let response = await waitForDankMemer(15000);
|
|
202
202
|
|
|
203
203
|
if (!response) {
|
|
204
204
|
LOG.warn('[inv] No response');
|
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/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/grinder.js
CHANGED
|
@@ -5,7 +5,7 @@ const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/
|
|
|
5
5
|
const rawLogger = require('./rawLogger');
|
|
6
6
|
const {
|
|
7
7
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
8
|
-
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue,
|
|
8
|
+
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue,
|
|
9
9
|
} = require('./structures');
|
|
10
10
|
const PKG_VERSION = require('../package.json').version;
|
|
11
11
|
|
|
@@ -279,9 +279,9 @@ function boxTop(w, color) { return color + BOX.dtl + BOX.dh.repeat(w - 2) + BOX.
|
|
|
279
279
|
function boxMid(w, color) { return color + BOX.tee + BOX.h.repeat(w - 2) + BOX.teeR + c.reset; }
|
|
280
280
|
function boxBot(w, color) { return color + BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr + c.reset; }
|
|
281
281
|
function boxLine(content, w, color) {
|
|
282
|
-
const stripped = content
|
|
282
|
+
const stripped = stripAnsi(content);
|
|
283
283
|
const pad = Math.max(0, w - 4 - stripped.length);
|
|
284
|
-
return color + BOX.dv + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
|
|
284
|
+
return color + BOX.dv + c.reset + ' ' + stripAnsi(content) + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
|
|
285
285
|
}
|
|
286
286
|
function thinLine(w) { return ' ' + c.dim + BOX.h.repeat(w - 4) + c.reset; }
|
|
287
287
|
|
|
@@ -868,8 +868,8 @@ const CMD_NAMES_CLEAN = {
|
|
|
868
868
|
bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
|
|
869
869
|
ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
|
|
870
870
|
hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
|
|
871
|
-
tidy: 'Tidy', farm: 'Farm', daily: 'Daily',
|
|
872
|
-
|
|
871
|
+
tidy: 'Tidy', farm: 'Farm', daily: 'Daily', monthly: 'Monthly',
|
|
872
|
+
adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
|
|
873
873
|
drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
|
|
874
874
|
work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
|
|
875
875
|
};
|
|
@@ -1490,7 +1490,9 @@ class AccountWorker {
|
|
|
1490
1490
|
totalValue: result.totalValue || 0,
|
|
1491
1491
|
}),
|
|
1492
1492
|
});
|
|
1493
|
-
} catch {
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
console.error(`[${this.username}] inventory API error:`, err?.message || err);
|
|
1495
|
+
}
|
|
1494
1496
|
|
|
1495
1497
|
return {
|
|
1496
1498
|
ok: true,
|
|
@@ -1524,6 +1526,60 @@ class AccountWorker {
|
|
|
1524
1526
|
}
|
|
1525
1527
|
}
|
|
1526
1528
|
|
|
1529
|
+
// ── Profile Check ────────────────────────────────────────────
|
|
1530
|
+
async checkProfile(silent = false) {
|
|
1531
|
+
try {
|
|
1532
|
+
const result = await commands.runProfile({
|
|
1533
|
+
channel: this.channel,
|
|
1534
|
+
waitForDankMemer: (t) => this.waitForDankMemer(t),
|
|
1535
|
+
accountId: this.account.id,
|
|
1536
|
+
redis,
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
if (result.error) {
|
|
1540
|
+
if (!silent) this.log('warn', `[profile] Check failed: ${result.error}`);
|
|
1541
|
+
return result;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (!silent) {
|
|
1545
|
+
this.log('info', `[profile] Level ${result.level} · ⏣ ${(result.wallet || 0).toLocaleString()} wallet · ⏣ ${(result.net || 0).toLocaleString()} net`);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Push to web dashboard via grinder status endpoint
|
|
1549
|
+
try {
|
|
1550
|
+
await fetch(`${API_URL}/api/grinder/profile`, {
|
|
1551
|
+
method: 'POST',
|
|
1552
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
1553
|
+
body: JSON.stringify({
|
|
1554
|
+
account_id: this.account.id,
|
|
1555
|
+
level: result.level,
|
|
1556
|
+
xp_current: result.xpCurrent,
|
|
1557
|
+
xp_max: result.xpMax,
|
|
1558
|
+
wallet: result.wallet,
|
|
1559
|
+
bank: result.bank,
|
|
1560
|
+
net: result.net,
|
|
1561
|
+
items_unique: result.itemsUnique,
|
|
1562
|
+
items_total: result.itemsTotal,
|
|
1563
|
+
items_worth: result.itemsWorth,
|
|
1564
|
+
cmds_total: result.cmdsTotal,
|
|
1565
|
+
favorite: result.favorite,
|
|
1566
|
+
badges: result.badges || [],
|
|
1567
|
+
pets: result.pets || [],
|
|
1568
|
+
showcase: result.showcase || [],
|
|
1569
|
+
raw_text: result.rawText || '',
|
|
1570
|
+
}),
|
|
1571
|
+
});
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
console.error(`[${this.username}] profile API error:`, err?.message || err);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
return { ok: true, ...result };
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
if (!silent) this.log('warn', `[profile] Check error: ${e.message}`);
|
|
1579
|
+
return { ok: false, error: e.message };
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1527
1583
|
async checkBalance(silent = false) {
|
|
1528
1584
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
1529
1585
|
const sentAt = Date.now();
|
|
@@ -1724,22 +1780,48 @@ class AccountWorker {
|
|
|
1724
1780
|
// Each modular command handler sends the command, waits for response,
|
|
1725
1781
|
// handles Hold Tight / cooldowns / item-buying internally.
|
|
1726
1782
|
async runCommand(cmdName, prefix) {
|
|
1727
|
-
|
|
1783
|
+
// Slash commands use '/cmd' without a space between slash and subcommand
|
|
1784
|
+
// (e.g. '/dep max', '/work shift'). Legacy prefix uses 'pls cmd'.
|
|
1785
|
+
const SLASH_CMD = new Map([
|
|
1786
|
+
['dep max', '/dep max'],
|
|
1787
|
+
['with max', '/with max'],
|
|
1788
|
+
['work shift', '/work shift'],
|
|
1789
|
+
['monthly', '/monthly'],
|
|
1790
|
+
]);
|
|
1728
1791
|
const bjBet = Math.max(5000, this.account.bet_amount || 5000);
|
|
1729
1792
|
const gambBet = Math.max(10000, this.account.bet_amount || 10000);
|
|
1730
1793
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1794
|
+
let cmdString;
|
|
1795
|
+
if (prefix === '/') {
|
|
1796
|
+
const slashVariant = SLASH_CMD.get(cmdName);
|
|
1797
|
+
if (slashVariant) {
|
|
1798
|
+
cmdString = slashVariant;
|
|
1799
|
+
} else if (cmdName === 'blackjack') {
|
|
1800
|
+
cmdString = `/bj ${bjBet}`;
|
|
1801
|
+
} else if (cmdName === 'cointoss') {
|
|
1802
|
+
cmdString = `/cointoss ${gambBet}`;
|
|
1803
|
+
} else if (cmdName === 'roulette') {
|
|
1804
|
+
cmdString = `/roulette ${gambBet}`;
|
|
1805
|
+
} else if (cmdName === 'slots') {
|
|
1806
|
+
cmdString = `/slots ${gambBet}`;
|
|
1807
|
+
} else if (cmdName === 'snakeeyes') {
|
|
1808
|
+
cmdString = `/snakeeyes ${gambBet}`;
|
|
1809
|
+
} else {
|
|
1810
|
+
cmdString = `/${cmdName}`;
|
|
1811
|
+
}
|
|
1812
|
+
} else {
|
|
1813
|
+
switch (cmdName) {
|
|
1814
|
+
case 'dep max': cmdString = `${prefix} dep max`; break;
|
|
1815
|
+
case 'with max': cmdString = `${prefix} with max`; break;
|
|
1816
|
+
case 'blackjack': cmdString = `${prefix} bj ${bjBet}`; break;
|
|
1817
|
+
case 'cointoss': cmdString = `${prefix} cointoss ${gambBet}`; break;
|
|
1818
|
+
case 'roulette': cmdString = `${prefix} roulette ${gambBet}`; break;
|
|
1819
|
+
case 'slots': cmdString = `${prefix} slots ${gambBet}`; break;
|
|
1820
|
+
case 'snakeeyes': cmdString = `${prefix} snakeeyes ${gambBet}`; break;
|
|
1821
|
+
case 'work shift': cmdString = `${prefix} work shift`; break;
|
|
1822
|
+
case 'monthly': cmdString = `${prefix} monthly`; break;
|
|
1823
|
+
default: cmdString = `${prefix} ${cmdName}`;
|
|
1824
|
+
}
|
|
1743
1825
|
}
|
|
1744
1826
|
|
|
1745
1827
|
if (shutdownCalled || !this.running) return;
|
|
@@ -1837,7 +1919,6 @@ class AccountWorker {
|
|
|
1837
1919
|
case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
|
|
1838
1920
|
case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
|
|
1839
1921
|
case 'fish': cmdResult = await commands.runFish(cmdOpts); break;
|
|
1840
|
-
case 'scratch': cmdResult = await commands.runScratch(cmdOpts); break;
|
|
1841
1922
|
case 'adventure': cmdResult = await commands.runAdventure(cmdOpts); break;
|
|
1842
1923
|
case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
|
|
1843
1924
|
case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
|
|
@@ -1892,7 +1973,9 @@ class AccountWorker {
|
|
|
1892
1973
|
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
1893
1974
|
body: JSON.stringify({ account_id: this.account.id, active: false }),
|
|
1894
1975
|
});
|
|
1895
|
-
} catch {
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
console.error(`[${this.username}] status deactivation API error:`, err?.message || err);
|
|
1978
|
+
}
|
|
1896
1979
|
await sendLog(this.username, cmdString, 'VERIFICATION — account deactivated', 'error');
|
|
1897
1980
|
sendWebhook('CAPTCHA ALERT', `**${this.username}** needs verification!\nCommand: \`${cmdName}\`\nSolve in Discord and re-enable from dashboard.`, 0xef4444);
|
|
1898
1981
|
return;
|
|
@@ -1943,16 +2026,6 @@ class AccountWorker {
|
|
|
1943
2026
|
return;
|
|
1944
2027
|
}
|
|
1945
2028
|
|
|
1946
|
-
// Died flag from crime/search handler (death detected in the command response)
|
|
1947
|
-
if (cmdResult.died) {
|
|
1948
|
-
this.log('error', `${cmdName} → DIED! Checking lifesaver count...`);
|
|
1949
|
-
// The DM will come separately with the actual death details
|
|
1950
|
-
// For now, be cautious — set a short cooldown and let DM listener handle the rest
|
|
1951
|
-
await this.setCooldown('crime', 300); // 5 min cooldown to check DMs
|
|
1952
|
-
await this.setCooldown('search', 300);
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
2029
|
// Premium-only command detection — disable permanently
|
|
1957
2030
|
if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
|
|
1958
2031
|
resultLower.includes('buy the ability to use this command') ||
|
|
@@ -1971,8 +2044,9 @@ class AccountWorker {
|
|
|
1971
2044
|
const timeMatch = result.match(/<t:(\d+):R>/);
|
|
1972
2045
|
let waitSec;
|
|
1973
2046
|
if (timeMatch) {
|
|
1974
|
-
|
|
1975
|
-
|
|
2047
|
+
// Discord <t:TS:R> format: :R = relative seconds from NOW (not Unix ms).
|
|
2048
|
+
// The captured number IS already the number of seconds to wait.
|
|
2049
|
+
waitSec = Math.max(60, parseInt(timeMatch[1]));
|
|
1976
2050
|
} else {
|
|
1977
2051
|
const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
|
|
1978
2052
|
waitSec = defaultWaits[cmdName] || 86400;
|
|
@@ -2152,13 +2226,13 @@ class AccountWorker {
|
|
|
2152
2226
|
|
|
2153
2227
|
// ── Command Map (shared across ticks, used to build the heap) ──
|
|
2154
2228
|
// Priority: higher = runs first when multiple commands are ready simultaneously.
|
|
2155
|
-
// 10 = time-gated (daily/
|
|
2229
|
+
// 10 = time-gated (daily/monthly — never miss),
|
|
2156
2230
|
// 8 = financial safety (deposit),
|
|
2157
2231
|
// 7 = gambling fast-cycle (2-3s CD — run MOST often),
|
|
2158
2232
|
// 6 = fast grinders (10s CD),
|
|
2159
2233
|
// 5 = medium grinders (20-40s CD),
|
|
2160
2234
|
// 4 = resource grinders (hunt/dig — need items),
|
|
2161
|
-
// 3 = interactive/long CD (adventure/stream/work
|
|
2235
|
+
// 3 = interactive/long CD (adventure/stream/work),
|
|
2162
2236
|
// 2 = utility (drops/use/tidy)
|
|
2163
2237
|
static COMMAND_MAP = [
|
|
2164
2238
|
// Gambling — 2-3s CD, highest frequency
|
|
@@ -2169,7 +2243,7 @@ class AccountWorker {
|
|
|
2169
2243
|
{ key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
|
|
2170
2244
|
// Fast grinders — 10s CD
|
|
2171
2245
|
{ key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
|
|
2172
|
-
|
|
2246
|
+
{ key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
|
|
2173
2247
|
{ key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
|
|
2174
2248
|
{ key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
|
|
2175
2249
|
// Medium grinders — 20-25s CD
|
|
@@ -2180,20 +2254,19 @@ class AccountWorker {
|
|
|
2180
2254
|
{ key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 5 },
|
|
2181
2255
|
// Slow grinders — 40s CD
|
|
2182
2256
|
{ key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
|
|
2183
|
-
{ key: 'cmd_crime', cmd: 'crime',
|
|
2184
|
-
{ key: 'cmd_tidy', cmd: 'tidy',
|
|
2257
|
+
{ key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
|
|
2258
|
+
{ key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
|
|
2185
2259
|
// Interactive — response-driven CD (handler sets nextCooldownSec)
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
2260
|
+
{ key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
|
|
2261
|
+
{ key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
|
|
2262
|
+
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
2190
2263
|
// Time-gated (run ASAP when available)
|
|
2191
|
-
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily',
|
|
2192
|
-
//
|
|
2193
|
-
{ key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly',
|
|
2264
|
+
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
|
|
2265
|
+
// monthly — premium only
|
|
2266
|
+
{ key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
|
|
2194
2267
|
// Financial safety
|
|
2195
|
-
{ key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit',
|
|
2196
|
-
{ key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops',
|
|
2268
|
+
{ key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
|
|
2269
|
+
{ key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
|
|
2197
2270
|
// Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
|
|
2198
2271
|
].map(Object.freeze);
|
|
2199
2272
|
|
|
@@ -2470,91 +2543,58 @@ class AccountWorker {
|
|
|
2470
2543
|
return;
|
|
2471
2544
|
}
|
|
2472
2545
|
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2546
|
+
// ── Scan entire queue for ready commands ─────────────────────
|
|
2547
|
+
// Previously we only peeked the top item, missing other commands that
|
|
2548
|
+
// were already ready while a slower command was at the top of the heap.
|
|
2549
|
+
// Now we drain the queue and separate ready vs. waiting commands.
|
|
2550
|
+
// All ready commands fire immediately — no priority, FIFO order.
|
|
2551
|
+
const readyItems = [];
|
|
2552
|
+
const waitingItems = [];
|
|
2553
|
+
while (this.commandQueue.size > 0) {
|
|
2554
|
+
const item = this.commandQueue.pop();
|
|
2555
|
+
if (!item) break;
|
|
2556
|
+
if (item.nextRunAt <= now) {
|
|
2557
|
+
readyItems.push(item);
|
|
2558
|
+
} else {
|
|
2559
|
+
waitingItems.push(item);
|
|
2560
|
+
}
|
|
2485
2561
|
}
|
|
2486
2562
|
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
item.nextRunAt = now + cd * 1000;
|
|
2491
|
-
if (this.commandQueue) this.commandQueue.push(item);
|
|
2492
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2493
|
-
return;
|
|
2563
|
+
// Re-insert waiting items back into the queue (they still have time to wait)
|
|
2564
|
+
for (const item of waitingItems) {
|
|
2565
|
+
this.commandQueue.push(item);
|
|
2494
2566
|
}
|
|
2495
2567
|
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
item.nextRunAt
|
|
2501
|
-
if (
|
|
2502
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2503
|
-
return;
|
|
2504
|
-
}
|
|
2505
|
-
if (redis) {
|
|
2506
|
-
try {
|
|
2507
|
-
const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
|
|
2508
|
-
if (done) {
|
|
2509
|
-
const ttlMap = { daily: 86400, weekly: 604800, monthly: 2592000, drops: 86400 };
|
|
2510
|
-
const ttl = ttlMap[item.cmd] || 86400;
|
|
2511
|
-
const expiry = now + ttl * 1000;
|
|
2512
|
-
this.doneToday.set(item.cmd, expiry);
|
|
2513
|
-
item.nextRunAt = expiry;
|
|
2514
|
-
if (this.commandQueue) this.commandQueue.push(item);
|
|
2515
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
|
-
} catch {}
|
|
2568
|
+
if (readyItems.length === 0) {
|
|
2569
|
+
// Nothing ready — wait until the soonest command becomes available.
|
|
2570
|
+
let minWaitMs = 5000; // cap at 5s to stay responsive
|
|
2571
|
+
for (const item of waitingItems) {
|
|
2572
|
+
const diff = item.nextRunAt - now;
|
|
2573
|
+
if (diff < minWaitMs) minWaitMs = diff;
|
|
2519
2574
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
// Smart gambling: skip gamble commands while loss-paused
|
|
2523
|
-
const GAMBLE_SET = new Set(['blackjack', 'cointoss', 'roulette', 'slots', 'snakeeyes']);
|
|
2524
|
-
if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > now) {
|
|
2525
|
-
item.nextRunAt = this._gamblePausedUntil;
|
|
2526
|
-
if (this.commandQueue) this.commandQueue.push(item);
|
|
2527
|
-
this.setStatus(`gamble paused (${Math.ceil((this._gamblePausedUntil - now) / 1000)}s)`);
|
|
2528
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2575
|
+
this.setStatus('cooldown...');
|
|
2576
|
+
this.tickTimeout = setTimeout(() => this.tick(), Math.max(100, Math.min(minWaitMs, 5000)));
|
|
2529
2577
|
return;
|
|
2530
2578
|
}
|
|
2531
|
-
if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > 0 && this._gamblePausedUntil <= now) {
|
|
2532
|
-
this._gamblePausedUntil = 0;
|
|
2533
|
-
this._gambleLossStreak = 0;
|
|
2534
|
-
this._gambleSessionLoss = 0;
|
|
2535
|
-
this.log('info', 'Gambling pause expired — resuming bets');
|
|
2536
|
-
}
|
|
2537
2579
|
|
|
2538
|
-
//
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2580
|
+
// FIFO — execute commands in the order they became ready.
|
|
2581
|
+
// Commands that aren't picked this tick go back into the queue for next tick.
|
|
2582
|
+
const item = readyItems[0];
|
|
2583
|
+
|
|
2584
|
+
// Any remaining ready items that we didn't execute go back immediately
|
|
2585
|
+
// so they run in the next tick without waiting for the slow top item.
|
|
2586
|
+
for (let i = 1; i < readyItems.length; i++) {
|
|
2587
|
+
this.commandQueue.push(readyItems[i]);
|
|
2545
2588
|
}
|
|
2546
2589
|
|
|
2547
|
-
this
|
|
2548
|
-
this.busy = true;
|
|
2590
|
+
// Anti-detection: per-account jitter + micro-pauses for this command
|
|
2549
2591
|
const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
|
|
2550
|
-
// Anti-detection: per-account jitter with varying patterns
|
|
2551
2592
|
const patternMod = this._activePattern;
|
|
2552
2593
|
const jitterBase = cd <= 5
|
|
2553
2594
|
? 0.3 + Math.random() * 0.7
|
|
2554
2595
|
: cd <= 20
|
|
2555
2596
|
? 0.5 + Math.random() * 1.5
|
|
2556
2597
|
: 1 + Math.random() * 2;
|
|
2557
|
-
// Add human-like micro-pauses (occasionally take longer, simulating distraction)
|
|
2558
2598
|
const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
|
|
2559
2599
|
const totalWait = cd + jitterBase + microPause;
|
|
2560
2600
|
|
|
@@ -2566,6 +2606,32 @@ class AccountWorker {
|
|
|
2566
2606
|
await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
|
|
2567
2607
|
}
|
|
2568
2608
|
|
|
2609
|
+
// TokenBucket rate limiter — prevent Discord 429s
|
|
2610
|
+
if (!this._rateLimiter.consume(1)) {
|
|
2611
|
+
const waitMs = this._rateLimiter.waitTime(1);
|
|
2612
|
+
this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
|
|
2613
|
+
if (this.commandQueue) this.commandQueue.push(item);
|
|
2614
|
+
this.tickTimeout = setTimeout(() => this.tick(), waitMs);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// Startup delay: don't send commands for the first 30s after grindLoop() starts.
|
|
2619
|
+
// This prevents flooding Dank Memer during the Phase 2 inventory check which
|
|
2620
|
+
// sends pls inv for all accounts simultaneously after login.
|
|
2621
|
+
if (this._startupDelayUntil && now < this._startupDelayUntil) {
|
|
2622
|
+
const waitMs = this._startupDelayUntil - now;
|
|
2623
|
+
this.setStatus('warming up...');
|
|
2624
|
+
item.nextRunAt = now + waitMs + 1000;
|
|
2625
|
+
if (this.commandQueue) this.commandQueue.push(item);
|
|
2626
|
+
this.tickTimeout = setTimeout(() => this.tick(), waitMs + 1000);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
this.busy = true;
|
|
2631
|
+
|
|
2632
|
+
// ── Run command (with interactive retry) ───────────────────
|
|
2633
|
+
// Commands run ONE BY ONE — sequential execution, no concurrency within this account.
|
|
2634
|
+
// Each runCommand() call waits for Dank Memer's Discord response before returning.
|
|
2569
2635
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
2570
2636
|
this.setStatus(formatCommandName(item.cmd));
|
|
2571
2637
|
|
|
@@ -2579,15 +2645,40 @@ class AccountWorker {
|
|
|
2579
2645
|
next_run_at: nextItemRun?.nextRunAt || null,
|
|
2580
2646
|
});
|
|
2581
2647
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2648
|
+
// Interactive commands (button-click): retry up to 3 times on failure.
|
|
2649
|
+
// Non-interactive commands run once.
|
|
2650
|
+
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
|
|
2651
|
+
const isInteractive = INTERACTIVE_CMDS.has(item.cmd);
|
|
2652
|
+
const maxAttempts = isInteractive ? 3 : 1;
|
|
2653
|
+
let attempt = 0;
|
|
2654
|
+
let lastError = null;
|
|
2655
|
+
let earned = 0;
|
|
2656
|
+
while (attempt < maxAttempts) {
|
|
2657
|
+
attempt++;
|
|
2658
|
+
try {
|
|
2659
|
+
const beforeCoins = this.stats.coins;
|
|
2660
|
+
await this.runCommand(item.cmd, prefix);
|
|
2661
|
+
earned = this.stats.coins - beforeCoins;
|
|
2662
|
+
lastError = null;
|
|
2663
|
+
this.lastCommandRun = Date.now();
|
|
2664
|
+
break; // success
|
|
2665
|
+
} catch (err) {
|
|
2666
|
+
lastError = err;
|
|
2667
|
+
this.log('warn', `${item.cmd} attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
|
|
2668
|
+
if (attempt < maxAttempts) {
|
|
2669
|
+
await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (lastError) {
|
|
2674
|
+
this.log('error', `${item.cmd} failed after ${maxAttempts} attempts — skipping`);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
this._cmdRate.increment();
|
|
2585
2678
|
|
|
2586
|
-
// Grace period for interactive
|
|
2587
|
-
//
|
|
2588
|
-
|
|
2589
|
-
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
|
|
2590
|
-
if (INTERACTIVE_CMDS.has(item.cmd)) {
|
|
2679
|
+
// Grace period for interactive commands — Dank Memer needs time to process
|
|
2680
|
+
// the interaction before accepting the next command.
|
|
2681
|
+
if (isInteractive) {
|
|
2591
2682
|
await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
|
|
2592
2683
|
}
|
|
2593
2684
|
|
|
@@ -2605,8 +2696,6 @@ class AccountWorker {
|
|
|
2605
2696
|
this.failStreak = 0;
|
|
2606
2697
|
}
|
|
2607
2698
|
|
|
2608
|
-
this.lastCommandRun = Date.now();
|
|
2609
|
-
|
|
2610
2699
|
// Exponential backoff: if too many consecutive failures, slow down
|
|
2611
2700
|
const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
|
|
2612
2701
|
// Minimum 5s cooldown for failed commands to prevent rapid-fire retries
|
|
@@ -2678,6 +2767,11 @@ class AccountWorker {
|
|
|
2678
2767
|
this.failStreak = 0;
|
|
2679
2768
|
this.cycleCount = 0;
|
|
2680
2769
|
this.lastCommandRun = 0;
|
|
2770
|
+
// Delay first command by 30s to avoid competing with Phase 2 inventory check
|
|
2771
|
+
// which sends pls inv for all accounts simultaneously after login.
|
|
2772
|
+
// Without this, the grind loop floods Dank Memer with commands during the
|
|
2773
|
+
// login surge, triggering rate-limits that cause Phase 2 inventory to fail.
|
|
2774
|
+
this._startupDelayUntil = Date.now() + 30000;
|
|
2681
2775
|
await this._loadLearnedCooldowns();
|
|
2682
2776
|
this.commandQueue = await this.buildCommandQueue();
|
|
2683
2777
|
this.lastHealthCheck = Date.now();
|
|
@@ -2781,11 +2875,28 @@ class AccountWorker {
|
|
|
2781
2875
|
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2782
2876
|
body: JSON.stringify({ action_id: action.id }),
|
|
2783
2877
|
});
|
|
2784
|
-
} catch {
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
console.error(`[${this.username}] delete pending action error:`, err?.message || err);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
if (action.action === 'check_profile' && !this.busy) {
|
|
2883
|
+
this.log('info', 'Dashboard requested profile check');
|
|
2884
|
+
await this.checkProfile().catch(() => {});
|
|
2885
|
+
try {
|
|
2886
|
+
await fetch(`${API_URL}/api/grinder/actions`, {
|
|
2887
|
+
method: 'DELETE',
|
|
2888
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2889
|
+
body: JSON.stringify({ action_id: action.id }),
|
|
2890
|
+
});
|
|
2891
|
+
} catch (err) {
|
|
2892
|
+
console.error(`[${this.username}] delete pending action error:`, err?.message || err);
|
|
2893
|
+
}
|
|
2785
2894
|
}
|
|
2786
2895
|
}
|
|
2787
2896
|
}
|
|
2788
|
-
} catch {
|
|
2897
|
+
} catch (err) {
|
|
2898
|
+
console.error(`[${this.username}] refreshConfig error:`, err?.message || err);
|
|
2899
|
+
}
|
|
2789
2900
|
}
|
|
2790
2901
|
|
|
2791
2902
|
async start() {
|
|
@@ -2835,9 +2946,8 @@ class AccountWorker {
|
|
|
2835
2946
|
{ key: 'cmd_fish', l: 'fish' }, { key: 'cmd_beg', l: 'beg' },
|
|
2836
2947
|
{ key: 'cmd_search', l: 'search' }, { key: 'cmd_hl', l: 'hl' },
|
|
2837
2948
|
{ key: 'cmd_crime', l: 'crime' }, { key: 'cmd_pm', l: 'pm' },
|
|
2838
|
-
{ key: 'cmd_daily', l: 'daily' }, { key: '
|
|
2839
|
-
{ key: '
|
|
2840
|
-
{ key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
|
|
2949
|
+
{ key: 'cmd_daily', l: 'daily' }, { key: 'cmd_monthly', l: 'monthly' },
|
|
2950
|
+
{ key: 'cmd_work', l: 'work' }, { key: 'cmd_stream', l: 'stream' },
|
|
2841
2951
|
{ key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
|
|
2842
2952
|
{ key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
|
|
2843
2953
|
{ key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
|
|
@@ -3062,19 +3172,38 @@ async function start(apiKey, apiUrl) {
|
|
|
3062
3172
|
// Init rawLogger Redis (uses same URL — logs all raw gateway data)
|
|
3063
3173
|
if (REDIS_URL) {
|
|
3064
3174
|
rawLogger.init(REDIS_URL).catch(() => {});
|
|
3065
|
-
//
|
|
3175
|
+
// Live DM listener: detect deaths and level-ups in real-time across all accounts
|
|
3066
3176
|
rawLogger.onDmEvent((event, raw) => {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3177
|
+
const dmChannelId = raw.channel_id;
|
|
3178
|
+
|
|
3179
|
+
// Find which worker owns this DM channel
|
|
3180
|
+
const worker = workers.find(w => w._dmChannelId === dmChannelId);
|
|
3181
|
+
if (!worker) return;
|
|
3182
|
+
|
|
3183
|
+
if (event.type === 'death') {
|
|
3184
|
+
const lsLeft = event.lifesaversLeft;
|
|
3185
|
+
|
|
3186
|
+
if (lsLeft === 0) {
|
|
3187
|
+
// 0 lifesavers — disable crime/search immediately
|
|
3188
|
+
worker.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
|
|
3189
|
+
worker.setCooldown?.('crime', 86400);
|
|
3190
|
+
worker.setCooldown?.('search', 86400);
|
|
3191
|
+
worker._lifesavers = 0;
|
|
3192
|
+
sendWebhook?.('DEATH ALERT (DM)', `**${worker.username}** died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
3193
|
+
} else if (lsLeft > 0) {
|
|
3194
|
+
// Lifesaver(s) used — update count in real-time
|
|
3195
|
+
worker._lifesavers = lsLeft;
|
|
3196
|
+
worker.log?.('warn', `Lifesaver used! ${lsLeft} remaining.`);
|
|
3197
|
+
if (lsLeft <= 2) {
|
|
3198
|
+
sendWebhook?.('LOW LIFESAVERS', `**${worker.username}** died! Only **${lsLeft}** lifesaver(s) left!`, 0xfbbf24);
|
|
3075
3199
|
}
|
|
3076
3200
|
}
|
|
3077
|
-
|
|
3201
|
+
} else if (event.type === 'levelup') {
|
|
3202
|
+
// Level up — update in-memory level
|
|
3203
|
+
if (event.to > 0) {
|
|
3204
|
+
worker._level = event.to;
|
|
3205
|
+
worker.log?.('info', `Level up! Now level ${event.to}.`);
|
|
3206
|
+
}
|
|
3078
3207
|
}
|
|
3079
3208
|
});
|
|
3080
3209
|
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
|
|
@@ -3282,7 +3411,7 @@ async function start(apiKey, apiUrl) {
|
|
|
3282
3411
|
const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
|
|
3283
3412
|
const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
|
|
3284
3413
|
let invRes;
|
|
3285
|
-
try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts:
|
|
3414
|
+
try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 5, silent: true }); }
|
|
3286
3415
|
catch { invRes = { ok: false }; }
|
|
3287
3416
|
invPending--;
|
|
3288
3417
|
const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
|
package/lib/rawLogger.js
CHANGED
|
@@ -151,24 +151,26 @@ function detectCommand(d) {
|
|
|
151
151
|
// Non-gambling CV2
|
|
152
152
|
if (cv2Text.includes('fishing') || cv2Text.includes('fisherfolk')) return 'fish';
|
|
153
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('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle') || cv2Text.includes('your aim was so bad') || cv2Text.includes('animals laughed') || cv2Text.includes('animals attacked') || cv2Text.includes('barely escaped') || cv2Text.includes('fell asleep in a tree') || cv2Text.includes('caught nothing') || cv2Text.includes('brought back literally nothing') || cv2Text.includes('rifle broke')) return 'hunt';
|
|
156
|
-
if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig') || cv2Text.includes('dug in the dirt') || cv2Text.includes('brought back') && (cv2Text.includes('ant') || cv2Text.includes('worm') || cv2Text.includes('stickbug') || cv2Text.includes('ladybug'))) return 'dig';
|
|
154
|
+
if (cv2Text.includes('begging') || cv2Text.includes('imagine begging') || cv2Text.includes('wumpus gives you') || (cv2Text.includes('you received') && !cv2Text.includes('search') && !cv2Text.includes('hunt') && !cv2Text.includes('dig'))) return 'beg';
|
|
155
|
+
if (cv2Text.includes('hunting') || cv2Text.includes('went hunting') || cv2Text.includes('hunting rifle') || cv2Text.includes('your aim was so bad') || cv2Text.includes('animals laughed') || cv2Text.includes('animals attacked') || cv2Text.includes('barely escaped') || cv2Text.includes('fell asleep in a tree') || cv2Text.includes('caught nothing') || cv2Text.includes('brought back literally nothing') || cv2Text.includes('rifle broke') || (cv2Text.includes('brought back') && (cv2Text.includes('deer') || cv2Text.includes('wolf') || cv2Text.includes('bear') || cv2Text.includes('boar') || cv2Text.includes('lion') || cv2Text.includes('rabbit') || cv2Text.includes('squirrel') || cv2Text.includes('moose') || cv2Text.includes('bird') || cv2Text.includes('elk') || cv2Text.includes('hunting') || cv2Text.includes('capybara')))) return 'hunt';
|
|
156
|
+
if (cv2Text.includes('digging') || cv2Text.includes('found nothing while') || cv2Text.includes('you dig') || cv2Text.includes('dug in the dirt') || (cv2Text.includes('brought back') && (cv2Text.includes('ant') || cv2Text.includes('worm') || cv2Text.includes('stickbug') || cv2Text.includes('ladybug')))) return 'dig';
|
|
157
157
|
if (cv2Text.includes('great work') || cv2Text.includes('for your shift') || cv2Text.includes('working as') || cv2Text.includes('work shift') || cv2Text.includes('what color was') || cv2Text.includes('remember words order') || cv2Text.includes('remember the colors') || cv2Text.includes('remember the emojis') || cv2Text.includes('what word was repeated') || cv2Text.includes('unscramble') || cv2Text.includes('remember the number') || cv2Text.includes('click the buttons in correct order') || cv2Text.includes('babysitter') || cv2Text.includes('click the matching')) return 'work';
|
|
158
|
+
// Quest completions (before generic daily/weekly)
|
|
159
|
+
if (cv2Text.includes('locations') && (cv2Text.includes('inventory locations') || cv2Text.includes('found locations') || cv2Text.includes('completed your') || cv2Text.includes('locations remaining'))) return 'search';
|
|
158
160
|
if (cv2Text.includes('weekly')) return 'weekly';
|
|
159
161
|
if (cv2Text.includes('daily')) return 'daily';
|
|
160
|
-
if
|
|
162
|
+
// Only match inventory if it looks like an actual inventory display (header format or sellable indicator)
|
|
163
|
+
if (cv2Text.includes('inventory') && (cv2Text.includes('###') || cv2Text.includes('sellable') || cv2Text.includes('<:reply:'))) return 'inventory';
|
|
161
164
|
if (cv2Text.includes('profile') || cv2Text.includes('level:')) return 'profile';
|
|
162
165
|
if (cv2Text.includes('balances') && cv2Text.includes('global rank')) return 'balance';
|
|
163
166
|
|
|
164
167
|
// Check content text (plain message content)
|
|
165
168
|
const contentText = (d.content || '').toLowerCase();
|
|
166
169
|
if (contentText.includes('balances') && contentText.includes('global rank')) return 'balance';
|
|
167
|
-
if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed')) return 'hunt';
|
|
168
|
-
if (contentText.includes('imagine going into the woods')) return 'hunt';
|
|
170
|
+
if (contentText.includes('your aim was so bad') || contentText.includes('animals laughed') || contentText.includes('imagine going into the woods')) return 'hunt';
|
|
169
171
|
if (contentText.includes('you ran an ad for') && contentText.includes('received')) return 'stream';
|
|
170
172
|
if (contentText.includes('you can\'t interact with your stream')) return 'stream';
|
|
171
|
-
if (contentText.includes('you dug in the dirt') || contentText.includes('found nothing while digging')) return 'dig';
|
|
173
|
+
if (contentText.includes('you dug in the dirt') || (contentText.includes('found nothing while digging') && (contentText.includes('dug') || contentText.includes('dirt')))) return 'dig';
|
|
172
174
|
|
|
173
175
|
// Check embed text
|
|
174
176
|
const embedText = extractEmbedText(d.embeds).toLowerCase();
|
|
@@ -197,8 +199,8 @@ function detectCommand(d) {
|
|
|
197
199
|
return 'search';
|
|
198
200
|
}
|
|
199
201
|
// Hunt / dig
|
|
200
|
-
if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('your aim was so bad') || embedText.includes('animals laughed') || embedText.includes('animals attacked') || embedText.includes('barely escaped') || embedText.includes('fell asleep in a tree') || embedText.includes('caught nothing') || embedText.includes('brought back literally nothing') || embedText.includes('rifle broke') || embedText.includes('imagine going into the woods')) return 'hunt';
|
|
201
|
-
if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('dug in the dirt') || embedText.includes('found nothing while') || embedText.includes('what are the odds lol') || embedText.includes('brought back') && (embedText.includes('ant') || embedText.includes('worm') || embedText.includes('stickbug') || embedText.includes('ladybug'))) return 'dig';
|
|
202
|
+
if (embedText.includes('hunting') || embedText.includes('came back with') || embedText.includes('hunting rifle') || embedText.includes('dragon\'s fireball') || embedText.includes('dodge the') || embedText.includes('went hunting') || embedText.includes('your aim was so bad') || embedText.includes('animals laughed') || embedText.includes('animals attacked') || embedText.includes('barely escaped') || embedText.includes('fell asleep in a tree') || embedText.includes('caught nothing') || embedText.includes('brought back literally nothing') || embedText.includes('rifle broke') || embedText.includes('imagine going into the woods') || (embedText.includes('brought back') && (embedText.includes('deer') || embedText.includes('wolf') || embedText.includes('bear') || embedText.includes('boar') || embedText.includes('lion') || embedText.includes('rabbit') || embedText.includes('squirrel') || embedText.includes('moose') || embedText.includes('bird') || embedText.includes('elk') || embedText.includes('capybara')))) return 'hunt';
|
|
203
|
+
if (embedText.includes('digging') || embedText.includes('you dig') || embedText.includes('dug in the dirt') || embedText.includes('found nothing while') || embedText.includes('what are the odds lol') || (embedText.includes('brought back') && (embedText.includes('ant') || embedText.includes('worm') || embedText.includes('stickbug') || embedText.includes('ladybug')))) return 'dig';
|
|
202
204
|
// Work — match both minigame prompt AND completion
|
|
203
205
|
if (embedText.includes('work') && (embedText.includes('shift') || embedText.includes('mini-game') || embedText.includes('color') || embedText.includes('what color') || embedText.includes('babysitter') || embedText.includes('great work') || embedText.includes('for your shift'))) return 'work';
|
|
204
206
|
if (embedText.includes('you were given') && embedText.includes('shift')) return 'work';
|
|
@@ -207,7 +209,7 @@ function detectCommand(d) {
|
|
|
207
209
|
// Postmemes
|
|
208
210
|
if (embedText.includes('pick a meme') || embedText.includes('meme posting')) return 'postmemes';
|
|
209
211
|
// Stream
|
|
210
|
-
if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream') || embedText.includes('you ran an ad for') || embedText.includes('you received') && embedText.includes('from your sponsors') || embedText.includes('### chat') && embedText.includes('hasanbabi')) return 'stream';
|
|
212
|
+
if (embedText.includes('stream manager') || embedText.includes('go live') || embedText.includes('what game do you want to stream') || embedText.includes('you ran an ad for') || (embedText.includes('you received') && embedText.includes('from your sponsors')) || (embedText.includes('### chat') && embedText.includes('hasanbabi'))) return 'stream';
|
|
211
213
|
if (embedText.includes('you can\'t interact with your stream') || embedText.includes('stream can last')) return 'stream';
|
|
212
214
|
// Deposit
|
|
213
215
|
if (embedText.includes('deposited') && embedText.includes('bank balance')) return 'deposit';
|
|
@@ -228,7 +230,7 @@ function detectCommand(d) {
|
|
|
228
230
|
// Farm
|
|
229
231
|
if (embedText.includes('farm') && (embedText.includes('harvest') || embedText.includes('plant') || embedText.includes('hoe') || embedText.includes('water'))) return 'farm';
|
|
230
232
|
// Beg
|
|
231
|
-
if (embedText.includes('begging')) return 'beg';
|
|
233
|
+
if (embedText.includes('begging') || embedText.includes('wumpus gives you') || embedText.includes('life saver') || embedText.includes('lifesaver')) return 'beg';
|
|
232
234
|
// Daily/weekly quest
|
|
233
235
|
if (embedText.includes('daily quest')) return 'daily';
|
|
234
236
|
// Fish
|
package/lib/structures.js
CHANGED
|
@@ -443,6 +443,25 @@ class MinHeap {
|
|
|
443
443
|
|
|
444
444
|
peek() { return this.heap[0]; }
|
|
445
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Remove a specific node from the heap by reference.
|
|
448
|
+
* Used by CommandScheduler.unschedule() to cancel a pending scheduled command.
|
|
449
|
+
* O(n) — linear scan since we store node references in a Map.
|
|
450
|
+
*/
|
|
451
|
+
remove(node) {
|
|
452
|
+
const idx = this.heap.indexOf(node);
|
|
453
|
+
if (idx === -1) return false;
|
|
454
|
+
const last = this.heap.pop();
|
|
455
|
+
if (idx < this.heap.length) {
|
|
456
|
+
this.heap[idx] = last;
|
|
457
|
+
// Bubble up or sink down depending on where the removed item was
|
|
458
|
+
if (this._bubbleUp(idx) === idx) {
|
|
459
|
+
this._sinkDown(idx);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
446
465
|
_bubbleUp(i) {
|
|
447
466
|
while (i > 0) {
|
|
448
467
|
const parent = (i - 1) >>> 1;
|
|
@@ -680,31 +699,6 @@ class AsyncBatchQueue {
|
|
|
680
699
|
destroy() { if (this._timer) clearTimeout(this._timer); this.queue.length = 0; }
|
|
681
700
|
}
|
|
682
701
|
|
|
683
|
-
// ═══════════════════════════════════════════════════════════════
|
|
684
|
-
// JitterBackoff – Decorrelated jitter (AWS-style), O(1)
|
|
685
|
-
// Standard exponential backoff causes "thundering herd" when 10K
|
|
686
|
-
// accounts retry simultaneously. Decorrelated jitter spreads them:
|
|
687
|
-
// sleep = min(cap, random_between(base, sleep_prev * 3))
|
|
688
|
-
// This is the recommended strategy from AWS Architecture Blog.
|
|
689
|
-
// ═══════════════════════════════════════════════════════════════
|
|
690
|
-
class JitterBackoff {
|
|
691
|
-
constructor(baseMs = 1000, capMs = 30000) {
|
|
692
|
-
this.baseMs = baseMs;
|
|
693
|
-
this.capMs = capMs;
|
|
694
|
-
this._sleep = baseMs;
|
|
695
|
-
this.attempt = 0;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
next() {
|
|
699
|
-
this._sleep = Math.min(this.capMs, this.baseMs + Math.random() * (this._sleep * 3 - this.baseMs));
|
|
700
|
-
this.attempt++;
|
|
701
|
-
return this._sleep;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
reset() { this._sleep = this.baseMs; this.attempt = 0; }
|
|
705
|
-
get current() { return this._sleep; }
|
|
706
|
-
}
|
|
707
|
-
|
|
708
702
|
module.exports = {
|
|
709
703
|
BloomFilter,
|
|
710
704
|
LRUCache,
|
|
@@ -721,5 +715,4 @@ module.exports = {
|
|
|
721
715
|
ObjectPool,
|
|
722
716
|
TimerWheel,
|
|
723
717
|
AsyncBatchQueue,
|
|
724
|
-
JitterBackoff,
|
|
725
718
|
};
|
package/package.json
CHANGED
package/lib/commands/scratch.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scratch command handler.
|
|
3
|
-
* Send "pls scratch", click through scratch card buttons.
|
|
4
|
-
* Requires level 25 — checks profile level before running.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
9
|
-
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
10
|
-
} = require('./utils');
|
|
11
|
-
const { meetsLevelRequirement } = require('./profile');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {object} opts
|
|
15
|
-
* @param {object} opts.channel
|
|
16
|
-
* @param {function} opts.waitForDankMemer
|
|
17
|
-
* @param {string} [opts.accountId]
|
|
18
|
-
* @param {object} [opts.redis]
|
|
19
|
-
* @returns {Promise<{result: string, coins: number}>}
|
|
20
|
-
*/
|
|
21
|
-
async function runScratch({ channel, waitForDankMemer, accountId, redis }) {
|
|
22
|
-
// Check level 25 requirement before wasting a command
|
|
23
|
-
const canRun = await meetsLevelRequirement({ channel, waitForDankMemer, accountId, redis }, 25);
|
|
24
|
-
if (!canRun) {
|
|
25
|
-
LOG.warn(`[scratch] Skipped — need level 25`);
|
|
26
|
-
return { result: 'skipped (need level 25)', coins: 0, skipReason: 'level' };
|
|
27
|
-
}
|
|
28
|
-
LOG.cmd(`${c.white}${c.bold}pls scratch${c.reset}`);
|
|
29
|
-
|
|
30
|
-
await channel.send('pls scratch');
|
|
31
|
-
const response = await waitForDankMemer(10000);
|
|
32
|
-
|
|
33
|
-
if (!response) {
|
|
34
|
-
LOG.warn('[scratch] No response');
|
|
35
|
-
return { result: 'no response', coins: 0 };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (isHoldTight(response)) {
|
|
39
|
-
const reason = getHoldTightReason(response);
|
|
40
|
-
LOG.warn(`[scratch] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
41
|
-
await sleep(30000);
|
|
42
|
-
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
logMsg(response, 'scratch');
|
|
46
|
-
const buttons = getAllButtons(response);
|
|
47
|
-
|
|
48
|
-
if (buttons.length === 0) {
|
|
49
|
-
const text = getFullText(response);
|
|
50
|
-
const coins = parseCoins(text);
|
|
51
|
-
if (coins > 0) {
|
|
52
|
-
LOG.coin(`[scratch] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
53
|
-
return { result: `scratch → +⏣ ${coins.toLocaleString()}`, coins };
|
|
54
|
-
}
|
|
55
|
-
return { result: text.substring(0, 60) || 'done', coins: 0 };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Click through all scratch card cells
|
|
59
|
-
let lastResponse = response;
|
|
60
|
-
let clickCount = 0;
|
|
61
|
-
for (let i = 0; i < Math.min(buttons.length, 9); i++) {
|
|
62
|
-
const btn = buttons[i];
|
|
63
|
-
if (btn && !btn.disabled) {
|
|
64
|
-
await humanDelay(300, 700);
|
|
65
|
-
try {
|
|
66
|
-
const followUp = await safeClickButton(lastResponse, btn);
|
|
67
|
-
if (followUp) { lastResponse = followUp; clickCount++; }
|
|
68
|
-
} catch { break; }
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const finalText = getFullText(lastResponse);
|
|
73
|
-
const coins = parseCoins(finalText);
|
|
74
|
-
|
|
75
|
-
if (coins > 0) {
|
|
76
|
-
LOG.coin(`[scratch] ${clickCount} clicks → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
77
|
-
return { result: `scratch (${clickCount} clicks) → +⏣ ${coins.toLocaleString()}`, coins };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { result: `scratch done (${clickCount} clicks)`, coins: 0 };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = { runScratch };
|