diff --git a/classes/CConfirmation.js b/classes/CConfirmation.js index f23c03f..b6e9150 100644 --- a/classes/CConfirmation.js +++ b/classes/CConfirmation.js @@ -1,3 +1,5 @@ +const StdLib = require('@doctormckay/stdlib'); + const SteamCommunity = require('../index.js'); module.exports = CConfirmation; @@ -18,20 +20,33 @@ function CConfirmation(community, data) { this.offerID = this.type == SteamCommunity.ConfirmationType.Trade ? this.creator : null; } +/** + * @param {number} time + * @param {string} key + * @param {function} [callback] + * @return Promise<{offerID: number}> + */ CConfirmation.prototype.getOfferID = function(time, key, callback) { - if (this.type && this.creator) { - if (this.type != SteamCommunity.ConfirmationType.Trade) { - callback(new Error('Not a trade confirmation')); - return; + return StdLib.Promises.callbackPromise(['offerID'], null, false, async (resolve, reject) => { + if (this.type && this.creator) { + if (this.type != SteamCommunity.ConfirmationType.Trade) { + return reject(new Error('Not a trade confirmation')); + } + + return resolve({offerID: this.creator}); } - callback(null, this.creator); - return; - } - - this._community.getConfirmationOfferID(this.id, time, key, callback); + return await this._community.getConfirmationOfferID(this.id, time, key, callback); + }); }; +/** + * @param {number} time + * @param {string} key + * @param {boolean} accept + * @param {function} [callback] + * @return Promise + */ CConfirmation.prototype.respond = function(time, key, accept, callback) { - this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback); + return this._community.respondToConfirmation(this.id, this.key, time, key, accept, callback); }; diff --git a/components/confirmations.js b/components/confirmations.js index e22f29e..9adcbd5 100644 --- a/components/confirmations.js +++ b/components/confirmations.js @@ -1,47 +1,34 @@ const Cheerio = require('cheerio'); +const StdLib = require('@doctormckay/stdlib'); const SteamTotp = require('steam-totp'); const SteamCommunity = require('../index.js'); const CConfirmation = require('../classes/CConfirmation.js'); -var EConfirmationType = require('../resources/EConfirmationType.js'); +const EConfirmationType = SteamCommunity.EConfirmationType; /** * Get a list of your account's currently outstanding confirmations. * @param {int} time - The unix timestamp with which the following key was generated * @param {string} key - The confirmation key that was generated using the preceeding time and the tag 'conf' (this key can be reused) - * @param {SteamCommunity~getConfirmations} callback - Called when the list of confirmations is received + * @param {SteamCommunity~getConfirmations} [callback] - Called when the list of confirmations is received + * @return Promise<{confirmations: CConfirmation[]}> */ SteamCommunity.prototype.getConfirmations = function(time, key, callback) { - var self = this; - - // Ugly hack to maintain backward compatibility - var tag = 'conf'; - if (typeof key == 'object') { - tag = key.tag; - key = key.key; - } - - // The official Steam app uses the tag 'list', but 'conf' still works so let's use that for backward compatibility. - request(this, 'getlist', key, time, tag, null, true, function(err, body) { - if (err) { - callback(err); - return; - } + return StdLib.Promises.callbackPromise(['confirmations'], callback, false, async (resolve, reject) => { + let body = await request(this, 'getlist', key, time, 'list', null); if (!body.success) { if (body.needauth) { - var err = new Error('Not Logged In'); - self._notifySessionExpired(err); - callback(err); - return; + let err = new Error('Not Logged In'); + this._notifySessionExpired(err); + return reject(err); } - callback(new Error(body.message || body.detail || 'Failed to get confirmation list')); - return; + return reject(new Error(body.message || body.detail || 'Failed to get confirmation list')); } - var confs = (body.conf || []).map(conf => new CConfirmation(self, { + let confs = (body.conf || []).map(conf => new CConfirmation(this, { id: conf.id, type: conf.type, creator: conf.creator_id, @@ -54,7 +41,7 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) { icon: conf.icon || '' })); - callback(null, confs); + resolve({confirmations: confs}); }); }; @@ -69,29 +56,24 @@ SteamCommunity.prototype.getConfirmations = function(time, key, callback) { * @param {int} confID - The ID of the confirmation in question * @param {int} time - The unix timestamp with which the following key was generated * @param {string} key - The confirmation key that was generated using the preceeding time and the tag "detail" (this key can be reused) - * @param {SteamCommunity~getConfirmationOfferID} callback + * @param {SteamCommunity~getConfirmationOfferID} [callback] + * @return Promise<{offerID: string|null}> */ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, callback) { - // The official Steam app uses the tag 'detail', but 'details' still works so let's use that for backward compatibility - request(this, 'detailspage/' + confID, key, time, 'details', null, false, function(err, body) { - if (err) { - callback(err); - return; - } + return StdLib.Promises.callbackPromise(['offerID'], callback, false, async (resolve, reject) => { + let body = await request(this, 'detailspage/' + confID, key, time, 'detail', null); if (typeof body != 'string') { - callback(new Error('Cannot load confirmation details')); - return; + return reject(new Error('Cannot load confirmation details')); } let $ = Cheerio.load(body); let offer = $('.tradeoffer'); if (offer.length < 1) { - callback(null, null); - return; + return resolve({offerID: null}); } - callback(null, offer.attr('id').split('_')[1]); + resolve({offerID: offer.attr('id').split('_')[1]}); }); }; @@ -108,42 +90,25 @@ SteamCommunity.prototype.getConfirmationOfferID = function(confID, time, key, ca * @param {int} time - The unix timestamp with which the following key was generated * @param {string} key - The confirmation key that was generated using the preceding time and the tag "allow" (if accepting) or "cancel" (if not accepting) * @param {boolean} accept - true if you want to accept the confirmation, false if you want to cancel it - * @param {SteamCommunity~genericErrorCallback} callback - Called when the request is complete + * @param {SteamCommunity~genericErrorCallback} [callback] - Called when the request is complete + * @return Promise */ SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, key, accept, callback) { - // Ugly hack to maintain backward compatibility - var tag = accept ? 'allow' : 'cancel'; - if (typeof key == 'object') { - tag = key.tag; - key = key.key; - } + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let tag = accept ? 'accept' : 'reject'; - // The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility - request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, { - op: accept ? 'allow' : 'cancel', - cid: confID, - ck: confKey - }, true, function(err, body) { - if (!callback) { - return; - } - - if (err) { - callback(err); - return; - } + // The official app uses tags reject/accept, but cancel/allow still works so use these for backward compatibility + let body = await request(this, (confID instanceof Array) ? 'multiajaxop' : 'ajaxop', key, time, tag, { + op: accept ? 'allow' : 'cancel', + cid: confID, + ck: confKey + }); if (body.success) { - callback(null); - return; + return resolve(); } - if (body.message) { - callback(new Error(body.message)); - return; - } - - callback(new Error('Could not act on confirmation')); + reject(new Error(body.message || body.detail || 'Could not act on confirmation')); }); }; @@ -151,94 +116,79 @@ SteamCommunity.prototype.respondToConfirmation = function(confID, confKey, time, * Accept a confirmation for a given object (trade offer or market listing) automatically. * @param {string} identitySecret * @param {number|string} objectID - * @param {SteamCommunity~genericErrorCallback} callback + * @param {SteamCommunity~genericErrorCallback} [callback] + * @return Promise */ SteamCommunity.prototype.acceptConfirmationForObject = function(identitySecret, objectID, callback) { this._usedConfTimes = this._usedConfTimes || []; - let doConfirmation = () => { + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + // Figure out our time offset + if (typeof this._timeOffset == 'undefined') { + await new Promise((resolve) => { + SteamTotp.getTimeOffset((err, offset) => { + if (err) { + // not critical that this succeeds + return resolve(); + } + + this._timeOffset = offset; + resolve(); + }); + }); + } + let offset = this._timeOffset; let time = SteamTotp.time(offset); - this.getConfirmations(time, SteamTotp.getConfirmationKey(identitySecret, time, 'conf'), (err, confs) => { - if (err) { - callback(err); - return; - } + let key = SteamTotp.getConfirmationKey(identitySecret, time, 'list'); + let {confirmations} = await this.getConfirmations(time, key); - let conf = confs.find(conf => conf.creator == objectID); - if (!conf) { - callback(new Error('Could not find confirmation for object ' + objectID)); - return; - } + let conf = confirmations.find(conf => conf.creator == objectID); + if (!conf) { + return reject(new Error(`Could not find confirmation for object ${objectID}`)); + } - // make sure we don't reuse the same time - let localOffset = 0; - do { - time = SteamTotp.time(offset) + localOffset++; - } while (this._usedConfTimes.indexOf(time) != -1); + // make sure we don't reuse the same time + let localOffset = 0; + do { + time = SteamTotp.time(offset) + localOffset++; + } while (this._usedConfTimes.includes(time)); - this._usedConfTimes.push(time); - if (this._usedConfTimes.length > 60) { - this._usedConfTimes.splice(0, this._usedConfTimes.length - 60); // we don't need to save more than 60 entries - } + this._usedConfTimes.push(time); + if (this._usedConfTimes.length > 60) { + this._usedConfTimes.splice(0, this._usedConfTimes.length - 60); // we don't need to save more than 60 entries + } - conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, 'allow'), true, callback); - }); - }; - - if (typeof this._timeOffset !== 'undefined') { - // time offset is already known and saved - doConfirmation(); - } else { - SteamTotp.getTimeOffset((err, offset) => { - if (err) { - callback(err); - return; - } - - this._timeOffset = offset; - doConfirmation(); - - setTimeout(() => { - // Delete the saved time offset after 12 hours because why not - delete this._timeOffset; - }, 1000 * 60 * 60 * 12).unref(); - }); - } + await conf.respond(time, SteamTotp.getConfirmationKey(identitySecret, time, 'accept'), true); + }); }; /** * Send a single request to Steam to accept all outstanding confirmations (after loading the list). If one fails, the * entire request will fail and there will be no way to know which failed without loading the list again. * @param {number} time - * @param {string} confKey - * @param {string} allowKey - * @param {function} callback + * @param {string} listKey + * @param {string} acceptKey + * @param {function} [callback] + * @return Promise<{confirmations: CConfirmation[]}> */ -SteamCommunity.prototype.acceptAllConfirmations = function(time, confKey, allowKey, callback) { - this.getConfirmations(time, confKey, (err, confs) => { - if (err) { - callback(err); - return; +SteamCommunity.prototype.acceptAllConfirmations = function(time, listKey, acceptKey, callback) { + return StdLib.Promises.callbackPromise(null, callback, true, async (resolve, reject) => { + let {confirmations} = await this.getConfirmations(time, listKey); + + if (confirmations.length == 0) { + return resolve({confirmations: []}); } - if (confs.length == 0) { - callback(null, []); - return; - } + let confIds = confirmations.map(conf => conf.id); + let confKeys = confirmations.map(conf => conf.key); + await this.respondToConfirmation(confIds, confKeys, time, acceptKey, true); - this.respondToConfirmation(confs.map(conf => conf.id), confs.map(conf => conf.key), time, allowKey, true, (err) => { - if (err) { - callback(err); - return; - } - - callback(err, confs); - }); + resolve({confirmations}); }); }; -function request(community, url, key, time, tag, params, json, callback) { +async function request(community, url, key, time, tag, params) { if (!community.steamID) { throw new Error('Must be logged in before trying to do anything with confirmations'); } @@ -253,8 +203,8 @@ function request(community, url, key, time, tag, params, json, callback) { let req = { method: url == 'multiajaxop' ? 'POST' : 'GET', - uri: 'https://steamcommunity.com/mobileconf/' + url, - json: !!json + url: `https://steamcommunity.com/mobileconf/${url}`, + source: 'steamcommunity' }; if (req.method == 'GET') { @@ -263,12 +213,6 @@ function request(community, url, key, time, tag, params, json, callback) { req.form = params; } - community.httpRequest(req, (err, response, body) => { - if (err) { - callback(err); - return; - } - - callback(null, body); - }, 'steamcommunity'); + let result = await community.httpRequest(req); + return result.jsonBody || result.textBody; } diff --git a/components/http.js b/components/http.js index 206c52a..62ad662 100644 --- a/components/http.js +++ b/components/http.js @@ -1,4 +1,5 @@ const {HttpResponse} = require('@doctormckay/stdlib/http'); // eslint-disable-line +const {betterPromise} = require('@doctormckay/stdlib/promises'); const SteamCommunity = require('../index.js'); @@ -20,67 +21,50 @@ const SteamCommunity = require('../index.js'); * @return {Promise} */ SteamCommunity.prototype.httpRequest = function(options) { - return new Promise((resolve, reject) => { + return betterPromise(async (resolve, reject) => { let requestID = ++this._httpRequestID; let source = options.source || ''; - let continued = false; - - let continueRequest = async (err) => { - if (continued) { - return; + await betterPromise((resolve, reject) => { + if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, (err) => { + err ? reject(err) : resolve(); + })) { + // No pre-hook, or the pre-hook doesn't want to delay the request. + resolve(); } + }); - continued = true; + let result = await this._httpClient.request({ + method: options.method, + url: options.url, + queryString: options.qs, + headers: options.headers, + body: options.body, + urlEncodedForm: options.form, + multipartForm: options.multipartForm, + json: options.json, + followRedirects: options.followRedirect + }); - if (err) { - return reject(err); - } + let httpError = options.checkHttpError !== false && this._checkHttpError(result); + let communityError = !options.json && options.checkCommunityError !== false && this._checkCommunityError(result); + let tradeError = !options.json && options.checkTradeError !== false && this._checkTradeError(result); + let jsonError = options.json && options.checkJsonError !== false && !result.jsonBody ? new Error('Malformed JSON response') : null; - /** @var {HttpResponse} result */ - let result; + this.emit('postHttpRequest', { + requestID, + source, + options, + response: result, + body: result.textBody, + error: httpError || communityError || tradeError || jsonError || null, + httpError, + communityError, + tradeError, + jsonError + }); - try { - result = await this._httpClient.request({ - method: options.method, - url: options.url, - queryString: options.qs, - headers: options.headers, - body: options.body, - urlEncodedForm: options.form, - multipartForm: options.multipartForm, - json: options.json, - followRedirects: options.followRedirect - }); - } catch (ex) { - return reject(ex); - } - - let httpError = options.checkHttpError !== false && this._checkHttpError(result); - let communityError = !options.json && options.checkCommunityError !== false && this._checkCommunityError(result); - let tradeError = !options.json && options.checkTradeError !== false && this._checkTradeError(result); - let jsonError = options.json && options.checkJsonError !== false && !result.jsonBody ? new Error('Malformed JSON response') : null; - - this.emit('postHttpRequest', { - requestID, - source, - options, - response: result, - body: result.textBody, - error: httpError || communityError || tradeError || jsonError || null, - httpError, - communityError, - tradeError, - jsonError - }); - - resolve(result); - }; - - if (!this.onPreHttpRequest || !this.onPreHttpRequest(requestID, source, options, continueRequest)) { - // No pre-hook, or the pre-hook doesn't want to delay the request. - continueRequest(null); - } + resolve(result); }); }; diff --git a/package.json b/package.json index 6d3c0a8..3c83092 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/DoctorMcKay/node-steamcommunity.git" }, "dependencies": { - "@doctormckay/stdlib": "^2.5.0", + "@doctormckay/stdlib": "^2.6.0", "cheerio": "0.22.0", "image-size": "^0.8.2", "request": "^2.88.0",