Skip to content

Display Metadata on /tradeoffers-pages #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions src/lib/alarms/trade_offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {AnnotateOffer} from '../bridge/handlers/annotate_offer';
import {PingCancelTrade} from '../bridge/handlers/ping_cancel_trade';
import {CancelTradeOffer} from '../bridge/handlers/cancel_trade_offer';
import {FetchSteamUser} from '../bridge/handlers/fetch_steam_user';
import {rgDescription} from '../types/steam';
import {HasPermissions} from '../bridge/handlers/has_permissions';
import {convertSteamID32To64} from '../utils/userinfo';

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

interface TradeOfferItem {
assetid: string;
appid: number;
contextid: string;
classid: string;
instanceid: string;
amount: string;
missing: boolean;
est_usd: string;
}

interface TradeOffersAPIOffer {
export interface TradeOffersAPIOffer {
tradeofferid: string;
accountid_other: string;
accountid_other: number;
trade_offer_state: TradeOfferState;
items_to_give?: TradeOfferItem[];
items_to_receive?: TradeOfferItem[];
Expand All @@ -207,6 +217,7 @@ interface TradeOffersAPIResponse {
response: {
trade_offers_sent: TradeOffersAPIOffer[];
trade_offers_received: TradeOffersAPIOffer[];
descriptions?: rgDescription[];
};
}

Expand All @@ -218,7 +229,7 @@ function offerStateMapper(e: TradeOffersAPIOffer): OfferStatus {
received_asset_ids: (e.items_to_receive || []).map((e) => e.assetid),
time_created: e.time_created,
time_updated: e.time_updated,
other_steam_id64: (BigInt('76561197960265728') + BigInt(e.accountid_other)).toString(),
other_steam_id64: convertSteamID32To64(e.accountid_other),
} as OfferStatus;
}

Expand Down Expand Up @@ -266,6 +277,51 @@ async function getSentAndReceivedTradeOffersFromAPI(): Promise<{
};
}

export async function getTradeOffersWithDescriptionFromAPI(steam_id?: string): Promise<{
received: TradeOffersAPIOffer[];
sent: TradeOffersAPIOffer[];
descriptions: rgDescription[];
steam_id?: string | null;
}> {
// check if permissions are granted
const steamPoweredPermissions = await HasPermissions.handleRequest(
{
permissions: [],
origins: ['https://api.steampowered.com/*'],
},
{}
);
if (!steamPoweredPermissions.granted) {
return {
received: [],
sent: [],
descriptions: [],
steam_id: steam_id,
};
}

const access = await getAccessToken(steam_id);

const resp = await fetch(
`https://api.steampowered.com/IEconService/GetTradeOffers/v1/?access_token=${access.token}&get_received_offers=true&get_sent_offers=true&get_descriptions=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 || [],
sent: data.response?.trade_offers_sent || [],
steam_id: access.steam_id,
descriptions: data.response?.descriptions || [],
};
}

const BANNER_TO_STATE: {[banner: string]: TradeOfferState} = {
accepted: TradeOfferState.Accepted,
counter: TradeOfferState.Countered,
Expand Down
34 changes: 34 additions & 0 deletions src/lib/bridge/handlers/fetch_steam_trades.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {getTradeOffersWithDescriptionFromAPI, TradeOffersAPIOffer} from '../../alarms/trade_offer';
import {rgDescription} from '../../types/steam';
import {CachedHandler} from '../wrappers/cached';
import {SimpleHandler} from './main';
import {RequestType} from './types';

interface FetchSteamTradesRequest {
steam_id?: string;
// Used for caching the request uniquely, does not affect the return results
trade_offer_id?: number;
}

export interface FetchSteamTradesResponse {
received: TradeOffersAPIOffer[];
sent: TradeOffersAPIOffer[];
descriptions: rgDescription[];
steam_id?: string | null;
}

export const FetchSteamTrades = new CachedHandler(
new SimpleHandler<FetchSteamTradesRequest, FetchSteamTradesResponse>(
RequestType.FETCH_STEAM_TRADES,
async (req) => {
const resp = await getTradeOffersWithDescriptionFromAPI(req.steam_id);
if (!resp) {
throw new Error('Error fetching Steam trade offers from API');
}

return resp;
}
),
1,
10 * 60 * 1000
);
2 changes: 2 additions & 0 deletions src/lib/bridge/handlers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {PingTradeStatus} from './ping_trade_status';
import {PingStatus} from './ping_status';
import {FetchOwnInventory} from './fetch_own_inventory';
import {CancelTradeOffer} from './cancel_trade_offer';
import {FetchSteamTrades} from './fetch_steam_trades';

export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
[RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage,
Expand All @@ -48,4 +49,5 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler<any, any>} = {
[RequestType.PING_STATUS]: PingStatus,
[RequestType.FETCH_OWN_INVENTORY]: FetchOwnInventory,
[RequestType.CANCEL_TRADE_OFFER]: CancelTradeOffer,
[RequestType.FETCH_STEAM_TRADES]: FetchSteamTrades,
};
1 change: 1 addition & 0 deletions src/lib/bridge/handlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export enum RequestType {
PING_STATUS = 20,
FETCH_OWN_INVENTORY = 21,
CANCEL_TRADE_OFFER = 22,
FETCH_STEAM_TRADES = 23,
}
25 changes: 25 additions & 0 deletions src/lib/components/trade_offers/trade_offer_holder_metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {CustomElement, InjectAppend, InjectionMode} from '../injectors';
import {ItemHolderMetadata} from '../common/item_holder_metadata';
import {rgAsset} from '../../types/steam';

// Annotates item info (float, seed, etc...) in boxes on the Trade Offers Page
@CustomElement()
// Items in received/sent trade offers
@InjectAppend('.tradeoffer .trade_item', InjectionMode.CONTINUOUS)
export class TradeOfferHolderMetadata extends ItemHolderMetadata {
get assetId(): string | undefined {
return $J(this).parent().attr('data-csfloat-assetid');
}

get asset(): rgAsset | undefined {
const dataDescription = $J(this).parent().attr('data-csfloat-description');

if (!dataDescription) return undefined;

return JSON.parse(dataDescription) as rgAsset;
}

get ownerSteamId(): string | undefined {
return $J(this).parent().attr('data-csfloat-owner-steamid');
}
}
90 changes: 90 additions & 0 deletions src/lib/page_scripts/trade_offers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,105 @@
import {init} from './utils';
import '../components/trade_offers/better_tracking';
import '../components/trade_offers/trade_offer_holder_metadata';
import {inPageContext} from '../utils/snips';
import {ClientSend} from '../bridge/client';
import {PingSetupExtension} from '../bridge/handlers/ping_setup_extension';
import {PingExtensionStatus} from '../bridge/handlers/ping_extension_status';
import {FetchSteamTrades, FetchSteamTradesResponse} from '../bridge/handlers/fetch_steam_trades';
import {convertSteamID32To64, getUserSteamID} from '../utils/userinfo';

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

function main() {}

/**
* Gets the trade offers from the local storage or fetches them from the API.
* Local storage serves as a cache here.
* @param steam_id the steam id of logged in user
* @returns the trade offers
*/
function fetchTradeOffers(steam_id: string): Promise<FetchSteamTradesResponse> | undefined {
const latestTradeIDFromPage = document.querySelector('.tradeoffer')?.id.split('_')[1];
const trade_offer_id = latestTradeIDFromPage ? Number.parseInt(latestTradeIDFromPage) : undefined;

if (!trade_offer_id) {
return;
}

return ClientSend(FetchSteamTrades, {steam_id, trade_offer_id});
}

/**
* Fetches the api data for trade offers and stores relevant data in the DOM to be used by Lit components.
*/
async function annotateTradeOfferItemElements() {
const steam_id = getUserSteamID();

if (!steam_id) {
console.error('Failed to get steam_id', steam_id);
return;
}

const steamTrades = await fetchTradeOffers(steam_id);

if (!steamTrades) {
return;
}

const tradeOfferElements = document.querySelectorAll('.tradeoffer');

for (const tradeOfferElement of tradeOfferElements) {
const tradeOfferID = tradeOfferElement.id.split('_')[1];
const tradeItemElements = tradeOfferElement.querySelectorAll('.trade_item');
const tradeOffer =
steamTrades.sent.find((t) => t.tradeofferid === tradeOfferID) ??
steamTrades.received.find((t) => t.tradeofferid === tradeOfferID);
if (!tradeOffer) {
continue;
}

for (const tradeItemElement of tradeItemElements) {
// Format: classinfo/{appid}/{classid}/{instanceid}
// Example: data-economy-item="classinfo/730/310777185/302028390"
const economyItemParts = tradeItemElement.getAttribute('data-economy-item')?.split('/');
const classId = economyItemParts?.[2];
const instanceId = economyItemParts?.[3];

if (!classId || !instanceId) {
continue;
}

const description = steamTrades.descriptions.find(
(d) => d.classid === classId && d.instanceid === instanceId
);
if (description) {
tradeItemElement.setAttribute('data-csfloat-description', JSON.stringify(description));
}

let isOwnItem = true;
let apiItem = tradeOffer?.items_to_give?.find((a) => a.classid === classId && a.instanceid === instanceId);
if (!apiItem) {
isOwnItem = false;
apiItem = tradeOffer?.items_to_receive?.find(
(a) => a.classid === classId && a.instanceid === instanceId
);
}

const ownerId = isOwnItem ? steam_id : convertSteamID32To64(tradeOffer.accountid_other);

if (ownerId) {
tradeItemElement.setAttribute('data-csfloat-owner-steamid', ownerId);
}
if (apiItem?.assetid) {
tradeItemElement.setAttribute('data-csfloat-assetid', apiItem.assetid);
}
}
}
}

if (!inPageContext()) {
annotateTradeOfferItemElements();

const refresh = setInterval(() => {
const widget = document.getElementsByTagName('csfloat-better-tracking-widget');
if (!widget || widget.length === 0) {
Expand Down
35 changes: 35 additions & 0 deletions src/lib/utils/userinfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
interface UserInfo {
account_name: string;
accountid: number;
country_code: string;
is_limited: boolean;
is_partner_member: boolean;
is_support: boolean;
logged_in: boolean;
steamid: string;
}

export function getUserInfo() {
const configUserInfo = document.getElementById('application_config')?.dataset?.userinfo;
if (!configUserInfo) {
return null;
}
return JSON.parse(configUserInfo) as UserInfo;
}

export function getUserSteamID() {
const userInfo = getUserInfo();
if (!userInfo?.logged_in) {
return null;
}
return userInfo.steamid;
}

/**
* Converts a SteamID32 to a SteamID64
* @param steamID32 number
* @returns SteamID64
*/
export function convertSteamID32To64(steamID32: number) {
return (BigInt('76561197960265728') + BigInt(steamID32)).toString();
}
Loading