diff --git a/src/environment.dev.ts b/src/environment.dev.ts index e35ca1fd..a0677260 100644 --- a/src/environment.dev.ts +++ b/src/environment.dev.ts @@ -5,4 +5,5 @@ export const environment = { ws: 'wss://notary.csfloat.com/proxy', loggingLevel: 'Error', }, + reverse_watch_base_api_url: 'http://localhost:3434/api', }; diff --git a/src/environment.ts b/src/environment.ts index 77df1bbd..02f83b4e 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -5,4 +5,5 @@ export const environment = { ws: 'wss://notary.csfloat.com/proxy', loggingLevel: 'Warn', }, + reverse_watch_base_api_url: 'https://reverse.watch/api', }; diff --git a/src/lib/bridge/handlers/fetch_reversal_status.ts b/src/lib/bridge/handlers/fetch_reversal_status.ts new file mode 100644 index 00000000..c9f24bbe --- /dev/null +++ b/src/lib/bridge/handlers/fetch_reversal_status.ts @@ -0,0 +1,31 @@ +import {RequestType} from './types'; +import {SimpleHandler} from './main'; +import {environment} from '../../../environment'; + +export interface FetchReversalStatusRequest { + steam_id64: string; +} + +export interface FetchReversalStatusResponse { + steam_id: string; + has_reversed: boolean; + last_reversal_timestamp?: number; +} + +export interface FetchReversalStatusError { + code: string; + message: string; + details?: string; +} + +export const FetchReversalStatus = new SimpleHandler( + RequestType.FETCH_REVERSAL_STATUS, + async (req) => { + const resp = await fetch(`${environment.reverse_watch_base_api_url}/v1/users/${req.steam_id64}`); + const data = (await resp.json()) as FetchReversalStatusResponse | FetchReversalStatusError; + if (!resp.ok) { + throw Error((data as FetchReversalStatusError).message ?? 'unknown error'); + } + return data as FetchReversalStatusResponse; + } +); diff --git a/src/lib/bridge/handlers/handlers.ts b/src/lib/bridge/handlers/handlers.ts index cd6181bd..1b679ac0 100644 --- a/src/lib/bridge/handlers/handlers.ts +++ b/src/lib/bridge/handlers/handlers.ts @@ -37,6 +37,7 @@ import {NotaryProve} from './notary_prove'; import {FetchNotaryMeta} from './fetch_notary_meta'; import {FetchNotaryToken} from './fetch_notary_token'; import {FetchSteamPoweredInventory} from './fetch_steam_inventory'; +import {FetchReversalStatus} from './fetch_reversal_status'; export const HANDLERS_MAP: {[key in RequestType]: RequestHandler} = { [RequestType.EXECUTE_SCRIPT_ON_PAGE]: ExecuteScriptOnPage, @@ -76,4 +77,5 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler} = { [RequestType.FETCH_NOTARY_META]: FetchNotaryMeta, [RequestType.FETCH_NOTARY_TOKEN]: FetchNotaryToken, [RequestType.FETCH_STEAM_POWERED_INVENTORY]: FetchSteamPoweredInventory, + [RequestType.FETCH_REVERSAL_STATUS]: FetchReversalStatus, }; diff --git a/src/lib/bridge/handlers/types.ts b/src/lib/bridge/handlers/types.ts index 70d0a2d5..6c9285b2 100644 --- a/src/lib/bridge/handlers/types.ts +++ b/src/lib/bridge/handlers/types.ts @@ -36,4 +36,5 @@ export enum RequestType { FETCH_NOTARY_META = 34, FETCH_NOTARY_TOKEN = 35, FETCH_STEAM_POWERED_INVENTORY = 36, + FETCH_REVERSAL_STATUS = 37, } diff --git a/src/lib/components/profile/reversal_status.ts b/src/lib/components/profile/reversal_status.ts new file mode 100644 index 00000000..b1020894 --- /dev/null +++ b/src/lib/components/profile/reversal_status.ts @@ -0,0 +1,132 @@ +import {CustomElement, InjectAfter, InjectionMode} from '../injectors'; +import {FloatElement} from '../custom'; +import {css, html} from 'lit'; +import {state} from 'lit/decorators.js'; +import {FetchReversalStatusResponse} from '../../bridge/handlers/fetch_reversal_status'; +import {gReversalFetcher} from '../../services/reversal_fetcher'; +import {defined} from '../../utils/checkers'; + +@CustomElement() +@InjectAfter( + '.profile_in_game.persona + .profile_ban_status, .profile_in_game.persona:not(:has(+ .profile_ban_status))', + InjectionMode.ONCE +) +export class ReversalStatus extends FloatElement { + @state() + reversalStatus: FetchReversalStatusResponse | undefined = undefined; + + static styles = [ + ...FloatElement.styles, + css` + .container { + display: flex; + align-items: center; + gap: 6px; + color: #de6667; + margin-bottom: 10px; + + .warning { + display: inline; + + .info-link-container { + color: #828282; + + .info-link { + text-decoration: none; + color: #ebebeb; + + &:hover { + color: #66c0f4; + } + } + } + + .powered-by-container { + font-size: 12px; + color: #828282; + + .powered-by-link { + text-decoration: none; + color: #ebebeb; + + &:hover { + color: #66c0f4; + } + } + } + } + } + `, + ]; + + get show(): boolean { + return !!this.reversalStatus?.has_reversed; + } + + get daysSinceLastReversal(): number | null { + if (!this.reversalStatus?.last_reversal_timestamp) { + return null; + } + + const now = Date.now(); + const timeSince = now - this.reversalStatus.last_reversal_timestamp; + return Math.floor(timeSince / (24 * 60 * 60 * 1000)); + } + + getSteamId(): string | undefined { + if (defined(typeof g_rgProfileData) && g_rgProfileData) { + return g_rgProfileData.steamid; + } + + const match = window.location.pathname.match(/^\/profiles\/(\d+)/); + if (match) { + return match[1]; + } + } + + async connectedCallback() { + super.connectedCallback(); + + try { + const steamId = this.getSteamId(); + if (!steamId) { + console.error('failed to get steam id'); + return; + } + + this.reversalStatus = await gReversalFetcher.fetch({steam_id64: steamId}); + } catch (e) { + console.error('failed to fetch reversal status', e); + } + } + + protected render() { + if (!this.show) { + return html``; + } + + const daysSince = this.daysSinceLastReversal ?? 0; + const message = `${daysSince} day(s) since last trade reversal`; + return html` +
+
+ ${message} + + | + + Info + + + (powered by reverse.watch) +
+
+ `; + } +} diff --git a/src/lib/page_scripts/profile.ts b/src/lib/page_scripts/profile.ts index 3f2b8271..2dbbeb74 100644 --- a/src/lib/page_scripts/profile.ts +++ b/src/lib/page_scripts/profile.ts @@ -1,5 +1,6 @@ import {init} from './utils'; import '../components/profile/comment_warning'; +import '../components/profile/reversal_status'; init('src/lib/page_scripts/profile.js', main); diff --git a/src/lib/services/reversal_fetcher.ts b/src/lib/services/reversal_fetcher.ts new file mode 100644 index 00000000..9b333a81 --- /dev/null +++ b/src/lib/services/reversal_fetcher.ts @@ -0,0 +1,33 @@ +import {ClientSend} from '../bridge/client'; +import { + FetchReversalStatus, + FetchReversalStatusRequest, + FetchReversalStatusResponse, +} from '../bridge/handlers/fetch_reversal_status'; +import {GenericJob, TTLCachedQueue} from '../utils/queue'; + +class ReversalFetcher extends TTLCachedQueue { + constructor(maxConcurrency: number, ttlMs: number) { + super(maxConcurrency, ttlMs); + } + + fetch(req: FetchReversalStatusRequest): Promise { + return this.add(new GenericJob(req)); + } + + protected async process(req: FetchReversalStatusRequest): Promise { + try { + return await ClientSend(FetchReversalStatus, req); + } catch (e) { + console.error('failed to fetch reversal status', e); + // Stub out to prevent future calls + return { + steam_id: '', + has_reversed: false, + last_reversal_timestamp: undefined, + } as FetchReversalStatusResponse; + } + } +} + +export const gReversalFetcher = new ReversalFetcher(1, 30 * 60 * 1000 /* 30 minutes */); diff --git a/src/lib/types/steam.d.ts b/src/lib/types/steam.d.ts index 39562fb8..1aa7defc 100644 --- a/src/lib/types/steam.d.ts +++ b/src/lib/types/steam.d.ts @@ -250,12 +250,21 @@ export type SteamAssets = { }; }; +// g_rgProfileData +export interface ProfileData { + url: string; + steamid: string; + personaname: string; + summary?: string; +} + // Declares globals available in the Steam Page Context declare global { const $J: typeof $; const g_rgListingInfo: {[listingId: string]: ListingData}; const g_rgWalletInfo: WalletInfo | undefined; // Not populated when user is signed-out const g_rgAssets: SteamAssets; + const g_rgProfileData: ProfileData | undefined; // Only populated on Steam profile pages const g_ActiveInventory: CAppwideInventory | CInventory | undefined; // Only populated on Steam inventory pages const g_steamID: string; const g_oSearchResults: CAjaxPagingControls;