Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] v3 #295

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9e1815f
Require node v6 or later
DoctorMcKay Nov 5, 2017
f223628
Use @doctormckay/stdlib for LeastUsedCache
DoctorMcKay Nov 5, 2017
aafce94
Added TradeSession
DoctorMcKay Nov 12, 2017
3f5a629
RIP automatic offer-getting once confirmed
DoctorMcKay Nov 12, 2017
c7037ee
Added jsdoc
DoctorMcKay Nov 12, 2017
f4d2b43
var -> let
DoctorMcKay Nov 12, 2017
e369f61
Cleaned up some unused constants
DoctorMcKay Nov 12, 2017
44ce958
Clean up self = this type stuff
DoctorMcKay Nov 12, 2017
5a6dad6
Consolidate callback of getExchangeDetails into a single object
DoctorMcKay Nov 12, 2017
16106d1
Updated functions in index.js to use promises
DoctorMcKay Nov 19, 2017
57998d4
Fixed promise not returned
DoctorMcKay Nov 19, 2017
8bc69ea
Removed loadInventory and loadUserInventory
DoctorMcKay Nov 19, 2017
9d80442
Revert "Consolidate callback of getExchangeDetails into a single object"
DoctorMcKay Nov 23, 2017
c87cc13
Added node 6 requirement to breaking list
DoctorMcKay Nov 23, 2017
db80d17
Throw an Error if someone tries to create a TradeOffer with a number …
DoctorMcKay Nov 16, 2018
8c12125
Merge branch 'master' into v3
DoctorMcKay Feb 5, 2019
1fa17fa
Add items to itemsToGive immediately but with a pendingAdd property
DoctorMcKay Feb 5, 2019
f0ca177
Update version number
DoctorMcKay Feb 5, 2019
259887d
Don't include pendingAdd items when auditing item count
DoctorMcKay Feb 5, 2019
76d6592
Immediately remove items from itemsToGive
DoctorMcKay Feb 5, 2019
bdd6d38
Check _itemsToGiveRemoving when we audit the trade
DoctorMcKay Feb 5, 2019
f76509f
Spoof events if necessary to keep trade in sync
DoctorMcKay Feb 5, 2019
5ee4910
Don't forget to resolve promise for our own chat messages
DoctorMcKay Feb 5, 2019
7ba9754
Also spoof AddItem events for our own items
DoctorMcKay Feb 5, 2019
f78db0d
Merge branch 'master' into v3
DoctorMcKay Feb 7, 2019
63a92c3
Removed async dependency
DoctorMcKay Feb 7, 2019
95e4162
Removed deep-equal dependency
DoctorMcKay Feb 7, 2019
a7c0262
Merge branch 'master' into v3
DoctorMcKay Mar 29, 2021
4c3373b
Disable trade session support for v3 release
DoctorMcKay Mar 29, 2021
0149013
Added jsdoc
DoctorMcKay Mar 29, 2021
98a83dd
Promisify assets.js
DoctorMcKay Mar 29, 2021
ebc9b8f
Removed getReceivedItems
DoctorMcKay Mar 29, 2021
64cacdd
Helpers cleanup
DoctorMcKay Mar 29, 2021
65b8d06
Promisified helpers
DoctorMcKay Mar 29, 2021
f05ffa3
Promisified index.js
DoctorMcKay Mar 29, 2021
d8a99a5
Promisified TradeOffer
DoctorMcKay Mar 29, 2021
2a39f8c
Fixed some callbacks
DoctorMcKay Mar 29, 2021
2aaa4e4
Delete v3.txt
DoctorMcKay Mar 29, 2021
54162a5
Fixed trade window parsing regex
DoctorMcKay Mar 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 1 addition & 40 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

267 changes: 141 additions & 126 deletions lib/assets.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
"use strict";

const TradeOfferManager = require('./index.js');
const Async = require('async');
const EconItem = require('./classes/EconItem.js');
const Helpers = require('./helpers.js');
const TradeOfferManager = require('./index.js');

const ITEMS_PER_CLASSINFO_REQUEST = 100;

/**
* Stores item descriptions in the internal description cache.
* @param {Object[]|Object} descriptions
* @private
*/
TradeOfferManager.prototype._digestDescriptions = function(descriptions) {
var cache = this._assetCache;
let cache = this._assetCache;

if (!this._language) {
return;
}

if (descriptions && !(descriptions instanceof Array)) {
descriptions = Object.keys(descriptions).map(key => descriptions[key]);
if (descriptions && !Array.isArray(descriptions)) {
descriptions = Object.values(descriptions);
}

(descriptions || []).forEach((item) => {
Expand All @@ -27,19 +32,28 @@ TradeOfferManager.prototype._digestDescriptions = function(descriptions) {
});
};

/**
* Attaches item descriptions to some items from our internal description cache.
* Does not request missing descriptions.
* @param {int|null} appid
* @param {int|null} contextid
* @param {Object[]|Object} items
* @returns {EconItem[]}
* @private
*/
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]);
if (!Array.isArray(items)) {
items = Object.values(items);
}

return items.map((item) => {
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);
Expand All @@ -55,150 +69,151 @@ TradeOfferManager.prototype._mapItemsToDescriptions = function(appid, contextid,
});
};

/**
* Checks whether we have a description for a given item in our cache.
* @param {{appid?: int, classid: int|string, instanceid?: int|string}} item
* @param {int} [appid]
* @returns {boolean}
* @private
*/
TradeOfferManager.prototype._hasDescription = function(item, appid) {
appid = appid || item.appid;
return !!this._assetCache.get(appid + '_' + item.classid + '_' + (item.instanceid || '0'));
};

TradeOfferManager.prototype._addDescriptions = function(items, callback) {
var descriptionRequired = items.filter(item => !this._hasDescription(item));

if (descriptionRequired.length == 0) {
callback(null, this._mapItemsToDescriptions(null, null, items));
/**
* Requests descriptions for a set of classes from the API.
* @param {{appid: int, classid: int|string, instanceid?: int|string}[]} classes
* @returns {Promise}
* @private
*/
TradeOfferManager.prototype._requestDescriptions = async function(classes) {
if (!this._language) {
return;
}

this._requestDescriptions(descriptionRequired, (err) => {
if (err) {
callback(err);
} else {
callback(null, this._mapItemsToDescriptions(null, null, items));
// Filter out any classes we already have descriptions for
classes = classes.filter(cls => !this._hasDescription(cls));
// Filter out any duplicate classes
classes = classes.filter((cls, idx) => classes.findIndex(cls2 => Helpers.classEquals(cls, cls2)) === idx);

if (classes.length == 0) {
return; // nothing to request
}

// Get whatever we can from disk
let filenames = classes.map(item => `asset_${item.appid}_${item.classid}_${item.instanceid || '0'}.json`);
let files = await this._getFromDisk(filenames); // _getFromDisk never rejects
for (let filename in files) {
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
}
});
};

TradeOfferManager.prototype._requestDescriptions = function(classes, callback) {
var getFromSteam = () => {
var apps = [];
var appids = [];
try {
this._assetCache.add(match[1], JSON.parse(files[filename].toString('utf8')));
} catch (ex) {
this.emit('debug', `Error parsing description file ${filename}: ${ex.message}`);
}
}

// Split this out into appids
classes.forEach((item) => {
// Don't add this if we already have it in the cache
if (this._assetCache.get(`${item.appid}_${item.classid}_${item.instanceid || '0'}`)) {
return;
}
// get the rest from steam
let apps = [];
let appids = [];

var index = appids.indexOf(item.appid);
if (index == -1) {
index = appids.push(item.appid) - 1;
var arr = [];
arr.appid = item.appid;
apps.push(arr);
}
// Split this out into appids
classes.forEach((item) => {
// Don't add this if we already have it in the cache
if (this._assetCache.get(`${item.appid}_${item.classid}_${item.instanceid || '0'}`)) {
return;
}

// Don't add a class/instanceid pair that we already have in the list
if (apps[index].indexOf(item.classid + '_' + (item.instanceid || '0')) == -1) {
apps[index].push(item.classid + '_' + (item.instanceid || '0'));
}
});
let index = appids.indexOf(item.appid);
if (index == -1) {
index = appids.push(item.appid) - 1;
let arr = [];
arr.appid = item.appid;
apps.push(arr);
}

// Don't add a class/instanceid pair that we already have in the list
if (apps[index].indexOf(item.classid + '_' + (item.instanceid || '0')) == -1) {
apps[index].push(item.classid + '_' + (item.instanceid || '0'));
}
});

Async.map(apps, (app, cb) => {
var chunks = [];
var items = [];
let appPromises = [];
apps.forEach((app) => {
appPromises.push(new Promise(async (resolve, reject) => {
let chunks = [];

// Split this into chunks of ITEMS_PER_CLASSINFO_REQUEST items
while (app.length > 0) {
chunks.push(app.splice(0, ITEMS_PER_CLASSINFO_REQUEST));
}

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];
});

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;
let chunkPromises = [];
chunks.forEach((chunk) => {
chunkPromises.push(new Promise(async (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];
});

try {
this.emit('debug', `Requesting classinfo for ${chunk.length} items from app ${app.appid}`);
let body = await this._apiCall('GET', {iface: 'ISteamEconomy', method: 'GetAssetClassInfo'}, 1, input);
if (!body.result || !body.result.success) {
return reject(new Error('Invalid API response'));
}

var item = body.result[id];
item.appid = app.appid;
return item;
}).filter(item => !!item);

items = items.concat(chunkItems);

chunkCb(null);
});
}, (err) => {
if (err) {
cb(err);
} else {
cb(null, items);
}
});
}, (err, result) => {
if (err) {
callback(err);
return;
}

result.forEach(this._digestDescriptions.bind(this));
callback();
});
};

// Get whatever we can from disk
var filenames = classes.map(item => `asset_${item.appid}_${item.classid}_${item.instanceid || '0'}.json`);
this._getFromDisk(filenames, (err, files) => {
if (err) {
getFromSteam();
return;
}
let chunkItems = Object.keys(body.result).map((id) => {
if (!id.match(/^\d+(_\d+)?$/)) {
return null;
}

for (var filename in files) {
if (!files.hasOwnProperty(filename)) {
continue;
}
let item = body.result[id];
item.appid = app.appid;
return item;
}).filter(item => !!item);

var 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
}
this._digestDescriptions(chunkItems);
resolve();
} catch (ex) {
return reject(ex);
}
}));
});

try {
this._assetCache.add(match[1], JSON.parse(files[filename].toString('utf8')));
await Promise.all(chunkPromises);
} catch (ex) {
this.emit('debug', "Error parsing description file " + filename + ": " + ex);
return reject(ex);
}
}

// get the rest from steam
getFromSteam();
resolve();
}));
});

await Promise.all(appPromises);
};

/**
* Requests descriptions (if missing) for items in some trade offers.
* @param {{items_to_give?: Object[], items_to_receive?: Object[]}[]} offers
* @returns {Promise}
* @private
*/
TradeOfferManager.prototype._requestDescriptionsForOffers = async function(offers) {
let items = [];
offers.forEach((offer) => items = items.concat(offer.items_to_give || []).concat(offer.items_to_receive || []));
return await this._requestDescriptions(items);
};
Loading