mirror of
https://github.com/DoctorMcKay/node-steamcommunity.git
synced 2025-01-29 18:30:13 +08:00
424 lines
12 KiB
JavaScript
424 lines
12 KiB
JavaScript
const {EventEmitter} = require('events');
|
|
const StdLib = require('@doctormckay/stdlib');
|
|
const SteamID = require('steamid');
|
|
const Util = require('util');
|
|
const xml2js = require('xml2js');
|
|
|
|
const Helpers = require('./components/helpers.js');
|
|
|
|
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36';
|
|
|
|
Util.inherits(SteamCommunity, EventEmitter);
|
|
|
|
module.exports = SteamCommunity;
|
|
|
|
SteamCommunity.SteamID = SteamID;
|
|
SteamCommunity.EConfirmationType = require('./resources/EConfirmationType.js');
|
|
SteamCommunity.EResult = require('./resources/EResult.js');
|
|
SteamCommunity.ESharedFileType = require('./resources/ESharedFileType.js');
|
|
SteamCommunity.EFriendRelationship = require('./resources/EFriendRelationship.js');
|
|
|
|
/**
|
|
*
|
|
* @param {object} [options]
|
|
* @param {number} [options.timeout=50000] - The time in milliseconds that SteamCommunity will wait for HTTP requests to complete.
|
|
* @param {string} [options.localAddress] - The local IP address that SteamCommunity will use for its HTTP requests.
|
|
* @param {string} [options.httpProxy] - A string containing the URI of an HTTP proxy to use for all requests, e.g. `http://user:pass@1.2.3.4:8888`
|
|
* @param {object} [options.defaultHttpHeaders] - An object containing some headers to send for every HTTP request
|
|
* @constructor
|
|
*/
|
|
function SteamCommunity(options) {
|
|
options = options || {};
|
|
|
|
this._jar = new StdLib.HTTP.CookieJar();
|
|
this._captchaGid = -1;
|
|
this._httpRequestID = 0;
|
|
|
|
let defaultHeaders = {
|
|
'user-agent': USER_AGENT
|
|
};
|
|
|
|
// Apply the user's custom default headers
|
|
for (let i in (options.defaultHttpHeaders || {})) {
|
|
// Make sure all header names are lower case to avoid conflicts
|
|
defaultHeaders[i.toLowerCase()] = options.defaultHttpHeaders[i];
|
|
}
|
|
|
|
this._httpClient = new StdLib.HTTP.HttpClient({
|
|
httpAgent: options.httpProxy ? StdLib.HTTP.getProxyAgent(false, options.httpProxy) : null,
|
|
httpsAgent: options.httpProxy ? StdLib.HTTP.getProxyAgent(true, options.httpProxy) : null,
|
|
localAddress: options.localAddress,
|
|
defaultHeaders,
|
|
defaultTimeout: options.timeout || 50000,
|
|
cookieJar: this._jar,
|
|
gzip: true
|
|
});
|
|
|
|
this._options = options;
|
|
|
|
// English
|
|
this._setCookie('Steam_Language=english');
|
|
|
|
// UTC
|
|
this._setCookie('timezoneOffset=0,0');
|
|
}
|
|
|
|
SteamCommunity.prototype.login = function(details) {
|
|
// TODO
|
|
};
|
|
|
|
/**
|
|
* Get a token that can be used to log onto Steam using steam-user.
|
|
* @param {function} [callback]
|
|
* @return Promise<{steamID: SteamID, accountName: string, webLogonToken: string}>
|
|
*/
|
|
SteamCommunity.prototype.getClientLogonToken = function(callback) {
|
|
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
|
let {jsonBody} = await this.httpRequest({
|
|
method: 'GET',
|
|
uri: 'https://steamcommunity.com/chat/clientjstoken',
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
if (!jsonBody.logged_in) {
|
|
let e = new Error('Not Logged In');
|
|
this._notifySessionExpired(e);
|
|
return reject(e);
|
|
}
|
|
|
|
if (!jsonBody.steamid || !jsonBody.account_name || !jsonBody.token) {
|
|
return reject(new Error('Malformed response'));
|
|
}
|
|
|
|
resolve({
|
|
steamID: new SteamID(jsonBody.steamid),
|
|
accountName: jsonBody.account_name,
|
|
webLogonToken: jsonBody.token
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sets a single cookie in our cookie jar.
|
|
* @param {string} cookie
|
|
* @private
|
|
*/
|
|
SteamCommunity.prototype._setCookie = function(cookie) {
|
|
this._jar.add(cookie, 'steamcommunity.com');
|
|
this._jar.add(cookie, 'store.steampowered.com');
|
|
this._jar.add(cookie, 'help.steampowered.com');
|
|
};
|
|
|
|
/**
|
|
* Set one or more cookies in this SteamCommunity's cookie jar.
|
|
* @param {string|string[]} cookies
|
|
*/
|
|
SteamCommunity.prototype.setCookies = function(cookies) {
|
|
if (!Array.isArray(cookies)) {
|
|
cookies = [cookies];
|
|
}
|
|
|
|
cookies.forEach((cookie) => {
|
|
let cookieName = cookie.match(/(.+)=/)[1];
|
|
if (cookieName == 'steamLogin' || cookieName == 'steamLoginSecure') {
|
|
this.steamID = new SteamID(cookie.match(/=(\d+)/)[1]);
|
|
}
|
|
|
|
this._setCookie(cookie);
|
|
});
|
|
|
|
// 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(domain = 'steamcommunity.com') {
|
|
let sessionIdCookie = this._jar.cookies
|
|
.filter(c => c.domain == domain)
|
|
.find(c => c.name == 'sessionid');
|
|
if (sessionIdCookie) {
|
|
return sessionIdCookie.content;
|
|
}
|
|
|
|
// No cookie found? Generate a new session id
|
|
let sessionID = require('crypto').randomBytes(12).toString('hex');
|
|
this._setCookie(`sessionid=${sessionID}`);
|
|
return sessionID;
|
|
};
|
|
|
|
/**
|
|
* @param {string} pin
|
|
* @param {function} [callback]
|
|
* @return Promise<void>
|
|
*/
|
|
SteamCommunity.prototype.parentalUnlock = function(pin, callback) {
|
|
let sessionID = this.getSessionID();
|
|
|
|
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
|
let {jsonBody} = await this.httpRequest({
|
|
method: 'POST',
|
|
url: 'https://steamcommunity.com/parental/ajaxunlock',
|
|
form: {
|
|
pin: pin,
|
|
sessionid: sessionID
|
|
},
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
if (!jsonBody || typeof jsonBody.success !== 'boolean') {
|
|
return reject('Invalid response');
|
|
}
|
|
|
|
if (!jsonBody.success) {
|
|
switch (jsonBody.eresult) {
|
|
case SteamCommunity.EResult.AccessDenied:
|
|
return reject('Incorrect PIN');
|
|
|
|
case SteamCommunity.EResult.LimitExceeded:
|
|
return reject('Too many invalid PIN attempts');
|
|
|
|
default:
|
|
return reject('Error ' + jsonBody.eresult);
|
|
}
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {function} [callback]
|
|
* @return Promise<object>
|
|
*/
|
|
SteamCommunity.prototype.getNotifications = function(callback) {
|
|
return StdLib.Promises.callbackPromise(null, callback, false, async (resolve, reject) => {
|
|
let {jsonBody} = await this.httpRequest({
|
|
method: 'GET',
|
|
url: 'https://steamcommunity.com/actions/GetNotificationCounts',
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
if (!jsonBody || !jsonBody.notifications) {
|
|
return reject(new Error('Malformed response'));
|
|
}
|
|
|
|
let notifications = {
|
|
trades: jsonBody.notifications[1] || 0,
|
|
gameTurns: jsonBody.notifications[2] || 0,
|
|
moderatorMessages: jsonBody.notifications[3] || 0,
|
|
comments: jsonBody.notifications[4] || 0,
|
|
items: jsonBody.notifications[5] || 0,
|
|
invites: jsonBody.notifications[6] || 0,
|
|
// dunno about 7
|
|
gifts: jsonBody.notifications[8] || 0,
|
|
chat: jsonBody.notifications[9] || 0,
|
|
helpRequestReplies: jsonBody.notifications[10] || 0,
|
|
accountAlerts: jsonBody.notifications[11] || 0
|
|
};
|
|
|
|
resolve(notifications);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {function} [callback]
|
|
* @return Promise<void>
|
|
*/
|
|
SteamCommunity.prototype.resetItemNotifications = function(callback) {
|
|
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
|
await this.httpRequest({
|
|
method: 'GET',
|
|
url: 'https://steamcommunity.com/my/inventory',
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
resolve();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {function} [callback]
|
|
* @return Promise<{loggedIn: boolean, familyView: boolean}>
|
|
*/
|
|
SteamCommunity.prototype.loggedIn = function(callback) {
|
|
return StdLib.Promises.callbackPromise(['loggedIn', 'familyView'], callback, false, async (resolve, reject) => {
|
|
let result = await this.httpRequest({
|
|
method: 'GET',
|
|
url: 'https://steamcommunity.com/my',
|
|
followRedirect: false,
|
|
checkHttpError: false,
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
if (result.statusCode != 302 && result.statusCode != 403) {
|
|
return reject(new Error(`HTTP error ${result.statusCode}`));
|
|
}
|
|
|
|
if (result.statusCode == 403) {
|
|
// TODO check response body to see if this is an akamai block
|
|
return resolve({
|
|
loggedIn: true,
|
|
familyView: true
|
|
});
|
|
}
|
|
|
|
return resolve({
|
|
loggedIn: !!result.headers.location.match(/steamcommunity\.com(\/(id|profiles)\/[^/]+)\/?/),
|
|
familyView: false
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {function} [callback]
|
|
* @return Promise<{url: string, token: string}>
|
|
*/
|
|
SteamCommunity.prototype.getTradeURL = function(callback) {
|
|
return StdLib.Promises.callbackPromise(['url', 'token'], callback, false, async (resolve, reject) => {
|
|
let {textBody} = await this._myProfile('tradeoffers/privacy');
|
|
|
|
let match = textBody.match(/https?:\/\/(www.)?steamcommunity.com\/tradeoffer\/new\/?\?partner=\d+(&|&)token=([a-zA-Z0-9-_]+)/);
|
|
if (!match) {
|
|
return reject(new Error('Malformed response'));
|
|
}
|
|
|
|
let token = match[3];
|
|
resolve({
|
|
url: match[0],
|
|
token
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param [callback]
|
|
* @return Promise<{url: string, token: string}>
|
|
*/
|
|
SteamCommunity.prototype.changeTradeURL = function(callback) {
|
|
return StdLib.Promises.callbackPromise(['url', 'token'], callback, true, async (resolve, reject) => {
|
|
let {textBody} = await this._myProfile('tradeoffers/newtradeurl', {sessionid: this.getSessionID()});
|
|
|
|
if (!textBody || typeof textBody !== 'string' || textBody.length < 3 || textBody.indexOf('"') !== 0) {
|
|
return reject(new Error('Malformed response'));
|
|
}
|
|
|
|
let newToken = textBody.replace(/"/g, ''); //"t1o2k3e4n" => t1o2k3e4n
|
|
resolve({
|
|
url: `https://steamcommunity.com/tradeoffer/new/?partner=${this.steamID.accountid}&token=${newToken}`,
|
|
token: newToken
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Clear your profile name (alias) history.
|
|
* @param {function} [callback]
|
|
* @return Promise<void>
|
|
*/
|
|
SteamCommunity.prototype.clearPersonaNameHistory = function(callback) {
|
|
return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => {
|
|
let {statusCode, textBody} = await this._myProfile('ajaxclearaliashistory/', {sessionid: this.getSessionID()});
|
|
|
|
if (statusCode != 200) {
|
|
return reject(new Error(`HTTP error ${statusCode}`));
|
|
}
|
|
|
|
try {
|
|
let body = JSON.parse(textBody);
|
|
let err = Helpers.eresultError(body.success);
|
|
return err ? reject(err) : resolve();
|
|
} catch (ex) {
|
|
return reject(new Error('Malformed response'));
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns an object whose keys are 64-bit SteamIDs, and whose values are values from the EFriendRelationship enum.
|
|
* Therefore, you can deduce your friends or blocked list from this object.
|
|
* @param {function} [callback]
|
|
* @return Promise<object[]>
|
|
*/
|
|
SteamCommunity.prototype.getFriendsList = function(callback) {
|
|
return StdLib.Promises.callbackPromise(['friends'], callback, false, async (resolve, reject) => {
|
|
let {jsonBody} = await this.httpRequest({
|
|
method: 'GET',
|
|
url: 'https://steamcommunity.com/textfilter/ajaxgetfriendslist',
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
if (jsonBody.success != SteamCommunity.EResult.OK) {
|
|
return reject(Helpers.eresultError(jsonBody.success));
|
|
}
|
|
|
|
if (!jsonBody.friendslist || !jsonBody.friendslist.friends) {
|
|
return reject(new Error('Malformed response'));
|
|
}
|
|
|
|
const friends = {};
|
|
jsonBody.friendslist.friends.forEach(friend => (friends[friend.ulfriendid] = friend.efriendrelationship));
|
|
resolve({friends});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @return Promise<{vanityURL: string, steamID: SteamID}>
|
|
* @private
|
|
*/
|
|
SteamCommunity.prototype._resolveVanityURL = async function(url) {
|
|
// 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
|
|
let {textBody} = await this._httpRequest({
|
|
method: 'GET',
|
|
url,
|
|
source: 'steamcommunity'
|
|
});
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
// Parse XML data returned from Steam into an object
|
|
new xml2js.Parser().parseString(textBody, (err, parsed) => {
|
|
if (err) {
|
|
return reject(new Error('Couldn\'t parse XML response'));
|
|
}
|
|
|
|
if (parsed.response && parsed.response.error) {
|
|
return reject(new Error('Couldn\'t find Steam ID'));
|
|
}
|
|
|
|
let steamID64 = parsed.profile.steamID64[0];
|
|
let vanityURL = parsed.profile.customURL[0];
|
|
|
|
resolve({
|
|
vanityURL,
|
|
steamID: new SteamID(steamID64)
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
require('./components/http.js');
|
|
require('./components/profile.js');
|
|
require('./components/market.js');
|
|
require('./components/groups.js');
|
|
require('./components/users.js');
|
|
require('./components/sharedfiles.js');
|
|
require('./components/webapi.js');
|
|
require('./components/twofactor.js');
|
|
require('./components/confirmations.js');
|
|
require('./components/help.js');
|
|
require('./classes/CMarketItem.js');
|
|
require('./classes/CMarketSearchResult.js');
|
|
require('./classes/CSteamGroup.js');
|
|
require('./classes/CSteamSharedFile.js');
|
|
require('./classes/CSteamUser.js');
|
|
|
|
/**
|
|
@callback SteamCommunity~genericErrorCallback
|
|
@param {Error|null} err - An Error object on failure, or null on success
|
|
*/
|