Skip to content

Commit

Permalink
Merge pull request #204 from csfloat/feature/trade-pings
Browse files Browse the repository at this point in the history
Trade Pings
  • Loading branch information
Step7750 authored Apr 12, 2024
2 parents e5a8255 + be49c5f commit 12a17b4
Show file tree
Hide file tree
Showing 26 changed files with 1,199 additions and 110 deletions.
3 changes: 2 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@
"service_worker": "src/background.js",
"type": "module"
},
"permissions": ["storage", "scripting"],
"permissions": ["storage", "scripting", "alarms"],
"host_permissions": [
"*://*.steamcommunity.com/market/listings/730/*",
"*://*.steamcommunity.com/id/*/inventory*",
"*://*.steamcommunity.com/id/*/tradehistory*",
"*://*.steamcommunity.com/profiles/*/inventory*"
],
"optional_host_permissions": ["*://*.steampowered.com/*"],
"externally_connectable": {
"matches": ["*://*.steamcommunity.com/*"]
},
Expand Down
431 changes: 393 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@types/lodash": "^4.14.195",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"cheerio": "^1.0.0-rc.12",
"copy-webpack-plugin": "^11.0.0",
"csgo-fade-percentage-calculator": "^1.1.2",
"css-loader": "^6.7.1",
Expand All @@ -40,6 +41,7 @@
"eslint-config-prettier": "^8.5.0",
"fast-json-stable-stringify": "^2.1.0",
"file-loader": "^6.2.0",
"file-replace-loader": "^1.4.0",
"filtrex": "^3.0.0",
"glob": "^8.0.3",
"html-loader": "^4.1.0",
Expand All @@ -56,5 +58,8 @@
"typescript": "^4.7.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"dependencies": {
"@types/cheerio": "^0.22.35"
}
}
30 changes: 30 additions & 0 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Handle} from './lib/bridge/server';
import {InternalResponseBundle} from './lib/bridge/types';
import MessageSender = chrome.runtime.MessageSender;
import {PING_CSFLOAT_TRADE_STATUS_ALARM_NAME, pingTradeStatus} from './lib/alarms/csfloat_trade_pings';

function unifiedHandler(request: any, sender: MessageSender, sendResponse: (response?: any) => void) {
Handle(request, sender)
Expand All @@ -20,7 +21,17 @@ function unifiedHandler(request: any, sender: MessageSender, sendResponse: (resp
});
}

function requestPermissions(permissions: string[], origins: string[], sendResponse: any) {
chrome.permissions.request({permissions, origins}, (granted) => sendResponse(granted));

return true;
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message === 'requestPermissions') {
return requestPermissions(request.permissions, request.origins, sendResponse);
}

unifiedHandler(request, sender, sendResponse);
return true;
});
Expand All @@ -29,3 +40,22 @@ chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) =>
unifiedHandler(request, sender, sendResponse);
return true;
});

chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === PING_CSFLOAT_TRADE_STATUS_ALARM_NAME) {
await pingTradeStatus();
}
});

async function checkAlarmState() {
const alarm = await chrome.alarms.get(PING_CSFLOAT_TRADE_STATUS_ALARM_NAME);

if (!alarm) {
await chrome.alarms.create(PING_CSFLOAT_TRADE_STATUS_ALARM_NAME, {
periodInMinutes: 5,
delayInMinutes: 1,
});
}
}

checkAlarmState();
3 changes: 3 additions & 0 deletions src/environment.dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const environment = {
csfloat_base_api_url: 'http://localhost:8080/api',
};
3 changes: 3 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const environment = {
csfloat_base_api_url: 'https://csfloat.com/api',
};
35 changes: 35 additions & 0 deletions src/lib/alarms/csfloat_trade_pings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Trade} from '../types/float_market';
import {FetchPendingTrades} from '../bridge/handlers/fetch_pending_trades';
import {pingTradeHistory} from './trade_history';
import {pingSentTradeOffers} from './trade_offer';

export const PING_CSFLOAT_TRADE_STATUS_ALARM_NAME = 'ping_csfloat_trade_status_alarm';

export async function pingTradeStatus() {
let pendingTrades: Trade[];
try {
const resp = await FetchPendingTrades.handleRequest({}, {});
pendingTrades = resp.trades;
} catch (e) {
console.error(e);
console.log('cannot fetch pending trades for CSFloat, may not be logged in or CSFloat down');
return;
}

if (pendingTrades.length === 0) {
// No active trades, return early
return;
}

try {
await pingTradeHistory(pendingTrades);
} catch (e) {
console.error('failed to ping trade history', e);
}

try {
await pingSentTradeOffers(pendingTrades);
} catch (e) {
console.error('failed to ping sent trade offer state', e);
}
}
154 changes: 154 additions & 0 deletions src/lib/alarms/trade_history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {Trade} from '../types/float_market';
import {TradeHistoryStatus, TradeHistoryType} from '../bridge/handlers/trade_history_status';
import cheerio from 'cheerio';
import {AppId} from '../types/steam_constants';
import {HasPermissions} from '../bridge/handlers/has_permissions';

export async function pingTradeHistory(pendingTrades: Trade[]) {
const {history, type} = await getTradeHistory();

// premature optimization in case it's 100 trades
const assetsToFind = pendingTrades.reduce((acc, e) => {
acc[e.contract.item.asset_id] = true;
return acc;
}, {} as {[key: string]: boolean});

// We only want to send history that is relevant to verifying trades on CSFloat
const historyForCSFloat = history.filter((e) => {
const received_ids = e.received_assets.map((e) => e.asset_id);
const given_ids = e.given_assets.map((e) => e.asset_id);
return !![...received_ids, ...given_ids].find((e) => {
return assetsToFind[e];
});
});

if (historyForCSFloat.length === 0) {
return;
}

await TradeHistoryStatus.handleRequest({history: historyForCSFloat, type}, {});
}

async function getTradeHistory(): Promise<{history: TradeHistoryStatus[]; type: TradeHistoryType}> {
const resp = await fetch(`https://steamcommunity.com/id/me/tradehistory`, {
credentials: 'include',
// Expect redirect since we're using `me` above
redirect: 'follow',
});

const body = await resp.text();

const hasPermissions = await HasPermissions.handleRequest(
{
permissions: [],
origins: ['*://*.steampowered.com/*'],
},
{}
);

if (hasPermissions.granted) {
const webAPIToken = /data-loyalty_webapi_token="&quot;([a-zA-Z0-9_.-]+)&quot;"/.exec(body);
if (webAPIToken && webAPIToken.length > 1) {
try {
const history = await getTradeHistoryFromAPI(webAPIToken[1]);
if (history.length > 0) {
// Hedge in case this endpoint gets killed, only return if there are results, fallback to HTML parser
return {history, type: TradeHistoryType.API};
}
} catch (e) {
console.error(e);
}
}

// Fallback to HTML parsing
}

if (body.includes('too many requests')) {
throw 'Too many requests';
}

return {history: parseTradeHistoryHTML(body), type: TradeHistoryType.HTML};
}

interface HistoryAsset {
assetid: string;
appid: AppId;
new_assetid: string;
}

interface TradeHistoryAPIResponse {
response: {
trades: {
tradeid: string;
steamid_other: string;
status: number;
assets_given?: HistoryAsset[];
assets_received?: HistoryAsset[];
}[];
};
}

async function getTradeHistoryFromAPI(accessToken: string): Promise<TradeHistoryStatus[]> {
// This only works if they have granted permission for https://api.steampowered.com
const resp = await fetch(
`https://api.steampowered.com/IEconService/GetTradeHistory/v1/?access_token=${accessToken}&max_trades=50`,
{
credentials: 'include',
}
);

if (resp.status !== 200) {
throw new Error('invalid status');
}

const data = (await resp.json()) as TradeHistoryAPIResponse;
return data.response.trades
.map((e) => {
return {
other_party_url: `https://steamcommunity.com/profiles/${e.steamid_other}`,
received_assets: (e.assets_received || [])
.filter((e) => e.appid === AppId.CSGO)
.map((e) => {
return {asset_id: e.assetid, new_asset_id: e.new_assetid};
}),
given_assets: (e.assets_given || [])
.filter((e) => e.appid === AppId.CSGO)
.map((e) => {
return {asset_id: e.assetid, new_asset_id: e.new_assetid};
}),
} as TradeHistoryStatus;
})
.filter((e) => {
// Remove non-CS related assets
return e.received_assets.length > 0 || e.given_assets.length > 0;
});
}

function parseTradeHistoryHTML(body: string): TradeHistoryStatus[] {
const doc = cheerio.load(body);

const statuses = doc('.tradehistoryrow .tradehistory_event_description a')
.toArray()
.map((row) => {
return {
other_party_url: doc(row).attr('href'),
received_assets: [],
given_assets: [],
} as TradeHistoryStatus;
});

const matches = body.matchAll(
/HistoryPageCreateItemHover\( 'trade(\d+)_(received|given)item\d+', 730, '2', '(\d+)', '1' \);/g
);
for (const match of matches) {
const [text, index, type, assetId] = match;
const tradeIndex = parseInt(index);
if (type === 'received') {
statuses[tradeIndex].received_assets.push({asset_id: assetId});
} else if (type === 'given') {
statuses[tradeIndex].given_assets.push({asset_id: assetId});
}
}

return statuses;
}
Loading

0 comments on commit 12a17b4

Please sign in to comment.