Compare commits

...

111 Commits

Author SHA1 Message Date
Alexander Corn
d716e8a494 3.32.2 2017-08-17 13:12:44 -04:00
Alexander Corn
458a179852 Fixed reference to undefined self 2017-08-17 13:12:36 -04:00
Alexander Corn
f5d76527ba 3.32.1 2017-08-15 21:43:14 -04:00
Alexander Corn
5875f47ebc Ignore chat friendstate responses if we no longer care 2017-08-15 21:43:10 -04:00
Alexander Corn
03e0a1bdac 3.32.0 2017-08-06 19:18:43 -04:00
Alexander Corn
a51973c9b9 Added CSteamGroup methods for group-join requests 2017-08-06 19:11:21 -04:00
Alexander Corn
96d87ee8e6 Added methods to deal with Steam group join requests 2017-08-06 19:08:45 -04:00
Alexander Corn
7beef548dc Merge pull request #174 from Unsigno/master
Updated getUserInventoryContexts
2017-07-11 12:32:27 -04:00
Unsigno
6d9ebabb17 Updated getUserInventoryContexts to return 0 inventories and handle private profile error 2017-07-11 14:02:01 +02:00
Alexander Corn
5d5d8548af 3.31.0 2017-06-15 10:06:54 -04:00
Alexander Corn
b5142ad0cc Added language argument to getUserInventoryContents 2017-06-15 10:04:37 -04:00
Alexander Corn
3045a5d0cc Added stats-reporter integration 2017-06-15 10:03:14 -04:00
Alexander Corn
2dcb123761 Update request dependency version
(make david-dm happy)
2017-06-11 13:26:52 -04:00
Alexander Corn
bd29d598b1 Throw Error if using confirmations without being logged in 2017-06-02 23:35:23 -04:00
Alexander Corn
7e39715427 Merge pull request #168 from morethanwords/patch-1
Update getUserInventoryContents
2017-04-21 14:23:25 -04:00
Eduard Kuzmenko
a97edfd492 Update users.js 2017-04-21 15:46:20 +03:00
Eduard Kuzmenko
68b1f21655 Update getUserInventoryContents
There will be a re-loop if description of the item not the last one in descriptions (and quickDescriptionLookup will rewrite everytime)
2017-04-21 15:26:45 +03:00
Alexander Corn
b4772baf39 3.30.7 2017-03-27 14:42:46 -04:00
Alexander Corn
47dc22ca0d Handle cases where inventories are private in getUserInventoryContexts 2017-03-27 14:39:41 -04:00
Alexander Corn
acbd71666b Merge pull request #165 from Aareksio/patch-1
Check if instance is logged in before comparing steamids (fixes #164)
2017-03-06 11:35:23 -05:00
Arkadiusz Sygulski
3c35026a32 Check if instance is logged in before comparing steamids (fixes #164) 2017-03-06 16:02:20 +01:00
Alexander Corn
aca375d9da Give different error message if family view unlock failed for rate-limit 2017-03-04 22:30:25 -05:00
Alexander Corn
758b749487 3.30.6 2017-02-25 19:12:42 -05:00
Alexander Corn
a9b16cef1e Fixed crash no non-string 403 responses
https://dev.doctormckay.com/topic/845-responsebodymatch-is-not-a-function/
2017-02-25 19:12:34 -05:00
Alexander Corn
4542793175 3.30.5 2017-02-12 02:22:43 -05:00
Alexander Corn
d18a0244ec Updated default user-agent to latest Chrome on Win10 x64 2017-02-12 02:22:17 -05:00
Alexander Corn
b1c772d9b9 Keep HTTP connections open between requests 2017-02-11 22:28:22 -05:00
Alexander Corn
b9ec1ccfb5 3.30.4 2017-01-18 15:51:44 -05:00
Alexander Corn
0c7c030f78 Fixed string in callback instead of Error (fixes #153) 2017-01-18 15:50:11 -05:00
Alexander Corn
e5f1f14aad Fixed empty inventory resulting in "Malformed response" (fixes #156)
Display Steam error message when available instead of "HTTP error 500"
2017-01-18 15:49:27 -05:00
Alexander Corn
0f72ea7d93 3.30.3 2017-01-04 00:10:40 -05:00
Alexander Corn
d1906a2c83 Fallback to the default "?" avatar if we don't have a hash (fixes #151) 2017-01-04 00:10:11 -05:00
Alexander Corn
3964af7ade Throw a descriptive error if accountName or password is missing in login
Closes #119
2017-01-04 00:07:33 -05:00
Alexander Corn
5a5c5373aa Bump dependency versions to make david happy 2017-01-04 00:03:18 -05:00
Alexander Corn
ec60e1b73e 3.30.2 2016-12-30 17:11:12 -05:00
Alexander Corn
a22a75d4e1 Merge pull request #150 from Vankxr/master
Check if requested inventory is ours.
2016-12-30 17:06:21 -05:00
Vankxr
313753bb05 Check if requested inventory is ours.
Should fix https://github.com/DoctorMcKay/node-steamcommunity/issues/149
2016-12-30 21:24:53 +00:00
Alexander Corn
459fe4d8b1 3.30.1 2016-12-15 00:53:46 -05:00
Alexander Corn
489c0441a5 Restore market_fee_app property to CEconItem for Steam Community items 2016-12-15 00:53:26 -05:00
Alexander Corn
d91e74d02a 3.30.0 2016-12-13 01:53:57 -05:00
Alexander Corn
d120d09970 Fixed tag data to be identical to old endpoint 2016-12-13 01:53:30 -05:00
Alexander Corn
84ce979a78 Fixed new inventory loading not working properly 2016-12-13 01:52:52 -05:00
Alexander Corn
88b0495386 Renamed getUserInventory, added back old one and marked it as deprecated 2016-12-13 01:32:17 -05:00
Alexander Corn
d367e8eae4 Added better paging controls for new inventory endpoint 2016-12-13 01:29:06 -05:00
Alexander Corn
9a3cf927c0 Fixed some more currency detection 2016-12-13 01:28:48 -05:00
Alexander Corn
56cdfff672 3.29.1 2016-12-13 01:07:12 -05:00
Alexander Corn
35e6dd16e7 Re-add "pos" property to inventory items for backwards compatibility 2016-12-13 01:07:00 -05:00
Alexander Corn
d0af8584c1 Send referer when fetching inventories 2016-12-13 00:50:29 -05:00
Alexander Corn
f1afcd1962 3.29.0 2016-12-13 00:46:23 -05:00
Alexander Corn
513b0bff1c Added some optimizations and backwards-compatibility for inv fetching 2016-12-13 00:46:02 -05:00
Alexander Corn
ab037b1931 Pass response body back to caller for inventory fetching 2016-12-13 00:45:38 -05:00
Alexander Corn
06df0ecd42 Handle currencies with new endpoint's data in CEconItem 2016-12-13 00:45:23 -05:00
João Silva
b789e13a70 Update inventory loading endpoint (#148)
* Update inventory loading endpoint

Should fix 429s :)

* Fix missing contextID parameter at CEconItem class

* Revert "Fix missing contextID parameter at CEconItem class"

This reverts commit 416321105b.

* Revert "Revert "Fix missing contextID parameter at CEconItem class""

This reverts commit 49ecd46954.

* Update #1
2016-12-13 00:23:02 -05:00
Alexander Corn
95a36b00c1 Remember time offset for purposes of acceptConfirmationForObject
Closes #147
2016-12-08 17:57:01 -05:00
Alexander Corn
41c87501c4 3.28.0 2016-10-28 19:24:05 -04:00
Alexander Corn
4f3ddcc787 Merge pull request #134 from Aareksio/confirmations
Emit `confirmationAccepted` after accepting the confirmation using `confirmationChecker`
2016-10-27 17:07:41 -04:00
Arkadiusz Sygulski
d10fa9983d Emit confirmationAccepted after accepting the confirmation 2016-10-27 22:30:58 +02:00
Arkadiusz Sygulski
666226731e Remove unused function: handleNewConfirmation 2016-10-27 22:18:31 +02:00
Alexander Corn
d096f47cac 3.27.2 2016-10-10 00:55:36 -04:00
Alexander Corn
a8eedb3067 Make it possible to skip json error check 2016-10-10 00:55:19 -04:00
Alexander Corn
ed12a54e81 3.27.1 2016-10-04 09:16:08 -04:00
Alexander Corn
eb341314ef Merge pull request #130 from Pavelas/master
Delete the profile URL cache after log in.
2016-10-04 09:15:03 -04:00
Pavelas
33cd9474ab Delete the profile URL cache after log in. 2016-10-04 10:26:20 +02:00
Alexander Corn
e8a43734be 3.27.0 2016-09-16 19:30:32 -04:00
Alexander Corn
8a27147f5c Added accountAlerts to getNotifications 2016-09-16 19:30:19 -04:00
Alexander Corn
d00d07f828 Added acceptConfirmationForObject convenience method 2016-09-16 19:03:49 -04:00
Alexander Corn
471c11d406 Merge pull request #128 from SzymonLisowiec/patch-1
Problem when search "Key:" in polish language.
2016-09-16 18:21:17 -04:00
Alexander Corn
dd3a9254aa 3.26.1 2016-09-12 11:22:29 -04:00
Alexander Corn
71a3893636 Fixed cookies not being set for non-community domains after logging in
Also fixed secure flag not working for cookies
2016-09-12 11:22:18 -04:00
Alexander Corn
5928c3b76a 3.26.0 2016-09-12 01:33:30 -04:00
Alexander Corn
1b60712b8b Updated getNotifications to use the new JSON endpoint 2016-09-12 01:33:11 -04:00
Alexander Corn
335a955abd Fixed cookies not being set 2016-09-12 01:24:39 -04:00
Alexander Corn
6d534df8ba Set cookies on all Steam domains, and properly flag as secure 2016-09-12 01:17:00 -04:00
Alexander Corn
9022a9a45e 3.25.0 2016-09-08 19:45:05 -04:00
Alexander Corn
ce4d7ae026 Pull trade offer ID directly from listing page 2016-09-08 19:23:15 -04:00
Kysune
b863377c87 Problem when search "Key:" in polish language. 2016-09-01 21:05:46 +02:00
Alexander Corn
20785ee92b 3.24.0 2016-08-31 22:20:49 -04:00
Alexander Corn
3afb393f58 Updated getInventoryHistory for new pagination scheme 2016-08-31 22:20:36 -04:00
Alexander Corn
79543187f0 Fixed timezones being incorrect in trade history entries 2016-08-31 22:20:24 -04:00
Alexander Corn
3139aec463 Reformat inventoryhistory.js 2016-08-31 21:48:17 -04:00
Alexander Corn
bb917af86e 3.23.3 2016-08-10 18:55:36 -04:00
Alexander Corn
17f51b9caa Fixed double callbacks on some json errors (fixes #123) 2016-08-10 18:55:23 -04:00
Alexander Corn
dd638df118 3.23.2 2016-08-09 15:45:32 -04:00
Alexander Corn
b937a05442 Make sure we actually get json back when we want it (fixes #122) 2016-08-09 15:45:21 -04:00
Alexander Corn
9c34d18cd9 3.23.1 2016-08-07 00:04:56 -04:00
Alexander Corn
02e3798124 Handle some cases where the session expires while we're using webchat 2016-08-07 00:04:30 -04:00
Alexander Corn
5d9922cc33 3.23.0 2016-07-13 13:12:39 -04:00
Alexander Corn
153011e1c9 Make _community non-enumerable on CConfirmation 2016-07-13 13:11:56 -04:00
Alexander Corn
575c196cb7 Added acceptAllConfirmations method 2016-07-13 13:11:44 -04:00
Alexander Corn
47a24d19cf Added ability to act on multiple confirmations 2016-07-13 13:11:36 -04:00
Alexander Corn
0eac19b71c 3.22.0 2016-07-10 01:02:48 -04:00
Alexander Corn
ca73ecc786 Parse times in getUserAliases into Date objects 2016-07-10 01:02:36 -04:00
Alexander Corn
e8733613d7 Merge pull request #117 from andrewda/master
Add ability to get user aliases
2016-07-10 00:52:13 -04:00
Andrew Dassonville
89a1df67ec Add ability to get user aliases 2016-07-06 16:20:42 -07:00
Alexander Corn
3ba0a8592e 3.21.2 2016-07-01 18:30:32 -04:00
Alexander Corn
57fa7a679f Fixed market search results including query string (fixes #113) 2016-07-01 18:29:56 -04:00
Alexander Corn
446e315183 Fixed market search result prices being horribly wrong 2016-07-01 18:27:39 -04:00
Alexander Corn
c6a9b74471 3.21.1 2016-06-29 23:08:54 -04:00
Alexander Corn
018e18cb5d Give "Family View Restricted" error if access denied due to Family View 2016-06-29 23:08:40 -04:00
Alexander Corn
303ce10e73 3.21.0 2016-06-27 16:57:12 -04:00
Alexander Corn
60e0e63a92 Fixed marketSearch (fixes #112) 2016-06-27 16:56:16 -04:00
Luke
17e5b94782 Added deleting events in CSteamGroup (#109)
* Added deleteEvent()

* Added deleteGroupEvent()
2016-06-21 22:39:23 -04:00
Alexander Corn
beaf012846 3.20.0 2016-06-15 15:07:45 -04:00
Alexander Corn
38c20a342c Merge pull request #105 from luop90/master
Added editing events to CSteamGroup
2016-06-15 14:17:41 -04:00
Luke
a406cfbfe2 Fixed a valid server object being overwritten 2016-06-15 11:25:44 -06:00
Luke Henke
c970eee9d3 Added editing events to CSteamGroup 2016-06-14 20:29:11 -06:00
Alexander Corn
38e3eefb33 3.19.10 2016-06-14 16:08:03 -04:00
Alexander Corn
a2e26459d9 Emit chatLogOnFailed on error, and handle fatal errors (fixes #104) 2016-06-14 16:07:46 -04:00
Alexander Corn
c1b04ee743 Fixed crash if chat poll returns no response
https://dev.doctormckay.com/topic/383-error-in-node-steamcommunity/
2016-06-14 16:02:04 -04:00
Alexander Corn
542eb8d47e 3.19.9 2016-06-03 20:11:56 -04:00
Alexander Corn
9081528f72 Fixed "Error: Error:" in trade errors 2016-06-03 20:10:33 -04:00
22 changed files with 1248 additions and 284 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
package-lock=false

View File

@@ -9,6 +9,8 @@ This module provides an easy interface for the Steam Community website. This mod
It supports Steam Guard and CAPTCHAs. It supports Steam Guard and CAPTCHAs.
This reports anonymous usage statistics to the author. [See here](https://github.com/DoctorMcKay/node-stats-reporter) for more information.
**Have a question about the module or coding in general? *Do not create a GitHub issue.* GitHub issues are for feature **Have a question about the module or coding in general? *Do not create a GitHub issue.* GitHub issues are for feature
requests and bug reports. Instead, post in the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/). requests and bug reports. Instead, post in the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/).
Such issues may be ignored!** Such issues may be ignored!**

View File

@@ -1,17 +1,32 @@
var SteamCommunity = require('../index.js');
module.exports = CConfirmation; module.exports = CConfirmation;
function CConfirmation(community, data) { function CConfirmation(community, data) {
this._community = community; Object.defineProperty(this, "_community", {"value": community});
this.id = data.id; this.id = data.id;
this.type = data.type;
this.creator = data.creator;
this.key = data.key; this.key = data.key;
this.title = data.title; this.title = data.title;
this.receiving = data.receiving; this.receiving = data.receiving;
this.time = data.time; this.time = data.time;
this.icon = data.icon; this.icon = data.icon;
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null;
} }
CConfirmation.prototype.getOfferID = function(time, key, callback) { CConfirmation.prototype.getOfferID = function(time, key, callback) {
if (this.type && this.creator) {
if (this.type != SteamCommunity.ConfirmationType.Trade) {
callback(new Error("Not a trade confirmation"));
return;
}
callback(null, this.creator);
return;
}
this._community.getConfirmationOfferID(this.id, time, key, callback); this._community.getConfirmationOfferID(this.id, time, key, callback);
}; };

View File

@@ -1,31 +1,40 @@
module.exports = CEconItem; module.exports = CEconItem;
function CEconItem(item, descriptions, contextID) { function CEconItem(item, description, contextID) {
var thing; var thing;
for(thing in item) { for (thing in item) {
if(item.hasOwnProperty(thing)) { if (item.hasOwnProperty(thing)) {
this[thing] = item[thing]; this[thing] = item[thing];
} }
} }
this.assetid = this.id = (this.id || this.assetid); var isCurrency = !!(this.is_currency || this.currency) || typeof this.currencyid !== 'undefined'; // I don't want to put this on the object yet; it's nice to have the ids at the top of printed output
if (isCurrency) {
this.currencyid = this.id = (this.id || this.currencyid);
} else {
this.assetid = this.id = (this.id || this.assetid);
}
this.instanceid = this.instanceid || '0'; this.instanceid = this.instanceid || '0';
this.amount = parseInt(this.amount, 10); this.amount = parseInt(this.amount, 10);
this.contextid = this.contextid || contextID.toString(); this.contextid = this.contextid || contextID.toString();
// Merge the description // Merge the description
if(descriptions) { if (description) {
var description = descriptions[this.classid + '_' + this.instanceid]; // Is this a listing of descriptions?
if(description) { if (description[this.classid + '_' + this.instanceid]) {
for(thing in description) { description = description[this.classid + '_' + this.instanceid];
if(description.hasOwnProperty(thing)) { }
this[thing] = description[thing];
} for (thing in description) {
if (description.hasOwnProperty(thing)) {
this[thing] = description[thing];
} }
} }
} }
this.is_currency = !!this.is_currency; this.is_currency = isCurrency;
this.tradable = !!this.tradable; this.tradable = !!this.tradable;
this.marketable = !!this.marketable; this.marketable = !!this.marketable;
this.commodity = !!this.commodity; this.commodity = !!this.commodity;
@@ -34,9 +43,34 @@ function CEconItem(item, descriptions, contextID) {
this.fraudwarnings = this.fraudwarnings || []; this.fraudwarnings = this.fraudwarnings || [];
this.descriptions = this.descriptions || []; this.descriptions = this.descriptions || [];
if(this.owner && JSON.stringify(this.owner) == '{}') { if (this.owner && JSON.stringify(this.owner) == '{}') {
this.owner = null; this.owner = null;
} }
// Restore old property names of tags
if (this.tags) {
this.tags = this.tags.map(function(tag) {
return {
"internal_name": tag.internal_name,
"name": tag.localized_tag_name || tag.name,
"category": tag.category,
"color": tag.color || "",
"category_name": tag.localized_category_name || tag.category_name
};
});
}
// Restore market_fee_app, if applicable
var match;
if (this.appid == 753 && this.contextid == 6 && this.market_hash_name && (match = this.market_hash_name.match(/^(\d+)\-/))) {
this.market_fee_app = parseInt(match[1], 10);
}
if (this.actions === "") {
this.actions = [];
}
delete this.currency;
} }
CEconItem.prototype.getImageURL = function() { CEconItem.prototype.getImageURL = function() {
@@ -52,12 +86,12 @@ CEconItem.prototype.getLargeImageURL = function() {
}; };
CEconItem.prototype.getTag = function(category) { CEconItem.prototype.getTag = function(category) {
if(!this.tags) { if (!this.tags) {
return null; return null;
} }
for(var i = 0; i < this.tags.length; i++) { for (var i = 0; i < this.tags.length; i++) {
if(this.tags[i].category == category) { if (this.tags[i].category == category) {
return this.tags[i]; return this.tags[i];
} }
} }

View File

@@ -27,60 +27,63 @@ SteamCommunity.prototype.marketSearch = function(options, callback) {
qs.count = 100; qs.count = 100;
qs.sort_column = 'price'; qs.sort_column = 'price';
qs.sort_dir = 'asc'; qs.sort_dir = 'asc';
performSearch.call(this, this.httpRequest, qs, [], callback);
var self = this;
var results = [];
performSearch();
function performSearch() {
self.httpRequest({
"uri": "https://steamcommunity.com/market/search/render/",
"qs": qs,
"headers": {
"referer": "https://steamcommunity.com/market/search"
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.success) {
callback(new Error("Success is not true"));
return;
}
if(!body.results_html) {
callback(new Error("No results_html in response"));
return;
}
var $ = Cheerio.load(body.results_html);
var $errorMsg = $('.market_listing_table_message');
if($errorMsg.length > 0) {
callback(new Error($errorMsg.text()));
return;
}
var rows = $('.market_listing_row_link');
for(var i = 0; i < rows.length; i++) {
results.push(new CMarketSearchResult($(rows[i])));
}
if(body.start + body.pagesize >= body.total_count) {
callback(null, results);
} else {
qs.start += body.pagesize;
performSearch();
}
}, "steamcommunity");
}
}; };
function performSearch(request, qs, results, callback) {
var self = this;
request({
"uri": "https://steamcommunity.com/market/search/render/",
"qs": qs,
"headers": {
"referer": "https://steamcommunity.com/market/search"
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.success) {
callback(new Error("Success is not true"));
return;
}
if(!body.results_html) {
callback(new Error("No results_html in response"));
return;
}
var $ = Cheerio.load(body.results_html);
if($('.market_listing_table_message').length > 0) {
callback(new Error($('.market_listing_table_message').text()));
return;
}
var rows = $('.market_listing_row_link');
for(var i = 0; i < rows.length; i++) {
results.push(new CMarketSearchResult($(rows[i])));
}
if(body.start + body.pagesize >= body.total_count) {
callback(null, results);
} else {
qs.start += body.pagesize;
performSearch.call(self, request, qs, results, callback);
}
}, "steamcommunity");
}
function CMarketSearchResult(row) { function CMarketSearchResult(row) {
var match = row.attr('href').match(/\/market\/listings\/(\d+)\/(.+)/); var match = row.attr('href').match(/\/market\/listings\/(\d+)\/([^\?\/]+)/);
this.appid = parseInt(match[1], 10); this.appid = parseInt(match[1], 10);
this.market_hash_name = decodeURIComponent(match[2]); this.market_hash_name = decodeURIComponent(match[2]);
this.image = row.find('.market_listing_item_img').attr('src').match(/^https?:\/\/[^\/]+\/economy\/image\/[^\/]+\//)[0]; this.image = row.find('.market_listing_item_img').attr('src').match(/^https?:\/\/[^\/]+\/economy\/image\/[^\/]+\//)[0];
this.price = parseInt(row.find('.market_listing_their_price .market_table_value span').text().replace(/[^\d]+/g, ''), 10); this.price = parseInt(row.find('.market_listing_their_price .market_table_value span.normal_price').text().replace(/[^\d]+/g, ''), 10);
this.quantity = parseInt(row.find('.market_listing_num_listings_qty').text().replace(/[^\d]+/g, ''), 10); this.quantity = parseInt(row.find('.market_listing_num_listings_qty').text().replace(/[^\d]+/g, ''), 10);
} }

View File

@@ -7,24 +7,24 @@ SteamCommunity.prototype.getSteamGroup = function(id, callback) {
if(typeof id !== 'string' && !Helpers.isSteamID(id)) { if(typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error("id parameter should be a group URL string or a SteamID object"); throw new Error("id parameter should be a group URL string or a SteamID object");
} }
if(typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.CLAN)) { if(typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.CLAN)) {
throw new Error("SteamID must stand for a clan account in the public universe"); throw new Error("SteamID must stand for a clan account in the public universe");
} }
var self = this; var self = this;
this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "groups/" + id : "gid/" + id.toString()) + "/memberslistxml/?xml=1", function(err, response, body) { this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "groups/" + id : "gid/" + id.toString()) + "/memberslistxml/?xml=1", function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
xml2js.parseString(body, function(err, result) { xml2js.parseString(body, function(err, result) {
if(err) { if(err) {
callback(err); callback(err);
return; return;
} }
callback(null, new CSteamGroup(self, result.memberList)); callback(null, new CSteamGroup(self, result.memberList));
}); });
}, "steamcommunity"); }, "steamcommunity");
@@ -32,7 +32,7 @@ SteamCommunity.prototype.getSteamGroup = function(id, callback) {
function CSteamGroup(community, groupData) { function CSteamGroup(community, groupData) {
this._community = community; this._community = community;
this.steamID = new SteamID(groupData.groupID64[0]); this.steamID = new SteamID(groupData.groupID64[0]);
this.name = groupData.groupDetails[0].groupName[0]; this.name = groupData.groupDetails[0].groupName[0];
this.url = groupData.groupDetails[0].groupURL[0]; this.url = groupData.groupDetails[0].groupURL[0];
@@ -48,7 +48,7 @@ function CSteamGroup(community, groupData) {
CSteamGroup.prototype.getAvatarURL = function(size, protocol) { CSteamGroup.prototype.getAvatarURL = function(size, protocol) {
size = size || ''; size = size || '';
protocol = protocol || 'http://'; protocol = protocol || 'http://';
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + this.avatarHash.substring(0, 2) + "/" + this.avatarHash; var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + this.avatarHash.substring(0, 2) + "/" + this.avatarHash;
if(size == 'full' || size == 'medium') { if(size == 'full' || size == 'medium') {
return url + "_" + size + ".jpg"; return url + "_" + size + ".jpg";
@@ -62,7 +62,7 @@ CSteamGroup.prototype.getMembers = function(addresses, callback) {
callback = addresses; callback = addresses;
addresses = null; addresses = null;
} }
this._community.getGroupMembers(this.steamID, callback, null, null, addresses, 0); this._community.getGroupMembers(this.steamID, callback, null, null, addresses, 0);
}; };
@@ -94,6 +94,14 @@ CSteamGroup.prototype.scheduleEvent = function(name, type, description, time, se
this._community.scheduleGroupEvent(this.steamID, name, type, description, time, server, callback); this._community.scheduleGroupEvent(this.steamID, name, type, description, time, server, callback);
}; };
CSteamGroup.prototype.editEvent = function(id, name, type, description, time, server, callback) {
this._community.editGroupEvent(this.steamID, id, name, type, description, time, server, callback);
};
CSteamGroup.prototype.deleteEvent = function (id, callback) {
this._community.deleteGroupEvent(this.steamID, id, callback);
};
CSteamGroup.prototype.setPlayerOfTheWeek = function(steamID, callback) { CSteamGroup.prototype.setPlayerOfTheWeek = function(steamID, callback) {
this._community.setGroupPlayerOfTheWeek(this.steamID, steamID, callback); this._community.setGroupPlayerOfTheWeek(this.steamID, steamID, callback);
}; };
@@ -105,3 +113,30 @@ CSteamGroup.prototype.kick = function(steamID, callback) {
CSteamGroup.prototype.getHistory = function(page, callback) { CSteamGroup.prototype.getHistory = function(page, callback) {
this._community.getGroupHistory(this.steamID, page, callback); this._community.getGroupHistory(this.steamID, page, callback);
}; };
/**
* Get requests to join this restricted group.
* @param {function} callback - First argument is null/Error, second is array of SteamID objects
*/
CSteamGroup.prototype.getJoinRequests = function(callback) {
this._community.getGroupJoinRequests(this.steamID, callback);
};
/**
* Respond to one or more join requests to this restricted group.
* @param {SteamID|string|SteamID[]|string[]} steamIDs - The SteamIDs of the users you want to approve or deny membership for (or a single value)
* @param {boolean} approve - True to put them in the group, false to deny their membership
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamGroup.prototype.respondToJoinRequests = function(steamIDs, approve, callback) {
this._community.respondToGroupJoinRequests(this.steamID, steamIDs, approve, callback);
};
/**
* Respond to *ALL* pending group-join requests for this group.
* @param {boolean} approve - True to allow everyone who requested into the group, false to not
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamGroup.prototype.respondToAllJoinRequests = function(approve, callback) {
this._community.respondToAllGroupJoinRequests(this.steamID, approve, callback);
};

View File

@@ -108,6 +108,8 @@ function CSteamUser(community, userData, customurl) {
CSteamUser.getAvatarURL = function(hash, size, protocol) { CSteamUser.getAvatarURL = function(hash, size, protocol) {
size = size || ''; size = size || '';
protocol = protocol || 'http://'; protocol = protocol || 'http://';
hash = hash || "72f78b4c8cc1f62323f8a33f6d53e27db57c2252"; // The default "?" avatar
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash; var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash;
if(size == 'full' || size == 'medium') { if(size == 'full' || size == 'medium') {
@@ -150,10 +152,33 @@ CSteamUser.prototype.inviteToGroup = function(groupID, callback) {
this._community.inviteUserToGroup(this.steamID, groupID, callback); this._community.inviteUserToGroup(this.steamID, groupID, callback);
}; };
CSteamUser.prototype.getAliases = function(callback) {
this._community.getUserAliases(this.steamID, callback);
};
CSteamUser.prototype.getInventoryContexts = function(callback) { CSteamUser.prototype.getInventoryContexts = function(callback) {
this._community.getUserInventoryContexts(this.steamID, callback); this._community.getUserInventoryContexts(this.steamID, callback);
}; };
/**
* Get the contents of a user's inventory context.
* @deprecated Use CSteamUser#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
*/
CSteamUser.prototype.getInventory = function(appID, contextID, tradableOnly, callback) { CSteamUser.prototype.getInventory = function(appID, contextID, tradableOnly, callback) {
this._community.getUserInventory(this.steamID, appID, contextID, tradableOnly, callback); this._community.getUserInventory(this.steamID, appID, contextID, tradableOnly, callback);
}; };
/**
* Get the contents of a user's inventory context.
* @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
*/
CSteamUser.prototype.getInventoryContents = function(appID, contextID, tradableOnly, callback) {
this._community.getUserInventoryContents(this.steamID, appID, contextID, tradableOnly, callback);
};

View File

@@ -1,32 +1,9 @@
var SteamCommunity = require('../index.js'); var SteamCommunity = require('../index.js');
var SteamID = require('steamid'); var SteamID = require('steamid');
SteamCommunity.ChatState = { SteamCommunity.ChatState = require('../resources/EChatState.js');
"Offline": 0, SteamCommunity.PersonaState = require('../resources/EPersonaState.js');
"LoggingOn": 1, SteamCommunity.PersonaStateFlag = require('../resources/EPersonaStateFlag.js');
"LogOnFailed": 2,
"LoggedOn": 3
};
SteamCommunity.PersonaState = {
"Offline": 0,
"Online": 1,
"Busy": 2,
"Away": 3,
"Snooze": 4,
"LookingToTrade": 5,
"LookingToPlay": 6,
"Max": 7
};
SteamCommunity.PersonaStateFlag = {
"HasRichPresence": 1,
"InJoinableGame": 2,
"OnlineUsingWeb": 256,
"OnlineUsingMobile": 512,
"OnlineUsingBigPicture": 1024
};
SteamCommunity.prototype.chatLogon = function(interval, uiMode) { SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
if(this.chatState == SteamCommunity.ChatState.LoggingOn || this.chatState == SteamCommunity.ChatState.LoggedOn) { if(this.chatState == SteamCommunity.ChatState.LoggingOn || this.chatState == SteamCommunity.ChatState.LoggedOn) {
@@ -42,9 +19,17 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
var self = this; var self = this;
this.getWebApiOauthToken(function(err, token) { this.getWebApiOauthToken(function(err, token) {
if(err) { if(err) {
var fatal = err.message.indexOf('not authorized') != -1;
if (!fatal) {
self.chatState = SteamCommunity.ChatState.LogOnFailed;
setTimeout(self.chatLogon.bind(self), 5000);
} else {
self.chatState = SteamCommunity.ChatState.Offline;
}
self.emit('chatLogOnFailed', err, fatal);
self.emit('debug', "Cannot get oauth token: " + err.message); self.emit('debug', "Cannot get oauth token: " + err.message);
self.chatState = SteamCommunity.ChatState.LogOnFailed;
setTimeout(self.chatLogon.bind(self), 5000);
return; return;
} }
@@ -57,15 +42,17 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
"json": true "json": true
}, function(err, response, body) { }, function(err, response, body) {
if(err || response.statusCode != 200) { if(err || response.statusCode != 200) {
self.emit('debug', 'Error logging into webchat: ' + (err ? err.message : "HTTP error " + response.statusCode));
self.chatState = SteamCommunity.ChatState.LogOnFailed; self.chatState = SteamCommunity.ChatState.LogOnFailed;
self.emit('chatLogOnFailed', err ? err : new Error("HTTP error " + response.statusCode), false);
self.emit('debug', 'Error logging into webchat: ' + (err ? err.message : "HTTP error " + response.statusCode));
setTimeout(self.chatLogon.bind(self), 5000); setTimeout(self.chatLogon.bind(self), 5000);
return; return;
} }
if(body.error != 'OK') { if(body.error != 'OK') {
self.emit('debug', 'Error logging into webchat: ' + body.error);
self.chatState = SteamCommunity.ChatState.LogOnFailed; self.chatState = SteamCommunity.ChatState.LogOnFailed;
self.emit('chatLogOnFailed', new Error(body.error), false);
self.emit('debug', 'Error logging into webchat: ' + body.error);
setTimeout(self.chatLogon.bind(self), 5000); setTimeout(self.chatLogon.bind(self), 5000);
return; return;
} }
@@ -74,7 +61,8 @@ SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
"umqid": body.umqid, "umqid": body.umqid,
"message": body.message, "message": body.message,
"accessToken": token, "accessToken": token,
"interval": interval "interval": interval,
"uiMode": uiMode
}; };
self.chatFriends = {}; self.chatFriends = {};
@@ -170,7 +158,7 @@ SteamCommunity.prototype._chatPoll = function() {
}, },
"json": true "json": true
}, function(err, response, body) { }, function(err, response, body) {
if(self.chatState == SteamCommunity.ChatState.Offline) { if (self.chatState == SteamCommunity.ChatState.Offline) {
return; return;
} }
@@ -178,11 +166,19 @@ SteamCommunity.prototype._chatPoll = function() {
if(err || response.statusCode != 200) { if(err || response.statusCode != 200) {
self.emit('debug', 'Error in chat poll: ' + (err ? err.message : "HTTP error " + response.statusCode)); self.emit('debug', 'Error in chat poll: ' + (err ? err.message : "HTTP error " + response.statusCode));
if (err.message == "Not Logged On") {
self._relogWebChat();
}
return; return;
} }
if(body.error != 'OK') { if(!body || body.error != 'OK') {
self.emit('debug', 'Error in chat poll: ' + body.error); self.emit('debug', 'Error in chat poll: ' + (body && body.error ? body.error : "Malformed response"));
if (body && body.error && body.error == "Not Logged On") {
self._relogWebChat();
}
return; return;
} }
@@ -215,13 +211,28 @@ SteamCommunity.prototype._chatPoll = function() {
}, "steamcommunity"); }, "steamcommunity");
}; };
SteamCommunity.prototype._relogWebChat = function() {
this.emit('debug', "Relogging web chat");
clearTimeout(this._chat.timer);
this.chatState = SteamCommunity.ChatState.Offline;
this.chatLogon(this._chat.interval, this._chat.uiMode);
};
SteamCommunity.prototype._chatUpdatePersona = function(steamID) { SteamCommunity.prototype._chatUpdatePersona = function(steamID) {
if (!this.chatFriends || this.chatState == SteamCommunity.ChatState.Offline) {
return; // we no longer care
}
this.emit('debug', 'Updating persona data for ' + steamID); this.emit('debug', 'Updating persona data for ' + steamID);
var self = this; var self = this;
this.httpRequest({ this.httpRequest({
"uri": "https://steamcommunity.com/chat/friendstate/" + steamID.accountid, "uri": "https://steamcommunity.com/chat/friendstate/" + steamID.accountid,
"json": true "json": true
}, function(err, response, body) { }, function(err, response, body) {
if (!self.chatFriends || self.chatState == SteamCommunity.ChatState.Offline) {
return; // welp
}
if(err || response.statusCode != 200) { if(err || response.statusCode != 200) {
self.emit('debug', 'Chat update persona error: ' + (err ? err.message : "HTTP error " + response.statusCode)); self.emit('debug', 'Chat update persona error: ' + (err ? err.message : "HTTP error " + response.statusCode));
setTimeout(function() { setTimeout(function() {

View File

@@ -52,6 +52,8 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
var img = conf.find('.mobileconf_list_entry_icon img'); var img = conf.find('.mobileconf_list_entry_icon img');
confs.push(new CConfirmation(self, { confs.push(new CConfirmation(self, {
"id": conf.data('confid'), "id": conf.data('confid'),
"type": conf.data('type'),
"creator": conf.data('creator'),
"key": conf.data('key'), "key": conf.data('key'),
"title": conf.find('.mobileconf_list_entry_description>div:nth-of-type(1)').text().trim(), "title": conf.find('.mobileconf_list_entry_description>div:nth-of-type(1)').text().trim(),
"receiving": conf.find('.mobileconf_list_entry_description>div:nth-of-type(2)').text().trim(), "receiving": conf.find('.mobileconf_list_entry_description>div:nth-of-type(2)').text().trim(),
@@ -108,15 +110,15 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca
/** /**
* Confirm or cancel a given confirmation. * Confirm or cancel a given confirmation.
* @param {int} confID - The ID of the confirmation in question * @param {int|int[]} confID - The ID of the confirmation in question, or an array of confirmation IDs
* @param {string} confKey - The confirmation key associated with the confirmation in question (not a TOTP key, the `key` property of CConfirmation) * @param {string|string[]} confKey - The confirmation key associated with the confirmation in question (or an array of them) (not a TOTP key, the `key` property of CConfirmation)
* @param {int} time - The unix timestamp with which the following key was generated * @param {int} time - The unix timestamp with which the following key was generated
* @param {string} key - The confirmation key that was generated using the preceeding time and the tag "allow" (if accepting) or "cancel" (if not accepting) * @param {string} key - The confirmation key that was generated using the preceding time and the tag "allow" (if accepting) or "cancel" (if not accepting)
* @param {boolean} accept - true if you want to accept the confirmation, false if you want to cancel it * @param {boolean} accept - true if you want to accept the confirmation, false if you want to cancel it
* @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete * @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete
*/ */
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) { SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
request(this, "ajaxop", key, time, accept ? "allow" : "cancel", { request(this, (confID instanceof Array) ? "multiajaxop" : "ajaxop", key, time, accept ? "allow" : "cancel", {
"op": accept ? "allow" : "cancel", "op": accept ? "allow" : "cancel",
"cid": confID, "cid": confID,
"ck": confKey "ck": confKey
@@ -144,7 +146,107 @@ SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time,
}); });
}; };
/**
* Accept a confirmation for a given object (trade offer or market listing) automatically.
* @param {string} identitySecret
* @param {number|string} objectID
* @param {SteamCommunity~genericErrorCallback} callback
*/
SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) {
var self = this;
this._usedConfTimes = this._usedConfTimes || [];
if (typeof this._timeOffset !== 'undefined') {
// time offset is already known and saved
doConfirmation();
} else {
SteamTotp.getTimeOffset(function(err, offset) {
if (err) {
callback(err);
return;
}
self._timeOffset = offset;
doConfirmation();
setTimeout(function() {
// Delete the saved time offset after 12 hours because why not
delete self._timeOffset;
}, 1000 * 60 * 60 * 12);
});
}
function doConfirmation() {
var offset = self._timeOffset;
var time = SteamTotp.time(offset);
self.getConfirmations(time, SteamTotp.getConfirmationKey(identitySecret, time, "conf"), function(err, confs) {
if (err) {
callback(err);
return;
}
var conf = confs.filter(function(conf) { return conf.creator == objectID; });
if (conf.length == 0) {
callback(new Error("Could not find confirmation for object " + objectID));
return;
}
conf = conf[0];
// make sure we don't reuse the same time
var localOffset = 0;
do {
time = SteamTotp.time(offset) + localOffset++;
} while (self._usedConfTimes.indexOf(time) != -1);
self._usedConfTimes.push(time);
if (self._usedConfTimes.length > 60) {
self._usedConfTimes.splice(0, self._usedConfTimes.length - 60); // we don't need to save more than 60 entries
}
conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, "allow"), true, callback);
});
}
};
/**
* Send a single request to Steam to accept all outstanding confirmations (after loading the list). If one fails, the
* entire request will fail and there will be no way to know which failed without loading the list again.
* @param {number} time
* @param {string} confKey
* @param {string} allowKey
* @param {function} callback
*/
SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowKey, callback) {
var self = this;
this.getConfirmations(time, confKey, function(err, confs) {
if (err) {
callback(err);
return;
}
if (confs.length == 0) {
callback(null, []);
return;
}
self.respondToConfirmation(confs.map(function(conf) { return conf.id; }), confs.map(function(conf) { return conf.key; }), time, allowKey, true, function(err) {
if (err) {
callback(err);
return;
}
callback(err, confs);
});
});
};
function request(community, url, key, time, tag, params, json, callback) { function request(community, url, key, time, tag, params, json, callback) {
if (!community.steamID) {
throw new Error("Must be logged in before trying to do anything with confirmations");
}
params = params || {}; params = params || {};
params.p = SteamTotp.getDeviceID(community.steamID); params.p = SteamTotp.getDeviceID(community.steamID);
params.a = community.steamID.getSteamID64(); params.a = community.steamID.getSteamID64();
@@ -153,11 +255,19 @@ function request(community, url, key, time, tag, params, json, callback) {
params.m = "android"; params.m = "android";
params.tag = tag; params.tag = tag;
community.httpRequestGet({ var req = {
"method": url == 'multiajaxop' ? 'POST' : 'GET',
"uri": "https://steamcommunity.com/mobileconf/" + url, "uri": "https://steamcommunity.com/mobileconf/" + url,
"qs": params,
"json": !!json "json": !!json
}, function(err, response, body) { };
if (req.method == "GET") {
req.qs = params;
} else {
req.form = params;
}
community.httpRequest(req, function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@@ -223,8 +333,9 @@ SteamCommunity.prototype.checkConfirmations = function() {
// We should accept this // We should accept this
self.emit('debug', "Accepting confirmation #" + conf.id); self.emit('debug', "Accepting confirmation #" + conf.id);
var time = Math.floor(Date.now() / 1000); var time = Math.floor(Date.now() / 1000);
conf.respond(time, SteamTotp.getConfirmationKey(self._identitySecret, time, "allow"), true, function() { conf.respond(time, SteamTotp.getConfirmationKey(self._identitySecret, time, "allow"), true, function(err) {
// If there was an error and it wasn't actually accepted, we'll pick it up again // If there was an error and it wasn't actually accepted, we'll pick it up again
if (!err) self.emit('confirmationAccepted', conf);
delete self._knownConfirmations[conf.id]; delete self._knownConfirmations[conf.id];
setTimeout(callback, 1000); // Call the callback in 1 second, to make sure the time changes setTimeout(callback, 1000); // Call the callback in 1 second, to make sure the time changes
}); });
@@ -261,24 +372,13 @@ SteamCommunity.prototype.checkConfirmations = function() {
return; // No new ones return; // No new ones
} }
// We have new confirmations! Grab a key to get details. // We have new confirmations!
self._confirmationCheckerGetKey('details', function(err, key) { newOnes.forEach(function(conf) {
newOnes.forEach(function(conf) { self._knownConfirmations[conf.id] = conf; // Add it to our list of known confirmations
self._knownConfirmations[conf.id] = conf; // Add it to our list of known confirmations self._confirmationQueue.push(conf);
if(err) {
self._confirmationQueue.push(conf);
} else {
// Get its offer ID, if we can
conf.getOfferID(key.time, key.key, function(err, offerID) {
conf.offerID = offerID ? offerID : null;
self._confirmationQueue.push(conf);
});
}
});
resetTimer();
}); });
resetTimer();
}); });
}); });
@@ -287,28 +387,6 @@ SteamCommunity.prototype.checkConfirmations = function() {
self._confirmationTimer = setTimeout(self.checkConfirmations.bind(self), self._confirmationPollInterval); self._confirmationTimer = setTimeout(self.checkConfirmations.bind(self), self._confirmationPollInterval);
} }
} }
function handleNewConfirmation(conf, handleNumber) {
self._knownConfirmations[conf.id] = conf;
// Delay them by 1 second per new confirmation that we see, so that keys won't be the same.
setTimeout(function() {
if(self._identitySecret) {
self.emit('debug', 'Accepting confirmation #' + conf.id);
var time = Math.floor(Date.now() / 1000);
conf.respond(time, SteamTotp.getConfirmationKey(self._identitySecret, time, "allow"), true, function(err) {
if (err) {
self.emit('debug', "Can't accept confirmation #" + conf.id + ": " + err.message);
}
// We'll just retry next time we poll
delete self._knownConfirmations[conf.id];
});
} else {
self.emit('newConfirmation', conf);
}
}, handleNumber * 1000);
}
}; };
SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) { SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) {

View File

@@ -2,6 +2,7 @@ var SteamCommunity = require('../index.js');
var SteamID = require('steamid'); var SteamID = require('steamid');
var xml2js = require('xml2js'); var xml2js = require('xml2js');
var Cheerio = require('cheerio'); var Cheerio = require('cheerio');
var EResult = SteamCommunity.EResult;
SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link, addresses, addressIdx) { SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link, addresses, addressIdx) {
members = members || []; members = members || [];
@@ -120,7 +121,7 @@ SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback
var self = this; var self = this;
this.httpRequest({ this.httpRequest({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/rss/" "uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/rss/"
}, function(err, response, body) { }, function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
@@ -152,7 +153,7 @@ SteamCommunity.prototype.getAllGroupAnnouncements = function(gid, time, callback
return callback(null, announcements); return callback(null, announcements);
}); });
}, "steamcommunity"); }, "steamcommunity");
} };
SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, callback) { SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content, callback) {
if(typeof gid === 'string') { if(typeof gid === 'string') {
@@ -242,7 +243,7 @@ SteamCommunity.prototype.scheduleGroupEvent = function(gid, name, type, descript
server = {"ip": "", "password": ""}; server = {"ip": "", "password": ""};
} else if(typeof server === 'string') { } else if(typeof server === 'string') {
server = {"ip": server, "password": ""}; server = {"ip": server, "password": ""};
} else { } else if(typeof server !== 'object') {
server = {"ip": "", "password": ""}; server = {"ip": "", "password": ""};
} }
@@ -286,6 +287,88 @@ SteamCommunity.prototype.scheduleGroupEvent = function(gid, name, type, descript
}, "steamcommunity"); }, "steamcommunity");
}; };
SteamCommunity.prototype.editGroupEvent = function (gid, id, name, type, description, time, server, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
// Event types: ChatEvent - Chat, OtherEvent - A lil somethin somethin, PartyEvent - Party!, MeetingEvent - Important meeting, SpecialCauseEvent - Special cause (charity ball?), MusicAndArtsEvent - Music or Art type thing, SportsEvent - Sporting endeavor, TripEvent - Out of town excursion
// Passing a number for type will make it a game event for that appid
if (typeof server === 'function') {
callback = server;
server = {"ip": "", "password": ""};
} else if (typeof server === 'string') {
server = {"ip": server, "password": ""};
} else if (typeof server !== 'object') {
server = {"ip": "", "password": ""};
}
var form = {
"sessionid": this.getSessionID(),
"action": "updateEvent",
"eventID": id,
"tzOffset": new Date().getTimezoneOffset() * -60,
"name": name,
"type": (typeof type === 'number' || !isNaN(parseInt(type, 10)) ? "GameEvent" : type),
"appID": (typeof type === 'number' || !isNaN(parseInt(type, 10)) ? type : ''),
"serverIP": server.ip,
"serverPassword": server.password,
"notes": description,
"eventQuickTime": "now"
};
if (time === null) {
form.startDate = 'MM/DD/YY';
form.startHour = '12';
form.startMinute = '00';
form.startAMPM = 'PM';
form.timeChoice = 'quick';
} else {
form.startDate = (time.getMonth() + 1 < 10 ? '0' : '') + (time.getMonth() + 1) + '/' + (time.getDate() < 10 ? '0' : '') + time.getDate() + '/' + time.getFullYear().toString().substring(2);
form.startHour = (time.getHours() === 0 ? '12' : (time.getHours() > 12 ? time.getHours() - 12 : time.getHours()));
form.startMinute = (time.getMinutes() < 10 ? '0' : '') + time.getMinutes();
form.startAMPM = (time.getHours() <= 12 ? 'AM' : 'PM');
form.timeChoice = 'specific';
}
var self = this;
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.toString() + "/eventEdit",
"form": form
}, function(err, response, body) {
if(!callback) {
return;
}
callback(err || null);
}, "steamcommunity");
};
SteamCommunity.prototype.deleteGroupEvent = function(gid, id, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
var form = {
"sessionid": this.getSessionID(),
"action": "deleteEvent",
"eventID": id
};
var self = this;
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.toString() + "/eventEdit",
"form": form
}, function(err, response, body) {
if(!callback) {
return;
}
callback(err || null);
}, "steamcommunity");
};
SteamCommunity.prototype.setGroupPlayerOfTheWeek = function(gid, steamID, callback) { SteamCommunity.prototype.setGroupPlayerOfTheWeek = function(gid, steamID, callback) {
if(typeof gid === 'string') { if(typeof gid === 'string') {
gid = new SteamID(gid); gid = new SteamID(gid);
@@ -435,3 +518,108 @@ SteamCommunity.prototype.getGroupHistory = function(gid, page, callback) {
callback(null, output); callback(null, output);
}, "steamcommunity"); }, "steamcommunity");
}; };
/**
* Get requests to join a restricted group.
* @param {SteamID|string} gid - The SteamID of the group you want to manage
* @param {function} callback - First argument is null/Error, second is array of SteamID objects
*/
SteamCommunity.prototype.getGroupJoinRequests = function(gid, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
this.httpRequestGet("https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage", (err, res, body) => {
if (!body) {
callback(new Error("Malformed response"));
return;
}
var matches = body.match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/g);
if (!matches) {
// no pending requests
callback(null, []);
return;
}
var requests = [];
for (var i = 0; i < matches.length; i++) {
requests.push(new SteamID("[U:1:" + matches[i].match(/JoinRequests_ApproveDenyUser\(\W*['"](\d+)['"],\W0\W\)/)[1] + "]"));
}
callback(null, requests);
}, "steamcommunity");
};
/**
* Respond to one or more join requests to a restricted group.
* @param {SteamID|string} gid - The SteamID of the group you want to manage
* @param {SteamID|string|SteamID[]|string[]} steamIDs - The SteamIDs of the users you want to approve or deny membership for (or a single value)
* @param {boolean} approve - True to put them in the group, false to deny their membership
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, approve, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
var rgAccounts = (!Array.isArray(steamIDs) ? [steamIDs] : steamIDs).map(sid => sid.toString());
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage",
"form": {
"rgAccounts": rgAccounts,
"bapprove": approve ? "1" : "0",
"json": "1",
"sessionID": this.getSessionID()
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (body != EResult.OK) {
var err = new Error(EResult[body] || ("Error " + body));
err.eresult = body;
callback(err);
} else {
callback(null);
}
}, "steamcommunity");
};
/**
* Respond to *ALL* pending group-join requests for a particular group.
* @param {SteamID|string} gid - The SteamID of the group you want to manage
* @param {boolean} approve - True to allow everyone who requested into the group, false to not
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.respondToAllGroupJoinRequests = function(gid, approve, callback) {
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/joinRequestsManage",
"form": {
"bapprove": approve ? "1" : "0",
"json": "1",
"action": "bulkrespond",
"sessionID": this.getSessionID()
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (body != EResult.OK) {
var err = new Error(EResult[body] || ("Error " + body));
err.eresult = body;
callback(err);
} else {
callback(null);
}
}, "steamcommunity");
};

View File

@@ -11,3 +11,13 @@ exports.isSteamID = function(input) {
return keys.length == 4; return keys.length == 4;
}; };
exports.decodeSteamTime = function(time) {
var parts = time.split('@');
if (!parts[0].match(/,/)) {
// no year, assume current year
parts[0] += ", " + (new Date()).getFullYear();
}
return new Date(parts.join('@').replace(/(am|pm)/, ' $1') + " UTC"); // add a space so JS can decode it
};

View File

@@ -47,19 +47,25 @@ SteamCommunity.prototype.httpRequest = function(uri, options, callback, source)
self.request(options, function (err, response, body) { self.request(options, function (err, response, body) {
var hasCallback = !!callback; var hasCallback = !!callback;
var httpError = options.checkHttpError !== false && self._checkHttpError(err, response, callback); var httpError = options.checkHttpError !== false && self._checkHttpError(err, response, callback, body);
var communityError = !options.json && options.checkCommunityError !== false && self._checkCommunityError(body, httpError ? function () {} : callback); // don't fire the callback if hasHttpError did it already var communityError = !options.json && options.checkCommunityError !== false && self._checkCommunityError(body, httpError ? function () {} : callback); // don't fire the callback if hasHttpError did it already
var tradeError = !options.json && options.checkTradeError !== false && self._checkTradeError(body, httpError || communityError ? function () {} : callback); // don't fire the callback if either of the previous already did var tradeError = !options.json && options.checkTradeError !== false && self._checkTradeError(body, httpError || communityError ? function () {} : callback); // don't fire the callback if either of the previous already did
var jsonError = options.json && options.checkJsonError !== false && !body ? new Error("Malformed JSON response") : null;
self.emit('postHttpRequest', requestID, source, options, httpError || communityError || tradeError || null, response, body, { self.emit('postHttpRequest', requestID, source, options, httpError || communityError || tradeError || jsonError || null, response, body, {
"hasCallback": hasCallback, "hasCallback": hasCallback,
"httpError": httpError, "httpError": httpError,
"communityError": communityError, "communityError": communityError,
"tradeError": tradeError "tradeError": tradeError,
"jsonError": jsonError
}); });
if(hasCallback && !(httpError || communityError || tradeError)) { if (hasCallback && !(httpError || communityError || tradeError)) {
callback.apply(self, arguments); if (jsonError) {
callback.call(self, jsonError, response);
} else {
callback.apply(self, arguments);
}
} }
}); });
} }
@@ -79,23 +85,29 @@ SteamCommunity.prototype._notifySessionExpired = function(err) {
this.emit('sessionExpired', err); this.emit('sessionExpired', err);
}; };
SteamCommunity.prototype._checkHttpError = function(err, response, callback) { SteamCommunity.prototype._checkHttpError = function(err, response, callback, body) {
if(err) { if (err) {
callback(err); callback(err, response, body);
return err; return err;
} }
if(response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) { if (response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) {
err = new Error("Not Logged In"); err = new Error("Not Logged In");
callback(err); callback(err, response, body);
this._notifySessionExpired(err); this._notifySessionExpired(err);
return err; return err;
} }
if(response.statusCode >= 400) { if (response.statusCode == 403 && typeof response.body === 'string' && response.body.match(/<div id="parental_notice_instructions">Enter your PIN below to exit Family View.<\/div>/)) {
err = new Error("Family View Restricted");
callback(err, response, body);
return err;
}
if (response.statusCode >= 400) {
err = new Error("HTTP error " + response.statusCode); err = new Error("HTTP error " + response.statusCode);
err.code = response.statusCode; err.code = response.statusCode;
callback(err); callback(err, response, body);
return err; return err;
} }
@@ -130,7 +142,7 @@ SteamCommunity.prototype._checkTradeError = function(html, callback) {
var match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/); var match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/);
if (match) { if (match) {
var err = new Error(match[1].trim()); var err = new Error(match[1].trim());
callback(new Error(err)); callback(err);
return err; return err;
} }

View File

@@ -5,118 +5,158 @@ var request = require('request');
var Cheerio = require('cheerio'); var Cheerio = require('cheerio');
var Async = require('async'); var Async = require('async');
/*
* Inventory history in a nutshell.
*
* There are no more page numbers. Now you have to request after_time and optionally after_trade.
* Without "prev" set, you will request 30 trades that were completed FURTHER IN THE PAST than after_time (and optionally after_trade)
* With "prev" set, you will request 30 trades that were completed MORE RECENTLY than after_time (and optionally after_trade)
*/
SteamCommunity.prototype.getInventoryHistory = function(options, callback) { SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
if(typeof options === 'function') { if (typeof options === 'function') {
callback = options; callback = options;
options = {}; options = {};
} }
options.page = options.page || 1; options.direction = options.direction || "past";
this.httpRequest("https://steamcommunity.com/my/inventoryhistory?l=english&p=" + options.page, function(err, response, body) { var qs = "?l=english";
if(err) { if (options.startTime) {
if (options.startTime instanceof Date) {
options.startTime = Math.floor(options.startTime.getTime() / 1000);
}
qs += "&after_time=" + options.startTime;
if (options.startTrade) {
qs += "&after_trade=" + options.startTrade;
}
}
if (options.direction == "future") {
qs += "&prev=1";
}
this._myProfile("inventoryhistory" + qs, null, function(err, response, body) {
if (err) {
callback(err); callback(err);
return; return;
} }
var output = {}; var output = {};
var vanityURLs = []; var vanityURLs = [];
var $ = Cheerio.load(body); var $ = Cheerio.load(body);
var html = $('.inventory_history_pagingrow').html(); if (!$('.inventory_history_pagingrow').html()) {
if(!html) { callback(new Error("Malformed page: no paging row found"));
callback("Malformed page: no paging row found");
return; return;
} }
var match = html.match(/(\d+) - (\d+) of (\d+) History Items/);
output.first = parseInt(match[1], 10);
output.last = parseInt(match[2], 10);
output.totalTrades = parseInt(match[3], 10);
// Load the inventory item data // Load the inventory item data
var match2 = body.match(/var g_rgHistoryInventory = (.*);/); var match2 = body.match(/var g_rgHistoryInventory = (.*);/);
if(!match2) { if (!match2) {
callback(new Error("Malformed page: no trade found")); callback(new Error("Malformed page: no trade found"));
return; return;
} }
var historyInventory = JSON.parse(match2[1]);
try {
var historyInventory = JSON.parse(match2[1]);
} catch (ex) {
callback(new Error("Malformed page: no well-formed trade data found"));
return;
}
var i;
// See if we've got paging buttons
var $paging = $('.inventory_history_nextbtn .pagebtn:not(.disabled)');
var href;
for (i = 0; i < $paging.length; i++) {
href = $paging[i].attribs.href;
if (href.match(/prev=1/)) {
output.firstTradeTime = new Date(href.match(/after_time=(\d+)/)[1] * 1000);
output.firstTradeID = href.match(/after_trade=(\d+)/)[1];
} else {
output.lastTradeTime = new Date(href.match(/after_time=(\d+)/)[1] * 1000);
output.lastTradeID = href.match(/after_trade=(\d+)/)[1];
}
}
output.trades = []; output.trades = [];
var trades = $('.tradehistoryrow'); var trades = $('.tradehistoryrow');
var item, trade, profileLink, items, j, econItem, timeMatch, time; var item, trade, profileLink, items, j, econItem, timeMatch, time;
for(var i = 0; i < trades.length; i++) { for (i = 0; i < trades.length; i++) {
item = $(trades[i]); item = $(trades[i]);
trade = {}; trade = {};
trade.onHold = !!item.find('span:nth-of-type(2)').text().match(/Trade on Hold/i); trade.onHold = !!item.find('span:nth-of-type(2)').text().match(/Trade on Hold/i);
timeMatch = item.find('.tradehistory_timestamp').html().match(/(\d+):(\d+)(am|pm)/); timeMatch = item.find('.tradehistory_timestamp').html().match(/(\d+):(\d+)(am|pm)/);
if(timeMatch[1] == 12 && timeMatch[3] == 'am') { if (timeMatch[1] == 12 && timeMatch[3] == 'am') {
timeMatch[1] = 0; timeMatch[1] = 0;
} }
if(timeMatch[1] < 12 && timeMatch[3] == 'pm') { if (timeMatch[1] < 12 && timeMatch[3] == 'pm') {
timeMatch[1] = parseInt(timeMatch[1], 10) + 12; timeMatch[1] = parseInt(timeMatch[1], 10) + 12;
} }
time = (timeMatch[1] < 10 ? '0' : '') + timeMatch[1] + ':' + timeMatch[2] + ':00'; time = (timeMatch[1] < 10 ? '0' : '') + timeMatch[1] + ':' + timeMatch[2] + ':00';
trade.date = new Date(item.find('.tradehistory_date').html() + ' ' + time); trade.date = new Date(item.find('.tradehistory_date').html() + ' ' + time + ' UTC');
trade.partnerName = item.find('.tradehistory_event_description a').html(); trade.partnerName = item.find('.tradehistory_event_description a').html();
trade.partnerSteamID = null; trade.partnerSteamID = null;
trade.partnerVanityURL = null; trade.partnerVanityURL = null;
trade.itemsReceived = []; trade.itemsReceived = [];
trade.itemsGiven = []; trade.itemsGiven = [];
profileLink = item.find('.tradehistory_event_description a').attr('href'); profileLink = item.find('.tradehistory_event_description a').attr('href');
if(profileLink.indexOf('/profiles/') != -1) { if (profileLink.indexOf('/profiles/') != -1) {
trade.partnerSteamID = new SteamID(profileLink.match(/(\d+)$/)[1]); trade.partnerSteamID = new SteamID(profileLink.match(/(\d+)$/)[1]);
} else { } else {
trade.partnerVanityURL = profileLink.match(/\/([^\/]+)$/)[1]; trade.partnerVanityURL = profileLink.match(/\/([^\/]+)$/)[1];
if(options.resolveVanityURLs && vanityURLs.indexOf(trade.partnerVanityURL) == -1) { if (options.resolveVanityURLs && vanityURLs.indexOf(trade.partnerVanityURL) == -1) {
vanityURLs.push(trade.partnerVanityURL); vanityURLs.push(trade.partnerVanityURL);
} }
} }
items = item.find('.history_item'); items = item.find('.history_item');
for(j = 0; j < items.length; j++) { for (j = 0; j < items.length; j++) {
match = body.match(new RegExp("HistoryPageCreateItemHover\\( '" + $(items[j]).attr('id') + "', (\\d+), '(\\d+)', '(\\d+|class_\\d+_instance_\\d+|class_\\d+)', '(\\d+)' \\);")); match = body.match(new RegExp("HistoryPageCreateItemHover\\( '" + $(items[j]).attr('id') + "', (\\d+), '(\\d+)', '(\\d+|class_\\d+_instance_\\d+|class_\\d+)', '(\\d+)' \\);"));
econItem = historyInventory[match[1]][match[2]][match[3]]; econItem = historyInventory[match[1]][match[2]][match[3]];
if($(items[j]).attr('id').indexOf('received') != -1) { if ($(items[j]).attr('id').indexOf('received') != -1) {
trade.itemsReceived.push(new CEconItem(econItem)); trade.itemsReceived.push(new CEconItem(econItem));
} else { } else {
trade.itemsGiven.push(new CEconItem(econItem)); trade.itemsGiven.push(new CEconItem(econItem));
} }
} }
output.trades.push(trade); output.trades.push(trade);
} }
if(options.resolveVanityURLs) { if (options.resolveVanityURLs) {
Async.map(vanityURLs, resolveVanityURL, function(err, results) { Async.map(vanityURLs, resolveVanityURL, function(err, results) {
if(err) { if (err) {
callback(err); callback(err);
return; return;
} }
for(i = 0; i < output.trades.length; i++) { for (i = 0; i < output.trades.length; i++) {
if(output.trades[i].partnerSteamID || !output.trades[i].partnerVanityURL) { if (output.trades[i].partnerSteamID || !output.trades[i].partnerVanityURL) {
continue; continue;
} }
// Find the vanity URL // Find the vanity URL
for(j = 0; j < results.length; j++) { for (j = 0; j < results.length; j++) {
if(results[j].vanityURL == output.trades[i].partnerVanityURL) { if (results[j].vanityURL == output.trades[i].partnerVanityURL) {
output.trades[i].partnerSteamID = new SteamID(results[j].steamID); output.trades[i].partnerSteamID = new SteamID(results[j].steamID);
break; break;
} }
} }
} }
callback(null, output); callback(null, output);
}); });
} else { } else {
@@ -127,17 +167,17 @@ SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
function resolveVanityURL(vanityURL, callback) { function resolveVanityURL(vanityURL, callback) {
request("https://steamcommunity.com/id/" + vanityURL + "/?xml=1", function(err, response, body) { request("https://steamcommunity.com/id/" + vanityURL + "/?xml=1", function(err, response, body) {
if(err) { if (err) {
callback(err); callback(err);
return; return;
} }
var match = body.match(/<steamID64>(\d+)<\/steamID64>/); var match = body.match(/<steamID64>(\d+)<\/steamID64>/);
if(!match || !match[1]) { if (!match || !match[1]) {
callback(new Error("Couldn't find Steam ID")); callback(new Error("Couldn't find Steam ID"));
return; return;
} }
callback(null, {"vanityURL": vanityURL, "steamID": match[1]}); callback(null, {"vanityURL": vanityURL, "steamID": match[1]});
}); });
} }

View File

@@ -1,6 +1,7 @@
var SteamCommunity = require('../index.js'); var SteamCommunity = require('../index.js');
var SteamID = require('steamid'); var SteamID = require('steamid');
var CEconItem = require('../classes/CEconItem.js'); var CEconItem = require('../classes/CEconItem.js');
var Helpers = require('./helpers.js');
SteamCommunity.prototype.addFriend = function(userID, callback) { SteamCommunity.prototype.addFriend = function(userID, callback) {
if(typeof userID === 'string') { if(typeof userID === 'string') {
@@ -190,17 +191,43 @@ SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback)
}, "steamcommunity"); }, "steamcommunity");
}; };
SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) { SteamCommunity.prototype.getUserAliases = function(userID, callback) {
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
if(typeof userID === 'function') { this.httpRequestGet({
"uri": "https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/ajaxaliases",
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (typeof body !== 'object') {
callback(new Error("Malformed response"));
return;
}
callback(null, body.map(function(entry) {
entry.timechanged = Helpers.decodeSteamTime(entry.timechanged);
return entry;
}));
}, "steamcommunity");
};
SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
if (typeof userID === 'function') {
callback = userID; callback = userID;
userID = this.steamID; userID = this.steamID;
} }
if(!userID) { if (!userID) {
callback(new Error("No SteamID specified and not logged in")); callback(new Error("No SteamID specified and not logged in"));
return; return;
} }
@@ -213,8 +240,19 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
} }
var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/); var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
if(!match) { if (!match) {
callback(new Error("Malformed response")); var errorMessage = "Malformed response";
if(body.match(/0 items in their inventory\./)){
callback(null, {});
return;
}else if(body.match(/inventory is currently private\./)){
errorMessage = "Private inventory";
}else if(body.match(/profile\_private\_info/)){
errorMessage = "Private profile";
}
callback(new Error(errorMessage));
return; return;
} }
@@ -230,10 +268,19 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
}, "steamcommunity"); }, "steamcommunity");
}; };
/**
* Get the contents of a user's inventory context.
* @deprecated Use getUserInventoryContents instead
* @param {SteamID|string} userID - 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 {function} callback
*/
SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) { SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) {
var self = this; var self = this;
if(typeof userID === 'string') { if (typeof userID === 'string') {
userID = new SteamID(userID); userID = new SteamID(userID);
} }
@@ -243,6 +290,9 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
function get(inventory, currency, start) { function get(inventory, currency, start) {
self.httpRequest({ self.httpRequest({
"uri": "https://steamcommunity.com" + endpoint + "/inventory/json/" + appID + "/" + contextID, "uri": "https://steamcommunity.com" + endpoint + "/inventory/json/" + appID + "/" + contextID,
"headers": {
"Referer": "https://steamcommunity.com" + endpoint + "/inventory"
},
"qs": { "qs": {
"start": start, "start": start,
"trading": tradableOnly ? 1 : undefined "trading": tradableOnly ? 1 : undefined
@@ -254,34 +304,34 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
return; return;
} }
if(!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) { if (!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) {
if(body) { if (body) {
callback(new Error(body.Error || "Malformed response")); callback(new Error(body.Error || "Malformed response"));
} else { } else {
callback(new Error("Malformed response")); callback(new Error("Malformed response"));
} }
return; return;
} }
var i; var i;
for(i in body.rgInventory) { for (i in body.rgInventory) {
if(!body.rgInventory.hasOwnProperty(i)) { if (!body.rgInventory.hasOwnProperty(i)) {
continue; continue;
} }
inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID)); inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
} }
for(i in body.rgCurrency) { for (i in body.rgCurrency) {
if(!body.rgCurrency.hasOwnProperty(i)) { if (!body.rgCurrency.hasOwnProperty(i)) {
continue; continue;
} }
currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID)); currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
} }
if(body.more) { if (body.more) {
var match = response.request.uri.href.match(/\/(profiles|id)\/([^\/]+)\//); var match = response.request.uri.href.match(/\/(profiles|id)\/([^\/]+)\//);
if(match) { if(match) {
endpoint = "/" + match[1] + "/" + match[2]; endpoint = "/" + match[1] + "/" + match[2];
@@ -294,3 +344,120 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
}, "steamcommunity"); }, "steamcommunity");
} }
}; };
/**
* Get the contents of a user's inventory context.
* @param {SteamID|string} userID - 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 {string} [language] - The language of item descriptions to return. Omit for default (which may either be English or your account's chosen language)
* @param {function} callback
*/
SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) {
var self = this;
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
if (typeof language === 'function') {
callback = language;
language = "english";
}
var pos = 1;
get([], []);
function get(inventory, currency, start) {
self.httpRequest({
"uri": "https://steamcommunity.com/inventory/" + userID.getSteamID64() + "/" + appID + "/" + contextID,
"headers": {
"Referer": "https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/inventory"
},
"qs": {
"l": language, // Default language
"count": 5000, // Max items per 'page'
"start_assetid": start
},
"json": true
}, function(err, response, body) {
if (err) {
if (err.message == "HTTP error 403" && body === null) {
// 403 with a body of "null" means the inventory/profile is private.
if (self.steamID && userID.getSteamID64() == self.steamID.getSteamID64()) {
// We can never get private profile error for our own inventory!
self._notifySessionExpired(err);
}
callback(new Error("This profile is private."));
return;
}
if (err.message == "HTTP error 500" && body && body.error) {
err = new Error(body.error);
var match = body.error.match(/^(.+) \((\d+)\)$/);
if (match) {
err.message = match[1];
err.eresult = match[2];
callback(err);
return;
}
}
callback(err);
return;
}
if (body && body.success && body.total_inventory_count === 0) {
// Empty inventory
callback(null, [], [], 0);
return;
}
if (!body || !body.success || !body.assets || !body.descriptions) {
if (body) {
// Dunno if the error/Error property even exists on this new endpoint
callback(new Error(body.error || body.Error || "Malformed response"));
} else {
callback(new Error("Malformed response"));
}
return;
}
for (var i = 0; i < body.assets.length; i++) {
var description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid);
if (!tradableOnly || (description && description.tradable)) {
body.assets[i].pos = pos++;
(body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID));
}
}
if (body.more_items) {
get(inventory, currency, body.last_assetid);
} else {
callback(null, inventory, currency, body.total_inventory_count);
}
}, "steamcommunity");
}
// A bit of optimization; objects are hash tables so it's more efficient to look up by key than to iterate an array
var quickDescriptionLookup = {};
function getDescription(descriptions, classID, instanceID) {
var key = classID + '_' + (instanceID || '0'); // instanceID can be undefined, in which case it's 0.
if (quickDescriptionLookup[key]) {
return quickDescriptionLookup[key];
}
for (var i = 0; i < descriptions.length; i++) {
quickDescriptionLookup[descriptions[i].classid + '_' + (descriptions[i].instanceid || '0')] = descriptions[i];
}
return quickDescriptionLookup[key];
}
};

View File

@@ -3,7 +3,7 @@ var SteamCommunity = require('../index.js');
SteamCommunity.prototype.getWebApiKey = function(domain, callback) { SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this; var self = this;
this.httpRequest({ this.httpRequest({
"uri": "https://steamcommunity.com/dev/apikey", "uri": "https://steamcommunity.com/dev/apikey?l=english",
"followRedirect": false "followRedirect": false
}, function(err, response, body) { }, function(err, response, body) {
if (err) { if (err) {
@@ -21,7 +21,7 @@ SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
callback(null, match[1]); callback(null, match[1]);
} else { } else {
// We need to register a new API key // We need to register a new API key
self.httpRequestPost('https://steamcommunity.com/dev/registerkey', { self.httpRequestPost('https://steamcommunity.com/dev/registerkey?l=english', {
"form": { "form": {
"domain": domain, "domain": domain,
"agreeToTerms": "agreed", "agreeToTerms": "agreed",

115
index.js
View File

@@ -1,15 +1,19 @@
require('@doctormckay/stats-reporter').setup(require('./package.json'));
var Request = require('request'); var Request = require('request');
var RSA = require('node-bignumber').Key; var RSA = require('node-bignumber').Key;
var hex2b64 = require('node-bignumber').hex2b64; var hex2b64 = require('node-bignumber').hex2b64;
var SteamID = require('steamid'); var SteamID = require('steamid');
const USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36"; const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36";
require('util').inherits(SteamCommunity, require('events').EventEmitter); require('util').inherits(SteamCommunity, require('events').EventEmitter);
module.exports = SteamCommunity; module.exports = SteamCommunity;
SteamCommunity.SteamID = SteamID; SteamCommunity.SteamID = SteamID;
SteamCommunity.ConfirmationType = require('./resources/EConfirmationType.js');
SteamCommunity.EResult = require('./resources/EResult.js');
function SteamCommunity(options) { function SteamCommunity(options) {
options = options || {}; options = options || {};
@@ -39,24 +43,31 @@ function SteamCommunity(options) {
defaults.localAddress = options.localAddress; defaults.localAddress = options.localAddress;
} }
this.request = options.request || Request.defaults(); this.request = options.request || Request.defaults({"forever": true}); // "forever" indicates that we want a keep-alive agent
this.request = this.request.defaults(defaults); this.request = this.request.defaults(defaults);
// English // English
this._jar.setCookie(Request.cookie('Steam_Language=english'), 'https://steamcommunity.com'); this._setCookie(Request.cookie('Steam_Language=english'));
// UTC // UTC
this._jar.setCookie(Request.cookie('timezoneOffset=0,0'), 'https://steamcommunity.com'); this._setCookie(Request.cookie('timezoneOffset=0,0'));
} }
SteamCommunity.prototype.login = function(details, callback) { SteamCommunity.prototype.login = function(details, callback) {
if(details.steamguard) { if (!details.accountName || !details.password) {
throw new Error("Missing either accountName or password to login; both are needed");
}
if (details.steamguard) {
var parts = details.steamguard.split('||'); var parts = details.steamguard.split('||');
this._jar.setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), 'https://steamcommunity.com'); this._setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), true);
} }
var self = this; var self = this;
// Delete the cache
delete self._profileURL;
// headers required to convince steam that we're logging in from a mobile device so that we can get the oAuth data // headers required to convince steam that we're logging in from a mobile device so that we can get the oAuth data
var mobileHeaders = { var mobileHeaders = {
"X-Requested-With": "com.valvesoftware.android.steam.community", "X-Requested-With": "com.valvesoftware.android.steam.community",
@@ -65,8 +76,8 @@ SteamCommunity.prototype.login = function(details, callback) {
"Accept": "text/javascript, text/html, application/xml, text/xml, */*" "Accept": "text/javascript, text/html, application/xml, text/xml, */*"
}; };
this._jar.setCookie(Request.cookie("mobileClientVersion=0 (2.1.3)"), "https://steamcommunity.com"); this._setCookie(Request.cookie("mobileClientVersion=0 (2.1.3)"));
this._jar.setCookie(Request.cookie("mobileClient=android"), "https://steamcommunity.com"); this._setCookie(Request.cookie("mobileClient=android"));
this.httpRequestPost("https://steamcommunity.com/login/getrsakey/", { this.httpRequestPost("https://steamcommunity.com/login/getrsakey/", {
"form": {"username": details.accountName}, "form": {"username": details.accountName},
@@ -140,7 +151,7 @@ SteamCommunity.prototype.login = function(details, callback) {
} else { } else {
var sessionID = generateSessionID(); var sessionID = generateSessionID();
var oAuth = JSON.parse( body.oauth ); var oAuth = JSON.parse( body.oauth );
self._jar.setCookie(Request.cookie('sessionid=' + sessionID), 'http://steamcommunity.com'); self._setCookie(Request.cookie('sessionid=' + sessionID));
self.steamID = new SteamID(oAuth.steamid); self.steamID = new SteamID(oAuth.steamid);
self.oAuthToken = oAuth.oauth_token; self.oAuthToken = oAuth.oauth_token;
@@ -158,6 +169,8 @@ SteamCommunity.prototype.login = function(details, callback) {
break; break;
} }
} }
self.setCookies(cookies);
callback(null, sessionID, cookies, steamguard, oAuth.oauth_token); callback(null, sessionID, cookies, steamguard, oAuth.oauth_token);
} }
@@ -167,11 +180,11 @@ SteamCommunity.prototype.login = function(details, callback) {
function deleteMobileCookies() { function deleteMobileCookies() {
var cookie = Request.cookie('mobileClientVersion='); var cookie = Request.cookie('mobileClientVersion=');
cookie.expires = new Date(0); cookie.expires = new Date(0);
self._jar.setCookie(cookie, "https://steamcommunity.com"); self._setCookie(cookie);
cookie = Request.cookie('mobileClient='); cookie = Request.cookie('mobileClient=');
cookie.expires = new Date(0); cookie.expires = new Date(0);
self._jar.setCookie(cookie, "https://steamcommunity.com"); self._setCookie(cookie);
} }
}; };
@@ -209,6 +222,15 @@ SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
}, "steamcommunity"); }, "steamcommunity");
}; };
SteamCommunity.prototype._setCookie = function(cookie, secure) {
var protocol = secure ? "https" : "http";
cookie.secure = !!secure;
this._jar.setCookie(cookie.clone(), protocol + "://steamcommunity.com");
this._jar.setCookie(cookie.clone(), protocol + "://store.steampowered.com");
this._jar.setCookie(cookie.clone(), protocol + "://help.steampowered.com");
};
SteamCommunity.prototype.setCookies = function(cookies) { SteamCommunity.prototype.setCookies = function(cookies) {
var self = this; var self = this;
cookies.forEach(function(cookie) { cookies.forEach(function(cookie) {
@@ -216,8 +238,8 @@ SteamCommunity.prototype.setCookies = function(cookies) {
if(cookieName == 'steamLogin') { if(cookieName == 'steamLogin') {
self.steamID = new SteamID(cookie.match(/=(\d+)/)[1]); self.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
} }
self._jar.setCookie(Request.cookie(cookie), (cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/) ? "https://" : "http://") + "steamcommunity.com"); self._setCookie(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
}); });
}; };
@@ -231,7 +253,7 @@ SteamCommunity.prototype.getSessionID = function() {
} }
var sessionID = generateSessionID(); var sessionID = generateSessionID();
this._jar.setCookie(Request.cookie('sessionid=' + sessionID), "http://steamcommunity.com"); this._setCookie(Request.cookie('sessionid=' + sessionID));
return sessionID; return sessionID;
}; };
@@ -257,13 +279,25 @@ SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
return; return;
} }
if(!body || typeof body.success !== 'boolean') { if (!body || typeof body.success !== 'boolean') {
callback("Invalid response"); callback("Invalid response");
return; return;
} }
if(!body.success) { if (!body.success) {
callback("Incorrect PIN"); switch (body.eresult) {
case 15:
callback("Incorrect PIN");
break;
case 25:
callback("Too many invalid PIN attempts");
break;
default:
callback("Error " + body.eresult);
}
return; return;
} }
@@ -273,37 +307,34 @@ SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
SteamCommunity.prototype.getNotifications = function(callback) { SteamCommunity.prototype.getNotifications = function(callback) {
var self = this; var self = this;
this.httpRequestGet("https://steamcommunity.com/actions/RefreshNotificationArea", function(err, response, body) { this.httpRequestGet({
"uri": "https://steamcommunity.com/actions/GetNotificationCounts",
"json": true
}, function(err, response, body) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
var notifications = { if (!body || !body.notifications) {
"comments": 0, callback(new Error("Malformed response"));
"items": 0, return;
"invites": 0,
"gifts": 0,
"chat": 0,
"trades": 0
};
var items = {
"comments": /(\d+) new comments?/,
"items": /(\d+) new items? in your inventory/,
"invites": /(\d+) new invites?/,
"gifts": /(\d+) new gifts?/,
"chat": /(\d+) unread chat messages?/,
"trades": /(\d+) new trade notifications?/
};
var match;
for(var i in items) {
if(match = body.match(items[i])) {
notifications[i] = parseInt(match[1], 10);
}
} }
var notifications = {
"trades": body.notifications[1] || 0,
"gameTurns": body.notifications[2] || 0,
"moderatorMessages": body.notifications[3] || 0,
"comments": body.notifications[4] || 0,
"items": body.notifications[5] || 0,
"invites": body.notifications[6] || 0,
// dunno about 7
"gifts": body.notifications[8] || 0,
"chat": body.notifications[9] || 0,
"helpRequestReplies": body.notifications[10] || 0,
"accountAlerts": body.notifications[11] || 0
};
callback(null, notifications); callback(null, notifications);
}, "steamcommunity"); }, "steamcommunity");
}; };

View File

@@ -1,8 +1,11 @@
{ {
"name": "steamcommunity", "name": "steamcommunity",
"version": "3.19.8", "version": "3.32.2",
"description": "Provides an interface for logging into and interacting with the Steam Community website", "description": "Provides an interface for logging into and interacting with the Steam Community website",
"keywords": ["steam", "steam community"], "keywords": [
"steam",
"steam community"
],
"homepage": "https://github.com/DoctorMcKay/node-steamcommunity", "homepage": "https://github.com/DoctorMcKay/node-steamcommunity",
"bugs": { "bugs": {
"url": "https://github.com/DoctorMcKay/node-steamcommunity/issues" "url": "https://github.com/DoctorMcKay/node-steamcommunity/issues"
@@ -13,12 +16,13 @@
"url": "https://github.com/DoctorMcKay/node-steamcommunity.git" "url": "https://github.com/DoctorMcKay/node-steamcommunity.git"
}, },
"dependencies": { "dependencies": {
"request": "^2.61.0", "@doctormckay/stats-reporter": "^1.0.2",
"request": "^2.81.0",
"node-bignumber": "^1.2.1", "node-bignumber": "^1.2.1",
"steamid": "^1.0.0", "steamid": "^1.0.0",
"xml2js": "^0.4.11", "xml2js": "^0.4.11",
"cheerio": "0.19.0", "cheerio": "0.22.0",
"async": "^1.4.2", "async": "^2.1.4",
"steam-totp": "^1.3.0" "steam-totp": "^1.3.0"
}, },
"engines": { "engines": {

14
resources/EChatState.js Normal file
View File

@@ -0,0 +1,14 @@
/**
* @enum EChatState
*/
module.exports = {
"Offline": 0,
"LoggingOn": 1,
"LogOnFailed": 2,
"LoggedOn": 3,
"0": "Offline",
"1": "LoggingOn",
"2": "LogOnFailed",
"3": "LoggedOn"
};

View File

@@ -0,0 +1,12 @@
/**
* @enum EConfirmationType
*/
module.exports = {
// 1 is unknown, possibly "Invalid"
"Trade": 2,
"MarketListing": 3,
// 4 is opt-out or other like account confirmation?
"2": "Trade",
"3": "MarketListing"
};

View File

@@ -0,0 +1,23 @@
/**
* @enum EPersonaState
*/
module.exports = {
"Offline": 0,
"Online": 1,
"Busy": 2,
"Away": 3,
"Snooze": 4,
"LookingToTrade": 5,
"LookingToPlay": 6,
"Max": 7,
// Value-to-name mapping for convenience
"0": "Offline",
"1": "Online",
"2": "Busy",
"3": "Away",
"4": "Snooze",
"5": "LookingToTrade",
"6": "LookingToPlay",
"7": "Max",
};

View File

@@ -0,0 +1,27 @@
/**
* @enum EPersonaStateFlag
*/
module.exports = {
"HasRichPresence": 1,
"InJoinableGame": 2,
"Golden": 4, // removed "no longer has any effect"
"OnlineUsingWeb": 256, // removed "renamed to ClientTypeWeb"
"ClientTypeWeb": 256,
"OnlineUsingMobile": 512, // removed "renamed to ClientTypeMobile"
"ClientTypeMobile": 512,
"OnlineUsingBigPicture": 1024, // removed "renamed to ClientTypeTenfoot"
"ClientTypeTenfoot": 1024,
"OnlineUsingVR": 2048, // removed "renamed to ClientTypeVR"
"ClientTypeVR": 2048,
"LaunchTypeGamepad": 4096,
// Value-to-name mapping for convenience
"1": "HasRichPresence",
"2": "InJoinableGame",
"4": "Golden",
"256": "ClientTypeWeb",
"512": "ClientTypeMobile",
"1024": "ClientTypeTenfoot",
"2048": "ClientTypeVR",
"4096": "LaunchTypeGamepad",
};

232
resources/EResult.js Normal file
View File

@@ -0,0 +1,232 @@
/**
* @enum EResult
*/
module.exports = {
"Invalid": 0,
"OK": 1,
"Fail": 2,
"NoConnection": 3,
"InvalidPassword": 5,
"LoggedInElsewhere": 6,
"InvalidProtocolVer": 7,
"InvalidParam": 8,
"FileNotFound": 9,
"Busy": 10,
"InvalidState": 11,
"InvalidName": 12,
"InvalidEmail": 13,
"DuplicateName": 14,
"AccessDenied": 15,
"Timeout": 16,
"Banned": 17,
"AccountNotFound": 18,
"InvalidSteamID": 19,
"ServiceUnavailable": 20,
"NotLoggedOn": 21,
"Pending": 22,
"EncryptionFailure": 23,
"InsufficientPrivilege": 24,
"LimitExceeded": 25,
"Revoked": 26,
"Expired": 27,
"AlreadyRedeemed": 28,
"DuplicateRequest": 29,
"AlreadyOwned": 30,
"IPNotFound": 31,
"PersistFailed": 32,
"LockingFailed": 33,
"LogonSessionReplaced": 34,
"ConnectFailed": 35,
"HandshakeFailed": 36,
"IOFailure": 37,
"RemoteDisconnect": 38,
"ShoppingCartNotFound": 39,
"Blocked": 40,
"Ignored": 41,
"NoMatch": 42,
"AccountDisabled": 43,
"ServiceReadOnly": 44,
"AccountNotFeatured": 45,
"AdministratorOK": 46,
"ContentVersion": 47,
"TryAnotherCM": 48,
"PasswordRequiredToKickSession": 49,
"AlreadyLoggedInElsewhere": 50,
"Suspended": 51,
"Cancelled": 52,
"DataCorruption": 53,
"DiskFull": 54,
"RemoteCallFailed": 55,
"PasswordNotSet": 56, // removed "renamed to PasswordUnset"
"PasswordUnset": 56,
"ExternalAccountUnlinked": 57,
"PSNTicketInvalid": 58,
"ExternalAccountAlreadyLinked": 59,
"RemoteFileConflict": 60,
"IllegalPassword": 61,
"SameAsPreviousValue": 62,
"AccountLogonDenied": 63,
"CannotUseOldPassword": 64,
"InvalidLoginAuthCode": 65,
"AccountLogonDeniedNoMailSent": 66, // removed "renamed to AccountLogonDeniedNoMail"
"AccountLogonDeniedNoMail": 66,
"HardwareNotCapableOfIPT": 67,
"IPTInitError": 68,
"ParentalControlRestricted": 69,
"FacebookQueryError": 70,
"ExpiredLoginAuthCode": 71,
"IPLoginRestrictionFailed": 72,
"AccountLocked": 73, // removed "renamed to AccountLockedDown"
"AccountLockedDown": 73,
"AccountLogonDeniedVerifiedEmailRequired": 74,
"NoMatchingURL": 75,
"BadResponse": 76,
"RequirePasswordReEntry": 77,
"ValueOutOfRange": 78,
"UnexpectedError": 79,
"Disabled": 80,
"InvalidCEGSubmission": 81,
"RestrictedDevice": 82,
"RegionLocked": 83,
"RateLimitExceeded": 84,
"AccountLogonDeniedNeedTwoFactorCode": 85, // removed "renamed to AccountLoginDeniedNeedTwoFactor"
"AccountLoginDeniedNeedTwoFactor": 85,
"ItemOrEntryHasBeenDeleted": 86, // removed "renamed to ItemDeleted"
"ItemDeleted": 86,
"AccountLoginDeniedThrottle": 87,
"TwoFactorCodeMismatch": 88,
"TwoFactorActivationCodeMismatch": 89,
"AccountAssociatedToMultiplePlayers": 90, // removed "renamed to AccountAssociatedToMultiplePartners"
"AccountAssociatedToMultiplePartners": 90,
"NotModified": 91,
"NoMobileDeviceAvailable": 92, // removed "renamed to NoMobileDevice"
"NoMobileDevice": 92,
"TimeIsOutOfSync": 93, // removed "renamed to TimeNotSynced"
"TimeNotSynced": 93,
"SMSCodeFailed": 94,
"TooManyAccountsAccessThisResource": 95, // removed "renamed to AccountLimitExceeded"
"AccountLimitExceeded": 95,
"AccountActivityLimitExceeded": 96,
"PhoneActivityLimitExceeded": 97,
"RefundToWallet": 98,
"EmailSendFailure": 99,
"NotSettled": 100,
"NeedCaptcha": 101,
"GSLTDenied": 102,
"GSOwnerDenied": 103,
"InvalidItemType": 104,
"IPBanned": 105,
"GSLTExpired": 106,
"InsufficientFunds": 107,
"TooManyPending": 108,
// Value-to-name mapping for convenience
"0": "Invalid",
"1": "OK",
"2": "Fail",
"3": "NoConnection",
"5": "InvalidPassword",
"6": "LoggedInElsewhere",
"7": "InvalidProtocolVer",
"8": "InvalidParam",
"9": "FileNotFound",
"10": "Busy",
"11": "InvalidState",
"12": "InvalidName",
"13": "InvalidEmail",
"14": "DuplicateName",
"15": "AccessDenied",
"16": "Timeout",
"17": "Banned",
"18": "AccountNotFound",
"19": "InvalidSteamID",
"20": "ServiceUnavailable",
"21": "NotLoggedOn",
"22": "Pending",
"23": "EncryptionFailure",
"24": "InsufficientPrivilege",
"25": "LimitExceeded",
"26": "Revoked",
"27": "Expired",
"28": "AlreadyRedeemed",
"29": "DuplicateRequest",
"30": "AlreadyOwned",
"31": "IPNotFound",
"32": "PersistFailed",
"33": "LockingFailed",
"34": "LogonSessionReplaced",
"35": "ConnectFailed",
"36": "HandshakeFailed",
"37": "IOFailure",
"38": "RemoteDisconnect",
"39": "ShoppingCartNotFound",
"40": "Blocked",
"41": "Ignored",
"42": "NoMatch",
"43": "AccountDisabled",
"44": "ServiceReadOnly",
"45": "AccountNotFeatured",
"46": "AdministratorOK",
"47": "ContentVersion",
"48": "TryAnotherCM",
"49": "PasswordRequiredToKickSession",
"50": "AlreadyLoggedInElsewhere",
"51": "Suspended",
"52": "Cancelled",
"53": "DataCorruption",
"54": "DiskFull",
"55": "RemoteCallFailed",
"56": "PasswordUnset",
"57": "ExternalAccountUnlinked",
"58": "PSNTicketInvalid",
"59": "ExternalAccountAlreadyLinked",
"60": "RemoteFileConflict",
"61": "IllegalPassword",
"62": "SameAsPreviousValue",
"63": "AccountLogonDenied",
"64": "CannotUseOldPassword",
"65": "InvalidLoginAuthCode",
"66": "AccountLogonDeniedNoMail",
"67": "HardwareNotCapableOfIPT",
"68": "IPTInitError",
"69": "ParentalControlRestricted",
"70": "FacebookQueryError",
"71": "ExpiredLoginAuthCode",
"72": "IPLoginRestrictionFailed",
"73": "AccountLockedDown",
"74": "AccountLogonDeniedVerifiedEmailRequired",
"75": "NoMatchingURL",
"76": "BadResponse",
"77": "RequirePasswordReEntry",
"78": "ValueOutOfRange",
"79": "UnexpectedError",
"80": "Disabled",
"81": "InvalidCEGSubmission",
"82": "RestrictedDevice",
"83": "RegionLocked",
"84": "RateLimitExceeded",
"85": "AccountLoginDeniedNeedTwoFactor",
"86": "ItemDeleted",
"87": "AccountLoginDeniedThrottle",
"88": "TwoFactorCodeMismatch",
"89": "TwoFactorActivationCodeMismatch",
"90": "AccountAssociatedToMultiplePartners",
"91": "NotModified",
"92": "NoMobileDevice",
"93": "TimeNotSynced",
"94": "SMSCodeFailed",
"95": "AccountLimitExceeded",
"96": "AccountActivityLimitExceeded",
"97": "PhoneActivityLimitExceeded",
"98": "RefundToWallet",
"99": "EmailSendFailure",
"100": "NotSettled",
"101": "NeedCaptcha",
"102": "GSLTDenied",
"103": "GSOwnerDenied",
"104": "InvalidItemType",
"105": "IPBanned",
"106": "GSLTExpired",
"107": "InsufficientFunds",
"108": "TooManyPending",
};