Compare commits

...

30 Commits

Author SHA1 Message Date
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
Alex Corn
f69519f547 Fixed getUserInventoryContexts not properly returning errors for private inventory/profile
Closes #305
2023-06-27 00:53:55 -04:00
DoctorMcKay
29e18806cc Merge pull request #301 from fjexe/patch-1
Update users.js
2023-06-27 00:43:07 -04:00
DoctorMcKay
c855b505c5 Merge pull request #307 from HerrEurobeat/reenable-primaryGroup-support
Re-enable primaryGroup profile setting
2023-06-27 00:42:46 -04:00
Alex Corn
6cc96b51fa 3.46.1 2023-06-27 00:17:17 -04:00
DoctorMcKay
edf2891800 Merge pull request #314 from HerrEurobeat/fix-sharedfile-owner
Fix resolving vanity returning error
2023-06-27 00:16:17 -04:00
3urobeat
0fc1120fd3 Fix resolving vanity #313 2023-06-26 16:02:15 +02:00
DoctorMcKay
a0aee350cc Update comment 2023-06-26 01:37:08 -04:00
3urobeat
c25cb31e34 Re-enable primaryGroup profile setting 2023-05-14 17:06:56 +02:00
fjexe
2a1d244114 Update users.js 2022-11-04 18:48:35 +03:00
14 changed files with 509 additions and 342 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
node_modules/*
node_modules/
test.js
dev/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

View File

@@ -166,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);
};

View File

@@ -175,11 +175,11 @@ SteamCommunity.prototype.postGroupAnnouncement = function(gid, headline, content
"languages[0][headline]": headline,
"languages[0][body]": content
};
if(hidden) {
form.is_hidden = "is_hidden"
}
this.httpRequestPost({
"uri": "https://steamcommunity.com/gid/" + gid.getSteamID64() + "/announcements",
form
@@ -669,7 +669,7 @@ SteamCommunity.prototype.respondToGroupJoinRequests = function(gid, steamIDs, ap
if (typeof gid === 'string') {
gid = new SteamID(gid);
}
var rgAccounts = (!Array.isArray(steamIDs) ? [steamIDs] : steamIDs).map(sid => sid.toString());
this.httpRequestPost({
@@ -730,3 +730,69 @@ SteamCommunity.prototype.respondToAllGroupJoinRequests = function(gid, approve,
}
}, "steamcommunity");
};
/**
* Follows a curator page
* @param {string|number} curatorId - ID of the curator (not a SteamID)
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.followCurator = function(curatorId, callback) {
this.httpRequestPost({
"uri": "https://store.steampowered.com/curators/ajaxfollow",
"form": {
"clanid": curatorId,
"sessionid": this.getSessionID(),
"follow": 1
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success.success));
return;
}
callback(null);
}, "steamcommunity");
};
/**
* Unfollows a curator page
* @param {string|number} curatorId - ID of the curator (not a SteamID)
* @param {function} callback - Takes only an Error object/null as the first argument
*/
SteamCommunity.prototype.unfollowCurator = function(curatorId, callback) {
this.httpRequestPost({
"uri": "https://store.steampowered.com/curators/ajaxfollow",
"form": {
"clanid": curatorId,
"sessionid": this.getSessionID(),
"follow": 0
},
"json": true
}, (err, res, body) => {
if (!callback) {
return;
}
if (err) {
callback(err);
return;
}
if (body.success && body.success.success != SteamCommunity.EResult.OK) {
callback(Helpers.eresultError(body.success.success));
return;
}
callback(null);
}, "steamcommunity");
};

View File

@@ -1,7 +1,9 @@
const EResult = require('../resources/EResult.js');
const request = require('request');
const SteamID = require('steamid');
const xml2js = require('xml2js');
const EResult = require('../resources/EResult.js');
exports.isSteamID = function(input) {
var keys = Object.keys(input);
if (keys.length != 4) {
@@ -99,10 +101,28 @@ exports.resolveVanityURL = function(url, callback) {
return;
}
let steamID64 = parsed.profile.steamID64;
let vanityURL = parsed.profile.customURL;
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);
};

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],
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

@@ -106,6 +106,15 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
values.customURL = settings[i];
break;
case 'primaryGroup':
if(typeof settings[i] === 'object' && settings[i].getSteamID64) {
values.primary_group_steamid = settings[i].getSteamID64();
} else {
values.primary_group_steamid = new SteamID(settings[i]).getSteamID64();
}
break;
// These don't work right now
/*
case 'background':
@@ -117,15 +126,6 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
// Currently, game badges aren't supported
values.favorite_badge_badgeid = settings[i];
break;
case 'primaryGroup':
if(typeof settings[i] === 'object' && settings[i].getSteamID64) {
values.primary_group_steamid = settings[i].getSteamID64();
} else {
values.primary_group_steamid = new SteamID(settings[i]).getSteamID64();
}
break;
*/
// TODO: profile showcases
}

View File

@@ -20,10 +20,10 @@ SteamCommunity.prototype.enableTwoFactor = function(callback) {
// TODO: Send this as protobuf to more closely mimic official app behavior
form: {
steamid: this.steamID.getSteamID64(),
authenticator_time: Math.floor(Date.now() / 1000),
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
device_identifier: SteamTotp.getDeviceID(this.steamID),
sms_phone_id: '1'
sms_phone_id: '1',
version: 2
},
json: true
}, (err, response, body) => {

View File

@@ -296,6 +296,66 @@ SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback)
}, "steamcommunity");
};
SteamCommunity.prototype.followUser = function(userID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
this.httpRequestPost({
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/followuser/`,
"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.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) {
if (typeof userID === 'string') {
userID = new SteamID(userID);
@@ -385,18 +445,7 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
if (!match) {
var errorMessage = "Malformed response";
if(body.match(/0 items in their inventory\./)){
callback(null, {});
return;
}else if(body.match(/inventory is currently private\./)){
errorMessage = "Private inventory";
}else if(body.match(/profile\_private\_info/)){
errorMessage = "Private profile";
}
callback(new Error(errorMessage));
callback(new Error('Malformed response'));
return;
}
@@ -408,6 +457,21 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
return;
}
if (Object.keys(data).length == 0) {
if (body.match(/inventory is currently private\./)) {
callback(new Error('Private inventory'));
return;
}
if (body.match(/profile_private_info/)) {
callback(new Error('Private profile'));
return;
}
// If they truly have no items in their inventory, Steam will send g_rgAppContextData as [] instead of {}.
data = {};
}
callback(null, data);
}, "steamcommunity");
};
@@ -472,7 +536,7 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
continue;
}
currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
}
if (body.more) {
@@ -526,7 +590,7 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
},
"qs": {
"l": language, // Default language
"count": 2000, // Max items per 'page'
"count": 5000, // Max items per 'page'
"start_assetid": start
},
"json": true

View File

@@ -1,49 +1,152 @@
var SteamCommunity = require('../index.js');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this;
/**
* 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(unused, callback) {
if (typeof unused == 'function') {
callback = unused;
}
this.httpRequest({
"uri": "https://steamcommunity.com/dev/apikey?l=english",
"followRedirect": false
}, function(err, response, body) {
uri: 'https://steamcommunity.com/dev/apikey?l=english',
followRedirect: false
}, (err, response, body) => {
if (err) {
callback(err);
return;
}
if(body.match(/<h2>Access Denied<\/h2>/)) {
return callback(new Error("Access Denied"));
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.'));
}
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."));
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'));
}
var match = body.match(/<p>Key: ([0-9A-F]+)<\/p>/);
if(match) {
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
callback(null, match[1]);
} else {
// We need to register a new API key
self.httpRequestPost('https://steamcommunity.com/dev/registerkey?l=english', {
"form": {
"domain": domain,
"agreeToTerms": "agreed",
"sessionid": self.getSessionID(),
"Submit": "Register"
}
}, function(err, response, body) {
if (err) {
callback(err);
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;
}
// 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.
// 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;
}
self.getWebApiKey(domain, callback);
}, "steamcommunity");
// 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));
}
}, "steamcommunity");
});
};
/**

View File

@@ -56,9 +56,9 @@ async function main() {
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 mobile confirmations are only possible with 2FA enabled, but just in case...
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.');
}

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,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') {

178
index.js
View File

@@ -1,12 +1,9 @@
const hex2b64 = require('node-bignumber').hex2b64;
const {chrome} = require('@doctormckay/user-agents');
const Request = require('request');
const RSA = require('node-bignumber').Key;
const SteamID = require('steamid');
const Helpers = require('./components/helpers.js');
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36";
require('util').inherits(SteamCommunity, require('events').EventEmitter);
module.exports = SteamCommunity;
@@ -31,7 +28,7 @@ function SteamCommunity(options) {
"timeout": options.timeout || 50000,
"gzip": true,
"headers": {
"User-Agent": options.userAgent || USER_AGENT
"User-Agent": options.userAgent || chrome()
}
};
@@ -61,158 +58,22 @@ SteamCommunity.prototype.login = function(details, callback) {
throw new Error("Missing either accountName or password to login; both are needed");
}
if (details.steamguard) {
var parts = details.steamguard.split('||');
this._setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), true);
}
var disableMobile = typeof details.disableMobile == 'undefined' ? true : details.disableMobile;
var self = this;
// Delete the cache
delete self._profileURL;
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
var 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, */*"
};
// default disableMobile to true
let logOnOptions = Object.assign({}, details);
logOnOptions.disableMobile = details.disableMobile !== false;
this._setCookie(Request.cookie("mobileClientVersion=0 (2.1.3)"));
this._setCookie(Request.cookie("mobileClient=android"));
} else {
mobileHeaders = {"Referer": "https://steamcommunity.com/login"};
}
this._modernLogin(logOnOptions).then(({sessionID, cookies, steamguard, mobileAccessToken}) => {
this.setCookies(cookies);
this.httpRequestPost("https://steamcommunity.com/login/getrsakey/", {
"form": {"username": details.accountName},
"headers": mobileHeaders,
"json": true
}, function(err, response, body) {
// Remove the mobile cookies
if (err) {
deleteMobileCookies();
callback(err);
return;
if (mobileAccessToken) {
this.setMobileAppAccessToken(mobileAccessToken);
}
if (!body.publickey_mod || !body.publickey_exp) {
deleteMobileCookies();
callback(new Error("Invalid RSA key received"));
return;
}
var key = new RSA();
key.setPublic(body.publickey_mod, body.publickey_exp);
var formObj = {
"captcha_text": details.captcha || "",
"captchagid": self._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";
}
self.httpRequestPost({
"uri": "https://steamcommunity.com/login/dologin/",
"json": true,
"form": formObj,
"headers": mobileHeaders
}, function(err, response, body) {
deleteMobileCookies();
if (err) {
callback(err);
return;
}
var error;
if (!body.success && body.emailauth_needed) {
// Steam Guard (email)
error = new Error("SteamGuard");
error.emaildomain = body.emaildomain;
callback(error);
} else if (!body.success && body.requires_twofactor) {
// Steam Guard (app)
callback(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;
self._captchaGid = body.captcha_gid;
callback(error);
} else if (!body.success) {
callback(new Error(body.message || "Unknown error"));
} else {
var sessionID = generateSessionID();
var oAuth = {};
self._setCookie(Request.cookie('sessionid=' + sessionID));
var cookies = self._jar.getCookieString("https://steamcommunity.com").split(';').map(function(cookie) {
return cookie.trim();
});
if (!disableMobile && body.oauth) {
oAuth = JSON.parse(body.oauth);
self.steamID = new SteamID(oAuth.steamid);
self.oAuthToken = oAuth.oauth_token;
} else {
for(var i = 0; i < cookies.length; i++) {
var parts = cookies[i].split('=');
if(parts[0] == 'steamLogin') {
self.steamID = new SteamID(decodeURIComponent(parts[1]).split('||')[0])
break;
}
}
self.oAuthToken = null;
}
// Find the Steam Guard cookie
var steamguard = null;
for (var i = 0; i < cookies.length; i++) {
var parts = cookies[i].split('=');
if(parts[0] == 'steamMachineAuth' + self.steamID) {
steamguard = self.steamID.toString() + '||' + decodeURIComponent(parts[1]);
break;
}
}
self.setCookies(cookies);
callback(null, sessionID, cookies, steamguard, disableMobile ? null : oAuth.oauth_token);
}
}, "steamcommunity");
}, "steamcommunity");
function deleteMobileCookies() {
var cookie = Request.cookie('mobileClientVersion=');
cookie.expires = new Date(0);
self._setCookie(cookie);
cookie = Request.cookie('mobileClient=');
cookie.expires = new Date(0);
self._setCookie(cookie);
}
callback(null, sessionID, cookies, steamguard, null);
}).catch(err => callback(err));
};
/**
@@ -293,16 +154,20 @@ SteamCommunity.prototype._setCookie = function(cookie, secure) {
var protocol = secure ? "https" : "http";
cookie.secure = !!secure;
this._jar.setCookie(cookie.clone(), protocol + "://steamcommunity.com");
this._jar.setCookie(cookie.clone(), protocol + "://store.steampowered.com");
this._jar.setCookie(cookie.clone(), protocol + "://help.steampowered.com");
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");
}
};
SteamCommunity.prototype.setCookies = function(cookies) {
cookies.forEach((cookie) => {
var 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(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
@@ -576,6 +441,7 @@ SteamCommunity.prototype.getFriendsList = function(callback) {
});
};
require('./components/login.js');
require('./components/http.js');
require('./components/chat.js');
require('./components/profile.js');

View File

@@ -1,7 +1,14 @@
{
"name": "steamcommunity",
"version": "3.46.0",
"version": "3.48.2",
"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"
@@ -21,17 +28,15 @@
"url": "https://github.com/DoctorMcKay/node-steamcommunity.git"
},
"dependencies": {
"@doctormckay/user-agents": "^1.0.0",
"async": "^2.6.3",
"cheerio": "0.22.0",
"image-size": "^0.8.2",
"node-bignumber": "^1.2.1",
"request": "^2.88.0",
"steam-session": "^1.7.2",
"steam-totp": "^1.5.0",
"steamid": "^1.1.3",
"xml2js": "^0.4.22"
},
"devDependencies": {
"steam-session": "^1.2.3"
"xml2js": "^0.6.2"
},
"engines": {
"node": ">=4.0.0"