mirror of
https://github.com/DoctorMcKay/node-steamcommunity.git
synced 2025-03-14 15:00:07 +08:00
Merge pull request #306 from HerrEurobeat/feature/steam-sharedfiles
Add full sharedfiles support
This commit is contained in:
commit
4ca0fc83aa
218
classes/CSteamSharedfile.js
Normal file
218
classes/CSteamSharedfile.js
Normal file
@ -0,0 +1,218 @@
|
||||
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');
|
||||
|
||||
|
||||
/**
|
||||
* Scrape a sharedfile's DOM to get all available information
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {function} callback - First argument is null/Error, second is object containing all available information
|
||||
*/
|
||||
SteamCommunity.prototype.getSteamSharedfile = function(sharedFileId, callback) {
|
||||
|
||||
// Construct object holding all the data we can scrape
|
||||
let sharedfile = {
|
||||
id: sharedFileId,
|
||||
type: null,
|
||||
appID: null,
|
||||
owner: null,
|
||||
fileSize: null,
|
||||
postDate: null,
|
||||
resolution: null,
|
||||
uniqueVisitorsCount: null,
|
||||
favoritesCount: null,
|
||||
upvoteCount: null,
|
||||
guideNumRatings: null,
|
||||
isUpvoted: null,
|
||||
isDownvoted: null
|
||||
};
|
||||
|
||||
|
||||
// Get DOM of sharedfile
|
||||
this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, (err, res, body) => {
|
||||
try {
|
||||
|
||||
/* --------------------- Preprocess output --------------------- */
|
||||
|
||||
// Load output into cheerio to make parsing easier
|
||||
let $ = Cheerio.load(body);
|
||||
|
||||
// Dynamically map detailsStatsContainerLeft to detailsStatsContainerRight in an object to make readout easier. It holds size, post date and resolution.
|
||||
let detailsStatsObj = {};
|
||||
let detailsLeft = $(".detailsStatsContainerLeft").children();
|
||||
let detailsRight = $(".detailsStatsContainerRight").children();
|
||||
|
||||
Object.keys(detailsLeft).forEach((e) => { // Dynamically get all details. Don't hardcore so that this also works for guides.
|
||||
if (isNaN(e)) {
|
||||
return; // Ignore invalid entries
|
||||
}
|
||||
|
||||
detailsStatsObj[detailsLeft[e].children[0].data.trim()] = detailsRight[e].children[0].data;
|
||||
});
|
||||
|
||||
// Dynamically map stats_table descriptions to values. This holds Unique Visitors and Current Favorites
|
||||
let statsTableObj = {};
|
||||
let statsTable = $(".stats_table").children();
|
||||
|
||||
Object.keys(statsTable).forEach((e) => {
|
||||
if (isNaN(e)) {
|
||||
return; // Ignore invalid entries
|
||||
}
|
||||
|
||||
// Value description is at index 3, value data at index 1
|
||||
statsTableObj[statsTable[e].children[3].children[0].data] = statsTable[e].children[1].children[0].data.replace(/,/g, ""); // Remove commas from 1k+ values
|
||||
});
|
||||
|
||||
|
||||
/* --------------------- Find and map values --------------------- */
|
||||
|
||||
// Find appID in share button onclick event
|
||||
sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sharedFileId}', '`, "").replace("' );", ""));
|
||||
|
||||
|
||||
// Find fileSize if not guide
|
||||
sharedfile.fileSize = detailsStatsObj["File Size"] || null; // TODO: Convert to bytes? It seems like to always be MB but no guarantee
|
||||
|
||||
|
||||
// Find postDate and convert to timestamp
|
||||
let posted = detailsStatsObj["Posted"].trim();
|
||||
|
||||
sharedfile.postDate = 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 uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false
|
||||
if (statsTableObj["Unique Visitors"]) {
|
||||
sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]);
|
||||
}
|
||||
|
||||
|
||||
// Find favoritesCount. We can't use ' || null' here as Number("0") casts to false
|
||||
if (statsTableObj["Current Favorites"]) {
|
||||
sharedfile.favoritesCount = Number(statsTableObj["Current Favorites"]);
|
||||
}
|
||||
|
||||
|
||||
// Find upvoteCount. We can't use ' || null' here as Number("0") casts to false
|
||||
let upvoteCount = $("#VotesUpCountContainer > #VotesUpCount").text();
|
||||
|
||||
if (upvoteCount) {
|
||||
sharedfile.upvoteCount = Number(upvoteCount);
|
||||
}
|
||||
|
||||
|
||||
// Find numRatings if this is a guide as they use a different voting system
|
||||
let numRatings = $(".ratingSection > .numRatings").text().replace(" ratings", "");
|
||||
|
||||
sharedfile.guideNumRatings = Number(numRatings) || null; // Set to null if not a guide or if the guide does not have enough ratings to show a value
|
||||
|
||||
|
||||
// Determine if this account has already voted on this sharedfile
|
||||
sharedfile.isUpvoted = String($(".workshopItemControlCtn > #VoteUpBtn")[0].attribs["class"]).includes("toggled"); // Check if upvote btn class contains "toggled"
|
||||
sharedfile.isDownvoted = String($(".workshopItemControlCtn > #VoteDownBtn")[0].attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled"
|
||||
|
||||
|
||||
// Determine type by looking at the second breadcrumb. Find the first separator as it has a unique name and go to the next element which holds our value of interest
|
||||
let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || "";
|
||||
|
||||
if (breadcrumb.includes("Screenshot")) {
|
||||
sharedfile.type = ESharedfileType.Screenshot;
|
||||
}
|
||||
|
||||
if (breadcrumb.includes("Artwork")) {
|
||||
sharedfile.type = ESharedfileType.Artwork;
|
||||
}
|
||||
|
||||
if (breadcrumb.includes("Guide")) {
|
||||
sharedfile.type = ESharedfileType.Guide;
|
||||
}
|
||||
|
||||
|
||||
// Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object
|
||||
let ownerHref = $(".friendBlockLinkOverlay").attr()["href"];
|
||||
|
||||
Helpers.resolveVanityURL(ownerHref, (err, data) => { // This request takes <1 sec
|
||||
if (!err) {
|
||||
sharedfile.owner = new SteamID(data.steamID);
|
||||
}
|
||||
|
||||
// Make callback when ID was resolved as otherwise owner will always be null
|
||||
callback(null, new CSteamSharedfile(this, sharedfile));
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
callback(err, null);
|
||||
}
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor - Creates a new Sharedfile object
|
||||
* @class
|
||||
* @param {SteamCommunity} community
|
||||
* @param {{ id: string, type: ESharedfileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data
|
||||
*/
|
||||
function CSteamSharedfile(community, data) {
|
||||
/**
|
||||
* @type {SteamCommunity}
|
||||
*/
|
||||
this._community = community;
|
||||
|
||||
// Clone all the data we recieved
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a comment from this sharedfile's comment section
|
||||
* @param {String} cid - ID of the comment to delete
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedfile.prototype.deleteComment = function(cid, callback) {
|
||||
this._community.deleteSharedfileComment(this.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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a comment to this sharedfile
|
||||
* @param {String} message - Content of the comment to post
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedfile.prototype.comment = function(message, callback) {
|
||||
this._community.postSharedfileComment(this.owner, this.id, message, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to this sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedfile.prototype.subscribe = function(callback) {
|
||||
this._community.subscribeSharedfileComments(this.owner, this.id, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unfavorites this sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedfile.prototype.unfavorite = function(callback) {
|
||||
this._community.unfavoriteSharedfile(this.id, this.appID, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribes from this sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
CSteamSharedfile.prototype.unsubscribe = function(callback) {
|
||||
this._community.unsubscribeSharedfileComments(this.owner, this.id, callback);
|
||||
};
|
@ -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);
|
||||
@ -65,4 +67,42 @@ exports.decodeJwt = function(jwt) {
|
||||
.replace(/_/g, '/');
|
||||
|
||||
return JSON.parse(Buffer.from(standardBase64, 'base64').toString('utf8'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a Steam profile URL to get steamID64 and vanityURL
|
||||
* @param {String} url - Full steamcommunity profile URL or only the vanity part.
|
||||
* @param {Object} callback - First argument is null/Error, second is object containing vanityURL (String) and steamID (String)
|
||||
*/
|
||||
exports.resolveVanityURL = function(url, callback) {
|
||||
// Precede url param if only the vanity was provided
|
||||
if (!url.includes("steamcommunity.com")) {
|
||||
url = "https://steamcommunity.com/id/" + url;
|
||||
}
|
||||
|
||||
// Make request to get XML data
|
||||
request(url + "/?xml=1", function(err, response, body) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse XML data returned from Steam into an object
|
||||
new xml2js.Parser().parseString(body, (err, parsed) => {
|
||||
if (err) {
|
||||
callback(new Error("Couldn't parse XML response"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.response && parsed.response.error) {
|
||||
callback(new Error("Couldn't find Steam ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
let steamID64 = parsed.profile.steamID64;
|
||||
let vanityURL = parsed.profile.customURL;
|
||||
|
||||
callback(null, {"vanityURL": vanityURL, "steamID": steamID64});
|
||||
});
|
||||
});
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
var CEconItem = require('../classes/CEconItem.js');
|
||||
var Helpers = require('./helpers.js');
|
||||
var SteamID = require('steamid');
|
||||
var request = require('request');
|
||||
var Cheerio = require('cheerio');
|
||||
@ -142,7 +143,7 @@ SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
|
||||
}
|
||||
|
||||
if (options.resolveVanityURLs) {
|
||||
Async.map(vanityURLs, resolveVanityURL, function(err, results) {
|
||||
Async.map(vanityURLs, Helpers.resolveVanityURL, function(err, results) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@ -170,19 +171,3 @@ SteamCommunity.prototype.getInventoryHistory = function(options, callback) {
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
function resolveVanityURL(vanityURL, callback) {
|
||||
request("https://steamcommunity.com/id/" + vanityURL + "/?xml=1", function(err, response, body) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var match = body.match(/<steamID64>(\d+)<\/steamID64>/);
|
||||
if (!match || !match[1]) {
|
||||
callback(new Error("Couldn't find Steam ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, {"vanityURL": vanityURL, "steamID": match[1]});
|
||||
});
|
||||
}
|
||||
|
157
components/sharedfiles.js
Normal file
157
components/sharedfiles.js
Normal file
@ -0,0 +1,157 @@
|
||||
var SteamCommunity = require('../index.js');
|
||||
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} sharedFileId - ID of the sharedfile
|
||||
* @param {String} cid - ID of the comment to delete
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.deleteSharedfileComment = function(userID, sharedFileId, cid, callback) {
|
||||
if (typeof userID === "string") {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`,
|
||||
"form": {
|
||||
"gidcomment": cid,
|
||||
"count": 10,
|
||||
"sessionid": this.getSessionID()
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null || err);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
/**
|
||||
* Favorites a sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} appid - ID of the app associated to this sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.favoriteSharedfile = function(sharedFileId, appid, callback) {
|
||||
this.httpRequestPost({
|
||||
"uri": "https://steamcommunity.com/sharedfiles/favorite",
|
||||
"form": {
|
||||
"id": sharedFileId,
|
||||
"appid": appid,
|
||||
"sessionid": this.getSessionID()
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null || err);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a comment to a sharedfile
|
||||
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} message - Content of the comment to post
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.postSharedfileComment = function(userID, sharedFileId, message, callback) {
|
||||
if (typeof userID === "string") {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`,
|
||||
"form": {
|
||||
"comment": message,
|
||||
"count": 10,
|
||||
"sessionid": this.getSessionID()
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null || err);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to a sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||
* @param {SteamID | String} userID ID of the user associated to this sharedfile
|
||||
* @param {String} sharedFileId ID of the sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.subscribeSharedfileComments = function(userID, sharedFileId, callback) {
|
||||
if (typeof userID === "string") {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`,
|
||||
"form": {
|
||||
"count": 10,
|
||||
"sessionid": this.getSessionID()
|
||||
}
|
||||
}, function(err, response, body) { // eslint-disable-line
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null || err);
|
||||
}, "steamcommunity");
|
||||
};
|
||||
|
||||
/**
|
||||
* Unfavorites a sharedfile
|
||||
* @param {String} sharedFileId - ID of the sharedfile
|
||||
* @param {String} appid - ID of the app associated to this sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.unfavoriteSharedfile = function(sharedFileId, appid, callback) {
|
||||
this.httpRequestPost({
|
||||
"uri": "https://steamcommunity.com/sharedfiles/unfavorite",
|
||||
"form": {
|
||||
"id": sharedFileId,
|
||||
"appid": appid,
|
||||
"sessionid": this.getSessionID()
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(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} sharedFileId - ID of the sharedfile
|
||||
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||
*/
|
||||
SteamCommunity.prototype.unsubscribeSharedfileComments = function(userID, sharedFileId, callback) {
|
||||
if (typeof userID === "string") {
|
||||
userID = new SteamID(userID);
|
||||
}
|
||||
|
||||
this.httpRequestPost({
|
||||
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`,
|
||||
"form": {
|
||||
"count": 10,
|
||||
"sessionid": this.getSessionID()
|
||||
}
|
||||
}, function(err, response, body) { // eslint-disable-line
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null || err);
|
||||
}, "steamcommunity");
|
||||
};
|
2
index.js
2
index.js
@ -581,6 +581,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');
|
||||
@ -589,6 +590,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');
|
||||
|
||||
/**
|
||||
|
13
resources/ESharedfileType.js
Normal file
13
resources/ESharedfileType.js
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @enum ESharedfileType
|
||||
*/
|
||||
module.exports = {
|
||||
"Screenshot": 0,
|
||||
"Artwork": 1,
|
||||
"Guide": 2,
|
||||
|
||||
// Value-to-name mapping for convenience
|
||||
"0": "Screenshot",
|
||||
"1": "Artwork",
|
||||
"2": "Guide"
|
||||
};
|
Loading…
Reference in New Issue
Block a user