mirror of
https://github.com/DoctorMcKay/node-steamcommunity.git
synced 2025-01-14 12:50:11 +08:00
Merge branch 'master' into v4
# Conflicts: # components/confirmations.js # components/inventoryhistory.js # components/twofactor.js # components/users.js # components/webapi.js # index.js # package.json
This commit is contained in:
commit
86e87e88ed
@ -2,8 +2,8 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/../node-steamcommunity Wiki/.idea/node-steamcommunity Wiki.iml" filepath="$PROJECT_DIR$/../node-steamcommunity Wiki/.idea/node-steamcommunity Wiki.iml" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/steamcommunity.iml" filepath="$PROJECT_DIR$/.idea/steamcommunity.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/steamcommunity.iml" filepath="$PROJECT_DIR$/.idea/steamcommunity.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/../steamcommunity Wiki/.idea/steamcommunity Wiki.iml" filepath="$PROJECT_DIR$/../steamcommunity Wiki/.idea/steamcommunity Wiki.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="module" module-name="node-steamcommunity Wiki" />
|
<orderEntry type="module" module-name="steamcommunity Wiki" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/../node-steamcommunity Wiki" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/../steamcommunity Wiki" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -1,7 +1,6 @@
|
|||||||
# Steam Community for Node.js
|
# Steam Community for Node.js
|
||||||
[![npm version](https://img.shields.io/npm/v/steamcommunity.svg)](https://npmjs.com/package/steamcommunity)
|
[![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)
|
[![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)
|
[![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)
|
[![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)
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ function CConfirmation(community, data) {
|
|||||||
this.key = data.key;
|
this.key = data.key;
|
||||||
this.title = data.title;
|
this.title = data.title;
|
||||||
this.receiving = data.receiving;
|
this.receiving = data.receiving;
|
||||||
|
this.sending = data.sending;
|
||||||
this.time = data.time;
|
this.time = data.time;
|
||||||
|
this.timestamp = data.timestamp;
|
||||||
this.icon = data.icon;
|
this.icon = data.icon;
|
||||||
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null;
|
this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null;
|
||||||
}
|
}
|
||||||
|
221
classes/CSteamSharedFile.js
Normal file
221
classes/CSteamSharedFile.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
const Cheerio = require('cheerio');
|
||||||
|
const SteamID = require('steamid');
|
||||||
|
|
||||||
|
const SteamCommunity = require('../index.js');
|
||||||
|
const Helpers = require('../components/helpers.js');
|
||||||
|
|
||||||
|
const ESharedFileType = require('../resources/ESharedFileType.js');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrape a sharedfile's DOM to get all available information
|
||||||
|
* @param {string} sharedFileId - ID of the sharedfile
|
||||||
|
* @param {function} callback - First argument is null/Error, second is object containing all available information
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) {
|
||||||
|
// Construct object holding all the data we can scrape
|
||||||
|
let sharedfile = {
|
||||||
|
id: sharedFileId,
|
||||||
|
type: null,
|
||||||
|
appID: null,
|
||||||
|
owner: null,
|
||||||
|
fileSize: null,
|
||||||
|
postDate: null,
|
||||||
|
resolution: null,
|
||||||
|
uniqueVisitorsCount: null,
|
||||||
|
favoritesCount: null,
|
||||||
|
upvoteCount: null,
|
||||||
|
guideNumRatings: null,
|
||||||
|
isUpvoted: null,
|
||||||
|
isDownvoted: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get DOM of sharedfile
|
||||||
|
this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}`, (err, res, body) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
/* --------------------- Preprocess output --------------------- */
|
||||||
|
|
||||||
|
// Load output into cheerio to make parsing easier
|
||||||
|
let $ = Cheerio.load(body);
|
||||||
|
|
||||||
|
// Dynamically map detailsStatsContainerLeft to detailsStatsContainerRight in an object to make readout easier. It holds size, post date and resolution.
|
||||||
|
let detailsStatsObj = {};
|
||||||
|
let detailsLeft = $(".detailsStatsContainerLeft").children();
|
||||||
|
let detailsRight = $(".detailsStatsContainerRight").children();
|
||||||
|
|
||||||
|
Object.keys(detailsLeft).forEach((e) => { // Dynamically get all details. Don't hardcore so that this also works for guides.
|
||||||
|
if (isNaN(e)) {
|
||||||
|
return; // Ignore invalid entries
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsStatsObj[detailsLeft[e].children[0].data.trim()] = detailsRight[e].children[0].data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamically map stats_table descriptions to values. This holds Unique Visitors and Current Favorites
|
||||||
|
let statsTableObj = {};
|
||||||
|
let statsTable = $(".stats_table").children();
|
||||||
|
|
||||||
|
Object.keys(statsTable).forEach((e) => {
|
||||||
|
if (isNaN(e)) {
|
||||||
|
return; // Ignore invalid entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value description is at index 3, value data at index 1
|
||||||
|
statsTableObj[statsTable[e].children[3].children[0].data] = statsTable[e].children[1].children[0].data.replace(/,/g, ""); // Remove commas from 1k+ values
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/* --------------------- Find and map values --------------------- */
|
||||||
|
|
||||||
|
// Find appID in share button onclick event
|
||||||
|
sharedfile.appID = Number($("#ShareItemBtn").attr()["onclick"].replace(`ShowSharePublishedFilePopup( '${sharedFileId}', '`, "").replace("' );", ""));
|
||||||
|
|
||||||
|
|
||||||
|
// Find fileSize if not guide
|
||||||
|
sharedfile.fileSize = detailsStatsObj["File Size"] || null; // TODO: Convert to bytes? It seems like to always be MB but no guarantee
|
||||||
|
|
||||||
|
|
||||||
|
// Find postDate and convert to timestamp
|
||||||
|
let posted = detailsStatsObj["Posted"].trim();
|
||||||
|
|
||||||
|
sharedfile.postDate = Helpers.decodeSteamTime(posted);
|
||||||
|
|
||||||
|
|
||||||
|
// Find resolution if artwork or screenshot
|
||||||
|
sharedfile.resolution = detailsStatsObj["Size"] || null;
|
||||||
|
|
||||||
|
|
||||||
|
// Find uniqueVisitorsCount. We can't use ' || null' here as Number("0") casts to false
|
||||||
|
if (statsTableObj["Unique Visitors"]) {
|
||||||
|
sharedfile.uniqueVisitorsCount = Number(statsTableObj["Unique Visitors"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find favoritesCount. We can't use ' || null' here as Number("0") casts to false
|
||||||
|
if (statsTableObj["Current Favorites"]) {
|
||||||
|
sharedfile.favoritesCount = Number(statsTableObj["Current Favorites"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find upvoteCount. We can't use ' || null' here as Number("0") casts to false
|
||||||
|
let upvoteCount = $("#VotesUpCountContainer > #VotesUpCount").text();
|
||||||
|
|
||||||
|
if (upvoteCount) {
|
||||||
|
sharedfile.upvoteCount = Number(upvoteCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find numRatings if this is a guide as they use a different voting system
|
||||||
|
let numRatings = $(".ratingSection > .numRatings").text().replace(" ratings", "");
|
||||||
|
|
||||||
|
sharedfile.guideNumRatings = Number(numRatings) || null; // Set to null if not a guide or if the guide does not have enough ratings to show a value
|
||||||
|
|
||||||
|
|
||||||
|
// Determine if this account has already voted on this sharedfile
|
||||||
|
sharedfile.isUpvoted = String($(".workshopItemControlCtn > #VoteUpBtn")[0].attribs["class"]).includes("toggled"); // Check if upvote btn class contains "toggled"
|
||||||
|
sharedfile.isDownvoted = String($(".workshopItemControlCtn > #VoteDownBtn")[0].attribs["class"]).includes("toggled"); // Check if downvote btn class contains "toggled"
|
||||||
|
|
||||||
|
|
||||||
|
// Determine type by looking at the second breadcrumb. Find the first separator as it has a unique name and go to the next element which holds our value of interest
|
||||||
|
let breadcrumb = $(".breadcrumbs > .breadcrumb_separator").next().get(0).children[0].data || "";
|
||||||
|
|
||||||
|
if (breadcrumb.includes("Screenshot")) {
|
||||||
|
sharedfile.type = ESharedFileType.Screenshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breadcrumb.includes("Artwork")) {
|
||||||
|
sharedfile.type = ESharedFileType.Artwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breadcrumb.includes("Guide")) {
|
||||||
|
sharedfile.type = ESharedFileType.Guide;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find owner profile link, convert to steamID64 using SteamIdResolver lib and create a SteamID object
|
||||||
|
let ownerHref = $(".friendBlockLinkOverlay").attr()["href"];
|
||||||
|
|
||||||
|
Helpers.resolveVanityURL(ownerHref, (err, data) => { // This request takes <1 sec
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedfile.owner = new SteamID(data.steamID);
|
||||||
|
|
||||||
|
// Make callback when ID was resolved as otherwise owner will always be null
|
||||||
|
callback(null, new CSteamSharedFile(this, sharedfile));
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
callback(err, null);
|
||||||
|
}
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor - Creates a new SharedFile object
|
||||||
|
* @class
|
||||||
|
* @param {SteamCommunity} community
|
||||||
|
* @param {{ id: string, type: ESharedFileType, appID: number, owner: SteamID|null, fileSize: string|null, postDate: number, resolution: string|null, uniqueVisitorsCount: number, favoritesCount: number, upvoteCount: number|null, guideNumRatings: Number|null, isUpvoted: boolean, isDownvoted: boolean }} data
|
||||||
|
*/
|
||||||
|
function CSteamSharedFile(community, data) {
|
||||||
|
/**
|
||||||
|
* @type {SteamCommunity}
|
||||||
|
*/
|
||||||
|
this._community = community;
|
||||||
|
|
||||||
|
// Clone all the data we received
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a comment from this sharedfile's comment section
|
||||||
|
* @param {String} cid - ID of the comment to delete
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
CSteamSharedFile.prototype.deleteComment = function(cid, callback) {
|
||||||
|
this._community.deleteSharedFileComment(this.owner, this.id, cid, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Favorites this sharedfile
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
CSteamSharedFile.prototype.favorite = function(callback) {
|
||||||
|
this._community.favoriteSharedFile(this.id, this.appID, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts a comment to this sharedfile
|
||||||
|
* @param {String} message - Content of the comment to post
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
CSteamSharedFile.prototype.comment = function(message, callback) {
|
||||||
|
this._community.postSharedFileComment(this.owner, this.id, message, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to this sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
CSteamSharedFile.prototype.subscribe = function(callback) {
|
||||||
|
this._community.subscribeSharedFileComments(this.owner, this.id, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfavorites this sharedfile
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
CSteamSharedFile.prototype.unfavorite = function(callback) {
|
||||||
|
this._community.unfavoriteSharedFile(this.id, this.appID, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes from this sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
CSteamSharedFile.prototype.unsubscribe = function(callback) {
|
||||||
|
this._community.unsubscribeSharedFileComments(this.owner, this.id, callback);
|
||||||
|
};
|
@ -4,61 +4,55 @@ const SteamTotp = require('steam-totp');
|
|||||||
const SteamCommunity = require('../index.js');
|
const SteamCommunity = require('../index.js');
|
||||||
|
|
||||||
const CConfirmation = require('../classes/CConfirmation.js');
|
const CConfirmation = require('../classes/CConfirmation.js');
|
||||||
|
var EConfirmationType = require('../resources/EConfirmationType.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of your account's currently outstanding confirmations.
|
* Get a list of your account's currently outstanding confirmations.
|
||||||
* @param {int} time - The unix timestamp with which the following key was generated
|
* @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
|
* @param {SteamCommunity~getConfirmations} callback - Called when the list of confirmations is received
|
||||||
*/
|
*/
|
||||||
SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
|
SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
|
||||||
request(this, 'conf', key, time, 'conf', null, false, (err, body) => {
|
var self = this;
|
||||||
if (err) {
|
|
||||||
if (err.message == 'Invalid protocol: steammobile:') {
|
|
||||||
err.message = 'Not Logged In';
|
|
||||||
this._notifySessionExpired(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Ugly hack to maintain backward compatibility
|
||||||
|
var tag = 'conf';
|
||||||
|
if (typeof key == 'object') {
|
||||||
|
tag = key.tag;
|
||||||
|
key = key.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The official Steam app uses the tag 'list', but 'conf' still works so let's use that for backward compatibility.
|
||||||
|
request(this, 'getlist', key, time, tag, null, true, function(err, body) {
|
||||||
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let $ = Cheerio.load(body);
|
if (!body.success) {
|
||||||
let empty = $('#mobileconf_empty');
|
if (body.needauth) {
|
||||||
if (empty.length > 0) {
|
var err = new Error('Not Logged In');
|
||||||
if (!$(empty).hasClass('mobileconf_done')) {
|
self._notifySessionExpired(err);
|
||||||
// An error occurred
|
callback(err);
|
||||||
callback(new Error(empty.find('div:nth-of-type(2)').text()));
|
return;
|
||||||
} else {
|
|
||||||
callback(null, []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callback(new Error(body.message || body.detail || 'Failed to get confirmation list'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have something to confirm
|
var confs = (body.conf || []).map(conf => new CConfirmation(self, {
|
||||||
let confirmations = $('#mobileconf_list');
|
id: conf.id,
|
||||||
if (!confirmations) {
|
type: conf.type,
|
||||||
callback(new Error('Malformed response'));
|
creator: conf.creator_id,
|
||||||
return;
|
key: conf.nonce,
|
||||||
}
|
title: `${conf.type_name || 'Confirm'} - ${conf.headline || ''}`,
|
||||||
|
receiving: conf.type == EConfirmationType.Trade ? ((conf.summary || [])[1] || '') : '',
|
||||||
let confs = [];
|
sending: (conf.summary || [])[0] || '',
|
||||||
Array.prototype.forEach.call(confirmations.find('.mobileconf_list_entry'), (conf) => {
|
time: (new Date(conf.creation_time * 1000)).toISOString(), // for backward compatibility
|
||||||
conf = $(conf);
|
timestamp: new Date(conf.creation_time * 1000),
|
||||||
|
icon: conf.icon || ''
|
||||||
let img = conf.find('.mobileconf_list_entry_icon img');
|
}));
|
||||||
confs.push(new CConfirmation(this, {
|
|
||||||
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')
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
callback(null, confs);
|
callback(null, confs);
|
||||||
});
|
});
|
||||||
@ -74,22 +68,23 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) {
|
|||||||
* Get the trade offer ID associated with a particular confirmation
|
* Get the trade offer ID associated with a particular confirmation
|
||||||
* @param {int} confID - The ID of the confirmation in question
|
* @param {int} confID - The ID of the confirmation in question
|
||||||
* @param {int} time - The unix timestamp with which the following key was generated
|
* @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
|
* @param {SteamCommunity~getConfirmationOfferID} callback
|
||||||
*/
|
*/
|
||||||
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
|
SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) {
|
||||||
request(this, 'details/' + confID, key, time, 'details', null, true, (err, body) => {
|
// 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) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.success) {
|
if (typeof body != 'string') {
|
||||||
callback(new Error('Cannot load confirmation details'));
|
callback(new Error('Cannot load confirmation details'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let $ = Cheerio.load(body.html);
|
let $ = Cheerio.load(body);
|
||||||
let offer = $('.tradeoffer');
|
let offer = $('.tradeoffer');
|
||||||
if (offer.length < 1) {
|
if (offer.length < 1) {
|
||||||
callback(null, null);
|
callback(null, null);
|
||||||
@ -116,11 +111,19 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca
|
|||||||
* @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete
|
* @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete
|
||||||
*/
|
*/
|
||||||
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
|
SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) {
|
||||||
request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, accept ? 'allow' : 'cancel', {
|
// Ugly hack to maintain backward compatibility
|
||||||
|
var tag = accept ? 'allow' : 'cancel';
|
||||||
|
if (typeof key == 'object') {
|
||||||
|
tag = key.tag;
|
||||||
|
key = key.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility
|
||||||
|
request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, {
|
||||||
op: accept ? 'allow' : 'cancel',
|
op: accept ? 'allow' : 'cancel',
|
||||||
cid: confID,
|
cid: confID,
|
||||||
ck: confKey
|
ck: confKey
|
||||||
}, true, (err, body) => {
|
}, true, function(err, body) {
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -203,7 +206,39 @@ SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doConfirmation() {
|
||||||
|
var offset = self._timeOffset;
|
||||||
|
var time = SteamTotp.time(offset);
|
||||||
|
var confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'list');
|
||||||
|
self.getConfirmations(time, {tag: 'list', key: confKey}, function(err, confs) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var conf = confs.filter(function(conf) { return conf.creator == objectID; });
|
||||||
|
if (conf.length == 0) {
|
||||||
|
callback(new Error('Could not find confirmation for object ' + objectID));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conf = conf[0];
|
||||||
|
|
||||||
|
// make sure we don't reuse the same time
|
||||||
|
var localOffset = 0;
|
||||||
|
do {
|
||||||
|
time = SteamTotp.time(offset) + localOffset++;
|
||||||
|
} while (self._usedConfTimes.indexOf(time) != -1);
|
||||||
|
|
||||||
|
self._usedConfTimes.push(time);
|
||||||
|
if (self._usedConfTimes.length > 60) {
|
||||||
|
self._usedConfTimes.splice(0, self._usedConfTimes.length - 60); // we don't need to save more than 60 entries
|
||||||
|
}
|
||||||
|
|
||||||
|
confKey = SteamTotp.getConfirmationKey(identitySecret, time, 'accept');
|
||||||
|
conf.respond(time, {tag: 'accept', key: confKey}, true, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -247,7 +282,7 @@ function request(community, url, key, time, tag, params, json, callback) {
|
|||||||
params.a = community.steamID.getSteamID64();
|
params.a = community.steamID.getSteamID64();
|
||||||
params.k = key;
|
params.k = key;
|
||||||
params.t = time;
|
params.t = time;
|
||||||
params.m = 'android';
|
params.m = 'react';
|
||||||
params.tag = tag;
|
params.tag = tag;
|
||||||
|
|
||||||
let req = {
|
let req = {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
const EResult = require('../resources/EResult.js');
|
const EResult = require('../resources/EResult.js');
|
||||||
|
const request = require('request');
|
||||||
|
const xml2js = require('xml2js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure that a provided input is a valid SteamID object.
|
* Make sure that a provided input is a valid SteamID object.
|
||||||
@ -50,3 +52,53 @@ exports.eresultError = function(eresult, message) {
|
|||||||
err.eresult = eresult;
|
err.eresult = eresult;
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.decodeJwt = function(jwt) {
|
||||||
|
let parts = jwt.split('.');
|
||||||
|
if (parts.length != 3) {
|
||||||
|
throw new Error('Invalid JWT');
|
||||||
|
}
|
||||||
|
|
||||||
|
let standardBase64 = parts[1].replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
return JSON.parse(Buffer.from(standardBase64, 'base64').toString('utf8'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a Steam profile URL to get steamID64 and vanityURL
|
||||||
|
* @param {String} url - Full steamcommunity profile URL or only the vanity part.
|
||||||
|
* @param {Object} callback - First argument is null/Error, second is object containing vanityURL (String) and steamID (String)
|
||||||
|
*/
|
||||||
|
exports.resolveVanityURL = function(url, callback) {
|
||||||
|
// Precede url param if only the vanity was provided
|
||||||
|
if (!url.includes("steamcommunity.com")) {
|
||||||
|
url = "https://steamcommunity.com/id/" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make request to get XML data
|
||||||
|
request(url + "/?xml=1", function(err, response, body) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse XML data returned from Steam into an object
|
||||||
|
new xml2js.Parser().parseString(body, (err, parsed) => {
|
||||||
|
if (err) {
|
||||||
|
callback(new Error("Couldn't parse XML response"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.response && parsed.response.error) {
|
||||||
|
callback(new Error("Couldn't find Steam ID"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let steamID64 = parsed.profile.steamID64[0];
|
||||||
|
let vanityURL = parsed.profile.customURL[0];
|
||||||
|
|
||||||
|
callback(null, {"vanityURL": vanityURL, "steamID": steamID64});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
@ -108,6 +108,15 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
|
|||||||
values.customURL = settings[i];
|
values.customURL = settings[i];
|
||||||
break;
|
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
|
// These don't work right now
|
||||||
/*
|
/*
|
||||||
case 'background':
|
case 'background':
|
||||||
@ -119,15 +128,6 @@ SteamCommunity.prototype.editProfile = function(settings, callback) {
|
|||||||
// Currently, game badges aren't supported
|
// Currently, game badges aren't supported
|
||||||
values.favorite_badge_badgeid = settings[i];
|
values.favorite_badge_badgeid = settings[i];
|
||||||
break;
|
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
|
// TODO: profile showcases
|
||||||
}
|
}
|
||||||
|
158
components/sharedfiles.js
Normal file
158
components/sharedfiles.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
var SteamID = require('steamid');
|
||||||
|
|
||||||
|
var SteamCommunity = require('../index.js');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a comment from a sharedfile's comment section
|
||||||
|
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||||
|
* @param {String} sharedFileId - ID of the sharedfile
|
||||||
|
* @param {String} cid - ID of the comment to delete
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.deleteSharedFileComment = function(userID, sharedFileId, cid, callback) {
|
||||||
|
if (typeof userID === "string") {
|
||||||
|
userID = new SteamID(userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpRequestPost({
|
||||||
|
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/delete/${userID.toString()}/${sharedFileId}/`,
|
||||||
|
"form": {
|
||||||
|
"gidcomment": cid,
|
||||||
|
"count": 10,
|
||||||
|
"sessionid": this.getSessionID()
|
||||||
|
}
|
||||||
|
}, function(err, response, body) {
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err);
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Favorites a sharedfile
|
||||||
|
* @param {String} sharedFileId - ID of the sharedfile
|
||||||
|
* @param {String} appid - ID of the app associated to this sharedfile
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.favoriteSharedFile = function(sharedFileId, appid, callback) {
|
||||||
|
this.httpRequestPost({
|
||||||
|
"uri": "https://steamcommunity.com/sharedfiles/favorite",
|
||||||
|
"form": {
|
||||||
|
"id": sharedFileId,
|
||||||
|
"appid": appid,
|
||||||
|
"sessionid": this.getSessionID()
|
||||||
|
}
|
||||||
|
}, function(err, response, body) {
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err);
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts a comment to a sharedfile
|
||||||
|
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||||
|
* @param {String} sharedFileId - ID of the sharedfile
|
||||||
|
* @param {String} message - Content of the comment to post
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) {
|
||||||
|
if (typeof userID === "string") {
|
||||||
|
userID = new SteamID(userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpRequestPost({
|
||||||
|
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`,
|
||||||
|
"form": {
|
||||||
|
"comment": message,
|
||||||
|
"count": 10,
|
||||||
|
"sessionid": this.getSessionID()
|
||||||
|
}
|
||||||
|
}, function(err, response, body) {
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err);
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to a sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||||
|
* @param {SteamID | String} userID ID of the user associated to this sharedfile
|
||||||
|
* @param {String} sharedFileId ID of the sharedfile
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.subscribeSharedFileComments = function(userID, sharedFileId, callback) {
|
||||||
|
if (typeof userID === "string") {
|
||||||
|
userID = new SteamID(userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpRequestPost({
|
||||||
|
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/subscribe/${userID.toString()}/${sharedFileId}/`,
|
||||||
|
"form": {
|
||||||
|
"count": 10,
|
||||||
|
"sessionid": this.getSessionID()
|
||||||
|
}
|
||||||
|
}, function(err, response, body) { // eslint-disable-line
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err);
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfavorites a sharedfile
|
||||||
|
* @param {String} sharedFileId - ID of the sharedfile
|
||||||
|
* @param {String} appid - ID of the app associated to this sharedfile
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.unfavoriteSharedFile = function(sharedFileId, appid, callback) {
|
||||||
|
this.httpRequestPost({
|
||||||
|
"uri": "https://steamcommunity.com/sharedfiles/unfavorite",
|
||||||
|
"form": {
|
||||||
|
"id": sharedFileId,
|
||||||
|
"appid": appid,
|
||||||
|
"sessionid": this.getSessionID()
|
||||||
|
}
|
||||||
|
}, function(err, response, body) {
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err);
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes from a sharedfile's comment section. Note: Checkbox on webpage does not update
|
||||||
|
* @param {SteamID | String} userID - ID of the user associated to this sharedfile
|
||||||
|
* @param {String} sharedFileId - ID of the sharedfile
|
||||||
|
* @param {function} callback - Takes only an Error object/null as the first argument
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.unsubscribeSharedFileComments = function(userID, sharedFileId, callback) {
|
||||||
|
if (typeof userID === "string") {
|
||||||
|
userID = new SteamID(userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpRequestPost({
|
||||||
|
"uri": `https://steamcommunity.com/comment/PublishedFile_Public/unsubscribe/${userID.toString()}/${sharedFileId}/`,
|
||||||
|
"form": {
|
||||||
|
"count": 10,
|
||||||
|
"sessionid": this.getSessionID()
|
||||||
|
}
|
||||||
|
}, function(err, response, body) { // eslint-disable-line
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err);
|
||||||
|
}, "steamcommunity");
|
||||||
|
};
|
@ -10,15 +10,18 @@ const ETwoFactorTokenType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SteamCommunity.prototype.enableTwoFactor = function(callback) {
|
SteamCommunity.prototype.enableTwoFactor = function(callback) {
|
||||||
if (!this.oAuthToken) {
|
this._verifyMobileAccessToken();
|
||||||
return callback(new Error('enableTwoFactor can only be used when logged on via steamcommunity\'s `login` method without the `disableMobile` option.'));
|
|
||||||
|
if (!this.mobileAccessToken) {
|
||||||
|
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.httpRequestPost({
|
this.httpRequestPost({
|
||||||
uri: 'https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/',
|
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: {
|
form: {
|
||||||
steamid: this.steamID.getSteamID64(),
|
steamid: this.steamID.getSteamID64(),
|
||||||
access_token: this.oAuthToken,
|
|
||||||
authenticator_time: Math.floor(Date.now() / 1000),
|
authenticator_time: Math.floor(Date.now() / 1000),
|
||||||
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
|
authenticator_type: ETwoFactorTokenType.ValveMobileApp,
|
||||||
device_identifier: SteamTotp.getDeviceID(this.steamID),
|
device_identifier: SteamTotp.getDeviceID(this.steamID),
|
||||||
@ -36,9 +39,11 @@ SteamCommunity.prototype.enableTwoFactor = function(callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let err2 = Helpers.eresultError(body.response.status);
|
if (body.response.status != 1) {
|
||||||
if (err2) {
|
var error = new Error('Error ' + body.response.status);
|
||||||
return callback(err2);
|
error.eresult = body.response.status;
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, body.response);
|
callback(null, body.response);
|
||||||
@ -46,21 +51,23 @@ SteamCommunity.prototype.enableTwoFactor = function(callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
|
SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, callback) {
|
||||||
if (!this.oAuthToken) {
|
this._verifyMobileAccessToken();
|
||||||
return callback(new Error('finalizeTwoFactor can only be used when logged on via steamcommunity\'s `login` method without the `disableMobile` option.'));
|
|
||||||
|
if (!this.mobileAccessToken) {
|
||||||
|
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let attemptsLeft = 30;
|
let attemptsLeft = 30;
|
||||||
let diff = 0;
|
let diff = 0;
|
||||||
|
|
||||||
const finalize = () => {
|
let finalize = () => {
|
||||||
let code = SteamTotp.generateAuthCode(secret, diff);
|
let code = SteamTotp.generateAuthCode(secret, diff);
|
||||||
|
|
||||||
this.httpRequestPost({
|
this.httpRequestPost({
|
||||||
uri: 'https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/',
|
uri: 'https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/?access_token=' + this.mobileAccessToken,
|
||||||
form: {
|
form: {
|
||||||
steamid: this.steamID.getSteamID64(),
|
steamid: this.steamID.getSteamID64(),
|
||||||
access_token: this.oAuthToken,
|
|
||||||
authenticator_code: code,
|
authenticator_code: code,
|
||||||
authenticator_time: Math.floor(Date.now() / 1000),
|
authenticator_time: Math.floor(Date.now() / 1000),
|
||||||
activation_code: activationCode
|
activation_code: activationCode
|
||||||
@ -90,19 +97,18 @@ SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, ca
|
|||||||
// We made more than 30 attempts, something must be wrong
|
// We made more than 30 attempts, something must be wrong
|
||||||
return callback(Helpers.eresultError(SteamCommunity.EResult.Fail));
|
return callback(Helpers.eresultError(SteamCommunity.EResult.Fail));
|
||||||
}
|
}
|
||||||
|
|
||||||
diff += 30;
|
diff += 30;
|
||||||
|
|
||||||
finalize();
|
finalize();
|
||||||
} else if (!body.success) {
|
} else if(!body.success) {
|
||||||
callback(Helpers.eresultError(body.status));
|
callback(new Error('Error ' + body.status));
|
||||||
} else {
|
} else {
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
}, 'steamcommunity');
|
}, 'steamcommunity');
|
||||||
};
|
}
|
||||||
|
|
||||||
SteamTotp.getTimeOffset((err, offset, latency) => {
|
SteamTotp.getTimeOffset(function(err, offset, latency) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
@ -114,20 +120,22 @@ SteamCommunity.prototype.finalizeTwoFactor = function(secret, activationCode, ca
|
|||||||
};
|
};
|
||||||
|
|
||||||
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
|
SteamCommunity.prototype.disableTwoFactor = function(revocationCode, callback) {
|
||||||
if (!this.oAuthToken) {
|
this._verifyMobileAccessToken();
|
||||||
return callback(new Error('disableTwoFactor can only be used when logged on via steamcommunity\'s `login` method without the `disableMobile` option.'));
|
|
||||||
|
if (!this.mobileAccessToken) {
|
||||||
|
callback(new Error('No mobile access token available. Provide one by calling setMobileAppAccessToken()'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.httpRequestPost({
|
this.httpRequestPost({
|
||||||
uri: 'https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/',
|
uri: 'https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/?access_token=' + this.mobileAccessToken,
|
||||||
form: {
|
form: {
|
||||||
steamid: this.steamID.getSteamID64(),
|
steamid: this.steamID.getSteamID64(),
|
||||||
access_token: this.oAuthToken,
|
|
||||||
revocation_code: revocationCode,
|
revocation_code: revocationCode,
|
||||||
steamguard_scheme: 1
|
steamguard_scheme: 1
|
||||||
},
|
},
|
||||||
json: true
|
json: true
|
||||||
}, (err, response, body) => {
|
}, function(err, response, body) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
|
@ -376,18 +376,7 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
|
|||||||
|
|
||||||
let match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
|
let match = body.match(/var g_rgAppContextData = ([^\n]+);\r?\n/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
let errorMessage = 'Malformed response';
|
callback(new Error('Malformed response'));
|
||||||
|
|
||||||
if (body.includes('0 items in their inventory.')) {
|
|
||||||
callback(null, {});
|
|
||||||
return;
|
|
||||||
} else if (body.includes('inventory is currently private.')) {
|
|
||||||
errorMessage = 'Private inventory';
|
|
||||||
} else if (body.includes('profile_private_info')) {
|
|
||||||
errorMessage = 'Private profile';
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(new Error(errorMessage));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,6 +388,21 @@ SteamCommunity.prototype.getUserInventoryContexts = function(userID, callback) {
|
|||||||
return;
|
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);
|
callback(null, data);
|
||||||
}, 'steamcommunity');
|
}, 'steamcommunity');
|
||||||
};
|
};
|
||||||
@ -449,7 +453,7 @@ SteamCommunity.prototype.getUserInventory = function(userID, appID, contextID, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i in body.rgCurrency) {
|
for (let i in body.rgCurrency) {
|
||||||
currency.push(new CEconItem(body.rgInventory[i], body.rgDescriptions, contextID));
|
currency.push(new CEconItem(body.rgCurrency[i], body.rgDescriptions, contextID));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.more) {
|
if (body.more) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const SteamCommunity = require('../index.js');
|
const SteamCommunity = require('../index.js');
|
||||||
|
|
||||||
|
const Helpers = require('./helpers.js');
|
||||||
|
|
||||||
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
|
SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
|
||||||
this.httpRequest({
|
this.httpRequest({
|
||||||
uri: 'https://steamcommunity.com/dev/apikey?l=english',
|
uri: 'https://steamcommunity.com/dev/apikey?l=english',
|
||||||
@ -40,5 +42,76 @@ SteamCommunity.prototype.getWebApiKey = function(domain, callback) {
|
|||||||
this.getWebApiKey(domain, callback);
|
this.getWebApiKey(domain, callback);
|
||||||
}, 'steamcommunity');
|
}, 'steamcommunity');
|
||||||
}
|
}
|
||||||
}, 'steamcommunity');
|
}, "steamcommunity");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated No longer works. Will be removed in a future release.
|
||||||
|
* @param {function} callback
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.getWebApiOauthToken = function(callback) {
|
||||||
|
if (this.oAuthToken) {
|
||||||
|
return callback(null, this.oAuthToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(new Error('This operation requires an OAuth token, which is no longer issued by Steam.'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an access_token generated by steam-session using EAuthTokenPlatformType.MobileApp.
|
||||||
|
* Required for some operations such as 2FA enabling and disabling.
|
||||||
|
* This will throw an Error if the provided token is not valid, was not generated for the MobileApp platform, is expired,
|
||||||
|
* or does not belong to the logged-in user account.
|
||||||
|
*
|
||||||
|
* @param {string} token
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype.setMobileAppAccessToken = function(token) {
|
||||||
|
if (!this.steamID) {
|
||||||
|
throw new Error('Log on to steamcommunity before setting a mobile app access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedToken = Helpers.decodeJwt(token);
|
||||||
|
|
||||||
|
if (!decodedToken.iss || !decodedToken.sub || !decodedToken.aud || !decodedToken.exp) {
|
||||||
|
throw new Error('Provided value is not a valid Steam access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedToken.iss == 'steam') {
|
||||||
|
throw new Error('Provided token is a refresh token, not an access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedToken.sub != this.steamID.getSteamID64()) {
|
||||||
|
throw new Error(`Provided token belongs to account ${decodedToken.sub}, but we are logged into ${this.steamID.getSteamID64()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedToken.exp < Math.floor(Date.now() / 1000)) {
|
||||||
|
throw new Error('Provided token is expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((decodedToken.aud || []).indexOf('mobile') == -1) {
|
||||||
|
throw new Error('Provided token is not valid for MobileApp platform type');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mobileAccessToken = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the mobile access token we already have set is still valid for current login.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
SteamCommunity.prototype._verifyMobileAccessToken = function() {
|
||||||
|
if (!this.mobileAccessToken) {
|
||||||
|
// No access token, so nothing to do here.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedToken = Helpers.decodeJwt(this.mobileAccessToken);
|
||||||
|
|
||||||
|
let isTokenInvalid = decodedToken.sub != this.steamID.getSteamID64() // SteamID doesn't match
|
||||||
|
|| decodedToken.exp < Math.floor(Date.now() / 1000); // Token is expired
|
||||||
|
|
||||||
|
if (isTokenInvalid) {
|
||||||
|
delete this.mobileAccessToken;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
35
examples/README.md
Normal file
35
examples/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# node-steamcommunity examples
|
||||||
|
|
||||||
|
The files in this directory are example scripts that you can use as a getting-started point for using node-steamcommunity.
|
||||||
|
|
||||||
|
## Enable or Disable Two-Factor Authentication
|
||||||
|
|
||||||
|
If you need to enable or disable 2FA on your bot account, you can use enable_twofactor.js and disable_twofactor.js to do so.
|
||||||
|
The way that you're intended to use these scripts is by cloning the repository locally, and then running them directly
|
||||||
|
from this examples directory.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/DoctorMcKay/node-steamcommunity node-steamcommunity
|
||||||
|
cd node-steamcommunity
|
||||||
|
npm install
|
||||||
|
cd examples
|
||||||
|
node enable_twofactor.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accept All Confirmations
|
||||||
|
|
||||||
|
If you need to accept trade or market confirmations on your bot account for which you have your identity secret, you can
|
||||||
|
use accept_all_confirmations.js to do so. The way that you're intended to use this script is by cloning the repository
|
||||||
|
locally, and then running it directly from this examples directory.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/DoctorMcKay/node-steamcommunity node-steamcommunity
|
||||||
|
cd node-steamcommunity
|
||||||
|
npm install
|
||||||
|
cd examples
|
||||||
|
node accept_all_confirmations.js
|
||||||
|
```
|
173
examples/accept_all_confirmations.js
Normal file
173
examples/accept_all_confirmations.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
// If you aren't running this script inside of the repository, replace the following line with:
|
||||||
|
// const SteamCommunity = require('steamcommunity');
|
||||||
|
const SteamCommunity = require('../index.js');
|
||||||
|
const SteamSession = require('steam-session');
|
||||||
|
const SteamTotp = require('steam-totp');
|
||||||
|
const ReadLine = require('readline');
|
||||||
|
|
||||||
|
const EConfirmationType = SteamCommunity.ConfirmationType;
|
||||||
|
|
||||||
|
let g_AbortPromptFunc = null;
|
||||||
|
|
||||||
|
let community = new SteamCommunity();
|
||||||
|
|
||||||
|
main();
|
||||||
|
async function main() {
|
||||||
|
let accountName = await promptAsync('Username: ');
|
||||||
|
let password = await promptAsync('Password (hidden): ', true);
|
||||||
|
|
||||||
|
// Create a LoginSession for us to use to attempt to log into steam
|
||||||
|
let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp);
|
||||||
|
|
||||||
|
// Go ahead and attach our event handlers before we do anything else.
|
||||||
|
session.on('authenticated', async () => {
|
||||||
|
abortPrompt();
|
||||||
|
|
||||||
|
let cookies = await session.getWebCookies();
|
||||||
|
community.setCookies(cookies);
|
||||||
|
|
||||||
|
doConfirmations();
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('timeout', () => {
|
||||||
|
abortPrompt();
|
||||||
|
console.log('This login attempt has timed out.');
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('error', (err) => {
|
||||||
|
abortPrompt();
|
||||||
|
|
||||||
|
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
|
||||||
|
// polling, e.g. the network connection goes down or Steam chokes on something.
|
||||||
|
|
||||||
|
console.log(`ERROR: This login attempt has failed! ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start our login attempt
|
||||||
|
let startResult = await session.startWithCredentials({accountName, password});
|
||||||
|
if (startResult.actionRequired) {
|
||||||
|
// Some Steam Guard action is required. We only care about email and device codes; in theory an
|
||||||
|
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
|
||||||
|
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
|
||||||
|
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
|
||||||
|
// could accept an email or device confirmation.
|
||||||
|
|
||||||
|
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
|
||||||
|
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
|
||||||
|
if (codeAction) {
|
||||||
|
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
|
||||||
|
// We wouldn't expect this to happen since mobile confirmations are only possible with 2FA enabled, but just in case...
|
||||||
|
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
|
||||||
|
} else {
|
||||||
|
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = await promptAsync('Code or Shared Secret: ');
|
||||||
|
if (code) {
|
||||||
|
// The code might've been a shared secret
|
||||||
|
if (code.length > 10) {
|
||||||
|
code = SteamTotp.getAuthCode(code);
|
||||||
|
}
|
||||||
|
await session.submitSteamGuardCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we fall through here without submitting a Steam Guard code, that means one of two things:
|
||||||
|
// 1. The user pressed enter without providing a code, in which case the script will simply exit
|
||||||
|
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doConfirmations() {
|
||||||
|
let identitySecret = await promptAsync('Identity Secret: ');
|
||||||
|
|
||||||
|
let confs = await new Promise((resolve, reject) => {
|
||||||
|
let time = SteamTotp.time();
|
||||||
|
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'conf');
|
||||||
|
community.getConfirmations(time, key, (err, confs) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(confs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${confs.length} outstanding confirmations.`);
|
||||||
|
|
||||||
|
// We need to track the previous timestamp we used, as we cannot reuse timestamps.
|
||||||
|
let previousTime = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < confs.length; i++) {
|
||||||
|
let conf = confs[i];
|
||||||
|
|
||||||
|
process.stdout.write(`Accepting confirmation for ${EConfirmationType[conf.type]} - ${conf.title}... `);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
let time = SteamTotp.time();
|
||||||
|
if (time == previousTime) {
|
||||||
|
time++;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousTime = time;
|
||||||
|
let key = SteamTotp.getConfirmationKey(identitySecret, time, 'allow');
|
||||||
|
conf.respond(time, key, true, (err) => {
|
||||||
|
err ? reject(err) : resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('success');
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(`error: ${ex.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleep 500ms so we don't run too far away from the current timestamp
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Finished processing confirmations');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing interesting below here, just code for prompting for input from the console.
|
||||||
|
|
||||||
|
function promptAsync(question, sensitiveInput = false) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let rl = ReadLine.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: sensitiveInput ? null : process.stdout,
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
g_AbortPromptFunc = () => {
|
||||||
|
rl.close();
|
||||||
|
resolve('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sensitiveInput) {
|
||||||
|
// We have to write the question manually if we didn't give readline an output stream
|
||||||
|
process.stdout.write(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.question(question, (result) => {
|
||||||
|
if (sensitiveInput) {
|
||||||
|
// We have to manually print a newline
|
||||||
|
process.stdout.write('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
g_AbortPromptFunc = null;
|
||||||
|
rl.close();
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function abortPrompt() {
|
||||||
|
if (!g_AbortPromptFunc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_AbortPromptFunc();
|
||||||
|
process.stdout.write('\n');
|
||||||
|
}
|
@ -1,62 +1,135 @@
|
|||||||
// If you aren't running this script inside of the repository, replace the following line with:
|
// If you aren't running this script inside of the repository, replace the following line with:
|
||||||
// const SteamCommunity = require('steamcommunity');
|
// const SteamCommunity = require('steamcommunity');
|
||||||
const SteamCommunity = require('../index.js');
|
const SteamCommunity = require('../index.js');
|
||||||
|
const SteamSession = require('steam-session');
|
||||||
const ReadLine = require('readline');
|
const ReadLine = require('readline');
|
||||||
|
|
||||||
|
let g_AbortPromptFunc = null;
|
||||||
|
|
||||||
let community = new SteamCommunity();
|
let community = new SteamCommunity();
|
||||||
let rl = ReadLine.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
rl.question('Username: ', (accountName) => {
|
main();
|
||||||
rl.question('Password: ', (password) => {
|
async function main() {
|
||||||
rl.question('Two-Factor Auth Code: ', (authCode) =>{
|
let accountName = await promptAsync('Username: ');
|
||||||
rl.question('Revocation Code: R', (rCode) => {
|
let password = await promptAsync('Password (hidden): ', true);
|
||||||
doLogin(accountName, password, authCode, '', rCode);
|
|
||||||
});
|
// 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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function doLogin(accountName, password, authCode, captcha, rCode) {
|
session.on('timeout', () => {
|
||||||
community.login({
|
abortPrompt();
|
||||||
accountName: accountName,
|
console.log('This login attempt has timed out.');
|
||||||
password: password,
|
});
|
||||||
twoFactorCode: authCode,
|
|
||||||
captcha: captcha
|
session.on('error', (err) => {
|
||||||
}, (err, sessionID, cookies, steamguard) => {
|
abortPrompt();
|
||||||
|
|
||||||
|
// This should ordinarily not happen. This only happens in case there's some kind of unexpected error while
|
||||||
|
// polling, e.g. the network connection goes down or Steam chokes on something.
|
||||||
|
|
||||||
|
console.log(`ERROR: This login attempt has failed! ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start our login attempt
|
||||||
|
let startResult = await session.startWithCredentials({accountName, password});
|
||||||
|
if (startResult.actionRequired) {
|
||||||
|
// Some Steam Guard action is required. We only care about email and device codes; in theory an
|
||||||
|
// EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those.
|
||||||
|
// If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically.
|
||||||
|
// The only consequence of ignoring it here is that we don't print a message to the user indicating that they
|
||||||
|
// could accept an email or device confirmation.
|
||||||
|
|
||||||
|
let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode];
|
||||||
|
let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type));
|
||||||
|
if (codeAction) {
|
||||||
|
if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) {
|
||||||
|
// We wouldn't expect this to happen since we're trying to disable 2FA, but just in case...
|
||||||
|
console.log(`A code has been sent to your email address at ${codeAction.detail}.`);
|
||||||
|
} else {
|
||||||
|
console.log('You need to provide a Steam Guard Mobile Authenticator code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = await promptAsync('Code: ');
|
||||||
|
if (code) {
|
||||||
|
await session.submitSteamGuardCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we fall through here without submitting a Steam Guard code, that means one of two things:
|
||||||
|
// 1. The user pressed enter without providing a code, in which case the script will simply exit
|
||||||
|
// 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRevoke() {
|
||||||
|
let rCode = await promptAsync('Revocation Code: R');
|
||||||
|
community.disableTwoFactor('R' + rCode, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.message == 'SteamGuard') {
|
|
||||||
console.log('This account does not have two-factor authentication enabled.');
|
|
||||||
process.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.message == 'CAPTCHA') {
|
|
||||||
console.log(err.captchaurl);
|
|
||||||
rl.question('CAPTCHA: ', (captchaInput) => {
|
|
||||||
doLogin(accountName, password, authCode, captchaInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
process.exit();
|
process.exit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Logged on!');
|
console.log('Two-factor authentication disabled!');
|
||||||
community.disableTwoFactor('R' + rCode, (err) => {
|
process.exit();
|
||||||
if (err) {
|
});
|
||||||
console.log(err);
|
}
|
||||||
process.exit();
|
|
||||||
return;
|
// Nothing interesting below here, just code for prompting for input from the console.
|
||||||
|
|
||||||
|
function promptAsync(question, sensitiveInput = false) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let rl = ReadLine.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: sensitiveInput ? null : process.stdout,
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
g_AbortPromptFunc = () => {
|
||||||
|
rl.close();
|
||||||
|
resolve('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sensitiveInput) {
|
||||||
|
// We have to write the question manually if we didn't give readline an output stream
|
||||||
|
process.stdout.write(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.question(question, (result) => {
|
||||||
|
if (sensitiveInput) {
|
||||||
|
// We have to manually print a newline
|
||||||
|
process.stdout.write('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Two-factor authentication disabled!');
|
g_AbortPromptFunc = null;
|
||||||
process.exit();
|
rl.close();
|
||||||
|
resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abortPrompt() {
|
||||||
|
if (!g_AbortPromptFunc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_AbortPromptFunc();
|
||||||
|
process.stdout.write('\n');
|
||||||
|
}
|
||||||
|
@ -1,52 +1,98 @@
|
|||||||
// If you aren't running this script inside of the repository, replace the following line with:
|
// If you aren't running this script inside of the repository, replace the following line with:
|
||||||
// const SteamCommunity = require('steamcommunity');
|
// const SteamCommunity = require('steamcommunity');
|
||||||
const SteamCommunity = require('../index.js');
|
const SteamCommunity = require('../index.js');
|
||||||
|
const SteamSession = require('steam-session');
|
||||||
const ReadLine = require('readline');
|
const ReadLine = require('readline');
|
||||||
const FS = require('fs');
|
const FS = require('fs');
|
||||||
|
|
||||||
const EResult = SteamCommunity.EResult;
|
const EResult = SteamCommunity.EResult;
|
||||||
|
|
||||||
|
let g_AbortPromptFunc = null;
|
||||||
|
|
||||||
let community = new SteamCommunity();
|
let community = new SteamCommunity();
|
||||||
let rl = ReadLine.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
rl.question('Username: ', (accountName) => {
|
main();
|
||||||
rl.question('Password: ', (password) => {
|
async function main() {
|
||||||
doLogin(accountName, password);
|
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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function doLogin(accountName, password, authCode, captcha) {
|
session.on('timeout', () => {
|
||||||
community.login({
|
abortPrompt();
|
||||||
accountName: accountName,
|
console.log('This login attempt has timed out.');
|
||||||
password: password,
|
});
|
||||||
authCode: authCode,
|
|
||||||
captcha: captcha
|
session.on('error', (err) => {
|
||||||
}, (err, sessionID, cookies, steamguard) => {
|
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() {
|
||||||
|
community.enableTwoFactor((err, response) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.message == 'SteamGuardMobile') {
|
if (err.eresult == EResult.Fail) {
|
||||||
console.log('This account already has two-factor authentication enabled.');
|
console.log('Error: Failed to enable two-factor authentication. Do you have a phone number attached to your account?');
|
||||||
process.exit();
|
process.exit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.message == 'SteamGuard') {
|
if (err.eresult == EResult.RateLimitExceeded) {
|
||||||
console.log(`An email has been sent to your address at ${err.emaildomain}`);
|
console.log('Error: RateLimitExceeded. Try again later.');
|
||||||
rl.question('Steam Guard Code: ', (code) => {
|
process.exit();
|
||||||
doLogin(accountName, password, code);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.message == 'CAPTCHA') {
|
|
||||||
console.log(err.captchaurl);
|
|
||||||
rl.question('CAPTCHA: ', (captchaInput) => {
|
|
||||||
doLogin(accountName, password, authCode, captchaInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,58 +101,82 @@ function doLogin(accountName, password, authCode, captcha) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Logged on!');
|
if (response.status != EResult.OK) {
|
||||||
community.enableTwoFactor((err, response) => {
|
console.log(`Error: Status ${response.status}`);
|
||||||
if (err) {
|
|
||||||
if (err.eresult == EResult.Fail) {
|
|
||||||
console.log('Error: Failed to enable two-factor authentication. Do you have a phone number attached to your account?');
|
|
||||||
process.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.eresult == EResult.RateLimitExceeded) {
|
|
||||||
console.log('Error: RateLimitExceeded. Try again later.');
|
|
||||||
process.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(err);
|
|
||||||
process.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status != EResult.OK) {
|
|
||||||
console.log(`Error: Status ${response.status}`);
|
|
||||||
process.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let filename = `twofactor_${community.steamID.getSteamID64()}.json`;
|
|
||||||
console.log(`Writing secrets to ${filename}`);
|
|
||||||
console.log(`Revocation code: ${response.revocation_code}`);
|
|
||||||
FS.writeFileSync(filename, JSON.stringify(response, null, '\t'));
|
|
||||||
|
|
||||||
promptActivationCode(response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptActivationCode(response) {
|
|
||||||
rl.question('SMS Code: ', (smsCode) => {
|
|
||||||
community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => {
|
|
||||||
if (err) {
|
|
||||||
if (err.message == 'Invalid activation code') {
|
|
||||||
console.log(err);
|
|
||||||
promptActivationCode(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(err);
|
|
||||||
} else {
|
|
||||||
console.log('Two-factor authentication enabled!');
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit();
|
process.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = `twofactor_${community.steamID.getSteamID64()}.json`;
|
||||||
|
console.log(`Writing secrets to ${filename}`);
|
||||||
|
console.log(`Revocation code: ${response.revocation_code}`);
|
||||||
|
FS.writeFileSync(filename, JSON.stringify(response, null, '\t'));
|
||||||
|
|
||||||
|
promptActivationCode(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptActivationCode(response) {
|
||||||
|
if (response.phone_number_hint) {
|
||||||
|
console.log(`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);
|
||||||
|
promptActivationCode(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
console.log('Two-factor authentication enabled!');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing interesting below here, just code for prompting for input from the console.
|
||||||
|
|
||||||
|
function promptAsync(question, sensitiveInput = false) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let rl = ReadLine.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: sensitiveInput ? null : process.stdout,
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
g_AbortPromptFunc = () => {
|
||||||
|
rl.close();
|
||||||
|
resolve('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sensitiveInput) {
|
||||||
|
// We have to write the question manually if we didn't give readline an output stream
|
||||||
|
process.stdout.write(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.question(question, (result) => {
|
||||||
|
if (sensitiveInput) {
|
||||||
|
// We have to manually print a newline
|
||||||
|
process.stdout.write('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
g_AbortPromptFunc = null;
|
||||||
|
rl.close();
|
||||||
|
resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abortPrompt() {
|
||||||
|
if (!g_AbortPromptFunc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_AbortPromptFunc();
|
||||||
|
process.stdout.write('\n');
|
||||||
|
}
|
||||||
|
33
index.js
33
index.js
@ -19,6 +19,7 @@ module.exports = SteamCommunity;
|
|||||||
SteamCommunity.SteamID = SteamID;
|
SteamCommunity.SteamID = SteamID;
|
||||||
SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
|
SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
|
||||||
SteamCommunity.EResult = require('./resources/EResult.js');
|
SteamCommunity.EResult = require('./resources/EResult.js');
|
||||||
|
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
|
||||||
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
|
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
|
||||||
|
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ SteamCommunity.prototype.login = function(details, callback) {
|
|||||||
this._setCookie(`steamMachineAuth${parts[0]}=${encodeURIComponent(parts[1])}`, true);
|
this._setCookie(`steamMachineAuth${parts[0]}=${encodeURIComponent(parts[1])}`, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let disableMobile = details.disableMobile;
|
let disableMobile = typeof details.disableMobile == 'undefined' ? true : details.disableMobile;
|
||||||
|
|
||||||
// Delete the cache
|
// Delete the cache
|
||||||
delete this._profileURL;
|
delete this._profileURL;
|
||||||
@ -159,18 +160,17 @@ SteamCommunity.prototype.login = function(details, callback) {
|
|||||||
|
|
||||||
this._captchaGid = body.captcha_gid;
|
this._captchaGid = body.captcha_gid;
|
||||||
|
|
||||||
return reject(error);
|
callback(error);
|
||||||
} else if (!body.success) {
|
} else if (!body.success) {
|
||||||
return reject(new Error(body.message || 'Unknown error'));
|
callback(new Error(body.message || 'Unknown error'));
|
||||||
} else if (!disableMobile && !body.oauth) {
|
} else {
|
||||||
return reject(new Error('Malformed response'));
|
var sessionID = generateSessionID();
|
||||||
} else {
|
var oAuth = {};
|
||||||
let sessionID = this.getSessionID();
|
self._setCookie(Request.cookie('sessionid=' + sessionID));
|
||||||
let oAuth;
|
|
||||||
|
|
||||||
let cookies = this._jar.getCookieStringSync('https://steamcommunity.com').split(';').map(cookie => cookie.trim());
|
let cookies = this._jar.getCookieStringSync('https://steamcommunity.com').split(';').map(cookie => cookie.trim());
|
||||||
|
|
||||||
if (!disableMobile) {
|
if (!disableMobile && body.oauth) {
|
||||||
oAuth = JSON.parse(body.oauth);
|
oAuth = JSON.parse(body.oauth);
|
||||||
this.steamID = new SteamID(oAuth.steamid);
|
this.steamID = new SteamID(oAuth.steamid);
|
||||||
this.oAuthToken = oAuth.oauth_token;
|
this.oAuthToken = oAuth.oauth_token;
|
||||||
@ -211,6 +211,12 @@ SteamCommunity.prototype.login = function(details, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* @param {string} steamguard
|
||||||
|
* @param {string} token
|
||||||
|
* @param {function} callback
|
||||||
|
*/
|
||||||
SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
|
SteamCommunity.prototype.oAuthLogin = function(steamguard, token, callback) {
|
||||||
steamguard = steamguard.split('||');
|
steamguard = steamguard.split('||');
|
||||||
let steamID = new SteamID(steamguard[0]);
|
let steamID = new SteamID(steamguard[0]);
|
||||||
@ -307,6 +313,10 @@ SteamCommunity.prototype.setCookies = function(cookies) {
|
|||||||
|
|
||||||
this._setCookie(cookie, !!(cookieName.match(/^steamMachineAuth/) || cookieName.match(/Secure$/)));
|
this._setCookie(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() {
|
SteamCommunity.prototype.getSessionID = function() {
|
||||||
@ -566,6 +576,8 @@ require('./components/profile.js');
|
|||||||
require('./components/market.js');
|
require('./components/market.js');
|
||||||
require('./components/groups.js');
|
require('./components/groups.js');
|
||||||
require('./components/users.js');
|
require('./components/users.js');
|
||||||
|
require('./components/sharedfiles.js');
|
||||||
|
require('./components/inventoryhistory.js');
|
||||||
require('./components/webapi.js');
|
require('./components/webapi.js');
|
||||||
require('./components/twofactor.js');
|
require('./components/twofactor.js');
|
||||||
require('./components/confirmations.js');
|
require('./components/confirmations.js');
|
||||||
@ -573,6 +585,7 @@ require('./components/help.js');
|
|||||||
require('./classes/CMarketItem.js');
|
require('./classes/CMarketItem.js');
|
||||||
require('./classes/CMarketSearchResult.js');
|
require('./classes/CMarketSearchResult.js');
|
||||||
require('./classes/CSteamGroup.js');
|
require('./classes/CSteamGroup.js');
|
||||||
|
require('./classes/CSteamSharedFile.js');
|
||||||
require('./classes/CSteamUser.js');
|
require('./classes/CSteamUser.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
"tough-cookie": "^4.0.0",
|
"tough-cookie": "^4.0.0",
|
||||||
"xml2js": "^0.4.22"
|
"xml2js": "^0.4.22"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"steam-session": "^1.2.3"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
|
13
resources/ESharedFileType.js
Normal file
13
resources/ESharedFileType.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @enum ESharedFileType
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
"Screenshot": 0,
|
||||||
|
"Artwork": 1,
|
||||||
|
"Guide": 2,
|
||||||
|
|
||||||
|
// Value-to-name mapping for convenience
|
||||||
|
"0": "Screenshot",
|
||||||
|
"1": "Artwork",
|
||||||
|
"2": "Guide"
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user