node-steamcommunity/index.js

582 lines
16 KiB
JavaScript
Raw Normal View History

2021-07-29 14:40:39 +08:00
const {EventEmitter} = require('events');
2021-07-29 17:47:24 +08:00
const Got = require('got');
2021-07-29 14:40:39 +08:00
const {hex2b64} = require('node-bignumber');
const Request = require('request');
2021-07-29 14:40:39 +08:00
const {Key: RSA} = require('node-bignumber');
2021-07-29 17:55:12 +08:00
const StdLib = require('@doctormckay/stdlib');
const SteamID = require('steamid');
2021-07-29 17:47:24 +08:00
const {CookieJar} = require('tough-cookie');
2019-02-14 13:29:21 +08:00
const Util = require('util');
2014-10-01 03:18:38 +08:00
2019-04-19 13:20:01 +08:00
const Helpers = require('./components/helpers.js');
2021-07-29 17:47:24 +08:00
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36';
2015-12-27 07:17:01 +08:00
2019-02-14 13:29:21 +08:00
Util.inherits(SteamCommunity, EventEmitter);
2015-06-18 12:22:05 +08:00
2014-10-01 03:18:38 +08:00
module.exports = SteamCommunity;
2014-12-17 13:55:43 +08:00
SteamCommunity.SteamID = SteamID;
SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
2019-02-14 13:29:21 +08:00
SteamCommunity.EResult = require('./resources/EResult.js');
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
2017-12-28 13:32:06 +08:00
2014-12-17 13:55:43 +08:00
function SteamCommunity(options) {
options = options || {};
2021-07-29 17:47:24 +08:00
this._jar = new CookieJar();
2015-07-18 10:26:29 +08:00
this._captchaGid = -1;
2016-03-05 08:35:04 +08:00
this._httpRequestID = 0;
2021-07-29 14:40:39 +08:00
let defaults = {
jar: this._jar,
timeout: options.timeout || 50000,
gzip: true,
headers: {
'User-Agent': options.userAgent || USER_AGENT
2015-12-27 07:17:01 +08:00
}
};
this._options = options;
if (options.localAddress) {
defaults.localAddress = options.localAddress;
}
2021-07-29 14:40:39 +08:00
this.request = options.request || Request.defaults({forever: true}); // "forever" indicates that we want a keep-alive agent
this.request = this.request.defaults(defaults);
2015-07-11 04:00:56 +08:00
// English
2021-07-29 17:47:24 +08:00
this._setCookie('Steam_Language=english');
2015-08-07 12:28:24 +08:00
// UTC
2021-07-29 17:47:24 +08:00
this._setCookie('timezoneOffset=0,0');
2014-10-01 03:18:38 +08:00
}
2014-12-17 13:48:19 +08:00
SteamCommunity.prototype.login = function(details, callback) {
if (!details.accountName || !details.password) {
2021-07-29 14:40:39 +08:00
throw new Error('Missing either accountName or password to login; both are needed');
}
2021-07-29 17:55:12 +08:00
let callbackArgs = ['sessionID', 'cookies', 'steamguard', 'oauthToken'];
return StdLib.Promises.callbackPromise(callbackArgs, callback, false, (resolve, reject) => {
if (details.steamguard) {
let parts = details.steamguard.split('||');
this._setCookie(`steamMachineAuth${parts[0]}=${encodeURIComponent(parts[1])}`, true);
2014-10-01 03:18:38 +08:00
}
2021-07-29 17:55:12 +08:00
let disableMobile = details.disableMobile;
// Delete the cache
delete this._profileURL;
// headers required to convince steam that we're logging in from a mobile device so that we can get the oAuth data
let mobileHeaders = {};
if (!disableMobile) {
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': this._options.mobileUserAgent || details.mobileUserAgent || '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._setCookie('mobileClientVersion=0 (2.1.3)');
this._setCookie('mobileClient=android');
} else {
mobileHeaders = {Referer: 'https://steamcommunity.com/login'};
}
2021-07-29 17:55:12 +08:00
const deleteMobileCookies = () => {
this._setCookie('mobileClientVersion=; max-age=0');
this._setCookie('mobileClient=; max-age=0');
};
2021-07-29 17:55:12 +08:00
this.httpRequestPost('https://steamcommunity.com/login/getrsakey/', {
form: {username: details.accountName},
headers: mobileHeaders,
json: true
2021-07-29 14:55:56 +08:00
}, (err, response, body) => {
2021-07-29 17:55:12 +08:00
// Remove the mobile cookies
if (err) {
2021-07-29 17:55:12 +08:00
deleteMobileCookies();
return reject(err);
}
if (!body.publickey_mod || !body.publickey_exp) {
deleteMobileCookies();
return reject(new Error('Invalid RSA key received'));
}
let key = new RSA();
key.setPublic(body.publickey_mod, body.publickey_exp);
let formObj = {
captcha_text: details.captcha || '',
captchagid: this._captchaGid,
emailauth: details.authCode || '',
emailsteamid: '',
password: hex2b64(key.encrypt(details.password)),
remember_login: 'true',
rsatimestamp: body.timestamp,
twofactorcode: details.twoFactorCode || '',
username: details.accountName,
loginfriendlyname: '',
donotcache: Date.now()
};
if (!disableMobile) {
formObj.oauth_client_id = 'DE45CD61';
formObj.oauth_scope = 'read_profile write_profile read_client write_client';
formObj.loginfriendlyname = '#login_emailauth_friendlyname_mobile';
2014-10-01 03:18:38 +08:00
}
2016-03-01 05:24:29 +08:00
2021-07-29 17:55:12 +08:00
this.httpRequestPost({
uri: 'https://steamcommunity.com/login/dologin/',
json: true,
form: formObj,
headers: mobileHeaders
}, (err, response, body) => {
deleteMobileCookies();
if (err) {
return reject(err);
}
let error;
if (!body.success && body.emailauth_needed) {
// Steam Guard (email)
error = new Error('SteamGuard');
error.emaildomain = body.emaildomain;
return reject(error);
} else if (!body.success && body.requires_twofactor) {
// Steam Guard (app)
return reject(new Error('SteamGuardMobile'));
} else if (!body.success && body.captcha_needed && body.message.match(/Please verify your humanity/)) {
error = new Error('CAPTCHA');
error.captchaurl = 'https://steamcommunity.com/login/rendercaptcha/?gid=' + body.captcha_gid;
this._captchaGid = body.captcha_gid;
return reject(error);
} else if (!body.success) {
return reject(new Error(body.message || 'Unknown error'));
} else if (!disableMobile && !body.oauth) {
return reject(new Error('Malformed response'));
2021-07-29 14:40:39 +08:00
} else {
2021-07-29 17:55:12 +08:00
let sessionID = this.getSessionID();
let oAuth;
let cookies = this._jar.getCookieStringSync('https://steamcommunity.com').split(';').map(cookie => cookie.trim());
if (!disableMobile) {
oAuth = JSON.parse(body.oauth);
this.steamID = new SteamID(oAuth.steamid);
this.oAuthToken = oAuth.oauth_token;
} else {
for (let i = 0; i < cookies.length; i++) {
let parts = cookies[i].split('=');
if (parts[0] == 'steamLogin') {
this.steamID = new SteamID(decodeURIComponent(parts[1]).split('||')[0]);
break;
}
}
this.oAuthToken = null;
}
// Find the Steam Guard cookie
let steamguard = null;
2021-07-29 14:40:39 +08:00
for (let i = 0; i < cookies.length; i++) {
let parts = cookies[i].split('=');
2021-07-29 17:55:12 +08:00
if (parts[0] == 'steamMachineAuth' + this.steamID) {
steamguard = this.steamID.toString() + '||' + decodeURIComponent(parts[1]);
break;
}
}
2021-07-29 17:55:12 +08:00
// Call setCookies to propagate our cookies to the other domains
this.setCookies(cookies);
2021-07-29 17:55:12 +08:00
return resolve({
sessionID,
cookies,
steamguard,
oauthToken: disableMobile ? null : oAuth.oauth_token
});
2014-10-01 03:18:38 +08:00
}
2021-07-29 17:55:12 +08:00
}, 'steamcommunity');
2021-07-29 14:40:39 +08:00
}, 'steamcommunity');
2021-07-29 17:55:12 +08:00
});
2014-12-17 13:48:19 +08:00
};
SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
steamguard = steamguard.split('||');
2021-07-29 14:40:39 +08:00
let steamID = new SteamID(steamguard[0]);
2015-12-27 07:15:09 +08:00
this.httpRequestPost({
2021-07-29 14:40:39 +08:00
uri: 'https://api.steampowered.com/IMobileAuthService/GetWGToken/v1/',
2021-07-29 14:55:56 +08:00
form: {access_token: token},
2021-07-29 14:40:39 +08:00
json: true
2021-07-29 14:55:56 +08:00
}, (err, response, body) => {
if (err) {
callback(err);
2015-12-27 07:15:09 +08:00
return;
}
2021-07-29 14:40:39 +08:00
if (!body.response || !body.response.token || !body.response.token_secure) {
callback(new Error('Malformed response'));
2015-12-27 07:15:09 +08:00
return;
}
2021-07-29 14:40:39 +08:00
let cookies = [
'steamLogin=' + encodeURIComponent(steamID.getSteamID64() + '||' + body.response.token),
'steamLoginSecure=' + encodeURIComponent(steamID.getSteamID64() + '||' + body.response.token_secure),
'steamMachineAuth' + steamID.getSteamID64() + '=' + steamguard[1],
2021-07-29 14:55:56 +08:00
'sessionid=' + this.getSessionID()
2015-12-27 07:15:09 +08:00
];
2021-07-29 14:55:56 +08:00
this.setCookies(cookies);
callback(null, this.getSessionID(), cookies);
2021-07-29 14:40:39 +08:00
}, 'steamcommunity');
2015-12-27 07:15:09 +08:00
};
2019-02-14 06:40:45 +08:00
/**
* Get a token that can be used to log onto Steam using steam-user.
* @param {function} callback
*/
SteamCommunity.prototype.getClientLogonToken = function(callback) {
this.httpRequestGet({
2021-07-29 14:40:39 +08:00
uri: 'https://steamcommunity.com/chat/clientjstoken',
json: true
2019-02-14 06:40:45 +08:00
}, (err, res, body) => {
if (err || res.statusCode != 200) {
callback(err ? err : new Error('HTTP error ' + res.statusCode));
return;
}
if (!body.logged_in) {
let e = new Error('Not Logged In');
callback(e);
this._notifySessionExpired(e);
return;
}
if (!body.steamid || !body.account_name || !body.token) {
2019-02-14 06:40:45 +08:00
callback(new Error('Malformed response'));
return;
}
callback(null, {
2021-07-29 14:40:39 +08:00
steamID: new SteamID(body.steamid),
accountName: body.account_name,
webLogonToken: body.token
});
2019-02-14 06:40:45 +08:00
});
};
2021-07-29 17:47:24 +08:00
/**
* Sets a single cookie in our cookie jar.
* @param {string} cookie
* @param {boolean} [secure=false]
* @private
*/
SteamCommunity.prototype._setCookie = function(cookie, secure) {
2021-07-29 14:40:39 +08:00
let protocol = secure ? 'https' : 'http';
2021-07-29 17:47:24 +08:00
this._jar.setCookieSync(cookie, `${protocol}://steamcommunity.com`);
this._jar.setCookieSync(cookie, `${protocol}://store.steampowered.com`);
this._jar.setCookieSync(cookie, `${protocol}://help.steampowered.com`);
};
2021-07-29 17:47:24 +08:00
/**
* Set one or more cookies in this SteamCommunity's cookie jar.
* @param {string|string[]} cookies
*/
2014-12-17 13:48:19 +08:00
SteamCommunity.prototype.setCookies = function(cookies) {
2021-07-29 17:47:24 +08:00
if (!Array.isArray(cookies)) {
cookies = [cookies];
}
2018-07-11 14:12:37 +08:00
cookies.forEach((cookie) => {
2021-07-29 14:40:39 +08:00
let cookieName = cookie.match(/(.+)=/)[1];
2018-07-11 14:12:37 +08:00
if (cookieName == 'steamLogin' || cookieName == 'steamLoginSecure') {
this.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
2014-12-17 13:48:19 +08:00
}
2021-07-29 17:47:24 +08:00
this._setCookie(cookie, !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
2014-12-17 13:48:19 +08:00
});
};
2021-07-29 17:47:24 +08:00
SteamCommunity.prototype.getSessionID = function() {
let sessionIdCookie = this._jar.getCookiesSync('http://steamcommunity.com').find(cookie => cookie.key == 'sessionid');
if (sessionIdCookie) {
return sessionIdCookie.value;
2014-12-17 13:48:19 +08:00
}
2021-07-29 17:47:24 +08:00
// Generate a new session id
let sessionID = require('crypto').randomBytes(12).toString('hex');
this._setCookie(`sessionid=${sessionID}`);
2014-12-17 13:48:19 +08:00
return sessionID;
};
2015-05-12 03:48:00 +08:00
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
2021-07-29 14:55:56 +08:00
let sessionID = this.getSessionID();
2021-07-29 14:40:39 +08:00
this.httpRequestPost('https://steamcommunity.com/parental/ajaxunlock', {
json: true,
form: {
pin: pin,
sessionid: sessionID
2015-05-12 03:48:00 +08:00
}
2021-07-29 14:55:56 +08:00
}, (err, response, body) => {
2021-07-29 14:40:39 +08:00
if (!callback) {
return;
}
if (err) {
callback(err);
return;
2015-05-12 03:48:00 +08:00
}
if (!body || typeof body.success !== 'boolean') {
2021-07-29 14:40:39 +08:00
callback('Invalid response');
return;
2015-05-12 03:48:00 +08:00
}
if (!body.success) {
switch (body.eresult) {
2021-07-29 14:40:39 +08:00
case SteamCommunity.EResult.AccessDenied:
callback('Incorrect PIN');
break;
2021-07-29 14:40:39 +08:00
case SteamCommunity.EResult.LimitExceeded:
callback('Too many invalid PIN attempts');
break;
default:
2021-07-29 14:40:39 +08:00
callback('Error ' + body.eresult);
}
return;
2015-05-12 03:48:00 +08:00
}
2015-05-12 03:48:00 +08:00
callback();
2021-07-29 14:55:56 +08:00
}, 'steamcommunity');
2015-05-12 03:48:00 +08:00
};
2015-05-26 01:37:01 +08:00
SteamCommunity.prototype.getNotifications = function(callback) {
this.httpRequestGet({
2021-07-29 14:40:39 +08:00
uri: 'https://steamcommunity.com/actions/GetNotificationCounts',
json: true
}, (err, response, body) => {
if (err) {
callback(err);
return;
2015-05-26 01:37:01 +08:00
}
if (!body || !body.notifications) {
2021-07-29 14:40:39 +08:00
callback(new Error('Malformed response'));
return;
}
2021-07-29 14:40:39 +08:00
let 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
2021-07-29 14:40:39 +08:00
gifts: body.notifications[8] || 0,
chat: body.notifications[9] || 0,
helpRequestReplies: body.notifications[10] || 0,
accountAlerts: body.notifications[11] || 0
2015-05-26 01:37:01 +08:00
};
2015-05-26 01:37:01 +08:00
callback(null, notifications);
2021-07-29 14:40:39 +08:00
}, 'steamcommunity');
2015-05-26 01:37:01 +08:00
};
2015-05-26 01:20:47 +08:00
SteamCommunity.prototype.resetItemNotifications = function(callback) {
2021-07-29 14:40:39 +08:00
this.httpRequestGet('https://steamcommunity.com/my/inventory', (err, response, body) => {
if (!callback) {
2015-05-26 01:20:47 +08:00
return;
}
callback(err || null);
2021-07-29 14:40:39 +08:00
}, 'steamcommunity');
2015-05-26 01:20:47 +08:00
};
2015-07-18 09:10:08 +08:00
SteamCommunity.prototype.loggedIn = function(callback) {
2016-03-07 14:32:27 +08:00
this.httpRequestGet({
2021-07-29 14:40:39 +08:00
uri: 'https://steamcommunity.com/my',
followRedirect: false,
checkHttpError: false
}, (err, response, body) => {
if (err || (response.statusCode != 302 && response.statusCode != 403)) {
callback(err || new Error('HTTP error ' + response.statusCode));
2015-07-18 09:10:08 +08:00
return;
}
2021-07-29 14:40:39 +08:00
if (response.statusCode == 403) {
callback(null, true, true);
return;
}
2021-07-29 14:40:39 +08:00
callback(null, !!response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/), false);
}, 'steamcommunity');
2015-07-18 09:10:08 +08:00
};
2017-10-03 02:55:29 +08:00
SteamCommunity.prototype.getTradeURL = function(callback) {
2021-07-29 14:40:39 +08:00
this._myProfile('tradeoffers/privacy', null, (err, response, body) => {
if (err) {
callback(err);
return;
}
2021-07-29 14:40:39 +08:00
let match = body.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&amp;)token=([a-zA-Z0-9-_]+)/);
2017-10-03 02:55:29 +08:00
if (match) {
2021-07-29 14:40:39 +08:00
let token = match[3];
2017-10-03 02:55:29 +08:00
callback(null, match[0], token);
} else {
2021-07-29 14:40:39 +08:00
callback(new Error('Malformed response'));
2017-10-03 02:55:29 +08:00
}
2021-07-29 14:40:39 +08:00
}, 'steamcommunity');
2017-10-03 02:55:29 +08:00
};
2017-12-28 13:32:06 +08:00
SteamCommunity.prototype.changeTradeURL = function(callback) {
2021-07-29 14:40:39 +08:00
this._myProfile('tradeoffers/newtradeurl', {sessionid: this.getSessionID()}, (err, response, body) => {
2017-12-28 13:32:06 +08:00
if (!callback) {
return;
}
2021-07-29 14:40:39 +08:00
if (!body || typeof body !== 'string' || body.length < 3 || body.indexOf('"') !== 0) {
callback(new Error('Malformed response'));
2017-12-28 13:32:06 +08:00
return;
}
2021-07-29 14:40:39 +08:00
let newToken = body.replace(/"/g, ''); //"t1o2k3e4n" => t1o2k3e4n
callback(null, 'https://steamcommunity.com/tradeoffer/new/?partner=' + this.steamID.accountid + '&token=' + newToken, newToken);
}, 'steamcommunity');
2017-12-28 07:19:57 +08:00
};
2019-04-19 13:20:01 +08:00
/**
* Clear your profile name (alias) history.
* @param {function} callback
*/
SteamCommunity.prototype.clearPersonaNameHistory = function(callback) {
2021-07-29 14:40:39 +08:00
this._myProfile('ajaxclearaliashistory/', {sessionid: this.getSessionID()}, (err, res, body) => {
2019-04-19 13:20:01 +08:00
if (!callback) {
return;
}
if (err) {
return callback(err);
}
if (res.statusCode != 200) {
2021-07-29 14:40:39 +08:00
return callback(new Error('HTTP error ' + res.statusCode));
2019-04-19 13:20:01 +08:00
}
try {
body = JSON.parse(body);
callback(Helpers.eresultError(body.success));
} catch (ex) {
2021-07-29 14:40:39 +08:00
return callback(new Error('Malformed response'));
2019-04-19 13:20:01 +08:00
}
});
};
SteamCommunity.prototype._myProfile = function(endpoint, form, callback) {
2021-07-29 14:55:56 +08:00
const completeRequest = (url) => {
let 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';
}
this.httpRequest(options, callback, 'steamcommunity');
};
if (this._profileURL) {
completeRequest(this._profileURL);
} else {
2021-07-29 14:55:56 +08:00
this.httpRequest('https://steamcommunity.com/my', {followRedirect: false}, (err, response, body) => {
2021-07-29 14:40:39 +08:00
if (err || response.statusCode != 302) {
callback(err || 'HTTP error ' + response.statusCode);
return;
}
2021-07-29 14:40:39 +08:00
let match = response.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/);
if (!match) {
callback(new Error('Can\'t get profile URL'));
return;
}
2021-07-29 14:55:56 +08:00
this._profileURL = match[1];
setTimeout(() => {
delete this._profileURL; // delete the cache
}, 60000).unref();
completeRequest(match[1]);
2021-07-29 14:40:39 +08:00
}, 'steamcommunity');
}
};
2021-06-04 10:08:07 +08:00
/**
* 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
*/
2021-07-22 15:04:38 +08:00
SteamCommunity.prototype.getFriendsList = function(callback) {
2021-06-04 10:08:07 +08:00
this.httpRequestGet({
2021-07-29 14:40:39 +08:00
uri: 'https://steamcommunity.com/textfilter/ajaxgetfriendslist',
json: true
2021-06-04 10:08:07 +08:00
}, (err, res, body) => {
if (err) {
callback(err ? err : new Error('HTTP error ' + res.statusCode));
return;
}
if (body.success != 1) {
callback(Helpers.eresultError(body.success));
return;
}
if (!body.friendslist || !body.friendslist.friends) {
callback(new Error('Malformed response'));
return;
}
const friends = {};
body.friendslist.friends.forEach(friend => (friends[friend.ulfriendid] = friend.efriendrelationship));
callback(null, friends);
});
};
require('./components/http.js');
2015-08-05 11:37:16 +08:00
require('./components/profile.js');
require('./components/market.js');
require('./components/groups.js');
require('./components/users.js');
2015-11-27 14:28:31 +08:00
require('./components/webapi.js');
2015-11-27 14:45:58 +08:00
require('./components/twofactor.js');
2015-12-03 10:31:17 +08:00
require('./components/confirmations.js');
require('./components/help.js');
2015-06-29 08:19:42 +08:00
require('./classes/CMarketItem.js');
2015-07-11 03:46:29 +08:00
require('./classes/CMarketSearchResult.js');
2014-12-17 14:48:17 +08:00
require('./classes/CSteamGroup.js');
2015-04-26 02:46:25 +08:00
require('./classes/CSteamUser.js');
2015-12-03 11:20:16 +08:00
/**
@callback SteamCommunity~genericErrorCallback
@param {Error|null} err - An Error object on failure, or null on success
2017-12-28 07:19:57 +08:00
*/