Compare commits

..

36 Commits
v4 ... v3.48.7

Author SHA1 Message Date
Alex Corn
c0b8dcf3a2 3.48.7 2025-05-27 20:27:10 -04:00
DoctorMcKay
dcf00b4ea1 Merge pull request #354 from metzz1/master
fix: steam now only accepts 1000 <
2025-05-27 20:26:40 -04:00
metzz
ee0cc34512 fix: steam now only accepts 1000 < 2025-05-28 03:08:12 +03:00
Alex Corn
c52533ea06 3.48.6 2025-02-12 03:16:04 -05:00
Alex Corn
67774f86f7 Handle "empty" CS2 inventories that aren't actually empty 2025-02-12 03:15:54 -05:00
Alex Corn
2f03e09f50 Require steam-session 1.9.1 2025-02-12 03:05:15 -05:00
Alex Corn
2868b1f45f Fixed malformed sessionid returned in login callback 2025-02-12 03:05:07 -05:00
Alex Corn
ac222ef8c3 Add origin header to all non-GET requests 2025-02-12 03:04:45 -05:00
Alex Corn
1c1ff82543 3.48.4 2024-10-05 18:50:47 -04:00
Alex Corn
63015259af Updated readme 2024-10-05 18:47:05 -04:00
Alex Corn
ad67805ad9 Prevent committing twofactor files to git 2024-10-05 18:45:26 -04:00
DoctorMcKay
3347d87f29 Merge pull request #347 from benschool/master
Fixes cache_expiration restore for CS2 Items
2024-10-05 18:41:48 -04:00
Benjamin Tyler
c97351543a Fixes cache_expiration restore for CS2 Items
As of Oct 2024, CS2 Item owner_description has changed from "Tradable After" to "Tradable/Marketable After".
2024-10-04 16:04:47 +01:00
DoctorMcKay
1067d4572e Merge pull request #341 from makss/reduce_redirects
Reduce Steam redirects
2024-04-29 10:21:41 -04:00
makss
49a7165052 Reduce Steam redirects 2024-04-29 16:40:01 +03:00
DoctorMcKay
e16136866c Merge pull request #340 from nolddor/patch-1
fix: getUserInventoryContents() make unnecesary calls
2024-04-18 06:04:06 -04:00
Jack Nolddor
cae85433f9 fix: getUserInventoryContents() make unnecesary calls
Steam inventory limits are back to normal there is no reason to just gather 2k items instead 5k max allowed by API
2024-04-18 08:14:14 +02:00
Alex Corn
ba6e29c935 3.48.2 2024-01-18 06:12:30 -05:00
Alex Corn
de95e25867 Fixed steamID not being properly set when cookies have a domain attribute 2024-01-18 06:12:18 -05:00
Alex Corn
e1820efeca 3.48.1 2024-01-18 05:38:41 -05:00
Alex Corn
7cee24199f Fixed login issue caused by differing tokens on different domains 2024-01-18 05:38:06 -05:00
Alex Corn
90eb4d38eb 3.48.0 2023-12-02 21:23:47 -05:00
Alex Corn
b58745c8b7 Added createWebApiKey method 2023-12-02 20:26:52 -05:00
Alex Corn
53154043a3 Use updated AddAuthenticator parameters
The old way still seems to work, but this is what the official mobile app sends now
2023-11-10 20:50:12 -05:00
Alex Corn
6c77c5231c Updated enable and disable_twofactor examples to not use steam-session 2023-11-10 20:49:48 -05:00
Alex Corn
7c564c1453 3.47.1 2023-10-20 00:40:31 -04:00
Alex Corn
b854d6d4d4 Use custom user agent for logins 2023-10-20 00:39:49 -04:00
Alex Corn
67867978f8 Get default user agent string from @doctormckay/user-agents 2023-10-20 00:39:38 -04:00
Alex Corn
e1af63e171 Use steam-session for logins 2023-10-19 22:12:40 -04:00
Alex Corn
e4da22c464 3.47.0 2023-10-01 21:39:33 -04:00
Alex Corn
0f91adf819 Updated xml2js dependency version 2023-10-01 21:39:16 -04:00
Alex Corn
d51c171a48 Rename clanid argument to curatorId 2023-10-01 21:34:04 -04:00
Alex Corn
50276c3a0c Explicitly define which files get published to npm 2023-10-01 21:33:39 -04:00
Alex Corn
b5b50cec50 Ignore dev/ 2023-10-01 21:30:28 -04:00
Alex Corn
cf4b474c70 Added steamID coalescing helper, although currently unused 2023-10-01 21:30:12 -04:00
3urobeat
bf2b4601ee Add user/workshop/curator follow & unfollow support (#320)
* Add follow & unfollow user functions

* Add follow & unfollow to CSteamUser object methods

* Add support for following & unfollowing curators

* Correctly parse for eresult on error

* Use eresultError() helper
2023-10-01 21:22:44 -04:00
40 changed files with 3003 additions and 2122 deletions

View File

@@ -1,50 +0,0 @@
module.exports = {
env: {
commonjs: true,
es2021: true,
node: true
},
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 12
},
rules: {
// Use tabs for indentation and require 'case' in switch to be indented 1 level (default 0)
indent: ['error', 'tab', {SwitchCase: 1}],
// Single quotes for strings
quotes: ['error', 'single'],
// Always require semicolons
semi: ['error', 'always'],
// Don't use 'var'
'no-var': 'error',
// Only use quotes in object literal keys as needed
'quote-props': ['error', 'as-needed'],
// Don't allow trailing spaces after a line
'no-trailing-spaces': 'error',
// Require spaces before and after keywords (like "if")
'keyword-spacing': 'error',
// Don't allow unused variables, but allow unused function args (e.g. in callbacks) and global vars
'no-unused-vars': ['error', {vars: 'local', args: 'none', varsIgnorePattern: '^_'}],
// Require using dot notation (obj.prop instead of obj['prop']) where possible
'dot-notation': 'error',
// Don't use spaces before parens in anonymous or named functions
'space-before-function-paren': ['error', {anonymous: 'never', named: 'never', asyncArrow: 'always'}]
// We will NOT be using eqeqeq for a few reasons:
// 1. I would have to go through and check every single `==` to make sure that it's not depending on loose equality checks.
// 2. I'm only using ESLint to enforce style, not actual differences in functionality. ==/=== is not merely a style choice.
// Yes, I know that 'no-var' is actually enforcing a difference in functionality, but in practice nobody uses
// (or even knows about) var's hoisting functionality, so at this point it's effectively a style choice.
// 3. A lot of the time, you actually *want* loose equality checks, especially when interacting with a web server
// (as HTTP as no concept of anything but strings). Yes, most of our interaction is JSON, but not all. And even then,
// not all JSON actually serializes numbers as numbers.
// 4. `==` is really nowhere near as dangerous as memes would lead you to believe, if you know what you're doing.
// 5. If the idea behind enforcing `===` is to prevent inexperienced developers from unwittingly introducing bugs
// via loose quality checks, in my opinion it could be just as harmful to instruct a code quality tool to
// naively demand that all `==` become `===`. If a developer were to build code that works, but upon opening
// a pull request they see that ESLint demands they use `===` instead, they might just click "fix" and resubmit,
// expecting the code quality tool to know what it's doing. But it *doesn't* know what it's doing, since it's
// just blindly alerting when it sees `==`. The change in functionality from `==` to `===` could very well
// introduce a bug by itself.
}
};

View File

@@ -1,31 +0,0 @@
name: ESLint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
# Run for external PRs, but not on our own internal PRs as they'll be run by the push to the branch.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'DoctorMcKay/node-steamcommunity'
strategy:
matrix:
node-version: [10.x]
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install
working-directory: .
run: npm install --ignore-scripts
- name: Run ESLint
run: npm run lint

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
node_modules/*
node_modules/
test.js
dev/

View File

@@ -1,45 +1,15 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</value>
</option>
<option name="LINE_SEPARATOR" value="&#10;" />
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
</JSCodeStyleSettings>
<codeStyleSettings language="CSS">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="INDENT_CASE_FROM_SWITCH" value="false" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

View File

@@ -3,4 +3,4 @@
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>
</component>

View File

@@ -4,10 +4,8 @@
<inspection_tool class="ES6ConvertRequireIntoImport" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ES6ConvertToForOf" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ES6ConvertVarToLetConst" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JSEqualityComparisonWithCoercion" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JSFunctionExpressionToArrowFunction" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="JSStringConcatenationToES6Template" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@@ -4,9 +4,8 @@
[![license](https://img.shields.io/npm/l/steamcommunity.svg)](https://github.com/DoctorMcKay/node-steamcommunity/blob/master/LICENSE)
[![paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity&currency_code=USD)
This module provides an easy interface for the Steam Community website. This module can be used to simply login to steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
It supports Steam Guard and CAPTCHAs.
This module provides an easy interface for the Steam Community website. This module can be used to simply login to
steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
**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/).

View File

@@ -1,6 +1,4 @@
const StdLib = require('@doctormckay/stdlib');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
module.exports = CConfirmation;
@@ -20,33 +18,20 @@ function CConfirmation(community, data) {
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null;
}
/**
* @param {number} time
* @param {string} key
* @param {function} [callback]
* @return Promise<{offerID: number}>
*/
CConfirmation.prototype.getOfferID = function(time, key, callback) {
return StdLib.Promises.callbackPromise(['offerID'], null, false, async (resolve, reject) => {
if (this.type && this.creator) {
if (this.type != SteamCommunity.ConfirmationType.Trade) {
return reject(new Error('Not a trade confirmation'));
}
return resolve({offerID: this.creator});
if (this.type && this.creator) {
if (this.type != SteamCommunity.ConfirmationType.Trade) {
callback(new Error('Not a trade confirmation'));
return;
}
return await this._community.getConfirmationOfferID(this.id, time, key, callback);
});
callback(null, this.creator);
return;
}
this._community.getConfirmationOfferID(this.id, time, key, callback);
};
/**
* @param {number} time
* @param {string} key
* @param {boolean} accept
* @param {function} [callback]
* @return Promise<void>
*/
CConfirmation.prototype.respond = function(time, key, accept, callback) {
return this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback);
this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback);
};

View File

@@ -1,11 +1,14 @@
module.exports = CEconItem;
function CEconItem(item, description, contextID) {
for (let thing in item) {
this[thing] = item[thing];
var thing;
for (thing in item) {
if (item.hasOwnProperty(thing)) {
this[thing] = item[thing];
}
}
let 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
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);
@@ -24,8 +27,10 @@ function CEconItem(item, description, contextID) {
description = description[this.classid + '_' + this.instanceid];
}
for (let thing in description) {
this[thing] = description[thing];
for (thing in description) {
if (description.hasOwnProperty(thing)) {
this[thing] = description[thing];
}
}
}
@@ -44,26 +49,28 @@ function CEconItem(item, description, contextID) {
// Restore old property names of tags
if (this.tags) {
this.tags = this.tags.map((tag) => ({
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
}));
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
let match;
if (this.appid == 753 && this.contextid == 6 && this.market_hash_name && (match = this.market_hash_name.match(/^(\d+)-/))) {
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);
}
// Restore cache_expiration, if we can (for CS:GO items)
if (this.appid == 730 && this.contextid == 2 && this.owner_descriptions) {
let description = this.owner_descriptions.find(d => d.value && d.value.indexOf('Tradable After ') == 0);
let description = this.owner_descriptions.find(d => d.value && d.value.indexOf('Tradable/Marketable After ') == 0);
if (description) {
let date = new Date(description.value.substring(15).replace(/[,()]/g, ''));
let date = new Date(description.value.substring(26).replace(/[,()]/g, ''));
if (date) {
this.cache_expiration = date.toISOString();
}
@@ -75,7 +82,7 @@ function CEconItem(item, description, contextID) {
this.cache_expiration = this.item_expiration;
}
if (this.actions === '') {
if (this.actions === "") {
this.actions = [];
}
@@ -87,15 +94,15 @@ function CEconItem(item, description, contextID) {
}
CEconItem.prototype.getImageURL = function() {
return 'https://steamcommunity-a.akamaihd.net/economy/image/' + this.icon_url + '/';
return "https://steamcommunity-a.akamaihd.net/economy/image/" + this.icon_url + "/";
};
CEconItem.prototype.getLargeImageURL = function() {
if (!this.icon_url_large) {
if(!this.icon_url_large) {
return this.getImageURL();
}
return 'https://steamcommunity-a.akamaihd.net/economy/image/' + this.icon_url_large + '/';
return "https://steamcommunity-a.akamaihd.net/economy/image/" + this.icon_url_large + "/";
};
CEconItem.prototype.getTag = function(category) {
@@ -103,7 +110,7 @@ CEconItem.prototype.getTag = function(category) {
return null;
}
for (let i = 0; i < this.tags.length; i++) {
for (var i = 0; i < this.tags.length; i++) {
if (this.tags[i].category == category) {
return this.tags[i];
}

View File

@@ -1,35 +1,33 @@
const Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
var Cheerio = require('cheerio');
SteamCommunity.prototype.getMarketItem = function(appid, hashName, currency, callback) {
if (typeof currency == 'function') {
if (typeof currency == "function") {
callback = currency;
currency = 1;
}
this.httpRequest('https://steamcommunity.com/market/listings/' + appid + '/' + encodeURIComponent(hashName), (err, response, body) => {
var self = this;
this.httpRequest("https://steamcommunity.com/market/listings/" + appid + "/" + encodeURIComponent(hashName), function(err, response, body) {
if (err) {
callback(err);
return;
}
let $ = Cheerio.load(body);
let $listingTableMessage = $('.market_listing_table_message');
if ($listingTableMessage && $listingTableMessage.text().trim() == 'There are no listings for this item.') {
callback(new Error('There are no listings for this item.'));
var $ = Cheerio.load(body);
if($('.market_listing_table_message') && $('.market_listing_table_message').text().trim() == 'There are no listings for this item.') {
callback(new Error("There are no listings for this item."));
return;
}
let item = new CMarketItem(appid, hashName, this, body, $);
item.updatePrice(currency, (err) => {
if (err) {
var item = new CMarketItem(appid, hashName, self, body, $);
item.updatePrice(currency, function(err) {
if(err) {
callback(err);
} else {
callback(null, item);
}
});
}, 'steamcommunity');
}, "steamcommunity");
};
function CMarketItem(appid, hashName, community, body, $) {
@@ -38,36 +36,38 @@ function CMarketItem(appid, hashName, community, body, $) {
this._community = community;
this._$ = $;
this._country = 'US';
let match = body.match(/var g_strCountryCode = "([^"]+)";/);
if (match) {
this._country = "US";
var match = body.match(/var g_strCountryCode = "([^"]+)";/);
if(match) {
this._country = match[1];
}
this._language = 'english';
this._language = "english";
match = body.match(/var g_strLanguage = "([^"]+)";/);
if (match) {
if(match) {
this._language = match[1];
}
this.commodity = false;
match = body.match(/Market_LoadOrderSpread\(\s*(\d+)\s*\);/);
if (match) {
if(match) {
this.commodity = true;
this.commodityID = parseInt(match[1], 10);
}
this.medianSalePrices = null;
match = body.match(/var line1=([^;]+);/);
if (match) {
if(match) {
try {
this.medianSalePrices = JSON.parse(match[1]);
this.medianSalePrices = this.medianSalePrices.map((item) => ({
hour: new Date(item[0]),
price: item[1],
quantity: parseInt(item[2], 10)
}));
} catch (e) {
this.medianSalePrices = this.medianSalePrices.map(function(item) {
return {
"hour": new Date(item[0]),
"price": item[1],
"quantity": parseInt(item[2], 10)
};
});
} catch(e) {
// ignore
}
}
@@ -91,7 +91,7 @@ function CMarketItem(appid, hashName, community, body, $) {
// TODO: Buying listings and placing buy orders
}
CMarketItem.prototype.updatePrice = function(currency, callback) {
CMarketItem.prototype.updatePrice = function (currency, callback) {
if (this.commodity) {
this.updatePriceForCommodity(currency, callback);
} else {
@@ -100,88 +100,90 @@ CMarketItem.prototype.updatePrice = function(currency, callback) {
};
CMarketItem.prototype.updatePriceForCommodity = function(currency, callback) {
if (!this.commodity) {
throw new Error('Cannot update price for non-commodity item');
if(!this.commodity) {
throw new Error("Cannot update price for non-commodity item");
}
var self = this;
this._community.httpRequest({
url: 'https://steamcommunity.com/market/itemordershistogram?country=US&language=english&currency=' + currency + '&item_nameid=' + this.commodityID,
json: true
}, (err, response, body) => {
"uri": "https://steamcommunity.com/market/itemordershistogram?country=US&language=english&currency=" + currency + "&item_nameid=" + this.commodityID,
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (body.success != 1) {
if (callback) {
callback(new Error('Error ' + body.success));
if(body.success != 1) {
if(callback) {
callback(new Error("Error " + body.success));
}
return;
}
let match = (body.sell_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
if (match) {
this.quantity = parseInt(match[1], 10);
var match = (body.sell_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
if(match) {
self.quantity = parseInt(match[1], 10);
}
this.buyQuantity = 0;
self.buyQuantity = 0;
match = (body.buy_order_summary || '').match(/<span class="market_commodity_orders_header_promote">(\d+)<\/span>/);
if (match) {
this.buyQuantity = parseInt(match[1], 10);
if(match) {
self.buyQuantity = parseInt(match[1], 10);
}
this.lowestPrice = parseInt(body.lowest_sell_order, 10);
this.highestBuyOrder = parseInt(body.highest_buy_order, 10);
self.lowestPrice = parseInt(body.lowest_sell_order, 10);
self.highestBuyOrder = parseInt(body.highest_buy_order, 10);
// TODO: The tables?
if (callback) {
if(callback) {
callback(null);
}
}, 'steamcommunity');
}, "steamcommunity");
};
CMarketItem.prototype.updatePriceForNonCommodity = function(currency, callback) {
if (this.commodity) {
throw new Error('Cannot update price for commodity item');
CMarketItem.prototype.updatePriceForNonCommodity = function (currency, callback) {
if(this.commodity) {
throw new Error("Cannot update price for commodity item");
}
var self = this;
this._community.httpRequest({
url: 'https://steamcommunity.com/market/listings/' +
this._appid + '/' +
"uri": "https://steamcommunity.com/market/listings/" +
this._appid + "/" +
encodeURIComponent(this._hashName) +
'/render/?query=&start=0&count=10&country=US&language=english&currency=' + currency,
json: true
}, (err, response, body) => {
"/render/?query=&start=0&count=10&country=US&language=english&currency=" + currency,
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (body.success != 1) {
callback && callback(new Error('Error ' + body.success));
callback && callback(new Error("Error " + body.success));
return;
}
let match = body.total_count;
var match = body.total_count;
if (match) {
this.quantity = parseInt(match, 10);
self.quantity = parseInt(match, 10);
}
let lowestPrice;
let $ = Cheerio.load(body.results_html);
match = $('.market_listing_price.market_listing_price_with_fee');
var lowestPrice;
var $ = Cheerio.load(body.results_html);
match = $(".market_listing_price.market_listing_price_with_fee");
if (match) {
for (let i = 0; i < match.length; i++) {
lowestPrice = parseFloat($(match[i]).text().replace(',', '.').replace(/[^\d.]/g, ''));
for (var i = 0; i < match.length; i++) {
lowestPrice = parseFloat($(match[i]).text().replace(",", ".").replace(/[^\d.]/g, ''));
if (!isNaN(lowestPrice)) {
this.lowestPrice = lowestPrice;
self.lowestPrice = lowestPrice;
break;
}
}
}
callback && callback(null);
}, 'steamcommunity');
}, "steamcommunity");
};

View File

@@ -1,20 +1,19 @@
const Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
var Cheerio = require('cheerio');
SteamCommunity.prototype.marketSearch = function(options, callback) {
let qs = {};
var qs = {};
if (typeof options === 'string') {
if(typeof options === 'string') {
qs.query = options;
} else {
qs.query = options.query || '';
qs.appid = options.appid;
qs.search_descriptions = options.searchDescriptions ? 1 : 0;
if (qs.appid) {
for (let i in options) {
if (['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) {
if(qs.appid) {
for(var i in options) {
if(['query', 'appid', 'searchDescriptions'].indexOf(i) != -1) {
continue;
}
@@ -29,61 +28,62 @@ SteamCommunity.prototype.marketSearch = function(options, callback) {
qs.sort_column = 'price';
qs.sort_dir = 'asc';
let results = [];
const performSearch = () => {
this.httpRequest({
url: 'https://steamcommunity.com/market/search/render/',
qs: qs,
headers: {
referer: 'https://steamcommunity.com/market/search'
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
}, (err, response, body) => {
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (!body.success) {
callback(new Error('Success is not true'));
if(!body.success) {
callback(new Error("Success is not true"));
return;
}
if (!body.results_html) {
callback(new Error('No results_html in response'));
if(!body.results_html) {
callback(new Error("No results_html in response"));
return;
}
let $ = Cheerio.load(body.results_html);
let $errorMsg = $('.market_listing_table_message');
if ($errorMsg.length > 0) {
var $ = Cheerio.load(body.results_html);
var $errorMsg = $('.market_listing_table_message');
if($errorMsg.length > 0) {
callback(new Error($errorMsg.text()));
return;
}
let rows = $('.market_listing_row_link');
for (let i = 0; i < rows.length; i++) {
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) {
if(body.start + body.pagesize >= body.total_count) {
callback(null, results);
} else {
qs.start += body.pagesize;
performSearch();
}
}, 'steamcommunity');
};
performSearch();
}, "steamcommunity");
}
};
function CMarketSearchResult(row) {
let match = row.attr('href').match(/\/market\/listings\/(\d+)\/([^?/]+)/);
var match = row.attr('href').match(/\/market\/listings\/(\d+)\/([^\?\/]+)/);
this.appid = parseInt(match[1], 10);
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.normal_price').text().replace(/[^\d]+/g, ''), 10);
this.quantity = parseInt(row.find('.market_listing_num_listings_qty').text().replace(/[^\d]+/g, ''), 10);
}

View File

@@ -1,33 +1,33 @@
const SteamID = require('steamid');
const XML2JS = require('xml2js');
const Helpers = require('../components/helpers.js');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
var Helpers = require('../components/helpers.js');
var SteamID = require('steamid');
var xml2js = require('xml2js');
SteamCommunity.prototype.getSteamGroup = function(id, callback) {
if (typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error('id parameter should be a group URL string or a SteamID object');
if(typeof id !== 'string' && !Helpers.isSteamID(id)) {
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)) {
throw new Error('SteamID must stand for a clan account in the public universe');
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");
}
this.httpRequest('https://steamcommunity.com/' + (typeof id === 'string' ? 'groups/' + id : 'gid/' + id.toString()) + '/memberslistxml/?xml=1', (err, response, body) => {
var self = this;
this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "groups/" + id : "gid/" + id.toString()) + "/memberslistxml/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
XML2JS.parseString(body, (err, result) => {
if (err) {
xml2js.parseString(body, function(err, result) {
if(err) {
callback(err);
return;
}
callback(null, new CSteamGroup(this, result.memberList));
callback(null, new CSteamGroup(self, result.memberList));
});
}, 'steamcommunity');
}, "steamcommunity");
};
function CSteamGroup(community, groupData) {
@@ -49,16 +49,16 @@ CSteamGroup.prototype.getAvatarURL = function(size, protocol) {
size = size || '';
protocol = protocol || 'http://';
let url = protocol + 'steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/' + this.avatarHash.substring(0, 2) + '/' + this.avatarHash;
if (size == 'full' || size == 'medium') {
return url + '_' + size + '.jpg';
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + this.avatarHash.substring(0, 2) + "/" + this.avatarHash;
if(size == 'full' || size == 'medium') {
return url + "_" + size + ".jpg";
} else {
return url + '.jpg';
return url + ".jpg";
}
};
CSteamGroup.prototype.getMembers = function(addresses, callback) {
if (typeof addresses === 'function') {
if(typeof addresses === 'function') {
callback = addresses;
addresses = null;
}
@@ -83,11 +83,11 @@ CSteamGroup.prototype.postAnnouncement = function(headline, content, hidden, cal
};
CSteamGroup.prototype.editAnnouncement = function(annoucementID, headline, content, callback) {
this._community.editGroupAnnouncement(this.steamID, annoucementID, headline, content, callback);
this._community.editGroupAnnouncement(this.steamID, annoucementID, headline, content, callback)
};
CSteamGroup.prototype.deleteAnnouncement = function(annoucementID, callback) {
this._community.deleteGroupAnnouncement(this.steamID, annoucementID, callback);
this._community.deleteGroupAnnouncement(this.steamID, annoucementID, callback)
};
CSteamGroup.prototype.scheduleEvent = function(name, type, description, time, server, callback) {
@@ -98,7 +98,7 @@ CSteamGroup.prototype.editEvent = function(id, name, type, description, time, se
this._community.editGroupEvent(this.steamID, id, name, type, description, time, server, callback);
};
CSteamGroup.prototype.deleteEvent = function(id, callback) {
CSteamGroup.prototype.deleteEvent = function (id, callback) {
this._community.deleteGroupEvent(this.steamID, id, callback);
};

View File

@@ -31,7 +31,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) {
};
// Get DOM of sharedfile
this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, async (err, res, body) => {
this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, (err, res, body) => {
try {
/* --------------------- Preprocess output --------------------- */
@@ -136,10 +136,18 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) {
// Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object
let ownerHref = $(".friendBlockLinkOverlay").attr()["href"];
let {steamID} = await this._resolveVanityURL(ownerHref);
sharedfile.owner = steamID;
Helpers.resolveVanityURL(ownerHref, (err, data) => { // This request takes <1 sec
if (err) {
callback(err);
return;
}
sharedfile.owner = new SteamID(data.steamID);
// Make callback when ID was resolved as otherwise owner will always be null
callback(null, new CSteamSharedFile(this, sharedfile));
});
callback(null, new CSteamSharedFile(this, sharedfile));
} catch (err) {
callback(err, null);
}

View File

@@ -1,52 +1,52 @@
const SteamID = require('steamid');
const XML2JS = require('xml2js');
const Helpers = require('../components/helpers.js');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
var Helpers = require('../components/helpers.js');
var SteamID = require('steamid');
var xml2js = require('xml2js');
SteamCommunity.prototype.getSteamUser = function(id, callback) {
if (typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error('id parameter should be a user URL string or a SteamID object');
if(typeof id !== 'string' && !Helpers.isSteamID(id)) {
throw new Error("id parameter should be a user URL string or a SteamID object");
}
if (typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.INDIVIDUAL)) {
throw new Error('SteamID must stand for an individual account in the public universe');
if(typeof id === 'object' && (id.universe != SteamID.Universe.PUBLIC || id.type != SteamID.Type.INDIVIDUAL)) {
throw new Error("SteamID must stand for an individual account in the public universe");
}
this.httpRequest('http://steamcommunity.com/' + (typeof id === 'string' ? 'id/' + id : 'profiles/' + id.toString()) + '/?xml=1', (err, response, body) => {
var self = this;
this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
XML2JS.parseString(body, (err, result) => {
if (err || (!result.response && !result.profile)) {
callback(err || new Error('No valid response'));
xml2js.parseString(body, function(err, result) {
if(err || (!result.response && !result.profile)) {
callback(err || new Error("No valid response"));
return;
}
if (result.response && result.response.error && result.response.error.length) {
if(result.response && result.response.error && result.response.error.length) {
callback(new Error(result.response.error[0]));
return;
}
// Try and find custom URL from redirect
let customurl = null;
if (response.request.redirects && response.request.redirects.length) {
let match = response.request.redirects[0].redirectUri.match(/https?:\/\/steamcommunity\.com\/id\/([^/])+\/\?xml=1/);
if (match) {
var customurl = null;
if(response.request.redirects && response.request.redirects.length) {
var match = response.request.redirects[0].redirectUri.match(/https?:\/\/steamcommunity\.com\/id\/([^/])+\/\?xml=1/);
if(match) {
customurl = match[1];
}
}
if (!result.profile.steamID64) {
callback(new Error('No valid response'));
if(!result.profile.steamID64) {
callback(new Error("No valid response"));
return;
}
callback(null, new CSteamUser(this, result.profile, customurl));
callback(null, new CSteamUser(self, result.profile, customurl));
});
}, 'steamcommunity');
}, "steamcommunity");
};
function CSteamUser(community, userData, customurl) {
@@ -59,7 +59,7 @@ function CSteamUser(community, userData, customurl) {
this.privacyState = processItem('privacyState', 'uncreated');
this.visibilityState = processItem('visibilityState');
this.avatarHash = processItem('avatarIcon', '').match(/([0-9a-f]+)\.[a-z]+$/);
if (this.avatarHash) {
if(this.avatarHash) {
this.avatarHash = this.avatarHash[1];
}
@@ -68,8 +68,8 @@ function CSteamUser(community, userData, customurl) {
this.isLimitedAccount = processItem('isLimitedAccount') == 1;
this.customURL = processItem('customURL', customurl);
if (this.visibilityState == SteamCommunity.PrivacyState.Public) {
let memberSinceValue = processItem('memberSince', '0').replace(/(\d{1,2})(st|nd|th)/, '$1');
if(this.visibilityState == 3) {
let memberSinceValue = processItem('memberSince', '0').replace(/(\d{1,2})(st|nd|th)/, "$1");
if (memberSinceValue.indexOf(',') === -1) {
memberSinceValue += ', ' + new Date().getFullYear();
@@ -91,10 +91,11 @@ function CSteamUser(community, userData, customurl) {
this.groups = null;
this.primaryGroup = null;
if (userData.groups && userData.groups[0] && userData.groups[0].group) {
this.groups = userData.groups[0].group.map((group) => {
if (group.$ && group.$.isPrimary === '1') {
this.primaryGroup = new SteamID(group.groupID64[0]);
var self = this;
if(userData.groups && userData.groups[0] && userData.groups[0].group) {
this.groups = userData.groups[0].group.map(function(group) {
if(group['$'] && group['$'].isPrimary === "1") {
self.primaryGroup = new SteamID(group.groupID64[0]);
}
return new SteamID(group.groupID64[0]);
@@ -102,7 +103,7 @@ function CSteamUser(community, userData, customurl) {
}
function processItem(name, defaultVal) {
if (!userData[name]) {
if(!userData[name]) {
return defaultVal;
}
@@ -114,13 +115,13 @@ CSteamUser.getAvatarURL = function(hash, size, protocol) {
size = size || '';
protocol = protocol || 'http://';
hash = hash || '72f78b4c8cc1f62323f8a33f6d53e27db57c2252'; // The default "?" avatar
hash = hash || "72f78b4c8cc1f62323f8a33f6d53e27db57c2252"; // The default "?" avatar
let url = protocol + 'steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/' + hash.substring(0, 2) + '/' + hash;
if (size == 'full' || size == 'medium') {
return url + '_' + size + '.jpg';
var url = protocol + "steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash;
if(size == 'full' || size == 'medium') {
return url + "_" + size + ".jpg";
} else {
return url + '.jpg';
return url + ".jpg";
}
};
@@ -165,6 +166,14 @@ CSteamUser.prototype.inviteToGroup = function(groupID, callback) {
this._community.inviteUserToGroup(this.steamID, groupID, callback);
};
CSteamUser.prototype.follow = function(callback) {
this._community.followUser(this.steamID, callback);
};
CSteamUser.prototype.unfollow = function(callback) {
this._community.unfollowUser(this.steamID, callback);
};
CSteamUser.prototype.getAliases = function(callback) {
this._community.getUserAliases(this.steamID, callback);
};

283
components/chat.js Normal file
View File

@@ -0,0 +1,283 @@
var SteamCommunity = require('../index.js');
var SteamID = require('steamid');
SteamCommunity.ChatState = require('../resources/EChatState.js');
SteamCommunity.PersonaState = require('../resources/EPersonaState.js');
SteamCommunity.PersonaStateFlag = require('../resources/EPersonaStateFlag.js');
/**
* @deprecated No support for new Steam chat. Use steam-user instead.
* @param {int} interval
* @param {string} uiMode
*/
SteamCommunity.prototype.chatLogon = function(interval, uiMode) {
if(this.chatState == SteamCommunity.ChatState.LoggingOn || this.chatState == SteamCommunity.ChatState.LoggedOn) {
return;
}
interval = interval || 500;
uiMode = uiMode || "web";
this.emit('debug', 'Requesting chat WebAPI token');
this.chatState = SteamCommunity.ChatState.LoggingOn;
var self = this;
this.getWebApiOauthToken(function(err, token) {
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);
return;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logon/v1",
"form": {
"ui_mode": uiMode,
"access_token": token
},
"json": true
}, function(err, response, body) {
if(err || response.statusCode != 200) {
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);
return;
}
if(body.error != 'OK') {
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);
return;
}
self._chat = {
"umqid": body.umqid,
"message": body.message,
"accessToken": token,
"interval": interval,
"uiMode": uiMode
};
self.chatFriends = {};
self.chatState = SteamCommunity.ChatState.LoggedOn;
self.emit('chatLoggedOn');
self._chatPoll();
}, "steamcommunity");
});
};
/**
* @deprecated No support for new Steam chat. Use steam-user instead.
* @param {string|SteamID} recipient
* @param {string} text
* @param {string} [type]
* @param {function} [callback]
*/
SteamCommunity.prototype.chatMessage = function(recipient, text, type, callback) {
if(this.chatState != SteamCommunity.ChatState.LoggedOn) {
throw new Error("Chat must be logged on before messages can be sent");
}
if(typeof recipient === 'string') {
recipient = new SteamID(recipient);
}
if(typeof type === 'function') {
callback = type;
type = 'saytext';
}
type = type || 'saytext';
var self = this;
this.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Message/v1",
"form": {
"access_token": this._chat.accessToken,
"steamid_dst": recipient.toString(),
"text": text,
"type": type,
"umqid": this._chat.umqid
},
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
if (err) {
callback(err);
return;
}
if(body.error != 'OK') {
callback(new Error(body.error));
} else {
callback(null);
}
}, "steamcommunity");
};
/**
* @deprecated No support for new Steam chat. Use steam-user instead.
*/
SteamCommunity.prototype.chatLogoff = function() {
var self = this;
this.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Logoff/v1",
"form": {
"access_token": this._chat.accessToken,
"umqid": this._chat.umqid
}
}, function(err, response, body) {
if(err || response.statusCode != 200) {
self.emit('debug', 'Error logging off of chat: ' + (err ? err.message : "HTTP error " + response.statusCode));
setTimeout(self.chatLogoff.bind(self), 1000);
} else {
self.emit('chatLoggedOff');
clearTimeout(self._chat.timer);
delete self._chat;
delete self.chatFriends;
self.chatState = SteamCommunity.ChatState.Offline;
}
}, "steamcommunity");
};
/**
* @private
*/
SteamCommunity.prototype._chatPoll = function() {
this.emit('debug', 'Doing chat poll');
var self = this;
this.httpRequestPost({
"uri": "https://api.steampowered.com/ISteamWebUserPresenceOAuth/Poll/v1",
"form": {
"umqid": self._chat.umqid,
"message": self._chat.message,
"pollid": 1,
"sectimeout": 20,
"secidletime": 0,
"use_accountids": 1,
"access_token": self._chat.accessToken
},
"json": true
}, function(err, response, body) {
if (self.chatState == SteamCommunity.ChatState.Offline) {
return;
}
self._chat.timer = setTimeout(self._chatPoll.bind(self), self._chat.interval);
if(err || response.statusCode != 200) {
self.emit('debug', 'Error in chat poll: ' + (err ? err.message : "HTTP error " + response.statusCode));
if (err.message == "Not Logged On") {
self._relogWebChat();
}
return;
}
if(!body || body.error != 'OK') {
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;
}
self._chat.message = body.messagelast;
(body.messages || []).forEach(function(message) {
var sender = new SteamID();
sender.universe = SteamID.Universe.PUBLIC;
sender.type = SteamID.Type.INDIVIDUAL;
sender.instance = SteamID.Instance.DESKTOP;
sender.accountid = message.accountid_from;
switch(message.type) {
case 'personastate':
self._chatUpdatePersona(sender);
break;
case 'saytext':
self.emit('chatMessage', sender, message.text);
break;
case 'typing':
self.emit('chatTyping', sender);
break;
default:
self.emit('debug', 'Unhandled chat message type: ' + message.type);
}
});
}, "steamcommunity");
};
/**
* @private
*/
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);
};
/**
* @param {SteamID} steamID
* @private
*/
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);
var self = this;
this.httpRequest({
"uri": "https://steamcommunity.com/chat/friendstate/" + steamID.accountid,
"json": true
}, function(err, response, body) {
if (!self.chatFriends || self.chatState == SteamCommunity.ChatState.Offline) {
return; // welp
}
if(err || response.statusCode != 200) {
self.emit('debug', 'Chat update persona error: ' + (err ? err.message : "HTTP error " + response.statusCode));
setTimeout(function() {
self._chatUpdatePersona(steamID);
}, 2000);
return;
}
var persona = {
"steamID": steamID,
"personaName": body.m_strName,
"personaState": body.m_ePersonaState,
"personaStateFlags": body.m_nPersonaStateFlags || 0,
"avatarHash": body.m_strAvatarHash,
"inGame": !!body.m_bInGame,
"inGameAppID": body.m_nInGameAppID ? parseInt(body.m_nInGameAppID, 10) : null,
"inGameName": body.m_strInGameName || null
};
self.emit('chatPersonaState', steamID, persona);
self.chatFriends[steamID.getSteamID64()] = persona;
}, "steamcommunity");
};

View File

@@ -1,34 +1,47 @@
const Cheerio = require('cheerio');
const StdLib = require('@doctormckay/stdlib');
const SteamTotp = require('steam-totp');
var SteamCommunity = require('../index.js');
var Cheerio = require('cheerio');
var SteamTotp = require('steam-totp');
var Async = require('async');
const SteamCommunity = require('../index.js');
const CConfirmation = require('../classes/CConfirmation.js');
const EConfirmationType = SteamCommunity.EConfirmationType;
var CConfirmation = require('../classes/CConfirmation.js');
var EConfirmationType = require('../resources/EConfirmationType.js');
/**
* Get a list of your account's currently outstanding confirmations.
* @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 'conf' (this key can be reused)
* @param {SteamCommunity~getConfirmations} [callback] - Called when the list of confirmations is received
* @return Promise<{confirmations: CConfirmation[]}>
* @param {SteamCommunity~getConfirmations} callback - Called when the list of confirmations is received
*/
SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
return StdLib.Promises.callbackPromise(['confirmations'], callback, false, async (resolve, reject) => {
let body = await request(this, 'getlist', key, time, 'list', null);
var self = this;
// Ugly hack to maintain backward compatibility
var tag = 'conf';
if (typeof key == 'object') {
tag = key.tag;
key = key.key;
}
// The official Steam app uses the tag 'list', but 'conf' still works so let's use that for backward compatibility.
request(this, 'getlist', key, time, tag, null, true, function(err, body) {
if (err) {
callback(err);
return;
}
if (!body.success) {
if (body.needauth) {
let err = new Error('Not Logged In');
this._notifySessionExpired(err);
return reject(err);
var err = new Error('Not Logged In');
self._notifySessionExpired(err);
callback(err);
return;
}
return reject(new Error(body.message || body.detail || 'Failed to get confirmation list'));
callback(new Error(body.message || body.detail || 'Failed to get confirmation list'));
return;
}
let confs = (body.conf || []).map(conf => new CConfirmation(this, {
var confs = (body.conf || []).map(conf => new CConfirmation(self, {
id: conf.id,
type: conf.type,
creator: conf.creator_id,
@@ -41,14 +54,14 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
icon: conf.icon || ''
}));
resolve({confirmations: confs});
callback(null, confs);
});
};
/**
* @callback SteamCommunity~getConfirmations
* @param {Error|null} err - An Error object on failure, or null on success
* @param {CConfirmation[]} [confirmations] - An array of CConfirmation objects
* @param {CConfirmation[]} confirmations - An array of CConfirmation objects
*/
/**
@@ -56,24 +69,29 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
* @param {int} confID - The ID of the confirmation in question
* @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 "detail" (this key can be reused)
* @param {SteamCommunity~getConfirmationOfferID} [callback]
* @return Promise<{offerID: string|null}>
* @param {SteamCommunity~getConfirmationOfferID} callback
*/
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
return StdLib.Promises.callbackPromise(['offerID'], callback, false, async (resolve, reject) => {
let body = await request(this, 'detailspage/' + confID, key, time, 'detail', null);
// The official Steam app uses the tag 'detail', but 'details' still works so let's use that for backward compatibility
request(this, 'detailspage/' + confID, key, time, 'details', null, false, function(err, body) {
if (err) {
callback(err);
return;
}
if (typeof body != 'string') {
return reject(new Error('Cannot load confirmation details'));
callback(new Error("Cannot load confirmation details"));
return;
}
let $ = Cheerio.load(body);
let offer = $('.tradeoffer');
if (offer.length < 1) {
return resolve({offerID: null});
var $ = Cheerio.load(body);
var offer = $('.tradeoffer');
if(offer.length < 1) {
callback(null, null);
return;
}
resolve({offerID: offer.attr('id').split('_')[1]});
callback(null, offer.attr('id').split('_')[1]);
});
};
@@ -85,30 +103,47 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca
/**
* Confirm or cancel a given confirmation.
* @param {int|int[]|string|string[]} confID - The ID of the confirmation in question, or an array of confirmation IDs
* @param {int|int[]} confID - The ID of the confirmation in question, or an array of confirmation IDs
* @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 {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 {SteamCommunity~genericErrorCallback} [callback] - Called when the request is complete
* @return Promise<void>
* @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete
*/
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let tag = accept ? 'accept' : 'reject';
// Ugly hack to maintain backward compatibility
var tag = accept ? 'allow' : 'cancel';
if (typeof key == 'object') {
tag = key.tag;
key = key.key;
}
// The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility
let body = await request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
op: accept ? 'allow' : 'cancel',
cid: confID,
ck: confKey
});
if (body.success) {
return resolve();
// The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility
request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
op: accept ? 'allow' : 'cancel',
cid: confID,
ck: confKey
}, true, function(err, body) {
if (!callback) {
return;
}
reject(new Error(body.message || body.detail || 'Could not act on confirmation'));
if (err) {
callback(err);
return;
}
if (body.success) {
callback(null);
return;
}
if (body.message) {
callback(new Error(body.message));
return;
}
callback(new Error('Could not act on confirmation'));
});
};
@@ -116,79 +151,101 @@ 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]
* @return Promise<void>
* @param {SteamCommunity~genericErrorCallback} callback
*/
SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) {
var self = this;
this._usedConfTimes = this._usedConfTimes || [];
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
// Figure out our time offset
if (typeof this._timeOffset == 'undefined') {
await new Promise((resolve) => {
SteamTotp.getTimeOffset((err, offset) => {
if (err) {
// not critical that this succeeds
return resolve();
}
if (typeof this._timeOffset !== 'undefined') {
// time offset is already known and saved
doConfirmation();
} else {
SteamTotp.getTimeOffset(function(err, offset) {
if (err) {
callback(err);
return;
}
this._timeOffset = offset;
resolve();
});
});
}
self._timeOffset = offset;
doConfirmation();
let offset = this._timeOffset;
let time = SteamTotp.time(offset);
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
let {confirmations} = await this.getConfirmations(time, key);
setTimeout(function() {
// Delete the saved time offset after 12 hours because why not
delete self._timeOffset;
}, 1000 * 60 * 60 * 12).unref();
});
}
let conf = confirmations.find(conf => conf.creator == objectID);
if (!conf) {
return reject(new Error(`Could not find confirmation for object ${objectID}`));
}
function doConfirmation() {
var offset = self._timeOffset;
var time = SteamTotp.time(offset);
var confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
self.getConfirmations(time, {tag: 'list', key: confKey}, function(err, confs) {
if (err) {
callback(err);
return;
}
// make sure we don't reuse the same time
let localOffset = 0;
do {
time = SteamTotp.time(offset) + localOffset++;
} while (this._usedConfTimes.includes(time));
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;
}
this._usedConfTimes.push(time);
if (this._usedConfTimes.length > 60) {
this._usedConfTimes.splice(0, this._usedConfTimes.length - 60); // we don't need to save more than 60 entries
}
conf = conf[0];
await conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, 'accept'), true);
});
// 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
}
confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'accept');
conf.respond(time, {tag: 'accept', key: confKey}, 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} listKey
* @param {string} acceptKey
* @param {function} [callback]
* @return Promise<{confirmations: CConfirmation[]}>
* @param {string} confKey
* @param {string} allowKey
* @param {function} callback
*/
SteamCommunity.prototype.acceptAllConfirmations = function(time, listKey, acceptKey, callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let {confirmations} = await this.getConfirmations(time, listKey);
SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowKey, callback) {
var self = this;
if (confirmations.length == 0) {
return resolve({confirmations: []});
this.getConfirmations(time, confKey, function(err, confs) {
if (err) {
callback(err);
return;
}
let confIds = confirmations.map(conf => conf.id);
let confKeys = confirmations.map(conf => conf.key);
await this.respondToConfirmation(confIds, confKeys, time, acceptKey, true);
if (confs.length == 0) {
callback(null, []);
return;
}
resolve({confirmations});
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);
});
});
};
async function request(community, url, key, time, tag, params) {
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');
}
@@ -201,10 +258,10 @@ async function request(community, url, key, time, tag, params) {
params.m = 'react';
params.tag = tag;
let req = {
var req = {
method: url == 'multiajaxop' ? 'POST' : 'GET',
url: `https://steamcommunity.com/mobileconf/${url}`,
source: 'steamcommunity'
uri: 'https://steamcommunity.com/mobileconf/' + url,
json: !!json
};
if (req.method == 'GET') {
@@ -213,6 +270,159 @@ async function request(community, url, key, time, tag, params) {
req.form = params;
}
let result = await community.httpRequest(req);
return result.jsonBody || result.textBody;
community.httpRequest(req, function(err, response, body) {
if (err) {
callback(err);
return;
}
callback(null, body);
}, 'steamcommunity');
}
// Confirmation checker
/**
* Start automatically polling our confirmations for new ones. The `confKeyNeeded` event will be emitted when we need a confirmation key, or `newConfirmation` when we get a new confirmation
* @param {int} pollInterval - The interval, in milliseconds, at which we will poll for confirmations. This should probably be at least 10,000 to avoid rate-limits.
* @param {Buffer|string|null} [identitySecret=null] - Your identity_secret. If passed, all confirmations will be automatically accepted and nothing will be emitted.
*/
SteamCommunity.prototype.startConfirmationChecker = function(pollInterval, identitySecret) {
this._confirmationPollInterval = pollInterval;
this._knownConfirmations = this._knownConfirmations || {};
this._confirmationKeys = this._confirmationKeys || {};
this._identitySecret = identitySecret;
if(this._confirmationTimer) {
clearTimeout(this._confirmationTimer);
}
setTimeout(this.checkConfirmations.bind(this), 500);
};
/**
* Stop automatically polling our confirmations.
*/
SteamCommunity.prototype.stopConfirmationChecker = function() {
if(this._confirmationPollInterval) {
delete this._confirmationPollInterval;
}
if(this._identitySecret) {
delete this._identitySecret;
}
if(this._confirmationTimer) {
clearTimeout(this._confirmationTimer);
delete this._confirmationTimer;
}
};
/**
* Run the confirmation checker right now instead of waiting for the next poll.
* Useful to call right after you send/accept an offer that needs confirmation.
*/
SteamCommunity.prototype.checkConfirmations = function() {
if(this._confirmationTimer) {
clearTimeout(this._confirmationTimer);
delete this._confirmationTimer;
}
var self = this;
if(!this._confirmationQueue) {
this._confirmationQueue = Async.queue(function(conf, callback) {
// Worker to process new confirmations
if(self._identitySecret) {
// We should accept this
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 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];
setTimeout(callback, 1000); // Call the callback in 1 second, to make sure the time changes
});
} else {
self.emit('newConfirmation', conf);
setTimeout(callback, 1000); // Call the callback in 1 second, to make sure the time changes
}
}, 1);
}
this.emit('debug', 'Checking confirmations');
this._confirmationCheckerGetKey('conf', function(err, key) {
if(err) {
resetTimer();
return;
}
self.getConfirmations(key.time, key.key, function(err, confirmations) {
if(err) {
self.emit('debug', "Can't check confirmations: " + err.message);
resetTimer();
return;
}
var known = self._knownConfirmations;
var newOnes = confirmations.filter(function(conf) {
return !known[conf.id];
});
if(newOnes.length < 1) {
resetTimer();
return; // No new ones
}
// We have new confirmations!
newOnes.forEach(function(conf) {
self._knownConfirmations[conf.id] = conf; // Add it to our list of known confirmations
self._confirmationQueue.push(conf);
});
resetTimer();
});
});
function resetTimer() {
if(self._confirmationPollInterval) {
self._confirmationTimer = setTimeout(self.checkConfirmations.bind(self), self._confirmationPollInterval);
}
}
};
SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) {
if(this._identitySecret) {
if(tag == 'details') {
// We don't care about details
callback(new Error("Disabled"));
return;
}
var time = Math.floor(Date.now() / 1000);
callback(null, {"time": time, "key": SteamTotp.getConfirmationKey(this._identitySecret, time, tag)});
return;
}
var existing = this._confirmationKeys[tag];
var reusable = ['conf', 'details'];
// See if we already have a key that we can reuse.
if(reusable.indexOf(tag) != -1 && existing && (Date.now() - (existing.time * 1000) < (1000 * 60 * 5))) {
callback(null, existing);
return;
}
// We need a fresh one
var self = this;
this.emit('confKeyNeeded', tag, function(err, time, key) {
if(err) {
callback(err);
return;
}
self._confirmationKeys[tag] = {"time": time, "key": key};
callback(null, {"time": time, "key": key});
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,24 @@
const StdLib = require('@doctormckay/stdlib');
// eslint-disable-next-line no-unused-vars
const {HttpResponse} = require('@doctormckay/stdlib/http');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
const HELP_SITE_DOMAIN = 'https://help.steampowered.com';
/**
* Restore a previously removed steam package from your steam account.
* @param {int|string} packageID
* @param {function} [callback]
* @return Promise<void>
* @param {function} callback
*/
SteamCommunity.prototype.restorePackage = function(packageID, callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let result = await this.httpRequest({
method: 'POST',
url: `${HELP_SITE_DOMAIN}/wizard/AjaxDoPackageRestore`,
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
source: 'steamcommunity'
});
wizardAjaxHandler(result, resolve, reject);
});
this.httpRequestPost({
uri: HELP_SITE_DOMAIN + '/wizard/AjaxDoPackageRestore',
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
json: true
}, wizardAjaxHandler(callback));
};
/**
@@ -35,32 +27,38 @@ SteamCommunity.prototype.restorePackage = function(packageID, callback) {
* @param {function} callback
*/
SteamCommunity.prototype.removePackage = function(packageID, callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let result = await this.httpRequest({
method: 'POST',
url: `${HELP_SITE_DOMAIN}/wizard/AjaxDoPackageRemove`,
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
source: 'steamcommunity'
});
wizardAjaxHandler(result, resolve, reject);
});
this.httpRequestPost({
uri: HELP_SITE_DOMAIN + '/wizard/AjaxDoPackageRemove',
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
json: true
}, wizardAjaxHandler(callback));
};
/**
*
* @param {HttpResponse} result
* @param {function} resolve
* @param {function} reject
* Returns a handler for wizard ajax HTTP requests.
* @param {function} callback
* @returns {(function(*=, *, *): void)|*}
*/
function wizardAjaxHandler(result, resolve, reject) {
if (!result.jsonBody || !result.jsonBody.success) {
return reject(new Error((result.jsonBody || {}).errorMsg || 'Unexpected error'));
}
function wizardAjaxHandler(callback) {
return (err, res, body) => {
if (!callback) {
return;
}
resolve();
if (err) {
callback(err);
return;
}
if (!body.success) {
callback(body.errorMsg ? new Error(body.errorMsg) : Helpers.eresultError(body.success));
return;
}
callback(null);
};
}

View File

@@ -1,32 +1,41 @@
const request = require('request');
const SteamID = require('steamid');
const xml2js = require('xml2js');
const EResult = require('../resources/EResult.js');
/**
* Make sure that a provided input is a valid SteamID object.
* @param {object} input
* @returns {boolean}
*/
exports.isSteamID = function(input) {
return ['universe', 'type', 'instance', 'accountid'].every(prop => typeof input[prop] == 'number' || typeof input[prop] == 'bigint');
var keys = Object.keys(input);
if (keys.length != 4) {
return false;
}
// Make sure it has the keys we expect
keys = keys.filter(function(item) {
return ['universe', 'type', 'instance', 'accountid'].indexOf(item) != -1;
});
return keys.length == 4;
};
exports.decodeSteamTime = function(time) {
let date = new Date();
var date = new Date();
if (time.includes('@')) {
let parts = time.split('@');
if (!parts[0].includes(',')) {
if (time.includes("@")) {
var parts = time.split('@');
if (!parts[0].includes(",")) {
// no year, assume current year
parts[0] += ', ' + date.getFullYear();
parts[0] += ", " + date.getFullYear();
}
date = new Date(parts.join('@').replace(/(am|pm)/, ' $1') + ' UTC'); // add a space so JS can decode it
date = new Date(parts.join('@').replace(/(am|pm)/, ' $1') + " UTC"); // add a space so JS can decode it
} else {
// Relative date
let amount = time.replace(/(\d) (minutes|hour|hours) ago/, '$1');
var amount = time.replace(/(\d) (minutes|hour|hours) ago/, "$1");
if (time.includes('minutes')) {
if(time.includes("minutes")) {
date.setMinutes(date.getMinutes() - amount);
} else if (time.match(/hour|hours/)) {
} else if(time.match(/hour|hours/)) {
date.setHours(date.getHours() - amount);
}
}
@@ -36,17 +45,16 @@ exports.decodeSteamTime = function(time) {
/**
* Get an Error object for a particular EResult
* @param {int|EResult} eresult
* @param {string} [message] - If eresult is a failure code and message exists, this message will be used in the Error object instead
* @param {int} eresult
* @returns {null|Error}
*/
exports.eresultError = function(eresult, message) {
exports.eresultError = function(eresult) {
if (eresult == EResult.OK) {
// no error
return null;
}
let err = new Error(message || EResult[eresult] || `Error ${eresult}`);
var err = new Error(EResult[eresult] || ("Error " + eresult));
err.eresult = eresult;
return err;
};
@@ -62,3 +70,59 @@ exports.decodeJwt = function(jwt) {
return JSON.parse(Buffer.from(standardBase64, 'base64').toString('utf8'));
};
/**
* Resolves a Steam profile URL to get steamID64 and vanityURL
* @param {String} url - Full steamcommunity profile URL or only the vanity part.
* @param {Object} callback - First argument is null/Error, second is object containing vanityURL (String) and steamID (String)
*/
exports.resolveVanityURL = function(url, callback) {
// Precede url param if only the vanity was provided
if (!url.includes("steamcommunity.com")) {
url = "https://steamcommunity.com/id/" + url;
}
// Make request to get XML data
request(url + "/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
// Parse XML data returned from Steam into an object
new xml2js.Parser().parseString(body, (err, parsed) => {
if (err) {
callback(new Error("Couldn't parse XML response"));
return;
}
if (parsed.response && parsed.response.error) {
callback(new Error("Couldn't find Steam ID"));
return;
}
let steamID64 = parsed.profile.steamID64[0];
let vanityURL = parsed.profile.customURL[0];
callback(null, {"vanityURL": vanityURL, "steamID": steamID64});
});
});
};
/**
* Converts `input` into a SteamID object, if it's a parseable string.
* @param {SteamID|string} input
* @return {SteamID}
*/
exports.steamID = function(input) {
if (exports.isSteamID(input)) {
return input;
}
if (typeof input != 'string') {
throw new Error(`Input SteamID value "${input}" is not a string`);
}
// This will throw if the input is not a well-formed SteamID
return new SteamID(input);
};

View File

@@ -1,166 +1,144 @@
const {HttpResponse} = require('@doctormckay/stdlib/http'); // eslint-disable-line
const {betterPromise} = require('@doctormckay/stdlib/promises');
var URL = require('url');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
/**
* @param {object} options
* @param {string} options.method
* @param {string} options.url
* @param {string} [options.source='']
* @param {object} [options.qs]
* @param {*} [options.body]
* @param {object} [options.form]
* @param {object} [options.multipartForm]
* @param {boolean} [options.json=false] - Controls whether the *REQUEST* should be sent as json.
* @param {boolean} [options.followRedirect=true]
* @param {boolean} [options.checkHttpError=true]
* @param {boolean} [options.checkCommunityError=true]
* @param {boolean} [options.checkTradeError=true]
* @param {boolean} [options.checkJsonError=true]
* @return {Promise<HttpResponse>}
*/
SteamCommunity.prototype.httpRequest = function(options) {
return betterPromise(async (resolve, reject) => {
let requestID = ++this._httpRequestID;
let source = options.source || '';
SteamCommunity.prototype.httpRequest = function(uri, options, callback, source) {
if (typeof uri === 'object') {
source = callback;
callback = options;
options = uri;
uri = options.url || options.uri;
} else if (typeof options === 'function') {
source = callback;
callback = options;
options = {};
}
await betterPromise((resolve, reject) => {
if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, (err) => {
err ? reject(err) : resolve();
})) {
// No pre-hook, or the pre-hook doesn't want to delay the request.
resolve();
options.url = options.uri = uri;
if (this._httpRequestConvenienceMethod) {
options.method = this._httpRequestConvenienceMethod;
delete this._httpRequestConvenienceMethod;
}
// Add origin header if necessary
// https://github.com/DoctorMcKay/node-steamcommunity/issues/351
if ((options.method || 'GET').toUpperCase() != 'GET') {
options.headers = options.headers || {};
if (!options.headers.origin) {
var parsedUrl = URL.parse(options.url);
options.headers.origin = parsedUrl.protocol + '//' + parsedUrl.host;
}
}
var requestID = ++this._httpRequestID;
source = source || "";
var self = this;
var continued = false;
if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, continueRequest)) {
// No pre-hook, or the pre-hook doesn't want to delay the request.
continueRequest(null);
}
function continueRequest(err) {
if (continued) {
return;
}
continued = true;
if (err) {
if (callback) {
callback(err);
}
return;
}
self.request(options, function (err, response, body) {
var hasCallback = !!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 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 || jsonError || null, response, body, {
"hasCallback": hasCallback,
"httpError": httpError,
"communityError": communityError,
"tradeError": tradeError,
"jsonError": jsonError
});
if (hasCallback && !(httpError || communityError || tradeError)) {
if (jsonError) {
callback.call(self, jsonError, response);
} else {
callback.apply(self, arguments);
}
}
});
let result = await this._httpClient.request({
method: options.method,
url: options.url,
queryString: options.qs,
headers: options.headers,
body: options.body,
urlEncodedForm: options.form,
multipartForm: options.multipartForm,
json: options.json,
followRedirects: options.followRedirect
});
let httpError = options.checkHttpError !== false && this._checkHttpError(result);
let communityError = !options.json && options.checkCommunityError !== false && this._checkCommunityError(result);
let tradeError = !options.json && options.checkTradeError !== false && this._checkTradeError(result);
let jsonError = options.json && options.checkJsonError !== false && !result.jsonBody ? new Error('Malformed JSON response') : null;
this.emit('postHttpRequest', {
requestID,
source,
options,
response: result,
body: result.textBody,
error: httpError || communityError || tradeError || jsonError || null,
httpError,
communityError,
tradeError,
jsonError
});
resolve(result);
});
}
};
/**
* @param {string|object} endpoint
* @param {object} [form]
* @private
*/
SteamCommunity.prototype._myProfile = async function(endpoint, form) {
if (!this._profileURL) {
let result = await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/my',
followRedirect: false,
source: 'steamcommunity'
});
SteamCommunity.prototype.httpRequestGet = function() {
this._httpRequestConvenienceMethod = "GET";
return this.httpRequest.apply(this, arguments);
};
if (result.statusCode != 302) {
throw new Error(`HTTP error ${result.statusCode}`);
}
let match = result.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/);
if (!match) {
throw new Error('Can\'t get profile URL');
}
this._profileURL = match[1];
setTimeout(() => {
delete this._profileURL; // delete the cache
}, 60000).unref();
}
let options = endpoint.endpoint ? endpoint : {};
options.url = `https://steamcommunity.com${this._profileURL}/${endpoint.endpoint || endpoint}`;
options.followRedirect = true;
if (form) {
options.method = 'POST';
options.form = form;
} else if (!options.method) {
options.method = 'GET';
}
options.source = 'steamcommunity';
return await this.httpRequest(options);
SteamCommunity.prototype.httpRequestPost = function() {
this._httpRequestConvenienceMethod = "POST";
return this.httpRequest.apply(this, arguments);
};
SteamCommunity.prototype._notifySessionExpired = function(err) {
this.emit('sessionExpired', err);
};
/**
* @param {HttpResponse} response
* @return {Error|boolean}
* @private
*/
SteamCommunity.prototype._checkHttpError = function(response) {
SteamCommunity.prototype._checkHttpError = function(err, response, callback, body) {
if (err) {
callback(err, response, body);
return err;
}
if (response.statusCode >= 300 && response.statusCode <= 399 && response.headers.location.indexOf('/login') != -1) {
let err = new Error('Not Logged In');
err = new Error("Not Logged In");
callback(err, response, body);
this._notifySessionExpired(err);
return err;
}
if (
response.statusCode == 403
&& typeof response.textBody == 'string'
&& response.textBody.match(/<div id="parental_notice_instructions">Enter your PIN below to exit Family View.<\/div>/)
) {
return new Error('Family View Restricted');
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) {
let err = new Error(`HTTP error ${response.statusCode}`);
err = new Error("HTTP error " + response.statusCode);
err.code = response.statusCode;
callback(err, response, body);
return err;
}
return false;
};
/**
* @param {HttpResponse} response
* @return {Error|boolean}
* @private
*/
SteamCommunity.prototype._checkCommunityError = function(response) {
let html = response.textBody;
SteamCommunity.prototype._checkCommunityError = function(html, callback) {
var err;
if (typeof html == 'string' && html.match(/<h1>Sorry!<\/h1>/)) {
let match = html.match(/<h3>(.+)<\/h3>/);
return new Error(match ? match[1] : 'Unknown error occurred');
if(typeof html === 'string' && html.match(/<h1>Sorry!<\/h1>/)) {
var match = html.match(/<h3>(.+)<\/h3>/);
err = new Error(match ? match[1] : "Unknown error occurred");
callback(err);
return err;
}
if (typeof html == 'string' && html.indexOf('g_steamID = false;') > -1 && html.indexOf('<title>Sign In</title>') > -1) {
let err = new Error('Not Logged In');
if (typeof html === 'string' && html.indexOf('g_steamID = false;') > -1 && html.indexOf('<title>Sign In</title>') > -1) {
err = new Error("Not Logged In");
callback(err);
this._notifySessionExpired(err);
return err;
}
@@ -168,21 +146,16 @@ SteamCommunity.prototype._checkCommunityError = function(response) {
return false;
};
/**
* @param {HttpResponse} response
* @return {Error|boolean}
* @private
*/
SteamCommunity.prototype._checkTradeError = function(response) {
let html = response.textBody;
SteamCommunity.prototype._checkTradeError = function(html, callback) {
if (typeof html !== 'string') {
return false;
}
let match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/);
var match = html.match(/<div id="error_msg">\s*([^<]+)\s*<\/div>/);
if (match) {
return new Error(match[1].trim());
var err = new Error(match[1].trim());
callback(err);
return err;
}
return false;

View File

@@ -0,0 +1,173 @@
var SteamCommunity = require('../index.js');
var CEconItem = require('../classes/CEconItem.js');
var Helpers = require('./helpers.js');
var SteamID = require('steamid');
var request = require('request');
var Cheerio = require('cheerio');
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)
*/
/**
* @deprecated Use GetTradeHistory instead: https://lab.xpaw.me/steam_api_documentation.html#IEconService_GetTradeHistory_v1
* @param {object} options
* @param {function} callback
*/
SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options.direction = options.direction || "past";
var qs = "?l=english";
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);
return;
}
var output = {};
var vanityURLs = [];
var $ = Cheerio.load(body);
if (!$('.inventory_history_pagingrow').html()) {
callback(new Error("Malformed page: no paging row found"));
return;
}
// Load the inventory item data
var match2 = body.match(/var g_rgHistoryInventory = (.*);/);
if (!match2) {
callback(new Error("Malformed page: no trade found"));
return;
}
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 = [];
var trades = $('.tradehistoryrow');
var item, trade, profileLink, items, j, econItem, timeMatch, time;
for (i = 0; i < trades.length; i++) {
item = $(trades[i]);
trade = {};
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)/);
if (timeMatch[1] == 12 && timeMatch[3] == 'am') {
timeMatch[1] = 0;
}
if (timeMatch[1] < 12 && timeMatch[3] == 'pm') {
timeMatch[1] = parseInt(timeMatch[1], 10) + 12;
}
time = (timeMatch[1] < 10 ? '0' : '') + timeMatch[1] + ':' + timeMatch[2] + ':00';
trade.date = new Date(item.find('.tradehistory_date').html() + ' ' + time + ' UTC');
trade.partnerName = item.find('.tradehistory_event_description a').html();
trade.partnerSteamID = null;
trade.partnerVanityURL = null;
trade.itemsReceived = [];
trade.itemsGiven = [];
profileLink = item.find('.tradehistory_event_description a').attr('href');
if (profileLink.indexOf('/profiles/') != -1) {
trade.partnerSteamID = new SteamID(profileLink.match(/(\d+)$/)[1]);
} else {
trade.partnerVanityURL = profileLink.match(/\/([^\/]+)$/)[1];
if (options.resolveVanityURLs && vanityURLs.indexOf(trade.partnerVanityURL) == -1) {
vanityURLs.push(trade.partnerVanityURL);
}
}
items = item.find('.history_item');
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+)' \\);"));
econItem = historyInventory[match[1]][match[2]][match[3]];
if ($(items[j]).attr('id').indexOf('received') != -1) {
trade.itemsReceived.push(new CEconItem(econItem));
} else {
trade.itemsGiven.push(new CEconItem(econItem));
}
}
output.trades.push(trade);
}
if (options.resolveVanityURLs) {
Async.map(vanityURLs, Helpers.resolveVanityURL, function(err, results) {
if (err) {
callback(err);
return;
}
for (i = 0; i < output.trades.length; i++) {
if (output.trades[i].partnerSteamID || !output.trades[i].partnerVanityURL) {
continue;
}
// Find the vanity URL
for (j = 0; j < results.length; j++) {
if (results[j].vanityURL == output.trades[i].partnerVanityURL) {
output.trades[i].partnerSteamID = new SteamID(results[j].steamID);
break;
}
}
}
callback(null, output);
});
} else {
callback(null, output);
}
}, "steamcommunity");
};

110
components/login.js Normal file
View File

@@ -0,0 +1,110 @@
const {chrome} = require('@doctormckay/user-agents');
const SteamCommunity = require('../index.js');
/**
* @typedef LogOnDetails
* @property {string} accountName
* @property {string} password
* @property {string} [steamguard]
* @property {string} [authCode]
* @property {string} [twoFactorCode]
* @property {boolean} disableMobile
*/
/**
* @typedef LogOnResponse
* @property {string} sessionID
* @property {string[]} cookies
* @property {string} steamguard
* @property {string} [mobileAccessToken]
*/
/**
*
* @param {LogOnDetails} logOnDetails
* @returns {Promise<LogOnResponse>}
* @private
*/
SteamCommunity.prototype._modernLogin = function(logOnDetails) {
return new Promise(async (resolve, reject) => {
if (!isNodeVersionNewEnough()) {
return reject(new Error(`Node.js version is too old! Need >=12.22.0 or later, got ${process.versions.node}.`));
}
if (this._options.request) {
return reject(new Error('SteamCommunity.login() is incompatible with node-steamcommunity v3\'s usage of \'request\'. If you need to specify a custom \'request\' instance (e.g. when using a proxy), use https://www.npmjs.com/package/steam-session directly to log onto Steam.'));
}
// Import this here so we don't cause problems on old Node versions if this code path isn't taken.
const {LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType} = require('steam-session');
let session = new LoginSession(
logOnDetails.disableMobile
? EAuthTokenPlatformType.WebBrowser
: EAuthTokenPlatformType.MobileApp,
{
localAddress: this._options.localAddress,
userAgent: this._options.userAgent || chrome()
}
);
session.on('authenticated', async () => {
try {
let webCookies = await session.getWebCookies();
let sessionIdCookie = webCookies.find(c => c.startsWith('sessionid='));
resolve({
sessionID: sessionIdCookie.split('=')[1].split(';')[0].trim(),
cookies: webCookies,
steamguard: session.steamGuardMachineToken,
mobileAccessToken: logOnDetails.disableMobile ? null : session.accessToken
});
} catch (ex) {
reject(ex);
}
});
session.on('error', (err) => {
reject(err);
});
try {
let startResult = await session.startWithCredentials({
accountName: logOnDetails.accountName,
password: logOnDetails.password,
steamGuardMachineToken: logOnDetails.steamguard,
steamGuardCode: logOnDetails.authCode || logOnDetails.twoFactorCode
});
if (startResult.actionRequired) {
// Cannot continue with login, need something from the user
session.cancelLoginAttempt();
let emailMfaAction = startResult.validActions.find(action => action.type == EAuthSessionGuardType.EmailCode);
if (emailMfaAction) {
let err = new Error('SteamGuard');
err.emaildomain = emailMfaAction.detail;
return reject(err);
}
return reject(new Error('SteamGuardMobile'));
}
} catch (ex) {
return reject(ex);
}
});
};
function isNodeVersionNewEnough() {
let [major, minor] = process.versions.node.split('.');
if (major < 12) {
return false;
}
if (major == 12 && minor < 22) {
return false;
}
return true;
}

View File

@@ -1,6 +1,6 @@
const SteamCommunity = require('../index.js');
const Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
/**
@@ -8,27 +8,28 @@ const Helpers = require('./helpers.js');
* @param {function} callback - First argument is null|Error, second is an object of appid => name
*/
SteamCommunity.prototype.getMarketApps = function(callback) {
this.httpRequest('https://steamcommunity.com/market/', (err, response, body) => {
var self = this;
this.httpRequest('https://steamcommunity.com/market/', function (err, response, body) {
if (err) {
callback(err);
return;
}
let $ = Cheerio.load(body);
var $ = Cheerio.load(body);
if ($('.market_search_game_button_group')) {
let apps = {};
$('.market_search_game_button_group a.game_button').each((i, element) => {
let e = Cheerio.load(element);
let name = e('.game_button_game_name').text().trim();
let url = element.attribs.href;
let appid = url.substr(url.indexOf('=') + 1);
$('.market_search_game_button_group a.game_button').each(function (i, element) {
var e = Cheerio.load(element);
var name = e('.game_button_game_name').text().trim();
var url = element.attribs.href;
var appid = url.substr(url.indexOf('=') + 1);
apps[appid] = name;
});
callback(null, apps);
} else {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
}
}, 'steamcommunity');
}, "steamcommunity");
};
/**
@@ -39,32 +40,34 @@ SteamCommunity.prototype.getMarketApps = function(callback) {
*/
SteamCommunity.prototype.getGemValue = function(appid, assetid, callback) {
this._myProfile({
endpoint: 'ajaxgetgoovalue/',
qs: {
sessionid: this.getSessionID(),
appid: appid,
contextid: 6,
assetid: assetid
"endpoint": "ajaxgetgoovalue/",
"qs": {
"sessionid": this.getSessionID(),
"appid": appid,
"contextid": 6,
"assetid": assetid
},
checkHttpError: false,
json: true
"checkHttpError": false,
"json": true
}, null, (err, res, body) => {
if (err) {
callback(err);
return;
}
let err2 = Helpers.eresultError(body.success, body.message);
if (err2) {
return callback(err2);
}
if (!body.goo_value || !body.strTitle) {
callback(new Error('Malformed response'));
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
callback(null, {promptTitle: body.strTitle, gemValue: parseInt(body.goo_value, 10)});
if (!body.goo_value || !body.strTitle) {
callback(new Error("Malformed response"));
return;
}
callback(null, {"promptTitle": body.strTitle, "gemValue": parseInt(body.goo_value, 10)});
});
};
@@ -77,33 +80,35 @@ SteamCommunity.prototype.getGemValue = function(appid, assetid, callback) {
*/
SteamCommunity.prototype.turnItemIntoGems = function(appid, assetid, expectedGemsValue, callback) {
this._myProfile({
endpoint: 'ajaxgrindintogoo/',
json: true,
checkHttpError: false
"endpoint": "ajaxgrindintogoo/",
"json": true,
"checkHttpError": false
}, {
appid: appid,
contextid: 6,
assetid: assetid,
goo_value_expected: expectedGemsValue,
sessionid: this.getSessionID()
"appid": appid,
"contextid": 6,
"assetid": assetid,
"goo_value_expected": expectedGemsValue,
"sessionid": this.getSessionID()
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
let err2 = Helpers.eresultError(body.success, body.message);
if (err2) {
return callback(err2);
}
if (!body['goo_value_received '] || !body.goo_value_total) { // lol valve, that trailing space is real
callback(new Error('Malformed response'));
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
callback(null, {gemsReceived: parseInt(body['goo_value_received '], 10), totalGems: parseInt(body.goo_value_total, 10)});
});
if (!body['goo_value_received '] || !body.goo_value_total) { // lol valve
callback(new Error("Malformed response"));
return;
}
callback(null, {"gemsReceived": parseInt(body['goo_value_received '], 10), "totalGems": parseInt(body.goo_value_total, 10)});
})
};
/**
@@ -114,31 +119,33 @@ SteamCommunity.prototype.turnItemIntoGems = function(appid, assetid, expectedGem
*/
SteamCommunity.prototype.openBoosterPack = function(appid, assetid, callback) {
this._myProfile({
endpoint: 'ajaxunpackbooster/',
json: true,
checkHttpError: false
"endpoint": "ajaxunpackbooster/",
"json": true,
"checkHttpError": false
}, {
appid: appid,
communityitemid: assetid,
sessionid: this.getSessionID()
"appid": appid,
"communityitemid": assetid,
"sessionid": this.getSessionID()
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
let err2 = Helpers.eresultError(body.success, body.message);
if (err2) {
return callback(err2);
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
if (!body.rgItems) {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
return;
}
callback(null, body.rgItems);
});
})
};
/**
@@ -221,7 +228,7 @@ SteamCommunity.prototype.createBoosterPack = function(appid, useUntradableGems,
}
this.httpRequestPost({
url: 'https://steamcommunity.com/tradingcards/ajaxcreatebooster/',
uri: 'https://steamcommunity.com/tradingcards/ajaxcreatebooster/',
form: {
sessionid: this.getSessionID(),
appid,
@@ -247,7 +254,6 @@ SteamCommunity.prototype.createBoosterPack = function(appid, useUntradableGems,
// We can now check HTTP status codes
if (this._checkHttpError(err, res, callback, body)) {
// TODO v4
return;
}
@@ -267,31 +273,33 @@ SteamCommunity.prototype.createBoosterPack = function(appid, useUntradableGems,
*/
SteamCommunity.prototype.getGiftDetails = function(giftID, callback) {
this.httpRequestPost({
url: `https://steamcommunity.com/gifts/${giftID}/validateunpack`,
form: {
sessionid: this.getSessionID()
"uri": "https://steamcommunity.com/gifts/" + giftID + "/validateunpack",
"form": {
"sessionid": this.getSessionID()
},
json: true
"json": true
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
let err2 = Helpers.eresultError(body.success, body.message);
if (err2) {
return callback(err2);
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
if (!body.packageid || !body.gift_name) {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
return;
}
callback(null, {
giftName: body.gift_name,
packageID: parseInt(body.packageid, 10),
owned: body.owned
"giftName": body.gift_name,
"packageID": parseInt(body.packageid, 10),
"owned": body.owned
});
});
};
@@ -303,20 +311,23 @@ SteamCommunity.prototype.getGiftDetails = function(giftID, callback) {
*/
SteamCommunity.prototype.redeemGift = function(giftID, callback) {
this.httpRequestPost({
url: `https://steamcommunity.com/gifts/${giftID}/unpack`,
form: {
sessionid: this.getSessionID()
"uri": "https://steamcommunity.com/gifts/" + giftID + "/unpack",
"form": {
"sessionid": this.getSessionID()
},
json: true
"json": true
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
let err2 = Helpers.eresultError(body.success, body.message);
if (err2) {
return callback(err2);
if (body.success && body.success != SteamCommunity.EResult.OK) {
let err = new Error(body.message || SteamCommunity.EResult[body.success]);
err.eresult = err.code = body.success;
callback(err);
return;
}
callback(null);

View File

@@ -1,46 +1,40 @@
const Cheerio = require('cheerio');
const FS = require('fs');
const SteamID = require('steamid');
const Helpers = require('./helpers.js');
const SteamCommunity = require('../index.js');
SteamCommunity.PrivacyState = {
Private: 1,
FriendsOnly: 2,
Public: 3
"Private": 1,
"FriendsOnly": 2,
"Public": 3
};
const CommentPrivacyState = {
1: 2, // private
2: 0, // friends only
3: 1 // anyone
var CommentPrivacyState = {
"1": 2, // private
"2": 0, // friends only
"3": 1 // anyone
};
/**
* Creates a profile page if you don't already have one.
* @param {function} callback
*/
SteamCommunity.prototype.setupProfile = function(callback) {
this._myProfile('edit?welcomed=1', null, (err, response, body) => {
if (!callback) {
var self = this;
this._myProfile("edit?welcomed=1", null, function(err, response, body) {
if(!callback) {
return;
}
if (err || response.statusCode != 200) {
callback(err || new Error('HTTP error ' + response.statusCode));
if(err || response.statusCode != 200) {
callback(err || new Error("HTTP error " + response.statusCode));
} else {
callback(null);
}
});
};
/**
* Edits your profile details.
* @param {object} settings
* @param {function} callback
*/
SteamCommunity.prototype.editProfile = function(settings, callback) {
this._myProfile('edit/info', null, (err, response, body) => {
var self = this;
this._myProfile('edit/info', null, function(err, response, body) {
if (err || response.statusCode != 200) {
if (callback) {
callback(err || new Error('HTTP error ' + response.statusCode));
@@ -49,8 +43,8 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
return;
}
let $ = Cheerio.load(body);
let existingSettings = $('#profile_edit_config').data('profile-edit');
var $ = Cheerio.load(body);
var existingSettings = $('#profile_edit_config').data('profile-edit');
if (!existingSettings || !existingSettings.strPersonaName) {
if (callback) {
callback(new Error('Malformed response'));
@@ -59,8 +53,8 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
return;
}
let values = {
sessionID: this.getSessionID(),
var values = {
sessionID: self.getSessionID(),
type: 'profileSave',
weblink_1_title: '',
weblink_1_url: '',
@@ -78,8 +72,12 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
json: 1
};
for (let i in settings) {
switch (i) {
for (var i in settings) {
if(!settings.hasOwnProperty(i)) {
continue;
}
switch(i) {
case 'name':
values.personaName = settings[i];
break;
@@ -133,9 +131,9 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
}
}
this._myProfile('edit', values, (err, response, body) => {
self._myProfile('edit', values, function(err, response, body) {
if (settings.customURL) {
delete this._profileURL;
delete self._profileURL;
}
if (!callback) {
@@ -148,10 +146,10 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
}
try {
let json = JSON.parse(body);
let err2 = Helpers.eresultError(json.success, json.errmsg);
if (err2) {
return callback(err2);
var json = JSON.parse(body);
if (!json.success || json.success != 1) {
callback(new Error(json.errmsg || 'Request was not successful'));
return;
}
callback(null);
@@ -172,8 +170,8 @@ SteamCommunity.prototype.profileSettings = function(settings, callback) {
return;
}
let $ = Cheerio.load(body);
let existingSettings = $('#profile_edit_config').data('profile-edit');
var $ = Cheerio.load(body);
var existingSettings = $('#profile_edit_config').data('profile-edit');
if (!existingSettings || !existingSettings.Privacy) {
if (callback) {
callback(new Error('Malformed response'));
@@ -184,10 +182,14 @@ SteamCommunity.prototype.profileSettings = function(settings, callback) {
// PrivacySettings => {PrivacyProfile, PrivacyInventory, PrivacyInventoryGifts, PrivacyOwnedGames, PrivacyPlaytime}
// eCommentPermission
let privacy = existingSettings.Privacy.PrivacySettings;
let commentPermission = existingSettings.Privacy.eCommentPermission;
var privacy = existingSettings.Privacy.PrivacySettings;
var commentPermission = existingSettings.Privacy.eCommentPermission;
for (var i in settings) {
if (!settings.hasOwnProperty(i)) {
continue;
}
for (let i in settings) {
switch (i) {
case 'profile':
privacy.PrivacyProfile = settings[i];
@@ -228,7 +230,7 @@ SteamCommunity.prototype.profileSettings = function(settings, callback) {
Privacy: JSON.stringify(privacy),
eCommentPermission: commentPermission
}
}, null, (err, response, body) => {
}, null, function(err, response, body) {
if (err || response.statusCode != 200) {
if (callback) {
callback(err || new Error('HTTP error ' + response.statusCode));
@@ -237,9 +239,11 @@ SteamCommunity.prototype.profileSettings = function(settings, callback) {
return;
}
let err2 = Helpers.eresultError(body.success);
if (err2) {
callback && callback(err2);
if (body.success != 1) {
if (callback) {
callback(new Error(body.success ? 'Error ' + body.success : 'Request was not successful'));
}
return;
}
@@ -251,34 +255,78 @@ SteamCommunity.prototype.profileSettings = function(settings, callback) {
};
SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
if (typeof format === 'function') {
if(typeof format === 'function') {
callback = format;
format = null;
}
// are we logged in?
if (!this.steamID) {
callback(new Error('Not Logged In'));
callback(new Error("Not Logged In"));
return;
}
const doUpload = (buffer) => {
if (!format) {
if (callback) {
callback(new Error('Unknown image format'));
var self = this;
if(image instanceof Buffer) {
doUpload(image);
} else if(image.match(/^https?:\/\//)) {
this.httpRequestGet({
"uri": image,
"encoding": null
}, function(err, response, body) {
if(err || response.statusCode != 200) {
if(callback) {
callback(err ? new Error(err.message + " downloading image") : new Error("HTTP error " + response.statusCode + " downloading image"));
}
return;
}
if(!format) {
format = response.headers['content-type'];
}
doUpload(body);
}, "steamcommunity");
} else {
if(!format) {
format = image.match(/\.([^\.]+)$/);
if(format) {
format = format[1];
}
}
FS.readFile(image, function(err, file) {
if(err) {
if(callback) {
callback(err);
}
return;
}
doUpload(file);
})
}
function doUpload(buffer) {
if(!format) {
if(callback) {
callback(new Error("Unknown image format"));
}
return;
}
if (format.match(/^image\//)) {
if(format.match(/^image\//)) {
format = format.substring(6);
}
let filename = '';
let contentType = '';
var filename = '';
var contentType = '';
switch (format.toLowerCase()) {
switch(format.toLowerCase()) {
case 'jpg':
case 'jpeg':
filename = 'avatar.jpg';
@@ -296,96 +344,68 @@ SteamCommunity.prototype.uploadAvatar = function(image, format, callback) {
break;
default:
if (callback) {
callback(new Error('Unknown or invalid image format'));
if(callback) {
callback(new Error("Unknown or invalid image format"));
}
return;
}
this.httpRequestPost({
url: 'https://steamcommunity.com/actions/FileUploader',
formData: {
MAX_FILE_SIZE: buffer.length,
type: 'player_avatar_image',
sId: this.steamID.getSteamID64(),
sessionid: this.getSessionID(),
doSub: 1,
json: 1,
avatar: {
value: buffer,
options: {
filename: filename,
contentType: contentType
self.httpRequestPost({
"uri": "https://steamcommunity.com/actions/FileUploader",
"formData": {
"MAX_FILE_SIZE": buffer.length,
"type": "player_avatar_image",
"sId": self.steamID.getSteamID64(),
"sessionid": self.getSessionID(),
"doSub": 1,
"json": 1,
"avatar": {
"value": buffer,
"options": {
"filename": filename,
"contentType": contentType
}
}
},
json: true
}, (err, response, body) => {
if (err) {
callback && callback(err);
return;
}
if (body && !body.success && body.message) {
callback && callback(new Error(body.message));
return;
}
if (response.statusCode != 200) {
callback && callback(new Error(`HTTP error ${response.statusCode}`));
return;
}
if (!body || !body.success) {
callback && callback(new Error('Malformed response'));
return;
}
callback && callback(null, body.images.full);
}, 'steamcommunity');
};
if (image instanceof Buffer) {
doUpload(image);
} else if (image.match(/^https?:\/\//)) {
this.httpRequestGet({
url: image,
encoding: null
}, (err, response, body) => {
if (err || response.statusCode != 200) {
if (callback) {
callback(new Error(err ? `${err.message} downloading image` : `HTTP error ${response.statusCode} downloading image`));
}
return;
}
if (!format) {
format = response.headers['content-type'];
}
doUpload(body);
}, 'steamcommunity');
} else {
if (!format) {
format = image.match(/\.([^.]+)$/);
if (format) {
format = format[1];
}
}
FS.readFile(image, (err, file) => {
if (err) {
if (callback) {
"json": true
}, function(err, response, body) {
if(err) {
if(callback) {
callback(err);
}
return;
}
doUpload(file);
});
if(body && !body.success && body.message) {
if(callback) {
callback(new Error(body.message));
}
return;
}
if(response.statusCode != 200) {
if(callback) {
callback(new Error("HTTP error " + response.statusCode));
}
return;
}
if(!body || !body.success) {
if(callback) {
callback(new Error("Malformed response"));
}
return;
}
if(callback) {
callback(null, body.images.full);
}
}, "steamcommunity");
}
};
@@ -401,10 +421,10 @@ SteamCommunity.prototype.postProfileStatus = function(statusText, options, callb
options = {};
}
this._myProfile('ajaxpostuserstatus/', {
appid: options.appID || 0,
sessionid: this.getSessionID(),
status_text: statusText
this._myProfile("ajaxpostuserstatus/", {
"appid": options.appID || 0,
"sessionid": this.getSessionID(),
"status_text": statusText
}, (err, res, body) => {
try {
body = JSON.parse(body);
@@ -413,9 +433,9 @@ SteamCommunity.prototype.postProfileStatus = function(statusText, options, callb
return;
}
let match = body.blotter_html.match(/id="userstatus_(\d+)_/);
var match = body.blotter_html.match(/id="userstatus_(\d+)_/);
if (!match) {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
return;
}
@@ -432,9 +452,9 @@ SteamCommunity.prototype.postProfileStatus = function(statusText, options, callb
* @param {function} [callback]
*/
SteamCommunity.prototype.deleteProfileStatus = function(postID, callback) {
this._myProfile('ajaxdeleteuserstatus/', {
sessionid: this.getSessionID(),
postid: postID
this._myProfile("ajaxdeleteuserstatus/", {
"sessionid": this.getSessionID(),
"postid": postID
}, (err, res, body) => {
if (!callback) {
return;
@@ -443,7 +463,7 @@ SteamCommunity.prototype.deleteProfileStatus = function(postID, callback) {
try {
body = JSON.parse(body);
if (!body.success) {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
return;
}

View File

@@ -1,7 +1,6 @@
const StdLib = require('@doctormckay/stdlib');
const SteamID = require('steamid');
var SteamID = require('steamid');
const SteamCommunity = require('../index.js');
var SteamCommunity = require('../index.js');
/**
@@ -9,52 +8,50 @@ const SteamCommunity = require('../index.js');
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
* @param {String} sharedFileId - ID of the sharedfile
* @param {String} cid - ID of the comment to delete
* @param {function} [callback] - Takes only an Error object/null as the first argument
* @return Promise<void>
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.deleteSharedFileComment = function(userID, sharedFileId, cid, callback) {
if (typeof userID == 'string') {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'POST',
url: `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`,
form: {
gidcomment: cid,
count: 10,
sessionid: this.getSessionID()
},
source: 'steamcommunity'
});
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`,
"form": {
"gidcomment": cid,
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
resolve();
});
callback(err);
}, "steamcommunity");
};
/**
* Favorites a sharedfile
* @param {String} sharedFileId - ID of the sharedfile
* @param {String} appid - ID of the app associated to this sharedfile
* @param {function} [callback] - Takes only an Error object/null as the first argument
* @return Promise<void>
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'POST',
url: 'https://steamcommunity.com/sharedfiles/favorite',
form: {
id: sharedFileId,
appid,
sessionid: this.getSessionID()
},
source: 'steamcommunity'
});
this.httpRequestPost({
"uri": "https://steamcommunity.com/sharedfiles/favorite",
"form": {
"id": sharedFileId,
"appid": appid,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
resolve();
});
callback(err);
}, "steamcommunity");
};
/**
@@ -62,79 +59,76 @@ SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, call
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
* @param {String} sharedFileId - ID of the sharedfile
* @param {String} message - Content of the comment to post
* @param {function} [callback] - Takes only an Error object/null as the first argument
* @return Promise<void>
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) {
if (typeof userID == 'string') {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'POST',
url: `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`,
form: {
comment: message,
count: 10,
sessionid: this.getSessionID()
},
source: 'steamcommunity'
});
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`,
"form": {
"comment": message,
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
resolve();
});
callback(err);
}, "steamcommunity");
};
/**
* Subscribes to a sharedfile's comment section. Note: Checkbox on webpage does not update
* @param {SteamID | String} userID ID of the user associated to this sharedfile
* @param {String} sharedFileId ID of the sharedfile
* @param {function} [callback] - Takes only an Error object/null as the first argument
* @return Promise<void>
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.subscribeSharedFileComments = function(userID, sharedFileId, callback) {
if (typeof userID == 'string') {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'POST',
url: `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`,
form: {
count: 10,
sessionid: this.getSessionID()
},
source: 'steamcommunity'
});
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`,
"form": {
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) { // eslint-disable-line
if (!callback) {
return;
}
resolve();
});
callback(err);
}, "steamcommunity");
};
/**
* Unfavorites a sharedfile
* @param {String} sharedFileId - ID of the sharedfile
* @param {String} appid - ID of the app associated to this sharedfile
* @param {function} [callback] - Takes only an Error object/null as the first argument
* @return Promise<void>
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'POST',
url: 'https://steamcommunity.com/sharedfiles/unfavorite',
form: {
id: sharedFileId,
appid,
sessionid: this.getSessionID()
},
source: 'steamcommunity'
});
this.httpRequestPost({
"uri": "https://steamcommunity.com/sharedfiles/unfavorite",
"form": {
"id": sharedFileId,
"appid": appid,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
resolve();
});
callback(err);
}, "steamcommunity");
};
/**
@@ -144,20 +138,21 @@ SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, ca
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.unsubscribeSharedFileComments = function(userID, sharedFileId, callback) {
if (typeof userID === 'string') {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'POST',
url: `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`,
form: {
count: 10,
sessionid: this.getSessionID()
}
});
this.httpRequestPost({
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`,
"form": {
"count": 10,
"sessionid": this.getSessionID()
}
}, function(err, response, body) { // eslint-disable-line
if (!callback) {
return;
}
resolve();
});
callback(err);
}, "steamcommunity");
};

View File

@@ -1,164 +1,152 @@
const StdLib = require('@doctormckay/stdlib');
const SteamTotp = require('steam-totp');
var SteamTotp = require('steam-totp');
var SteamCommunity = require('../index.js');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
const ETwoFactorTokenType = {
var ETwoFactorTokenType = {
None: 0, // No token-based two-factor authentication
ValveMobileApp: 1, // Tokens generated using Valve's special charset (5 digits, alphanumeric)
ThirdParty: 2 // Tokens generated using literally everyone else's standard charset (6 digits, numeric). This is disabled on the backend.
ThirdParty: 2 // Tokens generated using literally everyone else's standard charset (6 digits, numeric). This is disabled.
};
/**
* @param {function} [callback]
* @return {Promise<object>}
*/
SteamCommunity.prototype.enableTwoFactor = function(callback) {
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
this._verifyMobileAccessToken();
this._verifyMobileAccessToken();
if (!this.mobileAccessToken) {
return reject(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
this.httpRequestPost({
uri: "https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/?access_token=" + this.mobileAccessToken,
// TODO: Send this as protobuf to more closely mimic official app behavior
form: {
steamid: this.steamID.getSteamID64(),
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
device_identifier: SteamTotp.getDeviceID(this.steamID),
sms_phone_id: '1',
version: 2
},
json: true
}, (err, response, body) => {
if (err) {
callback(err);
return;
}
let {jsonBody} = await this.httpRequest({
method: 'POST',
url: `https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
// TODO: Send this as protobuf to more closely mimic official app behavior
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
if (body.response.status != 1) {
var error = new Error('Error ' + body.response.status);
error.eresult = body.response.status;
callback(error);
return;
}
callback(null, body.response);
}, 'steamcommunity');
};
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
this._verifyMobileAccessToken();
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
let attemptsLeft = 30;
let diff = 0;
let finalize = () => {
let code = SteamTotp.generateAuthCode(secret, diff);
this.httpRequestPost({
uri: 'https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=' + this.mobileAccessToken,
form: {
steamid: this.steamID.getSteamID64(),
authenticator_code: code,
authenticator_time: Math.floor(Date.now() / 1000),
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
device_identifier: SteamTotp.getDeviceID(this.steamID),
sms_phone_id: '1'
activation_code: activationCode
},
source: 'steamcommunity'
});
if (!jsonBody.response) {
return reject(new Error('Malformed response'));
}
if (jsonBody.response.status != 1) {
let error = new Error(`Error ${jsonBody.response.status}`);
error.eresult = jsonBody.response.status;
return reject(error);
}
resolve(jsonBody.response);
});
};
/**
* @param {string} secret
* @param {string} activationCode
* @param {function} [callback]
* @return Promise<void>
*/
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
this._verifyMobileAccessToken();
if (!this.mobileAccessToken) {
return reject(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
}
let attemptsLeft = 30;
let diff = 0;
await new Promise((resolve, reject) => {
SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
return reject(err);
}
diff = offset;
resolve();
});
});
let finalize = async () => {
let code = SteamTotp.generateAuthCode(secret, diff);
let {jsonBody} = this.httpRequest({
method: 'POST',
url: `https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
form: {
steamid: this.steamID.getSteamID64(),
authenticator_code: code,
authenticator_time: Math.floor(Date.now() / 1000),
activation_code: activationCode
},
source: 'steamcommunity'
});
if (!jsonBody.response) {
return reject(new Error('Malformed response'));
json: true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
jsonBody = jsonBody.response;
if (jsonBody.server_time) {
diff = jsonBody.server_time - Math.floor(Date.now() / 1000);
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
if (jsonBody.status == SteamCommunity.EResult.TwoFactorActivationCodeMismatch) {
return reject(new Error('Invalid activation code'));
} else if (jsonBody.want_more) {
if (--attemptsLeft <= 0) {
// We made more than 30 attempts, something must be wrong
return reject(Helpers.eresultError(SteamCommunity.EResult.Fail));
}
body = body.response;
if (body.server_time) {
diff = body.server_time - Math.floor(Date.now() / 1000);
}
if (body.status == 89) {
callback(new Error('Invalid activation code'));
} else if(body.want_more) {
attemptsLeft--;
diff += 30;
finalize();
} else if (!jsonBody.success) {
return reject(new Error(`Error ${jsonBody.status}`));
} else if(!body.success) {
callback(new Error('Error ' + body.status));
} else {
resolve();
callback(null);
}
};
}, 'steamcommunity');
}
SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
callback(err);
return;
}
diff = offset;
finalize();
});
};
/**
* @param {string} revocationCode
* @param {function} [callback]
* @return Promise<void>
*/
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
this._verifyMobileAccessToken();
this._verifyMobileAccessToken();
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
this.httpRequestPost({
uri: 'https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/?access_token=' + this.mobileAccessToken,
form: {
steamid: this.steamID.getSteamID64(),
revocation_code: revocationCode,
steamguard_scheme: 1
},
json: true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
let {jsonBody} = await this.httpRequest({
method: 'POST',
url: `https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/?access_token=${this.mobileAccessToken}`,
form: {
steamid: this.steamID.getSteamID64(),
revocation_code: revocationCode,
steamguard_scheme: 1
},
source: 'steamcommunity'
});
if (!jsonBody.response) {
return reject(new Error('Malformed response'));
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
if (!jsonBody.response.success) {
return reject(new Error('Request failed'));
if (!body.response.success) {
callback(new Error('Request failed'));
return;
}
// success = true means it worked
resolve();
});
callback(null);
}, 'steamcommunity');
};

View File

@@ -9,111 +9,116 @@ const CEconItem = require('../classes/CEconItem.js');
const Helpers = require('./helpers.js');
SteamCommunity.prototype.addFriend = function(userID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
url: 'https://steamcommunity.com/actions/AddFriendAjax',
form: {
accept_invite: 0,
sessionID: this.getSessionID(),
steamid: userID.toString()
"uri": "https://steamcommunity.com/actions/AddFriendAjax",
"form": {
"accept_invite": 0,
"sessionID": this.getSessionID(),
"steamid": userID.toString()
},
json: true
}, (err, response, body) => {
if (!callback) {
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
if (err) {
return callback(err);
callback(err);
return;
}
if (body.success) {
if(body.success) {
callback(null);
} else {
callback(new Error('Unknown error'));
callback(new Error("Unknown error"));
}
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.acceptFriendRequest = function(userID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
url: 'https://steamcommunity.com/actions/AddFriendAjax',
form: {
accept_invite: 1,
sessionID: this.getSessionID(),
steamid: userID.toString()
"uri": "https://steamcommunity.com/actions/AddFriendAjax",
"form": {
"accept_invite": 1,
"sessionID": this.getSessionID(),
"steamid": userID.toString()
}
}, (err, response, body) => {
if (!callback) {
}, function(err, response, body) {
if(!callback) {
return;
}
callback(err || null);
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.removeFriend = function(userID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
url: 'https://steamcommunity.com/actions/RemoveFriendAjax',
form: {
sessionID: this.getSessionID(),
steamid: userID.toString()
"uri": "https://steamcommunity.com/actions/RemoveFriendAjax",
"form": {
"sessionID": this.getSessionID(),
"steamid": userID.toString()
}
}, (err, response, body) => {
if (!callback) {
}, function(err, response, body) {
if(!callback) {
return;
}
callback(err || null);
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.blockCommunication = function(userID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
url: 'https://steamcommunity.com/actions/BlockUserAjax',
form: {
sessionID: this.getSessionID(),
steamid: userID.toString()
"uri": "https://steamcommunity.com/actions/BlockUserAjax",
"form": {
"sessionID": this.getSessionID(),
"steamid": userID.toString()
}
}, (err, response, body) => {
if (!callback) {
}, function(err, response, body) {
if(!callback) {
return;
}
callback(err || null);
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.unblockCommunication = function(userID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
let form = {action: 'unignore'};
var form = {"action": "unignore"};
form['friends[' + userID.toString() + ']'] = 1;
this._myProfile('friends/blocked/', form, (err, response, body) => {
if (!callback) {
this._myProfile('friends/blocked/', form, function(err, response, body) {
if(!callback) {
return;
}
if (err || response.statusCode >= 400) {
callback(err || new Error(`HTTP error ${response.statusCode}`));
if(err || response.statusCode >= 400) {
callback(err || new Error("HTTP error " + response.statusCode));
return;
}
@@ -122,20 +127,21 @@ SteamCommunity.prototype.unblockCommunication = function(userID, callback) {
};
SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
url: `https://steamcommunity.com/comment/Profile/post/${userID.toString()}/-1`,
form: {
comment: message,
count: 1,
sessionid: this.getSessionID()
"uri": "https://steamcommunity.com/comment/Profile/post/" + userID.toString() + "/-1",
"form": {
"comment": message,
"count": 1,
"sessionid": this.getSessionID()
},
json: true
}, (err, response, body) => {
if (!callback) {
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
@@ -144,36 +150,37 @@ SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
return;
}
if (body.success) {
if(body.success) {
const $ = Cheerio.load(body.comments_html);
const commentID = $('.commentthread_comment').attr('id').split('_')[1];
callback(null, commentID);
} else if (body.error) {
} else if(body.error) {
callback(new Error(body.error));
} else {
callback(new Error('Unknown error'));
callback(new Error("Unknown error"));
}
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.deleteUserComment = function(userID, commentID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
url: `https://steamcommunity.com/comment/Profile/delete/${userID.toString()}/-1`,
form: {
gidcomment: commentID,
start: 0,
count: 1,
sessionid: this.getSessionID(),
feature2: -1
"uri": "https://steamcommunity.com/comment/Profile/delete/" + userID.toString() + "/-1",
"form": {
"gidcomment": commentID,
"start": 0,
"count": 1,
"sessionid": this.getSessionID(),
"feature2": -1
},
json: true
}, (err, response, body) => {
if (!callback) {
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
@@ -182,20 +189,20 @@ SteamCommunity.prototype.deleteUserComment = function(userID, commentID, callbac
return;
}
if (body.success && !body.comments_html.includes(commentID)) {
if(body.success && !body.comments_html.includes(commentID)) {
callback(null);
} else if (body.error) {
} else if(body.error) {
callback(new Error(body.error));
} else if (body.comments_html.includes(commentID)) {
callback(new Error('Failed to delete comment'));
} else if(body.comments_html.includes(commentID)) {
callback(new Error("Failed to delete comment"));
} else {
callback(new Error('Unknown error'));
callback(new Error("Unknown error"));
}
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.getUserComments = function(userID, options, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
@@ -204,19 +211,19 @@ SteamCommunity.prototype.getUserComments = function(userID, options, callback) {
options = {};
}
let form = Object.assign({
start: 0,
count: 0,
feature2: -1,
sessionid: this.getSessionID()
var form = Object.assign({
"start": 0,
"count": 0,
"feature2": -1,
"sessionid": this.getSessionID()
}, options);
this.httpRequestPost({
url: `https://steamcommunity.com/comment/Profile/render/${userID.toString()}/-1`,
form,
json: true
}, (err, response, body) => {
if (!callback) {
"uri": "https://steamcommunity.com/comment/Profile/render/" + userID.toString() + "/-1",
"form": form,
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
@@ -225,50 +232,82 @@ SteamCommunity.prototype.getUserComments = function(userID, options, callback) {
return;
}
if (body.success) {
if(body.success) {
const $ = Cheerio.load(body.comments_html);
const comments = $('.commentthread_comment.responsive_body_text[id]').map((i, elem) => {
let $elem = $(elem),
$commentContent = $elem.find('.commentthread_comment_text');
const comments = $(".commentthread_comment.responsive_body_text[id]").map((i, elem) => {
var $elem = $(elem),
$commentContent = $elem.find(".commentthread_comment_text");
return {
id: $elem.attr('id').split('_')[1],
id: $elem.attr("id").split("_")[1],
author: {
steamID: new SteamID('[U:1:' + $elem.find('[data-miniprofile]').data('miniprofile') + ']'),
name: $elem.find('bdi').text(),
avatar: $elem.find('.playerAvatar img[src]').attr('src'),
state: $elem.find('.playerAvatar').attr('class').split(' ').pop()
steamID: new SteamID("[U:1:" + $elem.find("[data-miniprofile]").data("miniprofile") + "]"),
name: $elem.find("bdi").text(),
avatar: $elem.find(".playerAvatar img[src]").attr("src"),
state: $elem.find(".playerAvatar").attr("class").split(" ").pop()
},
date: new Date($elem.find('.commentthread_comment_timestamp').data('timestamp') * 1000),
date: new Date($elem.find(".commentthread_comment_timestamp").data("timestamp") * 1000),
text: $commentContent.text().trim(),
html: $commentContent.html().trim()
};
}
}).get();
callback(null, comments, body.total_count);
} else if (body.error) {
} else if(body.error) {
callback(new Error(body.error));
} else {
callback(new Error('Unknown error'));
callback(new Error("Unknown error"));
}
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback) {
if (typeof userID === 'string') {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
"uri": "https://steamcommunity.com/actions/GroupInvite",
"form": {
"group": groupID.toString(),
"invitee": userID.toString(),
"json": 1,
"sessionID": this.getSessionID(),
"type": "groupInvite"
},
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
if (err) {
callback(err);
return;
}
if(body.results == 'OK') {
callback(null);
} else if(body.results) {
callback(new Error(body.results));
} else {
callback(new Error("Unknown error"));
}
}, "steamcommunity");
};
SteamCommunity.prototype.followUser = function(userID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
this.httpRequestPost({
url: 'https://steamcommunity.com/actions/GroupInvite',
form: {
group: groupID.toString(),
invitee: userID.toString(),
json: 1,
sessionID: this.getSessionID(),
type: 'groupInvite'
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/followuser/`,
"form": {
"sessionid": this.getSessionID(),
},
json: true
}, (err, response, body) => {
"json": true
}, function(err, response, body) {
if (!callback) {
return;
}
@@ -278,14 +317,43 @@ SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback)
return;
}
if (body.results == 'OK') {
callback(null);
} else if (body.results) {
callback(new Error(body.results));
} else {
callback(new Error('Unknown error'));
if (body.success && body.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success));
return;
}
}, 'steamcommunity');
callback(null);
}, "steamcommunity");
};
SteamCommunity.prototype.unfollowUser = function(userID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/unfollowuser/`,
"form": {
"sessionid": this.getSessionID(),
},
"json": true
}, function(err, response, body) {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success));
return;
}
callback(null);
}, "steamcommunity");
};
SteamCommunity.prototype.getUserAliases = function(userID, callback) {
@@ -294,24 +362,24 @@ SteamCommunity.prototype.getUserAliases = function(userID, callback) {
}
this.httpRequestGet({
url: `https://steamcommunity.com/profiles/${userID.getSteamID64()}/ajaxaliases`,
json: true
}, (err, response, body) => {
"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'));
callback(new Error("Malformed response"));
return;
}
callback(null, body.map((entry) => {
callback(null, body.map(function(entry) {
entry.timechanged = Helpers.decodeSteamTime(entry.timechanged);
return entry;
}));
}, 'steamcommunity');
}, "steamcommunity");
};
/**
@@ -324,33 +392,33 @@ SteamCommunity.prototype.getUserProfileBackground = function(userID, callback) {
userID = new SteamID(userID);
}
this.httpRequest(`https://steamcommunity.com/profiles/${userID.getSteamID64()}`, (err, response, body) => {
this.httpRequest("https://steamcommunity.com/profiles/" + userID.getSteamID64(), (err, response, body) => {
if (err) {
callback(err);
return;
}
let $ = Cheerio.load(body);
var $ = Cheerio.load(body);
let $privateProfileInfo = $('.profile_private_info');
var $privateProfileInfo = $('.profile_private_info');
if ($privateProfileInfo.length > 0) {
callback(new Error($privateProfileInfo.text().trim()));
return;
}
if ($('body').hasClass('has_profile_background')) {
let backgroundUrl = $('div.profile_background_image_content').css('background-image');
let matcher = backgroundUrl.match(/\(([^)]+)\)/);
var backgroundUrl = $('div.profile_background_image_content').css('background-image');
var matcher = backgroundUrl.match(/\(([^)]+)\)/);
if (matcher.length != 2 || !matcher[1].length) {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
} else {
callback(null, matcher[1]);
}
} else {
callback(null, null);
}
}, 'steamcommunity');
}, "steamcommunity");
};
SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
@@ -364,27 +432,28 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
}
if (!userID) {
callback(new Error('No SteamID specified and not logged in'));
callback(new Error("No SteamID specified and not logged in"));
return;
}
this.httpRequest(`https://steamcommunity.com/profiles/${userID.getSteamID64()}/inventory/`, (err, response, body) => {
var self = this;
this.httpRequest("https://steamcommunity.com/profiles/" + userID.getSteamID64() + "/inventory/", function(err, response, body) {
if (err) {
callback(err);
return;
}
let match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
if (!match) {
callback(new Error('Malformed response'));
return;
}
let data;
var data;
try {
data = JSON.parse(match[1]);
} catch (e) {
callback(new Error('Malformed response'));
} catch(e) {
callback(new Error("Malformed response"));
return;
}
@@ -404,7 +473,7 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
}
callback(null, data);
}, 'steamcommunity');
}, "steamcommunity");
};
/**
@@ -417,22 +486,27 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
* @param {function} callback
*/
SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, tradableOnly, callback) {
var self = this;
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
const get = (inventory, currency, start) => {
this.httpRequest({
url: `https://steamcommunity.com${endpoint}/inventory/json/${appID}/${contextID}`,
headers: {
Referer: `https://steamcommunity.com${endpoint}/inventory`
var endpoint = "/profiles/" + userID.getSteamID64();
get([], []);
function get(inventory, currency, start) {
self.httpRequest({
"uri": "https://steamcommunity.com" + endpoint + "/inventory/json/" + appID + "/" + contextID,
"headers": {
"Referer": "https://steamcommunity.com" + endpoint + "/inventory"
},
qs: {
start: start,
trading: tradableOnly ? 1 : undefined
"qs": {
"start": start,
"trading": tradableOnly ? 1 : undefined
},
json: true
}, (err, response, body) => {
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
@@ -440,37 +514,43 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
if (!body || !body.success || !body.rgInventory || !body.rgDescriptions || !body.rgCurrency) {
if (body) {
callback(new Error(body.Error || 'Malformed response'));
callback(new Error(body.Error || "Malformed response"));
} else {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
}
return;
}
for (let i in body.rgInventory) {
var i;
for (i in body.rgInventory) {
if (!body.rgInventory.hasOwnProperty(i)) {
continue;
}
inventory.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
}
for (let i in body.rgCurrency) {
for (i in body.rgCurrency) {
if (!body.rgCurrency.hasOwnProperty(i)) {
continue;
}
currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
}
if (body.more) {
let match = response.request.uri.href.match(/\/(profiles|id)\/([^/]+)\//);
if (match) {
endpoint = `/${match[1]}/${match[2]}`;
var match = response.request.uri.href.match(/\/(profiles|id)\/([^\/]+)\//);
if(match) {
endpoint = "/" + match[1] + "/" + match[2];
}
get(inventory, currency, body.more_start);
} else {
callback(null, inventory, currency);
}
}, 'steamcommunity');
};
let endpoint = `/profiles/${userID.getSteamID64()}`;
get([], []);
}, "steamcommunity");
}
};
/**
@@ -485,64 +565,52 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, contextID, tradableOnly, language, callback) {
if (typeof language === 'function') {
callback = language;
language = 'english';
language = "english";
}
if (!userID) {
callback(new Error('The user\'s SteamID is invalid or missing.'));
callback(new Error("The user's SteamID is invalid or missing."));
return;
}
var self = this;
if (typeof userID === 'string') {
userID = new SteamID(userID);
}
// A bit of optimization; objects are hash tables so it's more efficient to look up by key than to iterate an array
let quickDescriptionLookup = {};
var pos = 1;
get([], []);
const getDescription = (descriptions, classID, instanceID) => {
let key = classID + '_' + (instanceID || '0'); // instanceID can be undefined, in which case it's 0.
if (quickDescriptionLookup[key]) {
return quickDescriptionLookup[key];
}
for (let i = 0; i < descriptions.length; i++) {
quickDescriptionLookup[descriptions[i].classid + '_' + (descriptions[i].instanceid || '0')] = descriptions[i];
}
return quickDescriptionLookup[key];
};
const get = (inventory, currency, start) => {
this.httpRequest({
url: `https://steamcommunity.com/inventory/${userID.getSteamID64()}/${appID}/${contextID}`,
headers: {
Referer: `https://steamcommunity.com/profiles/${userID.getSteamID64()}/inventory`
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: 2000, // Max items per 'page'
start_assetid: start
"qs": {
"l": language, // Default language
"count": 1000, // Max items per 'page'
"start_assetid": start
},
json: true
}, (err, response, body) => {
"json": true
}, function(err, response, body) {
if (err) {
if (err.message == 'HTTP error 403' && body === null) {
if (err.message == "HTTP error 403" && body === null) {
// 403 with a body of "null" means the inventory/profile is private.
if (this.steamID && userID.getSteamID64() == this.steamID.getSteamID64()) {
if (self.steamID && userID.getSteamID64() == self.steamID.getSteamID64()) {
// We can never get private profile error for our own inventory!
this._notifySessionExpired(err);
self._notifySessionExpired(err);
}
callback(new Error('This profile is private.'));
callback(new Error("This profile is private."));
return;
}
if (err.message == 'HTTP error 500' && body && body.error) {
if (err.message == "HTTP error 500" && body && body.error) {
err = new Error(body.error);
let match = body.error.match(/^(.+) \((\d+)\)$/);
var match = body.error.match(/^(.+) \((\d+)\)$/);
if (match) {
err.message = match[1];
err.eresult = match[2];
@@ -561,19 +629,26 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
return;
}
if (appID == 730 && body && body.success && !body.assets) {
// CS inventory has no visible items. We need a special case for this because Valve is incapable of
// doing anything not dumb.
callback(null, [], [], body.total_inventory_count);
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'));
callback(new Error(body.error || body.Error || "Malformed response"));
} else {
callback(new Error('Malformed response'));
callback(new Error("Malformed response"));
}
return;
}
for (let i = 0; i < body.assets.length; i++) {
let description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid);
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++;
@@ -586,11 +661,25 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
} else {
callback(null, inventory, currency, body.total_inventory_count);
}
}, 'steamcommunity');
};
}, "steamcommunity");
}
let pos = 1;
get([], []);
// 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];
}
};
/**
@@ -622,7 +711,7 @@ SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer,
return;
}
let imageDetails = null;
var imageDetails = null;
try {
imageDetails = imageSize(imageContentsBuffer);
} catch (ex) {
@@ -630,14 +719,14 @@ SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer,
return;
}
let imageHash = Crypto.createHash('sha1');
var imageHash = Crypto.createHash('sha1');
imageHash.update(imageContentsBuffer);
imageHash = imageHash.digest('hex');
let filename = Date.now() + '_image.' + imageDetails.type;
var filename = Date.now() + '_image.' + imageDetails.type;
this.httpRequestPost({
url: 'https://steamcommunity.com/chat/beginfileupload/?l=english',
uri: 'https://steamcommunity.com/chat/beginfileupload/?l=english',
headers: {
referer: 'https://steamcommunity.com/chat/'
},
@@ -655,7 +744,7 @@ SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer,
}, (err, res, body) => {
if (err) {
if (body && body.success) {
let err2 = Helpers.eresultError(body.success);
var err2 = Helpers.eresultError(body.success);
if (body.message) {
err2.message = body.message;
}
@@ -671,9 +760,9 @@ SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer,
return;
}
let hmac = body.hmac;
let timestamp = body.timestamp;
let startResult = body.result;
var hmac = body.hmac;
var timestamp = body.timestamp;
var startResult = body.result;
if (!startResult || !startResult.ugcid || !startResult.url_host || !startResult.request_headers) {
callback(new Error('Malformed response'));
@@ -681,14 +770,14 @@ SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer,
}
// Okay, now we need to PUT the file to the provided URL
let uploadUrl = (startResult.use_https ? 'https' : 'http') + '://' + startResult.url_host + startResult.url_path;
let headers = {};
var uploadUrl = (startResult.use_https ? 'https' : 'http') + '://' + startResult.url_host + startResult.url_path;
var headers = {};
startResult.request_headers.forEach((header) => {
headers[header.name.toLowerCase()] = header.value;
});
this.httpRequest({
url: uploadUrl,
uri: uploadUrl,
method: 'PUT',
headers,
body: imageContentsBuffer
@@ -700,7 +789,7 @@ SteamCommunity.prototype.sendImageToUser = function(userID, imageContentsBuffer,
// Now we need to commit the upload
this.httpRequestPost({
url: 'https://steamcommunity.com/chat/commitfileupload/',
uri: 'https://steamcommunity.com/chat/commitfileupload/',
headers: {
referer: 'https://steamcommunity.com/chat/'
},

View File

@@ -1,54 +1,166 @@
const StdLib = require('@doctormckay/stdlib');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
/**
* @param {string} domain
* @param {function} [callback]
* @return Promise<{key: string}>
* Retrieves your account's Steam Web API key, if you already have one. If you don't yet have one, this will fail.
* To create a Web API key, use `createWebApiKey()`.
*
* @param {null|function} unused - No longer used, kept for backward compatibility. You can omit this parameter and pass
* your callback directly as the first parameter if you want.
* @param {function} callback
*/
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
return StdLib.Promises.callbackPromise(['key'], callback, false, async (resolve, reject) => {
let {textBody} = await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/dev/apikey?l=english',
followRedirect: false,
source: 'steamcommunity'
});
SteamCommunity.prototype.getWebApiKey = function(unused, callback) {
if (typeof unused == 'function') {
callback = unused;
}
if (textBody.includes('<h2>Access Denied</h2>')) {
return reject(new Error('Access Denied'));
this.httpRequest({
uri: 'https://steamcommunity.com/dev/apikey?l=english',
followRedirect: false
}, (err, response, body) => {
if (err) {
callback(err);
return;
}
if (textBody.includes('You must have a validated email address to create a Steam Web API key.')) {
return reject(new Error('You must have a validated email address to create a Steam Web API key.'));
if (body.match(/You must have a validated email address to create a Steam Web API key./)) {
return callback(new Error('You must have a validated email address to create a Steam Web API key.'));
}
let match = textBody.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if (body.match(/Your account requires (<a [^>]+>)?Steam Guard Mobile Authenticator/)) {
return callback(new Error('Steam Guard Mobile Authenticator required to create a Steam Web API key'));
}
if (body.match(/<h2>Access Denied<\/h2>/)) {
return callback(new Error('Access Denied'));
}
let match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if (match) {
// We already have an API key registered
return resolve({key: match[1]});
callback(null, match[1]);
} else {
callback(new Error('No API key created for this account'));
}
}, "steamcommunity");
};
/**
* @typedef CreateApiKeyOptions
* @property {string} domain - The domain to associate with your API key
* @property {string} [requestID] - If finalizing an existing create request, include the request ID
* @property {string|Buffer} [identitySecret] - If you pass your identity_secret here, then steamcommunity will
* internally handle accepting any confirmations.
*/
/**
* @typedef CreateApiKeyResponse
* @property {boolean} confirmationRequired
* @property {string} [apiKey] - If creating your API key succeeded, this is the new key
* @property {CreateApiKeyOptions} [finalizeOptions] - If confirmation is required to create a key, then accept the
* confirmation, then call createWebApiKey again and pass this whole object for the `options` parameter.
*/
/**
* @callback createWebApiKeyCallback
* @param {Error|null} err
* @param {CreateApiKeyResponse} [result]
*/
/**
* Starts the process to create a Steam Web API key. When the callback is fired, you will need to approve a mobile
* confirmation in your app or using getConfirmations().
*
* @param {CreateApiKeyOptions} options
* @param {createWebApiKeyCallback} callback
*/
SteamCommunity.prototype.createWebApiKey = function(options, callback) {
if (!options.domain) {
callback(new Error('Passing a domain is required to register an API key'));
return;
}
this.httpRequestPost({
uri: 'https://steamcommunity.com/dev/requestkey',
form: {
domain: options.domain,
request_id: options.requestID || '0',
sessionid: this.getSessionID(),
agreeToTerms: 'true'
},
json: true
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
// We need to register a new API key
await this.httpRequest({
method: 'POST',
url: 'https://steamcommunity.com/dev/registerkey?l=english',
form: {
domain,
agreeToTerms: 'agreed',
sessionid: this.getSessionID(),
Submit: 'Register'
},
source: 'steamcommunity'
});
// body.requires_confirmation is 1/0, but the Steam website doesn't check this value and instead only checks the
// value of `success`. So let's just do that.
resolve({key: await this.getWebApiKey(domain)});
// This is a mess. I'm glad we have promises and await now.
switch (body.success) {
case SteamCommunity.EResult.OK:
if (body.api_key) {
callback(null, {confirmationRequired: false, apiKey: body.api_key});
return;
}
// It's not been observed that we get result OK without api_key included, but the Steam website doesn't
// use this value so let's be safe just in case it disappears in the future.
this.getWebApiKey((err, key) => {
if (err) {
callback(err);
return;
}
callback(null, {confirmationRequired: false, apiKey: key});
});
return;
case SteamCommunity.EResult.Pending:
let finalizeOptions = {
domain: options.domain,
requestID: body.request_id || options.requestID
}
if (options.identitySecret) {
this.acceptConfirmationForObject(options.identitySecret, finalizeOptions.requestID, (err) => {
if (err) {
callback(err);
} else {
this.createWebApiKey(finalizeOptions, callback);
}
});
return;
}
callback(null, {
confirmationRequired: true,
finalizeOptions: finalizeOptions
});
return;
default:
callback(Helpers.eresultError(body.success));
}
});
};
/**
* @deprecated No longer works. Will be removed in a future release.
* @param {function} callback
*/
SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
if (this.oAuthToken) {
return callback(null, this.oAuthToken);
}
callback(new Error('This operation requires an OAuth token, which is no longer issued by Steam.'));
};
/**
* Sets an access_token generated by steam-session using EAuthTokenPlatformType.MobileApp.
* Required for some operations such as 2FA enabling and disabling.

1
examples/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
twofactor_*.json

View File

@@ -1,7 +1,7 @@
// If you aren't running this script inside of the repository, replace the following line with:
// const SteamCommunity = require('steamcommunity');
const SteamCommunity = require('../index.js');
const SteamSession = require('steam-session');
const SteamTotp = require('steam-totp');
const ReadLine = require('readline');
let g_AbortPromptFunc = null;
@@ -13,69 +13,32 @@ async function main() {
let accountName = await promptAsync('Username: ');
let password = await promptAsync('Password (hidden): ', true);
// Create a LoginSession for us to use to attempt to log into steam
let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp);
attemptLogin(accountName, password);
}
// Go ahead and attach our event handlers before we do anything else.
session.on('authenticated', async () => {
abortPrompt();
function attemptLogin(accountName, password, twoFactorCode) {
community.login({
accountName,
password,
twoFactorCode,
disableMobile: false
}, async (err) => {
if (err && err.message == 'SteamGuardMobile') {
let code = await promptAsync('Steam Guard App Code OR Shared Secret: ');
if (code.length > 5) {
// If we were provided a shared secret, turn it into a code.
code = SteamTotp.getAuthCode(code);
}
attemptLogin(accountName, password, code);
return;
}
let accessToken = session.accessToken;
let cookies = await session.getWebCookies();
community.setCookies(cookies);
community.setMobileAppAccessToken(accessToken);
// Enabling or disabling 2FA is presently the only action in node-steamcommunity which requires an access token.
// In all other cases, using `community.setCookies(cookies)` is all you need to do in order to be logged in,
// although there's never any harm in setting a mobile app access token.
if (err) {
throw err;
}
doRevoke();
});
session.on('timeout', () => {
abortPrompt();
console.log('This login attempt has timed out.');
});
session.on('error', (err) => {
abortPrompt();
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
// polling, e.g. the network connection goes down or Steam chokes on something.
console.log(`ERROR: This login attempt has failed! ${err.message}`);
});
// Start our login attempt
let startResult = await session.startWithCredentials({accountName, password});
if (startResult.actionRequired) {
// Some Steam Guard action is required. We only care about email and device codes; in theory an
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
// could accept an email or device confirmation.
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
if (codeAction) {
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
// We wouldn't expect this to happen since we're trying to disable 2FA, but just in case...
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
} else {
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
}
let code = await promptAsync('Code: ');
if (code) {
await session.submitSteamGuardCode(code);
}
// If we fall through here without submitting a Steam Guard code, that means one of two things:
// 1. The user pressed enter without providing a code, in which case the script will simply exit
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
}
}
}
async function doRevoke() {

View File

@@ -1,47 +1,47 @@
let SteamCommunity = require('../index.js');
let ReadLine = require('readline');
var SteamCommunity = require('../index.js');
var ReadLine = require('readline');
let community = new SteamCommunity();
let rl = ReadLine.createInterface({
input: process.stdin,
output: process.stdout
var community = new SteamCommunity();
var rl = ReadLine.createInterface({
"input": process.stdin,
"output": process.stdout
});
rl.question('Username: ', function(accountName) {
rl.question('Password: ', function(password) {
rl.question("Username: ", function(accountName) {
rl.question("Password: ", function(password) {
doLogin(accountName, password);
});
});
function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
community.login({
accountName: accountName,
password: password,
authCode: authCode,
twoFactorCode: twoFactorCode,
captcha: captcha
"accountName": accountName,
"password": password,
"authCode": authCode,
"twoFactorCode": twoFactorCode,
"captcha": captcha
}, function(err, sessionID, cookies, steamguard) {
if (err) {
if (err.message == 'SteamGuardMobile') {
rl.question('Steam Authenticator Code: ', function(code) {
if(err) {
if(err.message == 'SteamGuardMobile') {
rl.question("Steam Authenticator Code: ", function(code) {
doLogin(accountName, password, null, code);
});
return;
}
if (err.message == 'SteamGuard') {
console.log('An email has been sent to your address at ' + err.emaildomain);
rl.question('Steam Guard Code: ', function(code) {
if(err.message == 'SteamGuard') {
console.log("An email has been sent to your address at " + err.emaildomain);
rl.question("Steam Guard Code: ", function(code) {
doLogin(accountName, password, code);
});
return;
}
if (err.message == 'CAPTCHA') {
if(err.message == 'CAPTCHA') {
console.log(err.captchaurl);
rl.question('CAPTCHA: ', function(captchaInput) {
rl.question("CAPTCHA: ", function(captchaInput) {
doLogin(accountName, password, authCode, twoFactorCode, captchaInput);
});
@@ -53,9 +53,9 @@ function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
return;
}
console.log('Logged on!');
console.log("Logged on!");
rl.question('Group ID: ', function(gid) {
rl.question("Group ID: ", function(gid) {
community.getSteamGroup(gid, function(err, group) {
if (err) {
console.log(err);
@@ -64,19 +64,19 @@ function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
group.getAllAnnouncements(function(err, announcements) {
if (announcements.length === 0) {
return console.log('This group has no announcements');
if(announcements.length === 0) {
return console.log("This group has no announcements");
}
for (let i = announcements.length - 1; i >= 0; i--) {
console.log('[%s] %s %s: %s', announcements[i].date, announcements[i].aid, announcements[i].author, announcements[i].content);
}
for (var i = announcements.length - 1; i >= 0; i--) {
console.log("[%s] %s %s: %s", announcements[i].date, announcements[i].aid, announcements[i].author, announcements[i].content);
};
rl.question('Would you like to delete delete or edit an annoucement? (Type edit/delete): ', function(choice) {
rl.question('Annoucement ID: ', function(aid) {
if (choice === 'edit') {
rl.question('New title: ', function(header) {
rl.question('New body: ', function(content) {
rl.question("Would you like to delete delete or edit an annoucement? (Type edit/delete): ", function(choice) {
rl.question("Annoucement ID: ", function(aid) {
if(choice === 'edit') {
rl.question("New title: ", function(header) {
rl.question("New body: ", function(content) {
// EW THE PYRAMID!
// Try replace this with delete!
editAnnouncement(group, aid, header, content);
@@ -96,10 +96,10 @@ function doLogin(accountName, password, authCode, twoFactorCode, captcha) {
function editAnnouncement(group, aid, header, content) {
// Actual community method.
group.editAnnouncement(aid, header, content, function(error) {
if (!error) {
console.log('Annoucement edited!');
if(!error) {
console.log("Annoucement edited!");
} else {
console.log('Unable to edit annoucement! %j', error);
console.log("Unable to edit annoucement! %j", error);
process.exit(1);
}
});
@@ -109,10 +109,10 @@ function deleteAnnouncement(group, aid) {
// group.deleteAnnouncement(aid);
// Or
group.deleteAnnouncement(aid, function(err) {
if (!err) {
console.log('Deleted');
if(!err) {
console.log("Deleted");
} else {
console.log('Error deleting announcement.');
console.log("Error deleting announcement.");
}
});
})
}

View File

@@ -1,7 +1,6 @@
// If you aren't running this script inside of the repository, replace the following line with:
// const SteamCommunity = require('steamcommunity');
const SteamCommunity = require('../index.js');
const SteamSession = require('steam-session');
const ReadLine = require('readline');
const FS = require('fs');
@@ -16,69 +15,28 @@ async function main() {
let accountName = await promptAsync('Username: ');
let password = await promptAsync('Password (hidden): ', true);
// Create a LoginSession for us to use to attempt to log into steam
let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp);
attemptLogin(accountName, password);
}
// Go ahead and attach our event handlers before we do anything else.
session.on('authenticated', async () => {
abortPrompt();
function attemptLogin(accountName, password, authCode) {
community.login({
accountName,
password,
authCode,
disableMobile: false
}, async (err) => {
if (err && err.message == 'SteamGuard') {
let code = await promptAsync('Steam Guard Email Code: ');
attemptLogin(accountName, password, code);
return;
}
let accessToken = session.accessToken;
let cookies = await session.getWebCookies();
community.setCookies(cookies);
community.setMobileAppAccessToken(accessToken);
// Enabling or disabling 2FA is presently the only action in node-steamcommunity which requires an access token.
// In all other cases, using `community.setCookies(cookies)` is all you need to do in order to be logged in,
// although there's never any harm in setting a mobile app access token.
if (err) {
throw err;
}
doSetup();
});
session.on('timeout', () => {
abortPrompt();
console.log('This login attempt has timed out.');
});
session.on('error', (err) => {
abortPrompt();
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
// polling, e.g. the network connection goes down or Steam chokes on something.
console.log(`ERROR: This login attempt has failed! ${err.message}`);
});
// Start our login attempt
let startResult = await session.startWithCredentials({accountName, password});
if (startResult.actionRequired) {
// Some Steam Guard action is required. We only care about email and device codes; in theory an
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
// could accept an email or device confirmation.
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
if (codeAction) {
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
} else {
// We wouldn't expect this to happen since we're trying to enable 2FA, but just in case...
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
}
let code = await promptAsync('Code: ');
if (code) {
await session.submitSteamGuardCode(code);
}
// If we fall through here without submitting a Steam Guard code, that means one of two things:
// 1. The user pressed enter without providing a code, in which case the script will simply exit
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
}
}
}
function doSetup() {
@@ -118,10 +76,13 @@ function doSetup() {
async function promptActivationCode(response) {
if (response.phone_number_hint) {
console.log(`A code has been sent to your phone ending in ${response.phone_number_hint}.`);
console.log(`An activation code has been sent to your phone ending in ${response.phone_number_hint}.`);
} else if (response.confirm_type == 3) {
// Exact meaning of confirm_type is unknown, but 3 appears to be email code
console.log('An activation code has been sent to your email.');
}
let smsCode = await promptAsync('SMS Code: ');
let smsCode = await promptAsync('Activation Code: ');
community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => {
if (err) {
if (err.message == 'Invalid activation code') {

661
index.js
View File

@@ -1,214 +1,176 @@
const {EventEmitter} = require('events');
const StdLib = require('@doctormckay/stdlib');
const {chrome} = require('@doctormckay/user-agents');
const Request = require('request');
const SteamID = require('steamid');
const {LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType} = require('steam-session');
const Util = require('util');
const xml2js = require('xml2js');
const Helpers = require('./components/helpers.js');
const Package = require('./package.json');
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36';
Util.inherits(SteamCommunity, EventEmitter);
require('util').inherits(SteamCommunity, require('events').EventEmitter);
module.exports = SteamCommunity;
SteamCommunity.SteamID = SteamID;
SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
SteamCommunity.ConfirmationType = require('./resources/EConfirmationType.js');
SteamCommunity.EResult = require('./resources/EResult.js');
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
/**
*
* @param {object} [options]
* @param {number} [options.timeout=50000] - The time in milliseconds that SteamCommunity will wait for HTTP requests to complete.
* @param {string} [options.localAddress] - The local IP address that SteamCommunity will use for its HTTP requests.
* @param {string} [options.httpProxy] - A string containing the URI of an HTTP proxy to use for all requests, e.g. `http://user:pass@1.2.3.4:8888`
* @param {object} [options.defaultHttpHeaders] - An object containing some headers to send for every HTTP request
* @constructor
*/
function SteamCommunity(options) {
options = options || {};
this.packageName = Package.name;
this.packageVersion = Package.version;
this._jar = new StdLib.HTTP.CookieJar();
this._jar = Request.jar();
this._captchaGid = -1;
this._httpRequestID = 0;
this.chatState = SteamCommunity.ChatState.Offline;
let defaultHeaders = {
'user-agent': USER_AGENT
var defaults = {
"jar": this._jar,
"timeout": options.timeout || 50000,
"gzip": true,
"headers": {
"User-Agent": options.userAgent || chrome()
}
};
// Apply the user's custom default headers
for (let i in (options.defaultHttpHeaders || {})) {
// Make sure all header names are lower case to avoid conflicts
defaultHeaders[i.toLowerCase()] = options.defaultHttpHeaders[i];
if (typeof options == "string") {
options = {
localAddress: options
};
}
this._httpClient = new StdLib.HTTP.HttpClient({
httpAgent: options.httpProxy ? StdLib.HTTP.getProxyAgent(false, options.httpProxy) : null,
httpsAgent: options.httpProxy ? StdLib.HTTP.getProxyAgent(true, options.httpProxy) : null,
localAddress: options.localAddress,
defaultHeaders,
defaultTimeout: options.timeout || 50000,
cookieJar: this._jar,
gzip: true
});
this._options = options;
// English
this._setCookie('Steam_Language=english');
// UTC
this._setCookie('timezoneOffset=0,0');
}
/**
* @param {object} details
* @param {string} details.accountName
* @param {string} details.password
* @param {string} [details.authCode]
* @param {string} [details.twoFactorCode]
* @param {string} [details.authTokenPlatformType] - A value from steam-session's EAuthTokenPlatformType enum. Defaults to MobileApp.
* @return Promise<{cookies: string[], sessionID: string, refreshToken: string}>
*/
SteamCommunity.prototype.login = function(details) {
if (typeof details.accountName != 'string' || typeof details.password != 'string') {
throw new Error('You must provide your accountName and password to login to steamcommunity.com');
if (options.localAddress) {
defaults.localAddress = options.localAddress;
}
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
let platformType = details.authTokenPlatformType || EAuthTokenPlatformType.MobileApp;
let session = new LoginSession(platformType, {
httpProxy: this._options.httpProxy
});
this.request = options.request || Request.defaults({"forever": true}); // "forever" indicates that we want a keep-alive agent
this.request = this.request.defaults(defaults);
session.on('authenticated', async () => {
try {
let cookies = await session.getWebCookies();
this.setCookies(cookies);
// English
this._setCookie(Request.cookie('Steam_Language=english'));
if (platformType == EAuthTokenPlatformType.MobileApp) {
this.setMobileAppAccessToken(session.accessToken);
}
// UTC
this._setCookie(Request.cookie('timezoneOffset=0,0'));
}
// TODO set refresh token for session keep-alive
SteamCommunity.prototype.login = function(details, callback) {
if (!details.accountName || !details.password) {
throw new Error("Missing either accountName or password to login; both are needed");
}
let sessionID = this.getSessionID();
if (!cookies.some(c => c.startsWith('sessionid='))) {
// make sure that the sessionid we return is in the cookies list we return
cookies.push(`sessionid=${sessionID}`);
}
// Delete the cache
delete this._profileURL;
resolve({
cookies,
sessionID,
refreshToken: session.refreshToken
});
} catch (ex) {
reject(ex);
}
});
// default disableMobile to true
let logOnOptions = Object.assign({}, details);
logOnOptions.disableMobile = details.disableMobile !== false;
session.on('timeout', () => {
// This really shouldn't happen
reject(new Error('Login attempt timed out'));
});
session.on('error', reject);
this._modernLogin(logOnOptions).then(({sessionID, cookies, steamguard, mobileAccessToken}) => {
this.setCookies(cookies);
try {
let startResult = await session.startWithCredentials({
accountName: details.accountName,
password: details.password,
steamGuardCode: details.twoFactorCode || details.authCode
});
if (!startResult.actionRequired) {
return; // 'authenticated' should get emitted soon
}
session.cancelLoginAttempt();
if (startResult.validActions.some(a => a.type == EAuthSessionGuardType.EmailCode)) {
return reject(new Error('SteamGuard'));
}
if (startResult.validActions.some(a => a.type == EAuthSessionGuardType.DeviceCode)) {
return reject(new Error('SteamGuardMobile'));
}
let validActions = startResult.validActions.map(a => a.type).join(', ');
return reject(new Error(`Unexpected guard action(s) ${validActions}`));
} catch (ex) {
reject(ex);
if (mobileAccessToken) {
this.setMobileAppAccessToken(mobileAccessToken);
}
});
callback(null, sessionID, cookies, steamguard, null);
}).catch(err => callback(err));
};
/**
* @deprecated
* @param {string} steamguard
* @param {string} token
* @param {function} callback
*/
SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
steamguard = steamguard.split('||');
var steamID = new SteamID(steamguard[0]);
var self = this;
this.httpRequestPost({
"uri": "https://api.steampowered.com/IMobileAuthService/GetWGToken/v1/",
"form": {
"access_token": token
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.response || !body.response.token || !body.response.token_secure) {
callback(new Error("Malformed response"));
return;
}
var cookies = [
'steamLogin=' + encodeURIComponent(steamID.getSteamID64() + '||' + body.response.token),
'steamLoginSecure=' + encodeURIComponent(steamID.getSteamID64() + '||' + body.response.token_secure),
'steamMachineAuth' + steamID.getSteamID64() + '=' + steamguard[1],
'sessionid=' + self.getSessionID()
];
self.setCookies(cookies);
callback(null, self.getSessionID(), cookies);
}, "steamcommunity");
};
/**
* Get a token that can be used to log onto Steam using steam-user.
* @param {function} [callback]
* @return Promise<{steamID: SteamID, accountName: string, webLogonToken: string}>
* @param {function} callback
*/
SteamCommunity.prototype.getClientLogonToken = function(callback) {
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
let {jsonBody} = await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/chat/clientjstoken',
source: 'steamcommunity'
});
this.httpRequestGet({
"uri": "https://steamcommunity.com/chat/clientjstoken",
"json": true
}, (err, res, body) => {
if (err || res.statusCode != 200) {
callback(err ? err : new Error('HTTP error ' + res.statusCode));
return;
}
if (!jsonBody.logged_in) {
if (!body.logged_in) {
let e = new Error('Not Logged In');
callback(e);
this._notifySessionExpired(e);
return reject(e);
return;
}
if (!jsonBody.steamid || !jsonBody.account_name || !jsonBody.token) {
return reject(new Error('Malformed response'));
if (!body.steamid || !body.account_name || !body.token) {
callback(new Error('Malformed response'));
return;
}
resolve({
steamID: new SteamID(jsonBody.steamid),
accountName: jsonBody.account_name,
webLogonToken: jsonBody.token
callback(null, {
"steamID": new SteamID(body.steamid),
"accountName": body.account_name,
"webLogonToken": body.token
});
});
};
/**
* Sets a single cookie in our cookie jar.
* @param {string} cookie
* @private
*/
SteamCommunity.prototype._setCookie = function(cookie) {
this._jar.add(cookie, 'steamcommunity.com');
this._jar.add(cookie, 'store.steampowered.com');
this._jar.add(cookie, 'help.steampowered.com');
SteamCommunity.prototype._setCookie = function(cookie, secure) {
var protocol = secure ? "https" : "http";
cookie.secure = !!secure;
if (cookie.domain) {
this._jar.setCookie(cookie.clone(), protocol + '://' + cookie.domain);
} else {
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");
}
};
/**
* Set one or more cookies in this SteamCommunity's cookie jar.
* @param {string|string[]} cookies
*/
SteamCommunity.prototype.setCookies = function(cookies) {
if (!Array.isArray(cookies)) {
cookies = [cookies];
}
cookies.forEach((cookie) => {
let cookieName = cookie.match(/(.+)=/)[1];
var cookieName = cookie.trim().split('=')[0];
if (cookieName == 'steamLogin' || cookieName == 'steamLoginSecure') {
this.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
this.steamID = new SteamID(cookie.match(/steamLogin(Secure)?=(\d+)/)[2]);
}
this._setCookie(cookie);
this._setCookie(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
});
// The account we're logged in as might have changed, so verify that our mobile access token (if any) is still valid
@@ -216,281 +178,278 @@ SteamCommunity.prototype.setCookies = function(cookies) {
this._verifyMobileAccessToken();
};
SteamCommunity.prototype.getSessionID = function(domain = 'steamcommunity.com') {
let sessionIdCookie = this._jar.cookies
.filter(c => c.domain == domain)
.find(c => c.name == 'sessionid');
if (sessionIdCookie) {
return sessionIdCookie.content;
SteamCommunity.prototype.getSessionID = function(host = "http://steamcommunity.com") {
var cookies = this._jar.getCookieString(host).split(';');
for(var i = 0; i < cookies.length; i++) {
var match = cookies[i].trim().match(/([^=]+)=(.+)/);
if(match[1] == 'sessionid') {
return decodeURIComponent(match[2]);
}
}
// No cookie found? Generate a new session id
let sessionID = require('crypto').randomBytes(12).toString('hex');
this._setCookie(`sessionid=${sessionID}`);
var sessionID = generateSessionID();
this._setCookie(Request.cookie('sessionid=' + sessionID));
return sessionID;
};
/**
* @param {string} pin
* @param {function} [callback]
* @return Promise<void>
*/
function generateSessionID() {
return require('crypto').randomBytes(12).toString('hex');
}
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
let sessionID = this.getSessionID();
var self = this;
var sessionID = self.getSessionID();
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let {jsonBody} = await this.httpRequest({
method: 'POST',
url: 'https://steamcommunity.com/parental/ajaxunlock',
form: {
pin: pin,
sessionid: sessionID
},
source: 'steamcommunity'
});
if (!jsonBody || typeof jsonBody.success !== 'boolean') {
return reject('Invalid response');
this.httpRequestPost("https://steamcommunity.com/parental/ajaxunlock", {
"json": true,
"form": {
"pin": pin,
"sessionid": sessionID
}
}, function(err, response, body) {
if(!callback) {
return;
}
if (!jsonBody.success) {
switch (jsonBody.eresult) {
case SteamCommunity.EResult.AccessDenied:
return reject('Incorrect PIN');
if (err) {
callback(err);
return;
}
case SteamCommunity.EResult.LimitExceeded:
return reject('Too many invalid PIN attempts');
if (!body || typeof body.success !== 'boolean') {
callback("Invalid response");
return;
}
if (!body.success) {
switch (body.eresult) {
case 15:
callback("Incorrect PIN");
break;
case 25:
callback("Too many invalid PIN attempts");
break;
default:
return reject('Error ' + jsonBody.eresult);
callback("Error " + body.eresult);
}
return;
}
resolve();
});
callback();
}.bind(this), "steamcommunity");
};
/**
* @param {function} [callback]
* @return Promise<object>
*/
SteamCommunity.prototype.getNotifications = function(callback) {
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
let {jsonBody} = await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/actions/GetNotificationCounts',
source: 'steamcommunity'
});
if (!jsonBody || !jsonBody.notifications) {
return reject(new Error('Malformed response'));
var self = this;
this.httpRequestGet({
"uri": "https://steamcommunity.com/actions/GetNotificationCounts",
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
let notifications = {
trades: jsonBody.notifications[1] || 0,
gameTurns: jsonBody.notifications[2] || 0,
moderatorMessages: jsonBody.notifications[3] || 0,
comments: jsonBody.notifications[4] || 0,
items: jsonBody.notifications[5] || 0,
invites: jsonBody.notifications[6] || 0,
if (!body || !body.notifications) {
callback(new Error("Malformed response"));
return;
}
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: jsonBody.notifications[8] || 0,
chat: jsonBody.notifications[9] || 0,
helpRequestReplies: jsonBody.notifications[10] || 0,
accountAlerts: jsonBody.notifications[11] || 0
"gifts": body.notifications[8] || 0,
"chat": body.notifications[9] || 0,
"helpRequestReplies": body.notifications[10] || 0,
"accountAlerts": body.notifications[11] || 0
};
resolve(notifications);
});
callback(null, notifications);
}, "steamcommunity");
};
/**
* @param {function} [callback]
* @return Promise<void>
*/
SteamCommunity.prototype.resetItemNotifications = function(callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/my/inventory',
source: 'steamcommunity'
});
var self = this;
this.httpRequestGet("https://steamcommunity.com/my/inventory", function(err, response, body) {
if(!callback) {
return;
}
resolve();
});
callback(err || null);
}, "steamcommunity");
};
/**
* @param {function} [callback]
* @return Promise<{loggedIn: boolean, familyView: boolean}>
*/
SteamCommunity.prototype.loggedIn = function(callback) {
return StdLib.Promises.callbackPromise(['loggedIn', 'familyView'], callback, false, async (resolve, reject) => {
let result = await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/my',
followRedirect: false,
checkHttpError: false,
source: 'steamcommunity'
});
if (result.statusCode != 302 && result.statusCode != 403) {
return reject(new Error(`HTTP error ${result.statusCode}`));
this.httpRequestGet({
"uri": "https://steamcommunity.com/my",
"followRedirect": false,
"checkHttpError": false
}, function(err, response, body) {
if(err || (response.statusCode != 302 && response.statusCode != 403)) {
callback(err || new Error("HTTP error " + response.statusCode));
return;
}
if (result.statusCode == 403) {
// TODO check response body to see if this is an akamai block
return resolve({
loggedIn: true,
familyView: true
});
if(response.statusCode == 403) {
callback(null, true, true);
return;
}
return resolve({
loggedIn: !!result.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/),
familyView: false
});
});
callback(null, !!response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/), false);
}, "steamcommunity");
};
/**
* @param {function} [callback]
* @return Promise<{url: string, token: string}>
*/
SteamCommunity.prototype.getTradeURL = function(callback) {
return StdLib.Promises.callbackPromise(['url', 'token'], callback, false, async (resolve, reject) => {
let {textBody} = await this._myProfile('tradeoffers/privacy');
let match = textBody.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&amp;)token=([a-zA-Z0-9-_]+)/);
if (!match) {
return reject(new Error('Malformed response'));
this._myProfile("tradeoffers/privacy", null, (err, response, body) => {
if (err) {
callback(err);
return;
}
let token = match[3];
resolve({
url: match[0],
token
});
});
var match = body.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&amp;)token=([a-zA-Z0-9-_]+)/);
if (match) {
var token = match[3];
callback(null, match[0], token);
} else {
callback(new Error("Malformed response"));
}
}, "steamcommunity");
};
/**
* @param [callback]
* @return Promise<{url: string, token: string}>
*/
SteamCommunity.prototype.changeTradeURL = function(callback) {
return StdLib.Promises.callbackPromise(['url', 'token'], callback, true, async (resolve, reject) => {
let {textBody} = await this._myProfile('tradeoffers/newtradeurl', {sessionid: this.getSessionID()});
if (!textBody || typeof textBody !== 'string' || textBody.length < 3 || textBody.indexOf('"') !== 0) {
return reject(new Error('Malformed response'));
this._myProfile("tradeoffers/newtradeurl", {"sessionid": this.getSessionID()}, (err, response, body) => {
if (!callback) {
return;
}
let newToken = textBody.replace(/"/g, ''); //"t1o2k3e4n" => t1o2k3e4n
resolve({
url: `https://steamcommunity.com/tradeoffer/new/?partner=${this.steamID.accountid}&token=${newToken}`,
token: newToken
});
});
if (!body || typeof body !== "string" || body.length < 3 || body.indexOf('"') !== 0) {
callback(new Error("Malformed response"));
return;
}
var newToken = body.replace(/"/g, ''); //"t1o2k3e4n" => t1o2k3e4n
callback(null, "https://steamcommunity.com/tradeoffer/new/?partner=" + this.steamID.accountid + "&token=" + newToken, newToken);
}, "steamcommunity");
};
/**
* Clear your profile name (alias) history.
* @param {function} [callback]
* @return Promise<void>
* @param {function} callback
*/
SteamCommunity.prototype.clearPersonaNameHistory = function(callback) {
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
let {statusCode, textBody} = await this._myProfile('ajaxclearaliashistory/', {sessionid: this.getSessionID()});
this._myProfile("ajaxclearaliashistory/", {"sessionid": this.getSessionID()}, (err, res, body) => {
if (!callback) {
return;
}
if (statusCode != 200) {
return reject(new Error(`HTTP error ${statusCode}`));
if (err) {
return callback(err);
}
if (res.statusCode != 200) {
return callback(new Error("HTTP error " + res.statusCode));
}
try {
let body = JSON.parse(textBody);
let err = Helpers.eresultError(body.success);
return err ? reject(err) : resolve();
body = JSON.parse(body);
callback(Helpers.eresultError(body.success));
} catch (ex) {
return reject(new Error('Malformed response'));
return callback(new Error("Malformed response"));
}
});
};
SteamCommunity.prototype._myProfile = function(endpoint, form, callback) {
var self = this;
if (this._profileURL) {
completeRequest(this._profileURL);
} else {
this.httpRequest("https://steamcommunity.com/my", {"followRedirect": false}, function(err, response, body) {
if(err || response.statusCode != 302) {
callback(err || "HTTP error " + response.statusCode);
return;
}
var match = response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^\/]+)\/?/);
if(!match) {
callback(new Error("Can't get profile URL"));
return;
}
self._profileURL = match[1];
setTimeout(function () {
delete self._profileURL; // delete the cache
}, 60000).unref();
completeRequest(match[1]);
}, "steamcommunity");
}
function completeRequest(url) {
var options = endpoint.endpoint ? endpoint : {};
options.uri = "https://steamcommunity.com" + url + "/" + (endpoint.endpoint || endpoint);
if (form) {
options.method = "POST";
options.form = form;
options.followAllRedirects = true;
} else if (!options.method) {
options.method = "GET";
}
self.httpRequest(options, callback, "steamcommunity");
}
};
/**
* Returns an object whose keys are 64-bit SteamIDs, and whose values are values from the EFriendRelationship enum.
* Therefore, you can deduce your friends or blocked list from this object.
* @param {function} [callback]
* @return Promise<object[]>
* @param {function} callback
*/
SteamCommunity.prototype.getFriendsList = function(callback) {
return StdLib.Promises.callbackPromise(['friends'], callback, false, async (resolve, reject) => {
let {jsonBody} = await this.httpRequest({
method: 'GET',
url: 'https://steamcommunity.com/textfilter/ajaxgetfriendslist',
source: 'steamcommunity'
});
if (jsonBody.success != SteamCommunity.EResult.OK) {
return reject(Helpers.eresultError(jsonBody.success));
this.httpRequestGet({
"uri": "https://steamcommunity.com/textfilter/ajaxgetfriendslist",
"json": true
}, (err, res, body) => {
if (err) {
callback(err ? err : new Error('HTTP error ' + res.statusCode));
return;
}
if (!jsonBody.friendslist || !jsonBody.friendslist.friends) {
return reject(new Error('Malformed response'));
if (body.success != 1) {
callback(Helpers.eresultError(body.success));
return;
}
if (!body.friendslist || !body.friendslist.friends) {
callback(new Error('Malformed response'));
return;
}
const friends = {};
jsonBody.friendslist.friends.forEach(friend => (friends[friend.ulfriendid] = friend.efriendrelationship));
resolve({friends});
});
};
/**
* @param {string} url
* @return Promise<{vanityURL: string, steamID: SteamID}>
* @private
*/
SteamCommunity.prototype._resolveVanityURL = async function(url) {
// Precede url param if only the vanity was provided
if (!url.includes('steamcommunity.com')) {
url = `https://steamcommunity.com/id/${url}`;
}
// Make request to get XML data
let {textBody} = await this._httpRequest({
method: 'GET',
url,
source: 'steamcommunity'
});
return await new Promise((resolve, reject) => {
// Parse XML data returned from Steam into an object
new xml2js.Parser().parseString(textBody, (err, parsed) => {
if (err) {
return reject(new Error('Couldn\'t parse XML response'));
}
if (parsed.response && parsed.response.error) {
return reject(new Error('Couldn\'t find Steam ID'));
}
let steamID64 = parsed.profile.steamID64[0];
let vanityURL = parsed.profile.customURL[0];
resolve({
vanityURL,
steamID: new SteamID(steamID64)
});
});
body.friendslist.friends.forEach(friend => (friends[friend.ulfriendid] = friend.efriendrelationship));
callback(null, friends);
});
};
require('./components/login.js');
require('./components/http.js');
require('./components/chat.js');
require('./components/profile.js');
require('./components/market.js');
require('./components/groups.js');
require('./components/users.js');
require('./components/sharedfiles.js');
require('./components/inventoryhistory.js');
require('./components/webapi.js');
require('./components/twofactor.js');
require('./components/confirmations.js');

View File

@@ -1,8 +1,14 @@
{
"name": "steamcommunity",
"version": "4.0.0-dev",
"private": true,
"version": "3.48.7",
"description": "Provides an interface for logging into and interacting with the Steam Community website",
"files": [
"/classes",
"/components",
"/examples",
"/resources",
"/index.js"
],
"keywords": [
"steam",
"steam community"
@@ -22,22 +28,17 @@
"url": "https://github.com/DoctorMcKay/node-steamcommunity.git"
},
"dependencies": {
"@doctormckay/stdlib": "^2.6.0",
"@doctormckay/user-agents": "^1.0.0",
"async": "^2.6.3",
"cheerio": "0.22.0",
"image-size": "^0.8.2",
"steam-session": "^1.2.4",
"steam-totp": "^2.1.0",
"steamid": "^2.0.0",
"tough-cookie": "^4.0.0",
"xml2js": "^0.6.0"
"request": "^2.88.0",
"steam-session": "^1.9.1",
"steam-totp": "^1.5.0",
"steamid": "^1.1.3",
"xml2js": "^0.6.2"
},
"engines": {
"node": ">=14.0.0"
},
"devDependencies": {
"eslint": "^7.31.0"
},
"scripts": {
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx"
"node": ">=4.0.0"
}
}

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

@@ -1,5 +1,3 @@
/* eslint-disable */
/**
* @enum EConfirmationType
*/

View File

@@ -1,9 +1,7 @@
/* eslint-disable */
/**
* @enum EFriendRelationship
*/
module.exports = {
module.exports = {
"None": 0,
"Blocked": 1,
"RequestRecipient": 2,

View File

@@ -1,5 +1,3 @@
/* eslint-disable */
/**
* @enum EPersonaState
*/

View File

@@ -1,5 +1,3 @@
/* eslint-disable */
/**
* @enum EPersonaStateFlag
*/

View File

@@ -1,6 +1,3 @@
/* eslint-disable */
// Auto-generated by generate-enums script on Thu Jul 29 2021 04:43:52 GMT-0400 (Eastern Daylight Time)
/**
* @enum EResult
*/
@@ -132,8 +129,7 @@ module.exports = {
"DeniedDueToCommunityCooldown": 116,
"NoLauncherSpecified": 117,
"MustAgreeToSSA": 118,
"ClientNoLongerSupported": 119, // obsolete
"LauncherMigrated": 119,
"ClientNoLongerSupported": 119,
// Value-to-name mapping for convenience
"0": "Invalid",
@@ -254,5 +250,5 @@ module.exports = {
"116": "DeniedDueToCommunityCooldown",
"117": "NoLauncherSpecified",
"118": "MustAgreeToSSA",
"119": "LauncherMigrated",
"119": "ClientNoLongerSupported",
};