Files
star-kitten/packages/eve/src/third-party/janice.ts

408 lines
11 KiB
TypeScript

/**
* Janice API integration for EVE Online market appraisals and pricing.
* This module provides interfaces and functions to interact with the Janice API.
*/
const BASE_URL = 'https://janice.e-351.com/api/rest/v2';
/**
* Represents an appraisal from the Janice API.
*/
export interface Appraisal {
/** Unique identifier for the appraisal */
id: number;
/** Creation timestamp */
created: string;
/** Expiration timestamp */
expires: string;
/** Dataset timestamp */
datasetTime: string;
/** Appraisal code */
code: string;
/** Designation type */
designation: AppraisalDesignation;
/** Pricing strategy */
pricing: AppraisalPricing;
/** Pricing variant */
pricingVariant: AppraisalPricingVariant;
/** Price percentage */
pricePercentage: number;
/** Whether the appraisal is compactized */
isCompactized: boolean;
/** Failure messages */
failures: string;
/** Market information */
market: PricerMarket;
/** Total volume */
totalVolume: number;
/** Total packaged volume */
totalPackagedVolume: number;
/** Effective prices */
effectivePrices: AppraisalValues;
/** Immediate prices */
immediatePrices: AppraisalValues;
/** Top 5 average prices */
top5AveragePrices: AppraisalValues;
/** List of items in the appraisal */
items: AppraisalItem[];
}
/**
* Price values for an appraisal.
*/
export interface AppraisalValues {
/** Total buy price */
totalBuyPrice: number;
/** Total split price */
totalSplitPrice: number;
/** Total sell price */
totalSellPrice: number;
}
/**
* Represents an item in an appraisal.
*/
export interface AppraisalItem {
/** Item ID */
id: number;
/** Amount of the item */
amount: number;
/** Number of buy orders */
buyOrderCount: number;
/** Buy volume */
buyVolume: number;
/** Number of sell orders */
sellOrderCount: number;
/** Sell volume */
sellVolume: number;
/** Effective prices for the item */
effectivePrices: AppraisalItemValues;
/** Immediate prices for the item */
immediatePrices: AppraisalItemValues;
/** Top 5 average prices for the item */
top5AveragePrices: AppraisalItemValues;
/** Total volume */
totalVolume: number;
/** Total packaged volume */
totalPackagedVolume: number;
/** Item type information */
itemType: ItemType;
}
/**
* Represents a pricer item from the API.
*/
export interface PricerItem {
/** Date of the price data */
date: string;
/** Market information */
market: PricerMarket;
/** Number of buy orders */
buyOrderCount: number;
/** Buy volume */
buyVolume: number;
/** Number of sell orders */
sellOrderCount: number;
/** Sell volume */
sellVolume: number;
/** Immediate prices */
immediatePrices: PricerItemValues;
/** Top 5 average prices */
top5AveragePrices: PricerItemValues;
/** Item type information */
itemType: ItemType;
}
/**
* Price values for a pricer item.
*/
export interface PricerItemValues {
/** Buy price */
buyPrice: number;
/** Split price */
splitPrice: number;
/** Sell price */
sellPrice: number;
/** 5-day median buy price */
buyPrice5DayMedian: number;
/** 5-day median split price */
splitPrice5DayMedian: number;
/** 5-day median sell price */
sellPrice5DayMedian: number;
/** 30-day median buy price */
buyPrice30DayMedian: number;
/** 30-day median split price */
splitPrice30DayMedian: number;
/** 30-day median sell price */
sellPrice30DayMedian: number;
}
/**
* Extended price values for appraisal items.
*/
export interface AppraisalItemValues extends PricerItemValues {
/** Total buy price */
buyPriceTotal: number;
/** Total split price */
splitPriceTotal: number;
/** Total sell price */
sellPriceTotal: number;
}
/**
* Represents an item type.
*/
export interface ItemType {
/** EVE item ID */
eid: number;
/** Item name (optional) */
name?: string;
/** Item volume */
volume: number;
/** Packaged volume */
packagedVolume: number;
}
/**
* Enumeration for appraisal designations.
*/
export enum AppraisalDesignation {
Appraisal = 'appraisal',
WantToBuy = 'wtb',
WantToSell = 'wts',
}
/**
* Enumeration for appraisal pricing strategies.
*/
export enum AppraisalPricing {
Buy = 'buy',
Split = 'split',
Sell = 'sell',
Purchase = 'purchase',
}
/**
* Enumeration for appraisal pricing variants.
*/
export enum AppraisalPricingVariant {
Immediate = 'immediate',
Top5Percent = 'top5percent',
}
/**
* Represents a market in the pricer system.
*/
export interface PricerMarket {
/** Market ID */
id: number;
/** Market name */
name: string;
}
/**
* Predefined list of available markets.
*/
export const markets: PricerMarket[] = [
{ id: 2, name: 'Jita 4-4' },
{ id: 3, name: 'R1O-GN' },
{ id: 6, name: 'NPC' },
{ id: 114, name: 'MJ-5F9' },
{ id: 115, name: 'Amarr' },
{ id: 116, name: 'Rens' },
{ id: 117, name: 'Dodixie' },
{ id: 118, name: 'Hek' },
] as const;
/**
* Simple cache for API responses to improve performance.
*/
const cache = new Map<string, { data: any; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
/**
* Clears the internal cache. Useful for testing.
*/
export function clearCache(): void {
cache.clear();
}
/**
* Validates if a value is a positive number.
*/
export function isPositiveNumber(value: any): value is number {
return typeof value === 'number' && value > 0 && isFinite(value);
}
/**
* Validates if a value is a non-empty string.
*/
export function isNonEmptyString(value: any): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
/**
* Fetches price data for a single item type.
* @param type_id - The EVE item type ID
* @param market_id - The market ID (default: 2 for Jita)
* @returns Promise resolving to PricerItem
* @throws Error if API call fails or validation fails
*/
export const fetchPrice = async (type_id: number, market_id: number = 2): Promise<PricerItem> => {
if (!isPositiveNumber(type_id)) {
throw new Error('Invalid type_id: must be a positive number');
}
if (!isPositiveNumber(market_id)) {
throw new Error('Invalid market_id: must be a positive number');
}
const cacheKey = `price_${type_id}_${market_id}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
try {
const response = await fetch(`${BASE_URL}/pricer/${type_id}?market=${market_id}`, {
method: 'GET',
headers: {
'X-ApiKey': process.env.JANICE_KEY || '',
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
cache.set(cacheKey, { data, timestamp: Date.now() });
return data as PricerItem;
} catch (error) {
console.error(`Error fetching price for type_id ${type_id}:`, error);
throw error;
}
};
/**
* Fetches price data for multiple item types.
* @param type_ids - Array of EVE item type IDs
* @param market_id - The market ID (default: 2 for Jita)
* @returns Promise resolving to array of PricerItem
* @throws Error if API call fails or validation fails
*/
export const fetchPrices = async (type_ids: number[], market_id: number = 2): Promise<PricerItem[]> => {
if (!Array.isArray(type_ids) || type_ids.length === 0 || !type_ids.every(isPositiveNumber)) {
throw new Error('Invalid type_ids: must be a non-empty array of positive numbers');
}
if (!isPositiveNumber(market_id)) {
throw new Error('Invalid market_id: must be a positive number');
}
const cacheKey = `prices_${type_ids.sort().join('_')}_${market_id}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
try {
const response = await fetch(`${BASE_URL}/pricer?market=${market_id}`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'X-ApiKey': process.env.JANICE_KEY || '',
'Accept': 'application/json',
},
body: type_ids.join('\n'),
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
cache.set(cacheKey, { data, timestamp: Date.now() });
return data as PricerItem[];
} catch (error) {
console.error(`Error fetching prices for type_ids ${type_ids}:`, error);
throw error;
}
};
/**
* Fetches an appraisal by its code.
* @param code - The appraisal code
* @returns Promise resolving to Appraisal
* @throws Error if API call fails or validation fails
*/
export const fetchAppraisal = async (code: string): Promise<Appraisal> => {
if (!isNonEmptyString(code)) {
throw new Error('Invalid code: must be a non-empty string');
}
const cacheKey = `appraisal_${code}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
try {
const response = await fetch(`${BASE_URL}/appraisal/${code}`, {
method: 'GET',
headers: {
'X-ApiKey': process.env.JANICE_KEY || '',
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
cache.set(cacheKey, { data, timestamp: Date.now() });
return data as Appraisal;
} catch (error) {
console.error(`Error fetching appraisal for code ${code}:`, error);
throw error;
}
};
/**
* Appraises items from text input.
* @param text - The text containing items to appraise
* @param market_id - The market ID (default: 2 for Jita)
* @returns Promise resolving to Appraisal
* @throws Error if API call fails or validation fails
*/
export const appraiseItems = async (text: string, market_id: number = 2, apiKey: string | undefined = process.env.JANICE_KEY): Promise<Appraisal> => {
if (!isNonEmptyString(text)) {
throw new Error('Invalid text: must be a non-empty string');
}
if (!isPositiveNumber(market_id)) {
throw new Error('Invalid market_id: must be a positive number');
}
try {
const response = await fetch(`${BASE_URL}/appraisal?market=${market_id}&persist=true&compactize=true&pricePercentage=1`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'X-ApiKey': apiKey || '',
'Accept': 'application/json',
},
body: text,
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data as Appraisal;
} catch (error) {
console.error('Error appraising items:', error);
throw error;
}
};