Updated confirmations.js methods to use new http interface

This commit is contained in:
Alex Corn 2023-06-27 03:38:56 -04:00
parent 028ee43bda
commit c1901d5f55
No known key found for this signature in database
GPG Key ID: E51989A3E7A27FDF
4 changed files with 148 additions and 205 deletions

View File

@ -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<void>
*/
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);
};

View File

@ -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<void>
*/
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<void>
*/
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;
}

View File

@ -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<HttpResponse>}
*/
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);
});
};

View File

@ -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",