From 2a1d244114d82c30041648c257e9556108d2add6 Mon Sep 17 00:00:00 2001 From: fjexe <34057423+fjexe@users.noreply.github.com> Date: Fri, 4 Nov 2022 18:48:35 +0300 Subject: [PATCH 01/50] Update users.js --- components/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/users.js b/components/users.js index 468853a..c0e9a22 100644 --- a/components/users.js +++ b/components/users.js @@ -472,7 +472,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) { From 6f97370710f9795206b3356b85ef6fad604fd4f4 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Fri, 12 May 2023 22:05:20 +0200 Subject: [PATCH 02/50] Add sharedfile comment support --- components/sharedfiles.js | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 components/sharedfiles.js diff --git a/components/sharedfiles.js b/components/sharedfiles.js new file mode 100644 index 0000000..eda5ea8 --- /dev/null +++ b/components/sharedfiles.js @@ -0,0 +1,60 @@ +var SteamCommunity = require('../index.js'); +var SteamID = require('steamid'); + +// Note: a CSteamSharedfile class does not exist because we can't get data using the "normal" xml way to fill a CSteamSharedfile object + +/** + * Deletes a comment from a sharedfile's comment section + * @param {SteamID | String} userID - ID of the user associated to this sharedfile + * @param {String} sid - 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, sid, cid, callback) { + if (typeof userID === "string") { + userID = new SteamID(userID); + } + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sid}/`, + "form": { + "gidcomment": cid, + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(null || err); + }, "steamcommunity"); +}; + +/** + * Posts a comment to a sharedfile + * @param {SteamID | String} userID - ID of the user associated to this sharedfile + * @param {String} sid - 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, sid, message, callback) { + if (typeof userID === "string") { + userID = new SteamID(userID); + } + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sid}/`, + "form": { + "comment": message, + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(null || err); + }, "steamcommunity"); +}; \ No newline at end of file From 18011b3fac901969642a48b9012cadab3df449f6 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Fri, 12 May 2023 22:10:37 +0200 Subject: [PATCH 03/50] Add sharedfile voting support --- components/sharedfiles.js | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/components/sharedfiles.js b/components/sharedfiles.js index eda5ea8..cb8f116 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -55,6 +55,48 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, return; } + callback(null || err); + }, "steamcommunity"); +}; + +/** + * Downvotes a sharedfile + * @param {String} sid - ID of the sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.voteDownSharedfile = function(sid, callback) { + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/votedown", + "form": { + "id": sid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(null || err); + }, "steamcommunity"); +}; + +/** + * Upvotes a sharedfile + * @param {String} sid - ID of the sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.voteUpSharedfile = function(sid, callback) { + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/voteup", + "form": { + "id": sid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + callback(null || err); }, "steamcommunity"); }; \ No newline at end of file From d807f106532811294cc360ed288f58014c55ba27 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Fri, 12 May 2023 23:16:01 +0200 Subject: [PATCH 04/50] Add sharedfile subscribing support --- components/sharedfiles.js | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/components/sharedfiles.js b/components/sharedfiles.js index cb8f116..0d0a6b9 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -59,6 +59,58 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, }, "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} sid ID of the sharedfileof + * @param {function} callback - Takes only an Error object/null as the first argument + */ +Bot.prototype.subscribeSharedfileComments = function(userID, sid, callback) { + if (typeof userID === "string") { + userID = new SteamID(userID); + } + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sid}/`, + "form": { + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } + + callback(null || 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} sid - ID of the sharedfileof + * @param {function} callback - Takes only an Error object/null as the first argument + */ +Bot.prototype.unsubscribeSharedfileComments = function(userID, sid, callback) { + if (typeof userID === "string") { + userID = new SteamID(userID); + } + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sid}/`, + "form": { + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } + + callback(null || err); + }, "steamcommunity"); +}; + /** * Downvotes a sharedfile * @param {String} sid - ID of the sharedfile From f09adc84a400f538c0a7967cbf416c40328c5ff0 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Fri, 12 May 2023 23:30:10 +0200 Subject: [PATCH 05/50] Add (disabled) sharedfile favorite support --- components/sharedfiles.js | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/components/sharedfiles.js b/components/sharedfiles.js index 0d0a6b9..13ac48a 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -31,6 +31,28 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sid, cid, ca }, "steamcommunity"); }; +/** + * Favorites a sharedfile + * @param {String} sid - ID of the sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +/* SteamCommunity.prototype.favoriteSharedfile = function(sid, callback) { + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/favorite", + "form": { + "id": sid, + "appid": , // TODO: How to get appid the sharedfile is associated to? + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(null || err); + }, "steamcommunity"); +}; */ + /** * Posts a comment to a sharedfile * @param {SteamID | String} userID - ID of the user associated to this sharedfile @@ -85,6 +107,28 @@ Bot.prototype.subscribeSharedfileComments = function(userID, sid, callback) { }, "steamcommunity"); }; +/** + * Unfavorites a sharedfile + * @param {String} sid - ID of the sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +/* SteamCommunity.prototype.unfavoriteSharedfile = function(sid, callback) { + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/unfavorite", + "form": { + "id": sid, + "appid": , // TODO: How to get appid the sharedfile is associated to? + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } + + callback(null || 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 From b55516f67f953e3306903e705dac050004bae1a7 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Fri, 12 May 2023 23:46:31 +0200 Subject: [PATCH 06/50] Oops, wrong object name --- components/sharedfiles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/sharedfiles.js b/components/sharedfiles.js index 13ac48a..8670421 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -87,7 +87,7 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, * @param {String} sid ID of the sharedfileof * @param {function} callback - Takes only an Error object/null as the first argument */ -Bot.prototype.subscribeSharedfileComments = function(userID, sid, callback) { +SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, callback) { if (typeof userID === "string") { userID = new SteamID(userID); } @@ -135,7 +135,7 @@ Bot.prototype.subscribeSharedfileComments = function(userID, sid, callback) { * @param {String} sid - ID of the sharedfileof * @param {function} callback - Takes only an Error object/null as the first argument */ -Bot.prototype.unsubscribeSharedfileComments = function(userID, sid, callback) { +SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, sid, callback) { if (typeof userID === "string") { userID = new SteamID(userID); } From c92801d25ee8c23f3956c0fc3d2b2ed6be5a17b4 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sat, 13 May 2023 16:09:47 +0200 Subject: [PATCH 07/50] Load sharedfiles component --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 54d028d..10c58ba 100644 --- a/index.js +++ b/index.js @@ -573,6 +573,7 @@ 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'); From c8642865a133a0b2e156d029655eaba3e9fde2b6 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 14:50:32 +0200 Subject: [PATCH 08/50] Add sharedfile type enum --- resources/ESharedfileType.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 resources/ESharedfileType.js diff --git a/resources/ESharedfileType.js b/resources/ESharedfileType.js new file mode 100644 index 0000000..b8732ce --- /dev/null +++ b/resources/ESharedfileType.js @@ -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" +}; \ No newline at end of file From 9ec5dcd7b0f8c44cce6ca2193bc93adbc571bac8 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 15:00:45 +0200 Subject: [PATCH 09/50] Add sharedfile class with fully working scraper --- classes/CSteamSharedfile.js | 152 ++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 153 insertions(+) create mode 100644 classes/CSteamSharedfile.js diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js new file mode 100644 index 0000000..072996a --- /dev/null +++ b/classes/CSteamSharedfile.js @@ -0,0 +1,152 @@ +const Cheerio = require("cheerio"); +const SteamID = require("steamid"); +const SteamCommunity = require("../index.js"); +const steamIdResolver = require("steamid-resolver"); +const ESharedfileType = require("../resources/ESharedfileType.js"); + + +/** + * Scrape a sharedfile's DOM to get all available information + * @param {String} sid - ID of the sharedfile + * @param {function} callback - First argument is null/Error, second is object containing all available information + */ +SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { + // Construct object holding all the data we can scrape + let sharedfile = { + id: sid, + type: null, + appID: null, + owner: null, + fileSize: null, + postDate: null, + resolution: null, + uniqueVisitorsCount: null, + favoritesCount: null, + upvoteCount: null + } + + + // Get DOM of sharedfile + this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sid}`, (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, i) => { + 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( '${sid}', '`, "").replace("' );", "")) + + + // Find owner profile link, convert to steamID64 using steamIdResolver lib and create a SteamID object + let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; + + steamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // Note: Callback will be called before this is done, takes around 1 sec to populate + if (!err) sharedfile.owner = new SteamID(steamID64); + }); + + + // 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 (Warning: Will get ugly) + let posted = detailsStatsObj["Posted"].replace(/,|@/g, "").split(" "); // Remove comma behind month and @, the date & time separator. Split by space to get: Day, Month, Year (if not current) and time + let months = { "Jan": "01", "Feb": "02", "Mar": "03", "Apr": "04", "May": "05", "Jun": "06", "Jul": "07", "Aug": "08", "Sep": "09", "Oct": "10", "Nov": "11", "Dec": "12" }; // Map all month abbreviations + + if (posted[0].split(":")[0].length == 1) posted[0] = "0" + posted[0]; // Add zero if day is <10 to have a fixed length + + posted[1] = months[posted[1]]; // Replace month abbreviation with corresponding Number + + if (!posted[2]) posted[2] = new Date().getUTCFullYear().toString(); // Add current year if Steam did not list one + else posted.splice(3, 1); // ...otherwise remove element 3 as it will be an empty String + + // Convert AM/PM time to 24h format - Credit: https://stackoverflow.com/a/40197728 (Modified) + if (posted[3].split(":")[0].length == 1) posted[3] = "0" + posted[3]; // Add zero if hour is <10 to have a fixed length + + let time = posted[3].substring(0, 5); + let modifier = posted[3].substring(5, 7); + let [hours, minutes] = time.split(":"); + + if (hours === "12") hours = "00"; + if (modifier === "pm") hours = parseInt(hours, 10) + 12; + + sharedfile.postDate = Date.parse(`${posted[2]}-${posted[1]}-${posted[0]}T${hours}:${minutes}:00.000Z`); // Construct Date String and parse it to get Unix timestamp + + + // 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); + + + // 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; + + + callback(null, new CSteamSharedfile(this, sharedfile)); + + } catch (err) { + callback(err, null); + } + }); +} + + +function CSteamSharedfile(community, data) { + this._community = community; + + // Clone all the data we recieved + Object.assign(this, data); // TODO: This is cleaner but might break IntelliSense + + /* this.id = data.id; + this.type = data.type; + this.appID = data.appID; + this.owner = data.owner; + this.fileSize = data.fileSize; + this.postDate = data.postDate; + this.resolution = data.resolution; + this.uniqueVisitorsCount = data.uniqueVisitorsCount; + this.favoritesCount = data.favoritesCount; + this.upvoteCount = data.upvoteCount; */ +} \ No newline at end of file diff --git a/package.json b/package.json index afd186a..64709eb 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "request": "^2.88.0", "steam-totp": "^1.5.0", "steamid": "^1.1.3", + "steamid-resolver": "^1.2.3", "xml2js": "^0.4.22" }, "engines": { From d6b0fbd5d0a7914d5e88b4bb496c302546a017c8 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 15:07:23 +0200 Subject: [PATCH 10/50] Update non-object methods to take appid param --- components/sharedfiles.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/components/sharedfiles.js b/components/sharedfiles.js index 8670421..08f5c52 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -34,14 +34,15 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sid, cid, ca /** * Favorites a sharedfile * @param {String} sid - 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(sid, callback) { +SteamCommunity.prototype.favoriteSharedfile = function(sid, appid, callback) { this.httpRequestPost({ "uri": "https://steamcommunity.com/sharedfiles/favorite", "form": { "id": sid, - "appid": , // TODO: How to get appid the sharedfile is associated to? + "appid": appid, "sessionid": this.getSessionID() } }, function(err, response, body) { @@ -51,7 +52,7 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sid, cid, ca callback(null || err); }, "steamcommunity"); -}; */ +}; /** * Posts a comment to a sharedfile @@ -110,14 +111,15 @@ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, cal /** * Unfavorites a sharedfile * @param {String} sid - 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(sid, callback) { +SteamCommunity.prototype.unfavoriteSharedfile = function(sid, appid, callback) { this.httpRequestPost({ "uri": "https://steamcommunity.com/sharedfiles/unfavorite", "form": { "id": sid, - "appid": , // TODO: How to get appid the sharedfile is associated to? + "appid": appid, "sessionid": this.getSessionID() } }, function(err, response, body) { @@ -127,7 +129,7 @@ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, cal callback(null || err); }, "steamcommunity"); -}; */ +}; /** * Unsubscribes from a sharedfile's comment section. Note: Checkbox on webpage does not update From ba2782066d4a0b588b3a9c548585f2b2a79c16d5 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 15:22:15 +0200 Subject: [PATCH 11/50] Misc --- classes/CSteamSharedfile.js | 5 ++--- components/sharedfiles.js | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 072996a..0094807 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -129,9 +129,8 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { } catch (err) { callback(err, null); } - }); -} - + }, "steamcommunity"); +}; function CSteamSharedfile(community, data) { this._community = community; diff --git a/components/sharedfiles.js b/components/sharedfiles.js index 08f5c52..be9fe3a 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -1,7 +1,6 @@ var SteamCommunity = require('../index.js'); var SteamID = require('steamid'); -// Note: a CSteamSharedfile class does not exist because we can't get data using the "normal" xml way to fill a CSteamSharedfile object /** * Deletes a comment from a sharedfile's comment section @@ -85,7 +84,7 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, /** * 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} sid ID of the sharedfileof + * @param {String} sid ID of the sharedfile * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, callback) { @@ -134,7 +133,7 @@ SteamCommunity.prototype.unfavoriteSharedfile = function(sid, appid, callback) { /** * 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} sid - ID of the sharedfileof + * @param {String} sid - ID of the sharedfile * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, sid, callback) { From 350f6288e4f9796d6d123a1cf2c3d4ca10315e4e Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 15:26:55 +0200 Subject: [PATCH 12/50] Add sharedfile object methods --- classes/CSteamSharedfile.js | 68 ++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 0094807..40ccdfe 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -148,4 +148,70 @@ function CSteamSharedfile(community, data) { this.uniqueVisitorsCount = data.uniqueVisitorsCount; this.favoritesCount = data.favoritesCount; this.upvoteCount = data.upvoteCount; */ -} \ No newline at end of file +} + +/** + * 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.userID, this.sid, 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.sid, 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.sid, 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.sid, 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.sid, 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.sid, callback); +}; + +/** + * Downvotes this sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamSharedfile.prototype.voteDown = function(callback) { + this._community.voteDownSharedfile(this.sid, callback); +}; + +/** + * Upvotes this sharedfile + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamSharedfile.prototype.voteUp = function(callback) { + this._community.voteUpSharedfile(this.sid, callback); +}; \ No newline at end of file From c0be17cd612817a628f999de5a59a424c6bf3037 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 15:47:49 +0200 Subject: [PATCH 13/50] Load CSteamSharedfile class --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 10c58ba..64e3756 100644 --- a/index.js +++ b/index.js @@ -573,7 +573,7 @@ require('./components/profile.js'); require('./components/market.js'); require('./components/groups.js'); require('./components/users.js'); -require("./components/sharedfiles.js"); +require('./components/sharedfiles.js'); require('./components/inventoryhistory.js'); require('./components/webapi.js'); require('./components/twofactor.js'); @@ -582,6 +582,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'); /** From 8538589fef6d3a7da7d2e1074e08beab28b0bc47 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 16:12:59 +0200 Subject: [PATCH 14/50] Fix owner remaining null and wrong param --- classes/CSteamSharedfile.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 40ccdfe..04ba9c0 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -64,14 +64,6 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sid}', '`, "").replace("' );", "")) - // Find owner profile link, convert to steamID64 using steamIdResolver lib and create a SteamID object - let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; - - steamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // Note: Callback will be called before this is done, takes around 1 sec to populate - if (!err) sharedfile.owner = new SteamID(steamID64); - }); - - // 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 @@ -124,7 +116,15 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { if (breadcrumb.includes("Guide")) sharedfile.type = ESharedfileType.Guide; - callback(null, new CSteamSharedfile(this, sharedfile)); + // Find owner profile link, convert to steamID64 using steamIdResolver lib and create a SteamID object + let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; + + steamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // This request takes <1 sec + if (!err) sharedfile.owner = new SteamID(steamID64); + + // Make callback when ID was resolved as otherwise owner will always be null + callback(null, new CSteamSharedfile(this, sharedfile)); + }); } catch (err) { callback(err, null); @@ -156,7 +156,7 @@ function CSteamSharedfile(community, data) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.deleteComment = function(cid, callback) { - this._community.deleteSharedfileComment(this.userID, this.sid, cid, callback); + this._community.deleteSharedfileComment(this.userID, this.id, cid, callback); }; /** @@ -164,7 +164,7 @@ CSteamSharedfile.prototype.deleteComment = function(cid, callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.favorite = function(callback) { - this._community.favoriteSharedfile(this.sid, this.appID, callback); + this._community.favoriteSharedfile(this.id, this.appID, callback); }; /** @@ -173,7 +173,7 @@ CSteamSharedfile.prototype.favorite = function(callback) { * @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.sid, message, callback); + this._community.postSharedfileComment(this.owner, this.id, message, callback); }; /** @@ -181,7 +181,7 @@ CSteamSharedfile.prototype.comment = function(message, callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.subscribe = function(callback) { - this._community.subscribeSharedfileComments(this.owner, this.sid, callback); + this._community.subscribeSharedfileComments(this.owner, this.id, callback); }; /** @@ -189,7 +189,7 @@ CSteamSharedfile.prototype.subscribe = function(callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.unfavorite = function(callback) { - this._community.unfavoriteSharedfile(this.sid, this.appID, callback); + this._community.unfavoriteSharedfile(this.id, this.appID, callback); }; /** @@ -197,7 +197,7 @@ CSteamSharedfile.prototype.unfavorite = function(callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.unsubscribe = function(callback) { - this._community.unsubscribeSharedfileComments(this.owner, this.sid, callback); + this._community.unsubscribeSharedfileComments(this.owner, this.id, callback); }; /** @@ -205,7 +205,7 @@ CSteamSharedfile.prototype.unsubscribe = function(callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.voteDown = function(callback) { - this._community.voteDownSharedfile(this.sid, callback); + this._community.voteDownSharedfile(this.id, callback); }; /** @@ -213,5 +213,5 @@ CSteamSharedfile.prototype.voteDown = function(callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.voteUp = function(callback) { - this._community.voteUpSharedfile(this.sid, callback); + this._community.voteUpSharedfile(this.id, callback); }; \ No newline at end of file From 81d8dc2a5cefa0940bbc93cd6eb1bafbeec5a255 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 16:27:09 +0200 Subject: [PATCH 15/50] Use decodeSteamTime() helper instead of doing it manually --- classes/CSteamSharedfile.js | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 04ba9c0..de9db50 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -1,7 +1,8 @@ const Cheerio = require("cheerio"); const SteamID = require("steamid"); +const Helpers = require('../components/helpers.js'); const SteamCommunity = require("../index.js"); -const steamIdResolver = require("steamid-resolver"); +const SteamIdResolver = require("steamid-resolver"); const ESharedfileType = require("../resources/ESharedfileType.js"); @@ -67,28 +68,11 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { // 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 (Warning: Will get ugly) - let posted = detailsStatsObj["Posted"].replace(/,|@/g, "").split(" "); // Remove comma behind month and @, the date & time separator. Split by space to get: Day, Month, Year (if not current) and time - let months = { "Jan": "01", "Feb": "02", "Mar": "03", "Apr": "04", "May": "05", "Jun": "06", "Jul": "07", "Aug": "08", "Sep": "09", "Oct": "10", "Nov": "11", "Dec": "12" }; // Map all month abbreviations - if (posted[0].split(":")[0].length == 1) posted[0] = "0" + posted[0]; // Add zero if day is <10 to have a fixed length + // Find postDate and convert to timestamp + let posted = detailsStatsObj["Posted"].trim(); - posted[1] = months[posted[1]]; // Replace month abbreviation with corresponding Number - - if (!posted[2]) posted[2] = new Date().getUTCFullYear().toString(); // Add current year if Steam did not list one - else posted.splice(3, 1); // ...otherwise remove element 3 as it will be an empty String - - // Convert AM/PM time to 24h format - Credit: https://stackoverflow.com/a/40197728 (Modified) - if (posted[3].split(":")[0].length == 1) posted[3] = "0" + posted[3]; // Add zero if hour is <10 to have a fixed length - - let time = posted[3].substring(0, 5); - let modifier = posted[3].substring(5, 7); - let [hours, minutes] = time.split(":"); - - if (hours === "12") hours = "00"; - if (modifier === "pm") hours = parseInt(hours, 10) + 12; - - sharedfile.postDate = Date.parse(`${posted[2]}-${posted[1]}-${posted[0]}T${hours}:${minutes}:00.000Z`); // Construct Date String and parse it to get Unix timestamp + sharedfile.postDate = Date.parse(Helpers.decodeSteamTime(posted)); // Pass String into helper and parse the returned String to get a Unix timestamp // Find resolution if artwork or screenshot @@ -116,10 +100,10 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { if (breadcrumb.includes("Guide")) sharedfile.type = ESharedfileType.Guide; - // Find owner profile link, convert to steamID64 using steamIdResolver lib and create a SteamID object + // Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; - steamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // This request takes <1 sec + SteamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // This request takes <1 sec if (!err) sharedfile.owner = new SteamID(steamID64); // Make callback when ID was resolved as otherwise owner will always be null From c25cb31e34fd6dca44fd66711f5a987b45735843 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Fri, 3 Jun 2022 11:46:20 +0200 Subject: [PATCH 16/50] Re-enable primaryGroup profile setting --- components/profile.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/components/profile.js b/components/profile.js index 32b14ac..081a1f0 100644 --- a/components/profile.js +++ b/components/profile.js @@ -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 } From 897ad161544f80fb2a4d01a7b8bb34b9b0a41071 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 14 May 2023 18:52:16 +0200 Subject: [PATCH 17/50] Formatting --- classes/CSteamSharedfile.js | 212 ++++++++++++++++++--------------- components/sharedfiles.js | 224 +++++++++++++++++------------------ resources/ESharedfileType.js | 14 +-- 3 files changed, 236 insertions(+), 214 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index de9db50..8ed43ad 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -1,9 +1,9 @@ -const Cheerio = require("cheerio"); -const SteamID = require("steamid"); +const Cheerio = require('cheerio'); +const SteamID = require('steamid'); const Helpers = require('../components/helpers.js'); -const SteamCommunity = require("../index.js"); -const SteamIdResolver = require("steamid-resolver"); -const ESharedfileType = require("../resources/ESharedfileType.js"); +const SteamCommunity = require('../index.js'); +const SteamIdResolver = require('steamid-resolver'); +const ESharedfileType = require('../resources/ESharedfileType.js'); /** @@ -12,126 +12,148 @@ const ESharedfileType = require("../resources/ESharedfileType.js"); * @param {function} callback - First argument is null/Error, second is object containing all available information */ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { - // Construct object holding all the data we can scrape - let sharedfile = { - id: sid, - type: null, - appID: null, - owner: null, - fileSize: null, - postDate: null, - resolution: null, - uniqueVisitorsCount: null, - favoritesCount: null, - upvoteCount: null - } + + // Construct object holding all the data we can scrape + let sharedfile = { + id: sid, + type: null, + appID: null, + owner: null, + fileSize: null, + postDate: null, + resolution: null, + uniqueVisitorsCount: null, + favoritesCount: null, + upvoteCount: null + }; - // Get DOM of sharedfile - this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sid}`, (err, res, body) => { - try { + // Get DOM of sharedfile + this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sid}`, (err, res, body) => { + try { - /* --------------------- Preprocess output --------------------- */ + /* --------------------- Preprocess output --------------------- */ - // Load output into cheerio to make parsing easier - let $ = Cheerio.load(body); + // 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(); + // 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 + 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; - }); + 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(); + // 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, i) => { - if (isNaN(e)) return; // Ignore invalid entries + 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 - }); + // 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 and map values --------------------- */ - // Find appID in share button onclick event - sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sid}', '`, "").replace("' );", "")) + // Find appID in share button onclick event + sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sid}', '`, "").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 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(); + // Find postDate and convert to timestamp + let posted = detailsStatsObj["Posted"].trim(); - sharedfile.postDate = Date.parse(Helpers.decodeSteamTime(posted)); // Pass String into helper and parse the returned String to get a Unix timestamp + sharedfile.postDate = Date.parse(Helpers.decodeSteamTime(posted)); // Pass String into helper and parse the returned String to get a Unix timestamp - // Find resolution if artwork or screenshot - sharedfile.resolution = detailsStatsObj["Size"] || null; + // 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 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 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 upvoteCount. We can't use ' || null' here as Number("0") casts to false + let upvoteCount = $("#VotesUpCountContainer > #VotesUpCount").text(); + + if (upvoteCount) { + sharedfile.upvoteCount = Number(upvoteCount); + } - // 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; + // 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"]; + // Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object + let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; - SteamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // This request takes <1 sec - if (!err) sharedfile.owner = new SteamID(steamID64); + SteamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // This request takes <1 sec + if (!err) { + sharedfile.owner = new SteamID(steamID64); + } - // Make callback when ID was resolved as otherwise owner will always be null - callback(null, new CSteamSharedfile(this, sharedfile)); - }); + // 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"); + } catch (err) { + callback(err, null); + } + }, "steamcommunity"); }; function CSteamSharedfile(community, data) { - this._community = community; + this._community = community; - // Clone all the data we recieved - Object.assign(this, data); // TODO: This is cleaner but might break IntelliSense + // Clone all the data we recieved + Object.assign(this, data); // TODO: This is cleaner but might break IntelliSense. I'm leaving the block below to be reactivated if necessary - /* this.id = data.id; - this.type = data.type; - this.appID = data.appID; - this.owner = data.owner; - this.fileSize = data.fileSize; - this.postDate = data.postDate; - this.resolution = data.resolution; - this.uniqueVisitorsCount = data.uniqueVisitorsCount; - this.favoritesCount = data.favoritesCount; - this.upvoteCount = data.upvoteCount; */ + /* this.id = data.id; + this.type = data.type; + this.appID = data.appID; + this.owner = data.owner; + this.fileSize = data.fileSize; + this.postDate = data.postDate; + this.resolution = data.resolution; + this.uniqueVisitorsCount = data.uniqueVisitorsCount; + this.favoritesCount = data.favoritesCount; + this.upvoteCount = data.upvoteCount; */ } /** @@ -140,7 +162,7 @@ function CSteamSharedfile(community, data) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.deleteComment = function(cid, callback) { - this._community.deleteSharedfileComment(this.userID, this.id, cid, callback); + this._community.deleteSharedfileComment(this.userID, this.id, cid, callback); }; /** @@ -148,7 +170,7 @@ CSteamSharedfile.prototype.deleteComment = function(cid, callback) { * @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); + this._community.favoriteSharedfile(this.id, this.appID, callback); }; /** @@ -157,7 +179,7 @@ CSteamSharedfile.prototype.favorite = function(callback) { * @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); + this._community.postSharedfileComment(this.owner, this.id, message, callback); }; /** @@ -165,7 +187,7 @@ CSteamSharedfile.prototype.comment = function(message, callback) { * @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); + this._community.subscribeSharedfileComments(this.owner, this.id, callback); }; /** @@ -173,7 +195,7 @@ CSteamSharedfile.prototype.subscribe = function(callback) { * @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); + this._community.unfavoriteSharedfile(this.id, this.appID, callback); }; /** @@ -181,7 +203,7 @@ CSteamSharedfile.prototype.unfavorite = function(callback) { * @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); + this._community.unsubscribeSharedfileComments(this.owner, this.id, callback); }; /** @@ -189,7 +211,7 @@ CSteamSharedfile.prototype.unsubscribe = function(callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.voteDown = function(callback) { - this._community.voteDownSharedfile(this.id, callback); + this._community.voteDownSharedfile(this.id, callback); }; /** @@ -197,5 +219,5 @@ CSteamSharedfile.prototype.voteDown = function(callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedfile.prototype.voteUp = function(callback) { - this._community.voteUpSharedfile(this.id, callback); + this._community.voteUpSharedfile(this.id, callback); }; \ No newline at end of file diff --git a/components/sharedfiles.js b/components/sharedfiles.js index be9fe3a..3ef690a 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -10,24 +10,24 @@ var SteamID = require('steamid'); * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sid, cid, callback) { - if (typeof userID === "string") { - userID = new SteamID(userID); - } + if (typeof userID === "string") { + userID = new SteamID(userID); + } - this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sid}/`, - "form": { - "gidcomment": cid, - "count": 10, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sid}/`, + "form": { + "gidcomment": cid, + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -37,20 +37,20 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sid, cid, ca * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.favoriteSharedfile = function(sid, appid, callback) { - this.httpRequestPost({ - "uri": "https://steamcommunity.com/sharedfiles/favorite", - "form": { - "id": sid, - "appid": appid, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/favorite", + "form": { + "id": sid, + "appid": appid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -61,24 +61,24 @@ SteamCommunity.prototype.favoriteSharedfile = function(sid, appid, callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, callback) { - if (typeof userID === "string") { - userID = new SteamID(userID); - } + if (typeof userID === "string") { + userID = new SteamID(userID); + } - this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sid}/`, - "form": { - "comment": message, - "count": 10, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sid}/`, + "form": { + "comment": message, + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -88,23 +88,23 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, callback) { - if (typeof userID === "string") { - userID = new SteamID(userID); - } + if (typeof userID === "string") { + userID = new SteamID(userID); + } - this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sid}/`, - "form": { - "count": 10, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { // eslint-disable-line - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sid}/`, + "form": { + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -114,20 +114,20 @@ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, cal * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.unfavoriteSharedfile = function(sid, appid, callback) { - this.httpRequestPost({ - "uri": "https://steamcommunity.com/sharedfiles/unfavorite", - "form": { - "id": sid, - "appid": appid, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/unfavorite", + "form": { + "id": sid, + "appid": appid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -137,23 +137,23 @@ SteamCommunity.prototype.unfavoriteSharedfile = function(sid, appid, callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, sid, callback) { - if (typeof userID === "string") { - userID = new SteamID(userID); - } + if (typeof userID === "string") { + userID = new SteamID(userID); + } - this.httpRequestPost({ - "uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sid}/`, - "form": { - "count": 10, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { // eslint-disable-line - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sid}/`, + "form": { + "count": 10, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { // eslint-disable-line + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -162,19 +162,19 @@ SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, sid, c * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.voteDownSharedfile = function(sid, callback) { - this.httpRequestPost({ - "uri": "https://steamcommunity.com/sharedfiles/votedown", - "form": { - "id": sid, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/votedown", + "form": { + "id": sid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; /** @@ -183,17 +183,17 @@ SteamCommunity.prototype.voteDownSharedfile = function(sid, callback) { * @param {function} callback - Takes only an Error object/null as the first argument */ SteamCommunity.prototype.voteUpSharedfile = function(sid, callback) { - this.httpRequestPost({ - "uri": "https://steamcommunity.com/sharedfiles/voteup", - "form": { - "id": sid, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } + this.httpRequestPost({ + "uri": "https://steamcommunity.com/sharedfiles/voteup", + "form": { + "id": sid, + "sessionid": this.getSessionID() + } + }, function(err, response, body) { + if (!callback) { + return; + } - callback(null || err); - }, "steamcommunity"); + callback(null || err); + }, "steamcommunity"); }; \ No newline at end of file diff --git a/resources/ESharedfileType.js b/resources/ESharedfileType.js index b8732ce..fc528d5 100644 --- a/resources/ESharedfileType.js +++ b/resources/ESharedfileType.js @@ -2,12 +2,12 @@ * @enum ESharedfileType */ module.exports = { - "Screenshot": 0, - "Artwork": 1, - "Guide": 2, + "Screenshot": 0, + "Artwork": 1, + "Guide": 2, - // Value-to-name mapping for convenience - "0": "Screenshot", - "1": "Artwork", - "2": "Guide" + // Value-to-name mapping for convenience + "0": "Screenshot", + "1": "Artwork", + "2": "Guide" }; \ No newline at end of file From 4723bd93a61a8aae0cf725d8d452fab3577173a3 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Mon, 15 May 2023 22:27:37 +0200 Subject: [PATCH 18/50] Improve resolveVanityURL() and move to helpers --- components/helpers.js | 40 ++++++++++++++++++++++++++++++++++ components/inventoryhistory.js | 19 ++-------------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/components/helpers.js b/components/helpers.js index 1da5443..b354da5 100644 --- a/components/helpers.js +++ b/components/helpers.js @@ -1,4 +1,6 @@ const EResult = require('../resources/EResult.js'); +const request = require('request'); +const xml2js = require('xml2js'); exports.isSteamID = function(input) { var keys = Object.keys(input); @@ -54,3 +56,41 @@ exports.eresultError = function(eresult) { err.eresult = eresult; return err; }; + +/** + * 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; + let vanityURL = parsed.profile.customURL; + + callback(null, {"vanityURL": vanityURL, "steamID": steamID64}); + }); + }); +}; \ No newline at end of file diff --git a/components/inventoryhistory.js b/components/inventoryhistory.js index fd7c522..961d9c1 100644 --- a/components/inventoryhistory.js +++ b/components/inventoryhistory.js @@ -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(/(\d+)<\/steamID64>/); - if (!match || !match[1]) { - callback(new Error("Couldn't find Steam ID")); - return; - } - - callback(null, {"vanityURL": vanityURL, "steamID": match[1]}); - }); -} From 27d2daac61113dddef2ece07bc4a58a72f6ec12e Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Mon, 15 May 2023 22:35:39 +0200 Subject: [PATCH 19/50] Remove steamid-resolver dep and use internal helper --- classes/CSteamSharedfile.js | 7 +++---- package.json | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 8ed43ad..8401cd6 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -2,14 +2,13 @@ const Cheerio = require('cheerio'); const SteamID = require('steamid'); const Helpers = require('../components/helpers.js'); const SteamCommunity = require('../index.js'); -const SteamIdResolver = require('steamid-resolver'); const ESharedfileType = require('../resources/ESharedfileType.js'); /** * Scrape a sharedfile's DOM to get all available information * @param {String} sid - ID of the sharedfile - * @param {function} callback - First argument is null/Error, second is object containing all available information + * @param {function} callback - First argument is null/Error, second is object containing all available information */ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { @@ -123,9 +122,9 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { // Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; - SteamIdResolver.customUrlToSteamID64(ownerHref, (err, steamID64) => { // This request takes <1 sec + Helpers.resolveVanityURL(ownerHref, (err, data) => { // This request takes <1 sec if (!err) { - sharedfile.owner = new SteamID(steamID64); + sharedfile.owner = new SteamID(data.steamID); } // Make callback when ID was resolved as otherwise owner will always be null diff --git a/package.json b/package.json index 64709eb..afd186a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "request": "^2.88.0", "steam-totp": "^1.5.0", "steamid": "^1.1.3", - "steamid-resolver": "^1.2.3", "xml2js": "^0.4.22" }, "engines": { From f7d7cf0660c5a15fc24ca770afeb3f9e40936403 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Mon, 15 May 2023 22:42:22 +0200 Subject: [PATCH 20/50] Remove support for up- & downvoting sharedfiles --- classes/CSteamSharedfile.js | 16 -------------- components/sharedfiles.js | 42 ------------------------------------- 2 files changed, 58 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 8401cd6..432f09a 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -204,19 +204,3 @@ CSteamSharedfile.prototype.unfavorite = function(callback) { CSteamSharedfile.prototype.unsubscribe = function(callback) { this._community.unsubscribeSharedfileComments(this.owner, this.id, callback); }; - -/** - * Downvotes this sharedfile - * @param {function} callback - Takes only an Error object/null as the first argument - */ -CSteamSharedfile.prototype.voteDown = function(callback) { - this._community.voteDownSharedfile(this.id, callback); -}; - -/** - * Upvotes this sharedfile - * @param {function} callback - Takes only an Error object/null as the first argument - */ -CSteamSharedfile.prototype.voteUp = function(callback) { - this._community.voteUpSharedfile(this.id, callback); -}; \ No newline at end of file diff --git a/components/sharedfiles.js b/components/sharedfiles.js index 3ef690a..00a754c 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -155,45 +155,3 @@ SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, sid, c callback(null || err); }, "steamcommunity"); }; - -/** - * Downvotes a sharedfile - * @param {String} sid - ID of the sharedfile - * @param {function} callback - Takes only an Error object/null as the first argument - */ -SteamCommunity.prototype.voteDownSharedfile = function(sid, callback) { - this.httpRequestPost({ - "uri": "https://steamcommunity.com/sharedfiles/votedown", - "form": { - "id": sid, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } - - callback(null || err); - }, "steamcommunity"); -}; - -/** - * Upvotes a sharedfile - * @param {String} sid - ID of the sharedfile - * @param {function} callback - Takes only an Error object/null as the first argument - */ -SteamCommunity.prototype.voteUpSharedfile = function(sid, callback) { - this.httpRequestPost({ - "uri": "https://steamcommunity.com/sharedfiles/voteup", - "form": { - "id": sid, - "sessionid": this.getSessionID() - } - }, function(err, response, body) { - if (!callback) { - return; - } - - callback(null || err); - }, "steamcommunity"); -}; \ No newline at end of file From f96d25ad52cb65b3355bea7b25b8aee597728206 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Mon, 15 May 2023 22:45:33 +0200 Subject: [PATCH 21/50] Rename sid to sharedFileId --- classes/CSteamSharedfile.js | 10 +++++----- components/sharedfiles.js | 36 ++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 432f09a..80d2a85 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -7,14 +7,14 @@ const ESharedfileType = require('../resources/ESharedfileType.js'); /** * Scrape a sharedfile's DOM to get all available information - * @param {String} sid - ID of the sharedfile + * @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(sid, callback) { +SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { // Construct object holding all the data we can scrape let sharedfile = { - id: sid, + id: sharedFileId, type: null, appID: null, owner: null, @@ -28,7 +28,7 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { // Get DOM of sharedfile - this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sid}`, (err, res, body) => { + this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, (err, res, body) => { try { /* --------------------- Preprocess output --------------------- */ @@ -66,7 +66,7 @@ SteamCommunity.prototype.getSteamSharedfile = function(sid, callback) { /* --------------------- Find and map values --------------------- */ // Find appID in share button onclick event - sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sid}', '`, "").replace("' );", "")); + sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sharedFileId}', '`, "").replace("' );", "")); // Find fileSize if not guide diff --git a/components/sharedfiles.js b/components/sharedfiles.js index 00a754c..c051943 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -5,17 +5,17 @@ var SteamID = require('steamid'); /** * Deletes a comment from a sharedfile's comment section * @param {SteamID | String} userID - ID of the user associated to this sharedfile - * @param {String} sid - ID of the 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, sid, cid, callback) { +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()}/${sid}/`, + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`, "form": { "gidcomment": cid, "count": 10, @@ -32,15 +32,15 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sid, cid, ca /** * Favorites a sharedfile - * @param {String} sid - ID of the 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(sid, appid, callback) { +SteamCommunity.prototype.favoriteSharedfile = function(sharedFileId, appid, callback) { this.httpRequestPost({ "uri": "https://steamcommunity.com/sharedfiles/favorite", "form": { - "id": sid, + "id": sharedFileId, "appid": appid, "sessionid": this.getSessionID() } @@ -56,17 +56,17 @@ SteamCommunity.prototype.favoriteSharedfile = function(sid, appid, callback) { /** * Posts a comment to a sharedfile * @param {SteamID | String} userID - ID of the user associated to this sharedfile - * @param {String} sid - ID of the 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, sid, message, callback) { +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()}/${sid}/`, + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`, "form": { "comment": message, "count": 10, @@ -84,16 +84,16 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sid, message, /** * 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} sid ID of the 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, sid, callback) { +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()}/${sid}/`, + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`, "form": { "count": 10, "sessionid": this.getSessionID() @@ -109,15 +109,15 @@ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sid, cal /** * Unfavorites a sharedfile - * @param {String} sid - ID of the 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(sid, appid, callback) { +SteamCommunity.prototype.unfavoriteSharedfile = function(sharedFileId, appid, callback) { this.httpRequestPost({ "uri": "https://steamcommunity.com/sharedfiles/unfavorite", "form": { - "id": sid, + "id": sharedFileId, "appid": appid, "sessionid": this.getSessionID() } @@ -133,16 +133,16 @@ SteamCommunity.prototype.unfavoriteSharedfile = function(sid, appid, callback) { /** * 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} sid - ID of the 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, sid, callback) { +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()}/${sid}/`, + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`, "form": { "count": 10, "sessionid": this.getSessionID() From 33bc8d83c93ad3d26da657ad0b3fb2ea22de9d0b Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 28 May 2023 13:45:00 +0200 Subject: [PATCH 22/50] Add support for determining up/downvote status --- classes/CSteamSharedfile.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 80d2a85..6935787 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -23,7 +23,9 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { resolution: null, uniqueVisitorsCount: null, favoritesCount: null, - upvoteCount: null + upvoteCount: null, + isUpvoted: null, + isDownvoted: null }; @@ -103,6 +105,11 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { } + // 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 || ""; From e23fa02c91efe792b6e0bf9e3ca37a35fc236e17 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Sun, 28 May 2023 14:02:40 +0200 Subject: [PATCH 23/50] Add support for reading numRatings of guides --- classes/CSteamSharedfile.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 6935787..2523b48 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -24,6 +24,7 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { uniqueVisitorsCount: null, favoritesCount: null, upvoteCount: null, + guideNumRatings: null, isUpvoted: null, isDownvoted: null }; @@ -105,6 +106,12 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { } + // 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" From 440b2f9ba98f0c9d3be243b379a3def367ce7dd9 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Mon, 29 May 2023 19:29:24 +0200 Subject: [PATCH 24/50] Add JsDoc --- classes/CSteamSharedfile.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedfile.js index 2523b48..889039a 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedfile.js @@ -107,7 +107,7 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { // Find numRatings if this is a guide as they use a different voting system - let numRatings = $(".ratingSection > .numRatings").text().replace(" ratings", "") + 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 @@ -151,22 +151,20 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { }, "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 recieved - Object.assign(this, data); // TODO: This is cleaner but might break IntelliSense. I'm leaving the block below to be reactivated if necessary - - /* this.id = data.id; - this.type = data.type; - this.appID = data.appID; - this.owner = data.owner; - this.fileSize = data.fileSize; - this.postDate = data.postDate; - this.resolution = data.resolution; - this.uniqueVisitorsCount = data.uniqueVisitorsCount; - this.favoritesCount = data.favoritesCount; - this.upvoteCount = data.upvoteCount; */ + Object.assign(this, data); } /** From be528d1a19b6a293a320bf071dfff029edfce3ff Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Wed, 14 Jun 2023 20:10:00 -0400 Subject: [PATCH 25/50] Change default value for disableMobile to true --- index.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 54d028d..17c73b7 100644 --- a/index.js +++ b/index.js @@ -65,7 +65,7 @@ SteamCommunity.prototype.login = function(details, callback) { this._setCookie(Request.cookie('steamMachineAuth' + parts[0] + '=' + encodeURIComponent(parts[1])), true); } - var disableMobile = details.disableMobile; + var disableMobile = typeof details.disableMobile == 'undefined' ? true : details.disableMobile; var self = this; @@ -123,7 +123,7 @@ SteamCommunity.prototype.login = function(details, callback) { "donotcache": Date.now() }; - if(!disableMobile){ + if (!disableMobile) { formObj.oauth_client_id = "DE45CD61"; formObj.oauth_scope = "read_profile write_profile read_client write_client"; formObj.loginfriendlyname = "#login_emailauth_friendlyname_mobile"; @@ -161,22 +161,20 @@ SteamCommunity.prototype.login = function(details, callback) { 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; + 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){ + if (!disableMobile && body.oauth) { oAuth = JSON.parse(body.oauth); self.steamID = new SteamID(oAuth.steamid); self.oAuthToken = oAuth.oauth_token; - }else{ + } else { for(var i = 0; i < cookies.length; i++) { var parts = cookies[i].split('='); if(parts[0] == 'steamLogin') { @@ -190,7 +188,7 @@ SteamCommunity.prototype.login = function(details, callback) { // Find the Steam Guard cookie var steamguard = null; - for(var i = 0; i < cookies.length; i++) { + 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]); From fd872490c88b566b738531ffd97a7ec07552169c Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Wed, 14 Jun 2023 20:10:14 -0400 Subject: [PATCH 26/50] 3.44.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afd186a..268efa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.44.3", + "version": "3.44.4", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From 6fa6a073a8ef167ddc4dd15ed3b8c2ab2e4be728 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Wed, 14 Jun 2023 20:52:12 -0400 Subject: [PATCH 27/50] Updated 2FA methods to use mobile app access token --- components/helpers.js | 12 ++ components/twofactor.js | 207 ++++++++++++++++------------------ components/webapi.js | 65 ++++++++++- examples/disable_twofactor.js | 35 ++++-- examples/enable_twofactor.js | 75 +++++++----- index.js | 10 ++ 6 files changed, 259 insertions(+), 145 deletions(-) diff --git a/components/helpers.js b/components/helpers.js index 1da5443..d611686 100644 --- a/components/helpers.js +++ b/components/helpers.js @@ -54,3 +54,15 @@ 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')); +} diff --git a/components/twofactor.js b/components/twofactor.js index 651c691..eb5d105 100644 --- a/components/twofactor.js +++ b/components/twofactor.js @@ -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_time: Math.floor(Date.now() / 1000), + authenticator_type: ETwoFactorTokenType.ValveMobileApp, + device_identifier: SteamTotp.getDeviceID(this.steamID), + sms_phone_id: '1' + }, + 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'); +}; diff --git a/components/webapi.js b/components/webapi.js index b1ea5c4..a138ee5 100644 --- a/components/webapi.js +++ b/components/webapi.js @@ -1,5 +1,7 @@ var SteamCommunity = require('../index.js'); +const Helpers = require('./helpers.js'); + SteamCommunity.prototype.getWebApiKey = function(domain, callback) { var self = this; this.httpRequest({ @@ -45,7 +47,7 @@ SteamCommunity.prototype.getWebApiKey = function(domain, callback) { }; /** - * @deprecated No longer works if not logged in via mobile login. Will be removed in a future release. + * @deprecated No longer works. Will be removed in a future release. * @param {function} callback */ SteamCommunity.prototype.getWebApiOauthToken = function(callback) { @@ -53,5 +55,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; + } }; diff --git a/examples/disable_twofactor.js b/examples/disable_twofactor.js index 1c2c617..389ca93 100644 --- a/examples/disable_twofactor.js +++ b/examples/disable_twofactor.js @@ -48,15 +48,34 @@ function doLogin(accountName, password, authCode, captcha, rCode) { } 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(); + if (community.mobileAccessToken) { + // If we already have a mobile access token, we don't need to prompt for one. + doRevoke(rCode); + return; + } + + console.log('You need to provide a mobile app access token to continue.'); + console.log('You can generate one using steam-session (https://www.npmjs.com/package/steam-session).'); + console.log('The access token needs to be generated using EAuthTokenPlatformType.MobileApp.'); + console.log('Make sure you provide an *ACCESS* token, not a refresh token.'); + + rl.question('Access Token: ', (accessToken) => { + community.setMobileAppAccessToken(accessToken); + doRevoke(rCode); }); }); } + +function doRevoke(rCode) { + community.disableTwoFactor('R' + rCode, (err) => { + if (err) { + console.log(err); + process.exit(); + return; + } + + console.log('Two-factor authentication disabled!'); + process.exit(); + }); +} diff --git a/examples/enable_twofactor.js b/examples/enable_twofactor.js index 60495a8..9f95942 100644 --- a/examples/enable_twofactor.js +++ b/examples/enable_twofactor.js @@ -56,41 +56,60 @@ function doLogin(accountName, password, authCode, captcha) { } 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; - } + if (community.mobileAccessToken) { + // If we already have a mobile access token, we don't need to prompt for one. + doSetup(); + return; + } - console.log(err); - process.exit(); - return; - } + console.log('You need to provide a mobile app access token to continue.'); + console.log('You can generate one using steam-session (https://www.npmjs.com/package/steam-session).'); + console.log('The access token needs to be generated using EAuthTokenPlatformType.MobileApp.'); + console.log('Make sure you provide an *ACCESS* token, not a refresh token.'); - 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); + rl.question('Access Token: ', (accessToken) => { + community.setMobileAppAccessToken(accessToken); + 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.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) => { diff --git a/index.js b/index.js index 17c73b7..dc38428 100644 --- a/index.js +++ b/index.js @@ -214,6 +214,12 @@ SteamCommunity.prototype.login = function(details, callback) { } }; +/** + * @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]); @@ -300,6 +306,10 @@ SteamCommunity.prototype.setCookies = function(cookies) { this._setCookie(Request.cookie(cookie), !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/))); }); + + // The account we're logged in as might have changed, so verify that our mobile access token (if any) is still valid + // for this account. + this._verifyMobileAccessToken(); }; SteamCommunity.prototype.getSessionID = function(host = "http://steamcommunity.com") { From 353156008391f8474212681e381c275a81dacd57 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Wed, 14 Jun 2023 20:52:40 -0400 Subject: [PATCH 28/50] 3.45.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 268efa5..a55aa78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.44.4", + "version": "3.45.0", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From 6e19ffd93bd5d629c005abc963b97ed1fe11b4bc Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 20 Jun 2023 21:42:28 -0400 Subject: [PATCH 29/50] Use new endpoints for trade confirmations --- classes/CConfirmation.js | 6 ++- components/confirmations.js | 93 ++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/classes/CConfirmation.js b/classes/CConfirmation.js index 0a450c4..81fb17c 100644 --- a/classes/CConfirmation.js +++ b/classes/CConfirmation.js @@ -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; } diff --git a/components/confirmations.js b/components/confirmations.js index 8c2a579..fd93cf1 100644 --- a/components/confirmations.js +++ b/components/confirmations.js @@ -4,6 +4,7 @@ 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. @@ -14,53 +15,37 @@ var CConfirmation = require('../classes/CConfirmation.js'); 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); - } - + // 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, 'conf', 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('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: conf.creation_time, + icon: conf.icon || '' + })); callback(null, confs); }); @@ -76,22 +61,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 +104,32 @@ 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 + // 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, accept ? 'allow' : 'cancel', { + 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')); }); }; @@ -252,7 +239,7 @@ 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 = { From d23d7e22c0e967f97bec85fa52ffeb830f113681 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 20 Jun 2023 21:42:54 -0400 Subject: [PATCH 30/50] 3.45.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a55aa78..2e8cb7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.45.0", + "version": "3.45.1", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From 37bac4d2405933f15f33a8b68719b193b05cce54 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 20 Jun 2023 21:48:30 -0400 Subject: [PATCH 31/50] Make CConfirmation.timestamp a Date object --- components/confirmations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/confirmations.js b/components/confirmations.js index fd93cf1..ef59551 100644 --- a/components/confirmations.js +++ b/components/confirmations.js @@ -43,7 +43,7 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) { receiving: conf.type == EConfirmationType.Trade ? ((conf.summary || [])[1] || '') : '', sending: (conf.summary || [])[0] || '', time: (new Date(conf.creation_time * 1000)).toISOString(), // for backward compatibility - timestamp: conf.creation_time, + timestamp: new Date(conf.creation_time * 1000), icon: conf.icon || '' })); From 77c2811e7ea354c1d7434d444378b3960e5068dc Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 20 Jun 2023 21:54:44 -0400 Subject: [PATCH 32/50] Use new confirmation tags in acceptConfirmationForObject --- components/confirmations.js | 42 +++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/components/confirmations.js b/components/confirmations.js index ef59551..eeb2229 100644 --- a/components/confirmations.js +++ b/components/confirmations.js @@ -9,14 +9,21 @@ 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; + // 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, 'conf', null, true, function(err, body) { + request(this, 'getlist', key, time, tag, null, true, function(err, body) { if (err) { callback(err); return; @@ -104,8 +111,15 @@ 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) { + // 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, accept ? 'allow' : 'cancel', { + request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, { op: accept ? 'allow' : 'cancel', cid: confID, ck: confKey @@ -166,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; @@ -174,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; } @@ -191,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); }); } }; @@ -231,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 || {}; @@ -239,16 +255,16 @@ function request(community, url, key, time, tag, params, json, callback) { params.a = community.steamID.getSteamID64(); params.k = key; params.t = time; - params.m = "react"; + params.m = 'react'; params.tag = tag; var req = { - "method": url == 'multiajaxop' ? 'POST' : 'GET', - "uri": "https://steamcommunity.com/mobileconf/" + url, - "json": !!json + method: url == 'multiajaxop' ? 'POST' : 'GET', + uri: 'https://steamcommunity.com/mobileconf/' + url, + json: !!json }; - if (req.method == "GET") { + if (req.method == 'GET') { req.qs = params; } else { req.form = params; @@ -261,7 +277,7 @@ function request(community, url, key, time, tag, params, json, callback) { } callback(null, body); - }, "steamcommunity"); + }, 'steamcommunity'); } // Confirmation checker From 76f2c55a73dd932c082a55144835894eafb3aa70 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 20 Jun 2023 21:54:50 -0400 Subject: [PATCH 33/50] 3.45.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e8cb7e..888bab5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.45.1", + "version": "3.45.2", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From 20ad260d680c545dcf741e94a6183961664627f0 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 20 Jun 2023 22:04:37 -0400 Subject: [PATCH 34/50] Remove defunct david-dm badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index a569dc6..499aac3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Steam Community for Node.js [![npm version](https://img.shields.io/npm/v/steamcommunity.svg)](https://npmjs.com/package/steamcommunity) [![npm downloads](https://img.shields.io/npm/dm/steamcommunity.svg)](https://npmjs.com/package/steamcommunity) -[![dependencies](https://img.shields.io/david/DoctorMcKay/node-steamcommunity.svg)](https://david-dm.org/DoctorMcKay/node-steamcommunity) [![license](https://img.shields.io/npm/l/steamcommunity.svg)](https://github.com/DoctorMcKay/node-steamcommunity/blob/master/LICENSE) [![paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=N36YVAT42CZ4G&item_name=node%2dsteamcommunity¤cy_code=USD) From e76d34891abe8b63ad67a318cd610fcd9eccf8c7 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Thu, 22 Jun 2023 00:13:28 -0400 Subject: [PATCH 35/50] Return a descriptive error message to getConfirmations, if available --- components/confirmations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/confirmations.js b/components/confirmations.js index eeb2229..cdf8692 100644 --- a/components/confirmations.js +++ b/components/confirmations.js @@ -37,7 +37,7 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) { return; } - callback(new Error('Failed to get confirmation list')); + callback(new Error(body.message || body.detail || 'Failed to get confirmation list')); return; } From 02cc5daafe45a98f5387f75920570216b0a182e4 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Thu, 22 Jun 2023 00:24:21 -0400 Subject: [PATCH 36/50] 3.45.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 888bab5..82f4872 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.45.2", + "version": "3.45.3", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From 5f46aee9fc0a54beb1e4578d2dc1eafaf16a3b59 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Fri, 23 Jun 2023 01:00:09 -0400 Subject: [PATCH 37/50] Updated enable_twofactor and disable_twofactor examples to use steam-session --- examples/disable_twofactor.js | 158 +++++++++++++++++---------- examples/enable_twofactor.js | 195 +++++++++++++++++++++------------- package.json | 3 + 3 files changed, 228 insertions(+), 128 deletions(-) diff --git a/examples/disable_twofactor.js b/examples/disable_twofactor.js index 389ca93..e03411b 100644 --- a/examples/disable_twofactor.js +++ b/examples/disable_twofactor.js @@ -1,73 +1,81 @@ // If you aren't running this script inside of the repository, replace the following line with: // const SteamCommunity = require('steamcommunity'); const SteamCommunity = require('../index.js'); +const SteamSession = require('steam-session'); const ReadLine = require('readline'); +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); + + // 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 accessToken = session.accessToken; + let cookies = await session.getWebCookies(); + + community.setCookies(cookies); + community.setMobileAppAccessToken(accessToken); + + doRevoke(); }); -}); -function doLogin(accountName, password, authCode, captcha, rCode) { - community.login({ - accountName: accountName, - password: password, - twoFactorCode: authCode, - captcha: captcha - }, (err, sessionID, cookies, steamguard) => { - if (err) { - if (err.message == 'SteamGuard') { - console.log('This account does not have two-factor authentication enabled.'); - process.exit(); - return; + session.on('timeout', () => { + abortPrompt(); + console.log('This login attempt has timed out.'); + }); + + session.on('error', (err) => { + abortPrompt(); + + // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while + // polling, e.g. the network connection goes down or Steam chokes on something. + + console.log(`ERROR: This login attempt has failed! ${err.message}`); + }); + + // Start our login attempt + let startResult = await session.startWithCredentials({accountName, password}); + if (startResult.actionRequired) { + // Some Steam Guard action is required. We only care about email and device codes; in theory an + // EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those. + // If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically. + // The only consequence of ignoring it here is that we don't print a message to the user indicating that they + // could accept an email or device confirmation. + + let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode]; + let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type)); + if (codeAction) { + if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) { + // We wouldn't expect this to happen since we're trying to disable 2FA, but just in case... + console.log(`A code has been sent to your email address at ${codeAction.detail}.`); + } else { + console.log('You need to provide a Steam Guard Mobile Authenticator code.'); } - if (err.message == 'CAPTCHA') { - console.log(err.captchaurl); - rl.question('CAPTCHA: ', (captchaInput) => { - doLogin(accountName, password, authCode, captchaInput); - }); - - return; + let code = await promptAsync('Code: '); + if (code) { + await session.submitSteamGuardCode(code); } - console.log(err); - process.exit(); - return; + // 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 } - - console.log('Logged on!'); - - if (community.mobileAccessToken) { - // If we already have a mobile access token, we don't need to prompt for one. - doRevoke(rCode); - return; - } - - console.log('You need to provide a mobile app access token to continue.'); - console.log('You can generate one using steam-session (https://www.npmjs.com/package/steam-session).'); - console.log('The access token needs to be generated using EAuthTokenPlatformType.MobileApp.'); - console.log('Make sure you provide an *ACCESS* token, not a refresh token.'); - - rl.question('Access Token: ', (accessToken) => { - community.setMobileAppAccessToken(accessToken); - doRevoke(rCode); - }); - }); + } } -function doRevoke(rCode) { +async function doRevoke() { + let rCode = await promptAsync('Revocation Code: R'); community.disableTwoFactor('R' + rCode, (err) => { if (err) { console.log(err); @@ -79,3 +87,45 @@ function doRevoke(rCode) { 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'); +} diff --git a/examples/enable_twofactor.js b/examples/enable_twofactor.js index 9f95942..a739a02 100644 --- a/examples/enable_twofactor.js +++ b/examples/enable_twofactor.js @@ -1,78 +1,80 @@ // If you aren't running this script inside of the repository, replace the following line with: // const SteamCommunity = require('steamcommunity'); const SteamCommunity = require('../index.js'); +const SteamSession = require('steam-session'); const ReadLine = require('readline'); const FS = require('fs'); 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); + + // 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 accessToken = session.accessToken; + let cookies = await session.getWebCookies(); + + community.setCookies(cookies); + community.setMobileAppAccessToken(accessToken); + + doSetup(); }); -}); -function doLogin(accountName, password, authCode, captcha) { - community.login({ - accountName: accountName, - password: password, - authCode: authCode, - captcha: captcha - }, (err, sessionID, cookies, steamguard) => { - if (err) { - if (err.message == 'SteamGuardMobile') { - console.log('This account already has two-factor authentication enabled.'); - 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); - }); - - return; - } - - console.log(err); - process.exit(); - return; - } - - console.log('Logged on!'); - - if (community.mobileAccessToken) { - // If we already have a mobile access token, we don't need to prompt for one. - doSetup(); - return; - } - - console.log('You need to provide a mobile app access token to continue.'); - console.log('You can generate one using steam-session (https://www.npmjs.com/package/steam-session).'); - console.log('The access token needs to be generated using EAuthTokenPlatformType.MobileApp.'); - console.log('Make sure you provide an *ACCESS* token, not a refresh token.'); - - rl.question('Access Token: ', (accessToken) => { - community.setMobileAppAccessToken(accessToken); - doSetup(); - }); + session.on('timeout', () => { + abortPrompt(); + console.log('This login attempt has timed out.'); }); + + session.on('error', (err) => { + abortPrompt(); + + // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while + // polling, e.g. the network connection goes down or Steam chokes on something. + + console.log(`ERROR: This login attempt has failed! ${err.message}`); + }); + + // Start our login attempt + let startResult = await session.startWithCredentials({accountName, password}); + if (startResult.actionRequired) { + // Some Steam Guard action is required. We only care about email and device codes; in theory an + // EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those. + // If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically. + // The only consequence of ignoring it here is that we don't print a message to the user indicating that they + // could accept an email or device confirmation. + + let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode]; + let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type)); + if (codeAction) { + if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) { + console.log(`A code has been sent to your email address at ${codeAction.detail}.`); + } else { + // We wouldn't expect this to happen since we're trying to enable 2FA, but just in case... + console.log('You need to provide a Steam Guard Mobile Authenticator code.'); + } + + let code = await promptAsync('Code: '); + if (code) { + await session.submitSteamGuardCode(code); + } + + // If we fall through here without submitting a Steam Guard code, that means one of two things: + // 1. The user pressed enter without providing a code, in which case the script will simply exit + // 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled + } + } } function doSetup() { @@ -110,22 +112,67 @@ function doSetup() { }); } -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; - } +async function promptActivationCode(response) { + if (response.phone_number_hint) { + console.log(`A code has been sent to your phone ending in ${response.phone_number_hint}.`); + } + let smsCode = await promptAsync('SMS Code: '); + community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => { + if (err) { + if (err.message == 'Invalid activation code') { console.log(err); - } else { - console.log('Two-factor authentication enabled!'); + promptActivationCode(response); + return; } - process.exit(); + 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'); +} diff --git a/package.json b/package.json index 82f4872..399f69a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "steamid": "^1.1.3", "xml2js": "^0.4.22" }, + "devDependencies": { + "steam-session": "^1.2.3" + }, "engines": { "node": ">=4.0.0" } From 7605dc8a41edcec296e8cc9b7003538a7508379b Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Fri, 23 Jun 2023 01:02:20 -0400 Subject: [PATCH 38/50] Added examples readme --- examples/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..2fca1c9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,19 @@ +# 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 this 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 +``` From d0da148d6b088b7d693f87e11508217b77aa6f0c Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Fri, 23 Jun 2023 01:04:48 -0400 Subject: [PATCH 39/50] Added comment explaining usage of setMobileAppAccessToken --- examples/disable_twofactor.js | 4 ++++ examples/enable_twofactor.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/examples/disable_twofactor.js b/examples/disable_twofactor.js index e03411b..98f9c90 100644 --- a/examples/disable_twofactor.js +++ b/examples/disable_twofactor.js @@ -26,6 +26,10 @@ async function main() { community.setCookies(cookies); community.setMobileAppAccessToken(accessToken); + // Enabling or disabling 2FA is presently the only action in node-steamcommunity which requires an access token. + // In all other cases, using `community.setCookies(cookies)` is all you need to do in order to be logged in, + // although there's never any harm in setting a mobile app access token. + doRevoke(); }); diff --git a/examples/enable_twofactor.js b/examples/enable_twofactor.js index a739a02..69adfda 100644 --- a/examples/enable_twofactor.js +++ b/examples/enable_twofactor.js @@ -29,6 +29,10 @@ async function main() { community.setCookies(cookies); community.setMobileAppAccessToken(accessToken); + // Enabling or disabling 2FA is presently the only action in node-steamcommunity which requires an access token. + // In all other cases, using `community.setCookies(cookies)` is all you need to do in order to be logged in, + // although there's never any harm in setting a mobile app access token. + doSetup(); }); From 2dd5bb490e96725e99db38b084446872dbb3b61d Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Fri, 23 Jun 2023 18:32:50 -0400 Subject: [PATCH 40/50] Added accept_all_confirmations.js example --- examples/README.md | 18 ++- examples/accept_all_confirmations.js | 173 +++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 examples/accept_all_confirmations.js diff --git a/examples/README.md b/examples/README.md index 2fca1c9..5e301a2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,7 +5,7 @@ The files in this directory are example scripts that you can use as a getting-st ## 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 this scripts is by cloning the repository locally, and then running them directly +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: @@ -17,3 +17,19 @@ 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 +``` diff --git a/examples/accept_all_confirmations.js b/examples/accept_all_confirmations.js new file mode 100644 index 0000000..9ab968e --- /dev/null +++ b/examples/accept_all_confirmations.js @@ -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) { + console.log(`A code has been sent to your email address at ${codeAction.detail}.`); + } else { + // We wouldn't expect this to happen since we're trying to enable 2FA, but just in case... + console.log('You need to provide a Steam Guard Mobile Authenticator code.'); + } + + let code = await promptAsync('Code 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'); +} From 36e8c79d87e19381b1bcc6f51d194c4087c32103 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sat, 24 Jun 2023 02:39:16 -0400 Subject: [PATCH 41/50] Some minor updates --- .idea/modules.xml | 4 +- .idea/steamcommunity.iml | 4 +- .idea/vcs.xml | 2 +- ...SteamSharedfile.js => CSteamSharedFile.js} | 52 +++++++++---------- components/sharedfiles.js | 27 +++++----- index.js | 2 +- ...{ESharedfileType.js => ESharedFileType.js} | 4 +- 7 files changed, 48 insertions(+), 47 deletions(-) rename classes/{CSteamSharedfile.js => CSteamSharedFile.js} (83%) rename resources/{ESharedfileType.js => ESharedFileType.js} (85%) diff --git a/.idea/modules.xml b/.idea/modules.xml index 60ab3a1..1d21c64 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,8 +2,8 @@ - + - \ No newline at end of file + diff --git a/.idea/steamcommunity.iml b/.idea/steamcommunity.iml index 3c366a8..52b9af4 100644 --- a/.idea/steamcommunity.iml +++ b/.idea/steamcommunity.iml @@ -5,6 +5,6 @@ - + - \ No newline at end of file + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 18cac0e..33d08e3 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/classes/CSteamSharedfile.js b/classes/CSteamSharedFile.js similarity index 83% rename from classes/CSteamSharedfile.js rename to classes/CSteamSharedFile.js index 889039a..09b31c4 100644 --- a/classes/CSteamSharedfile.js +++ b/classes/CSteamSharedFile.js @@ -1,17 +1,18 @@ const Cheerio = require('cheerio'); const SteamID = require('steamid'); -const Helpers = require('../components/helpers.js'); + const SteamCommunity = require('../index.js'); -const ESharedfileType = require('../resources/ESharedfileType.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 {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) { - +SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { // Construct object holding all the data we can scrape let sharedfile = { id: sharedFileId, @@ -29,7 +30,6 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { isDownvoted: null }; - // Get DOM of sharedfile this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, (err, res, body) => { try { @@ -121,15 +121,15 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || ""; if (breadcrumb.includes("Screenshot")) { - sharedfile.type = ESharedfileType.Screenshot; + sharedfile.type = ESharedFileType.Screenshot; } if (breadcrumb.includes("Artwork")) { - sharedfile.type = ESharedfileType.Artwork; + sharedfile.type = ESharedFileType.Artwork; } if (breadcrumb.includes("Guide")) { - sharedfile.type = ESharedfileType.Guide; + sharedfile.type = ESharedFileType.Guide; } @@ -142,7 +142,7 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { } // Make callback when ID was resolved as otherwise owner will always be null - callback(null, new CSteamSharedfile(this, sharedfile)); + callback(null, new CSteamSharedFile(this, sharedfile)); }); } catch (err) { @@ -152,18 +152,18 @@ SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) { }; /** - * Constructor - Creates a new Sharedfile object + * 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 + * @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) { +function CSteamSharedFile(community, data) { /** * @type {SteamCommunity} */ this._community = community; - // Clone all the data we recieved + // Clone all the data we received Object.assign(this, data); } @@ -172,16 +172,16 @@ function CSteamSharedfile(community, data) { * @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.userID, this.id, cid, callback); +CSteamSharedFile.prototype.deleteComment = function(cid, callback) { + this._community.deleteSharedFileComment(this.userID, 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); +CSteamSharedFile.prototype.favorite = function(callback) { + this._community.favoriteSharedFile(this.id, this.appID, callback); }; /** @@ -189,30 +189,30 @@ CSteamSharedfile.prototype.favorite = function(callback) { * @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); +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); +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); +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); +CSteamSharedFile.prototype.unsubscribe = function(callback) { + this._community.unsubscribeSharedFileComments(this.owner, this.id, callback); }; diff --git a/components/sharedfiles.js b/components/sharedfiles.js index c051943..b4fa2c7 100644 --- a/components/sharedfiles.js +++ b/components/sharedfiles.js @@ -1,6 +1,7 @@ -var SteamCommunity = require('../index.js'); var SteamID = require('steamid'); +var SteamCommunity = require('../index.js'); + /** * Deletes a comment from a sharedfile's comment section @@ -9,7 +10,7 @@ var SteamID = require('steamid'); * @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) { +SteamCommunity.prototype.deleteSharedFileComment = function(userID, sharedFileId, cid, callback) { if (typeof userID === "string") { userID = new SteamID(userID); } @@ -26,7 +27,7 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sharedFileId return; } - callback(null || err); + callback(err); }, "steamcommunity"); }; @@ -36,7 +37,7 @@ SteamCommunity.prototype.deleteSharedfileComment = function(userID, sharedFileId * @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) { +SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, callback) { this.httpRequestPost({ "uri": "https://steamcommunity.com/sharedfiles/favorite", "form": { @@ -49,7 +50,7 @@ SteamCommunity.prototype.favoriteSharedfile = function(sharedFileId, appid, call return; } - callback(null || err); + callback(err); }, "steamcommunity"); }; @@ -60,7 +61,7 @@ SteamCommunity.prototype.favoriteSharedfile = function(sharedFileId, appid, call * @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) { +SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) { if (typeof userID === "string") { userID = new SteamID(userID); } @@ -77,7 +78,7 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sharedFileId, return; } - callback(null || err); + callback(err); }, "steamcommunity"); }; @@ -87,7 +88,7 @@ SteamCommunity.prototype.postSharedfileComment = function(userID, sharedFileId, * @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) { +SteamCommunity.prototype.subscribeSharedFileComments = function(userID, sharedFileId, callback) { if (typeof userID === "string") { userID = new SteamID(userID); } @@ -103,7 +104,7 @@ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sharedFi return; } - callback(null || err); + callback(err); }, "steamcommunity"); }; @@ -113,7 +114,7 @@ SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sharedFi * @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) { +SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, callback) { this.httpRequestPost({ "uri": "https://steamcommunity.com/sharedfiles/unfavorite", "form": { @@ -126,7 +127,7 @@ SteamCommunity.prototype.unfavoriteSharedfile = function(sharedFileId, appid, ca return; } - callback(null || err); + callback(err); }, "steamcommunity"); }; @@ -136,7 +137,7 @@ SteamCommunity.prototype.unfavoriteSharedfile = function(sharedFileId, appid, ca * @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) { +SteamCommunity.prototype.unsubscribeSharedFileComments = function(userID, sharedFileId, callback) { if (typeof userID === "string") { userID = new SteamID(userID); } @@ -152,6 +153,6 @@ SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, shared return; } - callback(null || err); + callback(err); }, "steamcommunity"); }; diff --git a/index.js b/index.js index 6cd2257..e488491 100644 --- a/index.js +++ b/index.js @@ -590,7 +590,7 @@ require('./components/help.js'); require('./classes/CMarketItem.js'); require('./classes/CMarketSearchResult.js'); require('./classes/CSteamGroup.js'); -require('./classes/CSteamSharedfile.js'); +require('./classes/CSteamSharedFile.js'); require('./classes/CSteamUser.js'); /** diff --git a/resources/ESharedfileType.js b/resources/ESharedFileType.js similarity index 85% rename from resources/ESharedfileType.js rename to resources/ESharedFileType.js index fc528d5..21290c8 100644 --- a/resources/ESharedfileType.js +++ b/resources/ESharedFileType.js @@ -1,5 +1,5 @@ /** - * @enum ESharedfileType + * @enum ESharedFileType */ module.exports = { "Screenshot": 0, @@ -10,4 +10,4 @@ module.exports = { "0": "Screenshot", "1": "Artwork", "2": "Guide" -}; \ No newline at end of file +}; From bde2cafaf4cd43c36e241338ec1ea69155d62e63 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sat, 24 Jun 2023 02:43:09 -0400 Subject: [PATCH 42/50] We never want to return a sharedfile without an owner --- classes/CSteamSharedFile.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index 09b31c4..37c6ef9 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -137,10 +137,13 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { let ownerHref = $(".friendBlockLinkOverlay").attr()["href"]; Helpers.resolveVanityURL(ownerHref, (err, data) => { // This request takes <1 sec - if (!err) { - sharedfile.owner = new SteamID(data.steamID); + 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)); }); From 788f34ba7aeb426b9b146be89dc9253bb2a9e3a2 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sat, 24 Jun 2023 02:44:12 -0400 Subject: [PATCH 43/50] Export ESharedFileType --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index e488491..0fc31ed 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,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'); From ab9baeafc6d69e63a3554ef838561c8e5d721b93 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sat, 24 Jun 2023 02:48:26 -0400 Subject: [PATCH 44/50] Return postDate as a Date object --- classes/CSteamSharedFile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index 37c6ef9..c22c184 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -79,7 +79,7 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { // Find postDate and convert to timestamp let posted = detailsStatsObj["Posted"].trim(); - sharedfile.postDate = Date.parse(Helpers.decodeSteamTime(posted)); // Pass String into helper and parse the returned String to get a Unix timestamp + sharedfile.postDate = Helpers.decodeSteamTime(posted); // Find resolution if artwork or screenshot From b2bffddfbeb0c8e273624e70a00eba3914c60579 Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sat, 24 Jun 2023 02:53:15 -0400 Subject: [PATCH 45/50] Fixed wrong owner property --- classes/CSteamSharedFile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/CSteamSharedFile.js b/classes/CSteamSharedFile.js index c22c184..277dbb9 100644 --- a/classes/CSteamSharedFile.js +++ b/classes/CSteamSharedFile.js @@ -176,7 +176,7 @@ function CSteamSharedFile(community, data) { * @param {function} callback - Takes only an Error object/null as the first argument */ CSteamSharedFile.prototype.deleteComment = function(cid, callback) { - this._community.deleteSharedFileComment(this.userID, this.id, cid, callback); + this._community.deleteSharedFileComment(this.owner, this.id, cid, callback); }; /** From 2abc547f4906fbb70f55b5558b6a7258713d490f Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Sat, 24 Jun 2023 03:00:14 -0400 Subject: [PATCH 46/50] 3.46.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 399f69a..0435ca2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.45.3", + "version": "3.46.0", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From a0aee350cc87d7609148f3f5a127eb02d0f05693 Mon Sep 17 00:00:00 2001 From: DoctorMcKay Date: Mon, 26 Jun 2023 01:37:08 -0400 Subject: [PATCH 47/50] Update comment --- examples/accept_all_confirmations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/accept_all_confirmations.js b/examples/accept_all_confirmations.js index 9ab968e..e9518a2 100644 --- a/examples/accept_all_confirmations.js +++ b/examples/accept_all_confirmations.js @@ -56,9 +56,9 @@ async function main() { let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type)); if (codeAction) { if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) { + // We wouldn't expect this to happen since mobile confirmations are only possible with 2FA enabled, but just in case... console.log(`A code has been sent to your email address at ${codeAction.detail}.`); } else { - // We wouldn't expect this to happen since we're trying to enable 2FA, but just in case... console.log('You need to provide a Steam Guard Mobile Authenticator code.'); } From 0fc1120fd3dfbc5f9a9fe7e79f9b187be5afbe10 Mon Sep 17 00:00:00 2001 From: 3urobeat <35304405+HerrEurobeat@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:02:15 +0200 Subject: [PATCH 48/50] Fix resolving vanity #313 --- components/helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/helpers.js b/components/helpers.js index f077790..e887fff 100644 --- a/components/helpers.js +++ b/components/helpers.js @@ -99,8 +99,8 @@ exports.resolveVanityURL = function(url, callback) { return; } - let steamID64 = parsed.profile.steamID64; - let vanityURL = parsed.profile.customURL; + let steamID64 = parsed.profile.steamID64[0]; + let vanityURL = parsed.profile.customURL[0]; callback(null, {"vanityURL": vanityURL, "steamID": steamID64}); }); From 6cc96b51fac0b987c60c36c7af544cc10b2550ea Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 27 Jun 2023 00:17:17 -0400 Subject: [PATCH 49/50] 3.46.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0435ca2..d14dce1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steamcommunity", - "version": "3.46.0", + "version": "3.46.1", "description": "Provides an interface for logging into and interacting with the Steam Community website", "keywords": [ "steam", From f69519f547e4d545d9a3e030dea67f2a1148eedc Mon Sep 17 00:00:00 2001 From: Alex Corn Date: Tue, 27 Jun 2023 00:53:55 -0400 Subject: [PATCH 50/50] Fixed getUserInventoryContexts not properly returning errors for private inventory/profile Closes #305 --- components/users.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/components/users.js b/components/users.js index c0e9a22..df8a90f 100644 --- a/components/users.js +++ b/components/users.js @@ -385,18 +385,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 +397,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"); };