Compare commits

...

15 Commits

Author SHA1 Message Date
Alexander Corn
01217a3659 3.12.2 2015-12-02 22:38:59 -05:00
Alexander Corn
0e34bceb1f Only reset the polling timer if we have an interval set 2015-12-02 22:38:51 -05:00
Alexander Corn
c4ea8eeb41 3.12.1
I screwed up 3.12.0 and npm won't let me publish over it, so 3.12.0 just
doesn't exist.
2015-12-02 22:25:00 -05:00
Alexander Corn
0f83fe8c64 Added confirmation polling 2015-12-02 22:20:16 -05:00
Alexander Corn
5624611421 Moved getWebApiKey to webapi.js component 2015-12-02 21:35:35 -05:00
Alexander Corn
49d2bb4252 Fixed finalizeTwoFactor checking success before want_more (fixes #16) 2015-12-02 21:33:33 -05:00
Alexander Corn
b54ccff7c8 Added confirmations 2015-12-02 21:31:17 -05:00
Alexander Corn
b82f7ca045 Merge pull request #17 from s992/master
Updates login code to use mobile login headers and cookies
2015-12-02 19:19:50 -05:00
Sean Walsh
2526539266 removes extra loginfriendlyname 2015-12-01 21:41:30 -08:00
Sean Walsh
fc75036974 moves headers away from being default on everything. only use them on login. 2015-12-01 17:57:26 -08:00
Sean Walsh
ee1e819812 removes extra steam_language cookie 2015-12-01 17:40:51 -08:00
Sean Walsh
fceea0737d updates login code to use mobile login so that we can grab the oauth token. 2015-11-30 17:39:55 -08:00
Alexander Corn
f3ac556922 Added check for community error when getting webapi oauth token 2015-11-30 19:40:10 -05:00
Alexander Corn
2e02c14455 3.11.1 2015-11-27 22:09:47 -05:00
Alexander Corn
8f580fc231 Fixed enableTwoFactor returning success when it wasn't successful 2015-11-27 22:09:36 -05:00
6 changed files with 400 additions and 48 deletions

20
classes/CConfirmation.js Normal file
View File

@@ -0,0 +1,20 @@
module.exports = CConfirmation;
function CConfirmation(community, data) {
this._community = community;
this.id = data.id;
this.key = data.key;
this.title = data.title;
this.receiving = data.receiving;
this.time = data.time;
this.icon = data.icon;
}
CConfirmation.prototype.getOfferID = function(time, key, callback) {
this._community.getConfirmationOfferID(this.id, time, key, callback);
};
CConfirmation.prototype.respond = function(time, key, accept, callback) {
this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback);
};

284
components/confirmations.js Normal file
View File

@@ -0,0 +1,284 @@
var SteamCommunity = require('../index.js');
var Cheerio = require('cheerio');
var CConfirmation = require('../classes/CConfirmation.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
*/
SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
var self = this;
request(this, "conf", key, time, "conf", null, false, function(err, body) {
if(err) {
callback(err);
}
var $ = Cheerio.load(body);
var empty = $('#mobileconf_empty');
if(empty.length > 0) {
if(!$(empty).hasClass('mobileconf_done')) {
// An error occurred
callback(new Error(empty.find('div:nth-of-type(2)').text()));
} else {
callback(null, []);
}
return;
}
// We have something to confirm
var confirmations = $('#mobileconf_list');
if(!confirmations) {
callback(new Error("Malformed response"));
return;
}
var confs = [];
Array.prototype.forEach.call(confirmations.find('.mobileconf_list_entry'), function(conf) {
conf = $(conf);
var img = conf.find('.mobileconf_list_entry_icon img');
confs.push(new CConfirmation(self, {
"id": conf.data('confid'),
"key": conf.data('key'),
"title": conf.find('.mobileconf_list_entry_description>div:nth-of-type(1)').text().trim(),
"receiving": conf.find('.mobileconf_list_entry_description>div:nth-of-type(2)').text().trim(),
"time": conf.find('.mobileconf_list_entry_description>div:nth-of-type(3)').text().trim(),
"icon": img.length < 1 ? '' : $(img).attr('src')
}));
});
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
*/
/**
* Get the trade offer ID associated with a particular confirmation
* @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 "details" (this key can be reused)
* @param {SteamCommunity~getConfirmationOfferID} callback
*/
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
request(this, "details/" + confID, key, time, "details", null, true, function(err, body) {
if(err) {
callback(err);
}
if(!body.success) {
callback(new Error("Cannot load confirmation details"));
return;
}
var $ = Cheerio.load(body.html);
var offer = $('.tradeoffer');
if(offer.length < 1) {
callback(null, null);
return;
}
callback(null, offer.attr('id').split('_')[1]);
});
};
/**
* @callback SteamCommunity~getConfirmationOfferID
* @param {Error|null} err - An Error object on failure, or null on success
* @param {string} offerID - The trade offer ID associated with the specified confirmation, or null if not for an offer
*/
/**
* Confirm or cancel a given confirmation.
* @param {int} confID - The ID of the confirmation in question
* @param {string} confKey - The confirmation key associated with the confirmation in question (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 preceeding 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
*/
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
request(this, "ajaxop", key, time, accept ? "allow" : "cancel", {
"op": accept ? "allow" : "cancel",
"cid": confID,
"ck": confKey
}, true, function(err, body) {
if(!callback) {
return;
}
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"));
});
};
function request(community, url, key, time, tag, params, json, callback) {
params = params || {};
params.p = "android:" + Date.now();
params.a = community.steamID.getSteamID64();
params.k = key;
params.t = time;
params.m = "android";
params.tag = tag;
community.request.get({
"uri": "https://steamcommunity.com/mobileconf/" + url,
"qs": params,
"json": !!json
}, function(err, response, body) {
if(community._checkHttpError(err, response, callback)) {
return;
}
callback(null, body);
});
}
// 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 shouldn't be any less than 10,000 probably.
*/
SteamCommunity.prototype.startConfirmationChecker = function(pollInterval) {
this._confirmationPollInterval = pollInterval;
this._knownConfirmations = this._knownConfirmations || {};
this._confirmationKeys = this._confirmationKeys || {};
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._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;
this._confirmationCheckerGetKey('conf', function(err, key) {
if(err) {
resetTimer();
return;
}
self.getConfirmations(key.time, key.key, function(err, confirmations) {
if(err) {
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! Grab a key to get details.
self._confirmationCheckerGetKey('details', function(err, key) {
var handled = 0;
newOnes.forEach(function(conf) {
if(err) {
handleNewConfirmation(conf, handled++);
} else {
// Get its offer ID, if we can
conf.getOfferID(key.time, key.key, function(err, offerID) {
conf.offerID = offerID ? offerID : null;
handleNewConfirmation(conf, handled++);
});
}
});
resetTimer();
});
});
});
function resetTimer() {
if(self._confirmationPollInterval) {
self._confirmationTimer = setTimeout(self.checkConfirmations.bind(self), self._confirmationPollInterval);
}
}
function handleNewConfirmation(conf, handleNumber) {
self._knownConfirmations[conf.id] = conf;
// Delay them by 1 second per new confirmation that we see, so that keys won't be the same.
setTimeout(function() {
self.emit('newConfirmation', conf);
}, handleNumber * 1000);
}
};
SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) {
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});
});
};

View File

@@ -42,6 +42,13 @@ SteamCommunity.prototype.enableTwoFactor = function(callback) {
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);
});
});
@@ -85,7 +92,6 @@ SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, ca
}
body = body.response;
console.log(body);
if(body.server_time) {
diff = body.server_time - Math.floor(Date.now() / 1000);
@@ -93,13 +99,13 @@ SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, ca
if(body.status == 89) {
callback(new Error("Invalid activation code"));
} else if(!body.success) {
callback(new Error("Error " + body.status));
} else if(body.want_more) {
attemptsLeft--;
diff += 30;
finalize(token);
} else if(!body.success) {
callback(new Error("Error " + body.status));
} else {
callback(null);
}

View File

@@ -1,14 +1,60 @@
var SteamCommunity = require('../index.js');
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this;
this.request({
"uri": "https://steamcommunity.com/dev/apikey",
"followRedirect": false
}, function(err, response, body) {
if(self._checkHttpError(err, response, callback)) {
return;
}
if(body.match(/<h2>Access Denied<\/h2>/)) {
return callback(new Error("Access Denied"));
}
var match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if(match) {
// We already have an API key registered
callback(null, match[1]);
} else {
// We need to register a new API key
self.request.post('https://steamcommunity.com/dev/registerkey', {
"form": {
"domain": domain,
"agreeToTerms": "agreed",
"sessionid": self.getSessionID(),
"Submit": "Register"
}
}, function(err, response, body) {
if(self._checkHttpError(err, response, callback)) {
return;
}
self.getWebApiKey(domain, callback);
});
}
});
};
SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
var self = this;
if( this.oAuthToken ) {
return callback( null, this.oAuthToken );
}
// Pull an oauth token from the webchat UI
this.request("https://steamcommunity.com/chat", function(err, response, body) {
if(self._checkHttpError(err, response, callback)) {
return;
}
if(self._checkCommunityError(body, callback)) {
return;
}
var match = body.match(/"([0-9a-f]{32})"/);
if (!match) {
callback(new Error("Malformed response"));

View File

@@ -14,7 +14,11 @@ function SteamCommunity(localAddress) {
this._captchaGid = -1;
this.chatState = SteamCommunity.ChatState.Offline;
var defaults = {"jar": this._jar, "timeout": 50000};
var defaults = {
"jar": this._jar,
"timeout": 50000
};
if(localAddress) {
defaults.localAddress = localAddress;
}
@@ -26,6 +30,9 @@ function SteamCommunity(localAddress) {
// UTC
this._jar.setCookie(Request.cookie('timezoneOffset=0,0'), 'https://steamcommunity.com');
this._jar.setCookie(Request.cookie("mobileClientVersion=0 (2.1.3)"), "https://steamcommunity.com");
this._jar.setCookie(Request.cookie("mobileClient=android"), "https://steamcommunity.com");
}
SteamCommunity.prototype.login = function(details, callback) {
@@ -35,7 +42,21 @@ SteamCommunity.prototype.login = function(details, callback) {
}
var self = this;
this.request.post("https://steamcommunity.com/login/getrsakey/", {"form": {"username": details.accountName}}, function(err, response, body) {
// headers required to convince steam that we're logging in from a mobile device so that we can get the oAuth data
var mobileHeaders = {
"X-Requested-With": "com.valvesoftware.android.steam.community",
"referer": "https://steamcommunity.com/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client",
"user-agent": "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"accept": "text/javascript, text/html, application/xml, text/xml, */*"
};
this.request.post("https://steamcommunity.com/login/getrsakey/", {
"form": {
"username": details.accountName
},
"headers": mobileHeaders
}, function(err, response, body) {
if(err) {
callback(err);
return;
@@ -57,18 +78,21 @@ SteamCommunity.prototype.login = function(details, callback) {
"captchagid": self._captchaGid,
"emailauth": details.authCode || "",
"emailsteamid": "",
"loginfriendlyname": "",
"password": hex2b64(key.encrypt(details.password)),
"remember_login": "true",
"rsatimestamp": json.timestamp,
"twofactorcode": details.twoFactorCode || "",
"username": details.accountName
"username": details.accountName,
"oauth_client_id": "DE45CD61",
"oauth_scope": "read_profile write_profile read_client write_client",
"loginfriendlyname": "#login_emailauth_friendlyname_mobile"
};
self.request.post({
"uri": "https://steamcommunity.com/login/dologin/",
"json": true,
"form": form
"form": form,
"headers": mobileHeaders
}, function(err, response, body) {
if(self._checkHttpError(err, response, callback)) {
return;
@@ -94,9 +118,12 @@ SteamCommunity.prototype.login = function(details, callback) {
callback(new Error(body.message || "Unknown error"));
} else {
var sessionID = generateSessionID();
var oAuth = JSON.parse( body.oauth );
self._jar.setCookie(Request.cookie('sessionid=' + sessionID), 'http://steamcommunity.com');
self.steamID = new SteamID(body.transfer_parameters.steamid);
self.steamID = new SteamID(oAuth.steamid);
self.oAuthToken = oAuth.oauth_token;
var cookies = self._jar.getCookieString("https://steamcommunity.com").split(';').map(function(cookie) {
return cookie.trim();
});
@@ -147,44 +174,6 @@ function generateSessionID() {
return Math.floor(Math.random() * 1000000000);
}
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this;
this.request({
"uri": "https://steamcommunity.com/dev/apikey",
"followRedirect": false
}, function(err, response, body) {
if(self._checkHttpError(err, response, callback)) {
return;
}
if(body.match(/<h2>Access Denied<\/h2>/)) {
return callback(new Error("Access Denied"));
}
var match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if(match) {
// We already have an API key registered
callback(null, match[1]);
} else {
// We need to register a new API key
self.request.post('https://steamcommunity.com/dev/registerkey', {
"form": {
"domain": domain,
"agreeToTerms": "agreed",
"sessionid": self.getSessionID(),
"Submit": "Register"
}
}, function(err, response, body) {
if(self._checkHttpError(err, response, callback)) {
return;
}
self.getWebApiKey(domain, callback);
});
}
});
};
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
this.request.post("https://steamcommunity.com/parental/ajaxunlock", {
"json": true,
@@ -249,6 +238,7 @@ SteamCommunity.prototype.getNotifications = function(callback) {
};
SteamCommunity.prototype.resetItemNotifications = function(callback) {
var self = this;
this.request.get("https://steamcommunity.com/my/inventory", function(err, response, body) {
if(!callback) {
return;
@@ -335,7 +325,13 @@ require('./components/users.js');
require('./components/inventoryhistory.js');
require('./components/webapi.js');
require('./components/twofactor.js');
require('./components/confirmations.js');
require('./classes/CMarketItem.js');
require('./classes/CMarketSearchResult.js');
require('./classes/CSteamGroup.js');
require('./classes/CSteamUser.js');
/**
@callback SteamCommunity~genericErrorCallback
@param {Error|null} err - An Error object on failure, or null on success
*/

View File

@@ -1,6 +1,6 @@
{
"name": "steamcommunity",
"version": "3.11.0",
"version": "3.12.2",
"description": "Provides an interface for logging into and interacting with the Steam Community website",
"keywords": ["steam", "steam community"],
"homepage": "https://github.com/DoctorMcKay/node-steamcommunity",