@tf2autobot/tradeoffer-manager
Version:
A simple trade offers API for Steam
292 lines (244 loc) • 11.7 kB
JavaScript
"use strict";
const TradeOfferManager = require('./index.js');
const ETradeOfferState = TradeOfferManager.ETradeOfferState;
const EOfferFilter = TradeOfferManager.EOfferFilter;
const EConfirmationMethod = TradeOfferManager.EConfirmationMethod;
/*
* pollData is an object which has the following structure:
* - `offersSince` is the STANDARD unix time (Math.floor(Date.now() / 1000)) of the last known offer change
* - `sent` is an object whose keys are offer IDs for known offers we've sent and whose values are the last known states of those offers
* - `received` is the same as `sent`, for offers we've received
* - `offerData` is an object whose keys are offer IDs. Values are objects mapping arbitrary keys to arbitrary values.
* Some keys are reserved for offer-specific options. These are:
* - `cancelTime` - The time, in milliseconds, after which the offer should be canceled automatically. Defaults to the TradeOfferManager's set cancelTime.
* - `pendingCancelTime` - Ditto `cancelTime`, except only for offers which are CreatedNeedsConfirmation.
*/
TradeOfferManager.prototype.doPoll = function(doFullUpdate) {
if (!this.apiKey && !this.accessToken) {
// In case a race condition causes this to be called after we've shutdown or before we have an api key or access token
return;
}
const timeSinceLastPoll = Date.now() - this._lastPoll;
if (timeSinceLastPoll < this.minimumPollInterval) {
// We last polled less than a second ago... we shouldn't spam the API
// Reset the timer to poll minimumPollInterval after the last one
this._resetPollTimer(this.minimumPollInterval - timeSinceLastPoll);
return;
}
this._lastPoll = Date.now();
clearTimeout(this._pollTimer);
var offersSince = 0;
if (this.pollData.offersSince) {
// It looks like sometimes Steam can be dumb and backdate a modified offer. We need to handle this.
// Let's add a 30-minute buffer.
offersSince = this.pollData.offersSince - 1800;
}
var fullUpdate = false;
if (Date.now() - this._lastPollFullUpdate >= this.pollFullUpdateInterval || doFullUpdate) {
fullUpdate = true;
this._lastPollFullUpdate = Date.now();
offersSince = 1;
}
this.emit('debug', 'Doing trade offer poll since ' + offersSince + (fullUpdate ? ' (full update)' : ''));
var requestStart = Date.now();
this.getOffers(fullUpdate ? EOfferFilter.All : EOfferFilter.ActiveOnly, new Date(offersSince * 1000), (err, sent, received) => {
if (err) {
this.emit('debug', "Error getting trade offers for poll: " + err.message);
this.emit('pollFailure', err);
this._resetPollTimer();
return;
}
this.emit('debug', 'Trade offer poll succeeded in ' + (Date.now() - requestStart) + ' ms');
/*if (fullUpdate) {
// We can only purge stuff if this is a full update; otherwise, lack of an offer's presence doesn't mean Steam forgot about it
var trackedIds = sent.map(offerId).concat(received.map(offerId)); // OfferIDs that are active in Steam's memory
var oldIds = Object.keys(this.pollData.sent || {}).concat(Object.keys(this.pollData.received || {})); // OfferIDs that we have in our poll data
// This routine won't delete any offers that we last saw as Active. If it changed state and we didn't see it,
// it'll stick around forever. Perhaps someday we should account for this as well; e.g. by clearing offers
// with super super low IDs relative to the current ID.
oldIds.forEach((offerID) => {
if (trackedIds.indexOf(offerID) == -1) {
// This offer is no longer in Steam's memory. Let's clean it up.
var found = false;
var offerAgeDays = this.pollData.timestamps && this.pollData.timestamps[offerID] ? (Date.now() - this.pollData.timestamps[offerID]) / (1000 * 60 * 60 * 24) : null;
if (offerAgeDays && offerAgeDays <= 30) {
return; // it's too new to clean up
}
if (this.pollData.sent && this.pollData.sent[offerID] && (this.pollData.sent[offerID] != ETradeOfferState.Active || (offerAgeDays && offerAgeDays > 30))) {
found = found || delete this.pollData.sent[offerID];
}
if (this.pollData.received && this.pollData.received[offerID] && (this.pollData.received[offerID] != ETradeOfferState.Active || (offerAgeDays && offerAgeDays > 30))) {
found = found || delete this.pollData.received[offerID];
}
if (found) {
this.emit('debug', "Cleaning up stale offer #" + offerID + " from poll data");
}
}
});
var knownTimestamps = Object.keys(this.pollData.timestamps || {});
knownTimestamps.forEach((offerID) => {
var isSent = this.pollData.sent && this.pollData.sent[offerID];
var isReceived = this.pollData.received && this.pollData.received[offerID];
if (!isSent && !isReceived && Date.now() - this.pollData.timestamps[offerID] >= (1000 * 60 * 60 * 24 * 60)) {
this.emit('debug', "Cleaning up stale timestamp " + this.pollData.timestamps[offerID] + " for offer " + offerID + " from poll data");
delete this.pollData.timestamps[offerID];
}
});
}*/
let changed = false;
var timestamps = this.pollData.timestamps || {};
var offers = this.pollData.sent || {};
var hasGlitchedOffer = false;
sent.forEach((offer) => {
if (!offers[offer.id]) {
// We sent this offer, but we have no record of it! Good job Steam
// Apparently offers can appear in the API before the send() call has returned, so we'll need to add a delay
// Only emit the unknownOfferSent event if currently there's no offers that await a response in .send
if (!this._pendingOfferSendResponses) {
if (offer.fromRealTimeTrade) {
// This is a real-time trade offer.
if (offer.state == ETradeOfferState.CreatedNeedsConfirmation || (offer.state == ETradeOfferState.Active && offer.confirmationMethod != EConfirmationMethod.None)) {
// we need to confirm this
this.emit('realTimeTradeConfirmationRequired', offer);
} else if (offer.state == ETradeOfferState.Accepted) {
// both parties confirmed, trade complete
this.emit('realTimeTradeCompleted', offer);
}
}
this.emit('unknownOfferSent', offer);
offers[offer.id] = offer.state;
timestamps[offer.id] = offer.created.getTime() / 1000;
changed = true;
}
} else if (offer.state != offers[offer.id]) {
changed = true;
if (!offer.isGlitched()) {
// We sent this offer, and it has now changed state
if (offer.fromRealTimeTrade && offer.state == ETradeOfferState.Accepted) {
this.emit('realTimeTradeCompleted', offer);
}
this.emit('sentOfferChanged', offer, offers[offer.id]);
offers[offer.id] = offer.state;
timestamps[offer.id] = offer.created.getTime() / 1000;
} else {
hasGlitchedOffer = true;
var countWithoutName = !this._language ? 0 : offer.itemsToGive.concat(offer.itemsToReceive).filter(function(item) { return !item.name; }).length;
this.emit('debug', "Not emitting sentOfferChanged for " + offer.id + " right now because it's glitched (" +
offer.itemsToGive.length + " to give, " + offer.itemsToReceive.length + " to receive, " + countWithoutName + " without name)");
}
}
if (offer.state == ETradeOfferState.Active) {
// The offer is still Active, and we sent it. See if it's time to cancel it automatically.
var cancelTime = this.cancelTime;
// Check if this offer has a custom cancelTime
var customCancelTime = offer.data('cancelTime');
if (typeof customCancelTime !== 'undefined') {
cancelTime = customCancelTime;
}
if (cancelTime && (Date.now() - offer.updated.getTime() >= cancelTime)) {
changed = true;
offer.cancel((err) => {
if (!err) {
this.emit('sentOfferCanceled', offer, 'cancelTime');
} else {
this.emit('debug', "Can't auto-cancel offer #" + offer.id + ": " + err.message);
}
});
}
}
if (offer.state == ETradeOfferState.CreatedNeedsConfirmation && this.pendingCancelTime) {
// The offer needs to be confirmed to be sent. Let's see if the maximum time has elapsed before we cancel it.
var pendingCancelTime = this.pendingCancelTime;
var customPendingCancelTime = offer.data('pendingCancelTime');
if (typeof customPendingCancelTime !== 'undefined') {
pendingCancelTime = customPendingCancelTime;
}
if (pendingCancelTime && (Date.now() - offer.created.getTime() >= pendingCancelTime)) {
changed = true;
offer.cancel((err) => {
if (!err) {
this.emit('sentPendingOfferCanceled', offer);
} else {
this.emit('debug', "Can't auto-canceling pending-confirmation offer #" + offer.id + ": " + err.message);
}
});
}
}
});
if (this.cancelOfferCount) {
var sentActive = sent.filter(offer => offer.state == ETradeOfferState.Active);
if (sentActive.length >= this.cancelOfferCount) {
// We have too many offers out. Let's cancel the oldest.
// Use updated since that reflects when it was confirmed, if necessary.
var oldest = sentActive[0];
for (var i = 1; i < sentActive.length; i++) {
if (sentActive[i].updated.getTime() < oldest.updated.getTime()) {
oldest = sentActive[i];
}
}
// Make sure it's old enough
if (Date.now() - oldest.updated.getTime() >= this.cancelOfferCountMinAge) {
changed = true;
oldest.cancel((err) => {
if (!err) {
this.emit('sentOfferCanceled', oldest, 'cancelOfferCount');
}
});
}
}
}
this.pollData.sent = offers;
offers = this.pollData.received || {};
received.forEach((offer) => {
if (offer.isGlitched()) {
hasGlitchedOffer = true;
return;
}
changed = true;
if (offer.fromRealTimeTrade) {
// This is a real-time trade offer
if (!offers[offer.id] && (offer.state == ETradeOfferState.CreatedNeedsConfirmation || (offer.state == ETradeOfferState.Active && offer.confirmationMethod != EConfirmationMethod.None))) {
this.emit('realTimeTradeConfirmationRequired', offer);
} else if (offer.state == ETradeOfferState.Accepted && (!offers[offer.id] || (offers[offer.id] != offer.state))) {
this.emit('realTimeTradeCompleted', offer);
}
}
if (!offers[offer.id] && offer.state == ETradeOfferState.Active) {
this.emit('newOffer', offer);
} else if (offers[offer.id] && offer.state != offers[offer.id]) {
this.emit('receivedOfferChanged', offer, offers[offer.id]);
}
offers[offer.id] = offer.state;
timestamps[offer.id] = offer.created.getTime() / 1000;
});
this.pollData.received = offers;
this.pollData.timestamps = timestamps;
// Find the latest update time
if (!hasGlitchedOffer) {
var latest = this.pollData.offersSince || 0;
sent.concat(received).forEach((offer) => {
var updated = Math.floor(offer.updated.getTime() / 1000);
if (updated > latest) {
latest = updated;
}
});
this.pollData.offersSince = latest;
}
this.emit('pollSuccess');
// If something has changed, emit the event
if (changed) {
this.emit('pollData', this.pollData);
}
this._resetPollTimer();
});
};
TradeOfferManager.prototype._resetPollTimer = function(time) {
if (this.pollInterval < 0) {
// timed polling is disabled
return;
}
if (time || this.pollInterval >= this.minimumPollInterval) {
clearTimeout(this._pollTimer);
this._pollTimer = setTimeout(this.doPoll.bind(this), time || this.pollInterval);
}
};