steam-user
Version:
Steam client for Individual and AnonUser Steam account types
310 lines (263 loc) • 9.46 kB
JavaScript
const FileManager = require('file-manager');
const StdLib = require('@doctormckay/stdlib');
const SteamChatRoomClient = require('./components/chatroom.js');
const DefaultOptions = require('./resources/default_options.js');
const Package = require('./package.json');
const SteamUserTwoFactor = require('./components/twofactor.js');
const SteamUserBase = require('./components/00-base.js');
/**
* @class SteamUser
* @extends EventEmitter
* @property {SteamID|null} steamID
* @property {SteamChatRoomClient} chat
* @property {OptionsObject} options
* @property {string|null} publicIP
* @property {number|null} cellID
* @property {string|null} vanityURL
* @property {{name: string, country: string, authedMachines: number, flags: number, facebookID?: string, facebookName?: string}|null} accountInfo
* @property {{address: string, validated: boolean}|null} emailInfo
* @property {{limited: boolean, communityBanned: boolean, locked: boolean, canInviteFriends: boolean}|null} limitations
* @property {{numBans: number, appids: number[]}|null} vac
* @property {{hasWallet: boolean, currency: ECurrencyCode, balance: number}|null} wallet
* @property {Proto_CMsgClientLicenseList_License[]|null} licenses
* @property {{gid: string, packageid: string, TimeCreated: Date, TimeExpiration: Date, TimeSent: Date, TimeAcked: Date, TimeRedeemed: Date|null, RecipientAddress: string, SenderAddress: string, SenderName: string}[]|null} gifts
* @property {Object<string, UserPersona>} users
* @property {Object<string, GroupPersona>} groups
* @property {Object<string, EFriendRelationship>|null} myFriends
* @property {Object<string, EClanRelationship>|null} myGroups
* @property {Object<string, {name: string, members: SteamID[]}>|null} myFriendGroups
* @property {Object<string, string>|null} myNicknames
* @property {{changenumber: number, apps: object, packages: object}} picsCache
*/
class SteamUser extends SteamUserTwoFactor {
/**
* @param {OptionsObject} [options={}]
*/
constructor(options) {
super();
this.steamID = null;
this.chat = new SteamChatRoomClient(this);
this._initProperties();
this._connectTimeout = 1000;
this._initialized = false;
this._multiCount = 0;
// App and package cache
this._changelistUpdateTimer = null;
this.picsCache = {
changenumber: 0,
apps: {},
packages: {}
};
this.options = {};
for (let i in (options || {})) {
this._setOption(i, options[i]);
}
for (let i in DefaultOptions) {
if (typeof this.options[i] === 'undefined') {
this._setOption(i, DefaultOptions[i]);
}
}
this._checkOptionTypes();
if (!this.options.dataDirectory && this.options.dataDirectory !== null) {
if (process.env.OPENSHIFT_DATA_DIR) {
this.options.dataDirectory = process.env.OPENSHIFT_DATA_DIR + '/node-steamuser';
} else {
this.options.dataDirectory = StdLib.OS.appDataDirectory({
appName: 'node-steamuser',
appAuthor: 'doctormckay'
});
}
}
if (this.options.dataDirectory) {
this.storage = new FileManager(this.options.dataDirectory);
}
this._initialized = true;
}
_initProperties(isConnecting) {
// Account info
this.limitations = null;
this.vac = null;
this.wallet = null;
this.emailInfo = null;
this.licenses = null;
this.gifts = null;
// Friends and users info
this.users = {};
this.groups = {};
this.chats = {};
this.myFriends = {};
this.myGroups = {};
this.myFriendGroups = {};
this.myNicknames = {};
this.steamServers = {};
this.contentServersReady = false;
this.playingState = {blocked: false, appid: 0};
this._playingBlocked = false;
this._playingAppIds = [];
this._gcTokens = []; // game connect tokens
this._activeAuthTickets = [];
this._connectTime = 0;
this._connectionCount = 0;
this._authSeqMe = 0;
this._authSeqThem = 0;
this._hSteamPipe = Math.floor(Math.random() * 1000000) + 1;
this._contentServerCache = {};
this._contentServerTokens = {};
this._lastNotificationCounts = {};
this._sessionID = 0;
this._currentJobID = 0;
this._currentGCJobID = 0;
this._jobs = new StdLib.DataStructures.TTLCache(1000 * 60 * 2); // job callbacks are cleaned up after 2 minutes
this._jobsGC = new StdLib.DataStructures.TTLCache(1000 * 60 * 2);
this._richPresenceLocalization = {};
this._incomingMessageQueue = [];
this._useMessageQueue = false; // we only use the message queue while we're processing a multi message
this._ttlCache = new StdLib.DataStructures.TTLCache(1000 * 60 * 5); // default 5 minutes
this._getCmListAttempts = 0;
this._resetAllExponentialBackoffs(isConnecting);
delete this._machineAuthToken;
delete this._shouldAttemptRefreshTokenRenewal;
delete this._loginSession;
delete this._connectionClosed;
clearTimeout(this._reconnectForCloseDuringAuthTimeout);
delete this._reconnectForCloseDuringAuthTimeout;
}
get packageName() {
return Package.name;
}
get packageVersion() {
return Package.version;
}
/**
* Set a configuration option.
* @param {string} option
* @param {*} value
*/
setOption(option, value) {
this._setOption(option, value);
this._checkOptionTypes();
}
/**
* Set one or more configuration options
* @param {OptionsObject} options
*/
setOptions(options) {
for (let i in options) {
this._setOption(i, options[i]);
}
this._checkOptionTypes();
}
/**
* Actually commit an option change. This is a separate method since user-facing methods need to be able to call
* _checkOptionTypes() but we also want to be able to change options internally without calling it.
* @param {string} option
* @param {*} value
* @private
*/
_setOption(option, value) {
this.options[option] = value;
// Handle anything that needs to happen when particular options update
switch (option) {
case 'dataDirectory':
if (this._initialized) {
if (!this.storage) {
this.storage = new FileManager(value);
} else {
this.storage.directory = value;
}
}
break;
case 'enablePicsCache':
if (this._initialized) {
this._resetChangelistUpdateTimer();
this._getLicenseInfo();
}
break;
case 'changelistUpdateInterval':
if (this._initialized) {
this._resetChangelistUpdateTimer();
}
break;
case 'webCompatibilityMode':
case 'protocol':
if (
(option == 'webCompatibilityMode' && value && this.options.protocol == SteamUser.EConnectionProtocol.TCP) ||
(option == 'protocol' && value == SteamUser.EConnectionProtocol.TCP && this.options.webCompatibilityMode)
) {
this._warn('webCompatibilityMode is enabled so connection protocol is being forced to WebSocket');
}
break;
case 'httpProxy':
if (typeof this.options.httpProxy == 'string' && !this.options.httpProxy.includes('://')) {
this.options.httpProxy = 'http://' + this.options.httpProxy;
}
break;
}
}
/**
* Make sure that the types of all options are valid.
* @private
*/
_checkOptionTypes() {
// We'll infer types from DefaultOptions, but stuff that's null (for example) needs to be defined explicitly
let types = {
socksProxy: 'string',
httpProxy: 'string',
localAddress: 'string',
localPort: 'number',
machineIdFormat: 'array'
};
for (let opt in DefaultOptions) {
if (types[opt]) {
// already specified
continue;
}
types[opt] = typeof DefaultOptions[opt];
}
for (let opt in this.options) {
if (!types[opt]) {
// no type specified for this option, so bail
continue;
}
let requiredType = types[opt];
let providedType = typeof this.options[opt];
if (providedType == 'object' && Array.isArray(this.options[opt])) {
providedType = 'array';
} else if (requiredType == 'number' && providedType == 'string' && !isNaN(this.options[opt])) {
providedType = 'number';
this.options[opt] = parseFloat(this.options[opt]);
}
if (this.options[opt] !== null && requiredType != providedType) {
this._warn(`Incorrect type '${providedType}' provided for option ${opt}, '${requiredType}' expected. Resetting to default value ${DefaultOptions[opt]}`);
this._setOption(opt, DefaultOptions[opt]);
}
}
}
/**
* Issue a warning
* @param msg
* @private
*/
_warn(msg) {
process.emitWarning(msg, 'Warning', 'steam-user');
}
}
// Just in case any legacy code is directly accessing these (even though they shouldn't be since they're underscore-prefixed),
// let's add references to them directly to this prototype.
SteamUser.prototype._handlerManager = SteamUserBase.prototype._handlerManager;
// Also add legacy handler object for HandlerManager to look at
SteamUser.prototype._handlers = {};
// Tack on our extra enums
SteamUser.CurrencyData = require('./resources/CurrencyData.js');
SteamUser.EClientUIMode = require('./resources/EClientUIMode.js');
SteamUser.EConnectionProtocol = require('./resources/EConnectionProtocol.js');
SteamUser.EMachineIDType = require('./resources/EMachineIDType.js');
SteamUser.EPurchaseResult = require('./resources/EPurchaseResult.js');
SteamUser.EPrivacyState = require('./resources/EPrivacyState.js');
// Export the SteamUser class before we require components that demand it
module.exports = SteamUser;
/**
* Called when the request completes.
* @callback SteamUser~genericEResultCallback
* @param {Error|null} err - Error object on failure (with eresult property), null on success (represents EResult 1 - OK)
*/