Skip to content

Commit b651874

Browse files
authored
Display Metadata on /tradeoffers-pages (#283)
* Display item metadata on /tradeoffers * chore: run format * Remove csfloat check for offer tracking * fix: avoid circular dependency * clarify variable names * Move config parsing to utility * Handle access token caching * simplify tradeoffer types * update variable names * rework caching with CachedHandler * Handle case of no open trades * Rename variables and update references * Revert tracking prerequisite * Apply review suggestions
1 parent 04b9977 commit b651874

File tree

7 files changed

+246
-3
lines changed

7 files changed

+246
-3
lines changed

src/lib/alarms/trade_offer.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {AnnotateOffer} from '../bridge/handlers/annotate_offer';
66
import {PingCancelTrade} from '../bridge/handlers/ping_cancel_trade';
77
import {CancelTradeOffer} from '../bridge/handlers/cancel_trade_offer';
88
import {FetchSteamUser} from '../bridge/handlers/fetch_steam_user';
9+
import {rgDescription} from '../types/steam';
10+
import {HasPermissions} from '../bridge/handlers/has_permissions';
11+
import {convertSteamID32To64} from '../utils/userinfo';
912

1013
export async function pingSentTradeOffers(pendingTrades: Trade[]) {
1114
const {offers, type} = await getSentTradeOffers();
@@ -191,11 +194,18 @@ async function getSentTradeOffers(): Promise<{offers: OfferStatus[]; type: Trade
191194

192195
interface TradeOfferItem {
193196
assetid: string;
197+
appid: number;
198+
contextid: string;
199+
classid: string;
200+
instanceid: string;
201+
amount: string;
202+
missing: boolean;
203+
est_usd: string;
194204
}
195205

196-
interface TradeOffersAPIOffer {
206+
export interface TradeOffersAPIOffer {
197207
tradeofferid: string;
198-
accountid_other: string;
208+
accountid_other: number;
199209
trade_offer_state: TradeOfferState;
200210
items_to_give?: TradeOfferItem[];
201211
items_to_receive?: TradeOfferItem[];
@@ -207,6 +217,7 @@ interface TradeOffersAPIResponse {
207217
response: {
208218
trade_offers_sent: TradeOffersAPIOffer[];
209219
trade_offers_received: TradeOffersAPIOffer[];
220+
descriptions?: rgDescription[];
210221
};
211222
}
212223

@@ -218,7 +229,7 @@ function offerStateMapper(e: TradeOffersAPIOffer): OfferStatus {
218229
received_asset_ids: (e.items_to_receive || []).map((e) => e.assetid),
219230
time_created: e.time_created,
220231
time_updated: e.time_updated,
221-
other_steam_id64: (BigInt('76561197960265728') + BigInt(e.accountid_other)).toString(),
232+
other_steam_id64: convertSteamID32To64(e.accountid_other),
222233
} as OfferStatus;
223234
}
224235

@@ -266,6 +277,51 @@ async function getSentAndReceivedTradeOffersFromAPI(): Promise<{
266277
};
267278
}
268279

280+
export async function getTradeOffersWithDescriptionFromAPI(steam_id?: string): Promise<{
281+
received: TradeOffersAPIOffer[];
282+
sent: TradeOffersAPIOffer[];
283+
descriptions: rgDescription[];
284+
steam_id?: string | null;
285+
}> {
286+
// check if permissions are granted
287+
const steamPoweredPermissions = await HasPermissions.handleRequest(
288+
{
289+
permissions: [],
290+
origins: ['https://api.steampowered.com/*'],
291+
},
292+
{}
293+
);
294+
if (!steamPoweredPermissions.granted) {
295+
return {
296+
received: [],
297+
sent: [],
298+
descriptions: [],
299+
steam_id: steam_id,
300+
};
301+
}
302+
303+
const access = await getAccessToken(steam_id);
304+
305+
const resp = await fetch(
306+
`https://api.steampowered.com/IEconService/GetTradeOffers/v1/?access_token=${access.token}&get_received_offers=true&get_sent_offers=true&get_descriptions=true`,
307+
{
308+
credentials: 'include',
309+
}
310+
);
311+
312+
if (resp.status !== 200) {
313+
throw new Error('invalid status');
314+
}
315+
316+
const data = (await resp.json()) as TradeOffersAPIResponse;
317+
return {
318+
received: data.response?.trade_offers_received || [],
319+
sent: data.response?.trade_offers_sent || [],
320+
steam_id: access.steam_id,
321+
descriptions: data.response?.descriptions || [],
322+
};
323+
}
324+
269325
const BANNER_TO_STATE: {[banner: string]: TradeOfferState} = {
270326
accepted: TradeOfferState.Accepted,
271327
counter: TradeOfferState.Countered,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {getTradeOffersWithDescriptionFromAPI, TradeOffersAPIOffer} from '../../alarms/trade_offer';
2+
import {rgDescription} from '../../types/steam';
3+
import {CachedHandler} from '../wrappers/cached';
4+
import {SimpleHandler} from './main';
5+
import {RequestType} from './types';
6+
7+
interface FetchSteamTradesRequest {
8+
steam_id?: string;
9+
// Used for caching the request uniquely, does not affect the return results
10+
trade_offer_id?: number;
11+
}
12+
13+
export interface FetchSteamTradesResponse {
14+
received: TradeOffersAPIOffer[];
15+
sent: TradeOffersAPIOffer[];
16+
descriptions: rgDescription[];
17+
steam_id?: string | null;
18+
}
19+
20+
export const FetchSteamTrades = new CachedHandler(
21+
new SimpleHandler<FetchSteamTradesRequest, FetchSteamTradesResponse>(
22+
RequestType.FETCH_STEAM_TRADES,
23+
async (req) => {
24+
const resp = await getTradeOffersWithDescriptionFromAPI(req.steam_id);
25+
if (!resp) {
26+
throw new Error('Error fetching Steam trade offers from API');
27+
}
28+
29+
return resp;
30+
}
31+
),
32+
1,
33+
10 * 60 * 1000
34+
);

src/lib/bridge/handlers/handlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {PingTradeStatus} from './ping_trade_status';
2323
import {PingStatus} from './ping_status';
2424
import {FetchOwnInventory} from './fetch_own_inventory';
2525
import {CancelTradeOffer} from './cancel_trade_offer';
26+
import {FetchSteamTrades} from './fetch_steam_trades';
2627

2728
export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
2829
[RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage,
@@ -48,4 +49,5 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
4849
[RequestType.PING_STATUS]: PingStatus,
4950
[RequestType.FETCH_OWN_INVENTORY]: FetchOwnInventory,
5051
[RequestType.CANCEL_TRADE_OFFER]: CancelTradeOffer,
52+
[RequestType.FETCH_STEAM_TRADES]: FetchSteamTrades,
5153
};

src/lib/bridge/handlers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ export enum RequestType {
2222
PING_STATUS = 20,
2323
FETCH_OWN_INVENTORY = 21,
2424
CANCEL_TRADE_OFFER = 22,
25+
FETCH_STEAM_TRADES = 23,
2526
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {CustomElement, InjectAppend, InjectionMode} from '../injectors';
2+
import {ItemHolderMetadata} from '../common/item_holder_metadata';
3+
import {rgAsset} from '../../types/steam';
4+
5+
// Annotates item info (float, seed, etc...) in boxes on the Trade Offers Page
6+
@CustomElement()
7+
// Items in received/sent trade offers
8+
@InjectAppend('.tradeoffer .trade_item', InjectionMode.CONTINUOUS)
9+
export class TradeOfferHolderMetadata extends ItemHolderMetadata {
10+
get assetId(): string | undefined {
11+
return $J(this).parent().attr('data-csfloat-assetid');
12+
}
13+
14+
get asset(): rgAsset | undefined {
15+
const dataDescription = $J(this).parent().attr('data-csfloat-description');
16+
17+
if (!dataDescription) return undefined;
18+
19+
return JSON.parse(dataDescription) as rgAsset;
20+
}
21+
22+
get ownerSteamId(): string | undefined {
23+
return $J(this).parent().attr('data-csfloat-owner-steamid');
24+
}
25+
}

src/lib/page_scripts/trade_offers.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,105 @@
11
import {init} from './utils';
22
import '../components/trade_offers/better_tracking';
3+
import '../components/trade_offers/trade_offer_holder_metadata';
34
import {inPageContext} from '../utils/snips';
45
import {ClientSend} from '../bridge/client';
56
import {PingSetupExtension} from '../bridge/handlers/ping_setup_extension';
67
import {PingExtensionStatus} from '../bridge/handlers/ping_extension_status';
8+
import {FetchSteamTrades, FetchSteamTradesResponse} from '../bridge/handlers/fetch_steam_trades';
9+
import {convertSteamID32To64, getUserSteamID} from '../utils/userinfo';
710

811
init('src/lib/page_scripts/trade_offers.js', main);
912

1013
function main() {}
1114

15+
/**
16+
* Gets the trade offers from the local storage or fetches them from the API.
17+
* Local storage serves as a cache here.
18+
* @param steam_id the steam id of logged in user
19+
* @returns the trade offers
20+
*/
21+
function fetchTradeOffers(steam_id: string): Promise<FetchSteamTradesResponse> | undefined {
22+
const latestTradeIDFromPage = document.querySelector('.tradeoffer')?.id.split('_')[1];
23+
const trade_offer_id = latestTradeIDFromPage ? Number.parseInt(latestTradeIDFromPage) : undefined;
24+
25+
if (!trade_offer_id) {
26+
return;
27+
}
28+
29+
return ClientSend(FetchSteamTrades, {steam_id, trade_offer_id});
30+
}
31+
32+
/**
33+
* Fetches the api data for trade offers and stores relevant data in the DOM to be used by Lit components.
34+
*/
35+
async function annotateTradeOfferItemElements() {
36+
const steam_id = getUserSteamID();
37+
38+
if (!steam_id) {
39+
console.error('Failed to get steam_id', steam_id);
40+
return;
41+
}
42+
43+
const steamTrades = await fetchTradeOffers(steam_id);
44+
45+
if (!steamTrades) {
46+
return;
47+
}
48+
49+
const tradeOfferElements = document.querySelectorAll('.tradeoffer');
50+
51+
for (const tradeOfferElement of tradeOfferElements) {
52+
const tradeOfferID = tradeOfferElement.id.split('_')[1];
53+
const tradeItemElements = tradeOfferElement.querySelectorAll('.trade_item');
54+
const tradeOffer =
55+
steamTrades.sent.find((t) => t.tradeofferid === tradeOfferID) ??
56+
steamTrades.received.find((t) => t.tradeofferid === tradeOfferID);
57+
if (!tradeOffer) {
58+
continue;
59+
}
60+
61+
for (const tradeItemElement of tradeItemElements) {
62+
// Format: classinfo/{appid}/{classid}/{instanceid}
63+
// Example: data-economy-item="classinfo/730/310777185/302028390"
64+
const economyItemParts = tradeItemElement.getAttribute('data-economy-item')?.split('/');
65+
const classId = economyItemParts?.[2];
66+
const instanceId = economyItemParts?.[3];
67+
68+
if (!classId || !instanceId) {
69+
continue;
70+
}
71+
72+
const description = steamTrades.descriptions.find(
73+
(d) => d.classid === classId && d.instanceid === instanceId
74+
);
75+
if (description) {
76+
tradeItemElement.setAttribute('data-csfloat-description', JSON.stringify(description));
77+
}
78+
79+
let isOwnItem = true;
80+
let apiItem = tradeOffer?.items_to_give?.find((a) => a.classid === classId && a.instanceid === instanceId);
81+
if (!apiItem) {
82+
isOwnItem = false;
83+
apiItem = tradeOffer?.items_to_receive?.find(
84+
(a) => a.classid === classId && a.instanceid === instanceId
85+
);
86+
}
87+
88+
const ownerId = isOwnItem ? steam_id : convertSteamID32To64(tradeOffer.accountid_other);
89+
90+
if (ownerId) {
91+
tradeItemElement.setAttribute('data-csfloat-owner-steamid', ownerId);
92+
}
93+
if (apiItem?.assetid) {
94+
tradeItemElement.setAttribute('data-csfloat-assetid', apiItem.assetid);
95+
}
96+
}
97+
}
98+
}
99+
12100
if (!inPageContext()) {
101+
annotateTradeOfferItemElements();
102+
13103
const refresh = setInterval(() => {
14104
const widget = document.getElementsByTagName('csfloat-better-tracking-widget');
15105
if (!widget || widget.length === 0) {

src/lib/utils/userinfo.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
interface UserInfo {
2+
account_name: string;
3+
accountid: number;
4+
country_code: string;
5+
is_limited: boolean;
6+
is_partner_member: boolean;
7+
is_support: boolean;
8+
logged_in: boolean;
9+
steamid: string;
10+
}
11+
12+
export function getUserInfo() {
13+
const configUserInfo = document.getElementById('application_config')?.dataset?.userinfo;
14+
if (!configUserInfo) {
15+
return null;
16+
}
17+
return JSON.parse(configUserInfo) as UserInfo;
18+
}
19+
20+
export function getUserSteamID() {
21+
const userInfo = getUserInfo();
22+
if (!userInfo?.logged_in) {
23+
return null;
24+
}
25+
return userInfo.steamid;
26+
}
27+
28+
/**
29+
* Converts a SteamID32 to a SteamID64
30+
* @param steamID32 number
31+
* @returns SteamID64
32+
*/
33+
export function convertSteamID32To64(steamID32: number) {
34+
return (BigInt('76561197960265728') + BigInt(steamID32)).toString();
35+
}

0 commit comments

Comments
 (0)