Files
TurboTrades/services/steamBot.js
2026-01-10 04:57:43 +00:00

729 lines
19 KiB
JavaScript

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;