UNPKG

top.gg-voter

Version:

An auto voter client that automatically votes for a bot on Top.gg using Discord tokens

339 lines (295 loc) 10.7 kB
/// topgg.js const { connect } = require("puppeteer-real-browser"); /** * Delay execution for a number of milliseconds. * @param {number} ms * @returns {Promise<void>} */ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** * @typedef {Object} AutoClientOptions * @property {string[]} tokenList - Array of Discord tokens. * @property {string} botId - The top.gg bot ID. * @property {number} [cooldown] - ms between vote cycles (default 12h). * @property {boolean} [runInParallel] - Vote all tokens in parallel per cycle. * @property {string[]} [proxies] - Optional proxy URLs (`http://user:pass@host:port`). * @property {Function} [fetchFn] - Custom fetch (defaults to global.fetch). * @property {boolean} [verbose] - Whether to log progress. * @property {Function} [errorLog] - fn(err) for errors; if omitted, errors throw. */ class AutoClient { /** * @param {AutoClientOptions} opts */ constructor(opts) { const { tokenList, botId, cooldown = 12 * 60 * 60 * 1000, runInParallel = false, proxies = [], fetchFn = global.fetch, verbose = false, errorLog = null, } = opts; if (!Array.isArray(tokenList) || tokenList.length === 0) { throw new Error("tokenList must be a non-empty array"); } if (typeof botId !== "string" || !/^\d{15,20}$/.test(botId)) { throw new Error("botId must be a valid Discord snowflake"); } if (typeof cooldown !== "number" || cooldown <= 0) { throw new Error("cooldown must be a positive number (ms)"); } this.tokenList = tokenList; this.botId = botId; this.cooldown = cooldown; this.runInParallel = runInParallel; this.proxies = proxies; this.fetchFn = fetchFn; this.verbose = verbose; this.errorLog = errorLog; this.stats = { total: tokenList.length, success: 0, failed: 0, invalid: 0, }; } _log(msg) { if (this.verbose) console.log(`[AutoClient] ${msg}`); } _handleError(err) { if (typeof this.errorLog === "function") { try { this.errorLog(err); } catch (e) { console.error("Error in errorLog function:", e); throw e; } } else { throw err; } } /** * Start the continuous voting loop. */ async autovoteBot() { while (true) { this._log(`Starting cycle for ${this.stats.total} token(s)...`); try { if (this.runInParallel) { await this._parallelCycle(); } else { await this._sequentialCycle(); } } catch (err) { // fatal cycle error this._log(`Fatal cycle error: ${err.message}`); this._handleError(err); } this._log(`Cycle complete. Success: ${this.stats.success}, Failed: ${this.stats.failed}, Invalid: ${this.stats.invalid}`); this._resetStats(); this._log(`Waiting ${this.cooldown / 3600000}h before next cycle...`); await delay(this.cooldown); } } async _sequentialCycle() { for (let i = 0; i < this.tokenList.length; i++) { this._log(`Processing token ${i + 1}/${this.tokenList.length}...`); try { await this._handleSingle(this.tokenList[i], this.proxies[i % this.proxies.length]); } catch (err) { this._log(`Error processing token ${i + 1}: ${err.message}`); this._handleError(err); } } } async _parallelCycle() { const promises = this.tokenList.map((tok, i) => { return this._handleSingle(tok, this.proxies[i % this.proxies.length]) .catch(err => { this._log(`Error in parallel processing for token ${i + 1}: ${err.message}`); this._handleError(err); }); }); await Promise.allSettled(promises); } async _handleSingle(token, proxy) { const shortT = token.slice(0, 5) + "..."; this._log(`Starting process for token ${shortT}`); // validate token this._log(`Validating token ${shortT}...`); let valid = false; try { const res = await this.fetchFn("https://discord.com/api/v10/users/@me", { headers: { Authorization: token }, }); valid = res.ok; this._log(`Token ${shortT} validation: ${valid ? 'valid' : 'invalid'}`); } catch (err) { this._log(`Token ${shortT} validation error: ${err.message}`); valid = false; } if (!valid) { this.stats.invalid++; const err = new Error(`Invalid token ${shortT}`); this._handleError(err); return; } // attempt vote this._log(`Attempting vote for token ${shortT}...`); try { const result = await this._voteForBot(token, proxy); if (result) { this.stats.success++; this._log(`Voted successfully for ${shortT}`); } else { this.stats.failed++; this._log(`Skipped (already voted or unavailable) for ${shortT}`); } } catch (err) { this.stats.failed++; this._log(`Vote attempt failed for ${shortT}: ${err.message}`); this._handleError(new Error(`Token ${shortT} error: ${err.message}`)); } } async _voteForBot(token, proxy) { const timeoutMs = 60_000; const shortT = token.slice(0, 5) + "..."; this._log(`Connecting to browser for ${shortT}...`); const opts = { headless: false, turnstile: true, }; if (proxy) { opts.proxy = proxy; this._log(`Using proxy: ${proxy}`); } let browser, page; try { this._log(`Attempting to connect to puppeteer...`); const connection = await Promise.race([ connect(opts), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser connection timeout')), 30000) ) ]); browser = connection.browser; page = connection.page; this._log(`Browser connected successfully for ${shortT}`); this._log(`Setting up token in localStorage for ${shortT}...`); await page.evaluateOnNewDocument((t) => { window.localStorage.setItem("token", `"${t}"`); }, token); this._log(`Navigating to top.gg for ${shortT}...`); await page.goto("https://top.gg", { waitUntil: "load", timeout: 30000 }); await delay(3000); this._log(`Clicking login button for ${shortT}...`); await page.evaluate(() => { const loginBtn = [...document.querySelectorAll("button")] .find(btn => btn.textContent.includes("Login")); if (loginBtn) loginBtn.click(); }); this._log(`Waiting for navigation after login for ${shortT}...`); await page.waitForNavigation({ waitUntil: "load" }); await page.setViewport({ width: 1920, height: 1080 }); this._log(`Waiting for Discord authorization button for ${shortT}...`); await page.waitForSelector("div.action__3d3b0 button", { visible: true, timeout: 10000 }); this._log(`Clicking Discord authorization button for ${shortT}...`); await page.click("div.action__3d3b0 button"); await page.waitForNavigation({ waitUntil: "load" }); await delay(3000); this._log(`Checking login status for ${shortT}...`); const isLoggedIn = await page.evaluate(() => { return !document.body.innerText.includes("Login"); }); if (!isLoggedIn) { throw new Error("Authorization failed - Discord OAuth did not complete"); } this._log(`Authorization successful for ${shortT}`); this._log(`Navigating to vote page for ${shortT}...`); const voteUrl = `https://top.gg/bot/${this.botId}/vote`; await page.goto(voteUrl, { waitUntil: "load" }); this._log(`Checking vote status for ${shortT}...`); const status = await this._waitForVoteStatus(page, timeoutMs); this._log(`Vote status for ${shortT}: ${status}`); if (status === "vote") { this._log(`Attempting to click vote button for ${shortT}...`); await delay(3000); await page.evaluate(() => { const voteBtn = [...document.querySelectorAll("button")] .find(btn => btn.textContent.includes("Vote") && !btn.disabled); if (voteBtn) voteBtn.click(); }); this._log(`Vote button clicked for ${shortT}`); await delay(5000); return true; } else if (status === "already") { this._log(`Already voted for ${shortT}`); return false; } else { this._log(`Vote did not become available in time for ${shortT}`); return false; } } catch (err) { this._log(`Error in _voteForBot for ${shortT}: ${err.message}`); throw err; } finally { if (browser) { this._log(`Closing browser for ${shortT}...`); try { await browser.close(); this._log(`Browser closed successfully for ${shortT}`); } catch (closeErr) { this._log(`Error closing browser for ${shortT}: ${closeErr.message}`); } } } } async _waitForVoteStatus(page, timeoutMs) { const start = Date.now(); let attempts = 0; while (Date.now() - start < timeoutMs) { attempts++; this._log(`Checking vote status (attempt ${attempts})...`); try { const st = await page.evaluate(() => { const txt = document.body.innerText; if (txt.includes("You can vote now!")) return "vote"; if (txt.includes("You have already voted")) return "already"; return "wait"; }); this._log(`Vote status check result: ${st}`); if (st !== "wait") return st; } catch (err) { this._log(`Error checking vote status: ${err.message}`); } await delay(2500); } this._log(`Vote status check timed out after ${attempts} attempts`); return "timeout"; } _resetStats() { this.stats.success = 0; this.stats.failed = 0; this.stats.invalid = 0; } } /** * Functional shorthand for quick use. * @param {AutoClientOptions} opts */ async function autovoteBot(opts) { const client = new AutoClient(opts); await client.autovoteBot(); } module.exports = { AutoClient, autovoteBot, }; // For ESM default module.exports.default = { AutoClient, autovoteBot };