From 9e1815fc44b81d07db127a2ac9f005d6666bb0cb Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 5 Nov 2017 01:58:25 -0500 Subject: [PATCH 01/36] Require node v6 or later --- lib/index.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index bc48711..8da8fb1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -203,7 +203,7 @@ TradeOfferManager.prototype._persistToDisk = function(filename, content) { } if (typeof content === 'string') { - content = new Buffer(content, 'utf8'); + content = Buffer.from(content, 'utf8'); } if (this._dataGzip) { diff --git a/package.json b/package.json index b92bf6d..6cf93ed 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,6 @@ "steamid": "^1.1.0" }, "engines": { - "node": ">=4.0.0" + "node": ">=6.0.0" } } From f2236280f9301bf8d6ad356dea854b7a03d3bbb2 Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 5 Nov 2017 01:55:33 -0500 Subject: [PATCH 02/36] Use @doctormckay/stdlib for LeastUsedCache --- lib/index.js | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 8da8fb1..099d118 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,12 +2,13 @@ require('@doctormckay/stats-reporter').setup(require('../package.json')); -const SteamCommunity = require('steamcommunity'); const AppDirectory = require('appdirectory'); const Async = require('async'); const FileManager = require('file-manager'); -const LeastUsedCache = require('@doctormckay/leastused-cache'); +const StdLib = require('@doctormckay/stdlib'); +const SteamCommunity = require('steamcommunity'); const Zlib = require('zlib'); + const Helpers = require('./helpers.js'); module.exports = TradeOfferManager; @@ -42,10 +43,10 @@ function TradeOfferManager(options) { this._dataGzip = options.gzipData; if (options.globalAssetCache) { - global._steamTradeOfferManagerAssetCache = global._steamTradeOfferManagerAssetCache || new LeastUsedCache(500, 120000); + global._steamTradeOfferManagerAssetCache = global._steamTradeOfferManagerAssetCache || new StdLib.DataStructures.LeastUsedCache(500, 120000); this._assetCache = global._steamTradeOfferManagerAssetCache; } else { - this._assetCache = new LeastUsedCache(500, 120000); + this._assetCache = new StdLib.DataStructures.LeastUsedCache(500, 120000); } // Set up disk persistence diff --git a/package.json b/package.json index 6cf93ed..bf3aa69 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "update-resources": "node scripts/update-resources.js" }, "dependencies": { - "@doctormckay/leastused-cache": "^1.0.0", "@doctormckay/stats-reporter": "^1.0.3", + "@doctormckay/stdlib": "^1.1.0", "appdirectory": "^0.1.0", "async": "^2.5.0", "deep-equal": "^1.0.1", From aafce94d459963cd134474dd33c7411b93dcd239 Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 05:08:22 -0500 Subject: [PATCH 03/36] Added TradeSession --- lib/classes/TradeOffer.js | 70 +-- lib/classes/TradeSession.js | 769 +++++++++++++++++++++++++++++++ lib/helpers.js | 97 +++- lib/index.js | 9 +- resources/ETradeSessionAction.js | 17 + resources/ETradeSessionStatus.js | 17 + 6 files changed, 898 insertions(+), 81 deletions(-) create mode 100644 lib/classes/TradeSession.js create mode 100644 resources/ETradeSessionAction.js create mode 100644 resources/ETradeSessionStatus.js diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index 002bced..ed114bd 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -721,75 +721,7 @@ TradeOffer.prototype.getUserDetails = function(callback) { } } - this.manager._community.httpRequestGet(url, (err, response, body) => { - if (err || response.statusCode != 200) { - Helpers.makeAnError(err || new Error("HTTP error " + response.statusCode), callback); - return; - } - - var script = body.match(/\n\W*"); - if (pos != -1) { - script = script.substring(0, pos); - } - - // Run this script in a VM - var vmContext = require('vm').createContext({ - "UserYou": { - "SetProfileURL": function() { }, - "SetSteamId": function() { } - }, - "UserThem": { - "SetProfileURL": function() { }, - "SetSteamId": function() { } - }, - "$J": function() { } - }); - - require('vm').runInContext(script, vmContext); - - var me = { - "personaName": vmContext.g_strYourPersonaName, - "contexts": vmContext.g_rgAppContextData - }; - - var them = { - "personaName": vmContext.g_strTradePartnerPersonaName, - "contexts": vmContext.g_rgPartnerAppContextData, - "probation": vmContext.g_bTradePartnerProbation - }; - - // Escrow - var myEscrow = body.match(/var g_daysMyEscrow = (\d+);/); - var theirEscrow = body.match(/var g_daysTheirEscrow = (\d+);/); - if (myEscrow && theirEscrow) { - me.escrowDays = parseInt(myEscrow[1], 10); - them.escrowDays = parseInt(theirEscrow[1], 10); - } - - // Avatars - var myAvatar = body.match(new RegExp('[^')); - var theirAvatar = body.match(new RegExp('[^')); - if (myAvatar) { - me.avatarIcon = myAvatar[1]; - me.avatarMedium = myAvatar[1].replace('.jpg', '_medium.jpg'); - me.avatarFull = myAvatar[1].replace('.jpg', '_full.jpg'); - } - - if (theirAvatar) { - them.avatarIcon = theirAvatar[1]; - them.avatarMedium = theirAvatar[1].replace('.jpg', '_medium.jpg'); - them.avatarFull = theirAvatar[1].replace('.jpg', '_full.jpg'); - } - - callback(null, me, them); - }); + Helpers.getUserDetailsFromTradeWindow(this.manager, url, callback); }; TradeOffer.prototype.counter = function() { diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js new file mode 100644 index 0000000..44f525f --- /dev/null +++ b/lib/classes/TradeSession.js @@ -0,0 +1,769 @@ +"use strict"; + +// External modules +const EventEmitter = require('events').EventEmitter; +const StdLib = require('@doctormckay/stdlib'); +const Util = require('util'); + +// Internal modules/classes +const SteamID = require('steamid'); +const EconItem = require('./EconItem.js'); +const Helpers = require('../helpers.js'); +const TradeOfferManager = require('../index.js'); + +// Resources +const ETradeSessionAction = require('../../resources/ETradeSessionAction.js'); +const ETradeSessionStatus = require('../../resources/ETradeSessionStatus.js'); + +Util.inherits(TradeSession, EventEmitter); + +/** + * Open a started real-time trade session. TradeOfferManager cannot *start* a trade session, it can only open one. + * To start a trade session, see the `trade` method of `steam-user` (https://www.npmjs.com/package/steam-user). + * @param {string|SteamID} partner + * @param {function} callback - First arg is {Error|null}, second is a {TradeSession} + */ +TradeOfferManager.prototype.openTradeSession = function(partner, callback) { + if (typeof partner === 'string') { + partner = new SteamID(partner); + } + + Helpers.getUserDetailsFromTradeWindow(this, `https://steamcommunity.com/trade/${partner.getSteamID64()}`, (err, me, them) => { + if (err) { + callback(err); + return; + } + + callback(null, new TradeSession(this, partner, me, them)); + }); +}; + +function TradeSession(manager, partner, detailsMe, detailsThem) { + if (typeof partner === 'string') { + this.partner = new SteamID(partner); + } else { + this.partner = partner; + } + + this.itemsToGive = []; + this.itemsToReceive = []; + + this.me = detailsMe; + this.them = detailsThem; + + this.me.ready = false; + this.me.confirmed = false; + this.them.ready = false; + this.them.confirmed = false; + + this.pollInterval = 1000; // ms + + this._manager = manager; + this._pollInFlight = false; + this._ignoreNextPoll = false; + this._ended = false; + this._statusFailures = 0; + this._version = 1; + this._logPos = 0; + this._tradeStatusPoll = null; + this._myInventory = {}; + this._myInventoryLoading = {}; + this._myItemsInTradeOrder = []; + this._theirInventory = {}; + this._theirInventoryLoading = {}; + this._theirItemsInTradeOrder = []; + this._usedSlots = []; + this._cmdQueue = new StdLib.DataStructures.AsyncQueue((cmd, callback) => { + this._doCommand(cmd.command, cmd.args || {}, cmd.tryCount, callback); + }); + + this._enqueueTradeStatusPoll(); +} + +/** + * Get the contents of your own inventory. Identical to TradeOfferManager's getInventoryContents, but you should use this + * because it populates the local trade session cache. + * @param {int} appid + * @param {int} contextid + * @param {function} callback + */ +TradeSession.prototype.getInventory = function(appid, contextid, callback) { + let invKey = `${appid}_${contextid}`; + if (this._myInventory[invKey]) { + callback(null, this._myInventory[invKey]); + return; + } + + if (this._myInventoryLoading[invKey]) { + this._myInventoryLoading[invKey].push(callback); + return; + } + + this._myInventoryLoading[invKey] = []; + + let doAttempt = (attemptNum) => { + if (attemptNum > 3) { + let err = new Error(`Cannot get our inventory for appid ${appid} contextid ${contextid}`); + callback(err); + this._myInventoryLoading[invKey].forEach(cb => cb(err)); + return; + } + + this.emit('debug', 'Getting our inventory ' + invKey); + this._manager.getInventoryContents(appid, contextid, true, (err, inv) => { + if (err) { + this.emit('debug', 'Cannot get my inventory: ' + err.message); + setTimeout(() => doAttempt(attemptNum + 1), 500); + return; + } + + this._myInventory[invKey] = inv; + callback(null, inv); + this._myInventoryLoading[invKey].forEach(cb => cb(null, inv)); + }); + }; + + doAttempt(1); +}; + +/** + * Set yourself as ready (check the blue box). + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.ready = function(callback) { + this._cmdQueue.push({ + "command": "toggleready", + "args": { + "ready": "true" + }, + "tryCount": 5 + }, callback); +}; + +/** + * Set yourself as not ready (uncheck the blue box). + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.unready = function(callback) { + this._cmdQueue.push({ + "command": "toggleready", + "args": { + "ready": "false" + }, + "tryCount": 5 + }, callback); +}; + +/** + * Confirm the trade. Only has an effect if both parties are readied up. + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.confirm = function(callback) { + this._cmdQueue.push({ + "command": "confirm", + "tryCount": 5 + }, callback); +}; + +/** + * Send a chat message. This is enqueued and sent one at a time, so it's safe to call this multiple times in rapid succession. + * @param {string} msg + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.chat = function(msg, callback) { + this._cmdQueue.push({ + "command": "chat", + "args": { + "message": msg + }, + "tryCount": 3 + }, callback); +}; + +/** + * Adds an item to the trade. The trade is terminated if this fails. + * @param {object} item - Needs to have properties: appid, contextid, and either assetid or id + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.addItem = function(item, callback) { + this._cmdQueue.push({ + "command": "additem", + "args": { + "appid": item.appid, + "contextid": item.contextid, + "itemid": item.assetid || item.id, + "slot": this._getNextSlot() + }, + "tryCount": 5 + }, (err) => { + if (err) { + callback && callback(err); + this._terminateWithError(`Cannot add item ${item.appid}_${item.contextid}_${item.assetid || item.id} to trade`); + } else { + callback && callback(null); + } + }); +}; + +/** + * Removes an item from the trade. The trade is terminated if this fails. + * @param {object} item - Needs to have properties: appid, contextid, and either assetid or id + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.removeItem = function(item, callback) { + this._cmdQueue.push({ + "command": "removeitem", + "args": { + "appid": item.appid, + "contextid": item.contextid, + "itemid": item.assetid || item.id + }, + "tryCount": 5 + }, (err) => { + if (err) { + callback && callback(err); + this._terminateWithError(`Cannot remove item ${item.appid}_${item.contextid}_${item.assetid || item.id} from trade`); + } else { + callback && callback(null); + } + }); +}; + +/** + * Cancel the trade session. The `end` event will be emitted if this succeeds. + * @param {function} [callback] - Just gets an error + */ +TradeSession.prototype.cancel = function(callback) { + this._cmdQueue.push({"command": "cancel", "tryCount": 5}, callback); +}; + +TradeSession.prototype._getTradeStatus = function() { + if (this._pollInFlight || this._ended) { + return; + } + + this._pollInFlight = true; + this._clearTradeStatusPoll(); + this._doCommand('tradestatus', {}, 1, (err) => { + this._pollInFlight = false; + this._enqueueTradeStatusPoll(); + + if (err) { + this._statusFailures++; + this._onTradeStatusFailure(); + } else { + this._statusFailures = 0; + } + }, "tradeoffermanager"); +}; + +TradeSession.prototype._onTradeStatusFailure = function() { + if (!this._ended && ++this._statusFailures >= 5) { + this._setEnded(); + this.emit('end', ETradeSessionStatus.TimedOut); + } +}; + +TradeSession.prototype._clearTradeStatusPoll = function() { + if (this._tradeStatusPoll) { + clearTimeout(this._tradeStatusPoll); + this._tradeStatusPoll = null; + } +}; + +TradeSession.prototype._enqueueTradeStatusPoll = function() { + if (this._ended) { + return; + } + + this._clearTradeStatusPoll(); + this._tradeStatusPoll = setTimeout(() => this._getTradeStatus(), this.pollInterval); +}; + +TradeSession.prototype._handleTradeStatus = function(status) { + if (this._ended || !status.success) { + return; + } + + if (this._pollInFlight) { + // we got data from a non-poll request, so the poll that's in flight will be stale. just ignore it. + this._ignoreNextPoll = true; + } + + if (status.trade_status > ETradeSessionStatus.TurnedIntoTradeOffer) { + // Unknown trade status + this._setEnded(); + this.emit('error', new Error("Unknown trade session status: " + status.trade_status)); + return; + } else if (status.trade_status == ETradeSessionStatus.TurnedIntoTradeOffer) { + this._setEnded(); + this._getTradeOffer(status.tradeid, 1); + return; + } else if (status.trade_status > ETradeSessionStatus.Active) { + // the trade is over, yo + this._setEnded(); + this.emit('end', status.trade_status); + return; + } + + if (status.me && status.me.assets) { + this._usedSlots = Object.keys(status.me.assets).map(key => parseInt(key, 10)); + } + + if (status.me && status.them) { + if (status.me.assets) { + this._myItemsInTradeOrder = fixItemArray(status.me.assets); + } + if (status.them.assets) { + this._theirItemsInTradeOrder = fixItemArray(status.them.assets); + } + } + + // the trade session is active + // process events + if (status.events) { + let eventKeys = Object.keys(status.events).map(key => parseInt(key, 10)); + eventKeys.forEach((key) => { + if (key < this._logPos) { + this.emit('debug', 'Ignoring event ' + key + '; logPos is ' + this._logPos); + return; + } + + let event = status.events[key]; + let isUs = event.steamid == this._manager.steamID.getSteamID64(); + this.emit('debug', 'Handling event ' + event.action + ' (' + (ETradeSessionAction[event.action] || event.action) + ')'); + + switch (parseInt(event.action, 10)) { + case ETradeSessionAction.AddItem: + this.me.ready = false; + this.them.ready = false; + + if (isUs) { + this.getInventory(event.appid, event.contextid, (err, inv) => { + if (err) { + return this._terminateWithError("Cannot get my inventory: " + err.message); + } + + let item = inv.filter(item => item.assetid == event.assetid)[0]; + if (!item) { + this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in our inventory even though it was added to the trade`); + } else if (this.itemsToGive.some(tradeItem => Helpers.itemEquals(tradeItem, item))) { + // this item is already in the trade, do nothing + } else { + // this item was added to the trade + this.itemsToGive.push(item); + this._fixAssetOrder(); + } + }); + } else { + this._getTheirInventory(event.appid, event.contextid, (err, inv) => { + if (err) { + return this._terminateWithError("Cannot get partner inventory: " + err.message); + } + + let item = inv.filter(item => item.assetid == event.assetid)[0]; + if (!item) { + this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in partner's inventory even though it was added to the trade`); + } else if (this.itemsToReceive.some(tradeItem => Helpers.itemEquals(tradeItem, item))) { + // item is already in the trade + } else { + this.itemsToReceive.push(item); + this._fixAssetOrder(); + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('itemAdded', item); + }); + } + }); + } + + break; + + case ETradeSessionAction.RemoveItem: + this.me.ready = false; + this.them.ready = false; + + let itemArray = isUs ? this.itemsToGive : this.itemsToReceive; + for (let i = 0; i < itemArray.length; i++) { + if (!Helpers.itemEquals(itemArray[i], event)) { + continue; + } + + // we found it + let item = itemArray.splice(i, 1)[0]; + if (!isUs) { + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('itemRemoved', item); + }); + } + } + + break; + + case ETradeSessionAction.Ready: + if (isUs) { + this.me.ready = true; + } else { + this.them.ready = true; + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('ready'); + }); + } + + break; + + case ETradeSessionAction.Unready: + if (isUs) { + this.me.ready = false; + } else { + this.them.ready = false; + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('unready'); + }); + } + + break; + + case ETradeSessionAction.Confirm: + if (isUs) { + this.me.confirmed = true; + } else { + this.them.confirmed = true; + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('confirm'); + }); + } + + break; + + case ETradeSessionAction.Chat: + if (isUs) { + break; // don't care + } else { + this.emit('chat', event.text); + } + + break; + + default: + this.emit('debug', 'Unknown event ' + (ETradeSessionAction[event.action] || event.action)); + } + + if (this._logPos <= key) { + this._logPos = key + 1; + } + }); + } + + // all events have been processed. do some sanity checks to make sure we aren't out of sync + if (status.version && status.version > this._version) { + this.emit('debug', `Got new version ${status.version} (had ${this._version})`); + this._version = status.version; + } + + if (status.me && status.them) { + ['ready', 'confirmed'].forEach((thingToCheck) => { + for (let i = 0; i < 2; i++) { + if (this._ended) { + return; + } + + let who = i == 0 ? 'me' : 'them'; + let local = this[who][thingToCheck]; + let remote = status[who][thingToCheck]; + + if (local != remote) { + return this._terminateWithError(`Trade got out of sync. Local ${thingToCheck} status for ${who} is ${local} but we got ${remote}`); + } + } + }); + + // Assets? Only check if we aren't loading some inventory + if (!this._loadingInventory()) { + for (let i = 0; i < 2; i++) { + if (this._ended) { + return; + } + + let who = i == 0 ? 'me' : 'them'; + let remoteAssets = status[who].assets && fixItemArray(status[who].assets); + let localAssets = i == 0 ? this.itemsToGive : this.itemsToReceive; + + if (remoteAssets) { + if (remoteAssets.length != localAssets.length) { + return this._terminateWithError(`Trade got out of sync. Local asset count for ${who} is ${localAssets.length} but we got ${remoteAssets.length}`); + } + + // make sure the assets match up + localAssets.forEach((localAsset) => { + if (this._ended) { + return; + } + + // is this asset in the remote list? + if (!remoteAssets.some(remoteAsset => Helpers.itemEquals(localAsset, remoteAsset))) { + return this._terminateWithError(`Trade got out of sync. Couldn't find local asset ${localAsset.appid}_${localAsset.contextid}_${localAsset.assetid || localAsset.id} in remote list`); + } + }); + + // no need to check the remote list because we've already checked that the list lengths are the same + // if there was something in remote that we don't have, we'd have noticed since some item that's not in remote + // would need to be in the local list to make the lengths match + } + } + } + } + + function fixItemArray(obj) { + let keys = Object.keys(obj).map(key => parseInt(key, 10)); + keys.sort(); + let vals = []; + keys.forEach(key => vals.push(obj[key])); + return vals; + } +}; + +TradeSession.prototype._getTradeOffer = function(offerID, attemptNum) { + if (attemptNum > 5) { + this.emit('end', ETradeSessionStatus.TurnedIntoTradeOffer, offerID, null); + return; + } + + this._manager.getOffer(offerID, (err, offer) => { + if (err) { + this.emit('debug', 'Error getting trade offer ' + offerID + ': ' + err.message); + setTimeout(() => this._getTradeOffer(offerID, attemptNum + 1), 500); + return; + } + + this.emit('end', ETradeSessionStatus.TurnedIntoTradeOffer, offerID, offer); + }); +}; + +TradeSession.prototype._setEnded = function() { + this._ended = true; + this._clearTradeStatusPoll(); +}; + +TradeSession.prototype._terminateWithError = function(msg) { + this._setEnded(); + this.emit('error', new Error(msg)); +}; + +TradeSession.prototype._getTheirInventory = function(appid, contextid, callback) { + let invKey = `${appid}_${contextid}`; + if (this._theirInventory[invKey]) { + callback(null, this._theirInventory[invKey]); + return; + } + + if (this._theirInventoryLoading[invKey]) { + this._theirInventoryLoading[invKey].push(callback); + return; + } + + this._theirInventoryLoading[invKey] = []; + + let doAttempt = (attemptNum) => { + if (attemptNum > 3) { + let err = new Error(`Cannot get partner inventory for appid ${appid} contextid ${contextid}`); + callback(err); + this._theirInventoryLoading[invKey].forEach(cb => cb(err)); + return; + } + + this.emit('debug', 'Getting their inventory ' + invKey); + this._manager._community.httpRequestGet(`https://steamcommunity.com/trade/${this.partner.getSteamID64()}/foreigninventory/`, { + "qs": { + "sessionid": this._manager._community.getSessionID(), + "steamid": this.partner.getSteamID64(), + "appid": appid, + "contextid": contextid + }, + "headers": { + "Referer": `https://steamcommunity.com/trade/${this.partner.getSteamID64()}/foreigninventory/`, + "X-Requested-With": "XMLHttpRequest" + }, + "json": true + }, (err, res, body) => { + if (err || res.statusCode != 200) { + this.emit('debug', 'Error getting partner inventory: ' + (err ? err.message : res.statusCode)); + setTimeout(() => doAttempt(attemptNum + 1), 500); + return; + } + + if (!body.success || !body.rgInventory || !body.rgDescriptions) { + this.emit('debug', 'Error getting partner inventory: no success/rgInventory/rgDescriptions'); + setTimeout(() => doAttempt(attemptNum + 1), 500); + return; + } + + let inv = []; + for (let i in body.rgInventory) { + if (body.rgInventory.hasOwnProperty(i)) { + inv.push(body.rgInventory[i]); + } + } + + // gottem + let stahp = false; + inv = inv.map((item) => { + if (stahp) { + return null; + } + + item.appid = appid; + item.contextid = contextid; + item.assetid = item.id = item.id || item.assetid; + + let descKey = `${item.classid}_${item.instanceid}`; + if (!body.rgDescriptions[descKey]) { + stahp = true; + this.emit('debug', 'Error getting partner inventory: missing description'); + setTimeout(() => doAttempt(attemptNum + 1), 500); + return null; + } + + let desc = body.rgDescriptions[descKey]; + for (let i in desc) { + if (desc.hasOwnProperty(i)) { + item[i] = desc[i]; + } + } + + return new EconItem(item); + }); + + if (stahp) { + return; + } + + this._theirInventory[invKey] = inv; + callback(null, inv); + this._theirInventoryLoading[invKey].forEach(cb => cb(null, inv)); + }, "tradeoffermanager"); + }; + + doAttempt(1); +}; + +TradeSession.prototype._doCommand = function(command, args, tryCount, callback) { + args = args || {}; + tryCount = tryCount || 3; + + args.sessionid = this._manager._community.getSessionID(); + args.logpos = this._logPos; + args.version = this._version; + + if (command == "additem") { + args.slot = this._getNextSlot(); + } + + let lastError = null; + + let doAttempt = (attemptNum) => { + if (attemptNum > tryCount) { + callback && callback(lastError); + return; + } + + this._manager._community.httpRequestPost(`https://steamcommunity.com/trade/${this.partner.getSteamID64()}/${command}/`, { + "form": args, + "headers": { + "Referer": `https://steamcommunity.com/trade/${this.partner.getSteamID64()}`, + "X-Requested-With": "XMLHttpRequest" + }, + "json": true + }, (err, res, body) => { + if (err || res.statusCode != 200) { + lastError = err ? err : new Error('HTTP error ' + res.statusCode); + this.emit('debug', 'Cannot do command ' + command + ': ' + (err ? err.message : res.statusCode)); + setTimeout(() => doAttempt(attemptNum + 1), 500); + return; + } + + if (!body.success) { + lastError = new Error('No success in response'); + this.emit('debug', 'Cannot do command ' + command + ': no success'); + setTimeout(() => doAttempt(attemptNum + 1), 500); + return; + } + + if (command == 'tradestatus' && this._ignoreNextPoll) { + this._ignoreNextPoll = false; + } else { + this._handleTradeStatus(body); + } + + callback && callback(null); + }, "tradeoffermanager"); + }; + + doAttempt(1); +}; + +TradeSession.prototype._getNextSlot = function() { + for (let i = 0; i < 1000000; i++) { + if (!this._usedSlots.includes(i)) { + return i; + } + } + + throw new Error('wtf a million items'); +}; + +TradeSession.prototype._loadingInventory = function() { + for (let i in this._theirInventoryLoading) { + if (this._theirInventoryLoading.hasOwnProperty(i) && this._theirInventoryLoading[i] && !this._theirInventory[i]) { + return true; + } + } + + for (let i in this._myInventoryLoading) { + if (this._myInventoryLoading.hasOwnProperty(i) && this._myInventoryLoading[i] && !this._myInventoryLoading[i]) { + return true; + } + } + + return false; +}; + +TradeSession.prototype._fixAssetOrder = function() { + let itemsToGive = []; + let itemsToReceive = []; + + this._myItemsInTradeOrder.forEach((itemToFind) => { + let item = this.itemsToGive.filter(itemInTrade => Helpers.itemEquals(itemToFind, itemInTrade)); + if (!item[0]) { + return; + } + + itemsToGive.push(item[0]); + }); + + this._theirItemsInTradeOrder.forEach((itemToFind) => { + let item = this.itemsToReceive.filter(itemInTrade => Helpers.itemEquals(itemToFind, itemInTrade)); + if (!item[0]) { + return; + } + + itemsToReceive.push(item[0]); + }); + + this.itemsToGive = itemsToGive; + this.itemsToReceive = itemsToReceive; +}; diff --git a/lib/helpers.js b/lib/helpers.js index 644d665..2a98c62 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,17 +1,20 @@ "use strict"; const SteamID = require('steamid'); +const VM = require('vm'); const EResult = require('../resources/EResult.js'); const EconItem = require('./classes/EconItem.js'); const TradeOffer = require('./classes/TradeOffer.js'); const EConfirmationMethod = require('../resources/EConfirmationMethod.js'); -exports.itemEquals = function(a, b) { +const Helpers = module.exports; + +Helpers.itemEquals = function(a, b) { return a.appid == b.appid && a.contextid == b.contextid && (a.assetid || a.id) == (b.assetid || b.id); }; -exports.makeAnError = function(error, callback, body) { +Helpers.makeAnError = function(error, callback, body) { if (callback) { if (body && body.strError) { error = new Error(body.strError); @@ -61,17 +64,17 @@ function offerSuperMalformed(offer) { function offerMalformed(offer) { return offerSuperMalformed(offer) || ((offer.items_to_give || []).length == 0 && (offer.items_to_receive || []).length == 0); -}; +} function processItems(items) { return items.map(item => new EconItem(item)); -}; +} -exports.offerSuperMalformed = offerSuperMalformed; -exports.offerMalformed = offerMalformed; -exports.processItems = processItems; +Helpers.offerSuperMalformed = offerSuperMalformed; +Helpers.offerMalformed = offerMalformed; +Helpers.processItems = processItems; -exports.checkNeededDescriptions = function(manager, offers, callback) { +Helpers.checkNeededDescriptions = function(manager, offers, callback) { if (!manager._language) { callback(null); return; @@ -94,7 +97,7 @@ exports.checkNeededDescriptions = function(manager, offers, callback) { manager._requestDescriptions(items, callback); }; -exports.createOfferFromData = function(manager, data) { +Helpers.createOfferFromData = function(manager, data) { var offer = new TradeOffer(manager, new SteamID('[U:1:' + data.accountid_other + ']')); offer.id = data.tradeofferid.toString(); offer.message = data.message; @@ -121,3 +124,79 @@ exports.createOfferFromData = function(manager, data) { return offer; }; + +Helpers.getUserDetailsFromTradeWindow = function(manager, url, callback) { + manager._community.httpRequestGet(url, (err, response, body) => { + if (err || response.statusCode != 200) { + Helpers.makeAnError(err || new Error("HTTP error " + response.statusCode), callback); + return; + } + + let script = body.match(/\n\W*"); + if (pos != -1) { + script = script.substring(0, pos); + } + + // Run this script in a VM + let vmContext = VM.createContext({ + "UserYou": { + "SetProfileURL": function() { }, + "SetSteamId": function() { } + }, + "UserThem": { + "SetProfileURL": function() { }, + "SetSteamId": function() { } + }, + "$J": function() { }, + "Event": { + "observe": function() { } + }, + "document": null + }); + + VM.runInContext(script, vmContext); + + let me = { + "personaName": vmContext.g_strYourPersonaName, + "contexts": vmContext.g_rgAppContextData + }; + + let them = { + "personaName": vmContext.g_strTradePartnerPersonaName, + "contexts": vmContext.g_rgPartnerAppContextData || null, + "probation": vmContext.g_bTradePartnerProbation + }; + + // Escrow + let myEscrow = body.match(/var g_daysMyEscrow = (\d+);/); + let theirEscrow = body.match(/var g_daysTheirEscrow = (\d+);/); + if (myEscrow && theirEscrow) { + me.escrowDays = parseInt(myEscrow[1], 10); + them.escrowDays = parseInt(theirEscrow[1], 10); + } + + // Avatars + let myAvatar = body.match(new RegExp('[^')); + let theirAvatar = body.match(new RegExp('[^')); + if (myAvatar) { + me.avatarIcon = myAvatar[1]; + me.avatarMedium = myAvatar[1].replace('.jpg', '_medium.jpg'); + me.avatarFull = myAvatar[1].replace('.jpg', '_full.jpg'); + } + + if (theirAvatar) { + them.avatarIcon = theirAvatar[1]; + them.avatarMedium = theirAvatar[1].replace('.jpg', '_medium.jpg'); + them.avatarFull = theirAvatar[1].replace('.jpg', '_full.jpg'); + } + + callback(null, me, them); + }); +}; diff --git a/lib/index.js b/lib/index.js index 099d118..0702ceb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,12 +13,13 @@ const Helpers = require('./helpers.js'); module.exports = TradeOfferManager; -const SteamID = TradeOfferManager.SteamID = require('steamid'); -const ETradeOfferState = TradeOfferManager.ETradeOfferState = require('../resources/ETradeOfferState.js'); +const EConfirmationMethod = TradeOfferManager.EConfirmationMethod = require('../resources/EConfirmationMethod.js'); const EOfferFilter = TradeOfferManager.EOfferFilter = require('../resources/EOfferFilter.js'); const EResult = TradeOfferManager.EResult = require('../resources/EResult.js'); -const EConfirmationMethod = TradeOfferManager.EConfirmationMethod = require('../resources/EConfirmationMethod.js'); +const ETradeOfferState = TradeOfferManager.ETradeOfferState = require('../resources/ETradeOfferState.js'); +const ETradeSessionStatus = TradeOfferManager.ETradeSessionStatus = require('../resources/ETradeSessionStatus.js'); const ETradeStatus = TradeOfferManager.ETradeStatus = require('../resources/ETradeStatus.js'); +const SteamID = TradeOfferManager.SteamID = require('steamid'); const TradeOffer = require('./classes/TradeOffer.js'); @@ -490,3 +491,5 @@ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callb }); }); }; + +require('./classes/TradeSession.js'); diff --git a/resources/ETradeSessionAction.js b/resources/ETradeSessionAction.js new file mode 100644 index 0000000..c35d615 --- /dev/null +++ b/resources/ETradeSessionAction.js @@ -0,0 +1,17 @@ +module.exports = { + "AddItem": 0, // An item was added to the trade + "RemoveItem": 1, // An item was removed from the trade + "Ready": 2, // A party has readied up + "Unready": 3, // A party has unreadied + "Confirm": 4, // A party has confirmed + // 5 = ??? + // 6 = added/removed currency, which we don't support + "Chat": 7, // A party has sent a chat message + + "0": "AddItem", + "1": "RemoveItem", + "2": "Ready", + "3": "Unready", + "4": "Confirm", + "7": "Chat" +}; diff --git a/resources/ETradeSessionStatus.js b/resources/ETradeSessionStatus.js new file mode 100644 index 0000000..6892270 --- /dev/null +++ b/resources/ETradeSessionStatus.js @@ -0,0 +1,17 @@ +module.exports = { + "Active": 0, // The trade session is in progress and the parties are deciding what to do + "Complete": 1, // The trade has been accepted and completed + "Empty": 2, // The trade was accepted but there were no items on either side + "Canceled": 3, // The trade was canceled by one of the parties + "TimedOut": 4, // One of the parties stopped polling so the trade timed out + "Failed": 5, // There was a backend problem and the trade failed. No items were exchanged. + "TurnedIntoTradeOffer": 6, // This trade is now a trade offer + + "0": "Active", + "1": "Complete", + "2": "Empty", + "3": "Canceled", + "4": "TimedOut", + "5": "Failed", + "6": "TurnedIntoTradeOffer" +}; From 3f5a62971238ee0153478f651103cfdc1fd9d099 Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 14:24:54 -0500 Subject: [PATCH 04/36] RIP automatic offer-getting once confirmed --- lib/classes/TradeSession.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index 44f525f..eacbf9c 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -297,7 +297,7 @@ TradeSession.prototype._handleTradeStatus = function(status) { return; } else if (status.trade_status == ETradeSessionStatus.TurnedIntoTradeOffer) { this._setEnded(); - this._getTradeOffer(status.tradeid, 1); + this.emit('end', status.trade_status, status.tradeid); return; } else if (status.trade_status > ETradeSessionStatus.Active) { // the trade is over, yo @@ -537,23 +537,6 @@ TradeSession.prototype._handleTradeStatus = function(status) { } }; -TradeSession.prototype._getTradeOffer = function(offerID, attemptNum) { - if (attemptNum > 5) { - this.emit('end', ETradeSessionStatus.TurnedIntoTradeOffer, offerID, null); - return; - } - - this._manager.getOffer(offerID, (err, offer) => { - if (err) { - this.emit('debug', 'Error getting trade offer ' + offerID + ': ' + err.message); - setTimeout(() => this._getTradeOffer(offerID, attemptNum + 1), 500); - return; - } - - this.emit('end', ETradeSessionStatus.TurnedIntoTradeOffer, offerID, offer); - }); -}; - TradeSession.prototype._setEnded = function() { this._ended = true; this._clearTradeStatusPoll(); From c7037ee787c4411b1c6243f31b5c55f17863ce26 Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 16:43:43 -0500 Subject: [PATCH 05/36] Added jsdoc --- lib/classes/TradeOffer.js | 113 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index ed114bd..a85154d 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -63,6 +63,12 @@ function TradeOffer(manager, partner, token) { this.rawJson = ""; } +/** + * Figure out if this offer is "glitched". An offer is considered "glitched" if one of the following are true: + * - It contains no items on either side (an offer cannot be sent like this) + * - Any item in the offer does not have a name (and a language is set) + * @returns {boolean} + */ TradeOffer.prototype.isGlitched = function() { if (!this.id) { // not sent yet @@ -82,10 +88,21 @@ TradeOffer.prototype.isGlitched = function() { return false; }; +/** + * Figure out if the offer contains an item. + * @param {{appid, contextid, [assetid], [id]}} item + * @returns {boolean} + */ TradeOffer.prototype.containsItem = function(item) { return this.itemsToGive.concat(this.itemsToReceive).some(offerItem => Helpers.itemEquals(offerItem, item)); }; +/** + * Get or set a data element on the trade offer. + * @param {string} key + * @param {*} [value] - Omit to return the data element at that key + * @returns {*} + */ TradeOffer.prototype.data = function(key, value) { var pollData = this.manager.pollData; @@ -163,10 +180,20 @@ TradeOffer.prototype.getPartnerInventoryContents = function(appid, contextid, ca this.manager.getUserInventoryContents(this.partner, appid, contextid, true, callback); }; +/** + * Add one of your items to this trade offer. + * @param {{appid, contextid, [assetid], [id]}} item + * @returns {boolean} - Was the item added? + */ TradeOffer.prototype.addMyItem = function(item) { return addItem(item, this, this.itemsToGive); }; +/** + * Add one or more of your items to this trade offer. + * @param {{appid, contextid, [assetid], [id]}[]} items + * @returns {number} - Number of items added + */ TradeOffer.prototype.addMyItems = function(items) { var added = 0; var self = this; @@ -179,6 +206,11 @@ TradeOffer.prototype.addMyItems = function(items) { return added; }; +/** + * Remove one of your items from this trade offer. + * @param {{appid, contextid, [assetid], [id]}} item + * @returns {boolean} - Was the item removed? + */ TradeOffer.prototype.removeMyItem = function(item) { if (this.id) { throw new Error("Cannot remove items from an already-sent offer"); @@ -194,6 +226,11 @@ TradeOffer.prototype.removeMyItem = function(item) { return false; }; +/** + * Remove one or more of your items from this trade offer. + * @param {{appid, contextid, [assetid], [id]}[]} items + * @returns {number} - Number of items removed + */ TradeOffer.prototype.removeMyItems = function(items) { var removed = 0; items.forEach((item) => { @@ -205,10 +242,20 @@ TradeOffer.prototype.removeMyItems = function(items) { return removed; }; +/** + * Add one of their items to this trade offer. + * @param {{appid, contextid, [assetid], [id]}} item + * @returns {boolean} - Was the item added? + */ TradeOffer.prototype.addTheirItem = function(item) { return addItem(item, this, this.itemsToReceive); }; +/** + * Add one or more of their items to this trade offer. + * @param {{appid, contextid, [assetid], [id]}[]} items + * @returns {number} - Number of items added + */ TradeOffer.prototype.addTheirItems = function(items) { var added = 0; items.forEach((item) => { @@ -220,6 +267,11 @@ TradeOffer.prototype.addTheirItems = function(items) { return added; }; +/** + * Remove one of their items from this trade offer. + * @param {{appid, contextid, [assetid], [id]}} item + * @returns {boolean} - Was the item removed? + */ TradeOffer.prototype.removeTheirItem = function(item) { if (this.id) { throw new Error("Cannot remove items from an already-sent offer"); @@ -235,6 +287,11 @@ TradeOffer.prototype.removeTheirItem = function(item) { return false; }; +/** + * Remove one or more of their items from this trade offer. + * @param {{appid, contextid, [assetid], [id]}[]} items + * @returns {number} - Number of items removed + */ TradeOffer.prototype.removeTheirItems = function(items) { var removed = 0; items.forEach((item) => { @@ -246,6 +303,12 @@ TradeOffer.prototype.removeTheirItems = function(items) { return removed; }; +/** + * @param {{appid, contextid, [assetid], [id]}} details + * @param {TradeOffer} offer + * @param {Array} list - List of items already in trade + * @returns {boolean} + */ function addItem(details, offer, list) { if (offer.id) { throw new Error("Cannot add items to an already-sent offer"); @@ -272,6 +335,10 @@ function addItem(details, offer, list) { return true; } +/** + * Send this trade offer. + * @param {function} [callback] + */ TradeOffer.prototype.send = function(callback) { if (this.id) { Helpers.makeAnError(new Error("This offer has already been sent"), callback); @@ -409,6 +476,10 @@ TradeOffer.prototype.send = function(callback) { }, "tradeoffermanager"); }; +/** + * Cancel or decline this trade offer. + * @param {function} [callback] + */ TradeOffer.prototype.cancel = TradeOffer.prototype.decline = function(callback) { if (!this.id) { Helpers.makeAnError(new Error("Cannot cancel or decline an unsent offer"), callback); @@ -437,6 +508,11 @@ TradeOffer.prototype.cancel = TradeOffer.prototype.decline = function(callback) }); }; +/** + * Accept this trade offer. + * @param {boolean} [skipStateUpdate=false] - If true, don't bother updating the offer's state from the API. This means that you won't get data about whether it went into escrow. + * @param {function} [callback] + */ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { if (typeof skipStateUpdate === 'undefined') { skipStateUpdate = false; @@ -537,6 +613,10 @@ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { }, "tradeoffermanager"); }; +/** + * Update this offer from the API + * @param {function} callback + */ TradeOffer.prototype.update = function(callback) { this.manager.getOffer(this.id, (err, offer) => { if (err) { @@ -567,6 +647,12 @@ TradeOffer.prototype.update = function(callback) { }); }; +/** + * Get the items you received in this trade. You probably want to use getExchangeDetails instead. + * @deprecated Use getExchangeDetails instead + * @param {boolean} [getActions=false] + * @param {function} callback + */ TradeOffer.prototype.getReceivedItems = function(getActions, callback) { if (typeof getActions === 'function') { callback = getActions; @@ -643,6 +729,11 @@ TradeOffer.prototype.getReceivedItems = function(getActions, callback) { }, "tradeoffermanager"); }; +/** + * Get details about this item exchange. Only works if the trade was actually completed (i.e. it has a tradeID). + * @param {boolean} [getDetailsIfFailed=false] - Unless this is true, a trade that is failed (e.g. rolled back) will return an error instead of the data + * @param {function} callback + */ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) { if (typeof getDetailsIfFailed === 'function') { callback = getDetailsIfFailed; @@ -700,6 +791,12 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) }); }; +/** + * Get details about the users in this trade. Can only be used if: + * - The trade is created by you and *unsent* + * - The trade is created by them, sent, and *active* + * @param {function} callback + */ TradeOffer.prototype.getUserDetails = function(callback) { if (this.id && this.isOurOffer) { Helpers.makeAnError(new Error("Cannot get user details for an offer that we sent."), callback); @@ -724,6 +821,10 @@ TradeOffer.prototype.getUserDetails = function(callback) { Helpers.getUserDetailsFromTradeWindow(this.manager, url, callback); }; +/** + * Create a counter offer from this trade offer. Once the counter is sent, this trade will be marked as Countered. + * @returns {TradeOffer} + */ TradeOffer.prototype.counter = function() { if (this.state != ETradeOfferState.Active) { throw new Error("Cannot counter a non-active offer."); @@ -734,6 +835,10 @@ TradeOffer.prototype.counter = function() { return offer; }; +/** + * Create an unsent duplicate of this trade offer. + * @returns {TradeOffer} + */ TradeOffer.prototype.duplicate = function() { var offer = new TradeOffer(this.manager, this.partner, this._token); offer.itemsToGive = this.itemsToGive.slice(); @@ -743,6 +848,10 @@ TradeOffer.prototype.duplicate = function() { return offer; }; +/** + * Set this trade offer's message. + * @param {string} message + */ TradeOffer.prototype.setMessage = function(message) { if (this.id) { throw new Error("Cannot set message in an already-sent offer"); @@ -751,6 +860,10 @@ TradeOffer.prototype.setMessage = function(message) { this.message = message.toString().substring(0, 128); }; +/** + * Set the access token that will be used to send this trade offer. + * @param {string} token + */ TradeOffer.prototype.setToken = function(token) { if (this.id) { throw new Error("Cannot set token in an already-sent offer"); From f4d2b434cb6e81ee2ccd7559c6edd188d41b5501 Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 16:44:05 -0500 Subject: [PATCH 06/36] var -> let --- lib/classes/TradeOffer.js | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index a85154d..e853c93 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -104,7 +104,7 @@ TradeOffer.prototype.containsItem = function(item) { * @returns {*} */ TradeOffer.prototype.data = function(key, value) { - var pollData = this.manager.pollData; + let pollData = this.manager.pollData; if (arguments.length < 1) { // No arguments passed, so we return the whole offerData of this offer @@ -195,8 +195,8 @@ TradeOffer.prototype.addMyItem = function(item) { * @returns {number} - Number of items added */ TradeOffer.prototype.addMyItems = function(items) { - var added = 0; - var self = this; + let added = 0; + let self = this; items.forEach(function(item) { if (self.addMyItem(item)) { added++; @@ -232,7 +232,7 @@ TradeOffer.prototype.removeMyItem = function(item) { * @returns {number} - Number of items removed */ TradeOffer.prototype.removeMyItems = function(items) { - var removed = 0; + let removed = 0; items.forEach((item) => { if (this.removeMyItem(item)) { removed++; @@ -257,7 +257,7 @@ TradeOffer.prototype.addTheirItem = function(item) { * @returns {number} - Number of items added */ TradeOffer.prototype.addTheirItems = function(items) { - var added = 0; + let added = 0; items.forEach((item) => { if (this.addTheirItem(item)) { added++; @@ -293,7 +293,7 @@ TradeOffer.prototype.removeTheirItem = function(item) { * @returns {number} - Number of items removed */ TradeOffer.prototype.removeTheirItems = function(items) { - var removed = 0; + let removed = 0; items.forEach((item) => { if (this.removeTheirItem(item)) { removed++; @@ -318,7 +318,7 @@ function addItem(details, offer, list) { throw new Error("Missing appid, contextid, or assetid parameter"); } - var item = { + let item = { "id": (details.id || details.assetid).toString(), // always needs to be a string "assetid": (details.assetid || details.id).toString(), // always needs to be a string "appid": parseInt(details.appid, 10), // always needs to be an int @@ -359,7 +359,7 @@ TradeOffer.prototype.send = function(callback) { }; } - var offerdata = { + let offerdata = { "newversion": true, "version": 4, "me": { @@ -374,7 +374,7 @@ TradeOffer.prototype.send = function(callback) { } }; - var params = {}; + let params = {}; if (this._token) { params.trade_offer_access_token = this._token; } @@ -435,7 +435,7 @@ TradeOffer.prototype.send = function(callback) { this.expires = new Date(Date.now() + 1209600000); // Set any temporary local data into persistent poll data - for (var i in this._tempData) { + for (let i in this._tempData) { if (this._tempData.hasOwnProperty(i)) { this.manager.pollData.offerData = this.manager.pollData.offerData || {}; this.manager.pollData.offerData[this.id] = this.manager.pollData.offerData[this.id] || {}; @@ -626,7 +626,7 @@ TradeOffer.prototype.update = function(callback) { // Clone only the properties that might be out of date from the new TradeOffer onto this one, unless this one is // glitched. Sometimes Steam is bad and some properties are missing/malformed. - var properties = [ + let properties = [ 'id', 'state', 'expires', @@ -637,7 +637,7 @@ TradeOffer.prototype.update = function(callback) { 'tradeID' ]; - for (var i in offer) { + for (let i in offer) { if (offer.hasOwnProperty(i) && typeof offer[i] !== 'function' && (properties.indexOf(i) != -1 || this.isGlitched())) { this[i] = offer[i]; } @@ -681,13 +681,13 @@ TradeOffer.prototype.getReceivedItems = function(getActions, callback) { return; } - var match = body.match(/
\s*([^<]+)\s*<\/div>/); // I believe this is now redundant thanks to httpRequestGet + let match = body.match(/
\s*([^<]+)\s*<\/div>/); // I believe this is now redundant thanks to httpRequestGet if (match) { Helpers.makeAnError(new Error(match[1].trim()), callback); return; } - var script = body.match(/(var oItem;[\s\S]*)<\/script>/); + let script = body.match(/(let oItem;[\s\S]*)<\/script>/); if (!script) { if (body.length < 100 && body.match(/\{"success": ?false}/)) { Helpers.makeAnError(new Error("Not Logged In"), callback); @@ -699,7 +699,7 @@ TradeOffer.prototype.getReceivedItems = function(getActions, callback) { return; } - var items = []; + let items = []; require('vm').runInNewContext(script[1], { "UserYou": null, "BuildHover": function(str, item) { @@ -750,7 +750,7 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - var offer = this; + let offer = this; offer.manager._apiCall("GET", "GetTradeStatus", 1, {"tradeid": offer.tradeID}, (err, result) => { if (err) { Helpers.makeAnError(err, callback); @@ -762,7 +762,7 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - var trade = result.response.trades[0]; + let trade = result.response.trades[0]; if (!trade || trade.tradeid != offer.tradeID) { Helpers.makeAnError(new Error("Trade not found in GetTradeStatus response; try again later"), callback); return; @@ -783,8 +783,8 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - var received = offer.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); - var given = offer.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); + let received = offer.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); + let given = offer.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); callback(null, trade.status, new Date(trade.time_init * 1000), received, given); }); } @@ -808,7 +808,7 @@ TradeOffer.prototype.getUserDetails = function(callback) { return; } - var url; + let url; if (this.id) { url = `https://steamcommunity.com/tradeoffer/${this.id}/`; } else { @@ -830,7 +830,7 @@ TradeOffer.prototype.counter = function() { throw new Error("Cannot counter a non-active offer."); } - var offer = this.duplicate(); + let offer = this.duplicate(); offer._countering = this.id; return offer; }; @@ -840,7 +840,7 @@ TradeOffer.prototype.counter = function() { * @returns {TradeOffer} */ TradeOffer.prototype.duplicate = function() { - var offer = new TradeOffer(this.manager, this.partner, this._token); + let offer = new TradeOffer(this.manager, this.partner, this._token); offer.itemsToGive = this.itemsToGive.slice(); offer.itemsToReceive = this.itemsToReceive.slice(); offer.isOurOffer = true; From e369f61242c5c26cb7f78c43140f25afd88815cd Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 16:45:12 -0500 Subject: [PATCH 07/36] Cleaned up some unused constants --- lib/classes/TradeOffer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index e853c93..8508c2a 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -1,11 +1,9 @@ "use strict"; const SteamID = require('steamid'); -const EconItem = require('./EconItem.js'); const Helpers = require('../helpers.js'); const ETradeOfferState = require('../../resources/ETradeOfferState.js'); -const EOfferFilter = require('../../resources/EOfferFilter.js'); const EConfirmationMethod = require('../../resources/EConfirmationMethod.js'); const ETradeStatus = require('../../resources/ETradeStatus.js'); From 44ce958ae25c31620c271c875758cc54acdda1c8 Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 16:46:30 -0500 Subject: [PATCH 08/36] Clean up self = this type stuff --- lib/classes/TradeOffer.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index 8508c2a..76c3429 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -194,9 +194,8 @@ TradeOffer.prototype.addMyItem = function(item) { */ TradeOffer.prototype.addMyItems = function(items) { let added = 0; - let self = this; - items.forEach(function(item) { - if (self.addMyItem(item)) { + items.forEach((item) => { + if (this.addMyItem(item)) { added++; } }); @@ -748,8 +747,7 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - let offer = this; - offer.manager._apiCall("GET", "GetTradeStatus", 1, {"tradeid": offer.tradeID}, (err, result) => { + this.manager._apiCall("GET", "GetTradeStatus", 1, {"tradeid": this.tradeID}, (err, result) => { if (err) { Helpers.makeAnError(err, callback); return; @@ -761,7 +759,7 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) } let trade = result.response.trades[0]; - if (!trade || trade.tradeid != offer.tradeID) { + if (!trade || trade.tradeid != this.tradeID) { Helpers.makeAnError(new Error("Trade not found in GetTradeStatus response; try again later"), callback); return; } @@ -771,18 +769,18 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - if (!offer.manager._language) { + if (!this.manager._language) { // No need for descriptions callback(null, trade.status, new Date(trade.time_init * 1000), trade.assets_received || [], trade.assets_given || []); } else { - offer.manager._requestDescriptions((trade.assets_received || []).concat(trade.assets_given || []), (err) => { + this.manager._requestDescriptions((trade.assets_received || []).concat(trade.assets_given || []), (err) => { if (err) { callback(err); return; } - let received = offer.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); - let given = offer.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); + let received = this.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); + let given = this.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); callback(null, trade.status, new Date(trade.time_init * 1000), received, given); }); } From 5a6dad68628c96c619d401c61a110718b5a7d58f Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Sun, 12 Nov 2017 16:47:49 -0500 Subject: [PATCH 09/36] Consolidate callback of getExchangeDetails into a single object --- lib/classes/TradeOffer.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index 76c3429..1c48bb0 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -769,9 +769,16 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } + let output = { + "status": trade.status, + "tradeInitTime": new Date(trade.time_init * 1000), + "receivedItems": trade.assets_received || [], + "sentItems": trade.assets_given || [] + }; + if (!this.manager._language) { // No need for descriptions - callback(null, trade.status, new Date(trade.time_init * 1000), trade.assets_received || [], trade.assets_given || []); + callback(null, output); } else { this.manager._requestDescriptions((trade.assets_received || []).concat(trade.assets_given || []), (err) => { if (err) { @@ -779,9 +786,9 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - let received = this.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); - let given = this.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); - callback(null, trade.status, new Date(trade.time_init * 1000), received, given); + output.receivedItems = this.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); + output.sentItems = this.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); + callback(null, output); }); } }); From 16106d19070efae67e35845f327cf24ded2ac2cf Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sun, 19 Nov 2017 16:40:35 -0500 Subject: [PATCH 10/36] Updated functions in index.js to use promises --- lib/index.js | 329 +++++++++++++++++++++++++++++---------------------- 1 file changed, 187 insertions(+), 142 deletions(-) diff --git a/lib/index.js b/lib/index.js index 0702ceb..8eed116 100644 --- a/lib/index.js +++ b/lib/index.js @@ -115,55 +115,63 @@ function TradeOfferManager(options) { } } +/** + * Set your cookies so that the TradeOfferManager can talk to Steam under your account. + * @param {string[]} cookies - An array of cookies, each cookie in "name=value" format + * @param {string} [familyViewPin] - If your account is locked with Family View, provide the PIN here + * @param {function} [callback] + * @returns {Promise} + */ TradeOfferManager.prototype.setCookies = function(cookies, familyViewPin, callback) { - if (typeof familyViewPin === 'function') { - callback = familyViewPin; - familyViewPin = null; - } + return StdLib.Promises.callbackPromise([], callback, true, (accept, reject) => { + if (typeof familyViewPin === 'function') { + callback = familyViewPin; + familyViewPin = null; + } - this._community.setCookies(cookies); - this.steamID = this._community.steamID; + this._community.setCookies(cookies); + this.steamID = this._community.steamID; - if (this._getPollDataFromDisk) { - delete this._getPollDataFromDisk; - var filename = 'polldata_' + this.steamID + '.json'; - this._getFromDisk([filename], (err, files) => { - if (files[filename]) { - this.pollData = JSON.parse(files[filename].toString('utf8')); - } - }); - } + if (this._getPollDataFromDisk) { + delete this._getPollDataFromDisk; + var filename = 'polldata_' + this.steamID + '.json'; + this._getFromDisk([filename], (err, files) => { + if (files[filename]) { + this.pollData = JSON.parse(files[filename].toString('utf8')); + } + }); + } + + const checkDone = (err) => { + if (!err) { + if (this._languageName) { + this._community.setCookies(['Steam_Language=' + this._languageName]); + } - var checkDone = (err) => { - if (!err) { - if (this._languageName) { - this._community.setCookies(['Steam_Language=' + this._languageName]); + clearTimeout(this._pollTimer); + this.doPoll(); } - clearTimeout(this._pollTimer); - this.doPoll(); - } + err ? reject(err) : accept(); + }; - if (callback) { - callback(err); - } - }; - - if (familyViewPin) { - this.parentalUnlock(familyViewPin, (err) => { - if (err) { - if (callback) { - callback(err); + if (familyViewPin) { + this.parentalUnlock(familyViewPin, (err) => { + if (err) { + reject(err); + } else { + this._checkApiKey(checkDone); } - } else { - this._checkApiKey(checkDone); - } - }); - } else { - this._checkApiKey(checkDone); - } + }); + } else { + this._checkApiKey(checkDone); + } + }); }; +/** + * Shut down the TradeOfferManager. Clear its cookies and API key and stop polling. + */ TradeOfferManager.prototype.shutdown = function() { clearTimeout(this._pollTimer); this._community = new SteamCommunity(); @@ -171,14 +179,25 @@ TradeOfferManager.prototype.shutdown = function() { this.apiKey = null; }; +/** + * If the account has Family View, unlock it with the PIN. + * @param {string} pin + * @param {function} [callback] + * @returns {Promise} + */ TradeOfferManager.prototype.parentalUnlock = function(pin, callback) { - this._community.parentalUnlock(pin, (err) => { - if (callback) { - callback(err || null); - } + return StdLib.Promises.callbackPromise([], callback, true, (accept, reject) => { + this._community.parentalUnlock(pin, (err) => { + err ? reject(err) : accept(); + }); }); }; +/** + * Make sure we have an API key, and if we don't, get one. + * @param {function} callback + * @private + */ TradeOfferManager.prototype._checkApiKey = function(callback) { if (this.apiKey) { if (callback) { @@ -199,6 +218,12 @@ TradeOfferManager.prototype._checkApiKey = function(callback) { }); }; +/** + * Write a file to disk. + * @param {string} filename + * @param {Buffer|string} content + * @private + */ TradeOfferManager.prototype._persistToDisk = function(filename, content) { if (!this.storage) { return; @@ -229,6 +254,12 @@ TradeOfferManager.prototype._persistToDisk = function(filename, content) { } }; +/** + * Get some files from disk. + * @param {string[]} filenames + * @param {function} callback + * @private + */ TradeOfferManager.prototype._getFromDisk = function(filenames, callback) { if (!this.storage) { callback(null, {}); @@ -277,10 +308,11 @@ TradeOfferManager.prototype._getFromDisk = function(filenames, callback) { * @param {int} appid - The Steam application ID of the game for which you want an inventory * @param {int} contextid - The ID of the "context" within the game you want to retrieve * @param {boolean} tradableOnly - true to get only tradable items and currencies - * @param callback + * @param {function} [callback] + * @returns {Promise} */ TradeOfferManager.prototype.getInventoryContents = function(appid, contextid, tradableOnly, callback) { - this.getUserInventoryContents(this.steamID, appid, contextid, tradableOnly, callback); + return this.getUserInventoryContents(this.steamID, appid, contextid, tradableOnly, callback); }; /** @@ -289,10 +321,19 @@ TradeOfferManager.prototype.getInventoryContents = function(appid, contextid, tr * @param {int} appid - The Steam application ID of the game for which you want an inventory * @param {int} contextid - The ID of the "context" within the game you want to retrieve * @param {boolean} tradableOnly - true to get only tradable items and currencies - * @param callback + * @param {function} [callback] + * @returns {Promise} */ TradeOfferManager.prototype.getUserInventoryContents = function(sid, appid, contextid, tradableOnly, callback) { - this._community.getUserInventoryContents(sid, appid, contextid, tradableOnly, this._languageName || "english", callback); + StdLib.Promises.callbackPromise(['inventory', 'currencies'], callback, false, (accept, reject) => { + this._community.getUserInventoryContents(sid, appid, contextid, tradableOnly, this._languageName || "english", (err, inventory, currencies) => { + if (err) { + reject(err); + } else { + accept({inventory, currencies}); + } + }); + }); }; /** @@ -322,42 +363,46 @@ TradeOfferManager.prototype.loadUserInventory = function(sid, appid, contextid, /** * Get the token parameter from your account's Trade URL - * @param {function} callback + * @param {function} [callback] + * @returns {Promise} */ TradeOfferManager.prototype.getOfferToken = function(callback) { - this._community.getTradeURL((err, url, token) => { - if (err) { - callback(err); - return; - } - - callback(null, token); + return StdLib.Promises.callbackPromise(['token'], callback, false, (accept, reject) => { + this._community.getTradeURL((err, url, token) => { + err ? reject(err) : accept({token}); + }); }); }; +/** + * Get a list of trade offers that contain some input items. + * @param {object[]|object} items - One object or an array of objects, where each object contains appid+contextid+(assetid|id) properties + * @param {boolean} [includeInactive=false] If true, also include trade offers that are not Active + * @param {function} [callback] + * @returns {Promise} + */ TradeOfferManager.prototype.getOffersContainingItems = function(items, includeInactive, callback) { - if (typeof includeInactive === 'function') { - callback = includeInactive; - includeInactive = false; - } + return StdLib.Promises.callbackPromise(['sent', 'received'], callback, false, (accept, reject) => { + if (typeof includeInactive === 'function') { + callback = includeInactive; + includeInactive = false; + } - if (typeof items.length === 'undefined') { - // not an array - items = [items]; - } + includeInactive = includeInactive || false; - this.getOffers(includeInactive ? EOfferFilter.All : EOfferFilter.ActiveOnly, (err, sent, received) => { - if (err) { - callback(err); - return; + if (typeof items.length === 'undefined') { + // not an array + items = [items]; } - callback(null, sent.filter(filterFunc), received.filter(filterFunc)); - }); + this.getOffers(includeInactive ? EOfferFilter.All : EOfferFilter.ActiveOnly, (err, sent, received) => { + err ? reject(err) : accept({"sent": sent.filter(filterFunc), "received": received.filter(filterFunc)}); + }); - function filterFunc(offer) { - return items.some(item => offer.containsItem(item)); - } + function filterFunc(offer) { + return items.some(item => offer.containsItem(item)); + } + }); }; /** @@ -391,39 +436,37 @@ require('./polling.js'); /** * Get a trade offer that is already sent (either by you or to you) * @param {int|string} id - The offer's numeric ID - * @param {function} callback + * @param {function} [callback] + * @returns {Promise} */ TradeOfferManager.prototype.getOffer = function(id, callback) { - this._apiCall('GET', 'GetTradeOffer', 1, {"tradeofferid": id}, (err, body) => { - if (err) { - callback(err); - return; - } - - if (!body.response) { - callback(new Error("Malformed API response")); - return; - } + return StdLib.Promises.callbackPromise(['offer'], callback, false, (accept, reject) => { + this._apiCall('GET', 'GetTradeOffer', 1, {"tradeofferid": id}, (err, body) => { + if (err) { + reject(err); + return; + } - if (!body.response.offer) { - callback(new Error("No matching offer found")); - return; - } + if (!body.response) { + reject(new Error("Malformed API response")); + return; + } - // Make sure the response is well-formed - if (Helpers.offerMalformed(body.response.offer)) { - callback(new Error("Data temporarily unavailable")); - return; - } + if (!body.response.offer) { + reject(new Error("No matching offer found")); + return; + } - this._digestDescriptions(body.response.descriptions); - Helpers.checkNeededDescriptions(this, [body.response.offer], (err) => { - if (err) { - callback(err); + // Make sure the response is well-formed + if (Helpers.offerMalformed(body.response.offer)) { + reject(new Error("Data temporarily unavailable")); return; } - callback(null, Helpers.createOfferFromData(this, body.response.offer)); + this._digestDescriptions(body.response.descriptions); + Helpers.checkNeededDescriptions(this, [body.response.offer], (err) => { + err ? reject(err) : accept({"offer": Helpers.createOfferFromData(this, body.response.offer)}); + }); }); }); }; @@ -432,62 +475,64 @@ TradeOfferManager.prototype.getOffer = function(id, callback) { * Get a list of trade offers either sent to you or by you * @param {int} filter * @param {Date} [historicalCutoff] - Pass a Date object in the past along with ActiveOnly to also get offers that were updated since this time - * @param {function} callback + * @param {function} [callback] */ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callback) { - if (typeof historicalCutoff === 'function') { - callback = historicalCutoff; - historicalCutoff = new Date(Date.now() + 31536000000); - } else if (!historicalCutoff) { - historicalCutoff = new Date(Date.now() + 31536000000); - } - - // Currently the GetTradeOffers API doesn't include app_data, so we need to get descriptions from the WebAPI - - var options = { - "get_sent_offers": 1, - "get_received_offers": 1, - "get_descriptions": 0, - "language": this._language, - "active_only": (filter == EOfferFilter.ActiveOnly) ? 1 : 0, - "historical_only": (filter == EOfferFilter.HistoricalOnly) ? 1 : 0, - "time_historical_cutoff": Math.floor(historicalCutoff.getTime() / 1000) - }; - - this._apiCall('GET', 'GetTradeOffers', 1, options, (err, body) => { - if (err) { - callback(err); - return; - } - - if (!body.response) { - callback(new Error("Malformed API response")); - return; + return StdLib.Promises.callbackPromise(['sent', 'received'], callback, false, (accept, reject) => { + if (typeof historicalCutoff === 'function') { + callback = historicalCutoff; + historicalCutoff = new Date(Date.now() + 31536000000); + } else if (!historicalCutoff) { + historicalCutoff = new Date(Date.now() + 31536000000); } - // Make sure at least some offers are well-formed. Apparently some offers can be empty just forever. Because Steam. - var allOffers = (body.response.trade_offers_sent || []).concat(body.response.trade_offers_received || []); - if (allOffers.length > 0 && (allOffers.every(Helpers.offerMalformed) || allOffers.some(Helpers.offerSuperMalformed))) { - callback(new Error("Data temporarily unavailable")); - return; - } + // Currently the GetTradeOffers API doesn't include app_data, so we need to get descriptions from the WebAPI - //manager._digestDescriptions(body.response.descriptions); + var options = { + "get_sent_offers": 1, + "get_received_offers": 1, + "get_descriptions": 0, + "language": this._language, + "active_only": (filter == EOfferFilter.ActiveOnly) ? 1 : 0, + "historical_only": (filter == EOfferFilter.HistoricalOnly) ? 1 : 0, + "time_historical_cutoff": Math.floor(historicalCutoff.getTime() / 1000) + }; - // Let's check the asset cache and see if we have descriptions that match these items. - // If the necessary descriptions aren't in the asset cache, this will request them from the WebAPI and store - // them for future use. - Helpers.checkNeededDescriptions(this, (body.response.trade_offers_sent || []).concat(body.response.trade_offers_received || []), (err) => { + this._apiCall('GET', 'GetTradeOffers', 1, options, (err, body) => { if (err) { - callback(new Error("Descriptions: " + err.message)); + reject(err); + return; + } + + if (!body.response) { + reject(new Error("Malformed API response")); return; } - var sent = (body.response.trade_offers_sent || []).map(data => Helpers.createOfferFromData(this, data)); - var received = (body.response.trade_offers_received || []).map(data => Helpers.createOfferFromData(this, data)); + // Make sure at least some offers are well-formed. Apparently some offers can be empty just forever. Because Steam. + var allOffers = (body.response.trade_offers_sent || []).concat(body.response.trade_offers_received || []); + if (allOffers.length > 0 && (allOffers.every(Helpers.offerMalformed) || allOffers.some(Helpers.offerSuperMalformed))) { + reject(new Error("Data temporarily unavailable")); + return; + } - callback(null, sent, received); - this.emit('offerList', filter, sent, received); + //manager._digestDescriptions(body.response.descriptions); + + // Let's check the asset cache and see if we have descriptions that match these items. + // If the necessary descriptions aren't in the asset cache, this will request them from the WebAPI and store + // them for future use. + Helpers.checkNeededDescriptions(this, (body.response.trade_offers_sent || []).concat(body.response.trade_offers_received || []), (err) => { + if (err) { + reject(new Error("Descriptions: " + err.message)); + return; + } + + var sent = (body.response.trade_offers_sent || []).map(data => Helpers.createOfferFromData(this, data)); + var received = (body.response.trade_offers_received || []).map(data => Helpers.createOfferFromData(this, data)); + + accept({sent, received}); + this.emit('offerList', filter, sent, received); + }); }); }); }; From 57998d4567596debf7a1e83be9e4c9e3b0210af3 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sun, 19 Nov 2017 16:42:00 -0500 Subject: [PATCH 11/36] Fixed promise not returned --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 8eed116..afb152a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -325,7 +325,7 @@ TradeOfferManager.prototype.getInventoryContents = function(appid, contextid, tr * @returns {Promise} */ TradeOfferManager.prototype.getUserInventoryContents = function(sid, appid, contextid, tradableOnly, callback) { - StdLib.Promises.callbackPromise(['inventory', 'currencies'], callback, false, (accept, reject) => { + return StdLib.Promises.callbackPromise(['inventory', 'currencies'], callback, false, (accept, reject) => { this._community.getUserInventoryContents(sid, appid, contextid, tradableOnly, this._languageName || "english", (err, inventory, currencies) => { if (err) { reject(err); From 8bc69ea1ad1df39fcf9e8abe3b1b9eeb71bde603 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sun, 19 Nov 2017 16:42:17 -0500 Subject: [PATCH 12/36] Removed loadInventory and loadUserInventory --- lib/index.js | 25 ------------------------- v3.txt | 4 ++++ 2 files changed, 4 insertions(+), 25 deletions(-) create mode 100644 v3.txt diff --git a/lib/index.js b/lib/index.js index afb152a..291b0f1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -336,31 +336,6 @@ TradeOfferManager.prototype.getUserInventoryContents = function(sid, appid, cont }); }; -/** - * Get the contents of your own specific inventory context. - * @deprecated Use getInventoryContents instead - * @param {int} appid - The Steam application ID of the game for which you want an inventory - * @param {int} contextid - The ID of the "context" within the game you want to retrieve - * @param {boolean} tradableOnly - true to get only tradable items and currencies - * @param callback - */ -TradeOfferManager.prototype.loadInventory = function(appid, contextid, tradableOnly, callback) { - this.loadUserInventory(this.steamID, appid, contextid, tradableOnly, callback); -}; - -/** - * Get the contents of a user's specific inventory context. - * @deprecated Use getUserInventoryContents instead - * @param {SteamID|string} sid - The user's SteamID as a SteamID object or a string which can parse into one - * @param {int} appid - The Steam application ID of the game for which you want an inventory - * @param {int} contextid - The ID of the "context" within the game you want to retrieve - * @param {boolean} tradableOnly - true to get only tradable items and currencies - * @param callback - */ -TradeOfferManager.prototype.loadUserInventory = function(sid, appid, contextid, tradableOnly, callback) { - this._community.getUserInventory(sid, appid, contextid, tradableOnly, callback); -}; - /** * Get the token parameter from your account's Trade URL * @param {function} [callback] diff --git a/v3.txt b/v3.txt new file mode 100644 index 0000000..1a9de1b --- /dev/null +++ b/v3.txt @@ -0,0 +1,4 @@ +BREAKING: + +- Removed loadInventory +- Removed loadUserInventory From 9d80442020123deba6d2734eab711de59cad701f Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Wed, 22 Nov 2017 22:43:57 -0500 Subject: [PATCH 13/36] Revert "Consolidate callback of getExchangeDetails into a single object" This reverts commit 5a6dad6 --- lib/classes/TradeOffer.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index 1c48bb0..76c3429 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -769,16 +769,9 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - let output = { - "status": trade.status, - "tradeInitTime": new Date(trade.time_init * 1000), - "receivedItems": trade.assets_received || [], - "sentItems": trade.assets_given || [] - }; - if (!this.manager._language) { // No need for descriptions - callback(null, output); + callback(null, trade.status, new Date(trade.time_init * 1000), trade.assets_received || [], trade.assets_given || []); } else { this.manager._requestDescriptions((trade.assets_received || []).concat(trade.assets_given || []), (err) => { if (err) { @@ -786,9 +779,9 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) return; } - output.receivedItems = this.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); - output.sentItems = this.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); - callback(null, output); + let received = this.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); + let given = this.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); + callback(null, trade.status, new Date(trade.time_init * 1000), received, given); }); } }); From c87cc139b7aa7da7b02caca64efe6e0663128b6a Mon Sep 17 00:00:00 2001 From: Alexander Corn Date: Wed, 22 Nov 2017 22:44:10 -0500 Subject: [PATCH 14/36] Added node 6 requirement to breaking list --- v3.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/v3.txt b/v3.txt index 1a9de1b..4c49578 100644 --- a/v3.txt +++ b/v3.txt @@ -1,4 +1,7 @@ BREAKING: - - Removed loadInventory - Removed loadUserInventory +- now requires node 6 + +NEW: +- Trade sessions From db80d17a02b0ae3f4d848ff34ca166608ef90d9e Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Thu, 15 Nov 2018 19:17:27 -0500 Subject: [PATCH 15/36] Throw an Error if someone tries to create a TradeOffer with a number SteamID --- lib/classes/TradeOffer.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index 76c3429..3424172 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -8,7 +8,9 @@ const EConfirmationMethod = require('../../resources/EConfirmationMethod.js'); const ETradeStatus = require('../../resources/ETradeStatus.js'); function TradeOffer(manager, partner, token) { - if (typeof partner === 'string') { + if (typeof partner === 'number') { + throw new Error('Input SteamID ' + this.partner + ' is a number and not a string; did you make a mistake?'); + } else if (typeof partner === 'string') { this.partner = new SteamID(partner); } else { this.partner = partner; @@ -397,19 +399,19 @@ TradeOffer.prototype.send = function(callback) { "checkHttpError": false // we'll check it ourself. Some trade offer errors return HTTP 500 }, (err, response, body) => { this.manager._pendingOfferSendResponses--; - + if (err) { Helpers.makeAnError(err, callback); return; } - + if (response.statusCode != 200) { if (response.statusCode == 401) { this.manager._community._notifySessionExpired(new Error("HTTP error 401")); Helpers.makeAnError(new Error("Not Logged In"), callback); return; } - + Helpers.makeAnError(new Error("HTTP error " + response.statusCode), callback, body); return; } @@ -514,12 +516,12 @@ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { if (typeof skipStateUpdate === 'undefined') { skipStateUpdate = false; } - + if (typeof skipStateUpdate === 'function') { callback = skipStateUpdate; skipStateUpdate = false; } - + if (!this.id) { Helpers.makeAnError(new Error("Cannot accept an unsent offer"), callback); return; @@ -576,7 +578,7 @@ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { if (!callback) { return; } - + if (skipStateUpdate) { if (body.tradeid) { this.tradeID = body.tradeid; @@ -589,7 +591,7 @@ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { } return; } - + this.update((err) => { if (err) { From 1fa17fa5bb85c38925255356d6709faf5fca85b8 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:02:53 -0500 Subject: [PATCH 16/36] Add items to itemsToGive immediately but with a pendingAdd property --- .idea/inspectionProfiles/Project_Default.xml | 40 -------------------- lib/classes/TradeSession.js | 21 +++++++++- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 5a5f29a..fb1601d 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,46 +1,6 @@ \ No newline at end of file diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index eacbf9c..b37d9b8 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -203,6 +203,10 @@ TradeSession.prototype.addItem = function(item, callback) { callback && callback(null); } }); + + let cloned = shallowClone(item); + cloned.pendingAdd = true; + this.itemsToGive.push(cloned); }; /** @@ -345,10 +349,12 @@ TradeSession.prototype._handleTradeStatus = function(status) { } let item = inv.filter(item => item.assetid == event.assetid)[0]; + let filtered; if (!item) { this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in our inventory even though it was added to the trade`); - } else if (this.itemsToGive.some(tradeItem => Helpers.itemEquals(tradeItem, item))) { - // this item is already in the trade, do nothing + } else if ((filtered = this.itemsToGive.filter(tradeItem => Helpers.itemEquals(tradeItem, item))).length > 0) { + // this item is already in the trade + filtered[0].pendingAdd = false; } else { // this item was added to the trade this.itemsToGive.push(item); @@ -750,3 +756,14 @@ TradeSession.prototype._fixAssetOrder = function() { this.itemsToGive = itemsToGive; this.itemsToReceive = itemsToReceive; }; + +function shallowClone(obj) { + let newObj = {}; + for (let i in obj) { + if (obj.hasOwnProperty(i)) { + newObj[i] = obj[i]; + } + } + + return newObj; +} From f0ca177c9078eac4f420ff1884cab66a544d482b Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:03:42 -0500 Subject: [PATCH 17/36] Update version number --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c02fe0f..a5c1dda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "steam-tradeoffer-manager", - "version": "2.9.4", + "version": "3.0.0-dev", + "private": true, "description": "A simple trade offers API for Steam", "main": "./lib/index.js", "repository": { From 259887d41e702cd642d92bcf98540f0797c816fe Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:05:22 -0500 Subject: [PATCH 18/36] Don't include pendingAdd items when auditing item count --- lib/classes/TradeSession.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index b37d9b8..182e1e2 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -507,7 +507,7 @@ TradeSession.prototype._handleTradeStatus = function(status) { let who = i == 0 ? 'me' : 'them'; let remoteAssets = status[who].assets && fixItemArray(status[who].assets); - let localAssets = i == 0 ? this.itemsToGive : this.itemsToReceive; + let localAssets = (i == 0 ? this.itemsToGive : this.itemsToReceive).filter(item => !item.pendingAdd); if (remoteAssets) { if (remoteAssets.length != localAssets.length) { From 76d6592ae4c9ea75ab9ed961dcf7ed6d3e0d87de Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:16:14 -0500 Subject: [PATCH 19/36] Immediately remove items from itemsToGive --- lib/classes/TradeSession.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index 182e1e2..3a0269f 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -46,6 +46,7 @@ function TradeSession(manager, partner, detailsMe, detailsThem) { } this.itemsToGive = []; + this._itemsToGiveRemoving = []; this.itemsToReceive = []; this.me = detailsMe; @@ -231,6 +232,11 @@ TradeSession.prototype.removeItem = function(item, callback) { callback && callback(null); } }); + + let filtered = this.itemsToGive.filter(tradeItem => Helpers.itemEquals(item, tradeItem)); + if (filtered.length > 0) { + this._itemsToGiveRemoving.push(this.itemsToGive.splice(this.itemsToGive.indexOf(filtered[0]), 1)[0]); + } }; /** @@ -409,6 +415,11 @@ TradeSession.prototype._handleTradeStatus = function(status) { } } + let filtered; + if (isUs && (filtered = this._itemsToGiveRemoving.filter(tradeItem => Helpers.itemEquals(event, tradeItem))).length > 0) { + this._itemsToGiveRemoving.splice(this._itemsToGiveRemoving.indexOf(filtered[0]), 1); + } + break; case ETradeSessionAction.Ready: From bdd6d38412043c60fa9ad952fbda3d23f17083b0 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:22:32 -0500 Subject: [PATCH 20/36] Check _itemsToGiveRemoving when we audit the trade --- lib/classes/TradeSession.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index 3a0269f..144eaae 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -518,7 +518,7 @@ TradeSession.prototype._handleTradeStatus = function(status) { let who = i == 0 ? 'me' : 'them'; let remoteAssets = status[who].assets && fixItemArray(status[who].assets); - let localAssets = (i == 0 ? this.itemsToGive : this.itemsToReceive).filter(item => !item.pendingAdd); + let localAssets = (i == 0 ? this.itemsToGive.concat(this._itemsToGiveRemoving) : this.itemsToReceive).filter(item => !item.pendingAdd); if (remoteAssets) { if (remoteAssets.length != localAssets.length) { From f76509f1092633c518f6d2461acb8f5ac69bb68f Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:43:14 -0500 Subject: [PATCH 21/36] Spoof events if necessary to keep trade in sync --- lib/classes/TradeSession.js | 384 ++++++++++++++++++++++-------------- package.json | 2 +- v3.txt | 2 +- 3 files changed, 234 insertions(+), 154 deletions(-) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index 144eaae..5e8b0f2 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -290,7 +290,7 @@ TradeSession.prototype._enqueueTradeStatusPoll = function() { this._tradeStatusPoll = setTimeout(() => this._getTradeStatus(), this.pollInterval); }; -TradeSession.prototype._handleTradeStatus = function(status) { +TradeSession.prototype._handleTradeStatus = async function(status) { if (this._ended || !status.success) { return; } @@ -332,158 +332,30 @@ TradeSession.prototype._handleTradeStatus = function(status) { // the trade session is active // process events if (status.events) { - let eventKeys = Object.keys(status.events).map(key => parseInt(key, 10)); - eventKeys.forEach((key) => { - if (key < this._logPos) { - this.emit('debug', 'Ignoring event ' + key + '; logPos is ' + this._logPos); - return; - } - - let event = status.events[key]; - let isUs = event.steamid == this._manager.steamID.getSteamID64(); - this.emit('debug', 'Handling event ' + event.action + ' (' + (ETradeSessionAction[event.action] || event.action) + ')'); - - switch (parseInt(event.action, 10)) { - case ETradeSessionAction.AddItem: - this.me.ready = false; - this.them.ready = false; - - if (isUs) { - this.getInventory(event.appid, event.contextid, (err, inv) => { - if (err) { - return this._terminateWithError("Cannot get my inventory: " + err.message); - } - - let item = inv.filter(item => item.assetid == event.assetid)[0]; - let filtered; - if (!item) { - this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in our inventory even though it was added to the trade`); - } else if ((filtered = this.itemsToGive.filter(tradeItem => Helpers.itemEquals(tradeItem, item))).length > 0) { - // this item is already in the trade - filtered[0].pendingAdd = false; - } else { - // this item was added to the trade - this.itemsToGive.push(item); - this._fixAssetOrder(); - } - }); - } else { - this._getTheirInventory(event.appid, event.contextid, (err, inv) => { - if (err) { - return this._terminateWithError("Cannot get partner inventory: " + err.message); - } - - let item = inv.filter(item => item.assetid == event.assetid)[0]; - if (!item) { - this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in partner's inventory even though it was added to the trade`); - } else if (this.itemsToReceive.some(tradeItem => Helpers.itemEquals(tradeItem, item))) { - // item is already in the trade - } else { - this.itemsToReceive.push(item); - this._fixAssetOrder(); - // it's very probable that the user will want to immediately take some action based on this, - // but steam will reject it if the version we send doesn't match the actual version - // delay by a tick so the version can update - process.nextTick(() => { - this.emit('itemAdded', item); - }); - } - }); - } - - break; - - case ETradeSessionAction.RemoveItem: - this.me.ready = false; - this.them.ready = false; - - let itemArray = isUs ? this.itemsToGive : this.itemsToReceive; - for (let i = 0; i < itemArray.length; i++) { - if (!Helpers.itemEquals(itemArray[i], event)) { - continue; - } - - // we found it - let item = itemArray.splice(i, 1)[0]; - if (!isUs) { - // it's very probable that the user will want to immediately take some action based on this, - // but steam will reject it if the version we send doesn't match the actual version - // delay by a tick so the version can update - process.nextTick(() => { - this.emit('itemRemoved', item); - }); - } - } - - let filtered; - if (isUs && (filtered = this._itemsToGiveRemoving.filter(tradeItem => Helpers.itemEquals(event, tradeItem))).length > 0) { - this._itemsToGiveRemoving.splice(this._itemsToGiveRemoving.indexOf(filtered[0]), 1); - } - - break; - - case ETradeSessionAction.Ready: - if (isUs) { - this.me.ready = true; - } else { - this.them.ready = true; - // it's very probable that the user will want to immediately take some action based on this, - // but steam will reject it if the version we send doesn't match the actual version - // delay by a tick so the version can update - process.nextTick(() => { - this.emit('ready'); - }); - } - - break; - - case ETradeSessionAction.Unready: - if (isUs) { - this.me.ready = false; - } else { - this.them.ready = false; - // it's very probable that the user will want to immediately take some action based on this, - // but steam will reject it if the version we send doesn't match the actual version - // delay by a tick so the version can update - process.nextTick(() => { - this.emit('unready'); - }); - } - - break; - - case ETradeSessionAction.Confirm: - if (isUs) { - this.me.confirmed = true; - } else { - this.them.confirmed = true; - // it's very probable that the user will want to immediately take some action based on this, - // but steam will reject it if the version we send doesn't match the actual version - // delay by a tick so the version can update - process.nextTick(() => { - this.emit('confirm'); - }); - } - - break; - - case ETradeSessionAction.Chat: - if (isUs) { - break; // don't care - } else { - this.emit('chat', event.text); - } + try { + for (let keyStr in status.events) { + if (!status.events.hasOwnProperty(keyStr)) { + continue; + } - break; + let key = parseInt(keyStr, 10); + if (key < this._logPos) { + this.emit('debug', 'Ignoring event ' + key + '; logPos is ' + this._logPos); + return; + } - default: - this.emit('debug', 'Unknown event ' + (ETradeSessionAction[event.action] || event.action)); - } + let event = status.events[key]; + await this._handleEvent(event); - if (this._logPos <= key) { - this._logPos = key + 1; + if (this._logPos <= key) { + this._logPos = key + 1; + } } - }); + } catch (ex) { + // This should only happen if the trade is fatally errored + this.emit('debug', ex.message); + return; + } } // all events have been processed. do some sanity checks to make sure we aren't out of sync @@ -504,7 +376,19 @@ TradeSession.prototype._handleTradeStatus = function(status) { let remote = status[who][thingToCheck]; if (local != remote) { - return this._terminateWithError(`Trade got out of sync. Local ${thingToCheck} status for ${who} is ${local} but we got ${remote}`); + if (thingToCheck == 'ready') { + this._handleEvent({ + "action": remote ? ETradeSessionAction.Ready : ETradeSessionAction.Unready, + who + }); + } else if (thingToCheck == 'confirmed' && remote) { + this._handleEvent({ + "action": ETradeSessionAction.Confirm, + who + }); + } + + this.emit('debug', `Trade got out of sync. Local ${thingToCheck} status for ${who} is ${local} but we got ${remote}. Forging appropriate event.`); } } }); @@ -522,7 +406,39 @@ TradeSession.prototype._handleTradeStatus = function(status) { if (remoteAssets) { if (remoteAssets.length != localAssets.length) { - return this._terminateWithError(`Trade got out of sync. Local asset count for ${who} is ${localAssets.length} but we got ${remoteAssets.length}`); + if (who == 'me') { + return this._terminateWithError(`Trade got out of sync. Local asset count for ${who} is ${localAssets.length} but we got ${remoteAssets.length}`); + } else { + for (let j = 0; j < remoteAssets.length; j++) { + if (!localAssets.some(localAsset => Helpers.itemEquals(localAsset, remoteAssets[i]))) { + // It's a new asset + await this._handleEvent({ + "action": ETradeSessionAction.AddItem, + who, + "appid": remoteAssets[i].appid, + "contextid": remoteAssets[i].contextid, + "assetid": remoteAssets[i].assetid || remoteAssets[i].id + }); + + this.emit('debug', `Trade got out of sync and we spoofed an AddItem event for ${who} for item ${remoteAssets[i].appid}_${remoteAssets[i].contextid}_${remoteAssets[i].assetid}`); + } + } + + for (let j = 0; j < localAssets.length; j++) { + if (!remoteAssets.some(remoteAsset => Helpers.itemEquals(remoteAsset, localAssets[i]))) { + // It's a removed asset + await this._handleEvent({ + "action": ETradeSessionAction.RemoveItem, + who, + "appid": localAssets[i].appid, + "contextid": localAssets[i].contextid, + "assetid": localAssets[i].assetid || localAssets[i].id + }); + + this.emit('debug', `Trade got out of sync and we spoofed a RemoveItem event for ${who} for item ${remoteAssets[i].appid}_${remoteAssets[i].contextid}_${remoteAssets[i].assetid}`); + } + } + } } // make sure the assets match up @@ -554,6 +470,168 @@ TradeSession.prototype._handleTradeStatus = function(status) { } }; +TradeSession.prototype._handleEvent = function(event) { + return new Promise((resolve, reject) => { + if (!event.steamid && !event.who) { + return reject(this._terminateWithError("Unknown who committed event " + event.action + "!")); + } + + let isUs = event.steamid == this._manager.steamID.getSteamID64() || event.who == 'me'; + this.emit('debug', 'Handling event ' + event.action + ' (' + (ETradeSessionAction[event.action] || event.action) + ')'); + + switch (parseInt(event.action, 10)) { + case ETradeSessionAction.AddItem: + this.me.ready = false; + this.them.ready = false; + + if (isUs) { + this.getInventory(event.appid, event.contextid, (err, inv) => { + if (err) { + return reject(this._terminateWithError("Cannot get my inventory: " + err.message)); + } + + let item = inv.filter(item => item.assetid == event.assetid)[0]; + let filtered; + if (!item) { + return reject(this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in our inventory even though it was added to the trade`)); + } + + if ((filtered = this.itemsToGive.filter(tradeItem => Helpers.itemEquals(tradeItem, item))).length > 0) { + // this item is already in the trade + filtered[0].pendingAdd = false; + } else { + // this item was added to the trade + this.itemsToGive.push(item); + this._fixAssetOrder(); + } + + resolve(); + }); + } else { + this._getTheirInventory(event.appid, event.contextid, (err, inv) => { + if (err) { + return reject(this._terminateWithError("Cannot get partner inventory: " + err.message)); + } + + let item = inv.filter(item => item.assetid == event.assetid)[0]; + if (!item) { + return reject(this._terminateWithError(`Could not find item ${event.appid}_${event.contextid}_${event.assetid} in partner's inventory even though it was added to the trade`)); + } + + if (this.itemsToReceive.some(tradeItem => Helpers.itemEquals(tradeItem, item))) { + // item is already in the trade + } else { + this.itemsToReceive.push(item); + this._fixAssetOrder(); + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('itemAdded', item); + }); + } + + resolve(); + }); + } + + break; + + case ETradeSessionAction.RemoveItem: + this.me.ready = false; + this.them.ready = false; + + let itemArray = isUs ? this.itemsToGive : this.itemsToReceive; + for (let i = 0; i < itemArray.length; i++) { + if (!Helpers.itemEquals(itemArray[i], event)) { + continue; + } + + // we found it + let item = itemArray.splice(i, 1)[0]; + if (!isUs) { + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('itemRemoved', item); + }); + } + } + + let filtered; + if (isUs && (filtered = this._itemsToGiveRemoving.filter(tradeItem => Helpers.itemEquals(event, tradeItem))).length > 0) { + this._itemsToGiveRemoving.splice(this._itemsToGiveRemoving.indexOf(filtered[0]), 1); + } + + resolve(); + break; + + case ETradeSessionAction.Ready: + if (isUs) { + this.me.ready = true; + } else if (!isUs && !this.them.ready) { + this.them.ready = true; + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('ready'); + }); + } + + resolve(); + break; + + case ETradeSessionAction.Unready: + if (isUs) { + this.me.ready = false; + } else if (!isUs && this.them.ready) { + this.them.ready = false; + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('unready'); + }); + } + + resolve(); + break; + + case ETradeSessionAction.Confirm: + if (isUs) { + this.me.confirmed = true; + } else if (!isUs && !this.them.confirmed) { + this.them.confirmed = true; + // it's very probable that the user will want to immediately take some action based on this, + // but steam will reject it if the version we send doesn't match the actual version + // delay by a tick so the version can update + process.nextTick(() => { + this.emit('confirm'); + }); + } + + resolve(); + break; + + case ETradeSessionAction.Chat: + if (isUs) { + break; // don't care + } else { + this.emit('chat', event.text); + } + + resolve(); + break; + + default: + this.emit('debug', 'Unknown event ' + (ETradeSessionAction[event.action] || event.action)); + resolve(); + } + }); +}; + TradeSession.prototype._setEnded = function() { this._ended = true; this._clearTradeStatusPoll(); @@ -561,7 +639,9 @@ TradeSession.prototype._setEnded = function() { TradeSession.prototype._terminateWithError = function(msg) { this._setEnded(); - this.emit('error', new Error(msg)); + let err = new Error(msg); + this.emit('error', err); + return err; }; TradeSession.prototype._getTheirInventory = function(appid, contextid, callback) { diff --git a/package.json b/package.json index a5c1dda..75b9348 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,6 @@ "steamid": "^1.1.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=8.0.0" } } diff --git a/v3.txt b/v3.txt index 4c49578..f375be8 100644 --- a/v3.txt +++ b/v3.txt @@ -1,7 +1,7 @@ BREAKING: - Removed loadInventory - Removed loadUserInventory -- now requires node 6 +- now requires node 8 NEW: - Trade sessions From 5ee4910ef7fefee70cabe080b09c26dfd67a99d1 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:49:25 -0500 Subject: [PATCH 22/36] Don't forget to resolve promise for our own chat messages --- lib/classes/TradeSession.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index 5e8b0f2..debe2fc 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -477,7 +477,7 @@ TradeSession.prototype._handleEvent = function(event) { } let isUs = event.steamid == this._manager.steamID.getSteamID64() || event.who == 'me'; - this.emit('debug', 'Handling event ' + event.action + ' (' + (ETradeSessionAction[event.action] || event.action) + ')'); + this.emit('debug', 'Handling event ' + (ETradeSessionAction[event.action] || event.action) + ' - ' + JSON.stringify(event)); switch (parseInt(event.action, 10)) { case ETradeSessionAction.AddItem: @@ -616,9 +616,7 @@ TradeSession.prototype._handleEvent = function(event) { break; case ETradeSessionAction.Chat: - if (isUs) { - break; // don't care - } else { + if (!isUs) { this.emit('chat', event.text); } From 7ba975491d9037fd9d6956b4d4d37af7ce1f9c7d Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 5 Feb 2019 01:55:31 -0500 Subject: [PATCH 23/36] Also spoof AddItem events for our own items --- lib/classes/TradeSession.js | 55 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/lib/classes/TradeSession.js b/lib/classes/TradeSession.js index debe2fc..b7d7cb9 100644 --- a/lib/classes/TradeSession.js +++ b/lib/classes/TradeSession.js @@ -406,37 +406,33 @@ TradeSession.prototype._handleTradeStatus = async function(status) { if (remoteAssets) { if (remoteAssets.length != localAssets.length) { - if (who == 'me') { - return this._terminateWithError(`Trade got out of sync. Local asset count for ${who} is ${localAssets.length} but we got ${remoteAssets.length}`); - } else { - for (let j = 0; j < remoteAssets.length; j++) { - if (!localAssets.some(localAsset => Helpers.itemEquals(localAsset, remoteAssets[i]))) { - // It's a new asset - await this._handleEvent({ - "action": ETradeSessionAction.AddItem, - who, - "appid": remoteAssets[i].appid, - "contextid": remoteAssets[i].contextid, - "assetid": remoteAssets[i].assetid || remoteAssets[i].id - }); - - this.emit('debug', `Trade got out of sync and we spoofed an AddItem event for ${who} for item ${remoteAssets[i].appid}_${remoteAssets[i].contextid}_${remoteAssets[i].assetid}`); - } + for (let j = 0; j < remoteAssets.length; j++) { + if (!localAssets.some(localAsset => Helpers.itemEquals(localAsset, remoteAssets[i]))) { + // It's a new asset + await this._handleEvent({ + "action": ETradeSessionAction.AddItem, + who, + "appid": remoteAssets[i].appid, + "contextid": remoteAssets[i].contextid, + "assetid": remoteAssets[i].assetid || remoteAssets[i].id + }); + + this.emit('debug', `Trade got out of sync and we spoofed an AddItem event for ${who} for item ${remoteAssets[i].appid}_${remoteAssets[i].contextid}_${remoteAssets[i].assetid}`); } + } - for (let j = 0; j < localAssets.length; j++) { - if (!remoteAssets.some(remoteAsset => Helpers.itemEquals(remoteAsset, localAssets[i]))) { - // It's a removed asset - await this._handleEvent({ - "action": ETradeSessionAction.RemoveItem, - who, - "appid": localAssets[i].appid, - "contextid": localAssets[i].contextid, - "assetid": localAssets[i].assetid || localAssets[i].id - }); - - this.emit('debug', `Trade got out of sync and we spoofed a RemoveItem event for ${who} for item ${remoteAssets[i].appid}_${remoteAssets[i].contextid}_${remoteAssets[i].assetid}`); - } + for (let j = 0; j < localAssets.length; j++) { + if (!remoteAssets.some(remoteAsset => Helpers.itemEquals(remoteAsset, localAssets[i]))) { + // It's a removed asset + await this._handleEvent({ + "action": ETradeSessionAction.RemoveItem, + who, + "appid": localAssets[i].appid, + "contextid": localAssets[i].contextid, + "assetid": localAssets[i].assetid || localAssets[i].id + }); + + this.emit('debug', `Trade got out of sync and we spoofed a RemoveItem event for ${who} for item ${remoteAssets[i].appid}_${remoteAssets[i].contextid}_${remoteAssets[i].assetid}`); } } } @@ -499,6 +495,7 @@ TradeSession.prototype._handleEvent = function(event) { if ((filtered = this.itemsToGive.filter(tradeItem => Helpers.itemEquals(tradeItem, item))).length > 0) { // this item is already in the trade filtered[0].pendingAdd = false; + this._fixAssetOrder(); } else { // this item was added to the trade this.itemsToGive.push(item); From 63a92c38bf37066661c45d9cbaea4c524e5afba6 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Thu, 7 Feb 2019 00:25:30 -0500 Subject: [PATCH 24/36] Removed async dependency --- lib/assets.js | 152 +++++++++++++++++++++++++------------------------- lib/index.js | 56 ++++++++++--------- package.json | 1 - 3 files changed, 104 insertions(+), 105 deletions(-) diff --git a/lib/assets.js b/lib/assets.js index 6e3cb7a..3a1ebb5 100644 --- a/lib/assets.js +++ b/lib/assets.js @@ -1,13 +1,12 @@ "use strict"; -const TradeOfferManager = require('./index.js'); -const Async = require('async'); const EconItem = require('./classes/EconItem.js'); +const TradeOfferManager = require('./index.js'); const ITEMS_PER_CLASSINFO_REQUEST = 100; TradeOfferManager.prototype._digestDescriptions = function(descriptions) { - var cache = this._assetCache; + let cache = this._assetCache; if (!this._language) { return; @@ -28,7 +27,7 @@ TradeOfferManager.prototype._digestDescriptions = function(descriptions) { }; TradeOfferManager.prototype._mapItemsToDescriptions = function(appid, contextid, items) { - var cache = this._assetCache; + let cache = this._assetCache; if (!(items instanceof Array)) { items = Object.keys(items).map(key => items[key]); @@ -38,8 +37,8 @@ TradeOfferManager.prototype._mapItemsToDescriptions = function(appid, contextid, item.appid = appid || item.appid; item.contextid = contextid || item.contextid; - var key = `${item.appid}_${item.classid}_${item.instanceid || '0'}`; - var entry = cache.get(key); + let key = `${item.appid}_${item.classid}_${item.instanceid || '0'}`; + let entry = cache.get(key); if (!entry) { // This item isn't in our description cache return new EconItem(item); @@ -61,7 +60,7 @@ TradeOfferManager.prototype._hasDescription = function(item, appid) { }; TradeOfferManager.prototype._addDescriptions = function(items, callback) { - var descriptionRequired = items.filter(item => !this._hasDescription(item)); + let descriptionRequired = items.filter(item => !this._hasDescription(item)); if (descriptionRequired.length == 0) { callback(null, this._mapItemsToDescriptions(null, null, items)); @@ -78,9 +77,9 @@ TradeOfferManager.prototype._addDescriptions = function(items, callback) { }; TradeOfferManager.prototype._requestDescriptions = function(classes, callback) { - var getFromSteam = () => { - var apps = []; - var appids = []; + let getFromSteam = async () => { + let apps = []; + let appids = []; // Split this out into appids classes.forEach((item) => { @@ -89,10 +88,10 @@ TradeOfferManager.prototype._requestDescriptions = function(classes, callback) { return; } - var index = appids.indexOf(item.appid); + let index = appids.indexOf(item.appid); if (index == -1) { index = appids.push(item.appid) - 1; - var arr = []; + let arr = []; arr.appid = item.appid; apps.push(arr); } @@ -103,89 +102,88 @@ TradeOfferManager.prototype._requestDescriptions = function(classes, callback) { } }); - Async.map(apps, (app, cb) => { - var chunks = []; - var items = []; - - // Split this into chunks of ITEMS_PER_CLASSINFO_REQUEST items - while (app.length > 0) { - chunks.push(app.splice(0, ITEMS_PER_CLASSINFO_REQUEST)); - } + let appPromises = []; + apps.forEach((app) => { + appPromises.push(new Promise(async (resolve, reject) => { + let chunks = []; - Async.each(chunks, (chunk, chunkCb) => { - var input = { - "appid": app.appid, - "language": this._language, - "class_count": chunk.length - }; - - chunk.forEach((item, index) => { - var parts = item.split('_'); - input['classid' + index] = parts[0]; - input['instanceid' + index] = parts[1]; - }); + // Split this into chunks of ITEMS_PER_CLASSINFO_REQUEST items + while (app.length > 0) { + chunks.push(app.splice(0, ITEMS_PER_CLASSINFO_REQUEST)); + } - this.emit('debug', "Requesting classinfo for " + chunk.length + " items from app " + app.appid); - this._apiCall('GET', { - "iface": "ISteamEconomy", - "method": "GetAssetClassInfo" - }, 1, input, (err, body) => { - if (err) { - chunkCb(err); - return; - } - - if (!body.result || !body.result.success) { - chunkCb(new Error("Invalid API response")); - return; - } - - var chunkItems = Object.keys(body.result).map((id) => { - if (!id.match(/^\d+(_\d+)?$/)) { - return null; - } - - var item = body.result[id]; - item.appid = app.appid; - return item; - }).filter(item => !!item); - - items = items.concat(chunkItems); - - chunkCb(null); + let chunkPromises = []; + chunks.forEach((chunk) => { + chunkPromises.push(new Promise((resolve, reject) => { + let input = { + "appid": app.appid, + "language": this._language, + "class_count": chunk.length + }; + + chunk.forEach((item, index) => { + let parts = item.split('_'); + input['classid' + index] = parts[0]; + input['instanceid' + index] = parts[1]; + }); + + this.emit('debug', "Requesting classinfo for " + chunk.length + " items from app " + app.appid); + this._apiCall('GET', { + "iface": "ISteamEconomy", + "method": "GetAssetClassInfo" + }, 1, input, (err, body) => { + if (err) { + return reject(err); + } + + if (!body.result || !body.result.success) { + return reject(new Error("Invalid API response")); + } + + let chunkItems = Object.keys(body.result).map((id) => { + if (!id.match(/^\d+(_\d+)?$/)) { + return null; + } + + let item = body.result[id]; + item.appid = app.appid; + return item; + }).filter(item => !!item); + + this._digestDescriptions(chunkItems); + resolve(); + }); + })); }); - }, (err) => { - if (err) { - cb(err); - } else { - cb(null, items); - } - }); - }, (err, result) => { - if (err) { - callback(err); - return; - } - result.forEach(this._digestDescriptions.bind(this)); - callback(); + await Promise.all(chunkPromises); + resolve(); + })); }); + + try { + await Promise.all(appPromises); + } catch (ex) { + return callback(ex); + } + + callback(); }; // Get whatever we can from disk - var filenames = classes.map(item => `asset_${item.appid}_${item.classid}_${item.instanceid || '0'}.json`); + let filenames = classes.map(item => `asset_${item.appid}_${item.classid}_${item.instanceid || '0'}.json`); this._getFromDisk(filenames, (err, files) => { if (err) { getFromSteam(); return; } - for (var filename in files) { + for (let filename in files) { if (!files.hasOwnProperty(filename)) { continue; } - var match = filename.match(/asset_(\d+_\d+_\d+)\.json/); + let match = filename.match(/asset_(\d+_\d+_\d+)\.json/); if (!match) { this.emit('debug', "Shouldn't be possible, but filename " + filename + " doesn't match regex"); continue; // shouldn't be possible diff --git a/lib/index.js b/lib/index.js index 07fac5f..ffa301b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,10 +3,11 @@ require('@doctormckay/stats-reporter').setup(require('../package.json')); const AppDirectory = require('appdirectory'); -const Async = require('async'); +const EventEmitter = require('events').EventEmitter; const FileManager = require('file-manager'); const StdLib = require('@doctormckay/stdlib'); const SteamCommunity = require('steamcommunity'); +const Util = require('util'); const Zlib = require('zlib'); const Helpers = require('./helpers.js'); @@ -23,7 +24,7 @@ const SteamID = TradeOfferManager.SteamID = require('steamid'); const TradeOffer = require('./classes/TradeOffer.js'); -require('util').inherits(TradeOfferManager, require('events').EventEmitter); +Util.inherits(TradeOfferManager, EventEmitter); function TradeOfferManager(options) { options = options || {}; @@ -84,7 +85,7 @@ function TradeOfferManager(options) { this._language = 'zh'; this._languageName = 'tchinese'; } else { - var lang = require('languages').getLanguageInfo(this._language); + let lang = require('languages').getLanguageInfo(this._language); if (!lang.name) { this._language = null; this._languageName = null; @@ -134,7 +135,7 @@ TradeOfferManager.prototype.setCookies = function(cookies, familyViewPin, callba if (this._getPollDataFromDisk) { delete this._getPollDataFromDisk; - var filename = 'polldata_' + this.steamID + '.json'; + let filename = 'polldata_' + this.steamID + '.json'; this._getFromDisk([filename], (err, files) => { if (files[filename]) { try { @@ -274,8 +275,8 @@ TradeOfferManager.prototype._getFromDisk = function(filenames, callback) { filenames = filenames.map(name => name + '.gz'); } - this.storage.readFiles(filenames, (err, results) => { - var files = {}; + this.storage.readFiles(filenames, async (err, results) => { + let files = {}; results.forEach((file) => { if (file.contents) { files[file.filename] = file.contents; @@ -283,24 +284,25 @@ TradeOfferManager.prototype._getFromDisk = function(filenames, callback) { }); if (this._dataGzip) { - Async.mapValues(files, (content, filename, callback) => { - Zlib.gunzip(content, (err, data) => { - if (err) { - callback(null, null); - } else { - callback(null, data); - } + let filenames = Object.keys(files); + let unzipPromises = filenames.map((filename) => { + return new Promise((resolve, reject) => { + Zlib.gunzip(files[filename], (err, data) => { + resolve(data || null); + }); }); - }, (err, files) => { - var renamed = {}; - for (var i in files) { - if (files.hasOwnProperty(i)) { - renamed[i.replace(/\.gz$/, '')] = files[i]; - } - } + }); - callback(null, renamed); + let unzipped = await Promise.all(unzipPromises); + let renamed = {}; + unzipped.forEach((unzippedContent, idx) => { + let filename = filenames[idx]; + if (files.hasOwnProperty(filename)) { + renamed[filename.replace(/\.gz$/, '')] = unzippedContent; + } }); + + callback(null, renamed); } else { callback(null, files); } @@ -399,7 +401,7 @@ TradeOfferManager.prototype.getOffersContainingItems = function(items, includeIn TradeOfferManager.prototype.createOffer = function(partner, token) { if (typeof partner === 'string' && partner.match(/^https?:\/\//)) { // It's a trade URL I guess - var url = require('url').parse(partner, true); + let url = require('url').parse(partner, true); if (!url.query.partner) { throw new Error("Invalid trade URL"); } @@ -408,7 +410,7 @@ TradeOfferManager.prototype.createOffer = function(partner, token) { token = url.query.token; } - var offer = new TradeOffer(this, partner, token); + let offer = new TradeOffer(this, partner, token); offer.isOurOffer = true; offer.fromRealTimeTrade = false; return offer; @@ -473,7 +475,7 @@ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callb // Currently the GetTradeOffers API doesn't include app_data, so we need to get descriptions from the WebAPI - var options = { + let options = { "get_sent_offers": 1, "get_received_offers": 1, "get_descriptions": 0, @@ -495,7 +497,7 @@ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callb } // Make sure at least some offers are well-formed. Apparently some offers can be empty just forever. Because Steam. - var allOffers = (body.response.trade_offers_sent || []).concat(body.response.trade_offers_received || []); + let allOffers = (body.response.trade_offers_sent || []).concat(body.response.trade_offers_received || []); if (allOffers.length > 0 && (allOffers.every(Helpers.offerMalformed) || allOffers.some(Helpers.offerSuperMalformed))) { reject(new Error("Data temporarily unavailable")); return; @@ -512,8 +514,8 @@ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callb return; } - var sent = (body.response.trade_offers_sent || []).map(data => Helpers.createOfferFromData(this, data)); - var received = (body.response.trade_offers_received || []).map(data => Helpers.createOfferFromData(this, data)); + let sent = (body.response.trade_offers_sent || []).map(data => Helpers.createOfferFromData(this, data)); + let received = (body.response.trade_offers_received || []).map(data => Helpers.createOfferFromData(this, data)); accept({sent, received}); this.emit('offerList', filter, sent, received); diff --git a/package.json b/package.json index 75b9348..bbd1b0f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@doctormckay/stats-reporter": "^1.0.3", "@doctormckay/stdlib": "^1.5.1", "appdirectory": "^0.1.0", - "async": "^2.6.0", "deep-equal": "^1.0.1", "file-manager": "^1.0.1", "languages": "^0.1.3", From 95e4162d2330efa65d7bcafe67643f4eb0f06743 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Thu, 7 Feb 2019 00:55:07 -0500 Subject: [PATCH 25/36] Removed deep-equal dependency --- lib/classes/EconItem.js | 14 ++++------ lib/helpers.js | 16 ++++++------ lib/polling.js | 57 +++++++++++++++++++++-------------------- lib/webapi.js | 13 +++++----- package.json | 3 +-- 5 files changed, 49 insertions(+), 54 deletions(-) diff --git a/lib/classes/EconItem.js b/lib/classes/EconItem.js index 86416ee..1c7e8d8 100644 --- a/lib/classes/EconItem.js +++ b/lib/classes/EconItem.js @@ -36,7 +36,7 @@ function EconItem(item) { this.market_marketable_restriction = (this.market_marketable_restriction ? parseInt(this.market_marketable_restriction, 10) : 0); if (this.appid == 753 && !this.market_fee_app && this.market_hash_name) { - var match = this.market_hash_name.match(/^(\d+)\-/); + let match = this.market_hash_name.match(/^(\d+)-/); if (match) { this.market_fee_app = parseInt(match[1], 10); } @@ -48,8 +48,8 @@ function fixArray(obj) { return []; } - var array = []; - for (var i in obj) { + let array = []; + for (let i in obj) { if (obj.hasOwnProperty(i)) { array[i] = obj[i]; } @@ -74,15 +74,11 @@ function fixTags(tags) { } EconItem.prototype.getImageURL = function() { - return "https://steamcommunity-a.akamaihd.net/economy/image/" + this.icon_url + "/"; + return `https://steamcommunity-a.akamaihd.net/economy/image/${this.icon_url}/`; }; EconItem.prototype.getLargeImageURL = function() { - if (!this.icon_url_large) { - return this.getImageURL(); - } - - return "https://steamcommunity-a.akamaihd.net/economy/image/" + this.icon_url_large + "/"; + return this.icon_url_large ? `https://steamcommunity-a.akamaihd.net/economy/image/${this.icon_url_large}/` : this.getImageURL(); }; EconItem.prototype.getTag = function(category) { diff --git a/lib/helpers.js b/lib/helpers.js index 2a98c62..dd2dc3e 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -3,10 +3,10 @@ const SteamID = require('steamid'); const VM = require('vm'); -const EResult = require('../resources/EResult.js'); const EconItem = require('./classes/EconItem.js'); -const TradeOffer = require('./classes/TradeOffer.js'); const EConfirmationMethod = require('../resources/EConfirmationMethod.js'); +const EResult = require('../resources/EResult.js'); +const TradeOffer = require('./classes/TradeOffer.js'); const Helpers = module.exports; @@ -19,7 +19,7 @@ Helpers.makeAnError = function(error, callback, body) { if (body && body.strError) { error = new Error(body.strError); - var match = body.strError.match(/\((\d+)\)$/); + let match = body.strError.match(/\((\d+)\)$/); if (match) { error.eresult = parseInt(match[1], 10); } @@ -80,7 +80,7 @@ Helpers.checkNeededDescriptions = function(manager, offers, callback) { return; } - var items = []; + let items = []; offers.forEach((offer) => { (offer.items_to_give || []).concat(offer.items_to_receive || []).forEach((item) => { if (!manager._hasDescription(item)) { @@ -98,7 +98,7 @@ Helpers.checkNeededDescriptions = function(manager, offers, callback) { }; Helpers.createOfferFromData = function(manager, data) { - var offer = new TradeOffer(manager, new SteamID('[U:1:' + data.accountid_other + ']')); + let offer = new TradeOffer(manager, new SteamID('[U:1:' + data.accountid_other + ']')); offer.id = data.tradeofferid.toString(); offer.message = data.message; offer.state = data.trade_offer_state; @@ -132,7 +132,7 @@ Helpers.getUserDetailsFromTradeWindow = function(manager, url, callback) { return; } - let script = body.match(/\n\W*"); - if (pos != -1) { - script = script.substring(0, pos); - } + let script = body.match(/\n\W*'); + if (pos != -1) { + script = script.substring(0, pos); + } - VM.runInContext(script, vmContext); - - let me = { - "personaName": vmContext.g_strYourPersonaName, - "contexts": vmContext.g_rgAppContextData - }; - - let them = { - "personaName": vmContext.g_strTradePartnerPersonaName, - "contexts": vmContext.g_rgPartnerAppContextData || null, - "probation": vmContext.g_bTradePartnerProbation - }; - - // Escrow - let myEscrow = body.match(/let g_daysMyEscrow = (\d+);/); - let theirEscrow = body.match(/let g_daysTheirEscrow = (\d+);/); - if (myEscrow && theirEscrow) { - me.escrowDays = parseInt(myEscrow[1], 10); - them.escrowDays = parseInt(theirEscrow[1], 10); - } + // Run this script in a VM + let vmContext = VM.createContext({ + UserYou: { + SetProfileURL: function() {}, + SetSteamId: function() {} + }, + UserThem: { + SetProfileURL: function() {}, + SetSteamId: function() {} + }, + $J: function() {}, + Event: { + observe: function() {} + }, + document: null + }); + + VM.runInContext(script, vmContext); + + let me = { + personaName: vmContext.g_strYourPersonaName, + contexts: vmContext.g_rgAppContextData + }; + + let them = { + personaName: vmContext.g_strTradePartnerPersonaName, + contexts: vmContext.g_rgPartnerAppContextData || null, + probation: vmContext.g_bTradePartnerProbation + }; + + // Escrow + let myEscrow = body.match(/let g_daysMyEscrow = (\d+);/); + let theirEscrow = body.match(/let g_daysTheirEscrow = (\d+);/); + if (myEscrow && theirEscrow) { + me.escrowDays = parseInt(myEscrow[1], 10); + them.escrowDays = parseInt(theirEscrow[1], 10); + } - // Avatars - let myAvatar = body.match(new RegExp('[^')); - let theirAvatar = body.match(new RegExp('[^')); - if (myAvatar) { - me.avatarIcon = myAvatar[1]; - me.avatarMedium = myAvatar[1].replace('.jpg', '_medium.jpg'); - me.avatarFull = myAvatar[1].replace('.jpg', '_full.jpg'); - } + // Avatars + let myAvatar = body.match(new RegExp('[^')); + let theirAvatar = body.match(new RegExp('[^')); + if (myAvatar) { + me.avatarIcon = myAvatar[1]; + me.avatarMedium = myAvatar[1].replace('.jpg', '_medium.jpg'); + me.avatarFull = myAvatar[1].replace('.jpg', '_full.jpg'); + } - if (theirAvatar) { - them.avatarIcon = theirAvatar[1]; - them.avatarMedium = theirAvatar[1].replace('.jpg', '_medium.jpg'); - them.avatarFull = theirAvatar[1].replace('.jpg', '_full.jpg'); - } + if (theirAvatar) { + them.avatarIcon = theirAvatar[1]; + them.avatarMedium = theirAvatar[1].replace('.jpg', '_medium.jpg'); + them.avatarFull = theirAvatar[1].replace('.jpg', '_full.jpg'); + } - callback(null, me, them); + resolve({me, them}); + }); }); }; From f05ffa31a84f8c60e264a5a214e46f6bbe12c685 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Mon, 29 Mar 2021 00:54:22 -0400 Subject: [PATCH 32/36] Promisified index.js --- lib/index.js | 138 +++++++++++++++++++++++---------------------------- package.json | 2 +- 2 files changed, 64 insertions(+), 76 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5b1218e..398df96 100644 --- a/lib/index.js +++ b/lib/index.js @@ -125,53 +125,41 @@ function TradeOfferManager(options) { * @returns {Promise} */ TradeOfferManager.prototype.setCookies = function(cookies, familyViewPin, callback) { - return StdLib.Promises.callbackPromise([], callback, true, (accept, reject) => { - if (typeof familyViewPin === 'function') { - callback = familyViewPin; - familyViewPin = null; - } + if (typeof familyViewPin === 'function') { + callback = familyViewPin; + familyViewPin = null; + } + return StdLib.Promises.callbackPromise([], callback, true, async (resolve, reject) => { this._community.setCookies(cookies); this.steamID = this._community.steamID; if (this._getPollDataFromDisk) { delete this._getPollDataFromDisk; let filename = 'polldata_' + this.steamID + '.json'; - this._getFromDisk([filename]).then((files) => { - if (files[filename]) { - try { - this.pollData = JSON.parse(files[filename].toString('utf8')); - } catch (ex) { - this.emit('debug', 'Error parsing poll data from disk: ' + ex.message); - } + let files = await this._getFromDisk([filename]); + if (files[filename]) { + try { + this.pollData = JSON.parse(files[filename].toString('utf8')); + } catch (ex) { + this.emit('debug', 'Error parsing poll data from disk: ' + ex.message); } - }); + } } - const checkDone = (err) => { - if (!err) { - if (this._languageName) { - this._community.setCookies(['Steam_Language=' + this._languageName]); - } - - clearTimeout(this._pollTimer); - this.doPoll(); - } + if (familyViewPin) { + await this.parentalUnlock(familyViewPin); + } - err ? reject(err) : accept(); - }; + await this._checkApiKey(); - if (familyViewPin) { - this.parentalUnlock(familyViewPin, (err) => { - if (err) { - reject(err); - } else { - this._checkApiKey(checkDone); - } - }); - } else { - this._checkApiKey(checkDone); + if (this._languageName) { + this._community.setCookies(['Steam_Language=' + this._languageName]); } + + clearTimeout(this._pollTimer); + this.doPoll(); + resolve(); }); }; @@ -192,35 +180,33 @@ TradeOfferManager.prototype.shutdown = function() { * @returns {Promise} */ TradeOfferManager.prototype.parentalUnlock = function(pin, callback) { - return StdLib.Promises.callbackPromise([], callback, true, (accept, reject) => { + return StdLib.Promises.callbackPromise([], callback, true, (resolve, reject) => { this._community.parentalUnlock(pin, (err) => { - err ? reject(err) : accept(); + err ? reject(err) : resolve(); }); }); }; /** * Make sure we have an API key, and if we don't, get one. - * @param {function} callback + * @returns {Promise} * @private */ -TradeOfferManager.prototype._checkApiKey = function(callback) { - if (this.apiKey) { - if (callback) { - callback(); - } - - return; - } - - this._community.getWebApiKey(this._domain, (err, key) => { - if (err) { - callback(err); +TradeOfferManager.prototype._checkApiKey = function() { + return new Promise((resolve, reject) => { + if (this.apiKey) { + resolve(); return; } - this.apiKey = key; - callback(); + this._community.getWebApiKey(this._domain, (err, key) => { + if (err) { + return reject(err); + } + + this.apiKey = key; + resolve(); + }); }); }; @@ -313,16 +299,17 @@ TradeOfferManager.prototype._getFromDisk = function(filenames) { * @param {int} contextid - The ID of the "context" within the game you want to retrieve * @param {boolean} tradableOnly - true to get only tradable items and currencies * @param {function} [callback] - * @returns {Promise} + * @returns {Promise<{inventory: Object[], currencies: Object[]}>} */ TradeOfferManager.prototype.getInventoryContents = function(appid, contextid, tradableOnly, callback) { - // are we logged in? - if (!this.steamID) { - callback(new Error("Not Logged In")); - return; - } + return StdLib.Promises.callbackPromise(['inventory', 'currencies'], callback, false, async (resolve, reject) => { + // are we logged in? + if (!this.steamID) { + return reject(new Error('Not Logged In')); + } - this.getUserInventoryContents(this.steamID, appid, contextid, tradableOnly, callback); + resolve(await this.getUserInventoryContents(this.steamID, appid, contextid, tradableOnly)); + }); }; /** @@ -332,15 +319,15 @@ TradeOfferManager.prototype.getInventoryContents = function(appid, contextid, tr * @param {int} contextid - The ID of the "context" within the game you want to retrieve * @param {boolean} tradableOnly - true to get only tradable items and currencies * @param {function} [callback] - * @returns {Promise} + * @returns {Promise<{inventory: Object[], currencies: Object[]}>} */ TradeOfferManager.prototype.getUserInventoryContents = function(sid, appid, contextid, tradableOnly, callback) { - return StdLib.Promises.callbackPromise(['inventory', 'currencies'], callback, false, (accept, reject) => { - this._community.getUserInventoryContents(sid, appid, contextid, tradableOnly, this._languageName || "english", (err, inventory, currencies) => { + return StdLib.Promises.callbackPromise(['inventory', 'currencies'], callback, false, (resolve, reject) => { + this._community.getUserInventoryContents(sid, appid, contextid, tradableOnly, this._languageName || 'english', (err, inventory, currencies) => { if (err) { reject(err); } else { - accept({inventory, currencies}); + resolve({inventory, currencies}); } }); }); @@ -349,12 +336,12 @@ TradeOfferManager.prototype.getUserInventoryContents = function(sid, appid, cont /** * Get the token parameter from your account's Trade URL * @param {function} [callback] - * @returns {Promise} + * @returns {Promise<{token: string}>} */ TradeOfferManager.prototype.getOfferToken = function(callback) { - return StdLib.Promises.callbackPromise(['token'], callback, false, (accept, reject) => { + return StdLib.Promises.callbackPromise(['token'], callback, false, (resolve, reject) => { this._community.getTradeURL((err, url, token) => { - err ? reject(err) : accept({token}); + err ? reject(err) : resolve({token}); }); }); }; @@ -364,24 +351,23 @@ TradeOfferManager.prototype.getOfferToken = function(callback) { * @param {object[]|object} items - One object or an array of objects, where each object contains appid+contextid+(assetid|id) properties * @param {boolean} [includeInactive=false] If true, also include trade offers that are not Active * @param {function} [callback] - * @returns {Promise} + * @returns {Promise<{sent: Object[], received: Object[]}>} */ TradeOfferManager.prototype.getOffersContainingItems = function(items, includeInactive, callback) { - return StdLib.Promises.callbackPromise(['sent', 'received'], callback, false, (accept, reject) => { - if (typeof includeInactive === 'function') { - callback = includeInactive; - includeInactive = false; - } + if (typeof includeInactive === 'function') { + callback = includeInactive; + includeInactive = false; + } + return StdLib.Promises.callbackPromise(['sent', 'received'], callback, false, (resolve, reject) => { includeInactive = includeInactive || false; - if (typeof items.length === 'undefined') { - // not an array + if (!Array.isArray(items)) { items = [items]; } this.getOffers(includeInactive ? EOfferFilter.All : EOfferFilter.ActiveOnly, (err, sent, received) => { - err ? reject(err) : accept({"sent": sent.filter(filterFunc), "received": received.filter(filterFunc)}); + err ? reject(err) : resolve({sent: sent.filter(filterFunc), received: received.filter(filterFunc)}); }); function filterFunc(offer) { @@ -422,11 +408,12 @@ require('./polling.js'); * Get a trade offer that is already sent (either by you or to you) * @param {int|string} id - The offer's numeric ID * @param {function} [callback] - * @returns {Promise} + * @returns {Promise<{offer: Object}>} */ TradeOfferManager.prototype.getOffer = function(id, callback) { return StdLib.Promises.callbackPromise(['offer'], callback, false, async (resolve, reject) => { let body = await this._apiCall('GET', 'GetTradeOffer', 1, {tradeofferid: id}); + if (!body.response) { return reject(new Error('Malformed API response')); } @@ -451,6 +438,7 @@ TradeOfferManager.prototype.getOffer = function(id, callback) { * @param {int} filter * @param {Date} [historicalCutoff] - Pass a Date object in the past along with ActiveOnly to also get offers that were updated since this time * @param {function} [callback] + * @returns {Promise<{sent: Object[], received: Object[]}>} */ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callback) { return StdLib.Promises.callbackPromise(['sent', 'received'], callback, false, async (resolve, reject) => { diff --git a/package.json b/package.json index 641c35a..eded13c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "update-resources": "node scripts/update-resources.js" }, "dependencies": { - "@doctormckay/stdlib": "^1.9.0", + "@doctormckay/stdlib": "^1.14.0", "appdirectory": "^0.1.0", "file-manager": "^2.0.0", "languages": "^0.1.3", From d8a99a5c4e8279a5c7f073152feb64a1060f497d Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Mon, 29 Mar 2021 01:19:44 -0400 Subject: [PATCH 33/36] Promisified TradeOffer --- lib/assets.js | 4 +- lib/classes/TradeOffer.js | 620 +++++++++++++++++++------------------- 2 files changed, 314 insertions(+), 310 deletions(-) diff --git a/lib/assets.js b/lib/assets.js index 61c9064..23b5a4b 100644 --- a/lib/assets.js +++ b/lib/assets.js @@ -35,8 +35,8 @@ TradeOfferManager.prototype._digestDescriptions = function(descriptions) { /** * Attaches item descriptions to some items from our internal description cache. * Does not request missing descriptions. - * @param {int} appid - * @param {int} contextid + * @param {int|null} appid + * @param {int|null} contextid * @param {Object[]|Object} items * @returns {EconItem[]} * @private diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index 38db493..d2bd6ef 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -1,5 +1,6 @@ "use strict"; +const StdLib = require('@doctormckay/stdlib'); const SteamID = require('steamid'); const Helpers = require('../helpers.js'); @@ -9,7 +10,7 @@ const ETradeStatus = require('../../resources/ETradeStatus.js'); function TradeOffer(manager, partner, token) { if (typeof partner === 'number') { - throw new Error('Input SteamID ' + this.partner + ' is a number and not a string; did you make a mistake?'); + throw new Error(`Input SteamID ${this.partner} is a number and not a string; did you make a mistake?`); } else if (typeof partner === 'string') { this.partner = new SteamID(partner); } else { @@ -17,33 +18,33 @@ function TradeOffer(manager, partner, token) { } if (!this.partner.isValid || !this.partner.isValid() || this.partner.type != SteamID.Type.INDIVIDUAL) { - throw new Error("Invalid input SteamID " + this.partner); + throw new Error('Invalid input SteamID ' + this.partner); } Object.defineProperties(this, { - "_countering": { - "configurable": true, - "enumerable": false, - "writable": true, - "value": null + _countering: { + configurable: true, + enumerable: false, + writable: true, + value: null }, - "_tempData": { - "configurable": true, - "enumerable": false, - "writable": true, - "value": {} + _tempData: { + configurable: true, + enumerable: false, + writable: true, + value: {} }, - "_token": { - "configurable": true, - "enumerable": false, - "writable": true, - "value": token + _token: { + configurable: true, + enumerable: false, + writable: true, + value: token }, - "manager": { - "configurable": false, - "enumerable": false, - "writable": false, - "value": manager + manager: { + configurable: false, + enumerable: false, + writable: false, + value: manager } }); @@ -60,7 +61,7 @@ function TradeOffer(manager, partner, token) { this.fromRealTimeTrade = null; this.confirmationMethod = null; this.escrowEnds = null; - this.rawJson = ""; + this.rawJson = ''; } /** @@ -161,28 +162,18 @@ TradeOffer.prototype.data = function(key, value) { /** * Get the tradable contents of your trade partner's inventory for a specific context. - * @deprecated Use getPartnerInventoryContents instead * @param {int} appid * @param {int} contextid - * @param {function} callback - */ -TradeOffer.prototype.loadPartnerInventory = function(appid, contextid, callback) { - this.manager.loadUserInventory(this.partner, appid, contextid, true, callback); -}; - -/** - * Get the tradable contents of your trade partner's inventory for a specific context. - * @param {int} appid - * @param {int} contextid - * @param {function} callback + * @param {function} [callback] + * @returns {Promise<{inventory: Object[], currencies: Object[]}>} */ TradeOffer.prototype.getPartnerInventoryContents = function(appid, contextid, callback) { - this.manager.getUserInventoryContents(this.partner, appid, contextid, true, callback); + return this.manager.getUserInventoryContents(this.partner, appid, contextid, true, callback); }; /** * Add one of your items to this trade offer. - * @param {{appid, contextid, [assetid], [id]}} item + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}} item * @returns {boolean} - Was the item added? */ TradeOffer.prototype.addMyItem = function(item) { @@ -191,7 +182,7 @@ TradeOffer.prototype.addMyItem = function(item) { /** * Add one or more of your items to this trade offer. - * @param {{appid, contextid, [assetid], [id]}[]} items + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}[]} items * @returns {number} - Number of items added */ TradeOffer.prototype.addMyItems = function(items) { @@ -207,12 +198,12 @@ TradeOffer.prototype.addMyItems = function(items) { /** * Remove one of your items from this trade offer. - * @param {{appid, contextid, [assetid], [id]}} item + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}} item * @returns {boolean} - Was the item removed? */ TradeOffer.prototype.removeMyItem = function(item) { if (this.id) { - throw new Error("Cannot remove items from an already-sent offer"); + throw new Error('Cannot remove items from an already-sent offer'); } for (let i = 0; i < this.itemsToGive.length; i++) { @@ -227,7 +218,7 @@ TradeOffer.prototype.removeMyItem = function(item) { /** * Remove one or more of your items from this trade offer. - * @param {{appid, contextid, [assetid], [id]}[]} items + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}[]} items * @returns {number} - Number of items removed */ TradeOffer.prototype.removeMyItems = function(items) { @@ -243,7 +234,7 @@ TradeOffer.prototype.removeMyItems = function(items) { /** * Add one of their items to this trade offer. - * @param {{appid, contextid, [assetid], [id]}} item + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}} item * @returns {boolean} - Was the item added? */ TradeOffer.prototype.addTheirItem = function(item) { @@ -252,7 +243,7 @@ TradeOffer.prototype.addTheirItem = function(item) { /** * Add one or more of their items to this trade offer. - * @param {{appid, contextid, [assetid], [id]}[]} items + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}[]} items * @returns {number} - Number of items added */ TradeOffer.prototype.addTheirItems = function(items) { @@ -268,12 +259,12 @@ TradeOffer.prototype.addTheirItems = function(items) { /** * Remove one of their items from this trade offer. - * @param {{appid, contextid, [assetid], [id]}} item + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}} item * @returns {boolean} - Was the item removed? */ TradeOffer.prototype.removeTheirItem = function(item) { if (this.id) { - throw new Error("Cannot remove items from an already-sent offer"); + throw new Error('Cannot remove items from an already-sent offer'); } for (let i = 0; i < this.itemsToReceive.length; i++) { @@ -288,7 +279,7 @@ TradeOffer.prototype.removeTheirItem = function(item) { /** * Remove one or more of their items from this trade offer. - * @param {{appid, contextid, [assetid], [id]}[]} items + * @param {{appid: int, contextid: int|string, assetid?: int|string, id?: int|string}[]} items * @returns {number} - Number of items removed */ TradeOffer.prototype.removeTheirItems = function(items) { @@ -310,19 +301,19 @@ TradeOffer.prototype.removeTheirItems = function(items) { */ function addItem(details, offer, list) { if (offer.id) { - throw new Error("Cannot add items to an already-sent offer"); + throw new Error('Cannot add items to an already-sent offer'); } if (typeof details.appid === 'undefined' || typeof details.contextid === 'undefined' || (typeof details.assetid === 'undefined' && typeof details.id === 'undefined')) { - throw new Error("Missing appid, contextid, or assetid parameter"); + throw new Error('Missing appid, contextid, or assetid parameter'); } let item = { - "id": (details.id || details.assetid).toString(), // always needs to be a string - "assetid": (details.assetid || details.id).toString(), // always needs to be a string - "appid": parseInt(details.appid, 10), // always needs to be an int - "contextid": details.contextid.toString(), // always needs to be a string - "amount": parseInt(details.amount || 1, 10) // always needs to be an int + id: (details.id || details.assetid).toString(), // always needs to be a string + assetid: (details.assetid || details.id).toString(), // always needs to be a string + appid: parseInt(details.appid, 10), // always needs to be an int + contextid: details.contextid.toString(), // always needs to be a string + amount: parseInt(details.amount || 1, 10) // always needs to be an int }; if (list.some(tradeItem => Helpers.itemEquals(tradeItem, item))) { @@ -337,170 +328,171 @@ function addItem(details, offer, list) { /** * Send this trade offer. * @param {function} [callback] + * @returns {Promise<{status: string}>} */ TradeOffer.prototype.send = function(callback) { - if (this.id) { - Helpers.makeAnError(new Error("This offer has already been sent"), callback); - return; - } - - if (this.itemsToGive.length + this.itemsToReceive.length == 0) { - Helpers.makeAnError(new Error("Cannot send an empty trade offer"), callback); - return; - } - - function itemMapper(item){ - return { - "appid": item.appid, - "contextid": item.contextid, - "amount": item.amount || 1, - "assetid": item.assetid - }; - } + return StdLib.Promises.callbackPromise(['status'], callback, true, (resolve, reject) => { + if (this.id) { + Helpers.makeAnError(new Error('This offer has already been sent'), reject); + return; + } - let offerdata = { - "newversion": true, - "version": this.itemsToGive.length + this.itemsToReceive.length + 1, - "me": { - "assets": this.itemsToGive.map(itemMapper), - "currency": [], // TODO - "ready": false - }, - "them": { - "assets": this.itemsToReceive.map(itemMapper), - "currency": [], - "ready": false + if (this.itemsToGive.length + this.itemsToReceive.length == 0) { + Helpers.makeAnError(new Error('Cannot send an empty trade offer'), reject); + return; } - }; - let params = {}; - if (this._token) { - params.trade_offer_access_token = this._token; - } + function itemMapper(item) { + return { + appid: item.appid, + contextid: item.contextid, + amount: item.amount || 1, + assetid: item.assetid + }; + } + + let offerdata = { + newversion: true, + version: this.itemsToGive.length + this.itemsToReceive.length + 1, + me: { + assets: this.itemsToGive.map(itemMapper), + currency: [], // TODO + ready: false + }, + them: { + assets: this.itemsToReceive.map(itemMapper), + currency: [], + ready: false + } + }; - this.manager._pendingOfferSendResponses++; + let params = {}; + if (this._token) { + params.trade_offer_access_token = this._token; + } + + this.manager._pendingOfferSendResponses++; + + this.manager._community.httpRequestPost('https://steamcommunity.com/tradeoffer/new/send', { + headers: { + referer: `https://steamcommunity.com/tradeoffer/${(this.id || 'new')}/?partner=${this.partner.accountid}` + (this._token ? "&token=" + this._token : '') + }, + json: true, + form: { + sessionid: this.manager._community.getSessionID(), + serverid: 1, + partner: this.partner.toString(), + tradeoffermessage: this.message || '', + json_tradeoffer: JSON.stringify(offerdata), + captcha: '', + trade_offer_create_params: JSON.stringify(params), + tradeofferid_countered: this._countering + }, + checkJsonError: false, + checkHttpError: false // we'll check it ourself. Some trade offer errors return HTTP 500 + }, (err, response, body) => { + this.manager._pendingOfferSendResponses--; - this.manager._community.httpRequestPost('https://steamcommunity.com/tradeoffer/new/send', { - "headers": { - "referer": `https://steamcommunity.com/tradeoffer/${(this.id || 'new')}/?partner=${this.partner.accountid}` + (this._token ? "&token=" + this._token : '') - }, - "json": true, - "form": { - "sessionid": this.manager._community.getSessionID(), - "serverid": 1, - "partner": this.partner.toString(), - "tradeoffermessage": this.message || "", - "json_tradeoffer": JSON.stringify(offerdata), - "captcha": '', - "trade_offer_create_params": JSON.stringify(params), - "tradeofferid_countered": this._countering - }, - "checkJsonError": false, - "checkHttpError": false // we'll check it ourself. Some trade offer errors return HTTP 500 - }, (err, response, body) => { - this.manager._pendingOfferSendResponses--; + if (err) { + Helpers.makeAnError(err, reject); + return; + } - if (err) { - Helpers.makeAnError(err, callback); - return; - } + if (response.statusCode != 200) { + if (response.statusCode == 401) { + this.manager._community._notifySessionExpired(new Error('HTTP error 401')); + Helpers.makeAnError(new Error('Not Logged In'), reject); + return; + } - if (response.statusCode != 200) { - if (response.statusCode == 401) { - this.manager._community._notifySessionExpired(new Error("HTTP error 401")); - Helpers.makeAnError(new Error("Not Logged In"), callback); + Helpers.makeAnError(new Error('HTTP error ' + response.statusCode), reject, body); return; } - Helpers.makeAnError(new Error("HTTP error " + response.statusCode), callback, body); - return; - } - - if (!body) { - Helpers.makeAnError(new Error("Malformed JSON response"), callback); - return; - } + if (!body) { + Helpers.makeAnError(new Error('Malformed JSON response'), reject); + return; + } - if (body && body.strError) { - Helpers.makeAnError(null, callback, body); - return; - } + if (body && body.strError) { + Helpers.makeAnError(null, reject, body); + return; + } - if (body && body.tradeofferid) { - this.id = body.tradeofferid; - this.state = ETradeOfferState.Active; - this.created = new Date(); - this.updated = new Date(); - this.expires = new Date(Date.now() + 1209600000); - - // Set any temporary local data into persistent poll data - for (let i in this._tempData) { - if (this._tempData.hasOwnProperty(i)) { - this.manager.pollData.offerData = this.manager.pollData.offerData || {}; - this.manager.pollData.offerData[this.id] = this.manager.pollData.offerData[this.id] || {}; - this.manager.pollData.offerData[this.id][i] = this._tempData[i]; + if (body && body.tradeofferid) { + this.id = body.tradeofferid; + this.state = ETradeOfferState.Active; + this.created = new Date(); + this.updated = new Date(); + this.expires = new Date(Date.now() + 1209600000); + + // Set any temporary local data into persistent poll data + for (let i in this._tempData) { + if (this._tempData.hasOwnProperty(i)) { + this.manager.pollData.offerData = this.manager.pollData.offerData || {}; + this.manager.pollData.offerData[this.id] = this.manager.pollData.offerData[this.id] || {}; + this.manager.pollData.offerData[this.id][i] = this._tempData[i]; + } } + + delete this._tempData; } - delete this._tempData; - } + this.confirmationMethod = EConfirmationMethod.None; - this.confirmationMethod = EConfirmationMethod.None; + if (body && body.needs_email_confirmation) { + this.state = ETradeOfferState.CreatedNeedsConfirmation; + this.confirmationMethod = EConfirmationMethod.Email; + } - if (body && body.needs_email_confirmation) { - this.state = ETradeOfferState.CreatedNeedsConfirmation; - this.confirmationMethod = EConfirmationMethod.Email; - } + if (body && body.needs_mobile_confirmation) { + this.state = ETradeOfferState.CreatedNeedsConfirmation; + this.confirmationMethod = EConfirmationMethod.MobileApp; + } - if (body && body.needs_mobile_confirmation) { - this.state = ETradeOfferState.CreatedNeedsConfirmation; - this.confirmationMethod = EConfirmationMethod.MobileApp; - } + this.manager.pollData.sent = this.manager.pollData.sent || {}; + this.manager.pollData.sent[this.id] = this.state; + this.manager.emit('pollData', this.manager.pollData); - this.manager.pollData.sent = this.manager.pollData.sent || {}; - this.manager.pollData.sent[this.id] = this.state; - this.manager.emit('pollData', this.manager.pollData); + if (body && this.state == ETradeOfferState.CreatedNeedsConfirmation) { + return resolve({status: 'pending'}); + } - if (!callback) { - return; - } + if (body && body.tradeofferid) { + return resolve({status: 'sent'}); + } - if (body && this.state == ETradeOfferState.CreatedNeedsConfirmation) { - callback(null, 'pending'); - } else if (body && body.tradeofferid) { - callback(null, 'sent'); - } else { - callback(new Error("Unknown response")); - } - }, "tradeoffermanager"); + return reject(new Error('Unknown response')); + }, 'tradeoffermanager'); + }); }; /** * Cancel or decline this trade offer. * @param {function} [callback] + * @returns {Promise} */ TradeOffer.prototype.cancel = TradeOffer.prototype.decline = function(callback) { - if (!this.id) { - Helpers.makeAnError(new Error("Cannot cancel or decline an unsent offer"), callback); - return; - } + return StdLib.Promises.callbackPromise([], callback, true, async (resolve, reject) => { + if (!this.id) { + Helpers.makeAnError(new Error('Cannot cancel or decline an unsent offer'), reject); + return; + } - if (this.state != ETradeOfferState.Active && this.state != ETradeOfferState.CreatedNeedsConfirmation) { - Helpers.makeAnError(new Error(`Offer #${this.id} is not active, so it may not be cancelled or declined`), callback); - return; - } + if (this.state != ETradeOfferState.Active && this.state != ETradeOfferState.CreatedNeedsConfirmation) { + Helpers.makeAnError(new Error(`Offer #${this.id} is not active, so it may not be cancelled or declined`), reject); + return; + } - this.manager._apiCall('POST', this.isOurOffer ? 'CancelTradeOffer' : 'DeclineTradeOffer', 1, {tradeofferid: this.id}).then(() => { - this.state = this.isOurOffer ? ETradeOfferState.Canceled : ETradeOfferState.Declined; - this.updated = new Date(); + try { + await this.manager._apiCall('POST', this.isOurOffer ? 'CancelTradeOffer' : 'DeclineTradeOffer', 1, {tradeofferid: this.id}); + this.state = this.isOurOffer ? ETradeOfferState.Canceled : ETradeOfferState.Declined; + this.updated = new Date(); - if (callback) { - callback(null); + this.manager.doPoll(); + } catch (ex) { + Helpers.makeAnError(ex, reject); } - - this.manager.doPoll(); - }).catch((err) => { - Helpers.makeAnError(err, callback); }); }; @@ -508,6 +500,7 @@ TradeOffer.prototype.cancel = TradeOffer.prototype.decline = function(callback) * Accept this trade offer. * @param {boolean} [skipStateUpdate=false] - If true, don't bother updating the offer's state from the API. This means that you won't get data about whether it went into escrow. * @param {function} [callback] + * @returns {Promise<{status: string}>} */ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { if (typeof skipStateUpdate === 'undefined') { @@ -519,106 +512,103 @@ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { skipStateUpdate = false; } - if (!this.id) { - Helpers.makeAnError(new Error("Cannot accept an unsent offer"), callback); - return; - } - - if (this.state != ETradeOfferState.Active) { - Helpers.makeAnError(new Error(`Offer #${this.id} is not active, so it may not be accepted`), callback); - return; - } - - if (this.isOurOffer) { - Helpers.makeAnError(new Error(`Cannot accept our own offer #${this.id}`), callback); - return; - } - - this.manager._community.httpRequestPost(`https://steamcommunity.com/tradeoffer/${this.id}/accept`, { - "headers": { - "Referer": `https://steamcommunity.com/tradeoffer/${this.id}/` - }, - "json": true, - "form": { - "sessionid": this.manager._community.getSessionID(), - "serverid": 1, - "tradeofferid": this.id, - "partner": this.partner.toString(), - "captcha": "" - }, - "checkJsonError": false, - "checkHttpError": false // we'll check it ourself. Some trade offer errors return HTTP 500 - }, (err, response, body) => { - if (err || response.statusCode != 200) { - if (response && response.statusCode == 403) { - this.manager._community._notifySessionExpired(new Error("HTTP error 403")); - Helpers.makeAnError(new Error("Not Logged In"), callback, body); - } else { - Helpers.makeAnError(err || new Error("HTTP error " + response.statusCode), callback, body); - } - + return StdLib.Promises.callbackPromise(['status'], callback, true, (resolve, reject) => { + if (!this.id) { + Helpers.makeAnError(new Error('Cannot accept an unsent offer'), callback); return; } - if (!body) { - Helpers.makeAnError(new Error("Malformed JSON response"), callback); + if (this.state != ETradeOfferState.Active) { + Helpers.makeAnError(new Error(`Offer #${this.id} is not active, so it may not be accepted`), callback); return; } - if (body && body.strError) { - Helpers.makeAnError(null, callback, body); + if (this.isOurOffer) { + Helpers.makeAnError(new Error(`Cannot accept our own offer #${this.id}`), reject); return; } - this.manager.doPoll(); + this.manager._community.httpRequestPost(`https://steamcommunity.com/tradeoffer/${this.id}/accept`, { + headers: { + Referer: `https://steamcommunity.com/tradeoffer/${this.id}/` + }, + json: true, + form: { + sessionid: this.manager._community.getSessionID(), + serverid: 1, + tradeofferid: this.id, + partner: this.partner.toString(), + captcha: '' + }, + checkJsonError: false, + checkHttpError: false // we'll check it ourself. Some trade offer errors return HTTP 500 + }, async (err, response, body) => { + if (err || response.statusCode != 200) { + if (response && response.statusCode == 403) { + this.manager._community._notifySessionExpired(new Error('HTTP error 403')); + Helpers.makeAnError(new Error('Not Logged In'), reject, body); + } else { + Helpers.makeAnError(err || new Error('HTTP error ' + response.statusCode), reject, body); + } - if (!callback) { - return; - } + return; + } - if (skipStateUpdate) { - if (body.tradeid) { - this.tradeID = body.tradeid; + if (!body) { + Helpers.makeAnError(new Error('Malformed JSON response'), reject); + return; } - if (body.needs_mobile_confirmation || body.needs_email_confirmation) { - callback(null, 'pending'); - } else { - callback(null, 'accepted'); + if (body && body.strError) { + Helpers.makeAnError(null, reject, body); + return; } - return; - } + this.manager.doPoll(); - this.update((err) => { - if (err) { - callback(new Error("Cannot load new trade data: " + err.message)); - return; + if (skipStateUpdate) { + if (body.tradeid) { + this.tradeID = body.tradeid; + } + + if (body.needs_mobile_confirmation || body.needs_email_confirmation) { + return resolve({status: 'pending'}); + } + + return resolve({status: 'accepted'}); + } + + try { + await this.update(); + } catch (ex) { + return reject('Cannot load new trade data: ' + ex.message); } if (this.confirmationMethod !== null && this.confirmationMethod != EConfirmationMethod.None) { - callback(null, 'pending'); - } else if (this.state == ETradeOfferState.InEscrow) { - callback(null, 'escrow'); - } else if (this.state == ETradeOfferState.Accepted) { - callback(null, 'accepted'); - } else { - callback(new Error("Unknown state " + this.state)); + return resolve({status: 'pending'}); } - }); - }, "tradeoffermanager"); + + if (this.state == ETradeOfferState.InEscrow) { + return resolve({status: 'escrow'}); + } + + if (this.state == ETradeOfferState.Accepted) { + return resolve({status: 'accepted'}); + } + + return reject(new Error('Unknown state ' + this.state)); + }, 'tradeoffermanager'); + }); }; /** * Update this offer from the API - * @param {function} callback + * @param {function} [callback] + * @returns {Promise} */ TradeOffer.prototype.update = function(callback) { - this.manager.getOffer(this.id, (err, offer) => { - if (err) { - callback(err); - return; - } + return StdLib.Promises.callbackPromise([], callback, false, async (resolve, reject) => { + let offer = await this.manager.getOffer(this.id); // Clone only the properties that might be out of date from the new TradeOffer onto this one, unless this one is // glitched. Sometimes Steam is bad and some properties are missing/malformed. @@ -639,14 +629,15 @@ TradeOffer.prototype.update = function(callback) { } } - callback(null); + resolve(); }); }; /** * Get details about this item exchange. Only works if the trade was actually completed (i.e. it has a tradeID). * @param {boolean} [getDetailsIfFailed=false] - Unless this is true, a trade that is failed (e.g. rolled back) will return an error instead of the data - * @param {function} callback + * @param {function} [callback] + * @returns {Promise<{tradeStatus: int, tradeInitTime: Date, itemsReceived: Object[], itemsGiven: Object[]}>} */ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) { if (typeof getDetailsIfFailed === 'function') { @@ -654,47 +645,58 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) getDetailsIfFailed = false; } - if (!this.id) { - Helpers.makeAnError(new Error('Cannot get trade details for an unsent trade offer'), callback); - return; - } + let args = ['tradeStatus', 'tradeInitTime', 'itemsReceived', 'itemsGive']; + return StdLib.Promises.callbackPromise(args, callback, false, async (resolve, reject) => { + if (!this.id) { + Helpers.makeAnError(new Error('Cannot get trade details for an unsent trade offer'), reject); + return; + } - if (!this.tradeID) { - Helpers.makeAnError(new Error('No trade ID; unable to get trade details'), callback); - return; - } + if (!this.tradeID) { + Helpers.makeAnError(new Error('No trade ID; unable to get trade details'), reject); + return; + } + + let result; + try { + result = await this.manager._apiCall('GET', 'GetTradeStatus', 1, {tradeid: this.tradeID}); + } catch (ex) { + Helpers.makeAnError(ex, reject); + return; + } - this.manager._apiCall('GET', 'GetTradeStatus', 1, {tradeid: this.tradeID}).then((result) => { if (!result.response || !result.response.trades) { - Helpers.makeAnError(new Error("Malformed response"), callback); + Helpers.makeAnError(new Error('Malformed response'), reject); return; } let trade = result.response.trades[0]; if (!trade || trade.tradeid != this.tradeID) { - Helpers.makeAnError(new Error("Trade not found in GetTradeStatus response; try again later"), callback); + Helpers.makeAnError(new Error('Trade not found in GetTradeStatus response; try again later'), reject); return; } - if (!getDetailsIfFailed && [ETradeStatus.Complete, ETradeStatus.InEscrow, ETradeStatus.EscrowRollback].indexOf(trade.status) == -1) { - Helpers.makeAnError(new Error("Trade status is " + (ETradeStatus[trade.status] || trade.status)), callback); + if (!getDetailsIfFailed && ![ETradeStatus.Complete, ETradeStatus.InEscrow, ETradeStatus.EscrowRollback].includes(trade.status)) { + Helpers.makeAnError(new Error('Trade status is ' + (ETradeStatus[trade.status] || trade.status)), reject); return; } - if (!this.manager._language) { - // No need for descriptions - callback(null, trade.status, new Date(trade.time_init * 1000), trade.assets_received || [], trade.assets_given || []); - } else { - this.manager._requestDescriptions((trade.assets_received || []).concat(trade.assets_given || [])).then(() => { - let received = this.manager._mapItemsToDescriptions(null, null, trade.assets_received || []); - let given = this.manager._mapItemsToDescriptions(null, null, trade.assets_given || []); - callback(null, trade.status, new Date(trade.time_init * 1000), received, given); - }).catch((err) => { - callback(err); - }); - } - }).catch((err) => { - Helpers.makeAnError(err, callback); + let received = trade.assets_received || []; + let given = trade.assets_given || []; + + if (this.manager._language) { + // Map descriptions + await this.manager._requestDescriptions(received.concat(given)); + received = this.manager._mapItemsToDescriptions(null, null, received); + given = this.manager._mapItemsToDescriptions(null, null, given); + } + + resolve({ + tradeStatus: trade.status, + tradeInitTime: new Date(trade.time_init * 1000), + itemsReceived: received, + itemsGiven: given + }); }); }; @@ -702,32 +704,34 @@ TradeOffer.prototype.getExchangeDetails = function(getDetailsIfFailed, callback) * Get details about the users in this trade. Can only be used if: * - The trade is created by you and *unsent* * - The trade is created by them, sent, and *active* - * @param {function} callback + * @param {function} [callback] + * @returns {Promise<{me: {personaName: string, contexts: Object, escrowDays: int, avatarIcon: string, avatarMedium: string, avatarFull: string}, them: {personaName: string, contexts: Object, escrowDays: int, probation: boolean, avatarIcon: string, avatarMedium: string, avatarFull: string}}>} */ TradeOffer.prototype.getUserDetails = function(callback) { - if (this.id && this.isOurOffer) { - Helpers.makeAnError(new Error("Cannot get user details for an offer that we sent."), callback); - return; - } + return StdLib.Promises.callbackPromise(['me', 'them'], callback, false, async (resolve, reject) => { + if (this.id && this.isOurOffer) { + Helpers.makeAnError(new Error('Cannot get user details for an offer that we sent.'), reject); + return; + } - if (this.id && this.state != ETradeOfferState.Active) { - Helpers.makeAnError(new Error("Cannot get user details for an offer that is sent and not Active."), callback); - return; - } + if (this.id && this.state != ETradeOfferState.Active) { + Helpers.makeAnError(new Error('Cannot get user details for an offer that is sent and not Active.'), reject); + return; + } - let url; - if (this.id) { - url = `https://steamcommunity.com/tradeoffer/${this.id}/`; - } else { - url = `https://steamcommunity.com/tradeoffer/new/?partner=${this.partner.accountid}`; - if (this._token) { - url += "&token=" + this._token; + let url; + if (this.id) { + url = `https://steamcommunity.com/tradeoffer/${this.id}/`; + } else { + url = `https://steamcommunity.com/tradeoffer/new/?partner=${this.partner.accountid}`; + if (this._token) { + url += "&token=" + this._token; + } } - } - Helpers.getUserDetailsFromTradeWindow(this.manager, url).then(({me, them}) => { - callback(null, me, them); - }).catch(callback); + let {me, them} = await Helpers.getUserDetailsFromTradeWindow(this.manager, url); + resolve({me, them}); + }); }; /** @@ -763,7 +767,7 @@ TradeOffer.prototype.duplicate = function() { */ TradeOffer.prototype.setMessage = function(message) { if (this.id) { - throw new Error("Cannot set message in an already-sent offer"); + throw new Error('Cannot set message in an already-sent offer'); } this.message = message.toString().substring(0, 128); @@ -775,7 +779,7 @@ TradeOffer.prototype.setMessage = function(message) { */ TradeOffer.prototype.setToken = function(token) { if (this.id) { - throw new Error("Cannot set token in an already-sent offer"); + throw new Error('Cannot set token in an already-sent offer'); } this._token = token; From 2a39f8c66cf31cb6af927948a00b7a479e5473d1 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Mon, 29 Mar 2021 01:21:27 -0400 Subject: [PATCH 34/36] Fixed some callbacks --- lib/classes/TradeOffer.js | 4 ++-- lib/index.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/classes/TradeOffer.js b/lib/classes/TradeOffer.js index d2bd6ef..0c9eef4 100644 --- a/lib/classes/TradeOffer.js +++ b/lib/classes/TradeOffer.js @@ -514,12 +514,12 @@ TradeOffer.prototype.accept = function(skipStateUpdate, callback) { return StdLib.Promises.callbackPromise(['status'], callback, true, (resolve, reject) => { if (!this.id) { - Helpers.makeAnError(new Error('Cannot accept an unsent offer'), callback); + Helpers.makeAnError(new Error('Cannot accept an unsent offer'), reject); return; } if (this.state != ETradeOfferState.Active) { - Helpers.makeAnError(new Error(`Offer #${this.id} is not active, so it may not be accepted`), callback); + Helpers.makeAnError(new Error(`Offer #${this.id} is not active, so it may not be accepted`), reject); return; } diff --git a/lib/index.js b/lib/index.js index 398df96..95f31fd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -441,18 +441,18 @@ TradeOfferManager.prototype.getOffer = function(id, callback) { * @returns {Promise<{sent: Object[], received: Object[]}>} */ TradeOfferManager.prototype.getOffers = function(filter, historicalCutoff, callback) { + if (typeof historicalCutoff === 'function') { + callback = historicalCutoff; + historicalCutoff = new Date(Date.now() + 31536000000); + } else if (!historicalCutoff) { + historicalCutoff = new Date(Date.now() + 31536000000); + } + return StdLib.Promises.callbackPromise(['sent', 'received'], callback, false, async (resolve, reject) => { if ([EOfferFilter.ActiveOnly, EOfferFilter.HistoricalOnly, EOfferFilter.All].indexOf(filter) == -1) { return reject(new Error(`Unexpected value "${filter}" for "filter" parameter. Expected a value from the EOfferFilter enum.`)); } - if (typeof historicalCutoff === 'function') { - callback = historicalCutoff; - historicalCutoff = new Date(Date.now() + 31536000000); - } else if (!historicalCutoff) { - historicalCutoff = new Date(Date.now() + 31536000000); - } - // Currently the GetTradeOffers API doesn't include app_data, so we need to get descriptions from the WebAPI let body = await this._apiCall('GET', 'GetTradeOffers', 1, { get_sent_offers: 1, From 2aaa4e42f85027e9c37b89b714f027ae79d81067 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Mon, 29 Mar 2021 01:21:42 -0400 Subject: [PATCH 35/36] Delete v3.txt --- v3.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 v3.txt diff --git a/v3.txt b/v3.txt deleted file mode 100644 index f375be8..0000000 --- a/v3.txt +++ /dev/null @@ -1,7 +0,0 @@ -BREAKING: -- Removed loadInventory -- Removed loadUserInventory -- now requires node 8 - -NEW: -- Trade sessions From 54162a57d8916a7667d43f3332ab0cdf44e3ee6d Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Mon, 29 Mar 2021 01:27:55 -0400 Subject: [PATCH 36/36] Fixed trade window parsing regex --- lib/helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 49273e5..5652fd0 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -114,7 +114,7 @@ Helpers.getUserDetailsFromTradeWindow = function(manager, url) { return; } - let script = body.match(/\n\W*