diff --git a/src/lib/alarms/csfloat_trade_pings.ts b/src/lib/alarms/csfloat_trade_pings.ts index 30a64915..45e3393c 100644 --- a/src/lib/alarms/csfloat_trade_pings.ts +++ b/src/lib/alarms/csfloat_trade_pings.ts @@ -1,7 +1,7 @@ import {Trade} from '../types/float_market'; import {FetchPendingTrades} from '../bridge/handlers/fetch_pending_trades'; import {pingTradeHistory} from './trade_history'; -import {pingSentTradeOffers} from './trade_offer'; +import {pingCancelTrades, pingSentTradeOffers} from './trade_offer'; import {HasPermissions} from '../bridge/handlers/has_permissions'; import {PingExtensionStatus} from '../bridge/handlers/ping_extension_status'; @@ -53,4 +53,10 @@ export async function pingTradeStatus() { } catch (e) { console.error('failed to ping sent trade offer state', e); } + + try { + await pingCancelTrades(pendingTrades); + } catch (e) { + console.error('failed to ping cancel ping trade offers', e); + } } diff --git a/src/lib/alarms/trade_offer.ts b/src/lib/alarms/trade_offer.ts index 65887a86..22fb9a18 100644 --- a/src/lib/alarms/trade_offer.ts +++ b/src/lib/alarms/trade_offer.ts @@ -1,8 +1,9 @@ import {TradeOfferState} from '../types/steam_constants'; -import {Trade} from '../types/float_market'; +import {Trade, TradeState} from '../types/float_market'; import {TradeOfferStatus, TradeOffersType} from '../bridge/handlers/trade_offer_status'; import {clearAccessTokenFromStorage, getAccessToken} from './access_token'; import {AnnotateOffer} from '../bridge/handlers/annotate_offer'; +import {PingCancelTrade} from '../bridge/handlers/ping_cancel_trade'; interface OfferStatus { offer_id: string; @@ -64,6 +65,44 @@ export async function pingSentTradeOffers(pendingTrades: Trade[]) { } } +export async function pingCancelTrades(pendingTrades: Trade[]) { + const hasWaitForCancelPing = pendingTrades.find((e) => e.state === TradeState.PENDING && e.wait_for_cancel_ping); + if (!hasWaitForCancelPing) { + // Nothing to process/ping, exit + return; + } + + const tradeOffers = await getSentAndReceivedTradeOffersFromAPI(); + + const allTradeOffers = [...(tradeOffers.sent || []), ...(tradeOffers.received || [])]; + + for (const trade of pendingTrades) { + if (trade.state !== TradeState.PENDING) { + continue; + } + + if (!trade.wait_for_cancel_ping) { + continue; + } + + const tradeOffer = allTradeOffers.find((e) => e.offer_id === trade.steam_offer.id); + if ( + tradeOffer && + (tradeOffer.state === TradeOfferState.Active || + tradeOffer.state === TradeOfferState.Accepted || + tradeOffer.state === TradeOfferState.CreatedNeedsConfirmation) + ) { + // We don't want to send a cancel ping if the offer is active or valid + continue; + } + + try { + await PingCancelTrade.handleRequest({trade_id: trade.id}, {}); + } catch (e) { + console.error(`failed to send cancel ping for trade ${trade.id}`, e); + } + } +} async function getEnglishSentTradeOffersHTML(): Promise { const resp = await fetch(`https://steamcommunity.com/id/me/tradeoffers/sent`, { credentials: 'include', @@ -90,7 +129,7 @@ async function getEnglishSentTradeOffersHTML(): Promise { async function getSentTradeOffers(): Promise<{offers: OfferStatus[]; type: TradeOffersType}> { try { - const offers = await getTradeOffersFromAPI(); + const offers = await getSentTradeOffersFromAPI(); if (offers.length > 0) { // Hedge in case this endpoint gets killed, only return if there are results, fallback to HTML parser return {offers, type: TradeOffersType.API}; @@ -109,19 +148,31 @@ interface TradeOfferItem { assetid: string; } +interface TradeOffersAPIOffer { + tradeofferid: string; + accountid_other: string; + trade_offer_state: TradeOfferState; + items_to_give?: TradeOfferItem[]; + items_to_receive?: TradeOfferItem[]; +} + interface TradeOffersAPIResponse { response: { - trade_offers_sent: { - tradeofferid: string; - accountid_other: string; - trade_offer_state: TradeOfferState; - items_to_give?: TradeOfferItem[]; - items_to_receive?: TradeOfferItem[]; - }[]; + trade_offers_sent: TradeOffersAPIOffer[]; + trade_offers_received: TradeOffersAPIOffer[]; }; } -async function getTradeOffersFromAPI(): Promise { +function offerStateMapper(e: TradeOffersAPIOffer): OfferStatus { + return { + offer_id: e.tradeofferid, + state: e.trade_offer_state, + given_asset_ids: (e.items_to_give || []).map((e) => e.assetid), + received_asset_ids: (e.items_to_receive || []).map((e) => e.assetid), + } as OfferStatus; +} + +async function getSentTradeOffersFromAPI(): Promise { const accessToken = await getAccessToken(); const resp = await fetch( @@ -136,14 +187,28 @@ async function getTradeOffersFromAPI(): Promise { } const data = (await resp.json()) as TradeOffersAPIResponse; - return data.response.trade_offers_sent.map((e) => { - return { - offer_id: e.tradeofferid, - state: e.trade_offer_state, - given_asset_ids: (e.items_to_give || []).map((e) => e.assetid), - received_asset_ids: (e.items_to_receive || []).map((e) => e.assetid), - } as OfferStatus; - }); + return data.response.trade_offers_sent.map(offerStateMapper); +} + +async function getSentAndReceivedTradeOffersFromAPI(): Promise<{received: OfferStatus[]; sent: OfferStatus[]}> { + const accessToken = await getAccessToken(); + + const resp = await fetch( + `https://api.steampowered.com/IEconService/GetTradeOffers/v1/?access_token=${accessToken}&get_received_offers=true&get_sent_offers=true`, + { + credentials: 'include', + } + ); + + if (resp.status !== 200) { + throw new Error('invalid status'); + } + + const data = (await resp.json()) as TradeOffersAPIResponse; + return { + received: data.response.trade_offers_received.map(offerStateMapper), + sent: data.response.trade_offers_sent.map(offerStateMapper), + }; } const BANNER_TO_STATE: {[banner: string]: TradeOfferState} = { diff --git a/src/lib/bridge/handlers/handlers.ts b/src/lib/bridge/handlers/handlers.ts index 0aef750f..7b221be4 100644 --- a/src/lib/bridge/handlers/handlers.ts +++ b/src/lib/bridge/handlers/handlers.ts @@ -16,6 +16,7 @@ import {TradeOfferStatus} from './trade_offer_status'; import {HasPermissions} from './has_permissions'; import {PingSetupExtension} from './ping_setup_extension'; import {PingExtensionStatus} from './ping_extension_status'; +import {PingCancelTrade} from './ping_cancel_trade'; export const HANDLERS_MAP: {[key in RequestType]: RequestHandler} = { [RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage, @@ -34,4 +35,5 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler} = { [RequestType.HAS_PERMISSIONS]: HasPermissions, [RequestType.PING_SETUP_EXTENSION]: PingSetupExtension, [RequestType.PING_EXTENSION_STATUS]: PingExtensionStatus, + [RequestType.PING_CANCEL_TRADE]: PingCancelTrade, }; diff --git a/src/lib/bridge/handlers/ping_cancel_trade.ts b/src/lib/bridge/handlers/ping_cancel_trade.ts new file mode 100644 index 00000000..a319ebe0 --- /dev/null +++ b/src/lib/bridge/handlers/ping_cancel_trade.ts @@ -0,0 +1,31 @@ +import {SimpleHandler} from './main'; +import {RequestType} from './types'; +import {environment} from '../../../environment'; +import {Trade} from '../../types/float_market'; + +export interface PingCancelTradeRequest { + trade_id: string; +} + +export interface PingCancelTradeResponse { + trade: Trade; +} + +export const PingCancelTrade = new SimpleHandler( + RequestType.PING_CANCEL_TRADE, + async (req) => { + const resp = await fetch(`${environment.csfloat_base_api_url}/v1/trades/${req.trade_id}/cancel-ping`, { + credentials: 'include', + method: 'POST', + }); + + if (resp.status !== 200) { + throw new Error('invalid status'); + } + + const trade = (await resp.json()) as Trade; + return { + trade, + }; + } +); diff --git a/src/lib/bridge/handlers/types.ts b/src/lib/bridge/handlers/types.ts index ad03d9ab..5cc0fb70 100644 --- a/src/lib/bridge/handlers/types.ts +++ b/src/lib/bridge/handlers/types.ts @@ -15,4 +15,5 @@ export enum RequestType { HAS_PERMISSIONS, PING_SETUP_EXTENSION, PING_EXTENSION_STATUS, + PING_CANCEL_TRADE, } diff --git a/src/lib/types/float_market.ts b/src/lib/types/float_market.ts index 82069d79..d35e377b 100644 --- a/src/lib/types/float_market.ts +++ b/src/lib/types/float_market.ts @@ -102,4 +102,5 @@ export interface Trade { state: TradeState; trade_url: string; steam_offer: SteamOffer; + wait_for_cancel_ping?: boolean; }