BotAccount.prototype.__proto__ = require('events').EventEmitter.prototype;
const AuthManager = require('./Auth.js');
const TradeHandler = require('./Trade.js');
const SteamCommunity = require('steamcommunity');
const SteamUser = require('steam-user');
const SteamStore = require('steamstore');
const TradeOfferManager = require('steam-tradeoffer-manager');
const SteamID = TradeOfferManager.SteamID;
/**
* Creates a new BotAccount instance for a bot.
* @class
*/
//
function BotAccount(username, password, details, settings, logger) {
// Ensure account values are valid
var self = this;
// Init all required variables
if (typeof username != "string" || typeof password != "string")
if (!details.hasOwnProperty("steamguard") || !(details.hasOwnProperty("oAuthToken")))
throw Error("Invalid username/password or missing oAuthToken/Steamguard code");
self.username = username;
self.password = password;
self.settings = settings != undefined ? settings : {
api_key: "",
tradeCancelTime: 60 * 60 * 24,
tradePendingCancelTime: 60 * 60 * 24,
language: "en",
tradePollInterval: 5000,
tradeCancelOfferCount: 30,
tradeCancelOfferCountMinAge: 60 * 60,
cancelTradeOnOverflow: true
};
self.logger = logger;
self.community = new SteamCommunity();
self.client = new SteamUser();
self.TradeOfferManager = new TradeOfferManager({
"steam": self.client,
"community": self.community,
"cancelTime": self.settings.tradeCancelTime, // Keep offers upto 1 day, and then just cancel them.
"pendingCancelTime": self.settings.tradePendingCancelTime, // Keep offers upto 30 mins, and then cancel them if they still need confirmation
"cancelOfferCount": self.settings.tradeCancelOfferCount,// Cancel offers once we hit 7 day threshold
"cancelOfferCountMinAge": self.settings.tradeCancelOfferCountMinAge,// Keep offers until 7 days old
"language": self.settings.language, // We want English item descriptions
"pollInterval": 5000 // We want to poll every 5 seconds since we don't have Steam notifying us of offers
});
self.SteamID = TradeOfferManager.SteamID;
self.request = self.community.request;
self.store = new SteamStore();
self.loggedIn = false;
self.rateLimited = true;
self.delayedTasks = [];
self.details = details;
self.AuthManager = new AuthManager(self, logger);
self.AuthManager.on('updatedAccountDetails', function () {
self.emit('updatedAccountDetails');
});
self.TradeManager = new TradeHandler(self.TradeOfferManager, self.AuthManager, self.settings, logger);
};
/**
* Get the account's username, used to login to Steam
* @returns {String} username
*/
BotAccount.prototype.getAccountName = function () {
var self = this;
return self.username;
};
/**
* Get the account's username, used to login to Steam
* @returns {String} username
*/
BotAccount.prototype.TradeManager = function () {
var self = this;
return self.TradeOfferManager;
};
/**
* Get if the API/account is rate limited by SteamAPI
* @returns {Boolean} rateLimited
*/
BotAccount.prototype.getRateLimited = function () {
var self = this;
return self.rateLimited;
};
/**
* Get if the API/account is rate limited by SteamAPI
* @returns {Boolean} rateLimited
*/
BotAccount.prototype.setRateLimited = function (rateLimited) {
var self = this;
return self.rateLimited = rateLimited;
};
/**
* Fetch SteamID Object from the SteamID.
* @returns {Error | String}
*/
BotAccount.prototype.fromIndividualAccountID = function (id) {
var self = this;
var SteamID = TradeOfferManager.SteamID;
return new self.SteamID(id);
};
/**
* Get account details
* @returns {*|{username: *, password: *}}
*/
BotAccount.prototype.getAccount = function () {
var self = this;
return self.accountDetails;
};
/**
* Send a chat message to a receipient with callback
* @param {SteamID} recipient - Recipient of the message
* @param {String} message - Message to send
* @param {String} type - saytest or typing (message ignored for 'typing')
* @param {messageCallback} callback - Callback upon sending the message (undefined, or Error)
*/
BotAccount.prototype.sendMessage = function (recipient, message, type, callback) {
var self = this;
return self.community.chatMessage(recipient, message, type, callback);
};
/**
* Send a chat message to a receipient with callback
* @param {SteamID} recipient - Recipient of the message
* @param {String} message - Message to send
* @param {messageCallback} callback - Callback upon sending the message (undefined, or Error)
*/
BotAccount.prototype.sendMessage = function (recipient, message, callback) {
var self = this;
return self.community.chatMessage(recipient, message, callback);
};
/**
* Send a chat message to a receipient without callback
* @param {SteamID} recipient - Recipient of the message
* @param {String} message - Message to send
*/
BotAccount.prototype.sendMessage = function (recipient, message) {
var self = this;
return self.community.chatMessage(recipient, message);
};
/**
* Set the user we are chatting with
* @param {*|{username: *, sid: *}} chattingUserInfo
*/
BotAccount.prototype.setChatting = function (chattingUserInfo) {
var self = this;
self.currentChatting = chattingUserInfo;
};
/**
* Get the display name of the account
* @returns {String|undefined} displayName - Display name of the account
*/
BotAccount.prototype.getDisplayName = function () {
var self = this;
return (self.displayName ? self.displayName : undefined);
};
/**
* Function wrapper used to delay function calls by name and paramters
* @param fn - function reference
* @param context - Context to use for call
* @param params - Parameters in arraylist to send with function
* @returns {Function}
*/
BotAccount.prototype.wrapFunction = function (fn, context, params) {
return function () {
fn.apply(context, params);
};
};
/**
* Add a function to the queue which runs when we login usually.
* @param functionV
* @param functionData
*/
BotAccount.prototype.addToQueue = function (queueName, functionV, functionData) {
var self = this;
var functionVal = self.wrapFunction(functionV, self, functionData);
if (!self.delayedTasks.hasOwnProperty(queueName))
self.delayedTasks[queueName] = [];
self.delayedTasks[queueName].push(functionVal);
};
/**
* Process the queue to run tasks that were delayed.
* @param queueName
* @param callback
*/
BotAccount.prototype.processQueue = function (queueName, callback) {
var self = this;
if (self.delayedTasks.hasOwnProperty(queueName)) {
while (self.delayedTasks[queueName].length > 0) {
(self.delayedTasks[queueName].shift())();
}
}
callback(undefined);
};
/**
* Upvote an attachement file on SteamCommunity
* @param sharedFileId
* @param callback
*/
BotAccount.prototype.upvoteSharedFile = function (sharedFileId, callback) {
var self = this;
var options = {
form: {
'sessionid': self.accountDetails.sessionID,
'id': sharedFileId
}
};
self.community.httpRequestPost('https://steamcommunity.com/sharedfiles/voteup', options, function (error, response, body) {
if (!error && response.statusCode == 200)
callback(undefined);
else
callback(error);
});
};
/**
* Downvote an attachement file on SteamCommunity.
* @param sharedFileId
* @param callback
*/
BotAccount.prototype.downvoteSharedFile = function (sharedFileId, callback) {
var self = this;
var options = {
form: {
'sessionid': self.accountDetails.sessionID,
'id': sharedFileId
}
};
self.community.httpRequestPost('https://steamcommunity.com/sharedfiles/votedown', options, function (error, response, body) {
if (!error && response.statusCode == 200) {
callback(undefined);
}
else
callback(error);
});
};
/**
* Change the display name of the account (with prefix)
* @param {String} newName - The new display name
* @param {String} namePrefix - The prefix if there is one (Nullable)
* @param {errorCallback} errorCallback - A callback returned with possible errors
*/
BotAccount.prototype.changeName = function (newName, namePrefix, errorCallback) {
var self = this;
if (!self.loggedIn) {
self.addToQueue('login', self.changeName, [newName, namePrefix, errorCallback]);
}
else {
if (namePrefix == undefined) namePrefix = '';
self.community.editProfile({name: namePrefix + newName}, function (err) {
if (err)
return errorCallback(err.Error);
self.username = newName;
self.emit('updatedAccountDetails');
errorCallback(undefined);
});
}
};
/**
* @callback inventoryCallback
* @param {Error} error - An error message if the process failed, undefined if successful
* @param {Array} inventory - An array of Items returned via fetch (if undefined, then game is not owned by user)
* @param {Array} currencies - An array of currencies (Only a few games use this) - (if undefined, then game is not owned by user)
*/
/**
* Retrieve account inventory based on filters
* @param {Integer} appid - appid by-which to fetch inventory based on.
* @param {Integer} contextid - contextid of lookup (1 - Gifts, 2 - In-game Items, 3 - Coupons, 6 - Game Cards, Profile Backgrounds & Emoticons)
* @param {Boolean} tradableOnly - Items retrieved must be tradable
* @param {inventoryCallback} inventoryCallback - Inventory details (refer to inventoryCallback for more info.)
*/
BotAccount.prototype.getInventory = function (appid, contextid, tradableOnly, inventoryCallback) {
var self = this;
if (!self.loggedIn) {
self.addToQueue('login', self.getInventory, [appid, contextid, tradableOnly, inventoryCallback]);
}
else
self.TradeOfferManager.loadInventory(appid, contextid, tradableOnly, inventoryCallback);
};
/**
* Retrieve account inventory based on filters and provided steamID
* @param {SteamID} steamID - SteamID to use for lookup of inventory
* @param {Integer} appid - appid by-which to fetch inventory based on.
* @param {Integer} contextid - contextid of lookup (1 - Gifts, 2 - In-game Items, 3 - Coupons, 6 - Game Cards, Profile Backgrounds & Emoticons)
* @param {Boolean} tradableOnly - Items retrieved must be tradableOnly
* @param {inventoryCallback} inventoryCallback - Inventory details (refer to inventoryCallback for more info.)
*/
BotAccount.prototype.getUserInventory = function (steamID, appid, contextid, tradableOnly, inventoryCallback) {
var self = this;
if (!self.loggedIn) {
self.addToQueue('login', self.getUserInventory, [steamID, appid, contextid, tradableOnly, inventoryCallback]);
}
else
self.TradeOfferManager.loadUserInventory(steamID, appid, contextid, tradableOnly, inventoryCallback);
};
/**
* Add a phone-number to the account (For example before setting up 2-factor authentication)
* @param phoneNumber - Certain format must be followed
* @param {errorCallback} errorCallback - A callback returned with possible errors
*/
BotAccount.prototype.addPhoneNumber = function (phoneNumber, errorCallback) {
var self = this;
self.store.addPhoneNumber(phoneNumber, true, function (err) {
errorCallback(err);
});
};
/**
* @callback acceptedTradesCallback
* @param {Error} error - An error message if the process failed, undefined if successful
* @param {Array} acceptedTrades - An array of trades that were confirmed in the process.
*/
/**
* Confirm (not accept) all sent trades associated with a certain SteamID via the two-factor authenticator.
* @param {SteamID} steamID - SteamID to use for lookup of inventory
* @param {acceptedTradesCallback} acceptedTradesCallback - Inventory details (refer to inventoryCallback for more info.)
*/
BotAccount.prototype.confirmTradesFromUser = function (SteamID, callback) {
var self = this;
self.TradeOfferManager.getOffers(1, undefined, function (err, sent, received) {
var acceptedTrades = [];
for (var sentOfferIndex in sent) {
if (sent.hasOwnProperty(sentOfferIndex)) {
var sentOfferInfo = sent[sentOfferIndex];
if (sentOfferInfo.partner.getSteamID64() == SteamID.getSteamID64) {
sentOfferInfo.accept();
acceptedTrades.push(sentOfferInfo);
}
}
}
for (var receivedOfferIndex in received) {
if (received.hasOwnProperty(receivedOfferIndex)) {
var receievedOfferInfo = received[receivedOfferIndex];
if (receievedOfferInfo.partner.getSteamID64() == SteamID.getSteamID64) {
receievedOfferInfo.accept();
acceptedTrades.push(receievedOfferInfo);
}
}
}
self.confirmOutstandingTrades(function (err, confirmedTrades) {
callback(err, acceptedTrades);
});
});
};
BotAccount.prototype.verifyPhoneNumber = function (code, callback) {
var self = this;
self.store.verifyPhoneNumber(code, function (err) {
if (err) {
callback(err);
}
else {
callback(undefined);
}
});
};
BotAccount.prototype.getRequest = function (url, callback) {
var self = this;
self.request({
url: url,
method: "GET",
json: true
}, function (err, response, body) {
callback(err, body);
});
}
/**
* @callback callbackRequestAPI
* @param {Error} error - An error message if the process failed, undefined if successful
* @param {Object} body - An object of the parsed response (undefined if failed)
*/
/**
* Send GET Request to SteamAPI with details
* @param apiInterface (String) - Interface name
* @param version (String) - Interface version (v1 or v2 depending on interface)
* @param method (String) - method to access
* @param options - Data to attach to request
* @param callbackRequestAPI -
*/
BotAccount.prototype.getRequestAPI = function (apiInterface, version, method, options, callbackRequestAPI) {
var self = this;
var string = '?';
var x = 0
for (var option in options)
string += option + "=" + options[option] + (x++ < Object.keys(options).length - 1 ? "&" : '');
self.logger.log('debug', "Sending GET request to " + string);
self.community.request({
url: 'http://api.steampowered.com/' + apiInterface + '/' + method + '/' + version + '/' + string,
method: "GET",
json: true,
}, function (err, response, body) {
callbackRequestAPI(err, body);
});
}
BotAccount.prototype.getFriendsSummariesNonAPI = function (friends, atCount, friendsCompiled, callback) {
var self = this;
var steamids = ""
var maxCount = atCount + 100;
if (atCount + 100 > friends.length)
maxCount = friends.length;
for (var x = 0 + atCount; x < (maxCount); x++)
steamids += friends[x].steamid + (x < maxCount - 1 ? ',' : "");
self.getRequestAPI('ISteamUser', 'v2', 'GetPlayerSummaries', {
key: self.settings.api_key,
steamids: steamids
}, function (err, body) {
if (err)
return callback(err, friendsCompiled);
var compiledFriends = body.response.players;
if (maxCount < friends.length) {
self.getFriendsSummaries(friends, atCount + 100, friendsCompiled.concat(compiledFriends), function (err, friendsSummaries) {
return callback(undefined, compiled);
});
}
else
return callback(undefined, friendsCompiled.concat(compiledFriends));
})
};
BotAccount.prototype.getFriendsSummaries = function (friends, atCount, friendsCompiled, callback) {
var self = this;
var steamids = ""
var maxCount = atCount + 100;
if (atCount + 100 > friends.length)
maxCount = friends.length;
for (var x = 0 + atCount; x < (maxCount); x++)
steamids += friends[x].steamid + (x < maxCount - 1 ? ',' : "");
self.getRequestAPI('ISteamUser', 'v2', 'GetPlayerSummaries', {
key: self.settings.api_key,
steamids: steamids
}, function (err, body) {
if (err)
return callback(err, friendsCompiled);
var compiledFriends = body.response.players;
if (maxCount < friends.length) {
self.getFriendsSummaries(friends, atCount + 100, friendsCompiled.concat(compiledFriends), function (err, friendsSummaries) {
return callback(undefined, compiled);
});
}
else
return callback(undefined, friendsCompiled.concat(compiledFriends));
})
};
BotAccount.prototype.getFriends = function (callback) {
var self = this;
var onlineFriendsList = [];
self.logger.log('debug', "Getting friends list");
if (self.cachedFriendsList && (typeof self.cachedFriendsList == 'object') && ((new Date().getTime() / 1000) - (self.cachedFriendsList.cacheTime)) < (60 * 10)) {
onlineFriendsList = self.cachedFriendsList.friendsList.slice();
self.logger.log('debug', "Used cached friendslist");
return callback(undefined, onlineFriendsList);
} else {
// Due to the fact that we must submit an API call everytime we need friends list, we will cach the data for 5 minutes. Clear cach on force.
if (!self.loggedIn) {
self.logger.log('debug', "Queued getFriends method until login.");
self.addToQueue('login', self.getFriends, [callback]);
}
else {
self.logger.log('debug', "Getting a fresh list of friends");
self.getRequestAPI('ISteamUser', 'v1', 'GetFriendList', {
key: self.settings.api_key,
relationship: 'friend',
steamid: self.community.steamID
}, function (err, body) {
if (err)
return callback(err, undefined);
if (body.hasOwnProperty("friendslist")) {
var friends = body.friendslist.friends;
self.getFriendsSummaries(friends, 0, [], function (err, friendsSummaries) {
// We need to convert SteamID to names... To do that, we need SteamCommunity package.
for (var id in friendsSummaries) {
onlineFriendsList.push({
username: friendsSummaries[id].personaname,
accountSid: friendsSummaries[id].steamid
});
}
self.cachedFriendsList = {
friendsList: onlineFriendsList,
cacheTime: new Date().getTime() / 1000
};
return callback(undefined, onlineFriendsList.slice());
});
} else {
self.logger.log('debug', body);
self.logger.log('debug', "Failed to fetch friends - API call failed");
return callback(undefined, onlineFriendsList.slice());
}
})
}
}
};
/**
* This is a private method - but incase you would like to edit it for your own usage...
* @param cookies - Cookies sent by Steam when logged in
* @param sessionID - Session ID as sent by Steam
* @param {loginCallback} loginCallback - Login details (refer to loginCallback for more info.)
*/
BotAccount.prototype.loggedInAccount = function (cookies, sessionID, loginCallback) {
var self = this;
self.chatLogon(500, 'web');
self.logger.log('debug', 'Logged into %j', self.getAccountName());
if (self.sessionID != sessionID || self.cookies != cookies) {
self.sessionID = sessionID;
self.cookies = cookies;
delete self.twoFactorCode;
}
self.emit('loggedIn', self);
if (self.cookies) {
self.community.setCookies(cookies);
self.store.setCookies(cookies);
self.TradeOfferManager.setCookies(cookies, function (err) {
if (err) {
self.logger.log("debug", "Failed to get API Key - TradeOverflowChecking disabled for %j & getOffers call disabled.", self.getAccountName(), err.Error);
if (err.Error == "Access Denied")
self.api_access = false;
}
else
self.api_access = true;
self.TradeManager.setAPIAccess(self.api_access);
});
}
self.loggedIn = true;
self.processQueue('login', function (err) {
if (err) {
self.logger.log('error', err);
return loginCallback(err);
}
self.community.on('chatTyping', function (senderID) {
self.emit('chatTyping', senderID);
});
self.community.on('chatLoggedOff', function () {
self.emit('chatLoggedOff');
});
self.community.on('chatMessage', function (senderID, message) {
if (self.currentChatting != undefined && senderID == self.currentChatting.sid) {
console.log(("\n" + self.currentChatting.username + ": " + message).green);
}
/**
* Emitted when a friend message or chat room message is received.
*
* @event BotAccount#friendOrChatMessage
* @type {object}
* @property {SteamID} senderID - The message sender, as a SteamID object
* @property {String} message - The message text
* @property {SteamID} room - The room to which the message was sent. This is the user's SteamID if it was a friend message
*/
self.emit('chatMessage', senderID, message);
});
self.community.on('sessionExpired', function (err) {
self.logger.log('debug', "Login session expired due to " + err);
self.emit('sessionExpired', err);
});
self.TradeOfferManager.on('sentOfferChanged', function (offer, oldState) {
/**
* Emitted when a trade offer changes state (Ex. accepted, pending, escrow, etc...)
*
* @event BotAccount#offerChanged
* @type {object}
* @property {TradeOffer} offer - The new offer's details
* @property {TradeOffer} oldState - The old offer's details
*/
self.emit('offerChanged', offer, oldState);
});
self.TradeOfferManager.on('receivedOfferChanged', function (offer, oldState) {
self.emit('receivedOfferChanged', offer, oldState);
});
self.TradeOfferManager.on('offerList', function (filter, sent, received) {
self.emit('offerList', filter, sent, received);
});
self.TradeOfferManager.on('newOffer', function (offer) {
/**
* Emitted when we receive a new trade offer
*
* @event BotAccount#newOffer
* @type {object}
* @property {TradeOffer} offer - The offer's details
*/
self.emit('newOffer', offer);
});
self.TradeOfferManager.on('sentOfferChanged', function (offer) {
/**
* Emitted when we receive a new trade offer notification (only provides amount of offers and no other details)
*
* @event BotAccount#tradeOffers
* @type {object}
* @property {Integer} count - The amount of active trade offers (can be 0).
*/
self.emit('sentOfferChanged', offer);
});
self.TradeOfferManager.on('realTimeTradeConfirmationRequired', function (offer) {
/**
* Emitted when a trade offer is cancelled
*
* @event BotAccount#tradeOffers
* @type {object}
* @property {Integer} count - The amount of active trade offers (can be 0).
*/
self.emit('realTimeTradeConfirmationRequired', offer);
});
self.TradeOfferManager.on('realTimeTradeCompleted', function (offer) {
/**
* Emitted when a trade offer is cancelled
*
* @event BotAccount#tradeOffers
* @type {object}
* @property {Integer} count - The amount of active trade offers (can be 0).
*/
self.emit('realTimeTradeCompleted', offer);
});
self.TradeOfferManager.on('sentOfferCanceled', function (offer) {
/**
* Emitted when a trade offer is cancelled
*
* @event BotAccount#tradeOffers
* @type {object}
* @property {Integer} count - The amount of active trade offers (can be 0).
*/
self.emit('sentOfferCanceled', offer);
});
/**
* Emitted when we fully sign into Steam and all functions are usable.
*
* @event BotAccount#loggedIn
*/
return loginCallback(undefined);
});
}
BotAccount.prototype.hasPhone = function (callback) {
var self = this;
self.store.hasPhone(function (err, hasPhone, lastDigits) {
callback(err, hasPhone, lastDigits);
});
};
BotAccount.prototype.getStateName = function (state) {
return TradeOfferManager.getStateName(state);
};
BotAccount.prototype.setSetting = function (settingName, tempSettingValusettingName) {
var self = this;
self.settings[tempSetting] = tempSettingValue;
};
BotAccount.prototype.getSetting = function (tempSetting) {
var self = this;
if (self.settings.hasOwnProperty(tempSetting))
return self.settings[tempSetting];
return undefined;
};
BotAccount.prototype.deleteSetting = function (tempSetting) {
var self = this;
if (self.settings.hasOwnProperty(tempSetting))
delete self.settings[tempSetting];
};
BotAccount.prototype.chatLogon = function (interval, uiMode) {
var self = this;
if (interval == undefined)
interval = 500;
if (uiMode == undefined)
uiMode = 'web';
self.logger.log('debug', 'Logged on to chat on %j', self.getAccountName());
self.community.chatLogon(interval, uiMode);
};
BotAccount.prototype.logoutAccount = function () {
var self = this;
self.community = new SteamCommunity();
self.client = new SteamUser();
self.TradeOfferManager = new TradeOfferManager({
"steam": self.client,
"community": self.community,
"cancelTime": self.settings.tradeCancelTime, // Keep offers upto 1 day, and then just cancel them.
"pendingCancelTime": self.settings.tradePendingCancelTime, // Keep offers upto 30 mins, and then cancel them if they still need confirmation
"cancelOfferCount": self.settings.tradeCancelOfferCount,// Cancel offers once we hit 7 day threshold
"cancelOfferCountMinAge": self.settings.tradeCancelOfferCountMinAge,// Keep offers until 7 days old
"language": self.settings.language, // We want English item descriptions
"pollInterval": 5000 // We want to poll every 5 seconds since we don't have Steam notifying us of offers
});
self.request = self.community.request;
self.store = new SteamStore();
self.loggedIn = false;
};
module.exports = BotAccount;