import SteamUser from "steam-user"; import SteamCommunity from "steamcommunity"; import TradeOfferManager from "steam-tradeoffer-manager"; import SteamTotp from "steam-totp"; import { EventEmitter } from "events"; import { SocksProxyAgent } from "socks-proxy-agent"; import HttpsProxyAgent from "https-proxy-agent"; /** * Steam Bot Service with Multi-Bot Support, Proxies, and Verification Codes * * Features: * - Multiple bot instances with load balancing * - Proxy support (SOCKS5, HTTP/HTTPS) * - Verification codes for trades * - Automatic failover * - Health monitoring */ class SteamBotInstance extends EventEmitter { constructor(config, botId) { super(); this.botId = botId; this.config = config; // Setup proxy if provided const proxyAgent = this._createProxyAgent(); this.client = new SteamUser({ httpProxy: proxyAgent ? proxyAgent : undefined, enablePicsCache: true, }); this.community = new SteamCommunity({ request: proxyAgent ? { agent: proxyAgent, } : undefined, }); this.manager = new TradeOfferManager({ steam: this.client, community: this.community, language: "en", pollInterval: config.pollInterval || 30000, cancelTime: config.tradeTimeout || 600000, // 10 minutes default pendingCancelTime: config.tradeTimeout || 600000, }); this.isReady = false; this.isLoggedIn = false; this.isHealthy = true; this.activeTrades = new Map(); this.tradeCount = 0; this.lastTradeTime = null; this.errorCount = 0; this.loginAttempts = 0; this.maxLoginAttempts = 3; this._setupEventHandlers(); } /** * Create proxy agent based on config */ _createProxyAgent() { if (!this.config.proxy) return null; const { proxy } = this.config; try { if (proxy.type === "socks5" || proxy.type === "socks4") { const proxyUrl = `${proxy.type}://${ proxy.username ? `${proxy.username}:${proxy.password}@` : "" }${proxy.host}:${proxy.port}`; return new SocksProxyAgent(proxyUrl); } else if (proxy.type === "http" || proxy.type === "https") { const proxyUrl = `${proxy.type}://${ proxy.username ? `${proxy.username}:${proxy.password}@` : "" }${proxy.host}:${proxy.port}`; return new HttpsProxyAgent(proxyUrl); } } catch (error) { console.error( `❌ Failed to create proxy agent for bot ${this.botId}:`, error.message ); } return null; } /** * Setup event handlers */ _setupEventHandlers() { this.client.on("loggedOn", () => { console.log(`✅ Bot ${this.botId} logged in successfully`); this.isLoggedIn = true; this.loginAttempts = 0; this.client.setPersona(SteamUser.EPersonaState.Online); this.emit("loggedIn"); }); this.client.on("webSession", (sessionId, cookies) => { console.log(`✅ Bot ${this.botId} web session established`); this.manager.setCookies(cookies); this.community.setCookies(cookies); this.isReady = true; this.isHealthy = true; this.emit("ready"); }); this.client.on("error", (err) => { console.error(`❌ Bot ${this.botId} error:`, err.message); this.isLoggedIn = false; this.isReady = false; this.isHealthy = false; this.errorCount++; this.emit("error", err); // Auto-reconnect after delay if (this.loginAttempts < this.maxLoginAttempts) { setTimeout(() => { console.log(`🔄 Bot ${this.botId} attempting reconnect...`); this.login().catch((e) => console.error(`Reconnect failed:`, e.message) ); }, 30000); // Wait 30 seconds before retry } }); this.client.on("disconnected", (eresult, msg) => { console.warn( `⚠️ Bot ${this.botId} disconnected: ${eresult} - ${msg || "Unknown"}` ); this.isLoggedIn = false; this.isReady = false; this.emit("disconnected"); }); this.manager.on("newOffer", (offer) => { console.log(`📨 Bot ${this.botId} received trade offer: ${offer.id}`); this.emit("newOffer", offer); this._handleIncomingOffer(offer); }); this.manager.on("sentOfferChanged", (offer, oldState) => { console.log( `🔄 Bot ${this.botId} trade ${offer.id} changed: ${ TradeOfferManager.ETradeOfferState[oldState] } -> ${TradeOfferManager.ETradeOfferState[offer.state]}` ); this.emit("offerChanged", offer, oldState); this._handleOfferStateChange(offer, oldState); }); this.manager.on("pollFailure", (err) => { console.error(`❌ Bot ${this.botId} poll failure:`, err.message); this.errorCount++; if (this.errorCount > 10) { this.isHealthy = false; } this.emit("pollFailure", err); }); } /** * Login to Steam */ async login() { return new Promise((resolve, reject) => { if (!this.config.accountName || !this.config.password) { return reject( new Error(`Bot ${this.botId} credentials not configured`) ); } this.loginAttempts++; console.log( `🔐 Bot ${this.botId} logging in... (attempt ${this.loginAttempts})` ); const logOnOptions = { accountName: this.config.accountName, password: this.config.password, }; if (this.config.sharedSecret) { logOnOptions.twoFactorCode = SteamTotp.generateAuthCode( this.config.sharedSecret ); } const readyHandler = () => { this.removeListener("error", errorHandler); resolve(); }; const errorHandler = (err) => { this.removeListener("ready", readyHandler); reject(err); }; this.once("ready", readyHandler); this.once("error", errorHandler); this.client.logOn(logOnOptions); }); } /** * Logout from Steam */ logout() { console.log(`👋 Bot ${this.botId} logging out...`); this.client.logOff(); this.isLoggedIn = false; this.isReady = false; } /** * Create trade offer with verification code */ async createTradeOffer(options) { if (!this.isReady) { throw new Error(`Bot ${this.botId} is not ready`); } const { tradeUrl, itemsToReceive, verificationCode, metadata = {}, } = options; if (!tradeUrl) throw new Error("Trade URL is required"); if (!itemsToReceive || itemsToReceive.length === 0) { throw new Error("Items to receive are required"); } if (!verificationCode) throw new Error("Verification code is required"); console.log( `📤 Bot ${this.botId} creating trade offer for ${itemsToReceive.length} items (Code: ${verificationCode})` ); return new Promise((resolve, reject) => { const offer = this.manager.createOffer(tradeUrl); offer.addTheirItems(itemsToReceive); // Include verification code in trade message const message = `TurboTrades Trade\nVerification Code: ${verificationCode}\n\nPlease verify this code matches the one shown on our website before accepting.\n\nDo not accept trades without a valid verification code!`; offer.setMessage(message); offer.send((err, status) => { if (err) { console.error( `❌ Bot ${this.botId} failed to send trade:`, err.message ); this.errorCount++; return reject(err); } console.log( `✅ Bot ${this.botId} trade sent: ${offer.id} (Code: ${verificationCode})` ); this.activeTrades.set(offer.id, { id: offer.id, status: status, state: offer.state, itemsToReceive: itemsToReceive, verificationCode: verificationCode, metadata: metadata, createdAt: new Date(), botId: this.botId, }); this.tradeCount++; this.lastTradeTime = new Date(); if (status === "pending") { this._confirmTradeOffer(offer) .then(() => { resolve({ offerId: offer.id, botId: this.botId, status: "sent", verificationCode: verificationCode, requiresConfirmation: true, }); }) .catch((confirmErr) => { resolve({ offerId: offer.id, botId: this.botId, status: "pending_confirmation", verificationCode: verificationCode, requiresConfirmation: true, error: confirmErr.message, }); }); } else { resolve({ offerId: offer.id, botId: this.botId, status: "sent", verificationCode: verificationCode, requiresConfirmation: false, }); } }); }); } /** * Confirm trade offer */ async _confirmTradeOffer(offer) { if (!this.config.identitySecret) { throw new Error(`Bot ${this.botId} identity secret not configured`); } return new Promise((resolve, reject) => { this.community.acceptConfirmationForObject( this.config.identitySecret, offer.id, (err) => { if (err) { console.error( `❌ Bot ${this.botId} confirmation failed:`, err.message ); return reject(err); } console.log(`✅ Bot ${this.botId} trade ${offer.id} confirmed`); resolve(); } ); }); } /** * Handle incoming offers (decline by default) */ async _handleIncomingOffer(offer) { console.log(`⚠️ Bot ${this.botId} declining incoming offer ${offer.id}`); offer.decline((err) => { if (err) console.error(`Failed to decline offer:`, err.message); }); } /** * Handle offer state changes */ async _handleOfferStateChange(offer, oldState) { const tradeData = this.activeTrades.get(offer.id); if (!tradeData) return; tradeData.state = offer.state; tradeData.updatedAt = new Date(); switch (offer.state) { case TradeOfferManager.ETradeOfferState.Accepted: console.log(`✅ Bot ${this.botId} trade ${offer.id} ACCEPTED`); this.emit("tradeAccepted", offer, tradeData); this.errorCount = Math.max(0, this.errorCount - 1); // Decrease error count on success break; case TradeOfferManager.ETradeOfferState.Declined: console.log(`❌ Bot ${this.botId} trade ${offer.id} DECLINED`); this.emit("tradeDeclined", offer, tradeData); this.activeTrades.delete(offer.id); break; case TradeOfferManager.ETradeOfferState.Expired: console.log(`⏰ Bot ${this.botId} trade ${offer.id} EXPIRED`); this.emit("tradeExpired", offer, tradeData); this.activeTrades.delete(offer.id); break; case TradeOfferManager.ETradeOfferState.Canceled: console.log(`🚫 Bot ${this.botId} trade ${offer.id} CANCELED`); this.emit("tradeCanceled", offer, tradeData); this.activeTrades.delete(offer.id); break; } } /** * Get health metrics */ getHealth() { return { botId: this.botId, isReady: this.isReady, isLoggedIn: this.isLoggedIn, isHealthy: this.isHealthy, activeTrades: this.activeTrades.size, tradeCount: this.tradeCount, errorCount: this.errorCount, lastTradeTime: this.lastTradeTime, username: this.config.accountName, proxy: this.config.proxy ? `${this.config.proxy.type}://${this.config.proxy.host}:${this.config.proxy.port}` : null, }; } /** * Check if bot can accept new trades */ canAcceptTrade() { return ( this.isReady && this.isHealthy && this.activeTrades.size < (this.config.maxConcurrentTrades || 10) ); } /** * Get trade offer */ async getTradeOffer(offerId) { return new Promise((resolve, reject) => { this.manager.getOffer(offerId, (err, offer) => { if (err) return reject(err); resolve(offer); }); }); } /** * Cancel trade offer */ async cancelTradeOffer(offerId) { const offer = await this.getTradeOffer(offerId); return new Promise((resolve, reject) => { offer.cancel((err) => { if (err) return reject(err); this.activeTrades.delete(offerId); resolve(); }); }); } } /** * Multi-Bot Manager * Manages multiple Steam bot instances with load balancing */ class SteamBotManager extends EventEmitter { constructor() { super(); this.bots = new Map(); this.verificationCodes = new Map(); // Map of tradeId -> code this.isInitialized = false; } /** * Initialize bots from configuration */ async initialize(botsConfig) { console.log(`🤖 Initializing ${botsConfig.length} Steam bots...`); const loginPromises = []; for (let i = 0; i < botsConfig.length; i++) { const botConfig = botsConfig[i]; const botId = `bot_${i + 1}`; const bot = new SteamBotInstance(botConfig, botId); // Forward bot events bot.on("tradeAccepted", (offer, tradeData) => { this.emit("tradeAccepted", offer, tradeData, botId); }); bot.on("tradeDeclined", (offer, tradeData) => { this.emit("tradeDeclined", offer, tradeData, botId); }); bot.on("tradeExpired", (offer, tradeData) => { this.emit("tradeExpired", offer, tradeData, botId); }); bot.on("tradeCanceled", (offer, tradeData) => { this.emit("tradeCanceled", offer, tradeData, botId); }); bot.on("error", (err) => { this.emit("botError", err, botId); }); this.bots.set(botId, bot); // Login with staggered delays to avoid rate limiting loginPromises.push( new Promise((resolve) => { setTimeout(async () => { try { await bot.login(); console.log(`✅ Bot ${botId} ready`); resolve({ success: true, botId }); } catch (error) { console.error(`❌ Bot ${botId} failed to login:`, error.message); resolve({ success: false, botId, error: error.message }); } }, i * 5000); // Stagger by 5 seconds }) ); } const results = await Promise.all(loginPromises); const successCount = results.filter((r) => r.success).length; console.log( `✅ ${successCount}/${botsConfig.length} bots initialized successfully` ); this.isInitialized = true; return results; } /** * Generate verification code */ generateVerificationCode() { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Removed ambiguous characters let code = ""; for (let i = 0; i < 6; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)); } return code; } /** * Get best available bot (load balancing) */ getBestBot() { const availableBots = Array.from(this.bots.values()).filter((bot) => bot.canAcceptTrade() ); if (availableBots.length === 0) { throw new Error("No bots available to handle trade"); } // Sort by active trades (least busy first) availableBots.sort((a, b) => a.activeTrades.size - b.activeTrades.size); return availableBots[0]; } /** * Create trade offer with automatic bot selection */ async createTradeOffer(options) { if (!this.isInitialized) { throw new Error("Bot manager not initialized"); } const { tradeUrl, itemsToReceive, userId, metadata = {} } = options; // Generate verification code const verificationCode = this.generateVerificationCode(); // Get best available bot const bot = this.getBestBot(); console.log( `📤 Selected ${bot.botId} for trade (${bot.activeTrades.size} active trades)` ); // Create trade offer const result = await bot.createTradeOffer({ tradeUrl, itemsToReceive, verificationCode, metadata: { ...metadata, userId, }, }); // Store verification code this.verificationCodes.set(result.offerId, { code: verificationCode, botId: bot.botId, createdAt: new Date(), }); return { ...result, verificationCode, }; } /** * Verify trade code */ verifyTradeCode(offerId, code) { const stored = this.verificationCodes.get(offerId); if (!stored) return false; return stored.code.toUpperCase() === code.toUpperCase(); } /** * Get verification code for trade */ getVerificationCode(offerId) { const stored = this.verificationCodes.get(offerId); return stored ? stored.code : null; } /** * Get all bot health stats */ getAllBotsHealth() { const health = []; for (const [botId, bot] of this.bots.entries()) { health.push(bot.getHealth()); } return health; } /** * Get bot by ID */ getBot(botId) { return this.bots.get(botId); } /** * Get bot handling specific trade */ getBotForTrade(offerId) { for (const bot of this.bots.values()) { if (bot.activeTrades.has(offerId)) { return bot; } } return null; } /** * Cancel trade offer */ async cancelTradeOffer(offerId) { const bot = this.getBotForTrade(offerId); if (!bot) { throw new Error("Bot handling this trade not found"); } await bot.cancelTradeOffer(offerId); this.verificationCodes.delete(offerId); } /** * Get system-wide statistics */ getStats() { let totalTrades = 0; let totalActiveTrades = 0; let totalErrors = 0; let healthyBots = 0; let readyBots = 0; for (const bot of this.bots.values()) { totalTrades += bot.tradeCount; totalActiveTrades += bot.activeTrades.size; totalErrors += bot.errorCount; if (bot.isHealthy) healthyBots++; if (bot.isReady) readyBots++; } return { totalBots: this.bots.size, readyBots, healthyBots, totalTrades, totalActiveTrades, totalErrors, verificationCodesStored: this.verificationCodes.size, }; } /** * Cleanup expired verification codes */ cleanupVerificationCodes() { const now = Date.now(); const maxAge = 30 * 60 * 1000; // 30 minutes for (const [offerId, data] of this.verificationCodes.entries()) { if (now - data.createdAt.getTime() > maxAge) { this.verificationCodes.delete(offerId); } } } /** * Shutdown all bots */ shutdown() { console.log("👋 Shutting down all bots..."); for (const bot of this.bots.values()) { bot.logout(); } this.bots.clear(); this.verificationCodes.clear(); this.isInitialized = false; } } // Singleton instance let managerInstance = null; export function getSteamBotManager() { if (!managerInstance) { managerInstance = new SteamBotManager(); } return managerInstance; } export { SteamBotManager, SteamBotInstance }; export default SteamBotManager;