diff --git a/app/controllers/api.controller.ts b/app/controllers/api.controller.ts index 11da2ae..3c1e95d 100644 --- a/app/controllers/api.controller.ts +++ b/app/controllers/api.controller.ts @@ -168,6 +168,15 @@ router.get('/get_dbid_from_discord', async (req: Request, res: Response, next) = } }); +router.get('/celestial-market', async (req: Request, res: Response, next) => { + try { + let apiResult = await DataCoreAPI.getCelestial(); + res.status(apiResult.Status).send(apiResult.Body); + } catch (e) { + next(e); + } +}); + router.get('/telemetry', async (req: Request, res: Response, next) => { if (!req.query || !req.query.type) { res.status(400).send('Whaat?'); diff --git a/app/logic/api.ts b/app/logic/api.ts index 307080c..71189bb 100644 --- a/app/logic/api.ts +++ b/app/logic/api.ts @@ -11,6 +11,7 @@ import { PlayerData } from '../datacore/player'; import { Profile } from '../models/Profile'; import { voyageRawByDays } from './voyage_stats'; +import { CelestialAPI } from './celestial'; require('dotenv').config(); @@ -482,6 +483,20 @@ export class ApiClass { } } + async getCelestial() { + let result = await CelestialAPI.getCelestialMarket(this._stt_token); + if (!result){ + return { + Status: 500, + Body: JSON.stringify({ code: 500, error: "Could not read celestial market."}) + } + } + return { + Status: 200, + Body: result + } + } + async getVoyages(crew?: string[], days?: number, opAnd?: boolean) { days ??= 7; if (days <= 0) days = 1; @@ -537,8 +552,6 @@ export class ApiClass { } async sqliteGetPlayerData(dbid?: number, hash?: string): Promise { - - Logger.info('Get player data', { dbid }); let player: Profile | null = null; let playerData: PlayerData | null = null; diff --git a/app/logic/celestial.ts b/app/logic/celestial.ts new file mode 100644 index 0000000..e703a62 --- /dev/null +++ b/app/logic/celestial.ts @@ -0,0 +1,128 @@ +import fs from 'fs'; + +require('dotenv').config(); + +const CLIENT_API_VERSION = 24; +export type MarketAggregation = { + [key: string]: MarketListing; +}; + +export interface CelestialMarketData { + action: 'ephemeral'; + root: { + aggregation: MarketAggregation; + } +} + +export interface MarketListing { + sold_last_day: number; + buy_count: number; + sell_count: number; + high: number; + low: number; + wishlisted?: number; + last_price: number; + count_at_low: number; +} + +export interface KeystoneMarketEntry extends MarketListing { + date: string; +} + +class CelestialMarket { + readonly STATS_PATH: string; + readonly MARKET_FILE: string; + + lastRefresh = undefined as Date | undefined; + currentData: boolean; + + public get isCurrent(): boolean { + if (!this.lastRefresh) return false; + let now = new Date(); + return (now.getHours() - this.lastRefresh.getHours() <= 1); + } + + constructor() { + this.STATS_PATH = `${process.env.PROFILE_DATA_PATH}/stats/`; + this.MARKET_FILE = `${this.STATS_PATH}keystone_market_data.json` + if (!fs.existsSync(this.STATS_PATH)) { + fs.mkdirSync(this.STATS_PATH); + } + + this.currentData = fs.existsSync(this.MARKET_FILE); + + if (this.currentData) { + this.lastRefresh = fs.statSync(this.MARKET_FILE).mtime; + } + } + + private async remoteFetchCelestialMarket(access_token: string) { + try { + console.log("Loading Celestial Market ..."); + + let response = await fetch(`https://app.startrektimelines.com/marketplace/aggregate_order_details?access_token=${access_token}&client_api=${CLIENT_API_VERSION}`); + + if (!response.ok) return null; + let market: CelestialMarketData = await response.json(); + + if (market.root?.aggregation) { + let ids = Object.keys(market.root.aggregation); + console.log(`Market loaded ${ids.length} items listed ...`); + + for (let id of ids) { + delete market.root.aggregation[id].wishlisted; + } + + return market; + } else { + return undefined; + } + } catch (err) { + //console.error(err); + } + return undefined; + } + + public async getCelestialMarket(access_token: string): Promise { + let now = new Date(); + if (!this.currentData || !this.lastRefresh || !this.isCurrent) { + let result = await this.refreshCelestialMarket(access_token); + if (!result && fs.existsSync(this.MARKET_FILE)) { + this.lastRefresh = fs.statSync(this.MARKET_FILE).mtime; + return JSON.parse(fs.readFileSync(this.MARKET_FILE, 'utf-8')) as MarketAggregation; + } + return result; + } + else { + return JSON.parse(fs.readFileSync(this.MARKET_FILE, 'utf-8')) as MarketAggregation; + } + } + + public async refreshCelestialMarket(access_token: string) { + if (!fs.existsSync(this.STATS_PATH)) { + fs.mkdirSync(this.STATS_PATH); + } + + let market = await this.remoteFetchCelestialMarket(access_token); + + if (market) { + let keystonemarket = market.root.aggregation; + let market_file = this.MARKET_FILE; + fs.writeFileSync(market_file, JSON.stringify(keystonemarket)); + this.lastRefresh = new Date(); + this.currentData = true; + return keystonemarket; + } + else if (fs.existsSync(this.MARKET_FILE)) { + this.lastRefresh = fs.statSync(this.MARKET_FILE).mtime; + } + else { + this.lastRefresh = undefined; + } + this.currentData = false; + return null; + } +} + + +export let CelestialAPI = new CelestialMarket(); \ No newline at end of file