Compare commits

...

114 Commits

Author SHA1 Message Date
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
Alex Corn
2abc547f49 3.46.0 2023-06-24 03:00:14 -04:00
Alex Corn
b2bffddfbe Fixed wrong owner property 2023-06-24 02:53:15 -04:00
Alex Corn
ab9baeafc6 Return postDate as a Date object 2023-06-24 02:48:26 -04:00
Alex Corn
788f34ba7a Export ESharedFileType 2023-06-24 02:44:12 -04:00
Alex Corn
bde2cafaf4 We never want to return a sharedfile without an owner 2023-06-24 02:43:09 -04:00
Alex Corn
36e8c79d87 Some minor updates 2023-06-24 02:40:48 -04:00
DoctorMcKay
4ca0fc83aa Merge pull request #306 from HerrEurobeat/feature/steam-sharedfiles
Add full sharedfiles support
2023-06-24 02:30:12 -04:00
Alex Corn
2dd5bb490e Added accept_all_confirmations.js example 2023-06-23 18:32:50 -04:00
Alex Corn
d0da148d6b Added comment explaining usage of setMobileAppAccessToken 2023-06-23 01:04:48 -04:00
Alex Corn
7605dc8a41 Added examples readme 2023-06-23 01:02:20 -04:00
Alex Corn
5f46aee9fc Updated enable_twofactor and disable_twofactor examples to use steam-session 2023-06-23 01:00:09 -04:00
Alex Corn
02cc5daafe 3.45.3 2023-06-22 00:24:21 -04:00
Alex Corn
e76d34891a Return a descriptive error message to getConfirmations, if available 2023-06-22 00:13:28 -04:00
Alex Corn
20ad260d68 Remove defunct david-dm badge 2023-06-20 22:04:37 -04:00
Alex Corn
76f2c55a73 3.45.2 2023-06-20 21:54:50 -04:00
Alex Corn
77c2811e7e Use new confirmation tags in acceptConfirmationForObject 2023-06-20 21:54:44 -04:00
Alex Corn
37bac4d240 Make CConfirmation.timestamp a Date object 2023-06-20 21:48:30 -04:00
Alex Corn
d23d7e22c0 3.45.1 2023-06-20 21:42:54 -04:00
Alex Corn
6e19ffd93b Use new endpoints for trade confirmations 2023-06-20 21:42:28 -04:00
3urobeat
e5fbddc00c Merge branch 'master' into feature/steam-sharedfiles 2023-06-15 09:17:35 +00:00
Alex Corn
3531560083 3.45.0 2023-06-14 20:52:40 -04:00
Alex Corn
6fa6a073a8 Updated 2FA methods to use mobile app access token 2023-06-14 20:52:12 -04:00
Alex Corn
fd872490c8 3.44.4 2023-06-14 20:10:14 -04:00
Alex Corn
be528d1a19 Change default value for disableMobile to true 2023-06-14 20:10:00 -04:00
3urobeat
440b2f9ba9 Add JsDoc 2023-05-29 19:29:38 +02:00
3urobeat
e23fa02c91 Add support for reading numRatings of guides 2023-05-28 14:03:01 +02:00
3urobeat
33bc8d83c9 Add support for determining up/downvote status 2023-05-28 13:45:57 +02:00
3urobeat
f96d25ad52 Rename sid to sharedFileId 2023-05-15 22:45:44 +02:00
3urobeat
f7d7cf0660 Remove support for up- & downvoting sharedfiles 2023-05-15 22:42:38 +02:00
3urobeat
27d2daac61 Remove steamid-resolver dep and use internal helper 2023-05-15 22:37:52 +02:00
3urobeat
4723bd93a6 Improve resolveVanityURL() and move to helpers 2023-05-15 22:37:52 +02:00
3urobeat
897ad16154 Formatting 2023-05-14 18:52:52 +02:00
3urobeat
c25cb31e34 Re-enable primaryGroup profile setting 2023-05-14 17:06:56 +02:00
3urobeat
81d8dc2a5c Use decodeSteamTime() helper instead of doing it manually 2023-05-14 16:29:43 +02:00
3urobeat
8538589fef Fix owner remaining null and wrong param 2023-05-14 16:29:42 +02:00
3urobeat
c0be17cd61 Load CSteamSharedfile class 2023-05-14 16:29:42 +02:00
3urobeat
350f6288e4 Add sharedfile object methods 2023-05-14 16:29:42 +02:00
3urobeat
ba2782066d Misc 2023-05-14 16:29:41 +02:00
3urobeat
d6b0fbd5d0 Update non-object methods to take appid param 2023-05-14 16:29:40 +02:00
3urobeat
9ec5dcd7b0 Add sharedfile class with fully working scraper 2023-05-14 16:29:40 +02:00
3urobeat
c8642865a1 Add sharedfile type enum 2023-05-14 16:29:40 +02:00
3urobeat
c92801d25e Load sharedfiles component 2023-05-13 16:09:53 +02:00
3urobeat
b55516f67f Oops, wrong object name 2023-05-13 15:35:46 +02:00
3urobeat
f09adc84a4 Add (disabled) sharedfile favorite support 2023-05-13 15:35:46 +02:00
3urobeat
d807f10653 Add sharedfile subscribing support 2023-05-13 15:35:45 +02:00
3urobeat
18011b3fac Add sharedfile voting support 2023-05-13 15:35:45 +02:00
3urobeat
6f97370710 Add sharedfile comment support 2023-05-13 15:35:45 +02:00
fjexe
2a1d244114 Update users.js 2022-11-04 18:48:35 +03:00
Alex Corn
3bcaf40850 3.44.3 2022-10-28 04:31:42 -04:00
DoctorMcKay
0ccac69bea Merge pull request #300 from misterdudu/patch-1
Fix: New rate limit rule from steam
2022-10-28 04:30:41 -04:00
Eduardo
48d63857e2 Fix: New rate limit rule from steam
Steam added a new rule for inventories. Requesting with a "count" higher than 2000 will ban the proxy after four requests. All requests made with less than 2001 will have a rate limit of around 60/min per proxy.
2022-10-28 04:34:16 -03:00
Alex Corn
b11734cd9b 3.44.2 2022-08-24 21:47:03 -04:00
DoctorMcKay
5c37c38dda Merge pull request #291 from Heartz66/patch-2
Fix Not Logged In
2022-08-24 21:46:46 -04:00
Heartz66
260bfaa0ad Fix Not Logged In
Steam recently changed their login pages. This change should correctly identify the login state.
2022-08-24 23:59:51 +02:00
Alex Corn
1031a4dbd7 3.44.1 2022-05-13 22:49:06 -04:00
Alex Corn
e28fe5ca05 Merge pull request #286 from Heartz66/patch-1
Fix Not Logged In
2022-05-13 22:44:56 -04:00
Heartz66
90a9ee1aa9 Fix Not Logged In
Current version doesn't correctly identify login state. This change should correctly identify the state.
2022-05-13 17:10:52 +02:00
Alex Corn
9a069e80fb 3.44.0 2022-02-02 03:36:23 -05:00
Alex Corn
640c47b933 Updated comment 2022-02-02 03:35:44 -05:00
Alex Corn
f5d65a9ad4 Added getBoosterPackCatalog and createBoosterPack 2022-02-02 03:15:50 -05:00
Alex Corn
7ae8a97a0f Slight refactor for help.js component 2021-07-22 23:58:47 -04:00
Alex Corn
ec1284e961 Make callback optional in removePackage/restorePackage 2021-07-22 03:30:42 -04:00
Alex Corn
dc4bb1b554 3.43.1 2021-07-22 03:25:57 -04:00
Alex Corn
cd90d7a563 Trim comments before returning them 2021-07-22 03:25:47 -04:00
Alex Corn
851c14bd93 3.43.0 2021-07-22 03:09:56 -04:00
Alex Corn
24a42e9658 Export EFriendRelationship with the E prefix 2021-07-22 03:05:23 -04:00
Alex Corn
8ec671d952 Reformat 2021-07-22 03:04:38 -04:00
Alex Corn
2821a5900c Merge pull request #267 from Revadike/feature-get-friends-list
Added getFriendsList
2021-07-22 03:04:16 -04:00
Alex Corn
2da6725283 Merge branch 'master' into feature-get-friends-list 2021-07-22 03:04:06 -04:00
Alex Corn
23a7940064 Merge branch 'master' into feature-get-friends-list 2021-07-22 03:03:24 -04:00
Alex Corn
165c963b1a Create new help component and move restore/removePackage to it 2021-07-22 03:01:57 -04:00
Alex Corn
429a76edcc Reformat 2021-07-22 02:56:03 -04:00
Alex Corn
3a224ff45f Merge pull request #262 from Revadike/feature-package-control
Added restorePackage & removePackage
2021-07-22 02:55:01 -04:00
Alex Corn
dfad525e4e Merge pull request #220 from Revadike/feature/profile-comments
Added deleteUserComment and getUserComments
2021-07-22 02:34:32 -04:00
Revadike
81877f4bb7 Use object.assign instead 2021-07-22 07:48:46 +02:00
Alex Corn
f6a3186865 Merge pull request #268 from Revadike/enums-update
Updated Enums
2021-06-05 02:41:02 -04:00
Revadike
e0687f8828 Updated Enums 2021-06-04 04:24:11 +02:00
Revadike
d54dd2c892 Added getFriendsList 2021-06-04 04:08:07 +02:00
Revadike
b4818d6d67 Bugfix 2021-06-04 03:26:24 +02:00
Revadike
d8eb07f046 re-use variable
Co-authored-by: Mr-VIT <vit@v1t.su>
2021-05-13 01:14:52 +02:00
Revadike
e6995e337e Added restorePackage & removePackage 2021-03-19 15:22:41 +01:00
Alex Corn
2bbdbf36ad 3.42.0 2020-11-05 02:55:53 -05:00
Alex Corn
e6311c0fcb Removed attempt to get oauth token from webchat 2020-11-05 02:42:31 -05:00
Alex Corn
2fc8d1f4aa Refactored gem sack code 2020-11-05 02:19:31 -05:00
Jack Nolddor
b00b871a5b Added gems/sack transformations (#260)
Added Sack of Gems <---> Gems transformations
2020-11-05 01:31:35 -05:00
Alex Corn
15079621d2 3.41.8 2020-10-03 04:54:58 -04:00
Alex Corn
d91c600eae Merge pull request #258 from theanht1/master
Fix parentalUnlock
2020-10-02 02:17:35 -04:00
Anh Mai
0f98403c16 feat: Add sessionid to form of parentalUnlock 2020-09-30 17:43:00 +07:00
Revadike
9ed377e843 Oopsy 2019-04-05 00:13:02 +02:00
Revadike
868ce9cdb4 Added missing functions 2019-04-05 00:10:00 +02:00
Revadike
8edc5020e1 Added getUserComments(userID, options, callback)
https://github.com/DoctorMcKay/node-steamcommunity/issues/208
2019-04-05 00:06:48 +02:00
Revadike
f2be027a63 Updated postUserComment and added deleteUserComment
https://github.com/DoctorMcKay/node-steamcommunity/issues/219
2019-04-04 23:30:28 +02:00
34 changed files with 1975 additions and 553 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

@@ -1,8 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />

View File

@@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

3
.idea/modules.xml generated
View File

@@ -3,6 +3,7 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/steamcommunity.iml" filepath="$PROJECT_DIR$/.idea/steamcommunity.iml" />
<module fileurl="file://$PROJECT_DIR$/../steamcommunity Wiki/.idea/steamcommunity Wiki.iml" filepath="$PROJECT_DIR$/../steamcommunity Wiki/.idea/steamcommunity Wiki.iml" />
</modules>
</component>
</project>
</project>

View File

@@ -5,5 +5,6 @@
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="steamcommunity Wiki" />
</component>
</module>
</module>

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../steamcommunity Wiki" vcs="Git" />
</component>
</project>

View File

@@ -1,7 +1,6 @@
# Steam Community for Node.js
[![npm version](https://img.shields.io/npm/v/steamcommunity.svg)](https://npmjs.com/package/steamcommunity)
[![npm downloads](https://img.shields.io/npm/dm/steamcommunity.svg)](https://npmjs.com/package/steamcommunity)
[![dependencies](https://img.shields.io/david/DoctorMcKay/node-steamcommunity.svg)](https://david-dm.org/DoctorMcKay/node-steamcommunity)
[![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)

View File

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

221
classes/CSteamSharedFile.js Normal file
View File

@@ -0,0 +1,221 @@
const Cheerio = require('cheerio');
const SteamID = require('steamid');
const SteamCommunity = require('../index.js');
const Helpers = require('../components/helpers.js');
const ESharedFileType = require('../resources/ESharedFileType.js');
/**
* Scrape a sharedfile's DOM to get all available information
* @param {string} sharedFileId - ID of the sharedfile
* @param {function} callback - First argument is null/Error, second is object containing all available information
*/
SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) {
// Construct object holding all the data we can scrape
let sharedfile = {
id: sharedFileId,
type: null,
appID: null,
owner: null,
fileSize: null,
postDate: null,
resolution: null,
uniqueVisitorsCount: null,
favoritesCount: null,
upvoteCount: null,
guideNumRatings: null,
isUpvoted: null,
isDownvoted: null
};
// Get DOM of sharedfile
this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, (err, res, body) => {
try {
/* --------------------- Preprocess output --------------------- */
// Load output into cheerio to make parsing easier
let $ = Cheerio.load(body);
// Dynamically map detailsStatsContainerLeft to detailsStatsContainerRight in an object to make readout easier. It holds size, post date and resolution.
let detailsStatsObj = {};
let detailsLeft = $(".detailsStatsContainerLeft").children();
let detailsRight = $(".detailsStatsContainerRight").children();
Object.keys(detailsLeft).forEach((e) => { // Dynamically get all details. Don't hardcore so that this also works for guides.
if (isNaN(e)) {
return; // Ignore invalid entries
}
detailsStatsObj[detailsLeft[e].children[0].data.trim()] = detailsRight[e].children[0].data;
});
// Dynamically map stats_table descriptions to values. This holds Unique Visitors and Current Favorites
let statsTableObj = {};
let statsTable = $(".stats_table").children();
Object.keys(statsTable).forEach((e) => {
if (isNaN(e)) {
return; // Ignore invalid entries
}
// Value description is at index 3, value data at index 1
statsTableObj[statsTable[e].children[3].children[0].data] = statsTable[e].children[1].children[0].data.replace(/,/g, ""); // Remove commas from 1k+ values
});
/* --------------------- Find and map values --------------------- */
// Find appID in share button onclick event
sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sharedFileId}', '`, "").replace("' );", ""));
// Find fileSize if not guide
sharedfile.fileSize = detailsStatsObj["File Size"] || null; // TODO: Convert to bytes? It seems like to always be MB but no guarantee
// Find postDate and convert to timestamp
let posted = detailsStatsObj["Posted"].trim();
sharedfile.postDate = Helpers.decodeSteamTime(posted);
// Find resolution if artwork or screenshot
sharedfile.resolution = detailsStatsObj["Size"] || null;
// Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false
if (statsTableObj["Unique Visitors"]) {
sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]);
}
// Find favoritesCount. We can't use ' || null' here as Number("0") casts to false
if (statsTableObj["Current Favorites"]) {
sharedfile.favoritesCount = Number(statsTableObj["Current Favorites"]);
}
// Find upvoteCount. We can't use ' || null' here as Number("0") casts to false
let upvoteCount = $("#VotesUpCountContainer > #VotesUpCount").text();
if (upvoteCount) {
sharedfile.upvoteCount = Number(upvoteCount);
}
// Find numRatings if this is a guide as they use a different voting system
let numRatings = $(".ratingSection > .numRatings").text().replace(" ratings", "");
sharedfile.guideNumRatings = Number(numRatings) || null; // Set to null if not a guide or if the guide does not have enough ratings to show a value
// Determine if this account has already voted on this sharedfile
sharedfile.isUpvoted = String($(".workshopItemControlCtn > #VoteUpBtn")[0].attribs["class"]).includes("toggled"); // Check if upvote btn class contains "toggled"
sharedfile.isDownvoted = String($(".workshopItemControlCtn > #VoteDownBtn")[0].attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled"
// Determine type by looking at the second breadcrumb. Find the first separator as it has a unique name and go to the next element which holds our value of interest
let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || "";
if (breadcrumb.includes("Screenshot")) {
sharedfile.type = ESharedFileType.Screenshot;
}
if (breadcrumb.includes("Artwork")) {
sharedfile.type = ESharedFileType.Artwork;
}
if (breadcrumb.includes("Guide")) {
sharedfile.type = ESharedFileType.Guide;
}
// Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object
let ownerHref = $(".friendBlockLinkOverlay").attr()["href"];
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));
});
} catch (err) {
callback(err, null);
}
}, "steamcommunity");
};
/**
* Constructor - Creates a new SharedFile object
* @class
* @param {SteamCommunity} community
* @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data
*/
function CSteamSharedFile(community, data) {
/**
* @type {SteamCommunity}
*/
this._community = community;
// Clone all the data we received
Object.assign(this, data);
}
/**
* Deletes a comment from this sharedfile's comment section
* @param {String} cid - ID of the comment to delete
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamSharedFile.prototype.deleteComment = function(cid, callback) {
this._community.deleteSharedFileComment(this.owner, this.id, cid, callback);
};
/**
* Favorites this sharedfile
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamSharedFile.prototype.favorite = function(callback) {
this._community.favoriteSharedFile(this.id, this.appID, callback);
};
/**
* Posts a comment to this sharedfile
* @param {String} message - Content of the comment to post
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamSharedFile.prototype.comment = function(message, callback) {
this._community.postSharedFileComment(this.owner, this.id, message, callback);
};
/**
* Subscribes to this sharedfile's comment section. Note: Checkbox on webpage does not update
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamSharedFile.prototype.subscribe = function(callback) {
this._community.subscribeSharedFileComments(this.owner, this.id, callback);
};
/**
* Unfavorites this sharedfile
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamSharedFile.prototype.unfavorite = function(callback) {
this._community.unfavoriteSharedFile(this.id, this.appID, callback);
};
/**
* Unsubscribes from this sharedfile's comment section. Note: Checkbox on webpage does not update
* @param {function} callback - Takes only an Error object/null as the first argument
*/
CSteamSharedFile.prototype.unsubscribe = function(callback) {
this._community.unsubscribeSharedFileComments(this.owner, this.id, callback);
};

View File

@@ -154,10 +154,26 @@ CSteamUser.prototype.comment = function(message, callback) {
this._community.postUserComment(this.steamID, message, callback);
};
CSteamUser.prototype.deleteComment = function(commentID, callback) {
this._community.deleteUserComment(this.steamID, commentID, callback);
};
CSteamUser.prototype.getComments = function(options, callback) {
this._community.getUserComments(this.steamID, options, callback);
};
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

@@ -4,63 +4,55 @@ var SteamTotp = require('steam-totp');
var Async = require('async');
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 {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) {
if (err.message == "Invalid protocol: steammobile:") {
err.message = "Not Logged In";
self._notifySessionExpired(err);
}
// 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;
}
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, []);
if (!body.success) {
if (body.needauth) {
var err = new Error('Not Logged In');
self._notifySessionExpired(err);
callback(err);
return;
}
callback(new Error(body.message || body.detail || 'Failed to get confirmation list'));
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'),
"type": conf.data('type'),
"creator": conf.data('creator'),
"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')
}));
});
var confs = (body.conf || []).map(conf => new CConfirmation(self, {
id: conf.id,
type: conf.type,
creator: conf.creator_id,
key: conf.nonce,
title: `${conf.type_name || 'Confirm'} - ${conf.headline || ''}`,
receiving: conf.type == EConfirmationType.Trade ? ((conf.summary || [])[1] || '') : '',
sending: (conf.summary || [])[0] || '',
time: (new Date(conf.creation_time * 1000)).toISOString(), // for backward compatibility
timestamp: new Date(conf.creation_time * 1000),
icon: conf.icon || ''
}));
callback(null, confs);
});
@@ -76,22 +68,23 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
* 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 {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
*/
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
request(this, "details/" + confID, key, time, "details", null, true, function(err, body) {
if(err) {
// 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(!body.success) {
if (typeof body != 'string') {
callback(new Error("Cannot load confirmation details"));
return;
}
var $ = Cheerio.load(body.html);
var $ = Cheerio.load(body);
var offer = $('.tradeoffer');
if(offer.length < 1) {
callback(null, null);
@@ -118,31 +111,39 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca
* @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete
*/
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
request(this, (confID instanceof Array) ? "multiajaxop" : "ajaxop", key, time, accept ? "allow" : "cancel", {
"op": accept ? "allow" : "cancel",
"cid": confID,
"ck": confKey
// 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
request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
op: accept ? 'allow' : 'cancel',
cid: confID,
ck: confKey
}, true, function(err, body) {
if(!callback) {
if (!callback) {
return;
}
if(err) {
if (err) {
callback(err);
return;
}
if(body.success) {
if (body.success) {
callback(null);
return;
}
if(body.message) {
if (body.message) {
callback(new Error(body.message));
return;
}
callback(new Error("Could not act on confirmation"));
callback(new Error('Could not act on confirmation'));
});
};
@@ -179,7 +180,8 @@ SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret,
function doConfirmation() {
var offset = self._timeOffset;
var time = SteamTotp.time(offset);
self.getConfirmations(time, SteamTotp.getConfirmationKey(identitySecret, time, "conf"), function(err, confs) {
var confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
self.getConfirmations(time, {tag: 'list', key: confKey}, function(err, confs) {
if (err) {
callback(err);
return;
@@ -187,7 +189,7 @@ SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret,
var conf = confs.filter(function(conf) { return conf.creator == objectID; });
if (conf.length == 0) {
callback(new Error("Could not find confirmation for object " + objectID));
callback(new Error('Could not find confirmation for object ' + objectID));
return;
}
@@ -204,7 +206,8 @@ SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret,
self._usedConfTimes.splice(0, self._usedConfTimes.length - 60); // we don't need to save more than 60 entries
}
conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, "allow"), true, callback);
confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'accept');
conf.respond(time, {tag: 'accept', key: confKey}, true, callback);
});
}
};
@@ -244,7 +247,7 @@ SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowK
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");
throw new Error('Must be logged in before trying to do anything with confirmations');
}
params = params || {};
@@ -252,16 +255,16 @@ function request(community, url, key, time, tag, params, json, callback) {
params.a = community.steamID.getSteamID64();
params.k = key;
params.t = time;
params.m = "android";
params.m = 'react';
params.tag = tag;
var req = {
"method": url == 'multiajaxop' ? 'POST' : 'GET',
"uri": "https://steamcommunity.com/mobileconf/" + url,
"json": !!json
method: url == 'multiajaxop' ? 'POST' : 'GET',
uri: 'https://steamcommunity.com/mobileconf/' + url,
json: !!json
};
if (req.method == "GET") {
if (req.method == 'GET') {
req.qs = params;
} else {
req.form = params;
@@ -274,7 +277,7 @@ function request(community, url, key, time, tag, params, json, callback) {
}
callback(null, body);
}, "steamcommunity");
}, 'steamcommunity');
}
// Confirmation checker

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");
};

64
components/help.js Normal file
View File

@@ -0,0 +1,64 @@
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
*/
SteamCommunity.prototype.restorePackage = function(packageID, callback) {
this.httpRequestPost({
uri: HELP_SITE_DOMAIN + '/wizard/AjaxDoPackageRestore',
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
json: true
}, wizardAjaxHandler(callback));
};
/**
* Remove a steam package from your steam account.
* @param {int|string} packageID
* @param {function} callback
*/
SteamCommunity.prototype.removePackage = function(packageID, callback) {
this.httpRequestPost({
uri: HELP_SITE_DOMAIN + '/wizard/AjaxDoPackageRemove',
form: {
packageid: packageID,
sessionid: this.getSessionID(HELP_SITE_DOMAIN),
wizard_ajax: 1
},
json: true
}, wizardAjaxHandler(callback));
};
/**
* Returns a handler for wizard ajax HTTP requests.
* @param {function} callback
* @returns {(function(*=, *, *): void)|*}
*/
function wizardAjaxHandler(callback) {
return (err, res, body) => {
if (!callback) {
return;
}
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,3 +1,7 @@
const request = require('request');
const SteamID = require('steamid');
const xml2js = require('xml2js');
const EResult = require('../resources/EResult.js');
exports.isSteamID = function(input) {
@@ -54,3 +58,71 @@ exports.eresultError = function(eresult) {
err.eresult = eresult;
return err;
};
exports.decodeJwt = function(jwt) {
let parts = jwt.split('.');
if (parts.length != 3) {
throw new Error('Invalid JWT');
}
let standardBase64 = parts[1].replace(/-/g, '+')
.replace(/_/g, '/');
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

@@ -124,7 +124,7 @@ SteamCommunity.prototype._checkCommunityError = function(html, callback) {
return err;
}
if (typeof html === 'string' && html.match(/g_steamID = false;/) && html.match(/<h1>Sign In<\/h1>/)) {
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);

View File

@@ -1,5 +1,6 @@
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');
@@ -142,7 +143,7 @@ SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
}
if (options.resolveVanityURLs) {
Async.map(vanityURLs, resolveVanityURL, function(err, results) {
Async.map(vanityURLs, Helpers.resolveVanityURL, function(err, results) {
if (err) {
callback(err);
return;
@@ -170,19 +171,3 @@ SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
}, "steamcommunity");
};
function resolveVanityURL(vanityURL, callback) {
request("https://steamcommunity.com/id/" + vanityURL + "/?xml=1", function(err, response, body) {
if (err) {
callback(err);
return;
}
var match = body.match(/<steamID64>(\d+)<\/steamID64>/);
if (!match || !match[1]) {
callback(new Error("Couldn't find Steam ID"));
return;
}
callback(null, {"vanityURL": vanityURL, "steamID": match[1]});
});
}

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

@@ -1,5 +1,7 @@
var SteamCommunity = require('../index.js');
var Cheerio = require('cheerio');
const SteamCommunity = require('../index.js');
const Cheerio = require('cheerio');
const Helpers = require('./helpers.js');
/**
* Get a list of all apps on the market
@@ -146,6 +148,124 @@ SteamCommunity.prototype.openBoosterPack = function(appid, assetid, callback) {
})
};
/**
* Get the booster pack catalog to see what booster packs you can create
* @param {function} callback
*/
SteamCommunity.prototype.getBoosterPackCatalog = function(callback) {
this.httpRequestGet('https://steamcommunity.com/tradingcards/boostercreator/', (err, res, body) => {
if (err) {
callback(err);
return;
}
let idx = body.indexOf('CBoosterCreatorPage.Init(');
if (idx == -1) {
callback(new Error('Malformed response'));
return;
}
let lines = body.slice(idx).split('\n').map(l => l.trim());
for (let i = 1; i <= 4; i++) {
if (typeof lines[i] != 'string' || !lines[i].match(/,$/)) {
let err = new Error('Malformed response');
err.line = i;
callback(err);
return;
}
lines[i] = lines[i].replace(/,$/, '');
}
let boosterPackCatalog, totalGems, tradableGems, untradableGems;
try {
boosterPackCatalog = JSON.parse(lines[1]);
totalGems = parseInt(lines[2].match(/\d+/)[0], 10);
tradableGems = parseInt(lines[3].match(/\d+/)[0], 10);
untradableGems = parseInt(lines[4].match(/\d+/)[0], 10);
} catch (ex) {
let err = new Error('Malformed response');
err.inner = ex;
callback(err);
return;
}
let keyedCatalog = {};
boosterPackCatalog.forEach((app) => {
app.price = parseInt(app.price, 10);
app.unavailable = app.unavailable || false;
app.availableAtTime = app.available_at_time || null;
if (typeof app.availableAtTime == 'string') {
app.availableAtTime = Helpers.decodeSteamTime(app.availableAtTime);
}
delete app.available_at_time;
keyedCatalog[app.appid] = app;
});
callback(null, {
totalGems,
tradableGems,
untradableGems,
catalog: keyedCatalog
});
});
};
/**
* Create a booster pack using gems.
* @param {int} appid
* @param {boolean} [useUntradableGems=false]
* @param callback
*/
SteamCommunity.prototype.createBoosterPack = function(appid, useUntradableGems, callback) {
if (typeof useUntradableGems == 'function') {
callback = useUntradableGems;
useUntradableGems = false;
}
this.httpRequestPost({
uri: 'https://steamcommunity.com/tradingcards/ajaxcreatebooster/',
form: {
sessionid: this.getSessionID(),
appid,
series: 1,
// tradability_preference can be a value 1-3
// 1: Prefer using tradable gems, but use untradable if necessary
// 2: Only use tradable gems
// 3: Prefer using untradable gems, but use tradable if necessary
tradability_preference: useUntradableGems ? 3 : 2
},
json: true,
checkHttpError: false
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
if (body.purchase_eresult && body.purchase_eresult != 1) {
callback(Helpers.eresultError(body.purchase_eresult));
return;
}
// We can now check HTTP status codes
if (this._checkHttpError(err, res, callback, body)) {
return;
}
callback(null, {
totalGems: parseInt(body.goo_amount, 10),
tradableGems: parseInt(body.tradable_goo_amount, 10),
untradableGems: parseInt(body.untradable_goo_amount, 10),
resultItem: body.purchase_result
});
});
};
/**
* Get details about a gift in your inventory.
* @param {string} giftID
@@ -203,6 +323,7 @@ SteamCommunity.prototype.redeemGift = function(giftID, callback) {
}
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);
@@ -212,3 +333,55 @@ SteamCommunity.prototype.redeemGift = function(giftID, callback) {
callback(null);
});
};
/**
* @param {int|string} assetid
* @param {int} denominationIn
* @param {int} denominationOut
* @param {int} quantityIn
* @param {int} quantityOut
* @param {function} callback
* @private
*/
SteamCommunity.prototype._gemExchange = function(assetid, denominationIn, denominationOut, quantityIn, quantityOut, callback) {
this._myProfile({
endpoint: 'ajaxexchangegoo/',
json: true,
checkHttpError: false
}, {
appid: 753,
assetid,
goo_denomination_in: denominationIn,
goo_amount_in: quantityIn,
goo_denomination_out: denominationOut,
goo_amount_out_expected: quantityOut,
sessionid: this.getSessionID()
}, (err, res, body) => {
if (err) {
callback(err);
return;
}
callback(Helpers.eresultError(body.success));
});
};
/**
* Pack gems into sack of gems.
* @param {int|string} assetid - ID of gem stack you want to pack into sacks
* @param {int} desiredSackCount - How many sacks you want. You must have at least this amount * 1000 gems in the stack you're packing
* @param {function} callback
*/
SteamCommunity.prototype.packGemSacks = function(assetid, desiredSackCount, callback) {
this._gemExchange(assetid, 1, 1000, desiredSackCount * 1000, desiredSackCount, callback);
};
/**
* Unpack sack of gems into gems.
* @param {int|string} assetid - ID of sack stack you want to unpack (say that 5 times fast)
* @param {int} sacksToUnpack
* @param {function} callback
*/
SteamCommunity.prototype.unpackGemSacks = function(assetid, sacksToUnpack, callback) {
this._gemExchange(assetid, 1000, 1, sacksToUnpack, sacksToUnpack * 1000, callback);
};

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
}

158
components/sharedfiles.js Normal file
View File

@@ -0,0 +1,158 @@
var SteamID = require('steamid');
var SteamCommunity = require('../index.js');
/**
* Deletes a comment from a sharedfile's comment section
* @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
*/
SteamCommunity.prototype.deleteSharedFileComment = function(userID, sharedFileId, cid, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
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;
}
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
*/
SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, callback) {
this.httpRequestPost({
"uri": "https://steamcommunity.com/sharedfiles/favorite",
"form": {
"id": sharedFileId,
"appid": appid,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* Posts a comment to a sharedfile
* @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
*/
SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
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;
}
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
*/
SteamCommunity.prototype.subscribeSharedFileComments = function(userID, sharedFileId, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
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;
}
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
*/
SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, callback) {
this.httpRequestPost({
"uri": "https://steamcommunity.com/sharedfiles/unfavorite",
"form": {
"id": sharedFileId,
"appid": appid,
"sessionid": this.getSessionID()
}
}, function(err, response, body) {
if (!callback) {
return;
}
callback(err);
}, "steamcommunity");
};
/**
* Unsubscribes from 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
*/
SteamCommunity.prototype.unsubscribeSharedFileComments = function(userID, sharedFileId, callback) {
if (typeof userID === "string") {
userID = new SteamID(userID);
}
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;
}
callback(err);
}, "steamcommunity");
};

View File

@@ -2,158 +2,151 @@ var SteamTotp = require('steam-totp');
var SteamCommunity = require('../index.js');
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.
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.
};
SteamCommunity.prototype.enableTwoFactor = function(callback) {
var self = this;
this._verifyMobileAccessToken();
this.getWebApiOauthToken(function(err, token) {
if(err) {
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;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/",
"form": {
"steamid": self.steamID.getSteamID64(),
"access_token": token,
"authenticator_time": Math.floor(Date.now() / 1000),
"authenticator_type": ETwoFactorTokenType.ValveMobileApp,
"device_identifier": SteamTotp.getDeviceID(self.steamID),
"sms_phone_id": "1"
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
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;
}
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");
});
callback(null, body.response);
}, 'steamcommunity');
};
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
var attemptsLeft = 30;
var diff = 0;
this._verifyMobileAccessToken();
var self = this;
this.getWebApiOauthToken(function(err, token) {
if(err) {
callback(err);
return;
}
if (!this.mobileAccessToken) {
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
return;
}
SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
callback(err);
return;
}
let attemptsLeft = 30;
let diff = 0;
diff = offset;
finalize(token);
});
});
let finalize = () => {
let code = SteamTotp.generateAuthCode(secret, diff);
function finalize(token) {
var code = SteamTotp.generateAuthCode(secret, diff);
self.httpRequestPost({
"uri": "https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/",
"form": {
"steamid": self.steamID.getSteamID64(),
"access_token": token,
"authenticator_code": code,
"authenticator_time": Math.floor(Date.now() / 1000),
"activation_code": activationCode
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),
activation_code: activationCode
},
"json": true
json: true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.response) {
callback(new Error("Malformed response"));
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
body = body.response;
if(body.server_time) {
if (body.server_time) {
diff = body.server_time - Math.floor(Date.now() / 1000);
}
if(body.status == 89) {
callback(new Error("Invalid activation code"));
if (body.status == 89) {
callback(new Error('Invalid activation code'));
} else if(body.want_more) {
attemptsLeft--;
diff += 30;
finalize(token);
finalize();
} else if(!body.success) {
callback(new Error("Error " + body.status));
callback(new Error('Error ' + body.status));
} else {
callback(null);
}
}, "steamcommunity");
}, 'steamcommunity');
}
};
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
var self = this;
this.getWebApiOauthToken(function(err, token) {
if(err) {
SteamTotp.getTimeOffset(function(err, offset, latency) {
if (err) {
callback(err);
return;
}
self.httpRequestPost({
"uri": "https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/",
"form": {
"steamid": self.steamID.getSteamID64(),
"access_token": token,
"revocation_code": revocationCode,
"steamguard_scheme": 1
},
"json": true
}, function(err, response, body) {
if (err) {
callback(err);
return;
}
if(!body.response) {
callback(new Error("Malformed response"));
return;
}
if(!body.response.success) {
callback(new Error("Request failed"));
return;
}
// success = true means it worked
callback(null);
}, "steamcommunity");
diff = offset;
finalize();
});
};
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
this._verifyMobileAccessToken();
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;
}
if (!body.response) {
callback(new Error('Malformed response'));
return;
}
if (!body.response.success) {
callback(new Error('Request failed'));
return;
}
// success = true means it worked
callback(null);
}, 'steamcommunity');
};

View File

@@ -136,7 +136,7 @@ SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
"uri": "https://steamcommunity.com/comment/Profile/post/" + userID.toString() + "/-1",
"form": {
"comment": message,
"count": 6,
"count": 1,
"sessionid": this.getSessionID()
},
"json": true
@@ -151,9 +151,109 @@ SteamCommunity.prototype.postUserComment = function(userID, message, callback) {
}
if(body.success) {
const $ = Cheerio.load(body.comments_html);
const commentID = $('.commentthread_comment').attr('id').split('_')[1];
callback(null, commentID);
} else if(body.error) {
callback(new Error(body.error));
} else {
callback(new Error("Unknown error"));
}
}, "steamcommunity");
};
SteamCommunity.prototype.deleteUserComment = function(userID, commentID, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
var self = this;
this.httpRequestPost({
"uri": "https://steamcommunity.com/comment/Profile/delete/" + userID.toString() + "/-1",
"form": {
"gidcomment": commentID,
"start": 0,
"count": 1,
"sessionid": this.getSessionID(),
"feature2": -1
},
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
if (err) {
callback(err);
return;
}
if(body.success && !body.comments_html.includes(commentID)) {
callback(null);
} else if(body.error) {
callback(new Error(body.error));
} else if(body.comments_html.includes(commentID)) {
callback(new Error("Failed to delete comment"));
} else {
callback(new Error("Unknown error"));
}
}, "steamcommunity");
};
SteamCommunity.prototype.getUserComments = function(userID, options, callback) {
if(typeof userID === 'string') {
userID = new SteamID(userID);
}
if (typeof options === 'function') {
callback = options;
options = {};
}
var form = Object.assign({
"start": 0,
"count": 0,
"feature2": -1,
"sessionid": this.getSessionID()
}, options);
this.httpRequestPost({
"uri": "https://steamcommunity.com/comment/Profile/render/" + userID.toString() + "/-1",
"form": form,
"json": true
}, function(err, response, body) {
if(!callback) {
return;
}
if (err) {
callback(err);
return;
}
if(body.success) {
const $ = Cheerio.load(body.comments_html);
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],
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()
},
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) {
callback(new Error(body.error));
} else {
callback(new Error("Unknown error"));
}
@@ -196,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);
@@ -285,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;
}
@@ -308,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");
};
@@ -372,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) {
@@ -426,7 +590,7 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
},
"qs": {
"l": language, // Default language
"count": 5000, // Max items per 'page'
"count": 2000, // Max items per 'page'
"start_assetid": start
},
"json": true

View File

@@ -1,73 +1,221 @@
var SteamCommunity = require('../index.js');
const SteamCommunity = require('../index.js');
const Helpers = require('./helpers.js');
/**
* 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;
}
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
var self = this;
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);
return;
}
self.getWebApiKey(domain, callback);
}, "steamcommunity");
callback(new Error('No API key created for this account'));
}
}, "steamcommunity");
};
/**
* @deprecated No longer works if not logged in via mobile login. Will be removed in a future release.
* @param {function} callback
* @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.
*/
SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
var self = this;
if( this.oAuthToken ) {
return callback( null, this.oAuthToken );
/**
* @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;
}
// Pull an oauth token from the webchat UI
this.httpRequest("https://steamcommunity.com/chat", function(err, response, body) {
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;
}
var match = body.match(/"([0-9a-f]{32})"/);
if (!match) {
callback(new Error("Malformed response"));
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.
callback(null, match[1]);
}, "steamcommunity");
// 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.
* This will throw an Error if the provided token is not valid, was not generated for the MobileApp platform, is expired,
* or does not belong to the logged-in user account.
*
* @param {string} token
*/
SteamCommunity.prototype.setMobileAppAccessToken = function(token) {
if (!this.steamID) {
throw new Error('Log on to steamcommunity before setting a mobile app access token');
}
let decodedToken = Helpers.decodeJwt(token);
if (!decodedToken.iss || !decodedToken.sub || !decodedToken.aud || !decodedToken.exp) {
throw new Error('Provided value is not a valid Steam access token');
}
if (decodedToken.iss == 'steam') {
throw new Error('Provided token is a refresh token, not an access token');
}
if (decodedToken.sub != this.steamID.getSteamID64()) {
throw new Error(`Provided token belongs to account ${decodedToken.sub}, but we are logged into ${this.steamID.getSteamID64()}`);
}
if (decodedToken.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Provided token is expired');
}
if ((decodedToken.aud || []).indexOf('mobile') == -1) {
throw new Error('Provided token is not valid for MobileApp platform type');
}
this.mobileAccessToken = token;
};
/**
* Verifies that the mobile access token we already have set is still valid for current login.
*
* @private
*/
SteamCommunity.prototype._verifyMobileAccessToken = function() {
if (!this.mobileAccessToken) {
// No access token, so nothing to do here.
return;
}
let decodedToken = Helpers.decodeJwt(this.mobileAccessToken);
let isTokenInvalid = decodedToken.sub != this.steamID.getSteamID64() // SteamID doesn't match
|| decodedToken.exp < Math.floor(Date.now() / 1000); // Token is expired
if (isTokenInvalid) {
delete this.mobileAccessToken;
}
};

35
examples/README.md Normal file
View File

@@ -0,0 +1,35 @@
# node-steamcommunity examples
The files in this directory are example scripts that you can use as a getting-started point for using node-steamcommunity.
## Enable or Disable Two-Factor Authentication
If you need to enable or disable 2FA on your bot account, you can use enable_twofactor.js and disable_twofactor.js to do so.
The way that you're intended to use these scripts is by cloning the repository locally, and then running them directly
from this examples directory.
For example:
```shell
git clone https://github.com/DoctorMcKay/node-steamcommunity node-steamcommunity
cd node-steamcommunity
npm install
cd examples
node enable_twofactor.js
```
## Accept All Confirmations
If you need to accept trade or market confirmations on your bot account for which you have your identity secret, you can
use accept_all_confirmations.js to do so. The way that you're intended to use this script is by cloning the repository
locally, and then running it directly from this examples directory.
For example:
```shell
git clone https://github.com/DoctorMcKay/node-steamcommunity node-steamcommunity
cd node-steamcommunity
npm install
cd examples
node accept_all_confirmations.js
```

View File

@@ -0,0 +1,173 @@
// 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');
const EConfirmationType = SteamCommunity.ConfirmationType;
let g_AbortPromptFunc = null;
let community = new SteamCommunity();
main();
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);
// Go ahead and attach our event handlers before we do anything else.
session.on('authenticated', async () => {
abortPrompt();
let cookies = await session.getWebCookies();
community.setCookies(cookies);
doConfirmations();
});
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 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 {
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
}
let code = await promptAsync('Code or Shared Secret: ');
if (code) {
// The code might've been a shared secret
if (code.length > 10) {
code = SteamTotp.getAuthCode(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 doConfirmations() {
let identitySecret = await promptAsync('Identity Secret: ');
let confs = await new Promise((resolve, reject) => {
let time = SteamTotp.time();
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'conf');
community.getConfirmations(time, key, (err, confs) => {
if (err) {
return reject(err);
}
resolve(confs);
});
});
console.log(`Found ${confs.length} outstanding confirmations.`);
// We need to track the previous timestamp we used, as we cannot reuse timestamps.
let previousTime = 0;
for (let i = 0; i < confs.length; i++) {
let conf = confs[i];
process.stdout.write(`Accepting confirmation for ${EConfirmationType[conf.type]} - ${conf.title}... `);
try {
await new Promise((resolve, reject) => {
let time = SteamTotp.time();
if (time == previousTime) {
time++;
}
previousTime = time;
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'allow');
conf.respond(time, key, true, (err) => {
err ? reject(err) : resolve();
});
});
console.log('success');
} catch (ex) {
console.log(`error: ${ex.message}`);
}
// sleep 500ms so we don't run too far away from the current timestamp
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log('Finished processing confirmations');
process.exit(0);
}
// Nothing interesting below here, just code for prompting for input from the console.
function promptAsync(question, sensitiveInput = false) {
return new Promise((resolve) => {
let rl = ReadLine.createInterface({
input: process.stdin,
output: sensitiveInput ? null : process.stdout,
terminal: true
});
g_AbortPromptFunc = () => {
rl.close();
resolve('');
};
if (sensitiveInput) {
// We have to write the question manually if we didn't give readline an output stream
process.stdout.write(question);
}
rl.question(question, (result) => {
if (sensitiveInput) {
// We have to manually print a newline
process.stdout.write('\n');
}
g_AbortPromptFunc = null;
rl.close();
resolve(result);
});
});
}
function abortPrompt() {
if (!g_AbortPromptFunc) {
return;
}
g_AbortPromptFunc();
process.stdout.write('\n');
}

View File

@@ -1,62 +1,98 @@
// 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 SteamTotp = require('steam-totp');
const ReadLine = require('readline');
let g_AbortPromptFunc = null;
let community = new SteamCommunity();
let rl = ReadLine.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Username: ', (accountName) => {
rl.question('Password: ', (password) => {
rl.question('Two-Factor Auth Code: ', (authCode) =>{
rl.question('Revocation Code: R', (rCode) => {
doLogin(accountName, password, authCode, '', rCode);
});
});
});
});
main();
async function main() {
let accountName = await promptAsync('Username: ');
let password = await promptAsync('Password (hidden): ', true);
function doLogin(accountName, password, authCode, captcha, rCode) {
attemptLogin(accountName, password);
}
function attemptLogin(accountName, password, twoFactorCode) {
community.login({
accountName: accountName,
password: password,
twoFactorCode: authCode,
captcha: captcha
}, (err, sessionID, cookies, steamguard) => {
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;
}
if (err) {
if (err.message == 'SteamGuard') {
console.log('This account does not have two-factor authentication enabled.');
process.exit();
return;
}
throw err;
}
if (err.message == 'CAPTCHA') {
console.log(err.captchaurl);
rl.question('CAPTCHA: ', (captchaInput) => {
doLogin(accountName, password, authCode, captchaInput);
});
return;
}
doRevoke();
});
}
async function doRevoke() {
let rCode = await promptAsync('Revocation Code: R');
community.disableTwoFactor('R' + rCode, (err) => {
if (err) {
console.log(err);
process.exit();
return;
}
console.log('Logged on!');
community.disableTwoFactor('R' + rCode, (err) => {
if (err) {
console.log(err);
process.exit();
return;
console.log('Two-factor authentication disabled!');
process.exit();
});
}
// Nothing interesting below here, just code for prompting for input from the console.
function promptAsync(question, sensitiveInput = false) {
return new Promise((resolve) => {
let rl = ReadLine.createInterface({
input: process.stdin,
output: sensitiveInput ? null : process.stdout,
terminal: true
});
g_AbortPromptFunc = () => {
rl.close();
resolve('');
};
if (sensitiveInput) {
// We have to write the question manually if we didn't give readline an output stream
process.stdout.write(question);
}
rl.question(question, (result) => {
if (sensitiveInput) {
// We have to manually print a newline
process.stdout.write('\n');
}
console.log('Two-factor authentication disabled!');
process.exit();
g_AbortPromptFunc = null;
rl.close();
resolve(result);
});
});
}
function abortPrompt() {
if (!g_AbortPromptFunc) {
return;
}
g_AbortPromptFunc();
process.stdout.write('\n');
}

View File

@@ -6,47 +6,51 @@ const FS = require('fs');
const EResult = SteamCommunity.EResult;
let g_AbortPromptFunc = null;
let community = new SteamCommunity();
let rl = ReadLine.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Username: ', (accountName) => {
rl.question('Password: ', (password) => {
doLogin(accountName, password);
});
});
main();
async function main() {
let accountName = await promptAsync('Username: ');
let password = await promptAsync('Password (hidden): ', true);
function doLogin(accountName, password, authCode, captcha) {
attemptLogin(accountName, password);
}
function attemptLogin(accountName, password, authCode) {
community.login({
accountName: accountName,
password: password,
authCode: authCode,
captcha: captcha
}, (err, sessionID, cookies, steamguard) => {
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;
}
if (err) {
if (err.message == 'SteamGuardMobile') {
console.log('This account already has two-factor authentication enabled.');
throw err;
}
doSetup();
});
}
function doSetup() {
community.enableTwoFactor((err, response) => {
if (err) {
if (err.eresult == EResult.Fail) {
console.log('Error: Failed to enable two-factor authentication. Do you have a phone number attached to your account?');
process.exit();
return;
}
if (err.message == 'SteamGuard') {
console.log(`An email has been sent to your address at ${err.emaildomain}`);
rl.question('Steam Guard Code: ', (code) => {
doLogin(accountName, password, code);
});
return;
}
if (err.message == 'CAPTCHA') {
console.log(err.captchaurl);
rl.question('CAPTCHA: ', (captchaInput) => {
doLogin(accountName, password, authCode, captchaInput);
});
if (err.eresult == EResult.RateLimitExceeded) {
console.log('Error: RateLimitExceeded. Try again later.');
process.exit();
return;
}
@@ -55,58 +59,85 @@ function doLogin(accountName, password, authCode, captcha) {
return;
}
console.log('Logged on!');
community.enableTwoFactor((err, response) => {
if (err) {
if (err.eresult == EResult.Fail) {
console.log('Error: Failed to enable two-factor authentication. Do you have a phone number attached to your account?');
process.exit();
return;
}
if (err.eresult == EResult.RateLimitExceeded) {
console.log('Error: RateLimitExceeded. Try again later.');
process.exit();
return;
}
console.log(err);
process.exit();
return;
}
if (response.status != EResult.OK) {
console.log(`Error: Status ${response.status}`);
process.exit();
return;
}
let filename = `twofactor_${community.steamID.getSteamID64()}.json`;
console.log(`Writing secrets to ${filename}`);
console.log(`Revocation code: ${response.revocation_code}`);
FS.writeFileSync(filename, JSON.stringify(response, null, '\t'));
promptActivationCode(response);
});
});
}
function promptActivationCode(response) {
rl.question('SMS Code: ', (smsCode) => {
community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => {
if (err) {
if (err.message == 'Invalid activation code') {
console.log(err);
promptActivationCode(response);
return;
}
console.log(err);
} else {
console.log('Two-factor authentication enabled!');
}
if (response.status != EResult.OK) {
console.log(`Error: Status ${response.status}`);
process.exit();
return;
}
let filename = `twofactor_${community.steamID.getSteamID64()}.json`;
console.log(`Writing secrets to ${filename}`);
console.log(`Revocation code: ${response.revocation_code}`);
FS.writeFileSync(filename, JSON.stringify(response, null, '\t'));
promptActivationCode(response);
});
}
async function promptActivationCode(response) {
if (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('Activation Code: ');
community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => {
if (err) {
if (err.message == 'Invalid activation code') {
console.log(err);
promptActivationCode(response);
return;
}
console.log(err);
} else {
console.log('Two-factor authentication enabled!');
}
process.exit();
});
}
// Nothing interesting below here, just code for prompting for input from the console.
function promptAsync(question, sensitiveInput = false) {
return new Promise((resolve) => {
let rl = ReadLine.createInterface({
input: process.stdin,
output: sensitiveInput ? null : process.stdout,
terminal: true
});
g_AbortPromptFunc = () => {
rl.close();
resolve('');
};
if (sensitiveInput) {
// We have to write the question manually if we didn't give readline an output stream
process.stdout.write(question);
}
rl.question(question, (result) => {
if (sensitiveInput) {
// We have to manually print a newline
process.stdout.write('\n');
}
g_AbortPromptFunc = null;
rl.close();
resolve(result);
});
});
}
function abortPrompt() {
if (!g_AbortPromptFunc) {
return;
}
g_AbortPromptFunc();
process.stdout.write('\n');
}

220
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;
@@ -14,6 +11,8 @@ module.exports = SteamCommunity;
SteamCommunity.SteamID = SteamID;
SteamCommunity.ConfirmationType = require('./resources/EConfirmationType.js');
SteamCommunity.EResult = require('./resources/EResult.js');
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
function SteamCommunity(options) {
@@ -29,7 +28,7 @@ function SteamCommunity(options) {
"timeout": options.timeout || 50000,
"gzip": true,
"headers": {
"User-Agent": options.userAgent || USER_AGENT
"User-Agent": options.userAgent || chrome()
}
};
@@ -59,162 +58,30 @@ 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 = 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 if (!disableMobile && !body.oauth) {
callback(new Error("Malformed response"));
} 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){
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));
};
/**
* @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]);
@@ -301,10 +168,14 @@ SteamCommunity.prototype.setCookies = function(cookies) {
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
// for this account.
this._verifyMobileAccessToken();
};
SteamCommunity.prototype.getSessionID = function() {
var cookies = this._jar.getCookieString("http://steamcommunity.com").split(';');
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') {
@@ -323,11 +194,13 @@ function generateSessionID() {
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
var self = this;
var sessionID = self.getSessionID();
this.httpRequestPost("https://steamcommunity.com/parental/ajaxunlock", {
"json": true,
"form": {
"pin": pin
"pin": pin,
"sessionid": sessionID
}
}, function(err, response, body) {
if(!callback) {
@@ -533,19 +406,54 @@ SteamCommunity.prototype._myProfile = function(endpoint, form, callback) {
}
};
/**
* 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
*/
SteamCommunity.prototype.getFriendsList = function(callback) {
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 (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/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');
require('./components/help.js');
require('./classes/CMarketItem.js');
require('./classes/CMarketSearchResult.js');
require('./classes/CSteamGroup.js');
require('./classes/CSteamSharedFile.js');
require('./classes/CSteamUser.js');
/**

View File

@@ -1,7 +1,14 @@
{
"name": "steamcommunity",
"version": "3.41.7",
"version": "3.48.0",
"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,14 +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.6.0",
"steam-totp": "^1.5.0",
"steamid": "^1.1.3",
"xml2js": "^0.4.22"
"xml2js": "^0.6.2"
},
"engines": {
"node": ">=4.0.0"

View File

@@ -0,0 +1,23 @@
/**
* @enum EFriendRelationship
*/
module.exports = {
"None": 0,
"Blocked": 1,
"RequestRecipient": 2,
"Friend": 3,
"RequestInitiator": 4,
"Ignored": 5,
"IgnoredFriend": 6,
"SuggestedFriend": 7, // removed "was used by the original implementation of the facebook linking feature; but now unused."
// Value-to-name mapping for convenience
"0": "None",
"1": "Blocked",
"2": "RequestRecipient",
"3": "Friend",
"4": "RequestInitiator",
"5": "Ignored",
"6": "IgnoredFriend",
"7": "SuggestedFriend",
};

View File

@@ -9,7 +9,7 @@ module.exports = {
"Snooze": 4,
"LookingToTrade": 5,
"LookingToPlay": 6,
"Max": 7,
"Invisible": 7,
// Value-to-name mapping for convenience
"0": "Offline",
@@ -19,5 +19,5 @@ module.exports = {
"4": "Snooze",
"5": "LookingToTrade",
"6": "LookingToPlay",
"7": "Max",
"7": "Invisible",
};

View File

@@ -4,7 +4,8 @@
module.exports = {
"HasRichPresence": 1,
"InJoinableGame": 2,
"Golden": 4, // removed "no longer has any effect"
"Golden": 4,
"RemotePlayTogether": 8,
"OnlineUsingWeb": 256, // removed "renamed to ClientTypeWeb"
"ClientTypeWeb": 256,
"OnlineUsingMobile": 512, // removed "renamed to ClientTypeMobile"
@@ -14,14 +15,18 @@ module.exports = {
"OnlineUsingVR": 2048, // removed "renamed to ClientTypeVR"
"ClientTypeVR": 2048,
"LaunchTypeGamepad": 4096,
"LaunchTypeCompatTool": 8192,
// Value-to-name mapping for convenience
"1": "HasRichPresence",
"2": "InJoinableGame",
"4": "Golden",
"8": "RemotePlayTogether",
"256": "ClientTypeWeb",
"512": "ClientTypeMobile",
"1024": "ClientTypeTenfoot",
"2048": "ClientTypeVR",
"4096": "LaunchTypeGamepad",
"8192": "LaunchTypeCompatTool",
};

View File

@@ -123,6 +123,13 @@ module.exports = {
"WGNetworkSendExceeded": 110,
"AccountNotFriends": 111,
"LimitedUserAccount": 112,
"CantRemoveItem": 113,
"AccountHasBeenDeleted": 114,
"AccountHasAnExistingUserCancelledLicense": 115,
"DeniedDueToCommunityCooldown": 116,
"NoLauncherSpecified": 117,
"MustAgreeToSSA": 118,
"ClientNoLongerSupported": 119,
// Value-to-name mapping for convenience
"0": "Invalid",
@@ -237,4 +244,11 @@ module.exports = {
"110": "WGNetworkSendExceeded",
"111": "AccountNotFriends",
"112": "LimitedUserAccount",
"113": "CantRemoveItem",
"114": "AccountHasBeenDeleted",
"115": "AccountHasAnExistingUserCancelledLicense",
"116": "DeniedDueToCommunityCooldown",
"117": "NoLauncherSpecified",
"118": "MustAgreeToSSA",
"119": "ClientNoLongerSupported",
};

View File

@@ -0,0 +1,13 @@
/**
* @enum ESharedFileType
*/
module.exports = {
"Screenshot": 0,
"Artwork": 1,
"Guide": 2,
// Value-to-name mapping for convenience
"0": "Screenshot",
"1": "Artwork",
"2": "Guide"
};