@tf2autobot/tradeoffer-manager
Version:
A simple trade offers API for Steam
208 lines (170 loc) • 5.75 kB
JavaScript
;
const TradeOfferManager = require('./index.js');
const Async = require('async');
const EconItem = require('./classes/EconItem.js');
const ITEMS_PER_CLASSINFO_REQUEST = 100;
TradeOfferManager.prototype._digestDescriptions = function(descriptions) {
var cache = this._assetCache;
if (!this._language) {
return;
}
if (descriptions && !(descriptions instanceof Array)) {
descriptions = Object.keys(descriptions).map(key => descriptions[key]);
}
(descriptions || []).forEach((item) => {
if (!item || !item.appid || !item.classid) {
return;
}
cache.add(`${item.appid}_${item.classid}_${item.instanceid || '0'}`, item);
this._persistToDisk(`asset_${item.appid}_${item.classid}_${item.instanceid || '0'}.json`, JSON.stringify(item));
});
};
TradeOfferManager.prototype._mapItemsToDescriptions = function(appid, contextid, items) {
var cache = this._assetCache;
if (!(items instanceof Array)) {
items = Object.keys(items).map(key => items[key]);
}
return items.map((item) => {
item.appid = appid || item.appid;
item.contextid = contextid || item.contextid;
item.assetid = item.id = (item.id || item.assetid).toString();
var key = `${item.appid}_${item.classid}_${item.instanceid || '0'}`;
var descriptionCacheEntry = cache.get(key);
if (!descriptionCacheEntry) {
// This item isn't in our description cache
return new EconItem(item);
}
for (let i in descriptionCacheEntry) {
// don't overwrite properties that already exist on the item since steam has been known to include bad
// data like assetid in the description
if (descriptionCacheEntry.hasOwnProperty(i) && !item.hasOwnProperty(i)) {
item[i] = descriptionCacheEntry[i];
}
}
return new EconItem(item);
});
};
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));
return;
}
this._requestDescriptions(descriptionRequired, (err) => {
if (err) {
callback(err);
} else {
callback(null, this._mapItemsToDescriptions(null, null, items));
}
});
};
TradeOfferManager.prototype._requestDescriptions = function(classes, callback) {
var getFromSteam = () => {
var apps = [];
var appids = [];
// 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;
}
var index = appids.indexOf(item.appid);
if (index == -1) {
index = appids.push(item.appid) - 1;
var 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 = [];
// 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;
}
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;
}
for (var filename in files) {
if (!files.hasOwnProperty(filename)) {
continue;
}
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
}
try {
this._assetCache.add(match[1], JSON.parse(files[filename].toString('utf8')));
} catch (ex) {
this.emit('debug', "Error parsing description file " + filename + ": " + ex);
}
}
// get the rest from steam
getFromSteam();
});
};