dankgrinder 6.39.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/fishVision.js +27 -1
- package/lib/commands/index.js +2 -3
- package/lib/commands/profile.js +200 -52
- package/lib/commands/utils.js +4 -2
- package/lib/grinder.js +223 -120
- 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,
|
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;
|
|
@@ -1971,8 +2054,9 @@ class AccountWorker {
|
|
|
1971
2054
|
const timeMatch = result.match(/<t:(\d+):R>/);
|
|
1972
2055
|
let waitSec;
|
|
1973
2056
|
if (timeMatch) {
|
|
1974
|
-
|
|
1975
|
-
|
|
2057
|
+
// Discord <t:TS:R> format: :R = relative seconds from NOW (not Unix ms).
|
|
2058
|
+
// The captured number IS already the number of seconds to wait.
|
|
2059
|
+
waitSec = Math.max(60, parseInt(timeMatch[1]));
|
|
1976
2060
|
} else {
|
|
1977
2061
|
const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
|
|
1978
2062
|
waitSec = defaultWaits[cmdName] || 86400;
|
|
@@ -2152,13 +2236,13 @@ class AccountWorker {
|
|
|
2152
2236
|
|
|
2153
2237
|
// ── Command Map (shared across ticks, used to build the heap) ──
|
|
2154
2238
|
// Priority: higher = runs first when multiple commands are ready simultaneously.
|
|
2155
|
-
// 10 = time-gated (daily/
|
|
2239
|
+
// 10 = time-gated (daily/monthly — never miss),
|
|
2156
2240
|
// 8 = financial safety (deposit),
|
|
2157
2241
|
// 7 = gambling fast-cycle (2-3s CD — run MOST often),
|
|
2158
2242
|
// 6 = fast grinders (10s CD),
|
|
2159
2243
|
// 5 = medium grinders (20-40s CD),
|
|
2160
2244
|
// 4 = resource grinders (hunt/dig — need items),
|
|
2161
|
-
// 3 = interactive/long CD (adventure/stream/work
|
|
2245
|
+
// 3 = interactive/long CD (adventure/stream/work),
|
|
2162
2246
|
// 2 = utility (drops/use/tidy)
|
|
2163
2247
|
static COMMAND_MAP = [
|
|
2164
2248
|
// Gambling — 2-3s CD, highest frequency
|
|
@@ -2169,7 +2253,7 @@ class AccountWorker {
|
|
|
2169
2253
|
{ key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
|
|
2170
2254
|
// Fast grinders — 10s CD
|
|
2171
2255
|
{ key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
|
|
2172
|
-
|
|
2256
|
+
{ key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
|
|
2173
2257
|
{ key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
|
|
2174
2258
|
{ key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
|
|
2175
2259
|
// Medium grinders — 20-25s CD
|
|
@@ -2180,20 +2264,19 @@ class AccountWorker {
|
|
|
2180
2264
|
{ key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 5 },
|
|
2181
2265
|
// Slow grinders — 40s CD
|
|
2182
2266
|
{ key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
|
|
2183
|
-
{ key: 'cmd_crime', cmd: 'crime',
|
|
2184
|
-
{ key: 'cmd_tidy', cmd: 'tidy',
|
|
2267
|
+
{ key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
|
|
2268
|
+
{ key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
|
|
2185
2269
|
// Interactive — response-driven CD (handler sets nextCooldownSec)
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
2270
|
+
{ key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
|
|
2271
|
+
{ key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
|
|
2272
|
+
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
2190
2273
|
// 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',
|
|
2274
|
+
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
|
|
2275
|
+
// monthly — premium only
|
|
2276
|
+
{ key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
|
|
2194
2277
|
// Financial safety
|
|
2195
|
-
{ key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit',
|
|
2196
|
-
{ key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops',
|
|
2278
|
+
{ key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
|
|
2279
|
+
{ key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
|
|
2197
2280
|
// Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
|
|
2198
2281
|
].map(Object.freeze);
|
|
2199
2282
|
|
|
@@ -2470,91 +2553,58 @@ class AccountWorker {
|
|
|
2470
2553
|
return;
|
|
2471
2554
|
}
|
|
2472
2555
|
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2556
|
+
// ── Scan entire queue for ready commands ─────────────────────
|
|
2557
|
+
// Previously we only peeked the top item, missing other commands that
|
|
2558
|
+
// were already ready while a slower command was at the top of the heap.
|
|
2559
|
+
// Now we drain the queue and separate ready vs. waiting commands.
|
|
2560
|
+
// All ready commands fire immediately — no priority, FIFO order.
|
|
2561
|
+
const readyItems = [];
|
|
2562
|
+
const waitingItems = [];
|
|
2563
|
+
while (this.commandQueue.size > 0) {
|
|
2564
|
+
const item = this.commandQueue.pop();
|
|
2565
|
+
if (!item) break;
|
|
2566
|
+
if (item.nextRunAt <= now) {
|
|
2567
|
+
readyItems.push(item);
|
|
2568
|
+
} else {
|
|
2569
|
+
waitingItems.push(item);
|
|
2570
|
+
}
|
|
2485
2571
|
}
|
|
2486
2572
|
|
|
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;
|
|
2573
|
+
// Re-insert waiting items back into the queue (they still have time to wait)
|
|
2574
|
+
for (const item of waitingItems) {
|
|
2575
|
+
this.commandQueue.push(item);
|
|
2494
2576
|
}
|
|
2495
2577
|
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
item.nextRunAt
|
|
2501
|
-
if (
|
|
2502
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2503
|
-
return;
|
|
2578
|
+
if (readyItems.length === 0) {
|
|
2579
|
+
// Nothing ready — wait until the soonest command becomes available.
|
|
2580
|
+
let minWaitMs = 5000; // cap at 5s to stay responsive
|
|
2581
|
+
for (const item of waitingItems) {
|
|
2582
|
+
const diff = item.nextRunAt - now;
|
|
2583
|
+
if (diff < minWaitMs) minWaitMs = diff;
|
|
2504
2584
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
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 {}
|
|
2519
|
-
}
|
|
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);
|
|
2585
|
+
this.setStatus('cooldown...');
|
|
2586
|
+
this.tickTimeout = setTimeout(() => this.tick(), Math.max(100, Math.min(minWaitMs, 5000)));
|
|
2529
2587
|
return;
|
|
2530
2588
|
}
|
|
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
2589
|
|
|
2538
|
-
//
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2590
|
+
// FIFO — execute commands in the order they became ready.
|
|
2591
|
+
// Commands that aren't picked this tick go back into the queue for next tick.
|
|
2592
|
+
const item = readyItems[0];
|
|
2593
|
+
|
|
2594
|
+
// Any remaining ready items that we didn't execute go back immediately
|
|
2595
|
+
// so they run in the next tick without waiting for the slow top item.
|
|
2596
|
+
for (let i = 1; i < readyItems.length; i++) {
|
|
2597
|
+
this.commandQueue.push(readyItems[i]);
|
|
2545
2598
|
}
|
|
2546
2599
|
|
|
2547
|
-
this
|
|
2548
|
-
this.busy = true;
|
|
2600
|
+
// Anti-detection: per-account jitter + micro-pauses for this command
|
|
2549
2601
|
const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
|
|
2550
|
-
// Anti-detection: per-account jitter with varying patterns
|
|
2551
2602
|
const patternMod = this._activePattern;
|
|
2552
2603
|
const jitterBase = cd <= 5
|
|
2553
2604
|
? 0.3 + Math.random() * 0.7
|
|
2554
2605
|
: cd <= 20
|
|
2555
2606
|
? 0.5 + Math.random() * 1.5
|
|
2556
2607
|
: 1 + Math.random() * 2;
|
|
2557
|
-
// Add human-like micro-pauses (occasionally take longer, simulating distraction)
|
|
2558
2608
|
const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
|
|
2559
2609
|
const totalWait = cd + jitterBase + microPause;
|
|
2560
2610
|
|
|
@@ -2566,6 +2616,20 @@ class AccountWorker {
|
|
|
2566
2616
|
await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
|
|
2567
2617
|
}
|
|
2568
2618
|
|
|
2619
|
+
// TokenBucket rate limiter — prevent Discord 429s
|
|
2620
|
+
if (!this._rateLimiter.consume(1)) {
|
|
2621
|
+
const waitMs = this._rateLimiter.waitTime(1);
|
|
2622
|
+
this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
|
|
2623
|
+
if (this.commandQueue) this.commandQueue.push(item);
|
|
2624
|
+
this.tickTimeout = setTimeout(() => this.tick(), waitMs);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
this.busy = true;
|
|
2629
|
+
|
|
2630
|
+
// ── Run command (with interactive retry) ───────────────────
|
|
2631
|
+
// Commands run ONE BY ONE — sequential execution, no concurrency within this account.
|
|
2632
|
+
// Each runCommand() call waits for Dank Memer's Discord response before returning.
|
|
2569
2633
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
2570
2634
|
this.setStatus(formatCommandName(item.cmd));
|
|
2571
2635
|
|
|
@@ -2579,15 +2643,40 @@ class AccountWorker {
|
|
|
2579
2643
|
next_run_at: nextItemRun?.nextRunAt || null,
|
|
2580
2644
|
});
|
|
2581
2645
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2646
|
+
// Interactive commands (button-click): retry up to 3 times on failure.
|
|
2647
|
+
// Non-interactive commands run once.
|
|
2648
|
+
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
|
|
2649
|
+
const isInteractive = INTERACTIVE_CMDS.has(item.cmd);
|
|
2650
|
+
const maxAttempts = isInteractive ? 3 : 1;
|
|
2651
|
+
let attempt = 0;
|
|
2652
|
+
let lastError = null;
|
|
2653
|
+
let earned = 0;
|
|
2654
|
+
while (attempt < maxAttempts) {
|
|
2655
|
+
attempt++;
|
|
2656
|
+
try {
|
|
2657
|
+
const beforeCoins = this.stats.coins;
|
|
2658
|
+
await this.runCommand(item.cmd, prefix);
|
|
2659
|
+
earned = this.stats.coins - beforeCoins;
|
|
2660
|
+
lastError = null;
|
|
2661
|
+
this.lastCommandRun = Date.now();
|
|
2662
|
+
break; // success
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
lastError = err;
|
|
2665
|
+
this.log('warn', `${item.cmd} attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
|
|
2666
|
+
if (attempt < maxAttempts) {
|
|
2667
|
+
await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (lastError) {
|
|
2672
|
+
this.log('error', `${item.cmd} failed after ${maxAttempts} attempts — skipping`);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
this._cmdRate.increment();
|
|
2585
2676
|
|
|
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)) {
|
|
2677
|
+
// Grace period for interactive commands — Dank Memer needs time to process
|
|
2678
|
+
// the interaction before accepting the next command.
|
|
2679
|
+
if (isInteractive) {
|
|
2591
2680
|
await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
|
|
2592
2681
|
}
|
|
2593
2682
|
|
|
@@ -2605,8 +2694,6 @@ class AccountWorker {
|
|
|
2605
2694
|
this.failStreak = 0;
|
|
2606
2695
|
}
|
|
2607
2696
|
|
|
2608
|
-
this.lastCommandRun = Date.now();
|
|
2609
|
-
|
|
2610
2697
|
// Exponential backoff: if too many consecutive failures, slow down
|
|
2611
2698
|
const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
|
|
2612
2699
|
// Minimum 5s cooldown for failed commands to prevent rapid-fire retries
|
|
@@ -2781,11 +2868,28 @@ class AccountWorker {
|
|
|
2781
2868
|
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2782
2869
|
body: JSON.stringify({ action_id: action.id }),
|
|
2783
2870
|
});
|
|
2784
|
-
} catch {
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
console.error(`[${this.username}] delete pending action error:`, err?.message || err);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (action.action === 'check_profile' && !this.busy) {
|
|
2876
|
+
this.log('info', 'Dashboard requested profile check');
|
|
2877
|
+
await this.checkProfile().catch(() => {});
|
|
2878
|
+
try {
|
|
2879
|
+
await fetch(`${API_URL}/api/grinder/actions`, {
|
|
2880
|
+
method: 'DELETE',
|
|
2881
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2882
|
+
body: JSON.stringify({ action_id: action.id }),
|
|
2883
|
+
});
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
console.error(`[${this.username}] delete pending action error:`, err?.message || err);
|
|
2886
|
+
}
|
|
2785
2887
|
}
|
|
2786
2888
|
}
|
|
2787
2889
|
}
|
|
2788
|
-
} catch {
|
|
2890
|
+
} catch (err) {
|
|
2891
|
+
console.error(`[${this.username}] refreshConfig error:`, err?.message || err);
|
|
2892
|
+
}
|
|
2789
2893
|
}
|
|
2790
2894
|
|
|
2791
2895
|
async start() {
|
|
@@ -2835,9 +2939,8 @@ class AccountWorker {
|
|
|
2835
2939
|
{ key: 'cmd_fish', l: 'fish' }, { key: 'cmd_beg', l: 'beg' },
|
|
2836
2940
|
{ key: 'cmd_search', l: 'search' }, { key: 'cmd_hl', l: 'hl' },
|
|
2837
2941
|
{ 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' },
|
|
2942
|
+
{ key: 'cmd_daily', l: 'daily' }, { key: 'cmd_monthly', l: 'monthly' },
|
|
2943
|
+
{ key: 'cmd_work', l: 'work' }, { key: 'cmd_stream', l: 'stream' },
|
|
2841
2944
|
{ key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
|
|
2842
2945
|
{ key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
|
|
2843
2946
|
{ key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
|
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 };
|