mirror of
https://github.com/DoctorMcKay/node-steamcommunity.git
synced 2026-08-19 13:13:28 +08:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa8d897590 | ||
|
|
f7fe587312 | ||
|
|
08d14f19e6 | ||
|
|
cfed757eab | ||
|
|
63defbb643 | ||
|
|
6e20fbe299 | ||
|
|
59090242f9 | ||
|
|
00e82f7e79 | ||
|
|
7b901b147b | ||
|
|
0f7fc98ab5 | ||
|
|
2a12174a7c | ||
|
|
caa76ee84b | ||
|
|
bc8893fedd | ||
|
|
d3e90f6fd3 | ||
|
|
a61b50b7a2 | ||
|
|
39fe4a4106 | ||
|
|
0d991c2655 | ||
|
|
0a307de9b9 | ||
|
|
c0b8dcf3a2 | ||
|
|
dcf00b4ea1 | ||
|
|
ee0cc34512 | ||
|
|
c52533ea06 | ||
|
|
67774f86f7 | ||
|
|
2f03e09f50 | ||
|
|
2868b1f45f | ||
|
|
ac222ef8c3 | ||
|
|
4b8799ddd4 | ||
|
|
f02f85a2aa | ||
|
|
4948cbeac0 | ||
|
|
1c1ff82543 | ||
|
|
63015259af | ||
|
|
ad67805ad9 | ||
|
|
3347d87f29 | ||
|
|
c97351543a | ||
|
|
1067d4572e | ||
|
|
49a7165052 | ||
|
|
0fb2798424 | ||
|
|
e16136866c | ||
|
|
cae85433f9 | ||
|
|
ba6e29c935 | ||
|
|
de95e25867 | ||
|
|
e1820efeca | ||
|
|
7cee24199f | ||
|
|
90eb4d38eb | ||
|
|
b58745c8b7 | ||
|
|
53154043a3 | ||
|
|
6c77c5231c | ||
|
|
7c564c1453 | ||
|
|
b854d6d4d4 | ||
|
|
67867978f8 | ||
|
|
e1af63e171 | ||
|
|
e4da22c464 | ||
|
|
0f91adf819 | ||
|
|
d51c171a48 | ||
|
|
50276c3a0c | ||
|
|
b5b50cec50 | ||
|
|
cf4b474c70 | ||
|
|
bf2b4601ee | ||
|
|
f69519f547 | ||
|
|
29e18806cc | ||
|
|
c855b505c5 | ||
|
|
6cc96b51fa | ||
|
|
edf2891800 | ||
|
|
0fc1120fd3 | ||
|
|
a0aee350cc | ||
|
|
2abc547f49 | ||
|
|
b2bffddfbe | ||
|
|
ab9baeafc6 | ||
|
|
788f34ba7a | ||
|
|
bde2cafaf4 | ||
|
|
36e8c79d87 | ||
|
|
4ca0fc83aa | ||
|
|
2dd5bb490e | ||
|
|
d0da148d6b | ||
|
|
7605dc8a41 | ||
|
|
5f46aee9fc | ||
|
|
02cc5daafe | ||
|
|
e76d34891a | ||
|
|
20ad260d68 | ||
|
|
76f2c55a73 | ||
|
|
77c2811e7e | ||
|
|
37bac4d240 | ||
|
|
d23d7e22c0 | ||
|
|
6e19ffd93b | ||
|
|
e5fbddc00c | ||
|
|
3531560083 | ||
|
|
6fa6a073a8 | ||
|
|
fd872490c8 | ||
|
|
be528d1a19 | ||
|
|
440b2f9ba9 | ||
|
|
e23fa02c91 | ||
|
|
33bc8d83c9 | ||
|
|
f96d25ad52 | ||
|
|
f7d7cf0660 | ||
|
|
27d2daac61 | ||
|
|
4723bd93a6 | ||
|
|
897ad16154 | ||
|
|
c25cb31e34 | ||
|
|
81d8dc2a5c | ||
|
|
8538589fef | ||
|
|
c0be17cd61 | ||
|
|
350f6288e4 | ||
|
|
ba2782066d | ||
|
|
d6b0fbd5d0 | ||
|
|
9ec5dcd7b0 | ||
|
|
c8642865a1 | ||
|
|
c92801d25e | ||
|
|
b55516f67f | ||
|
|
f09adc84a4 | ||
|
|
d807f10653 | ||
|
|
18011b3fac | ||
|
|
6f97370710 | ||
|
|
2a1d244114 | ||
|
|
3bcaf40850 | ||
|
|
0ccac69bea | ||
|
|
48d63857e2 | ||
|
|
b11734cd9b | ||
|
|
5c37c38dda | ||
|
|
260bfaa0ad | ||
|
|
1031a4dbd7 | ||
|
|
e28fe5ca05 | ||
|
|
90a9ee1aa9 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||
|
||||
4
.idea/modules.xml
generated
4
.idea/modules.xml
generated
@@ -2,8 +2,8 @@
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/../node-steamcommunity Wiki/.idea/node-steamcommunity Wiki.iml" filepath="$PROJECT_DIR$/../node-steamcommunity Wiki/.idea/node-steamcommunity Wiki.iml" />
|
||||
<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>
|
||||
|
||||
4
.idea/steamcommunity.iml
generated
4
.idea/steamcommunity.iml
generated
@@ -5,6 +5,6 @@
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module" module-name="node-steamcommunity Wiki" />
|
||||
<orderEntry type="module" module-name="steamcommunity Wiki" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -2,6 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../node-steamcommunity Wiki" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../steamcommunity Wiki" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,13 +1,11 @@
|
||||
# Steam Community for Node.js
|
||||
[](https://npmjs.com/package/steamcommunity)
|
||||
[](https://npmjs.com/package/steamcommunity)
|
||||
[](https://david-dm.org/DoctorMcKay/node-steamcommunity)
|
||||
[](https://github.com/DoctorMcKay/node-steamcommunity/blob/master/LICENSE)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity¤cy_code=USD)
|
||||
|
||||
This module provides an easy interface for the Steam Community website. This module can be used to simply login to steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
|
||||
|
||||
It supports Steam Guard and CAPTCHAs.
|
||||
This module provides an easy interface for the Steam Community website. This module can be used to simply login to
|
||||
steamcommunity.com for use with other libraries, or to interact with steamcommunity.com.
|
||||
|
||||
**Have a question about the module or coding in general? *Do not create a GitHub issue.* GitHub issues are for feature
|
||||
requests and bug reports. Instead, post in the [dedicated forum](https://dev.doctormckay.com/forum/8-node-steamcommunity/).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = CEconItem;
|
||||
|
||||
function CEconItem(item, description, contextID) {
|
||||
function CEconItem(item, description, contextID, assetProperties) {
|
||||
var thing;
|
||||
for (thing in item) {
|
||||
if (item.hasOwnProperty(thing)) {
|
||||
@@ -28,7 +28,7 @@ function CEconItem(item, description, contextID) {
|
||||
}
|
||||
|
||||
for (thing in description) {
|
||||
if (description.hasOwnProperty(thing)) {
|
||||
if (description.hasOwnProperty(thing) && !this.hasOwnProperty(thing)) {
|
||||
this[thing] = description[thing];
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,26 @@ function CEconItem(item, description, contextID) {
|
||||
|
||||
// Restore cache_expiration, if we can (for CS:GO items)
|
||||
if (this.appid == 730 && this.contextid == 2 && this.owner_descriptions) {
|
||||
let description = this.owner_descriptions.find(d => d.value && d.value.indexOf('Tradable After ') == 0);
|
||||
let description = this.owner_descriptions.find((d) => d.value && d.value.indexOf("Tradable/Marketable After ") == 0);
|
||||
if (description) {
|
||||
let date = new Date(description.value.substring(15).replace(/[,()]/g, ''));
|
||||
if (date) {
|
||||
let date;
|
||||
const match = description.value.match(/(\d{1,2}) (\w+) @ (\d{1,2}:\d{2})(am|pm)/i);
|
||||
if (match) {
|
||||
// New day-first format, e.g. "Tradable/Marketable After 17 Jul @ 3:00am"
|
||||
const [, day, month, time, meridian] = match;
|
||||
const now = new Date();
|
||||
let year = now.getFullYear();
|
||||
date = new Date(`${day} ${month} ${year} ${time} ${meridian}`);
|
||||
|
||||
if (date.getTime() < now.getTime()) {
|
||||
date = new Date(`${day} ${month} ${year + 1} ${time} ${meridian}`);
|
||||
}
|
||||
} else {
|
||||
// Legacy month-first format, e.g. "Tradable/Marketable After Jul 11, 2025 (2:00:00)"
|
||||
date = new Date(description.value.substring("Tradable/Marketable After ".length).replace(/[,()]/g, ""));
|
||||
}
|
||||
|
||||
if (date && !isNaN(date.getTime())) {
|
||||
this.cache_expiration = date.toISOString();
|
||||
}
|
||||
}
|
||||
@@ -86,6 +102,11 @@ function CEconItem(item, description, contextID) {
|
||||
this.actions = [];
|
||||
}
|
||||
|
||||
// Assign asset_properties if provided
|
||||
if (assetProperties) {
|
||||
this.asset_properties = assetProperties;
|
||||
}
|
||||
|
||||
// One wouldn't think that we need this if statement, but apparently v8 has a weird bug/quirk where deleting a
|
||||
// property results in greatly increased memory usage. Because that makes sense.
|
||||
if (this.currency) {
|
||||
|
||||
221
classes/CSteamSharedFile.js
Normal file
221
classes/CSteamSharedFile.js
Normal 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);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ SteamCommunity.prototype.getSteamUser = function(id, callback) {
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.httpRequest("http://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
|
||||
this.httpRequest("https://steamcommunity.com/" + (typeof id === 'string' ? "id/" + id : "profiles/" + id.toString()) + "/?xml=1", function(err, response, body) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@@ -166,6 +166,14 @@ CSteamUser.prototype.inviteToGroup = function(groupID, callback) {
|
||||
this._community.inviteUserToGroup(this.steamID, groupID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.follow = function(callback) {
|
||||
this._community.followUser(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.unfollow = function(callback) {
|
||||
this._community.unfollowUser(this.steamID, callback);
|
||||
};
|
||||
|
||||
CSteamUser.prototype.getAliases = function(callback) {
|
||||
this._community.getUserAliases(this.steamID, callback);
|
||||
};
|
||||
|
||||
@@ -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,19 @@ 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,
|
||||
headers: {
|
||||
'user-agent': 'okhttp/4.9.2'
|
||||
}
|
||||
};
|
||||
|
||||
if (req.method == "GET") {
|
||||
if (req.method == 'GET') {
|
||||
req.qs = params;
|
||||
} else {
|
||||
req.form = params;
|
||||
@@ -274,7 +280,7 @@ function request(community, url, key, time, tag, params, json, callback) {
|
||||
}
|
||||
|
||||
callback(null, body);
|
||||
}, "steamcommunity");
|
||||
}, 'steamcommunity');
|
||||
}
|
||||
|
||||
// Confirmation checker
|
||||
@@ -389,6 +395,22 @@ SteamCommunity.prototype.checkConfirmations = function() {
|
||||
}
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.acknowledgeTradeProtection = function(callback) {
|
||||
this.httpRequestPost({
|
||||
uri: 'https://steamcommunity.com//trade/new/acknowledge',
|
||||
form: {
|
||||
sessionid: this.getSessionID(),
|
||||
message: 1
|
||||
}
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
||||
callback && callback(null);
|
||||
}, 'steamcommunity');
|
||||
}
|
||||
|
||||
SteamCommunity.prototype._confirmationCheckerGetKey = function(tag, callback) {
|
||||
if(this._identitySecret) {
|
||||
if(tag == 'details') {
|
||||
|
||||
@@ -11,17 +11,17 @@ SteamCommunity.prototype.getGroupMembers = function(gid, callback, members, link
|
||||
if (!link) {
|
||||
if (typeof gid !== 'string') {
|
||||
// It's a SteamID object
|
||||
link = "http://steamcommunity.com/gid/" + gid.toString() + "/memberslistxml/?xml=1";
|
||||
link = "https://steamcommunity.com/gid/" + gid.toString() + "/memberslistxml/?xml=1";
|
||||
} else {
|
||||
try {
|
||||
var sid = new SteamID(gid);
|
||||
if (sid.type == SteamID.Type.CLAN && sid.isValid()) {
|
||||
link = "http://steamcommunity.com/gid/" + sid.getSteamID64() + "/memberslistxml/?xml=1";
|
||||
link = "https://steamcommunity.com/gid/" + sid.getSteamID64() + "/memberslistxml/?xml=1";
|
||||
} else {
|
||||
throw new Error("Doesn't particularly matter what this message is");
|
||||
}
|
||||
} catch (e) {
|
||||
link = "http://steamcommunity.com/groups/" + gid + "/memberslistxml/?xml=1";
|
||||
link = "https://steamcommunity.com/groups/" + gid + "/memberslistxml/?xml=1";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
var URL = require('url');
|
||||
|
||||
var SteamCommunity = require('../index.js');
|
||||
|
||||
SteamCommunity.prototype.httpRequest = function(uri, options, callback, source) {
|
||||
@@ -19,6 +21,16 @@ SteamCommunity.prototype.httpRequest = function(uri, options, callback, source)
|
||||
delete this._httpRequestConvenienceMethod;
|
||||
}
|
||||
|
||||
// Add origin header if necessary
|
||||
// https://github.com/DoctorMcKay/node-steamcommunity/issues/351
|
||||
if ((options.method || 'GET').toUpperCase() != 'GET') {
|
||||
options.headers = options.headers || {};
|
||||
if (!options.headers.origin) {
|
||||
var parsedUrl = URL.parse(options.url);
|
||||
options.headers.origin = parsedUrl.protocol + '//' + parsedUrl.host;
|
||||
}
|
||||
}
|
||||
|
||||
var requestID = ++this._httpRequestID;
|
||||
source = source || "";
|
||||
|
||||
@@ -124,7 +136,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);
|
||||
|
||||
@@ -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]});
|
||||
});
|
||||
}
|
||||
|
||||
114
components/login.js
Normal file
114
components/login.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const {chrome} = require('@doctormckay/user-agents');
|
||||
|
||||
const SteamCommunity = require('../index.js');
|
||||
|
||||
/**
|
||||
* @typedef LogOnDetails
|
||||
* @property {string} accountName
|
||||
* @property {string} password
|
||||
* @property {string} [steamguard]
|
||||
* @property {string} [authCode]
|
||||
* @property {string} [twoFactorCode]
|
||||
* @property {boolean} disableMobile
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef LogOnResponse
|
||||
* @property {string} sessionID
|
||||
* @property {string[]} cookies
|
||||
* @property {string} steamguard
|
||||
* @property {string} [mobileAccessToken]
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LogOnDetails} logOnDetails
|
||||
* @returns {Promise<LogOnResponse>}
|
||||
* @private
|
||||
*/
|
||||
SteamCommunity.prototype._modernLogin = function(logOnDetails) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!isNodeVersionNewEnough()) {
|
||||
return reject(new Error(`Node.js version is too old! Need >=12.22.0 or later, got ${process.versions.node}.`));
|
||||
}
|
||||
|
||||
if (this._options.request) {
|
||||
return reject(new Error('SteamCommunity.login() is incompatible with node-steamcommunity v3\'s usage of \'request\'. If you need to specify a custom \'request\' instance (e.g. when using a proxy), use https://www.npmjs.com/package/steam-session directly to log onto Steam.'));
|
||||
}
|
||||
|
||||
// Import this here so we don't cause problems on old Node versions if this code path isn't taken.
|
||||
const {LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType} = require('steam-session');
|
||||
|
||||
let session = new LoginSession(
|
||||
logOnDetails.disableMobile
|
||||
? EAuthTokenPlatformType.WebBrowser
|
||||
: EAuthTokenPlatformType.MobileApp,
|
||||
{
|
||||
localAddress: this._options.localAddress,
|
||||
userAgent: this._options.userAgent || chrome()
|
||||
}
|
||||
);
|
||||
|
||||
session.on('authenticated', async () => {
|
||||
try {
|
||||
let webCookies = await session.getWebCookies();
|
||||
let sessionIdCookie = webCookies.find(c => c.startsWith('sessionid='));
|
||||
resolve({
|
||||
sessionID: sessionIdCookie.split('=')[1].split(';')[0].trim(),
|
||||
cookies: webCookies,
|
||||
steamguard: session.steamGuardMachineToken,
|
||||
mobileAccessToken: logOnDetails.disableMobile ? null : session.accessToken
|
||||
});
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
|
||||
session.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
session.on('timeout', () => {
|
||||
reject(new Error('Login timed out'));
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
158
components/sharedfiles.js
Normal 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");
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -296,6 +296,66 @@ SteamCommunity.prototype.inviteUserToGroup = function(userID, groupID, callback)
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.followUser = function(userID, callback) {
|
||||
if(typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/followuser/`,
|
||||
"form": {
|
||||
"sessionid": this.getSessionID(),
|
||||
},
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success && body.success != SteamCommunity.EResult.OK) {
|
||||
callback(Helpers.eresultError(body.success));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.unfollowUser = function(userID, callback) {
|
||||
if(typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
"uri": `https://steamcommunity.com/profiles/${userID.toString()}/unfollowuser/`,
|
||||
"form": {
|
||||
"sessionid": this.getSessionID(),
|
||||
},
|
||||
"json": true
|
||||
}, function(err, response, body) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.success && body.success != SteamCommunity.EResult.OK) {
|
||||
callback(Helpers.eresultError(body.success));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.getUserAliases = function(userID, callback) {
|
||||
if (typeof userID === 'string') {
|
||||
userID = new SteamID(userID);
|
||||
@@ -385,18 +445,7 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
|
||||
|
||||
var match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
|
||||
if (!match) {
|
||||
var errorMessage = "Malformed response";
|
||||
|
||||
if(body.match(/0 items in their inventory\./)){
|
||||
callback(null, {});
|
||||
return;
|
||||
}else if(body.match(/inventory is currently private\./)){
|
||||
errorMessage = "Private inventory";
|
||||
}else if(body.match(/profile\_private\_info/)){
|
||||
errorMessage = "Private profile";
|
||||
}
|
||||
|
||||
callback(new Error(errorMessage));
|
||||
callback(new Error('Malformed response'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -408,6 +457,21 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length == 0) {
|
||||
if (body.match(/inventory is currently private\./)) {
|
||||
callback(new Error('Private inventory'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.match(/profile_private_info/)) {
|
||||
callback(new Error('Private profile'));
|
||||
return;
|
||||
}
|
||||
|
||||
// If they truly have no items in their inventory, Steam will send g_rgAppContextData as [] instead of {}.
|
||||
data = {};
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
@@ -472,7 +536,7 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
|
||||
continue;
|
||||
}
|
||||
|
||||
currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
|
||||
currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
|
||||
}
|
||||
|
||||
if (body.more) {
|
||||
@@ -526,7 +590,7 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
|
||||
},
|
||||
"qs": {
|
||||
"l": language, // Default language
|
||||
"count": 5000, // Max items per 'page'
|
||||
"count": 1000, // Max items per 'page'
|
||||
"start_assetid": start
|
||||
},
|
||||
"json": true
|
||||
@@ -565,6 +629,13 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
|
||||
return;
|
||||
}
|
||||
|
||||
if (appID == 730 && body && body.success && !body.assets) {
|
||||
// CS inventory has no visible items. We need a special case for this because Valve is incapable of
|
||||
// doing anything not dumb.
|
||||
callback(null, [], [], body.total_inventory_count);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body || !body.success || !body.assets || !body.descriptions) {
|
||||
if (body) {
|
||||
// Dunno if the error/Error property even exists on this new endpoint
|
||||
@@ -576,12 +647,21 @@ SteamCommunity.prototype.getUserInventoryContents = function(userID, appID, cont
|
||||
return;
|
||||
}
|
||||
|
||||
// Build asset_properties lookup by assetid
|
||||
var assetPropertiesLookup = {};
|
||||
if (body.asset_properties) {
|
||||
for (var j = 0; j < body.asset_properties.length; j++) {
|
||||
assetPropertiesLookup[body.asset_properties[j].assetid] = body.asset_properties[j].asset_properties;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < body.assets.length; i++) {
|
||||
var description = getDescription(body.descriptions, body.assets[i].classid, body.assets[i].instanceid);
|
||||
|
||||
if (!tradableOnly || (description && description.tradable)) {
|
||||
body.assets[i].pos = pos++;
|
||||
(body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID));
|
||||
var assetProperties = assetPropertiesLookup[body.assets[i].assetid] || null;
|
||||
(body.assets[i].currencyid ? currency : inventory).push(new CEconItem(body.assets[i], description, contextID, assetProperties));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,156 @@
|
||||
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.
|
||||
* @typedef CreateApiKeyOptions
|
||||
* @property {string} domain - The domain to associate with your API key
|
||||
* @property {string} [requestID] - If finalizing an existing create request, include the request ID
|
||||
* @property {string|Buffer} [identitySecret] - If you pass your identity_secret here, then steamcommunity will
|
||||
* internally handle accepting any confirmations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef CreateApiKeyResponse
|
||||
* @property {boolean} confirmationRequired
|
||||
* @property {string} [apiKey] - If creating your API key succeeded, this is the new key
|
||||
* @property {CreateApiKeyOptions} [finalizeOptions] - If confirmation is required to create a key, then accept the
|
||||
* confirmation, then call createWebApiKey again and pass this whole object for the `options` parameter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback createWebApiKeyCallback
|
||||
* @param {Error|null} err
|
||||
* @param {CreateApiKeyResponse} [result]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Starts the process to create a Steam Web API key. When the callback is fired, you will need to approve a mobile
|
||||
* confirmation in your app or using getConfirmations().
|
||||
*
|
||||
* @param {CreateApiKeyOptions} options
|
||||
* @param {createWebApiKeyCallback} callback
|
||||
*/
|
||||
SteamCommunity.prototype.createWebApiKey = function(options, callback) {
|
||||
if (!options.domain) {
|
||||
callback(new Error('Passing a domain is required to register an API key'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
uri: 'https://steamcommunity.com/dev/requestkey',
|
||||
form: {
|
||||
domain: options.domain,
|
||||
request_id: options.requestID || '0',
|
||||
sessionid: this.getSessionID(),
|
||||
agreeToTerms: 'true'
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// body.requires_confirmation is 1/0, but the Steam website doesn't check this value and instead only checks the
|
||||
// value of `success`. So let's just do that.
|
||||
|
||||
// This is a mess. I'm glad we have promises and await now.
|
||||
|
||||
switch (body.success) {
|
||||
case SteamCommunity.EResult.OK:
|
||||
if (body.api_key) {
|
||||
callback(null, {confirmationRequired: false, apiKey: body.api_key});
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -53,5 +158,64 @@ SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
|
||||
return callback(null, this.oAuthToken);
|
||||
}
|
||||
|
||||
callback(new Error('This operation requires an OAuth token, which can only be obtained from node-steamcommunity\'s `login` method.'));
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
1
examples/.gitignore
vendored
Normal file
1
examples/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
twofactor_*.json
|
||||
35
examples/README.md
Normal file
35
examples/README.md
Normal 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
|
||||
```
|
||||
173
examples/accept_all_confirmations.js
Normal file
173
examples/accept_all_confirmations.js
Normal 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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
193
index.js
193
index.js
@@ -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,7 @@ 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');
|
||||
|
||||
|
||||
@@ -30,7 +28,7 @@ function SteamCommunity(options) {
|
||||
"timeout": options.timeout || 50000,
|
||||
"gzip": true,
|
||||
"headers": {
|
||||
"User-Agent": options.userAgent || USER_AGENT
|
||||
"User-Agent": options.userAgent || chrome()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,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]);
|
||||
@@ -288,20 +154,28 @@ SteamCommunity.prototype._setCookie = function(cookie, secure) {
|
||||
var protocol = secure ? "https" : "http";
|
||||
cookie.secure = !!secure;
|
||||
|
||||
this._jar.setCookie(cookie.clone(), protocol + "://steamcommunity.com");
|
||||
this._jar.setCookie(cookie.clone(), protocol + "://store.steampowered.com");
|
||||
this._jar.setCookie(cookie.clone(), protocol + "://help.steampowered.com");
|
||||
if (cookie.domain) {
|
||||
this._jar.setCookie(cookie.clone(), protocol + '://' + cookie.domain);
|
||||
} else {
|
||||
this._jar.setCookie(cookie.clone(), protocol + "://steamcommunity.com");
|
||||
this._jar.setCookie(cookie.clone(), protocol + "://store.steampowered.com");
|
||||
this._jar.setCookie(cookie.clone(), protocol + "://help.steampowered.com");
|
||||
}
|
||||
};
|
||||
|
||||
SteamCommunity.prototype.setCookies = function(cookies) {
|
||||
cookies.forEach((cookie) => {
|
||||
var cookieName = cookie.match(/(.+)=/)[1];
|
||||
var cookieName = cookie.trim().split('=')[0];
|
||||
if (cookieName == 'steamLogin' || cookieName == 'steamLoginSecure') {
|
||||
this.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
|
||||
this.steamID = new SteamID(cookie.match(/steamLogin(Secure)?=(\d+)/)[2]);
|
||||
}
|
||||
|
||||
this._setCookie(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
|
||||
});
|
||||
|
||||
// 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(host = "http://steamcommunity.com") {
|
||||
@@ -567,12 +441,14 @@ SteamCommunity.prototype.getFriendsList = function(callback) {
|
||||
});
|
||||
};
|
||||
|
||||
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');
|
||||
@@ -581,6 +457,7 @@ require('./components/help.js');
|
||||
require('./classes/CMarketItem.js');
|
||||
require('./classes/CMarketSearchResult.js');
|
||||
require('./classes/CSteamGroup.js');
|
||||
require('./classes/CSteamSharedFile.js');
|
||||
require('./classes/CSteamUser.js');
|
||||
|
||||
/**
|
||||
|
||||
14
package.json
14
package.json
@@ -1,7 +1,14 @@
|
||||
{
|
||||
"name": "steamcommunity",
|
||||
"version": "3.44.0",
|
||||
"version": "3.50.3",
|
||||
"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.9.1",
|
||||
"steam-totp": "^1.5.0",
|
||||
"steamid": "^1.1.3",
|
||||
"xml2js": "^0.4.22"
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
|
||||
13
resources/ESharedFileType.js
Normal file
13
resources/ESharedFileType.js
Normal 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"
|
||||
};
|
||||
Reference in New Issue
Block a user