893 lines
24 KiB
JavaScript
893 lines
24 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";
|
|
import wsManager from "../utils/websocket.js";
|
|
|
|
/**
|
|
* 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 a trade offer
|
|
* @param {Object} options - Trade offer options
|
|
* @returns {Promise<Object>} Trade offer result
|
|
*/
|
|
async createTradeOffer(options) {
|
|
if (!this.isReady) {
|
|
throw new Error(`Bot ${this.botId} is not ready`);
|
|
}
|
|
|
|
const {
|
|
tradeUrl,
|
|
itemsToReceive,
|
|
verificationCode,
|
|
metadata = {},
|
|
userId,
|
|
} = 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})`
|
|
);
|
|
|
|
// Notify user that trade is being created
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_creating",
|
|
data: {
|
|
verificationCode,
|
|
itemCount: itemsToReceive.length,
|
|
botId: this.botId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
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++;
|
|
|
|
// Notify user of error
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_error",
|
|
data: {
|
|
verificationCode,
|
|
error: err.message,
|
|
botId: this.botId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
return reject(err);
|
|
}
|
|
|
|
console.log(
|
|
`✅ Bot ${this.botId} trade sent: ${offer.id} (Code: ${verificationCode})`
|
|
);
|
|
|
|
// Get trade offer URL
|
|
const tradeOfferUrl = `https://steamcommunity.com/tradeoffer/${offer.id}`;
|
|
|
|
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,
|
|
userId: userId,
|
|
tradeOfferUrl: tradeOfferUrl,
|
|
});
|
|
|
|
this.tradeCount++;
|
|
this.lastTradeTime = new Date();
|
|
|
|
// Notify user that trade was sent
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_sent",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode,
|
|
status,
|
|
botId: this.botId,
|
|
itemCount: itemsToReceive.length,
|
|
tradeOfferUrl,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (status === "pending") {
|
|
this._confirmTradeOffer(offer)
|
|
.then(() => {
|
|
// Notify user that trade was confirmed
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_confirmed",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode,
|
|
botId: this.botId,
|
|
tradeOfferUrl,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
resolve({
|
|
offerId: offer.id,
|
|
botId: this.botId,
|
|
status: "sent",
|
|
verificationCode: verificationCode,
|
|
requiresConfirmation: true,
|
|
tradeOfferUrl,
|
|
});
|
|
})
|
|
.catch((confirmErr) => {
|
|
// Notify user of confirmation error
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_confirmation_error",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode,
|
|
error: confirmErr.message,
|
|
botId: this.botId,
|
|
tradeOfferUrl,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
resolve({
|
|
offerId: offer.id,
|
|
botId: this.botId,
|
|
status: "pending_confirmation",
|
|
verificationCode: verificationCode,
|
|
requiresConfirmation: true,
|
|
error: confirmErr.message,
|
|
tradeOfferUrl,
|
|
});
|
|
});
|
|
} else {
|
|
resolve({
|
|
offerId: offer.id,
|
|
botId: this.botId,
|
|
status: "sent",
|
|
verificationCode: verificationCode,
|
|
requiresConfirmation: false,
|
|
tradeOfferUrl,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
|
|
const userId = tradeData.userId;
|
|
|
|
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
|
|
|
|
// Notify user via WebSocket
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_accepted",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode: tradeData.verificationCode,
|
|
botId: this.botId,
|
|
itemCount: tradeData.itemsToReceive?.length || 0,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
|
|
case TradeOfferManager.ETradeOfferState.Declined:
|
|
console.log(`❌ Bot ${this.botId} trade ${offer.id} DECLINED`);
|
|
this.emit("tradeDeclined", offer, tradeData);
|
|
this.activeTrades.delete(offer.id);
|
|
|
|
// Notify user via WebSocket
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_declined",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode: tradeData.verificationCode,
|
|
botId: this.botId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
|
|
case TradeOfferManager.ETradeOfferState.Expired:
|
|
console.log(`⏰ Bot ${this.botId} trade ${offer.id} EXPIRED`);
|
|
this.emit("tradeExpired", offer, tradeData);
|
|
this.activeTrades.delete(offer.id);
|
|
|
|
// Notify user via WebSocket
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_expired",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode: tradeData.verificationCode,
|
|
botId: this.botId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
|
|
case TradeOfferManager.ETradeOfferState.Canceled:
|
|
console.log(`🚫 Bot ${this.botId} trade ${offer.id} CANCELED`);
|
|
this.emit("tradeCanceled", offer, tradeData);
|
|
this.activeTrades.delete(offer.id);
|
|
|
|
// Notify user via WebSocket
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_canceled",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode: tradeData.verificationCode,
|
|
botId: this.botId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
|
|
case TradeOfferManager.ETradeOfferState.Invalid:
|
|
console.log(`⚠️ Bot ${this.botId} trade ${offer.id} INVALID`);
|
|
this.emit("tradeInvalid", offer, tradeData);
|
|
this.activeTrades.delete(offer.id);
|
|
|
|
// Notify user via WebSocket
|
|
if (userId) {
|
|
wsManager.sendToUser(userId, {
|
|
type: "trade_invalid",
|
|
data: {
|
|
offerId: offer.id,
|
|
verificationCode: tradeData.verificationCode,
|
|
botId: this.botId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
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,
|
|
userId,
|
|
metadata: {
|
|
...metadata,
|
|
userId,
|
|
},
|
|
});
|
|
|
|
// Store verification code
|
|
this.verificationCodes.set(result.offerId, {
|
|
code: verificationCode,
|
|
botId: bot.botId,
|
|
createdAt: new Date(),
|
|
});
|
|
|
|
// Return result with trade offer URL
|
|
return {
|
|
...result,
|
|
code: verificationCode,
|
|
tradeOfferUrl: result.tradeOfferUrl,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|