Skip to content
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

Display Metadata on /tradeoffers-pages #283

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
95 changes: 95 additions & 0 deletions src/lib/alarms/trade_offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ 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';

export interface ExtendedOfferStatus {
offer_id: string;
state: TradeOfferState;
given_asset_ids?: ExtendedSingleOffer[];
received_asset_ids?: ExtendedSingleOffer[];
time_created?: number;
time_updated?: number;
other_steam_id64?: string;
}

export interface ExtendedSingleOffer {
assetid: string;
classid: string;
instanceid: string;
}

export async function pingSentTradeOffers(pendingTrades: Trade[]) {
const {offers, type} = await getSentTradeOffers();
Expand Down Expand Up @@ -191,6 +209,13 @@ 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 {
Expand All @@ -207,6 +232,7 @@ interface TradeOffersAPIResponse {
response: {
trade_offers_sent: TradeOffersAPIOffer[];
trade_offers_received: TradeOffersAPIOffer[];
descriptions?: rgDescription[];
};
}

Expand All @@ -222,6 +248,30 @@ function offerStateMapper(e: TradeOffersAPIOffer): OfferStatus {
} as OfferStatus;
}

function extendedOfferStateMapper(e: TradeOffersAPIOffer) {
return {
offer_id: e.tradeofferid,
state: e.trade_offer_state,
given_asset_ids: (e.items_to_give || []).map((e) => {
return {
assetid: e.assetid,
classid: e.classid,
instanceid: e.instanceid,
} as ExtendedSingleOffer;
}),
received_asset_ids: (e.items_to_receive || []).map((e) => {
return {
assetid: e.assetid,
classid: e.classid,
instanceid: e.instanceid,
} as ExtendedSingleOffer;
}),
time_created: e.time_created,
time_updated: e.time_updated,
other_steam_id64: (BigInt('76561197960265728') + BigInt(e.accountid_other)).toString(),
};
}

async function getSentTradeOffersFromAPI(): Promise<OfferStatus[]> {
const access = await getAccessToken();

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

export async function getTradeOffersWithDescriptionFromAPI(): Promise<{
received: ExtendedOfferStatus[];
sent: ExtendedOfferStatus[];
descriptions: rgDescription[];
steam_id?: string | null;
}> {
const access = await getAccessToken();

// 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: access.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 || []).map(extendedOfferStateMapper),
sent: (data.response?.trade_offers_sent || []).map(extendedOfferStateMapper),
steam_id: access.steam_id,
descriptions: data.response?.descriptions || [],
};
}

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

interface FetchSteamTradesRequest {}

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

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

return resp;
}
);
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,
}
32 changes: 11 additions & 21 deletions src/lib/components/trade_offers/better_tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,19 @@ export class BetterTrackingWidget extends FloatElement {
async connectedCallback() {
super.connectedCallback();

try {
// Used for api.steampowered.com requests, all tokens stay on the users' device
const hasPermissions = await ClientSend(HasPermissions, {
permissions: ['alarms'],
origins: ['*://*.steampowered.com/*'],
});
// Used for api.steampowered.com requests, all tokens stay on the users' device
const hasPermissions = await ClientSend(HasPermissions, {
permissions: ['alarms'],
origins: ['*://*.steampowered.com/*'],
});

if (hasPermissions.granted) {
// In case they switched accounts on CSFloat or Steam or initial ping was lost, send redundant pings
ClientSend(PingSetupExtension, {});
return;
}

const trades = await ClientSend(FetchPendingTrades, {state: 'queued,pending,verified', limit: 1});
if (trades.count === 0) {
// They aren't actively using CSFloat Market, no need to show this
return;
}

this.show = true;
} catch (e) {
console.info('user is not logged into CSFloat');
if (hasPermissions.granted) {
// In case they switched accounts on CSFloat or Steam or initial ping was lost, send redundant pings
ClientSend(PingSetupExtension, {});
return;
}

this.show = true;
}

render() {
Expand Down
21 changes: 21 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,21 @@
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-assetid');
}

get asset(): rgAsset | undefined {
return JSON.parse($J(this).parent().attr('data-description') ?? '{}') as rgAsset;
}

get ownerSteamId(): string | undefined {
return $J(this).parent().attr('data-owner-steamid');
}
}
81 changes: 81 additions & 0 deletions src/lib/page_scripts/trade_offers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,96 @@
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';

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 isSentPage if the current page is the sent trade offers page
* @returns the trade offers
*/
async function fetchTradeOffers(isSentPage: boolean) {
const g_steamTrades = JSON.parse(localStorage.getItem('g_steamTrades') || '{}') as FetchSteamTradesResponse;
let refetchRequired = true;
if (g_steamTrades.sent || g_steamTrades.received) {
const latestTradeId = Number.parseInt(g_steamTrades[isSentPage ? 'sent' : 'received']?.[0].offer_id);
const latestTradeElement = Number.parseInt(document.querySelector('.tradeoffer')?.id.split('_')[1] ?? '0');

refetchRequired = Number.isNaN(latestTradeId) || latestTradeId !== latestTradeElement;
}

if (!refetchRequired) {
return g_steamTrades;
}

const steamTrades = await ClientSend(FetchSteamTrades, {});

localStorage.setItem('g_steamTrades', JSON.stringify(steamTrades));
return steamTrades;
}

/**
* Fetches the api data for trade offers and stores relevant data in the DOM to be used by Lit components.
*/
async function getAndStoreTradeOffers() {
const isSentPage = location.pathname.includes('sent');

const steamTrades = await fetchTradeOffers(isSentPage);

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

for (const tradeOffer of tradeOffers) {
const tradeId = tradeOffer.id.split('_')[1];
const tradeItems = tradeOffer.querySelectorAll('.trade_item');
const trade = isSentPage
? steamTrades.sent.find((t) => t.offer_id === tradeId)
: steamTrades.received.find((t) => t.offer_id === tradeId);

for (const tradeItem of tradeItems) {
const economyItemParts = tradeItem.getAttribute('data-economy-item')?.split('/');
const classId = economyItemParts?.[2];
const instanceId = economyItemParts?.[3];

if (!instanceId) {
continue;
}

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

let isOwnItem = true;
let apiItem = trade?.given_asset_ids?.find((a) => a.classid === classId && a.instanceid === instanceId);
if (!apiItem) {
isOwnItem = false;
apiItem = trade?.received_asset_ids?.find((a) => a.classid === classId && a.instanceid === instanceId);
}
const ownerId = isOwnItem
? JSON.parse(document.getElementById('application_config')?.dataset?.userinfo || '{}').steamid
: trade?.other_steam_id64;

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

if (!inPageContext()) {
getAndStoreTradeOffers();

const refresh = setInterval(() => {
const widget = document.getElementsByTagName('csfloat-better-tracking-widget');
if (!widget || widget.length === 0) {
Expand Down
Loading