break up library, move bots to their own repositories

This commit is contained in:
JB
2026-02-12 21:05:15 -05:00
parent 415aa3dbfe
commit cb39431a11
289 changed files with 1931 additions and 6235 deletions

23
packages/eve/README.md Normal file
View File

@@ -0,0 +1,23 @@
# tsdown-starter
A starter for creating a TypeScript package.
## Development
- Install dependencies:
```bash
npm install
```
- Run the unit tests:
```bash
npm run test
```
- Build the library:
```bash
npm run build
```

View File

@@ -0,0 +1,29 @@
{
"colors": ["red", "blue", "green", "yellow"],
"testText": {
"simple": "Hello World",
"multiline": "Line 1\nLine 2\nLine 3",
"withSpecialChars": "Text with !@#$%^&*()_+-=[]{}|;':\",./<>?",
"empty": "",
"unicode": "Unicode: 🌟 ❤️ 🔥",
"code": "function test() { return 'hello'; }"
},
"expected": {
"red": {
"simple": "```ansi\n\u001b[2;31mHello World\u001b[0m```\n",
"empty": "```ansi\n\u001b[2;31m\u001b[0m```\n"
},
"blue": {
"simple": "```ansi\n\u001b[2;32m\u001b[2;36m\u001b[2;34mHello World\u001b[0m\u001b[2;36m\u001b[0m\u001b[2;32m\u001b[0m```\n",
"empty": "```ansi\n\u001b[2;32m\u001b[2;36m\u001b[2;34m\u001b[0m\u001b[2;36m\u001b[0m\u001b[2;32m\u001b[0m```\n"
},
"green": {
"simple": "```ansi\n\u001b[2;36mHello World\u001b[0m```\n",
"empty": "```ansi\n\u001b[2;36m\u001b[0m```\n"
},
"yellow": {
"simple": "```ansi\n\u001b[2;33mHello World\u001b[0m```\n",
"empty": "```ansi\n\u001b[2;33m\u001b[0m```\n"
}
}
}

View File

@@ -0,0 +1,41 @@
{
"boldMarkup": {
"complete": "<b>bold text</b>",
"openOnly": "<b>bold text",
"closeOnly": "bold text</b>",
"nested": "<b>outer <b>inner</b> text</b>",
"empty": "<b></b>",
"multiple": "<b>first</b> and <b>second</b>",
"mixed": "<b>bold</b> with <i>italic</i> text"
},
"italicMarkup": {
"complete": "<i>italic text</i>",
"openOnly": "<i>italic text",
"closeOnly": "italic text</i>",
"nested": "<i>outer <i>inner</i> text</i>",
"empty": "<i></i>",
"multiple": "<i>first</i> and <i>second</i>",
"mixed": "<i>italic</i> with <b>bold</b> text"
},
"colorTags": {
"hex6": "<color=0xFF5733>colored text</color>",
"hex8": "<color=0xFF5733AA>colored text</color>",
"hexWithoutPrefix": "<color=FF5733>colored text</color>",
"namedColor": "<color=red>colored text</color>",
"nested": "<color=blue>outer <color=red>inner</color> text</color>",
"empty": "<color=green></color>",
"multiple": "<color=red>first</color> and <color=blue>second</color>"
},
"eveLinks": {
"simple": "<a href=showinfo:587>Rifter</a>",
"withSpaces": "<a href=showinfo:12345>Ship Name With Spaces</a>",
"multiple": "<a href=showinfo:587>Rifter</a> and <a href=showinfo:588>Merlin</a>",
"nested": "Check out <a href=showinfo:587>Rifter</a> for PvP",
"empty": "<a href=showinfo:587></a>"
},
"combined": {
"allMarkup": "<b>Bold</b> <i>italic</i> <color=red>colored</color> <a href=showinfo:587>linked</a>",
"nestedComplex": "<b><color=blue><a href=showinfo:587>Bold Blue Rifter</a></color></b>",
"realWorldExample": "The <b><color=0xFF5733>Rifter</color></b> is a <i>fast</i> <a href=showinfo:587>frigate</a> used in PvP."
}
}

View File

@@ -0,0 +1,34 @@
{
"milliseconds": {
"zero": 0,
"oneSecond": 1000,
"oneMinute": 60000,
"oneHour": 3600000,
"complex": 3661500,
"daysWorthMs": 86400000,
"fractionalSeconds": 1500,
"smallFraction": 100
},
"seconds": {
"zero": 0,
"oneSecond": 1,
"oneMinute": 60,
"oneHour": 3600,
"complex": 3661,
"daysWorthSec": 86400,
"fractionalInput": 3661.5
},
"expected": {
"zero": "0.0s",
"oneSecond": "1.0s",
"oneMinute": "1m",
"oneHour": "1h",
"complexMs": "1h 1m 1.5s",
"complexSec": "1h 1m 1s",
"daysMs": "24h",
"daysSec": "24h",
"fractionalSeconds": "1.5s",
"smallFraction": "0.1s",
"fractionalInputSec": "1h 1m 1s"
}
}

115
packages/eve/package.json Normal file
View File

@@ -0,0 +1,115 @@
{
"name": "@star-kitten/eve",
"version": "0.0.1",
"description": "Star Kitten EVE Library.",
"type": "module",
"license": "MIT",
"homepage": "https://git.f302.me/jb/star-kitten#readme",
"bugs": {
"url": "https://git.f302.me/jb/star-kitten/issues"
},
"repository": {
"type": "git",
"url": "git+https://git.f302.me/jb/star-kitten.git"
},
"author": "JB <j-b-3.deviate267@passmail.net>",
"files": [
"dist"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index*.d.ts"
},
"./esi": {
"import": "./dist/esi/index.js",
"types": "./dist/esi/index*.d.ts",
"require": "./dist/esi/index.js"
},
"./db": {
"import": "./dist/db/index.js",
"types": "./dist/db/index*.d.ts",
"require": "./dist/db/index.js"
},
"./ref": {
"import": "./dist/ref/index.js",
"types": "./dist/ref/index*.d.ts",
"require": "./dist/ref/index.js"
},
"./third-party/janice.js": {
"import": "./dist/third-party/janice.js",
"types": "./dist/types/third-party/janice.d.ts",
"require": "./dist/third-party/janice.js"
},
"./models": {
"import": "./dist/models/index.js",
"types": "./dist/models/index*.d.ts",
"require": "./dist/models/index.js"
},
"./data/*": "./data/*",
"./discord": {
"import": "./dist/discord/index.js",
"require": "./dist/discord/index.js",
"types": "./dist/types/discord/index.d.ts"
}
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/bun": "^1.3.5",
"@types/jsonwebtoken": "^9.0.10",
"@types/jwk-to-pem": "^2.0.3",
"@types/lodash": "^4.17.20",
"@types/node": "^22.15.17",
"@types/node-cache": "^4.2.5",
"@types/stream-chain": "^2.1.0",
"@types/stream-json": "^1.7.8",
"@vitest/coverage-v8": "^3.2.4",
"bumpp": "^10.1.0",
"drizzle-kit": "^0.31.4",
"openapi-fetch": "^0.15.0",
"openapi-typescript": "^7.10.1",
"prettier-plugin-multiline-arrays": "^4.0.3",
"tsdown": "^0.14.2",
"typescript": "beta"
},
"dependencies": {
"@star-kitten/util": "link:@star-kitten/util",
"@orama/orama": "^3.1.13",
"@oslojs/encoding": "^1.1.0",
"cron-parser": "^5.3.1",
"date-fns": "^4.1.0",
"domhandler": "^5.0.3",
"drizzle-orm": "^0.44.5",
"elysia": "^1.4.20",
"fp-filters": "^0.5.4",
"html-dom-parser": "^5.1.1",
"jsonwebtoken": "^9.0.2",
"jwk-to-pem": "^2.0.7",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"node-cache": "^5.1.2",
"stream-chain": "^3.4.0",
"stream-json": "^1.9.1",
"winston": "^3.17.0"
},
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"link": "bun link",
"test": "bun test",
"typecheck": "tsc --noEmit",
"release": "bumpp && npm publish",
"generate-migrations": "bunx drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts",
"migrate": "bun run ./src/db/migrate.ts",
"everef-api": "bunx openapi-typescript https://raw.githubusercontent.com/autonomouslogic/eve-ref/refs/heads/main/spec/eve-ref-api.yaml -o src/eve/everef/schema.d.ts",
"get-data": "bun run refresh:reference-data && bun run refresh:hoboleaks",
"refresh:reference-data": "bun run scripts/download-and-extract.ts https://data.everef.net/reference-data/reference-data-latest.tar.xz ./data/reference-data",
"refresh:hoboleaks": "bun run scripts/download-and-extract.ts https://data.everef.net/hoboleaks-sde/hoboleaks-sde-latest.tar.xz ./data/hoboleaks",
"static-export": "bun run scripts/export-solar-systems.ts"
}
}

View File

@@ -0,0 +1,59 @@
/**
* EVE Swagger Interface - Alliance Endpoints
* https://developers.eveonline.com/api-explorer#/operations/GetAlliances
*/
import { esiFetch, type PublicEsiOptions } from './util/fetch';
/**
* List all active player alliances
* - This route is cached for an hour
* @returns {number[]} - An array of all active player alliance ids
*/
export async function listAlliances(options?: PublicEsiOptions): Promise<number[]> {
return await esiFetch<number[]>('/alliances/', options);
}
interface AllianceInfo {
creator_corporation_id: number;
creator_id: number;
date_founded: string;
executor_corporation_id: number;
faction_id: number;
name: string;
ticker: string;
}
/**
* Get information about a specific alliance
* - This route is cached for an hour
* @param alliance_id Alliance id
* @returns {AllianceInfo}
*/
export async function getAllianceInformation(alliance_id: number, options?: PublicEsiOptions): Promise<Partial<AllianceInfo>> {
return await esiFetch<Partial<AllianceInfo>>(`/alliances/${alliance_id}/`, options);
}
/**
* List all corporations in an alliance
* - This route is cached for an hour
* @param alliance_id Alliance id
* @returns {number[]} - Array of corporation ids
*/
export async function listAllianceCorporations(alliance_id: number, options?: PublicEsiOptions): Promise<number[]> {
return await esiFetch<number[]>(`/alliances/${alliance_id}/corporations/`, options);
}
interface AllianceIcon {
px128x128: string;
px64x64: string;
}
/**
* Get alliance icon
* - This route is cached for an hour
* @param alliance_id Alliance id
* @returns {AllianceIcon}
*/
export async function getAllianceIcon(alliance_id: number, options?: PublicEsiOptions): Promise<Partial<AllianceIcon>> {
return await esiFetch<Partial<AllianceIcon>>(`/alliances/${alliance_id}/icons/`, options);
}

View File

@@ -0,0 +1,132 @@
/**
* EVE ESI Assets API
* https://developers.eveonline.com/api-explorer#/operations/GetCharactersCharacterIdAssets
*/
import { ESI_SCOPE } from '../oauth';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
import type { LocationFlag } from './types/location-flag';
export enum AssetLocationType {
STATION = 'station',
SOLAR_SYSTEM = 'solar_system',
ITEM = 'item',
OTHER = 'other',
}
export interface Asset {
is_blueprint_copy: boolean;
is_singleton: boolean;
item_id: number;
location_flag: LocationFlag;
location_id: number;
location_type: AssetLocationType;
quantity: number;
type_id: number;
}
export function getCharacterAssets(options: EsiOptions, page: number = 1): Promise<Partial<Asset>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_assets.v1']);
return esiFetch<Partial<Asset>[]>(`/characters/${character_id}/assets/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
});
}
export interface AssetLocation {
item_id: number;
position: {
x: number;
y: number;
z: number;
};
}
export function getCharacterAssetLocations(options: EsiOptions, ids: number[]): any[] | Promise<Partial<AssetLocation>[]> {
if (ids.length === 0) return [];
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_assets.v1']);
return esiFetch<Partial<AssetLocation>[]>(`/characters/${character_id}/assets/locations/`, {
method: 'POST',
body: JSON.stringify(ids),
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
});
}
export interface AssetNames {
item_id: number;
name: string;
}
export function getCharacterAssetNames(options: EsiOptions, ids: number[]): any[] | Promise<Partial<AssetNames>[]> {
if (ids.length === 0) return [];
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_assets.v1']);
return esiFetch<Partial<AssetNames>[]>(`/characters/${character_id}/assets/names/`, {
...options,
method: 'POST',
body: JSON.stringify(ids),
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
});
}
export interface CorpAsset {
is_blueprint_copy: boolean;
is_singleton: boolean;
item_id: number;
location_flag: LocationFlag;
location_id: number;
location_type: 'station' | 'solar_system' | 'item' | 'other';
quantity: number;
type_id: number;
}
export async function getCorporationAssets(options: EsiOptions, corporation_id: number, page: number = 1): Promise<Partial<CorpAsset>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_corporation_assets.v1']);
return await esiFetch<Partial<CorpAsset>[]>(`/corporations/${corporation_id}/assets/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
});
}
export interface AssetLocation {
item_id: number;
position: {
x: number;
y: number;
z: number;
};
}
export async function getCorporationAssetLocations(
options: EsiOptions,
corporation_id: number,
ids: number[],
): Promise<Partial<AssetLocation>[]> {
if (ids.length === 0) return [];
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_corporation_assets.v1']);
return await esiFetch<Partial<AssetLocation>[]>(`/corporations/${corporation_id}/assets/locations/`, {
...options,
method: 'POST',
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
});
}
export interface AssetNames {
item_id: number;
name: string;
}
export async function getCorporationAssetNames(options: EsiOptions, id: number, ids: number[]): Promise<Partial<AssetNames>[]> {
if (ids.length === 0) return [];
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_corporation_assets.v1']);
return await esiFetch<Partial<AssetNames>[]>(`/corporations/${id}/assets/names/`, {
...options,
method: 'POST',
body: JSON.stringify(ids),
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
});
}

View File

@@ -0,0 +1,117 @@
/**
* EVE ESI Caldendar Module
*
* This module provides functions to interact with EVE Online's ESI Calendar API,
* allowing retrieval and management of calendar events for characters.
*
* ref: https://developers.eveonline.com/api-explorer#/operations/GetCharactersCharacterIdCalendar
*/
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
import { tokenHasScopes } from '../oauth/eve-auth';
import { ESI_SCOPE } from '../oauth';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
export interface CalendarEvent {
event_date: string; // date-time string
event_id: number;
event_response: 'accepted' | 'declined' | 'tentative' | 'no_response';
importance: number;
title: string;
}
/**
* List calendar event summaries for a character.
*
* Get 50 event summaries from the calendar. If no from_event ID is given, the resource will
* return the next 50 chronological event summaries from now. If a from_event ID is
* specified, it will return the next 50 chronological event summaries from after that event
* - cached for 5 seconds
*
* @param options EsiOptions
* @param from_event Event from which to get the next 50 chronological event summaries
* @returns {Partial<CalendarEvent>[]}
*/
export async function listCalendarEventSummaries(options: EsiOptions, from_event?: number): Promise<Partial<CalendarEvent>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.read_calendar_events.v1']);
return await esiFetch<Partial<CalendarEvent>[]>(`/characters/${character_id}/calendar/${from_event ?? '?from_event=' + from_event}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export interface CalendarEventDetails {
date: string; // date-time string
duration: number; // in minutes
event_id: number;
importance: number;
owner_id: number;
owner_name: string;
owner_type: 'eve_server' | 'corporation' | 'faction' | 'alliance' | 'character';
response: 'accepted' | 'declined' | 'tentative' | 'no_response';
text: string;
title: string;
}
/**
* Get an event's details by its ID.
*
* Get all the information for a specific event.
* - cached for 5 seconds
*
* @param options EsiOptions
* @param event_id Event Id
* @returns {Partial<CalendarEventDetails>}
*/
export async function getEventDetails(options: EsiOptions, event_id: number): Promise<Partial<CalendarEventDetails>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.read_calendar_events.v1']);
return await esiFetch<Partial<CalendarEventDetails>>(`/characters/${character_id}/calendar/${event_id}/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
/**
* Respond to a calendar event.
*
* Accept, decline, or tentatively accept an event invitation.
*
* @param options EsiOptions
* @param event_id Event Id
* @param response Response: 'accepted' | 'declined' | 'tentative'
*/
export async function respondToEvent(
options: EsiOptions,
event_id: number,
response: 'accepted' | 'declined' | 'tentative',
): Promise<void> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.respond_calendar_events.v1']);
return await esiFetch<void>(`/characters/${character_id}/calendar/${event_id}/`, {
...options,
method: 'PUT',
body: JSON.stringify({ response }),
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export interface CalendarEventAttendee {
character_id: number;
event_response: 'accepted' | 'declined' | 'tentative' | 'no_response';
}
/**
* Get the attendees of a calendar event.
*
* Get the list of attendees for a specific event.
* - cached for 5 seconds
*
* @param options EsiOptions
* @param event_id Event Id
* @returns {Partial<CalendarEventAttendee>[]}
*/
export async function getEventAttendees(options: EsiOptions, event_id: number): Promise<Partial<CalendarEventAttendee>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.read_calendar_events.v1']);
return await esiFetch<Partial<CalendarEventAttendee>[]>(`/characters/${character_id}/calendar/${event_id}/attendees/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}

View File

@@ -0,0 +1,223 @@
import { ESI_SCOPE } from '../oauth';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
import type { NotificationType } from './types/notification-type';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
import type { Blueprint } from './types/shared';
export interface CharacterAffiliations {
character_id: number;
corporation_id: number;
alliance_id?: number;
faction_id?: number;
}
export function getCharacterAffiliations(
character_ids: number[],
options?: PublicEsiOptions,
): any[] | Promise<Partial<CharacterAffiliations>[]> {
if (character_ids.length === 0) return [];
if (character_ids.length > 1000) throw 'Maximum of 1000 character IDs can be requested at once';
return esiFetch<Partial<CharacterAffiliations>[]>(`/characters/affiliation/`, {
...options,
method: 'POST',
body: JSON.stringify(character_ids),
});
}
export interface CharacterData {
alliance_id: number;
birthday: string;
bloodline_id: number;
corporation_id: number;
description: string;
faction_id: number;
gender: 'male' | 'female';
name: string;
race_id: number;
security_status: number;
title: string;
}
export function getCharacterPublicData(id: number, options?: PublicEsiOptions): Promise<Partial<CharacterData>> {
return esiFetch<Partial<CharacterData>>(`/characters/${id}/`, options);
}
export interface AgentResearch {
agent_id: number;
points_per_day: number;
remainder_points: number;
skill_type_id: number;
started_at: string;
}
// required scope: esi-characters.read_agents_research.v1
export function getCharacterAgentResearch(options: EsiOptions): Promise<Partial<AgentResearch>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_agents_research.v1']);
return esiFetch<Partial<AgentResearch>[]>(`/characters/${character_id}/agents_research/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_INDUSTRY,
});
}
// required scope: esi-characters.read_blueprints.v1
export function getCharacterBlueprints(options: EsiOptions, page: number = 1): Promise<Partial<Blueprint>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_blueprints.v1']);
return esiFetch<Partial<Blueprint>[]>(`/characters/${character_id}/blueprints/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_INDUSTRY,
});
}
export interface CharacterCorporationHistory {
corporation_id: number;
is_deleted: boolean;
record_id: number; // An incrementing ID that can be used to order records where start_date is ambiguous
start_date: string;
}
export function getCharacterCorporationHistory(options: EsiOptions): Promise<Partial<CharacterCorporationHistory>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_corporation_roles.v1']);
return esiFetch<Partial<CharacterCorporationHistory>[]>(`/characters/${character_id}/corporationhistory/`, options);
}
export function calculateCSPAChargeCost(options: EsiOptions, target_character_ids: number[]): Promise<number[]> {
if (target_character_ids.length === 0) return null;
if (target_character_ids.length > 100) throw 'Maximum of 100 target character IDs can be requested at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_cspa.v1']);
return esiFetch<number[]>(`/characters/${character_id}/cspa/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
method: 'POST',
body: JSON.stringify(target_character_ids),
});
}
export interface JumpFatigue {
jump_fatigue_expire_date: string;
last_jump_date: string;
last_update_date: string;
}
export function getCharacterJumpFatigue(options: EsiOptions): Promise<Partial<JumpFatigue>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_fatigue.v1']);
return esiFetch<Partial<JumpFatigue>>(`/characters/${character_id}/fatigue/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
});
}
export interface Medals {
corporation_id: number;
date: string;
description: string;
graphics: {
color: number;
graphic: number;
layer: number;
part: number;
}[];
issuer_id: number;
medal_id: number;
reason: string;
status: 'private' | 'public';
title: string;
}
export function getCharacterMedals(options: EsiOptions): Promise<Partial<Medals>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_medals.v1']);
return esiFetch<Partial<Medals>[]>(`/characters/${character_id}/medals/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
});
}
export interface Notification {
is_read: boolean;
notification_id: number;
sender_id: number;
sender_type: 'character' | 'corporation' | 'alliance' | 'faction' | 'other';
text: string;
timestamp: string;
type: NotificationType;
}
export function getCharacterNotifications(options: EsiOptions): Promise<Partial<Notification>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_notifications.v1']);
return esiFetch<Partial<Notification>[]>(`/characters/${character_id}/notifications/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_NOTIFICATION,
});
}
export interface ContactNotification {
message: string;
notification_id: number;
send_date: string;
sender_character_id: number;
standing_level: -10 | -5 | 0 | 5 | 10;
}
export function getCharacterContactNotifications(options: EsiOptions): Promise<Partial<ContactNotification>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_notifications.v1']);
return esiFetch<Partial<ContactNotification>[]>(`/characters/${character_id}/notifications/contacts`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export interface CharacterPortraits {
px128x128: string;
px256x256: string;
px512x512: string;
px64x64: string;
}
export function getCharacterPortraits(character_id: number, options?: PublicEsiOptions): Promise<Partial<CharacterPortraits>> {
return esiFetch<Partial<CharacterPortraits>>(`/characters/${character_id}/portrait/`, options);
}
export function getPortraitURL(character_id: number) {
return `https://images.evetech.net/characters/${character_id}/portrait`;
}
export interface CharacterCorporationRoles {
roles: string[];
roles_at_base: string[];
roles_at_hq: string[];
roles_at_other: string[];
}
export function getCharacterCorporationRoles(options: EsiOptions): Promise<Partial<CharacterCorporationRoles>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_corporation_roles.v1']);
return esiFetch<Partial<CharacterCorporationRoles>>(`/characters/${character_id}/roles`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
});
}
export interface CharacterStandings {
from_id: number;
from_type: 'agent' | 'npc_corp' | 'faction';
standing: number;
}
export function getCharacterStandings(options: EsiOptions): Promise<Partial<CharacterStandings>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_standings.v1']);
return esiFetch<Partial<CharacterStandings>[]>(`/characters/${character_id}/standings`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export interface CharacterTitles {
titles: {
name: string;
title_id: number;
}[];
}
export function getCharacterTitles(options: EsiOptions): Promise<Partial<CharacterTitles>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_titles.v1']);
return esiFetch<Partial<CharacterTitles>>(`/characters/${character_id}/titles`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
});
}

View File

@@ -0,0 +1,35 @@
import { ESI_SCOPE } from '../oauth/auth.types';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
export interface CharacterClones {
home_location: {
location_id: number;
location_type: 'station' | 'structure';
};
jump_clones: {
implants: number[];
jump_clone_id: number;
location_id: number;
location_type: 'station' | 'structure';
name: string;
}[];
last_clone_jump_date: string;
last_station_change_date: string;
}
export function getCharacterClones(options: EsiOptions): Promise<Partial<CharacterClones>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-clones.read_clones.v1']);
return esiFetch<Partial<CharacterClones>>(`/characters/${character_id}/clones`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_LOCATION,
});
}
export function getCharacterActiveImplants(options: EsiOptions): Promise<number[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-clones.read_implants.v1']);
return esiFetch<number[]>(`/characters/${character_id}/implants`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
});
}

View File

@@ -0,0 +1,133 @@
import { ESI_SCOPE } from '../oauth/auth.types';
import type { STANDING } from './types/shared';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
export interface Contact {
contact_id: number;
contact_type: 'character' | 'corporation' | 'alliance' | 'faction';
label_ids?: number[];
standing: STANDING;
}
export function getAllianceContacts(options: EsiOptions, alliance_id: number, page: number = 1): Promise<Contact[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-alliances.read_contacts.v1']);
return esiFetch<Contact[]>(`/alliances/${alliance_id}/contacts/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.ALLIANCE_SOCIAL,
});
}
export interface ContactLabel {
label_id: number;
name: string;
}
export function getAllianceContactLabels(options: EsiOptions, alliance_id: number): Promise<ContactLabel[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-alliances.read_contacts.v1']);
return esiFetch<ContactLabel[]>(`/alliances/${alliance_id}/contacts/labels/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.ALLIANCE_SOCIAL,
});
}
export function deleteCharacterContacts(options: EsiOptions, character_ids: number[]): Promise<void> {
if (character_ids.length === 0) return;
if (character_ids.length > 20) throw 'Maximum of 20 IDs can be deleted at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.write_contacts.v1']);
return esiFetch<void>(`/characters/${character_id}/contacts/`, {
method: 'DELETE',
body: JSON.stringify({ contact_ids: character_ids }),
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export interface CharacterContact extends Contact {
is_blocked?: boolean;
is_watched?: boolean;
}
export function getCharacterContacts(options: EsiOptions, page: number = 1): Promise<CharacterContact[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_contacts.v1']);
return esiFetch<CharacterContact[]>(`/characters/${character_id}/contacts/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export function addCharacterContacts(
options: EsiOptions,
character_ids: number[],
standing: STANDING,
label_ids?: number[],
watched?: boolean,
): Promise<void> {
if (character_ids.length === 0) return;
if (character_ids.length > 100) throw 'Maximum of 100 IDs can be added at once';
if (label_ids && label_ids.length > 63) throw 'Maximum of 63 label IDs can be assigned at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.write_contacts.v1']);
return esiFetch<void>(
`/characters/${character_id}/contacts?standing=${standing}${watched ? '&watched=true' : ''}${
label_ids ? `&label_ids=${label_ids.join(',')}` : ''
}`,
{
...options,
method: 'POST',
body: JSON.stringify(character_ids),
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
},
);
}
export function editCharacterContacts(
options: EsiOptions,
character_ids: number[],
standing: STANDING,
label_ids?: number[],
watched?: boolean,
): Promise<void> {
if (character_ids.length === 0) return;
if (character_ids.length > 100) throw 'Maximum of 100 IDs can be edited at once';
if (label_ids && label_ids.length > 63) throw 'Maximum of 63 label IDs can be assigned at once';
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.write_contacts.v1']);
return esiFetch<void>(
`/characters/${character_id}/contacts?standing=${standing}${watched ? '&watched=true' : ''}${
label_ids ? `&label_ids=${label_ids.join(',')}` : ''
}`,
{
...options,
method: 'PUT',
body: JSON.stringify(character_ids),
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
},
);
}
export function getCharacterContactLabels(options: EsiOptions): Promise<Partial<ContactLabel>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_contacts.v1']);
return esiFetch<Partial<ContactLabel>[]>(`/characters/${character_id}/contacts/labels/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
});
}
export interface CorporationContact extends Contact {
is_watched?: boolean;
}
export function getCorporationContacts(options: EsiOptions, corporation_id: number, page: number = 1): Promise<CorporationContact[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_contacts.v1']);
return esiFetch<CorporationContact[]>(`/corporations/${corporation_id}/contacts/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_SOCIAL,
});
}
export function getCorporationContactLabels(options: EsiOptions, corporation_id: number): Promise<ContactLabel[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_contacts.v1']);
return esiFetch<ContactLabel[]>(`/corporations/${corporation_id}/contacts/labels/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_SOCIAL,
});
}

View File

@@ -0,0 +1,161 @@
import { ESI_SCOPE } from '../oauth/auth.types';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
export enum ContractType {
ITEM_EXCHANGE = 'item_exchange',
AUCTION = 'auction',
COURIER = 'courier',
LOAN = 'loan',
}
export interface PublicContract {
buyout?: number;
collateral?: number;
contract_id: number;
date_expired: string;
date_issued: string;
days_to_complete?: number;
end_location_id?: number;
for_corporation: boolean;
issuer_corporation_id: number;
issuer_id: number;
price?: number;
reward?: number;
start_location_id?: number;
title?: string;
type: 'item_exchange' | 'auction' | 'courier' | 'loan';
volume?: number;
}
export enum ContractAvailability {
PUBLIC = 'public',
PERSONAL = 'personal',
CORPORATION = 'corporation',
ALLIANCE = 'alliance',
}
export enum ContractStatus {
OUTSTANDING = 'outstanding',
IN_PROGRESS = 'in_progress',
FINISHED_ISSUER = 'finished_issuer',
FINISHED_CONTRACTOR = 'finished_contractor',
CANCELLED = 'cancelled',
REJECTED = 'rejected',
FAILED = 'failed',
DELETED = 'deleted',
REVERSED = 'reversed',
}
export interface Contract extends PublicContract {
acceptor_id: number;
assignee_id: number;
availability: ContractAvailability;
date_accepted?: string;
date_completed?: string;
status:
| 'outstanding'
| 'in_progress'
| 'finished_issuer'
| 'finished_contractor'
| 'cancelled'
| 'rejected'
| 'failed'
| 'deleted'
| 'reversed';
}
/**
* Returns contracts available to a character, only if the character is issuer, acceptor or assignee.
* Only returns contracts no older than 30 days, or if the status is "in_progress".
*/
export function getCharacterContracts(options: EsiOptions, page: number = 1): Promise<Contract[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_character_contracts.v1']);
return esiFetch<Contract[]>(`/characters/${character_id}/contracts/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
});
}
export interface PublicContractBid {
amount: number;
bid_id: number;
date_bid: string;
}
export interface ContractBid extends PublicContractBid {
bidder_id: number;
}
export function getContractBids(options: EsiOptions, contract_id: number): Promise<ContractBid[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_character_contracts.v1']);
return esiFetch<ContractBid[]>(`/characters/${character_id}/contracts/${contract_id}/bids/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
});
}
export interface ContractItem {
is_included: boolean; // true if the item is included in the contract, false if it is being requested
is_singleton: boolean;
quantity: number; // number of items (for stackable items)
raw_quantity?: number; // -1 indicates that the item is a singleton (non-stackable). If the item happens to be a Blueprint, -1 is an Original and -2 is a Blueprint Copy
record_id: number; // unique ID for this item in the contract
type_id: number; // type ID of the item
}
export function getContractItems(options: EsiOptions, contract_id: number): Promise<ContractItem[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_character_contracts.v1']);
return esiFetch<ContractItem[]>(`/characters/${character_id}/contracts/${contract_id}/items/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
});
}
export function getPublicContractBids(contract_id: number, page: number = 1, options?: PublicEsiOptions): Promise<PublicContractBid[]> {
return esiFetch<PublicContractBid[]>(`/contracts/public/bids/${contract_id}?page=${page}`, options);
}
export interface PublicContractItem {
is_blueprint_copy?: boolean;
is_included: boolean; // true if the item is included in the contract, false if it is being requested
item_id: number;
material_efficiency?: number; // Material efficiency level of the blueprint
quantity: number;
record_id: number; // unique ID for this item in the contract
runs?: number; // Number of runs for the blueprint
time_efficiency?: number; // Time efficiency level of the blueprint
type_id: number; // type ID of the item
}
export function getPublicContractItems(contract_id: number, page: number = 1, options?: PublicEsiOptions): Promise<PublicContractItem[]> {
return esiFetch<PublicContractItem[]>(`/contracts/public/items/${contract_id}?page=${page}`, options);
}
export function getPublicContracts(region_id: number, page: number = 1, options?: PublicEsiOptions): Promise<PublicContract[]> {
return esiFetch<PublicContract[]>(`/contracts/public/${region_id}?page=${page}`, options);
}
export function getCorporationContracts(options: EsiOptions, corporation_id: number, page: number = 1): Promise<Contract[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_corporation_contracts.v1']);
return esiFetch<Contract[]>(`/corporations/${corporation_id}/contracts/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
});
}
export function getCorporationContractBids(options: EsiOptions, corporation_id: number, contract_id: number): Promise<ContractBid[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_corporation_contracts.v1']);
return esiFetch<ContractBid[]>(`/corporations/${corporation_id}/contracts/${contract_id}/bids/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
});
}
export function getCorporationContractItems(options: EsiOptions, corporation_id: number, contract_id: number): Promise<ContractItem[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_corporation_contracts.v1']);
return esiFetch<ContractItem[]>(`/corporations/${corporation_id}/contracts/${contract_id}/items/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
});
}

View File

@@ -0,0 +1,67 @@
/**
* Eve Corporation Projects ESI endpoints
* ref: https://developers.eveonline.com/api-explorer
*/
import { ESI_SCOPE } from '../oauth/auth.types';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
import type { Blueprint, Icons, STANDING } from './types/shared';
import type { CorporationRoles } from './types/corporation';
export interface CorporationProject {
id: string; // uuid
last_modified: string;
name: string;
progress: {
current: number;
desired: number;
};
reward: {
initial: number; // original ISK amount reserved for the project
remaining: number; // ISK amount still remaining for the project
};
state: 'Unspecified' | 'Active' | 'Closed' | 'Completed' | 'Expired' | 'Deleted';
/**
* State:
* Unspecified - An unspecified state
* Active - Active and accepting contributions
* Closed - Closed by the corporation
* Completed - Completed
* Expired - Expired
* Deleted - Deleted and the details are no longer available
*/
}
export interface CorporationProjectResponse {
cursor?: {
after?: string; // cursor to use as 'after' in your next request, to continue walking fowards in time
before?: string; // cursor to use as 'before' in your next request, to continue walking backwards in time
};
projects: CorporationProject[];
}
export function listCorporationProjects(
options: EsiOptions,
corporation_id: number,
filters: {
after?: string;
before?: string;
limit?: number;
state?: 'Unspecified' | 'Active' | 'Closed' | 'Completed' | 'Expired' | 'Deleted';
} = {},
): Promise<CorporationProjectResponse> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_projects.v1']);
const queryParams = new URLSearchParams();
if (filters.after) queryParams.append('after', filters.after);
if (filters.before) queryParams.append('before', filters.before);
if (filters.limit) queryParams.append('limit', filters.limit.toString());
if (filters.state) queryParams.append('state', filters.state);
return esiFetch<CorporationProjectResponse>(`/corporations/${corporation_id}/projects/?${queryParams.toString()}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_PROJECT,
});
}
// TODO: finish the remaining endpoints later

View File

@@ -0,0 +1,393 @@
/**
* Eve Corporation ESI endpoints
* ref: https://developers.eveonline.com/api-explorer#/operations/GetCorporationsNpccorps
*/
import { ESI_SCOPE } from '../oauth/auth.types';
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
import type { Blueprint, Icons, STANDING } from './types/shared';
import type { CorporationRoles } from './types/corporation';
export async function getNpcCorporations(options?: PublicEsiOptions): Promise<number[]> {
return await esiFetch<number[]>('/corporations/npccorps', options);
}
interface CorporationInfo {
alliance_id?: number;
ceo_id: number;
creator_id: number;
date_founded?: string;
description?: string;
faction_id?: number;
home_station_id?: number;
member_count: number;
name: string;
shares?: number;
tax_rate: number;
ticker: string;
url?: string;
war_eligible?: boolean;
}
export async function getCorporationData(corporation_id: number, options?: PublicEsiOptions): Promise<CorporationInfo> {
return await esiFetch<CorporationInfo>(`/corporations/${corporation_id}/`, options);
}
interface AllianceHistory {
alliance_id?: number;
is_deleted?: boolean;
record_id: number;
start_date: string;
}
export async function getCorporationAllianceHistory(corporation_id: number, options?: PublicEsiOptions): Promise<AllianceHistory[]> {
return await esiFetch<AllianceHistory[]>(`/corporations/${corporation_id}/alliancehistory/`, options);
}
export async function getCorporationBlueprints(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<Partial<Blueprint>[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_blueprints.v1']);
return await esiFetch<Partial<Blueprint>[]>(`/corporations/${corporation_id}/blueprints/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_INDUSTRY,
});
}
export async function getAllCorporationALSCLogs(options: EsiOptions, corporation_id: number, page: number = 1): Promise<any[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_container_logs.v1']);
return await esiFetch<any[]>(`/corporations/${corporation_id}/containers/logs/?page=${page}`, {
...options,
});
}
export interface CorporationDivisions {
hangar: {
division: number;
name: string;
}[];
wallet: {
division: number;
name: string;
}[];
}
export async function getCorporationDivisions(options: EsiOptions, corporation_id: number): Promise<CorporationDivisions> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_divisions.v1']);
return await esiFetch<CorporationDivisions>(`/corporations/${corporation_id}/divisions/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_WALLET,
});
}
export interface CorporationFacility {
facility_id: number;
system_id: number;
type_id: number;
}
export async function getCorporationFacilities(options: EsiOptions, corporation_id: number): Promise<CorporationFacility[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_facilities.v1']);
return await esiFetch<CorporationFacility[]>(`/corporations/${corporation_id}/facilities/`, {
...options,
});
}
export async function getCorporationIcons(corporation_id: number, options?: PublicEsiOptions): Promise<Icons> {
return await esiFetch<Icons>(`/corporations/${corporation_id}/icons/`, options);
}
export interface CorporationMedal {
created_at: string;
createor_id: number;
description: string;
medal_id: number;
title: string;
}
export async function getCorporationMedals(options: EsiOptions, corporation_id: number, page: number = 1): Promise<CorporationMedal[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_medals.v1']);
return await esiFetch<CorporationMedal[]>(`/corporations/${corporation_id}/medals/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
});
}
export interface CorporationIssuedMedal {
character_id: number;
issued_at: string;
issuer_id: number;
medal_id: number;
reason: string;
status: string;
}
export async function getCorporationIssuedMedals(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<CorporationIssuedMedal[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_medals.v1']);
return await esiFetch<CorporationIssuedMedal[]>(`/corporations/${corporation_id}/medals/issued/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
});
}
export async function getCorporationMembers(options: EsiOptions, corporation_id: number): Promise<number[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_corporation_membership.v1']);
return await esiFetch<number[]>(`/corporations/${corporation_id}/members/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export async function getCorporationMemberLimit(options: EsiOptions, corporation_id: number): Promise<number> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.track_members.v1']);
return await esiFetch<number>(`/corporations/${corporation_id}/members/limit/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export interface CorporationMemberTitles {
character_id: number;
titles: number[];
}
export async function getCorporationMemberTitles(options: EsiOptions, corporation_id: number): Promise<CorporationMemberTitles[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_titles.v1']);
return await esiFetch<CorporationMemberTitles[]>(`/corporations/${corporation_id}/members/titles/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export interface CorporationMemberTracking {
base_id?: number;
character_id: number;
location_id?: number;
logoff_date?: string;
logon_date?: string;
ship_type_id?: number;
start_date?: string;
}
export async function getCorporationMemberTracking(options: EsiOptions, corporation_id: number): Promise<CorporationMemberTracking[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.track_members.v1']);
return await esiFetch<CorporationMemberTracking[]>(`/corporations/${corporation_id}/membertracking/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export interface CorporationMemberRole {
character_id: number;
grantable_roles: CorporationRoles[];
grantable_roles_at_base: CorporationRoles[];
grantable_roles_at_hq: CorporationRoles[];
grantable_roles_at_other: CorporationRoles[];
roles: CorporationRoles[];
roles_at_base: CorporationRoles[];
roles_at_hq: CorporationRoles[];
roles_at_other: CorporationRoles[];
}
export async function getCorporationMemberRoles(options: EsiOptions, corporation_id: number): Promise<CorporationMemberRole[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_corporation_membership.v1']);
return await esiFetch<CorporationMemberRole[]>(`/corporations/${corporation_id}/members/roles/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export interface CorporationMemberRoleHistory {
changed_at: string;
character_id: number;
issuer_id: number;
new_roles: CorporationRoles[];
old_roles: CorporationRoles[];
role_type:
| 'grantable_roles'
| 'grantable_roles_at_base'
| 'grantable_roles_at_hq'
| 'grantable_roles_at_other'
| 'roles'
| 'roles_at_base'
| 'roles_at_hq'
| 'roles_at_other';
}
export async function getCorporationMemberRoleHistory(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<CorporationMemberRoleHistory[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_corporation_membership.v1']);
return await esiFetch<CorporationMemberRoleHistory[]>(`/corporations/${corporation_id}/roles/history/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export interface CorporationShareholder {
share_count: number;
shareholder_id: number;
shareholder_type: 'character' | 'corporation';
}
export async function getCorporationShareholders(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<CorporationShareholder[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-wallet.read_corporation_wallets.v1']);
return await esiFetch<CorporationShareholder[]>(`/corporations/${corporation_id}/shareholders/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
});
}
export interface CorporationStanding {
from_id: number;
from_type: 'agent' | 'npc_corp' | 'faction';
standing: STANDING;
}
export async function getCorporationStandings(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<CorporationStanding[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_standings.v1']);
return await esiFetch<CorporationStanding[]>(`/corporations/${corporation_id}/standings/?page=${page}`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
});
}
export interface CorporationStarbase {
moon_id?: number;
onlined_since?: string;
reinforced_until?: string;
starbase_id: number;
state?: 'offline' | 'online' | 'onlining' | 'reinforced' | 'unanchoring';
system_id: number;
type_id: number;
unanchor_at?: string;
}
export async function getCorporationStarbases(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<CorporationStarbase[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_starbases.v1']);
return await esiFetch<CorporationStarbase[]>(`/corporations/${corporation_id}/starbases/?page=${page}`, {
...options,
});
}
export type StarbaseRole = 'alliance_member' | 'config_starbase_equipment_role' | 'corporation_member' | 'starbase_fuel_technician_role';
export interface StarbaseDetail {
allow_alliance_members: boolean;
allow_corporation_members: boolean;
anchor: StarbaseRole;
attack_if_at_war: boolean;
attach_if_other_security_status_dropping: boolean;
attack_security_status_threshold?: number;
attack_standing_threshold?: STANDING;
fuel_bay_take: StarbaseRole;
fuel_bay_view: StarbaseRole;
fuels?: {
type_id: number;
quantity: number;
}[];
offline: StarbaseRole;
online: StarbaseRole;
unanchor: StarbaseRole;
use_alliance_standings: boolean;
}
export async function getCorporationStarbaseDetail(
options: EsiOptions,
corporation_id: number,
starbase_id: number,
system_id: number,
): Promise<StarbaseDetail> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_starbases.v1']);
return await esiFetch<StarbaseDetail>(`/corporations/${corporation_id}/starbases/${starbase_id}/?system_id=${system_id}`, {
...options,
});
}
export interface StationService {
name: string;
state: 'online' | 'offline' | 'cleanup';
}
export interface CorporationStructure {
corporation_id: number;
fuel_expires: string;
name: string;
next_reinforce_apply: string;
next_reinforce_hour: number;
profile_id: number;
reinforce_hour: number;
services: StationService[];
state:
| 'anchor_vulnerable'
| 'anchoring'
| 'armor_reinforce'
| 'armor_vulnerable'
| 'deploy_vulnerable'
| 'fitting_invulnerable'
| 'hull_reinforce'
| 'hull_vulnerable'
| 'online_depreceated'
| 'onlining_vulnerable'
| 'shield_vulnerable'
| 'unanchored'
| 'unknown';
state_timer_end: string; // date at which the structure will move to it's next state
state_timer_start: string; // date at which the structue entered it's current state
structure_id: number;
system_id: number;
type_id: number;
unanchors_at: string;
}
export async function getCorporationStructures(
options: EsiOptions,
corporation_id: number,
page: number = 1,
): Promise<CorporationStructure[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_structures.v1']);
return await esiFetch<CorporationStructure[]>(`/corporations/${corporation_id}/structures/?page=${page}`, {
...options,
});
}
export interface CorporationTitle {
grantable_roles: CorporationRoles[];
grantable_roles_at_base: CorporationRoles[];
grantable_roles_at_hq: CorporationRoles[];
grantable_roles_at_other: CorporationRoles[];
name: string;
roles: CorporationRoles[];
roles_at_base: CorporationRoles[];
roles_at_hq: CorporationRoles[];
roles_at_other: CorporationRoles[];
title_id: number;
}
export async function getCorporationTitles(options: EsiOptions, corporation_id: number): Promise<CorporationTitle[]> {
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_titles.v1']);
return await esiFetch<CorporationTitle[]>(`/corporations/${corporation_id}/titles/`, {
...options,
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
});
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,18 @@
export * as CharacterAPI from './character';
export * as CorporationAPI from './corporation';
export * as AllianceAPI from './alliance';
export * from './util/fetch';
export * from './skills';
export * from './util/options';
export * from './mail';
export * from './character';
export * from './alliance';
export * from './contracts';
import * as alliance from './alliance';
import * as assets from './assets';
import * as character from './character';
import * as corporation from './corporation';
import * as contracts from './contracts';
export { alliance, assets, character, corporation, contracts };

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,39 @@
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
import { ESI_SCOPE } from '../oauth';
export interface Location {
solar_system_id: number;
station_id: number;
structure_id: number;
}
// required scope: esi-location.read_location.v1
export function getCharacterLocation(options: EsiOptions): Promise<Partial<Location>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-location.read_location.v1']);
return esiFetch<Partial<Location>>(`/characters/${character_id}/location/`, options);
}
export interface Online {
last_login: string;
last_logout: string;
logins: number;
online: boolean;
}
// required scope: esi-location.read_online.v1
export function getCharacterOnline(options: EsiOptions): Promise<Partial<Online>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-location.read_online.v1']);
return esiFetch<Partial<Online>>(`/characters/${character_id}/online/`, options);
}
export interface CurrentShip {
ship_item_id: number;
ship_type_id: number;
ship_name: string;
}
// required scope: esi-location.read_ship_type.v1
export function getCharacterCurrentShip(options: EsiOptions): Promise<Partial<CurrentShip>> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-location.read_ship_type.v1']);
return esiFetch<Partial<CurrentShip>>(`/characters/${character_id}/ship/`, options);
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,144 @@
import { ESI_SCOPE } from '../oauth';
import { esiFetch, type EsiOptions, checkScopesAndGetCharacterId } from './util/fetch';
import { CharacterHelper, type Character } from '@/db';
export interface MailHeader {
from: number; // From whom the mail was sent
is_read: boolean; // is_read boolean
labels: string[]; // maxItems: 25, minimum: 0, title: get_characters_character_id_mail_labels, uniqueItems: true, labels array
mail_id: number; // mail_id integer
recipients: {
recipient_id: number; // recipient_id integer
recipient_type: 'alliance' | 'character' | 'corporation' | 'mailing_list'; // recipient_type enum
}[]; // maxItems: 52, minimum: 0, title: get_characters_character_id_mail_recipients, uniqueItems: true, recipients of the mail
subject: string; // Mail subject
timestamp: string; // When the mail was sent
}
// requires scope: esi-mail.read_mail.v1
export function getMailHeaders(options: EsiOptions): Promise<MailHeader[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.read_mail.v1']);
return esiFetch<MailHeader[]>(`/characters/${character_id}/mail/`, {
...options,
});
}
export interface SendMail {
approved_cost?: number; // approved_cost number
body: string; // body string; max length 10000
recipients: {
recipient_id: number; // recipient_id integer
recipient_type: 'alliance' | 'character' | 'corporation' | 'mailing_list'; // recipient_type enum
}[]; // maxItems: 50, minimum: 1, title: post_characters_character_id_mail, recipients of the mail
subject: string; // subject string; max length 1000
}
// requires scope: esi-mail.send_mail.v1
export function sendMail(options: EsiOptions, mail: SendMail): Promise<any> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.send_mail.v1']);
return esiFetch(`/characters/${character_id}/mail/`, {
...options,
method: 'POST',
body: JSON.stringify(mail),
});
}
// requires scope: esi-mail.read_mail.v1
export function deleteMail(options: EsiOptions, mailID: number): Promise<any> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.organize_mail.v1']);
return esiFetch(`/characters/${character_id}/mail/${mailID}/`, {
...options,
method: 'DELETE',
});
}
export interface Mail {
body: string; // body string
from: number; // from integer
labels: string[]; // labels array
read: boolean; // read boolean
subject: string; // subject string
timestamp: string; // timestamp string
recipients: {
recipient_id: number; // recipient_id integer
recipient_type: 'alliance' | 'character' | 'corporation' | 'mailing_list'; // recipient_type enum
}[]; // recipients array
}
// requires scope: esi-mail.read_mail.v1
export function getMail(options: EsiOptions, mailID: number): Promise<Mail> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.read_mail.v1']);
return esiFetch<Mail>(`/characters/${character_id}/mail/${mailID}/`, {
...options,
});
}
export interface MailMetadata {
labels: string[]; // labels array
read: boolean; // read boolean
}
// requires scope: esi-mail.organize_mail.v1
export function updateMailMetadata(options: EsiOptions, mailID: number, metadata: MailMetadata): Promise<any> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.organize_mail.v1']);
return esiFetch(`/characters/${character_id}/mail/${mailID}/`, {
...options,
method: 'PUT',
body: JSON.stringify(metadata),
});
}
export interface MailLabels {
labels: {
color: number; // color integer
label_id: number; // label_id integer
name: string; // name string
unread_count: number; // unread_count integer
}[]; // labels array
total_unread_count: number; // total_unread_count integer
}
// requires scope: esi-mail.read_mail.v1
export function getMailLabels(options: EsiOptions): Promise<MailLabels> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.read_mail.v1']);
return esiFetch<MailLabels>(`/characters/${character_id}/mail/labels/`, {
...options,
});
}
export interface CreateMailLabel {
color: number; // color integer
name: string; // name string
}
// requires scope: esi-mail.organize_mail.v1
export function createMailLabel(options: EsiOptions, label: CreateMailLabel): Promise<any> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.organize_mail.v1']);
return esiFetch(`/characters/${character_id}/mail/labels/`, {
...options,
method: 'POST',
body: JSON.stringify(label),
});
}
// requires scope: esi-mail.organize_mail.v1
export function deleteMailLabel(options: EsiOptions, labelID: number): Promise<any> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.organize_mail.v1']);
return esiFetch(`/characters/${character_id}/mail/labels/${labelID}/`, {
...options,
method: 'DELETE',
});
}
export interface MailingList {
mailing_list_id: number; // mailing_list_id integer
name: string; // name string
}
// requires scope: esi-mail.read_mail.v1
export function getMailingLists(options: EsiOptions): Promise<MailingList[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-mail.read_mail.v1']);
return esiFetch<MailingList[]>(`/characters/${character_id}/mail/lists/`, {
...options,
});
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,72 @@
import { ESI_SCOPE } from '../oauth';
import { esiFetch, checkScopesAndGetCharacterId, type EsiOptions } from './util/fetch';
export interface CharacterAttributes {
charisma: number;
intelligence: number;
memory: number;
perception: number;
willpower: number;
last_remap_date?: string;
bonus_remaps?: number;
accrued_remap_cooldown_date?: string;
}
// required scope: esi-skills.read_skills.v1
export function getCharacterAttributes(options: EsiOptions): Promise<CharacterAttributes> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-skills.read_skills.v1']);
return esiFetch<CharacterAttributes>(`/characters/${character_id}/attributes`, {
...options,
});
}
export interface SkillQueueItem {
finish_date?: string;
finished_level: number;
level_end_sp?: number;
level_start_sp?: number;
queue_position: number;
skill_id: number;
start_date?: string;
training_start_sp?: number;
}
// required scope: esi-skills.read_skillqueue.v1
export function getCharacterSkillQueue(options: EsiOptions): Promise<SkillQueueItem[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-skills.read_skillqueue.v1']);
return esiFetch<SkillQueueItem[]>(`/characters/${character_id}/skillqueue`, {
...options,
});
}
export interface APISkill {
active_skill_level: number;
skill_id: number;
skillpoints_in_skill: number;
trained_skill_level: number;
}
export interface CharacterSkills {
skills: APISkill[];
total_sp: number;
unallocated_sp?: number;
}
// required scope: esi-skills.read_skills.v1
export function getCharacterSkills(options: EsiOptions): Promise<CharacterSkills> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-skills.read_skills.v1']);
return esiFetch<CharacterSkills>(`/characters/${character_id}/skills`, {
...options,
});
}
export function calculateTrainingPercentage(queuedSkill: SkillQueueItem): number {
// percentage in when training started
const trainingStartPosition = (queuedSkill.training_start_sp! - queuedSkill.level_start_sp!) / queuedSkill.level_end_sp!;
// percentage completed between start and now
const timePosition =
(new Date().getTime() - new Date(queuedSkill.start_date!).getTime()) /
(new Date(queuedSkill.finish_date!).getTime() - new Date(queuedSkill.start_date!).getTime());
// percentage completed
return trainingStartPosition + (1 - trainingStartPosition) * timePosition;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,56 @@
export enum CorporationRoles {
ACCOUNT_TAKE_1 = 'Account_Take_1',
ACCOUNT_TAKE_2 = 'Account_Take_2',
ACCOUNT_TAKE_3 = 'Account_Take_3',
ACCOUNT_TAKE_4 = 'Account_Take_4',
ACCOUNT_TAKE_5 = 'Account_Take_5',
ACCOUNT_TAKE_6 = 'Account_Take_6',
ACCOUNT_TAKE_7 = 'Account_Take_7',
ACCOUNTANT = 'Accountant',
AUDITOR = 'Auditor',
BRAND_MANAGER = 'Brand_Manager',
COMMUNICATIONS_OFFICER = 'Communications_Officer',
CONFIG_EQUIPMENT = 'Config_Equipment',
CONFIG_STARBASE_EQUIPMENT = 'Config_Starbase_Equipment',
CONTAINER_TAKE_1 = 'Container_Take_1',
CONTAINER_TAKE_2 = 'Container_Take_2',
CONTAINER_TAKE_3 = 'Container_Take_3',
CONTAINER_TAKE_4 = 'Container_Take_4',
CONTAINER_TAKE_5 = 'Container_Take_5',
CONTAINER_TAKE_6 = 'Container_Take_6',
CONTAINER_TAKE_7 = 'Container_Take_7',
CONTRACT_MANAGER = 'Contract_Manager',
DELIVERIES_CONTAINER_TAKE = 'Deliveries_Container_Take',
DELIVERIES_QUERY = 'Deliveries_Query',
DELIVERIES_TAKE = 'Deliveries_Take',
DIPLOMAT = 'Diplomat',
DIRECTOR = 'Director',
FACTORY_MANAGER = 'Factory_Manager',
FITTING_MANAGER = 'Fitting_Manager',
HANGAR_QUERY_1 = 'Hangar_Query_1',
HANGAR_QUERY_2 = 'Hangar_Query_2',
HANGAR_QUERY_3 = 'Hangar_Query_3',
HANGAR_QUERY_4 = 'Hangar_Query_4',
HANGAR_QUERY_5 = 'Hangar_Query_5',
HANGAR_QUERY_6 = 'Hangar_Query_6',
HANGAR_QUERY_7 = 'Hangar_Query_7',
HANGAR_TAKE_1 = 'Hangar_Take_1',
HANGAR_TAKE_2 = 'Hangar_Take_2',
HANGAR_TAKE_3 = 'Hangar_Take_3',
HANGAR_TAKE_4 = 'Hangar_Take_4',
HANGAR_TAKE_5 = 'Hangar_Take_5',
HANGAR_TAKE_6 = 'Hangar_Take_6',
HANGAR_TAKE_7 = 'Hangar_Take_7',
JUNIOR_ACCOUNTANT = 'Junior_Accountant',
PERSONNEL_MANAGER = 'Personnel_Manager',
PROJECT_MANAGER = 'Project_Manager',
RENT_FACTORY_FACILITY = 'Rent_Factory_Facility',
RENT_OFFICE = 'Rent_Office',
RENT_RESEARCH_FACILITY = 'Rent_Research_Facility',
SECURITY_OFFICER = 'Security_Officer',
SKILL_PLAN_MANAGER = 'Skill_Plan_Manager',
STARBASE_DEFENSE_OPERATOR = 'Starbase_Defense_Operator',
STARBASE_FUEL_TECHNICIAN = 'Starbase_Fuel_Technician',
STATION_MANAGER = 'Station_Manager',
TRADER = 'Trader',
}

View File

@@ -0,0 +1,128 @@
export enum LocationFlag {
ASSET_SAFETY = 'AssetSafety',
AUTOFIT = 'AutoFit',
BONUS = 'Bonus',
BOOSTER = 'Booster',
BOOSTER_BAY = 'BoosterBay',
CAPSULE = 'Capsule',
CAPSULEER_DELIVERIES = 'CapsuleerDeliveries',
CARGO = 'Cargo',
CORP_DELIVERIES = 'CorpDeliveries',
CORP_SAG1 = 'CorpSAG1',
CORP_SAG2 = 'CorpSAG2',
CORP_SAG3 = 'CorpSAG3',
CORP_SAG4 = 'CorpSAG4',
CORP_SAG5 = 'CorpSAG5',
CORP_SAG6 = 'CorpSAG6',
CORP_SAG7 = 'CorpSAG7',
CORPORATION_GOAL_DELIVERIES = 'CorporationGoalDeliveries',
CORPSE_BAY = 'CorpseBay',
CRATE_LOOT = 'CrateLoot',
DELIVERIES = 'Deliveries',
DRONE_BAY = 'DroneBay',
DUST_BATTLE = 'DustBattle',
DUST_DATABANK = 'DustDatabank',
EXPEDITION_HOLD = 'ExpeditionHold',
FIGHTER_BAY = 'FighterBay',
FIGHTER_TUBE_0 = 'FighterTube0',
FIGHTER_TUBE_1 = 'FighterTube1',
FIGHTER_TUBE_2 = 'FighterTube2',
FIGHTER_TUBE_3 = 'FighterTube3',
FIGHTER_TUBE_4 = 'FighterTube4',
FLEET_HANGAR = 'FleetHangar',
FRIGATE_ESCAPE_BAY = 'FrigateEscapeBay',
HANGAR = 'Hangar',
HANGAR_ALL = 'HangarAll',
HI_SLOT_0 = 'HiSlot0',
HI_SLOT_1 = 'HiSlot1',
HI_SLOT_2 = 'HiSlot2',
HI_SLOT_3 = 'HiSlot3',
HI_SLOT_4 = 'HiSlot4',
HI_SLOT_5 = 'HiSlot5',
HI_SLOT_6 = 'HiSlot6',
HI_SLOT_7 = 'HiSlot7',
HIDDEN_MODIFIERS = 'HiddenModifiers',
IMPLANT = 'Implant',
IMPOUNDED = 'Impounded',
INFRASTRUCTURE_HANGAR = 'InfrastructureHangar',
JUNKYARD_REPROCESSED = 'JunkyardReprocessed',
JUNKYARD_TRASHED = 'JunkyardTrashed',
LO_SLOT_0 = 'LoSlot0',
LO_SLOT_1 = 'LoSlot1',
LO_SLOT_2 = 'LoSlot2',
LO_SLOT_3 = 'LoSlot3',
LO_SLOT_4 = 'LoSlot4',
LO_SLOT_5 = 'LoSlot5',
LO_SLOT_6 = 'LoSlot6',
LO_SLOT_7 = 'LoSlot7',
LOCKED = 'Locked',
MED_SLOT_0 = 'MedSlot0',
MED_SLOT_1 = 'MedSlot1',
MED_SLOT_2 = 'MedSlot2',
MED_SLOT_3 = 'MedSlot3',
MED_SLOT_4 = 'MedSlot4',
MED_SLOT_5 = 'MedSlot5',
MED_SLOT_6 = 'MedSlot6',
MED_SLOT_7 = 'MedSlot7',
MOBILE_DEPOT_HOLD = 'MobileDepotHold',
MOON_MATERIAL_BAY = 'MoonMaterialBay',
OFFICE_FOLDER = 'OfficeFolder',
PILOT = 'Pilot',
PLANET_SURFACE = 'PlanetSurface',
QUAFE_BAY = 'QuafeBay',
QUANTUM_CORE_ROOM = 'QuantumCoreRoom',
REWARD = 'Reward',
RIG_SLOT_0 = 'RigSlot0',
RIG_SLOT_1 = 'RigSlot1',
RIG_SLOT_2 = 'RigSlot2',
RIG_SLOT_3 = 'RigSlot3',
RIG_SLOT_4 = 'RigSlot4',
RIG_SLOT_5 = 'RigSlot5',
RIG_SLOT_6 = 'RigSlot6',
RIG_SLOT_7 = 'RigSlot7',
SECONDARY_STORAGE = 'SecondaryStorage',
SERVICE_SLOT_0 = 'ServiceSlot0',
SERVICE_SLOT_1 = 'ServiceSlot1',
SERVICE_SLOT_2 = 'ServiceSlot2',
SERVICE_SLOT_3 = 'ServiceSlot3',
SERVICE_SLOT_4 = 'ServiceSlot4',
SERVICE_SLOT_5 = 'ServiceSlot5',
SERVICE_SLOT_6 = 'ServiceSlot6',
SERVICE_SLOT_7 = 'ServiceSlot7',
SHIP_HANGAR = 'ShipHangar',
SHIP_OFFLINE = 'ShipOffline',
SKILL = 'Skill',
SKILL_IN_TRAINING = 'SkillInTraining',
SPECIALIZED_AMMO_HOLD = 'SpecializedAmmoHold',
SPECIALIZED_ASTEROID_HOLD = 'SpecializedAsteroidHold',
SPECIALIZED_COMMAND_CENTER_HOLD = 'SpecializedCommandCenterHold',
SPECIALIZED_FUEL_BAY = 'SpecializedFuelBay',
SPECIALIZED_GAS_HOLD = 'SpecializedGasHold',
SPECIALIZED_ICE_HOLD = 'SpecializedIceHold',
SPECIALIZED_INDUSTRIAL_SHIP_HOLD = 'SpecializedIndustrialShipHold',
SPECIALIZED_LARGE_SHIP_HOLD = 'SpecializedLargeShipHold',
SPECIALIZED_MATERIAL_BAY = 'SpecializedMaterialBay',
SPECIALIZED_MEDIUM_SHIP_HOLD = 'SpecializedMediumShipHold',
SPECIALIZED_MINERAL_HOLD = 'SpecializedMineralHold',
SPECIALIZED_ORE_HOLD = 'SpecializedOreHold',
SPECIALIZED_PLANETARY_COMMODITIES_HOLD = 'SpecializedPlanetaryCommoditiesHold',
SPECIALIZED_SALVAGE_HOLD = 'SpecializedSalvageHold',
SPECIALIZED_SHIP_HOLD = 'SpecializedShipHold',
SPECIALIZED_SMALL_SHIP_HOLD = 'SpecializedSmallShipHold',
STRUCTURE_ACTIVE = 'StructureActive',
STRUCTURE_FUEL = 'StructureFuel',
STRUCTURE_INACTIVE = 'StructureInactive',
STRUCTURE_OFFLINE = 'StructureOffline',
SUB_SYSTEM_BAY = 'SubSystemBay',
SUB_SYSTEM_SLOT_0 = 'SubSystemSlot0',
SUB_SYSTEM_SLOT_1 = 'SubSystemSlot1',
SUB_SYSTEM_SLOT_2 = 'SubSystemSlot2',
SUB_SYSTEM_SLOT_3 = 'SubSystemSlot3',
SUB_SYSTEM_SLOT_4 = 'SubSystemSlot4',
SUB_SYSTEM_SLOT_5 = 'SubSystemSlot5',
SUB_SYSTEM_SLOT_6 = 'SubSystemSlot6',
SUB_SYSTEM_SLOT_7 = 'SubSystemSlot7',
UNLOCKED = 'Unlocked',
WALLET = 'Wallet',
WARDROBE = 'Wardrobe',
}

View File

@@ -0,0 +1,249 @@
export enum NotificationType {
ACCEPTED_ALLY = 'AcceptedAlly',
ACCEPTED_SURRENDER = 'AcceptedSurrender',
AGENT_RETIRED_TRIGRAVIAN = 'AgentRetiredTrigravian',
ALL_ANCHORING_MSG = 'AllAnchoringMsg',
ALL_MAINTENANCE_BILL_MSG = 'AllMaintenanceBillMsg',
ALL_STRUC_INVULNERABLE_MSG = 'AllStrucInvulnerableMsg',
ALL_STRUCT_VULNERABLE_MSG = 'AllStructVulnerableMsg',
ALL_WAR_CORP_JOINED_ALLIANCE_MSG = 'AllWarCorpJoinedAllianceMsg',
ALL_WAR_DECLARED_MSG = 'AllWarDeclaredMsg',
ALL_WAR_INVALIDATED_MSG = 'AllWarInvalidatedMsg',
ALL_WAR_RETRACTED_MSG = 'AllWarRetractedMsg',
ALL_WAR_SURRENDER_MSG = 'AllWarSurrenderMsg',
ALLIANCE_CAPITAL_CHANGED = 'AllianceCapitalChanged',
ALLIANCE_WAR_DECLARED_V2 = 'AllianceWarDeclaredV2',
ALLY_CONTRACT_CANCELLED = 'AllyContractCancelled',
ALLY_JOINED_WAR_AGGRESSOR_MSG = 'AllyJoinedWarAggressorMsg',
ALLY_JOINED_WAR_ALLY_MSG = 'AllyJoinedWarAllyMsg',
ALLY_JOINED_WAR_DEFENDER_MSG = 'AllyJoinedWarDefenderMsg',
BATTLE_PUNISH_FRIENDLY_FIRE = 'BattlePunishFriendlyFire',
BILL_OUT_OF_MONEY_MSG = 'BillOutOfMoneyMsg',
BILL_PAID_CORP_ALL_MSG = 'BillPaidCorpAllMsg',
BOUNTY_CLAIM_MSG = 'BountyClaimMsg',
BOUNTY_ESS_SHARED = 'BountyESSShared',
BOUNTY_ESS_TAKEN = 'BountyESSTaken',
BOUNTY_PLACED_ALLIANCE = 'BountyPlacedAlliance',
BOUNTY_PLACED_CHAR = 'BountyPlacedChar',
BOUNTY_PLACED_CORP = 'BountyPlacedCorp',
BOUNTY_YOUR_BOUNTY_CLAIMED = 'BountyYourBountyClaimed',
BUDDY_CONNECT_CONTACT_ADD = 'BuddyConnectContactAdd',
CHAR_APP_ACCEPT_MSG = 'CharAppAcceptMsg',
CHAR_APP_REJECT_MSG = 'CharAppRejectMsg',
CHAR_APP_WITHDRAW_MSG = 'CharAppWithdrawMsg',
CHAR_LEFT_CORP_MSG = 'CharLeftCorpMsg',
CHAR_MEDAL_MSG = 'CharMedalMsg',
CHAR_TERMINATION_MSG = 'CharTerminationMsg',
CLONE_ACTIVATION_MSG = 'CloneActivationMsg',
CLONE_ACTIVATION_MSG2 = 'CloneActivationMsg2',
CLONE_MOVED_MSG = 'CloneMovedMsg',
CLONE_REVOKED_MSG1 = 'CloneRevokedMsg1',
CLONE_REVOKED_MSG2 = 'CloneRevokedMsg2',
COMBAT_OPERATION_FINISHED = 'CombatOperationFinished',
CONTACT_ADD = 'ContactAdd',
CONTACT_EDIT = 'ContactEdit',
CONTAINER_PASSWORD_MSG = 'ContainerPasswordMsg',
CONTRACT_REGION_CHANGED_TO_POCHVEN = 'ContractRegionChangedToPochven',
CORP_ALL_BILL_MSG = 'CorpAllBillMsg',
CORP_APP_ACCEPT_MSG = 'CorpAppAcceptMsg',
CORP_APP_INVITED_MSG = 'CorpAppInvitedMsg',
CORP_APP_NEW_MSG = 'CorpAppNewMsg',
CORP_APP_REJECT_CUSTOM_MSG = 'CorpAppRejectCustomMsg',
CORP_APP_REJECT_MSG = 'CorpAppRejectMsg',
CORP_BECAME_WAR_ELIGIBLE = 'CorpBecameWarEligible',
CORP_DIVIDEND_MSG = 'CorpDividendMsg',
CORP_FRIENDLY_FIRE_DISABLE_TIMER_COMPLETED = 'CorpFriendlyFireDisableTimerCompleted',
CORP_FRIENDLY_FIRE_DISABLE_TIMER_STARTED = 'CorpFriendlyFireDisableTimerStarted',
CORP_FRIENDLY_FIRE_ENABLE_TIMER_COMPLETED = 'CorpFriendlyFireEnableTimerCompleted',
CORP_FRIENDLY_FIRE_ENABLE_TIMER_STARTED = 'CorpFriendlyFireEnableTimerStarted',
CORP_KICKED = 'CorpKicked',
CORP_LIQUIDATION_MSG = 'CorpLiquidationMsg',
CORP_NEW_CEO_MSG = 'CorpNewCEOMsg',
CORP_NEWS_MSG = 'CorpNewsMsg',
CORP_NO_LONGER_WAR_ELIGIBLE = 'CorpNoLongerWarEligible',
CORP_OFFICE_EXPIRATION_MSG = 'CorpOfficeExpirationMsg',
CORP_STRUCT_LOST_MSG = 'CorpStructLostMsg',
CORP_TAX_CHANGE_MSG = 'CorpTaxChangeMsg',
CORP_VOTE_CEO_REVOKED_MSG = 'CorpVoteCEORevokedMsg',
CORP_VOTE_MSG = 'CorpVoteMsg',
CORP_WAR_DECLARED_MSG = 'CorpWarDeclaredMsg',
CORP_WAR_DECLARED_V2 = 'CorpWarDeclaredV2',
CORP_WAR_FIGHTING_LEGAL_MSG = 'CorpWarFightingLegalMsg',
CORP_WAR_INVALIDATED_MSG = 'CorpWarInvalidatedMsg',
CORP_WAR_RETRACTED_MSG = 'CorpWarRetractedMsg',
CORP_WAR_SURRENDER_MSG = 'CorpWarSurrenderMsg',
CORPORATION_GOAL_CLOSED = 'CorporationGoalClosed',
CORPORATION_GOAL_COMPLETED = 'CorporationGoalCompleted',
CORPORATION_GOAL_CREATED = 'CorporationGoalCreated',
CORPORATION_GOAL_EXPIRED = 'CorporationGoalExpired',
CORPORATION_GOAL_LIMIT_REACHED = 'CorporationGoalLimitReached',
CORPORATION_GOAL_NAME_CHANGE = 'CorporationGoalNameChange',
CORPORATION_LEFT = 'CorporationLeft',
CUSTOMS_MSG = 'CustomsMsg',
DAILY_ITEM_REWARD_AUTO_CLAIMED = 'DailyItemRewardAutoClaimed',
DECLARE_WAR = 'DeclareWar',
DISTRICT_ATTACKED = 'DistrictAttacked',
DUST_APP_ACCEPTED_MSG = 'DustAppAcceptedMsg',
ESS_MAIN_BANK_LINK = 'ESSMainBankLink',
ENTOSIS_CAPTURE_STARTED = 'EntosisCaptureStarted',
EXPERT_SYSTEM_EXPIRED = 'ExpertSystemExpired',
EXPERT_SYSTEM_EXPIRY_IMMINENT = 'ExpertSystemExpiryImminent',
FW_ALLIANCE_KICK_MSG = 'FWAllianceKickMsg',
FW_ALLIANCE_WARNING_MSG = 'FWAllianceWarningMsg',
FW_CHAR_KICK_MSG = 'FWCharKickMsg',
FW_CHAR_RANK_GAIN_MSG = 'FWCharRankGainMsg',
FW_CHAR_RANK_LOSS_MSG = 'FWCharRankLossMsg',
FW_CHAR_WARNING_MSG = 'FWCharWarningMsg',
FW_CORP_JOIN_MSG = 'FWCorpJoinMsg',
FW_CORP_KICK_MSG = 'FWCorpKickMsg',
FW_CORP_LEAVE_MSG = 'FWCorpLeaveMsg',
FW_CORP_WARNING_MSG = 'FWCorpWarningMsg',
FAC_WAR_CORP_JOIN_REQUEST_MSG = 'FacWarCorpJoinRequestMsg',
FAC_WAR_CORP_JOIN_WITHDRAW_MSG = 'FacWarCorpJoinWithdrawMsg',
FAC_WAR_CORP_LEAVE_REQUEST_MSG = 'FacWarCorpLeaveRequestMsg',
FAC_WAR_CORP_LEAVE_WITHDRAW_MSG = 'FacWarCorpLeaveWithdrawMsg',
FAC_WAR_DIRECT_ENLISTMENT_REVOKED = 'FacWarDirectEnlistmentRevoked',
FAC_WAR_LP_DISQUALIFIED_EVENT = 'FacWarLPDisqualifiedEvent',
FAC_WAR_LP_DISQUALIFIED_KILL = 'FacWarLPDisqualifiedKill',
FAC_WAR_LP_PAYOUT_EVENT = 'FacWarLPPayoutEvent',
FAC_WAR_LP_PAYOUT_KILL = 'FacWarLPPayoutKill',
FREELANCE_PROJECT_CLOSED = 'FreelanceProjectClosed',
FREELANCE_PROJECT_COMPLETED = 'FreelanceProjectCompleted',
FREELANCE_PROJECT_CREATED = 'FreelanceProjectCreated',
FREELANCE_PROJECT_EXPIRED = 'FreelanceProjectExpired',
FREELANCE_PROJECT_LIMIT_REACHED = 'FreelanceProjectLimitReached',
FREELANCE_PROJECT_PARTICIPANT_KICKED = 'FreelanceProjectParticipantKicked',
GAME_TIME_ADDED = 'GameTimeAdded',
GAME_TIME_RECEIVED = 'GameTimeReceived',
GAME_TIME_SENT = 'GameTimeSent',
GIFT_RECEIVED = 'GiftReceived',
I_HUB_DESTROYED_BY_BILL_FAILURE = 'IHubDestroyedByBillFailure',
INCURSION_COMPLETED_MSG = 'IncursionCompletedMsg',
INDUSTRY_OPERATION_FINISHED = 'IndustryOperationFinished',
INDUSTRY_TEAM_AUCTION_LOST = 'IndustryTeamAuctionLost',
INDUSTRY_TEAM_AUCTION_WON = 'IndustryTeamAuctionWon',
INFRASTRUCTURE_HUB_BILL_ABOUT_TO_EXPIRE = 'InfrastructureHubBillAboutToExpire',
INSURANCE_EXPIRATION_MSG = 'InsuranceExpirationMsg',
INSURANCE_FIRST_SHIP_MSG = 'InsuranceFirstShipMsg',
INSURANCE_INVALIDATED_MSG = 'InsuranceInvalidatedMsg',
INSURANCE_ISSUED_MSG = 'InsuranceIssuedMsg',
INSURANCE_PAYOUT_MSG = 'InsurancePayoutMsg',
INVASION_COMPLETED_MSG = 'InvasionCompletedMsg',
INVASION_SYSTEM_LOGIN = 'InvasionSystemLogin',
INVASION_SYSTEM_START = 'InvasionSystemStart',
JUMP_CLONE_DELETED_MSG1 = 'JumpCloneDeletedMsg1',
JUMP_CLONE_DELETED_MSG2 = 'JumpCloneDeletedMsg2',
KILL_REPORT_FINAL_BLOW = 'KillReportFinalBlow',
KILL_REPORT_VICTIM = 'KillReportVictim',
KILL_RIGHT_AVAILABLE = 'KillRightAvailable',
KILL_RIGHT_AVAILABLE_OPEN = 'KillRightAvailableOpen',
KILL_RIGHT_EARNED = 'KillRightEarned',
KILL_RIGHT_UNAVAILABLE = 'KillRightUnavailable',
KILL_RIGHT_UNAVAILABLE_OPEN = 'KillRightUnavailableOpen',
KILL_RIGHT_USED = 'KillRightUsed',
LP_AUTO_REDEEMED = 'LPAutoRedeemed',
LOCATE_CHAR_MSG = 'LocateCharMsg',
MADE_WAR_MUTUAL = 'MadeWarMutual',
MERC_OFFER_RETRACTED_MSG = 'MercOfferRetractedMsg',
MERC_OFFERED_NEGOTIATION_MSG = 'MercOfferedNegotiationMsg',
MERCENARY_DEN_ATTACKED = 'MercenaryDenAttacked',
MERCENARY_DEN_NEW_MTO = 'MercenaryDenNewMTO',
MERCENARY_DEN_REINFORCED = 'MercenaryDenReinforced',
MISSION_CANCELED_TRIGLAVIAN = 'MissionCanceledTriglavian',
MISSION_OFFER_EXPIRATION_MSG = 'MissionOfferExpirationMsg',
MISSION_TIMEOUT_MSG = 'MissionTimeoutMsg',
MOONMINING_AUTOMATIC_FRACTURE = 'MoonminingAutomaticFracture',
MOONMINING_EXTRACTION_CANCELLED = 'MoonminingExtractionCancelled',
MOONMINING_EXTRACTION_FINISHED = 'MoonminingExtractionFinished',
MOONMINING_EXTRACTION_STARTED = 'MoonminingExtractionStarted',
MOONMINING_LASER_FIRED = 'MoonminingLaserFired',
MUTUAL_WAR_EXPIRED = 'MutualWarExpired',
MUTUAL_WAR_INVITE_ACCEPTED = 'MutualWarInviteAccepted',
MUTUAL_WAR_INVITE_REJECTED = 'MutualWarInviteRejected',
MUTUAL_WAR_INVITE_SENT = 'MutualWarInviteSent',
NPC_STANDINGS_GAINED = 'NPCStandingsGained',
NPC_STANDINGS_LOST = 'NPCStandingsLost',
OFFER_TO_ALLY_RETRACTED = 'OfferToAllyRetracted',
OFFERED_SURRENDER = 'OfferedSurrender',
OFFERED_TO_ALLY = 'OfferedToAlly',
OFFICE_LEASE_CANCELED_INSUFFICIENT_STANDINGS = 'OfficeLeaseCanceledInsufficientStandings',
OLD_LSC_MESSAGES = 'OldLscMessages',
OPERATION_FINISHED = 'OperationFinished',
ORBITAL_ATTACKED = 'OrbitalAttacked',
ORBITAL_REINFORCED = 'OrbitalReinforced',
OWNERSHIP_TRANSFERRED = 'OwnershipTransferred',
RAFFLE_CREATED = 'RaffleCreated',
RAFFLE_EXPIRED = 'RaffleExpired',
RAFFLE_FINISHED = 'RaffleFinished',
REIMBURSEMENT_MSG = 'ReimbursementMsg',
RESEARCH_MISSION_AVAILABLE_MSG = 'ResearchMissionAvailableMsg',
RETRACTS_WAR = 'RetractsWar',
SP_AUTO_REDEEMED = 'SPAutoRedeemed',
SEASONAL_CHALLENGE_COMPLETED = 'SeasonalChallengeCompleted',
SKIN_SEQUENCING_COMPLETED = 'SkinSequencingCompleted',
SKYHOOK_DEPLOYED = 'SkyhookDeployed',
SKYHOOK_DESTROYED = 'SkyhookDestroyed',
SKYHOOK_LOST_SHIELDS = 'SkyhookLostShields',
SKYHOOK_ONLINE = 'SkyhookOnline',
SKYHOOK_UNDER_ATTACK = 'SkyhookUnderAttack',
SOV_ALL_CLAIM_ACQUIRED_MSG = 'SovAllClaimAquiredMsg',
SOV_ALL_CLAIM_LOST_MSG = 'SovAllClaimLostMsg',
SOV_COMMAND_NODE_EVENT_STARTED = 'SovCommandNodeEventStarted',
SOV_CORP_BILL_LATE_MSG = 'SovCorpBillLateMsg',
SOV_CORP_CLAIM_FAIL_MSG = 'SovCorpClaimFailMsg',
SOV_DISRUPTOR_MSG = 'SovDisruptorMsg',
SOV_STATION_ENTERED_FREEPORT = 'SovStationEnteredFreeport',
SOV_STRUCTURE_DESTROYED = 'SovStructureDestroyed',
SOV_STRUCTURE_REINFORCED = 'SovStructureReinforced',
SOV_STRUCTURE_SELF_DESTRUCT_CANCEL = 'SovStructureSelfDestructCancel',
SOV_STRUCTURE_SELF_DESTRUCT_FINISHED = 'SovStructureSelfDestructFinished',
SOV_STRUCTURE_SELF_DESTRUCT_REQUESTED = 'SovStructureSelfDestructRequested',
SOVEREIGNTY_IH_DAMAGE_MSG = 'SovereigntyIHDamageMsg',
SOVEREIGNTY_SBU_DAMAGE_MSG = 'SovereigntySBUDamageMsg',
SOVEREIGNTY_TCU_DAMAGE_MSG = 'SovereigntyTCUDamageMsg',
STATION_AGGRESSION_MSG1 = 'StationAggressionMsg1',
STATION_AGGRESSION_MSG2 = 'StationAggressionMsg2',
STATION_CONQUER_MSG = 'StationConquerMsg',
STATION_SERVICE_DISABLED = 'StationServiceDisabled',
STATION_SERVICE_ENABLED = 'StationServiceEnabled',
STATION_STATE_CHANGE_MSG = 'StationStateChangeMsg',
STORY_LINE_MISSION_AVAILABLE_MSG = 'StoryLineMissionAvailableMsg',
STRUCTURE_ANCHORING = 'StructureAnchoring',
STRUCTURE_COURIER_CONTRACT_CHANGED = 'StructureCourierContractChanged',
STRUCTURE_DESTROYED = 'StructureDestroyed',
STRUCTURE_FUEL_ALERT = 'StructureFuelAlert',
STRUCTURE_IMPENDING_ABANDONMENT_ASSETS_AT_RISK = 'StructureImpendingAbandonmentAssetsAtRisk',
STRUCTURE_ITEMS_DELIVERED = 'StructureItemsDelivered',
STRUCTURE_ITEMS_MOVED_TO_SAFETY = 'StructureItemsMovedToSafety',
STRUCTURE_LOST_ARMOR = 'StructureLostArmor',
STRUCTURE_LOST_SHIELDS = 'StructureLostShields',
STRUCTURE_LOW_REAGENTS_ALERT = 'StructureLowReagentsAlert',
STRUCTURE_NO_REAGENTS_ALERT = 'StructureNoReagentsAlert',
STRUCTURE_ONLINE = 'StructureOnline',
STRUCTURE_PAINT_PURCHASED = 'StructurePaintPurchased',
STRUCTURE_SERVICES_OFFLINE = 'StructureServicesOffline',
STRUCTURE_UNANCHORING = 'StructureUnanchoring',
STRUCTURE_UNDER_ATTACK = 'StructureUnderAttack',
STRUCTURE_WENT_HIGH_POWER = 'StructureWentHighPower',
STRUCTURE_WENT_LOW_POWER = 'StructureWentLowPower',
STRUCTURES_JOBS_CANCELLED = 'StructuresJobsCancelled',
STRUCTURES_JOBS_PAUSED = 'StructuresJobsPaused',
STRUCTURES_REINFORCEMENT_CHANGED = 'StructuresReinforcementChanged',
TOWER_ALERT_MSG = 'TowerAlertMsg',
TOWER_RESOURCE_ALERT_MSG = 'TowerResourceAlertMsg',
TRANSACTION_REVERSAL_MSG = 'TransactionReversalMsg',
TUTORIAL_MSG = 'TutorialMsg',
WAR_ADOPTED = 'WarAdopted',
WAR_ALLY_INHERITED = 'WarAllyInherited',
WAR_ALLY_OFFER_DECLINED_MSG = 'WarAllyOfferDeclinedMsg',
WAR_CONCORD_INVALIDATES = 'WarConcordInvalidates',
WAR_DECLARED = 'WarDeclared',
WAR_ENDED_HQ_SECURITY_DROP = 'WarEndedHqSecurityDrop',
WAR_HQ_REMOVED_FROM_SPACE = 'WarHQRemovedFromSpace',
WAR_INHERITED = 'WarInherited',
WAR_INVALID = 'WarInvalid',
WAR_RETRACTED = 'WarRetracted',
WAR_RETRACTED_BY_CONCORD = 'WarRetractedByConcord',
WAR_SURRENDER_DECLINED_MSG = 'WarSurrenderDeclinedMsg',
WAR_SURRENDER_OFFER_MSG = 'WarSurrenderOfferMsg',
}

View File

@@ -0,0 +1,26 @@
import type { LocationFlag } from './location-flag';
export enum STANDING {
HOSTILE = -10,
UNFRIENDLY = -5,
NEUTRAL = 0,
FRIENDLY = 5,
ALLY = 10,
}
export interface Blueprint {
item_id: number;
location_flag: LocationFlag;
location_id: number;
material_efficiency: number;
quantity: number;
runs: number;
time_efficiency: number;
type_id: number;
}
export interface Icons {
px256x256: string;
px128x128: string;
px64x64: string;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,15 @@
/**
* Simple caching for ESI fetches using bun sqlite key-value store.
*/
import * as kv from '@star-kitten/util/kv';
export const defaultCacheDuration = 1000 * 60 * 30; // 30 minutes
export function set(key: string, data: any, expires: number = Date.now() + defaultCacheDuration) {
kv.setExact(`esi_cache_${key}`, data, expires);
}
export function get<T>(key: string): T | undefined {
const item = kv.get<T>(`esi_cache_${key}`);
return undefined;
}

View File

@@ -0,0 +1,115 @@
import * as cache from './cache';
import { options } from './options';
import { ESI_SCOPE, extractCharacterInfoFromToken, isValidToken, tokenHasScopes } from '@/oauth';
import { ESI_RATE_LIMIT_GROUP, rateLimitedFetch } from './rate-limits';
export const ESI_BASE_URL = 'https://esi.evetech.net';
const compatibility_date = '2026-01-04';
export enum Language {
EN = 'en',
DE = 'de',
FR = 'fr',
JA = 'ja',
RU = 'ru',
ZH = 'zh',
KO = 'ko',
ES = 'es',
}
export interface PublicEsiOptions {
language?: Language;
noCache?: boolean;
cacheDuration?: number; // default 30 minutes
}
export interface EsiOptions extends PublicEsiOptions {
access_token?: string;
}
interface RequestOptions extends Partial<EsiOptions & RequestInit> {
rateLimitGroup?: ESI_RATE_LIMIT_GROUP;
}
export function checkScopesAndGetCharacterId(options: EsiOptions, scope: ESI_SCOPE): number {
if (!tokenHasScopes(options, scope)) {
throw `Required scope '${scope}' not present in token`;
}
const character_id = extractCharacterInfoFromToken(options)?.id;
if (!character_id) {
throw 'Character ID could not be determined from access token';
}
return character_id;
}
export async function esiFetch<T = any>(
path: string,
{
access_token,
method = 'GET',
body,
noCache = false,
cacheDuration,
rateLimitGroup = ESI_RATE_LIMIT_GROUP.DEFAULT,
language = Language.EN,
}: Partial<RequestOptions> = {},
) {
try {
const headers = {
'User-Agent': options.user_agent,
'Accept': 'application/json',
'Accept-Language': language,
'X-Compatibility-Date': compatibility_date,
};
if (access_token && !isValidToken({ access_token })) {
throw 'Invalid or expired EVE tokens provided for ESI request';
}
let character_id: number | undefined;
if (access_token) {
headers['Authorization'] = `Bearer ${access_token}`;
character_id = extractCharacterInfoFromToken({ access_token }).id;
}
const init: RequestInit = {
headers,
method: method,
body: body,
};
const url = new URL(`${ESI_BASE_URL}${path.startsWith('/') ? path : '/' + path}`);
url.searchParams.set('datasource', 'tranquility');
if (!noCache && init.method === 'GET') {
const cached = cache.get<T>(url.href);
if (cached) {
return cached;
}
}
const res = await rateLimitedFetch(() => fetch(url, init), rateLimitGroup, character_id);
const data = await res.json();
if (!res.ok) {
console.error(`ESI request failure at ${path} | ${res.status}:${res.statusText} => ${JSON.stringify(data)}`);
return null;
}
if (init.method === 'GET') {
cache.set(
url.href,
data,
Math.max(
(res.headers.get('expires') && new Date(Number(res.headers.get('expires') || '')).getTime()) || 0,
Date.now() + cacheDuration,
),
);
}
return data as T;
} catch (err) {
console.error(`ESI request failure at ${path} | ${JSON.stringify(err)}`, err);
return null;
}
}

View File

@@ -0,0 +1,32 @@
/**
* EVE Online ESI Authentication Options Module
*
* This module defines and initializes the configuration options required
* for authenticating with EVE Online's ESI API. It reads from environment
* variables by default and allows for custom overrides.
*/
export interface EveAuthOptions {
client_id: string;
client_secret: string;
callback_url: string;
user_agent: string;
}
const CLIENT_ID = process.env.EVE_CLIENT_ID || '';
const CLIENT_SECRET = process.env.EVE_CLIENT_SECRET || '';
const CALLBACK_URL = process.env.EVE_CALLBACK_URL || '';
const USER_AGENT = process.env.ESI_USER_AGENT || '';
export const options: EveAuthOptions = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
callback_url: CALLBACK_URL,
user_agent: USER_AGENT,
};
export function initESI(customOptions: Partial<EveAuthOptions>) {
options.client_id = customOptions.client_id || options.client_id;
options.client_secret = customOptions.client_secret || options.client_secret;
options.callback_url = customOptions.callback_url || options.callback_url;
options.user_agent = customOptions.user_agent || options.user_agent;
}

View File

@@ -0,0 +1,251 @@
/**
* ESI Rate Limiting Module
*
* This module implements rate limiting for EVE Online's ESI API based on predefined
* rate limit groups. Each group has its own limits and windows, and requests are
* tracked to ensure compliance with ESI's rate limiting policies.
*
* ref: https://developers.eveonline.com/docs/services/esi/rate-limiting/
*/
export enum ESI_RATE_LIMIT_GROUP {
UNAUTHENTICATED = 'unauthenticated',
DEFAULT = 'default',
CHAR_DETAIL = 'char-detail',
CHAR_ASSET = 'char-asset',
CHAR_SOCIAL = 'char-social',
CHAR_CONTRACT = 'char-contract',
CHAR_INDUSTRY = 'char-industry',
CHAR_LOCATION = 'char-location',
CHAR_NOTIFICATION = 'char-notification',
CORP_ASSET = 'corp-asset',
CORP_SOCIAL = 'corp-social',
CORP_CONTRACT = 'corp-contract',
CORP_INDUSTRY = 'corp-industry',
CORP_WALLET = 'corp-wallet',
CORP_DETAIL = 'corp-detail',
CORP_MEMBER = 'corp-member',
CORP_PROJECT = 'corp-project',
ALLIANCE_SOCIAL = 'alliance-social',
}
interface RateLimitGroup {
type: ESI_RATE_LIMIT_GROUP;
limit: number; // max tokens per window
window: number; // in seconds
countOnlyErrors?: boolean; // whether to count only non 2xx/3xx responses
}
const ESI_GROUP_LIMITS: Record<ESI_RATE_LIMIT_GROUP, RateLimitGroup> = {
[ESI_RATE_LIMIT_GROUP.UNAUTHENTICATED]: {
type: ESI_RATE_LIMIT_GROUP.UNAUTHENTICATED,
limit: 100, // only counts non 2xx/3xx responses
window: 60 * 1, // 1 minute
countOnlyErrors: true,
},
[ESI_RATE_LIMIT_GROUP.DEFAULT]: {
type: ESI_RATE_LIMIT_GROUP.DEFAULT,
limit: 100, // only counts non 2xx/3xx responses
window: 60 * 1, // 1 minute
countOnlyErrors: true,
},
[ESI_RATE_LIMIT_GROUP.CHAR_DETAIL]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
limit: 600,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CHAR_ASSET]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
limit: 1800,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_ASSET]: {
type: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
limit: 1800,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
limit: 600,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CHAR_INDUSTRY]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_INDUSTRY,
limit: 600,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CHAR_LOCATION]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_LOCATION,
limit: 1200,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CHAR_NOTIFICATION]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_NOTIFICATION,
limit: 15,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.ALLIANCE_SOCIAL]: {
type: ESI_RATE_LIMIT_GROUP.ALLIANCE_SOCIAL,
limit: 300,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_SOCIAL]: {
type: ESI_RATE_LIMIT_GROUP.CORP_SOCIAL,
limit: 300,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT]: {
type: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
limit: 600,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_CONTRACT]: {
type: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
limit: 600,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_INDUSTRY]: {
type: ESI_RATE_LIMIT_GROUP.CORP_INDUSTRY,
limit: 600,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_WALLET]: {
type: ESI_RATE_LIMIT_GROUP.CORP_WALLET,
limit: 300,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_DETAIL]: {
type: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
limit: 300,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_MEMBER]: {
type: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
limit: 300,
window: 60 * 15, // 15 minutes
},
[ESI_RATE_LIMIT_GROUP.CORP_PROJECT]: {
type: ESI_RATE_LIMIT_GROUP.CORP_PROJECT,
limit: 600,
window: 60 * 15, // 15 minutes
},
};
const TOKEN_COST = {
['2XX']: 2,
['3XX']: 1,
['4XX']: 5,
['5XX']: 0,
};
interface LimitBucket {
tokens: number;
uses: { number; timestamp: number }[];
}
// Rate limit buckets to track request timestamps
// keys:
// authentcated routes: ESI_RATE_LIMIT_GROUP:applicationId:characterId
// unauthenticated routes based on the ip address or ip and application id, but we'll just use `unauthenticated` for simplicity
const rateLimitBuckets: Record<string, LimitBucket> = {
[ESI_RATE_LIMIT_GROUP.UNAUTHENTICATED]: {
tokens: 0,
uses: [],
},
};
function canMakeRequest(group: ESI_RATE_LIMIT_GROUP, characterId?: number): boolean {
const now = Date.now();
const limit = ESI_GROUP_LIMITS[group];
const bucketKey = `${group}${characterId ? `:${characterId}` : ''}`;
const bucket = rateLimitBuckets[bucketKey] || rateLimitBuckets[ESI_RATE_LIMIT_GROUP.UNAUTHENTICATED];
if (!bucket) {
rateLimitBuckets[bucketKey] = {
tokens: 0,
uses: [],
};
return true; // no requests made yet
}
// If we have enough tokens, allow the request without further checks
if (bucket.tokens < limit.limit - 5) {
return true;
}
// Check if the most recent request is outside the window
if (bucket.uses.at(-1)?.timestamp < now - limit.window) {
bucket.uses = [];
bucket.tokens = 0;
return true;
}
// Remove timestamps outside the window
while (bucket.uses.length > 0 && bucket.uses[0].timestamp < now - limit.window) {
bucket.uses.shift();
}
bucket.tokens = 0;
bucket.tokens = bucket.uses.reduce((acc, use) => acc + use.number, 0);
return bucket.tokens < limit.limit - 5; // keep a small buffer
}
export async function rateLimitedFetch(
fetchFn: () => Promise<Response>,
group: ESI_RATE_LIMIT_GROUP = ESI_RATE_LIMIT_GROUP.DEFAULT,
characterId?: number,
): Promise<Response> {
if (!canMakeRequest(group, characterId)) {
return new Response('Rate limit exceeded', { status: 429 });
}
const response = await fetchFn();
const limit = ESI_GROUP_LIMITS[group];
const bucket =
rateLimitBuckets[`${group}${characterId ? `:${characterId}` : ''}`] || rateLimitBuckets[ESI_RATE_LIMIT_GROUP.UNAUTHENTICATED];
if (response.status >= 200 && response.status < 300) {
// 2XX
var cost = TOKEN_COST['2XX'];
if (limit.countOnlyErrors) {
return response;
}
bucket.tokens += cost;
bucket.uses.push({ number: cost, timestamp: Date.now() });
} else if (response.status >= 300 && response.status < 400) {
// 3XX
var cost = TOKEN_COST['3XX'];
if (limit.countOnlyErrors) {
return response;
}
bucket.tokens += cost;
bucket.uses.push({ number: cost, timestamp: Date.now() });
} else if (response.status >= 400 && response.status < 500) {
// 4XX
var cost = limit.countOnlyErrors ? 1 : TOKEN_COST['4XX'];
bucket.tokens += cost;
bucket.uses.push({ number: cost, timestamp: Date.now() });
} else {
// 5XX
var cost = limit.countOnlyErrors ? 1 : TOKEN_COST['5XX'];
bucket.tokens += cost;
bucket.uses.push({ number: cost, timestamp: Date.now() });
}
// verify our tokens count matches headers and update accordingly
const remaining = response.headers.get('X-RateLimit-Remaining');
if (remaining) {
const expectedTokens = limit.limit - parseInt(remaining);
if (expectedTokens !== bucket.tokens) {
console.warn(
`Rate limit token count mismatch for group ${group} (characterId: ${characterId || 'N/A'}). Expected ${expectedTokens}, but have ${
bucket.tokens
}. Adjusting.`,
);
const diff = expectedTokens - bucket.tokens;
bucket.tokens = expectedTokens;
bucket.uses.push({ number: diff, timestamp: Date.now() });
}
}
return response;
}

View File

@@ -0,0 +1,57 @@
import { ESI_SCOPE } from '../oauth';
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
// required scope: esi-wallet.read_character_wallet.v1
export function getCharacterWallet(options: EsiOptions): Promise<number> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-wallet.read_character_wallet.v1']);
return esiFetch<number>(`/characters/${character_id}/wallet/`, {
...options,
});
}
export interface WalletTransaction {
client_id: number;
date: string;
is_buy: boolean;
is_personal: boolean;
journal_ref_id: number;
location_id: number;
quantity: number;
transaction_id: number;
type_id: number;
unit_price: number;
}
// required scope: esi-wallet.read_character_wallet.v1
export function getCharacterWalletTransactions(options: EsiOptions, fromId: number): Promise<Partial<WalletTransaction>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-wallet.read_character_wallet.v1']);
return esiFetch<Partial<WalletTransaction>[]>(`/characters/${character_id}/wallet/transactions/`, {
...options,
method: 'POST',
body: JSON.stringify(fromId),
});
}
export interface WalletJournalEntry {
amount: number; // The amount of ISK given or taken from the wallet as a result of the given transaction. Positive when ISK is deposited into the wallet and negative when ISK is withdrawn
balance: number; // Wallet balance after transaction occurred
context_id: number; // And ID that gives extra context to the particualr transaction. Because of legacy reasons the context is completely different per ref_type and means different things. It is also possible to not have a context_id
context_id_type: 'character' | 'corporation' | 'alliance' | 'faction'; // The type of the given context_id if present
date: string; // Date and time of transaction
description: string;
first_party_id: number;
id: number;
reason: string;
ref_type: 'agent' | 'assetSafety' | 'bounty' | 'bountyPrizes' | 'contract' | 'dividend' | 'marketTransaction' | 'other';
second_party_id: number;
tax: number;
tax_receiver_id: number;
}
// required scope: esi-wallet.read_character_wallet.v1
export function getCharacterWalletJournal(options: EsiOptions, page: number = 1): Promise<Partial<WalletJournalEntry>[]> {
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-wallet.read_character_wallet.v1']);
return esiFetch<Partial<WalletJournalEntry>[]>(`/characters/${character_id}/wallet/journal/?page=${page}`, {
...options,
});
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,10 @@
// Get info for a blueprint
GET https://api.everef.net/v1/industry/cost
?blueprint_id=21028
&runs=40
&structure_type_id=35825
&security=NULL_SEC
&rig_id=43891
&rig_id=43892
&copying_cost=0.0168

View File

@@ -0,0 +1,94 @@
import createClient, { type FetchOptions, type FetchResponse } from 'openapi-fetch';
import type { components, paths } from './schema.d';
export * as schema from './schema.d';
const client = createClient<paths>({
baseUrl: 'https://api.everef.net',
});
export function getBlueprintCosts(blueprintTypeId: number): Promise<
FetchResponse<
{
parameters: {
query?: {
input?: components['schemas']['IndustryCostInput'];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['IndustryCost'];
};
};
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['ApiError'];
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['ApiError'];
};
};
};
},
FetchOptions<{
parameters: {
query?: {
input?: components['schemas']['IndustryCostInput'];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['IndustryCost'];
};
};
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['ApiError'];
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['ApiError'];
};
};
};
}>,
`${string}/${string}`
>
> {
return client.GET('/v1/industry/cost', {
query: {
blueprint_type_id: blueprintTypeId,
},
});
}

702
packages/eve/src/everef/schema.d.ts vendored Normal file
View File

@@ -0,0 +1,702 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/v1/industry/cost": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["industryCost"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/search": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Search for inventory types
* @description Search for EVE Online inventory types by name
*/
get: operations["search"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
ApiError: {
message?: string;
};
CopyingCost: {
/** @description The alpha clone tax amount */
alpha_clone_tax?: number;
/** @description The estimated item value (EIV). This may not be completely accurate. */
estimated_item_value?: number;
/** @description The facility amount */
facility_tax?: number;
job_cost_base?: number;
materials?: {
[key: string]: components["schemas"]["MaterialCost"];
};
materials_volume?: number;
/** Format: int64 */
product_id?: number;
product_volume?: number;
/**
* Format: double
* @description The number of runs
*/
runs?: number;
/** @description The SCC surcharge amount */
scc_surcharge?: number;
/** @description Bonuses to system cost from structures, rigs, etc. */
system_cost_bonuses?: number;
/**
* @description The system cost index amount.
* Note that this will always be a slightly off, as the ESI does not report the full precision of the system cost index rates.
* See https://github.com/esi/esi-issues/issues/1411
*/
system_cost_index?: number;
time?: string;
total_cost?: number;
total_cost_per_run?: number;
/** @description The total amount of ISK required to start the job */
total_job_cost?: number;
total_material_cost?: number;
};
IndustryCost: {
copying?: {
[key: string]: components["schemas"]["CopyingCost"];
};
input?: components["schemas"]["IndustryCostInput"];
invention?: {
[key: string]: components["schemas"]["InventionCost"];
};
manufacturing?: {
[key: string]: components["schemas"]["ProductionCost"];
};
reaction?: {
[key: string]: components["schemas"]["ProductionCost"];
};
};
IndustryCostInput: {
/**
* Format: int32
* @description The Advanced Capital Ship Construction skill level the installing character
* @default 5
*/
advanced_capital_ship_construction: number;
/**
* Format: int32
* @description The Advanced Industrial Ship Construction skill level the installing character
* @default 5
*/
advanced_industrial_ship_construction: number;
/**
* Format: int32
* @description The Advanced Industry skill level the installing character
* @default 5
*/
advanced_industry: number;
/**
* Format: int32
* @description The Advanced Large Ship Construction skill level the installing character
* @default 5
*/
advanced_large_ship_construction: number;
/**
* Format: int32
* @description The Advanced Medium Ship Construction skill level the installing character
* @default 5
*/
advanced_medium_ship_construction: number;
/**
* Format: int32
* @description The Advanced Small Ship Construction skill level the installing character
* @default 5
*/
advanced_small_ship_construction: number;
/**
* @description Whether installing character is an alpha clone or not
* @default false
*/
alpha: boolean;
/**
* Format: int32
* @description The Amarr Encryption Methods skill level the installing character
* @default 5
*/
amarr_encryption_methods: number;
/**
* Format: int32
* @description The Amarr Starship Engineering skill level the installing character
* @default 5
*/
amarr_starship_engineering: number;
/**
* Format: int64
* @description The blueprint ID to calculate
*/
blueprint_id?: number;
/**
* Format: int32
* @description The Caldari Encryption Methods skill level the installing character
* @default 5
*/
caldari_encryption_methods: number;
/**
* Format: int32
* @description The Caldari Starship Engineering skill level the installing character
* @default 5
*/
caldari_starship_engineering: number;
/** @description The copying cost index of the system where the job is installed */
copying_cost?: number;
/**
* Format: int32
* @description The Core Subsystem Technology skill level the installing character
* @default 5
*/
core_subsystem_technology: number;
/**
* Format: int64
* @description The decryptor type ID to use
*/
decryptor_id?: number;
/**
* Format: int32
* @description The Defensive Subsystem Technology skill level the installing character
* @default 5
*/
defensive_subsystem_technology: number;
/**
* Format: int32
* @description The Electromagnetic Physics skill level the installing character
* @default 5
*/
electromagnetic_physics: number;
/**
* Format: int32
* @description The Electronic Engineering skill level the installing character
* @default 5
*/
electronic_engineering: number;
/**
* @description The facility tax rate of the station or structure where the job is installed
* @default 0
*/
facility_tax: number;
/**
* Format: int32
* @description The Gallente Encryption Methods skill level the installing character
* @default 5
*/
gallente_encryption_methods: number;
/**
* Format: int32
* @description The Gallente Starship Engineering skill level the installing character
* @default 5
*/
gallente_starship_engineering: number;
/**
* Format: int32
* @description The Graviton Physics skill level the installing character
* @default 5
*/
graviton_physics: number;
/**
* Format: int32
* @description The High Energy Physics skill level the installing character
* @default 5
*/
high_energy_physics: number;
/**
* Format: int32
* @description The Hydromagnetic Physics skill level the installing character
* @default 5
*/
hydromagnetic_physics: number;
/**
* Format: int32
* @description The Industry skill level the installing character
* @default 5
*/
industry: number;
/** @description The invention cost index of the system where the job is installed */
invention_cost?: number;
/**
* Format: int32
* @description The Laser Physics skill level the installing character
* @default 5
*/
laser_physics: number;
/** @description The manufacturing cost index of the system where the job is installed */
manufacturing_cost?: number;
/**
* @description Where to get material prices from
* @default ESI_AVG
* @enum {string}
*/
material_prices: "ESI_AVG" | "FUZZWORK_JITA_SELL_MIN" | "FUZZWORK_JITA_SELL_AVG" | "FUZZWORK_JITA_BUY_MAX" | "FUZZWORK_JITA_BUY_AVG";
/**
* Format: int32
* @description The material efficiency of the blueprint. Defaults to 10 for T1 products or invention output ME to T2 products
*/
me?: number;
/**
* Format: int32
* @description The Mechanical Engineering skill level the installing character
* @default 5
*/
mechanical_engineering: number;
/**
* Format: int32
* @description The Metallurgy skill level the installing character
* @default 5
*/
metallurgy: number;
/**
* Format: int32
* @description The Minmatar Encryption Methods skill level the installing character
* @default 5
*/
minmatar_encryption_methods: number;
/**
* Format: int32
* @description The Minmatar Starship Engineering skill level the installing character
* @default 5
*/
minmatar_starship_engineering: number;
/**
* Format: int32
* @description The Molecular Engineering skill level the installing character
* @default 5
*/
molecular_engineering: number;
/**
* Format: int32
* @description The Mutagenic Stabilization skill level the installing character
* @default 5
*/
mutagenic_stabilization: number;
/**
* Format: int32
* @description The Nanite Engineering skill level the installing character
* @default 5
*/
nanite_engineering: number;
/**
* Format: int32
* @description The Nuclear Physics skill level the installing character
* @default 5
*/
nuclear_physics: number;
/**
* Format: int32
* @description The Offensive Subsystem Technology skill level the installing character
* @default 5
*/
offensive_subsystem_technology: number;
/**
* Format: int32
* @description The Outpost Construction skill level the installing character
* @default 5
*/
outpost_construction: number;
/**
* Format: int32
* @description The Plasma Physics skill level the installing character
* @default 5
*/
plasma_physics: number;
/**
* Format: int64
* @description The desired product type ID
*/
product_id?: number;
/**
* Format: int32
* @description The Propulsion Subsystem Technology skill level the installing character
* @default 5
*/
propulsion_subsystem_technology: number;
/**
* Format: int32
* @description The Quantum Physics skill level the installing character
* @default 5
*/
quantum_physics: number;
/** @description The reaction cost index of the system where the job is installed */
reaction_cost?: number;
/**
* Format: int32
* @description The Reactions skill level the installing character
* @default 5
*/
reactions: number;
/**
* Format: int32
* @description The Research skill level the installing character
* @default 5
*/
research: number;
/** @description The researching material efficiency cost index of the system where the job is installed */
researching_me_cost?: number;
/** @description The researching time efficiency cost index of the system where the job is installed */
researching_te_cost?: number;
/** @description The type IDs of the rigs installed on the sture structure where the job is installed */
rig_id?: number[];
/**
* Format: int32
* @description The Rocket Science skill level the installing character
* @default 5
*/
rocket_science: number;
/**
* Format: int32
* @description The number of runs
* @default 1
*/
runs: number;
/**
* Format: int32
* @description The Science skill level the installing character
* @default 5
*/
science: number;
/**
* @description The security class of the system where the job is installed. If neither security nor system is supplied, high sec is assumed
* @enum {string}
*/
security?: "HIGH_SEC" | "LOW_SEC" | "NULL_SEC";
/**
* Format: int32
* @description The Sleeper Encryption Methods skill level the installing character
* @default 5
*/
sleeper_encryption_methods: number;
/**
* Format: int64
* @description The type ID of the structure where the job is installed. If not set, an NPC station is assumed.
*/
structure_type_id?: number;
/**
* @description Bonus to apply to system cost, such as the faction warfare bonus
* @default 0
* @example -0.5
*/
system_cost_bonus: number;
/**
* Format: int32
* @description The ID of the system where the job is installed. This will resolve security class and cost indices. If neither security nor system is supplied, high sec is assumed
*/
system_id?: number;
/**
* Format: int32
* @description The time efficiency of the blueprint. Defaults to 20 for T1 products or invention output TE to T2 products
*/
te?: number;
/**
* Format: int32
* @description The Triglavian Encryption Methods skill level the installing character
* @default 5
*/
triglavian_encryption_methods: number;
/**
* Format: int32
* @description The Triglavian Quantum Engineering skill level the installing character
* @default 5
*/
triglavian_quantum_engineering: number;
/**
* Format: int32
* @description The Upwell Encryption Methods skill level the installing character
* @default 5
*/
upwell_encryption_methods: number;
/**
* Format: int32
* @description The Upwell Starship Engineering skill level the installing character
* @default 5
*/
upwell_starship_engineering: number;
};
InventionCost: {
/** @description The alpha clone tax amount */
alpha_clone_tax?: number;
avg_cost_per_copy?: number;
avg_cost_per_run?: number;
avg_cost_per_unit?: number;
avg_time_per_copy?: string;
avg_time_per_run?: string;
avg_time_per_unit?: string;
/**
* Format: int64
* @description The source blueprint of the invention
*/
blueprint_id?: number;
/** @description The estimated item value (EIV). This may not be completely accurate. */
estimated_item_value?: number;
/** Format: double */
expected_copies?: number;
/** Format: double */
expected_runs?: number;
/** Format: double */
expected_units?: number;
/** @description The facility amount */
facility_tax?: number;
job_cost_base?: number;
materials?: {
[key: string]: components["schemas"]["MaterialCost"];
};
materials_volume?: number;
/**
* Format: int32
* @description The material efficiency of the invented blueprint
*/
me?: number;
/** Format: double */
probability?: number;
/** Format: int64 */
product_id?: number;
product_volume?: number;
/**
* Format: double
* @description The number of runs
*/
runs?: number;
/**
* Format: int32
* @description The number of runs on each successfully invented copy
*/
runs_per_copy?: number;
/** @description The SCC surcharge amount */
scc_surcharge?: number;
/** @description Bonuses to system cost from structures, rigs, etc. */
system_cost_bonuses?: number;
/**
* @description The system cost index amount.
* Note that this will always be a slightly off, as the ESI does not report the full precision of the system cost index rates.
* See https://github.com/esi/esi-issues/issues/1411
*/
system_cost_index?: number;
/**
* Format: int32
* @description The time efficiency of the invented blueprint
*/
te?: number;
time?: string;
total_cost?: number;
/** @description The total amount of ISK required to start the job */
total_job_cost?: number;
total_material_cost?: number;
/** Format: int32 */
units_per_run?: number;
};
MaterialCost: {
cost?: number;
cost_per_unit?: number;
/** Format: double */
quantity?: number;
/** Format: int64 */
type_id?: number;
volume?: number;
volume_per_unit?: number;
};
ProductionCost: {
/** @description The alpha clone tax amount */
alpha_clone_tax?: number;
/**
* Format: int64
* @description The source blueprint of the manufacture
*/
blueprint_id?: number;
/** @description The estimated item value (EIV). This may not be completely accurate. */
estimated_item_value?: number;
/** @description The facility amount */
facility_tax?: number;
materials?: {
[key: string]: components["schemas"]["MaterialCost"];
};
materials_volume?: number;
/**
* Format: int32
* @description The material efficiency used
*/
me?: number;
/** Format: int64 */
product_id?: number;
product_volume?: number;
/**
* Format: double
* @description The number of runs
*/
runs?: number;
/** @description The SCC surcharge amount */
scc_surcharge?: number;
/** @description Bonuses to system cost from structures, rigs, etc. */
system_cost_bonuses?: number;
/**
* @description The system cost index amount.
* Note that this will always be a slightly off, as the ESI does not report the full precision of the system cost index rates.
* See https://github.com/esi/esi-issues/issues/1411
*/
system_cost_index?: number;
/**
* Format: int32
* @description The time efficiency used
*/
te?: number;
time?: string;
time_per_run?: string;
time_per_unit?: string;
total_cost?: number;
total_cost_per_run?: number;
total_cost_per_unit?: number;
/** @description The total amount of ISK required to start the job */
total_job_cost?: number;
total_material_cost?: number;
/**
* Format: int64
* @description Total number of item produced
*/
units?: number;
/**
* Format: int64
* @description Total number of item produced
*/
units_per_run?: number;
};
SearchEntry: {
/** Format: int64 */
id?: number;
language?: string;
/**
* Format: int64
* @description Relevance score of the search result. Lower is better.
*/
relevance?: number;
title?: string;
/** @enum {string} */
type?: "inventory_type" | "market_group" | "category" | "group";
type_name?: string;
urls?: components["schemas"]["SearchEntryUrls"];
};
SearchEntryUrls: {
everef?: string;
reference_data?: string;
};
SearchResult: {
entries?: components["schemas"]["SearchEntry"][];
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
industryCost: {
parameters: {
query?: {
input?: components["schemas"]["IndustryCostInput"];
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["IndustryCost"];
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
/** @description Server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
search: {
parameters: {
query: {
/** @description Search query (minimum 3 characters) */
q: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SearchResult"];
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
/** @description Server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiError"];
};
};
};
};
}

View File

@@ -0,0 +1,6 @@
export * from './esi';
export * from './db';
export * from './ref';
export * from './third-party';
export * as locales from './locales';
export * from './utils';

View File

View File

View File

@@ -0,0 +1,19 @@
import { schema } from '@/everef';
export type IndustryCostInput = schema.components['schemas']['IndustryCostInput'];
export type GeneralOptions = Pick<IndustryCostInput, 'alpha' | 'runs' | 'product_id' | 'material_prices'>;
export type LocationOptions = Pick<
IndustryCostInput,
'system_id' | 'structure_type_id' | 'rig_id' | 'system_cost_bonus' | 'security' | 'facility_tax'
>;
export type IndexOptions = Pick<
IndustryCostInput,
'researching_me_cost' | 'researching_te_cost' | 'manufacturing_cost' | 'copying_cost' | 'invention_cost' | 'reaction_cost'
>;
export type BlueprintOptions = Pick<IndustryCostInput, 'blueprint_id' | 'copying_cost' | 'me' | 'te' | 'decryptor_id'>;
export type Skills = Omit<IndustryCostInput, keyof BlueprintOptions | keyof IndexOptions | keyof LocationOptions | keyof GeneralOptions>;

View File

@@ -0,0 +1,35 @@
export type Locales = 'en' | 'ru' | 'de' | 'fr' | 'ja' | 'es' | 'zh' | 'ko';
export const ALL_LOCALES: Locales[] = ['en', 'ru', 'de', 'fr', 'ja', 'es', 'zh', 'ko'];
export const DEFAULT_LOCALE: Locales = 'en';
export const LOCALE_NAMES: { [key in Locales]: string } = {
en: 'English',
ru: 'Русский',
de: 'Deutsch',
fr: 'Français',
ja: '日本語',
es: 'Español',
zh: '中文',
ko: '한국어',
};
export function toDiscordLocale(locale: Locales): string {
switch (locale) {
case 'en':
return 'en-US';
case 'ru':
return 'ru';
case 'de':
return 'de';
case 'fr':
return 'fr';
case 'ja':
return 'ja';
case 'es':
return 'es-ES';
case 'zh':
return 'zh-CN';
case 'ko':
return 'ko';
default:
return 'en-US';
}
}

View File

@@ -0,0 +1,130 @@
import type { LocalizedString } from './shared-types';
import { dataSets, loadModels } from './loadModels';
export interface Attribute {
readonly attribute_id: number;
readonly category_id: number;
readonly data_type: number;
readonly default_value: number;
readonly description: LocalizedString;
readonly high_is_good: boolean;
readonly icon_id?: number;
readonly name: string;
readonly published: boolean;
readonly stackable: boolean;
readonly unit_id?: number;
readonly display_name: LocalizedString;
readonly tooltip_title?: LocalizedString;
readonly tooltip_description?: LocalizedString;
}
export const getAttribute = (id: number): Attribute => {
if (!dataSets.loaded) loadModels();
const data = dataSets.dogma_attributes[String(id)];
if (!data) throw new Error(`Attribute ID ${id} not found in reference data`);
return data;
};
export enum CommonAttribute {
// Structure
StructureHitpoints = 9,
CargoCapacity = 38,
DroneCapacity = 283,
DroneBandwidth = 1271,
Mass = 4,
Volume = 161,
InertiaModifier = 70,
StructureEMResistance = 113,
StructureThermalResistance = 110,
StructureKineticResistance = 109,
StructureExplosiveResistance = 111,
// Armor
ArmorHitpoints = 265,
ArmorEMResistance = 267,
ArmorThermalResistance = 270,
ArmorKineticResistance = 269,
ArmorExplosiveResistance = 268,
// Shield
ShieldCapacity = 263,
ShieldRechargeTime = 479,
ShieldEMResistance = 271,
ShieldThermalResistance = 274,
ShieldKineticResistance = 273,
ShieldExplosiveResistance = 272,
// Electronic Resistances
CapacitorWarfareResistance = 2045,
StasisWebifierResistance = 2115,
WeaponDisruptionResistance = 2113,
// Capacitor
CapacitorCapacity = 482,
CapacitorRechargeTime = 55,
// Targeting
MaxTargetRange = 76,
MaxLockedTargets = 192,
SignatureRadius = 552,
ScanResolution = 564,
RadarSensorStrength = 208,
MagnetometricSensorStrength = 210,
GravimetricSensorStrength = 211,
LadarSensorStrength = 209,
// Jump Drive Systems
HasJumpDrive = 861,
JumpDriveCapacitorNeed = 898,
MaxJumpRange = 867,
JumpDriveFuelNeed = 866,
JumpDriveConsumptionAmount = 868,
FuelBayCapacity = 1549,
ConduitJumpConsumptionAmount = 3131,
COnduitJumpPassengerCapacity = 3133,
// Propulsion
MaxVelocity = 37,
WarpSpeed = 600,
// FITTING
// Slots
HighSlots = 14,
MediumSlots = 13,
LowSlots = 12,
// Stats
PowergridOutput = 11,
CPUOutput = 48,
TurretHardpoints = 102,
LauncherHardpoints = 101,
// Rigging
RigSlots = 1137,
RigSize = 1547,
Calibration = 1132,
// Module
CPUUsage = 50,
PowergridUsage = 30,
ActivationCost = 6,
// EWAR
MaxVelocityBonus = 20,
WarpScrambleStrength = 105,
WarpDisruptionStrength = 2425,
WarpDisruptionRange = 103,
// Weapon
DamageMultiplier = 64,
AccuracyFalloff = 158,
OptimalRange = 54,
RateOfFire = 51,
TrackingSpeed = 160,
ReloadTime = 1795,
ActivationTime = 73,
UsedWithCharge1 = 604,
UsedWithCharge2 = 605,
ChargeSize = 128,
}

View File

@@ -0,0 +1,140 @@
import { ActivityType, type TypeIDQuantity } from './shared-types';
import type { Type } from './type';
import { getType } from './type';
import { dataSets, loadModels } from './loadModels';
export interface Activity {
time: number;
}
export interface ManufacturingActivity extends Activity {
time: number;
materials: { [type_id: string]: TypeIDQuantity };
products: { [type_id: string]: TypeIDQuantity };
}
export interface InventionActivity extends Activity {
time: number;
materials: { [type_id: string]: TypeIDQuantity };
products: { [type_id: string]: TypeIDQuantity };
skills: { [skill_type_id: string]: number }; // skill_type_id : level
}
export interface TypeQuantity {
type: Type;
quantity: number;
}
export interface Blueprint {
readonly blueprint_type_id: number;
readonly max_production_limit: number;
readonly activities: {
[ActivityType.MANUFACTURING]?: ManufacturingActivity;
[ActivityType.RESEARCH_MATERIAL]?: Activity;
[ActivityType.RESEARCH_TIME]?: Activity;
[ActivityType.COPYING]?: Activity;
[ActivityType.INVENTION]?: InventionActivity;
};
}
export function getBlueprint(blueprint_type_id: number): Blueprint {
if (!dataSets.loaded) loadModels();
const data = dataSets.blueprints[String(blueprint_type_id)];
if (!data) throw new Error(`Blueprint Type ID ${blueprint_type_id} not found in reference data`);
return data;
}
export function getManufacturingMaterials(blueprint: Blueprint):
| any[]
| Promise<
{
type: Type;
quantity: number;
}[]
> {
const manufacturing = blueprint.activities[ActivityType.MANUFACTURING];
if (!manufacturing) return [];
return Promise.all(
Object.entries(manufacturing.materials).map(([type_id, { quantity }]) => ({
type: getType(parseInt(type_id)),
quantity,
})),
);
}
export function getManufacturingProducts(blueprint: Blueprint):
| any[]
| Promise<
{
type: Type;
quantity: number;
}[]
> {
const manufacturing = blueprint.activities[ActivityType.MANUFACTURING];
if (!manufacturing) return [];
return Promise.all(
Object.entries(manufacturing.products).map(([type_id, { quantity }]) => ({
type: getType(parseInt(type_id)),
quantity,
})),
);
}
export function getInventionMaterials(blueprint: Blueprint):
| any[]
| Promise<
{
type: Type;
quantity: number;
}[]
> {
const invention = blueprint.activities[ActivityType.INVENTION];
if (!invention) return [];
return Promise.all(
Object.entries(invention.materials).map(([type_id, { quantity }]) => ({
type: getType(parseInt(type_id)),
quantity,
})),
);
}
export function getInventionProducts(blueprint: Blueprint):
| any[]
| Promise<
{
type: Type;
quantity: number;
}[]
> {
const invention = blueprint.activities[ActivityType.INVENTION];
if (!invention) return [];
return Promise.all(
Object.entries(invention.products).map(([type_id, { quantity }]) => ({
type: getType(parseInt(type_id)),
quantity,
})),
);
}
export function getInventionSkills(blueprint: Blueprint):
| any[]
| Promise<
{
type: Type;
level: number;
}[]
> {
const invention = blueprint.activities[ActivityType.INVENTION];
if (!invention) return [];
return Promise.all(
Object.entries(invention.skills).map(([skill_type_id, level]) => ({
type: getType(parseInt(skill_type_id)),
level,
})),
);
}

View File

@@ -0,0 +1,35 @@
import type { LocalizedString } from './shared-types';
import { dataSets, loadModels } from './loadModels';
export enum CommonCategory {
CARGO = 5,
SHIP = 6,
MODULE = 7,
CHARGE = 8,
BLUEPRINT = 9,
SKILL = 16,
DRONE = 18,
IMPLANT = 20,
APPAREL = 30,
DEPLOYABLE = 22,
REACTION = 24,
SUBSYSTEM = 32,
STRUCTURE = 65,
STRUCTURE_MODULE = 66,
FIGHTER = 87,
}
export interface Category {
readonly category_id: number;
readonly name: LocalizedString;
readonly published: boolean;
readonly group_ids: number[];
readonly icon_id?: number;
}
export function getCategory(category_id: number): Category {
if (!dataSets.loaded) loadModels();
const data = dataSets.categories[String(category_id)];
if (!data) throw new Error(`Category ID ${category_id} not found in reference data`);
return data;
}

View File

@@ -0,0 +1,65 @@
import { getAttribute, type Attribute } from './attribute';
import type { LocalizedString } from './shared-types';
import { dataSets, loadModels } from './loadModels';
interface Modifier {
domain: number;
func: number;
group_id?: number;
modified_attribute_id: number;
modifying_attribute_id: number;
skill_type_id?: number;
operator: number;
}
export interface Effect {
readonly effect_id: number;
readonly disallow_auto_repeat: boolean;
readonly discharge_attribute_id?: number;
readonly distribution?: number;
readonly duration_attribute_id?: number;
readonly effect_category: number;
readonly effect_name: string;
readonly electronic_chance: boolean;
readonly falloff_attribute_id?: number;
readonly guid: string;
readonly is_assistance: boolean;
readonly is_offensive: boolean;
readonly is_warp_safe: boolean;
readonly propulsion_chance: boolean;
readonly published: boolean;
readonly range_attribute_id?: number;
readonly range_chance: boolean;
readonly modifiers: Modifier[];
readonly tracking_speed_attribute_id?: number;
readonly description: LocalizedString;
readonly display_name: LocalizedString;
readonly name: string;
}
export function getEffect(effect_id: number): Effect {
if (!dataSets.loaded) loadModels();
const data = dataSets.dogma_effects[String(effect_id)];
if (!data) throw new Error(`Effect ID ${effect_id} not found in reference data`);
return data;
}
export function getDischargeAttribute(effect: Effect): Attribute {
return effect.discharge_attribute_id && getAttribute(effect.discharge_attribute_id);
}
export function getFalloffAttribute(effect: Effect): Attribute {
return effect.falloff_attribute_id && getAttribute(effect.falloff_attribute_id);
}
export function getDurationAttribute(effect: Effect): Attribute {
return effect.duration_attribute_id && getAttribute(effect.duration_attribute_id);
}
export function getRangeAttribute(effect: Effect): Attribute {
return effect.range_attribute_id && getAttribute(effect.range_attribute_id);
}
export function getTrackingSpeedAttribute(effect: Effect): Attribute {
return effect.tracking_speed_attribute_id && getAttribute(effect.tracking_speed_attribute_id);
}

View File

@@ -0,0 +1,29 @@
import type { LocalizedString } from './shared-types';
import { dataSets, loadModels } from './loadModels';
export interface Group {
readonly group_id: number;
readonly category_id: number;
readonly name: LocalizedString;
readonly published: boolean;
readonly icon_id?: number;
readonly anchorable: boolean;
readonly anchored: boolean;
readonly fittable_non_singleton: boolean;
readonly use_base_price: boolean;
readonly type_ids?: number[];
}
export function getGroup(group_id: number): Group {
if (!dataSets.loaded) loadModels();
const data = dataSets.groups[String(group_id)];
if (!data) throw new Error(`Group ID ${group_id} not found in reference data`);
return data;
}
export function groupEveRefLink(group_id: number) {
return `https://everef.net/groups/${group_id}`;
}
export function renderGroupEveRefLink(group: Group, locale: string = 'en') {
return `[${group.name[locale] ?? group.name.en}](${groupEveRefLink(group.group_id)})`;
}

View File

@@ -0,0 +1,37 @@
import { dataSets, loadModels } from './loadModels';
export enum IconSize {
SIZE_32 = 32,
SIZE_64 = 64,
SIZE_128 = 128,
SIZE_256 = 256,
SIZE_512 = 512,
}
export interface Icon {
readonly icon_id: number;
readonly description: string;
readonly file: string;
}
export function getIcon(icon_id: number): Icon {
if (!dataSets.loaded) loadModels();
const data = dataSets.icons[String(icon_id)];
if (!data) throw new Error(`Icon ID ${icon_id} not found in reference data`);
return data;
}
export function getIconUrl(
icon_id: Icon,
{
size = IconSize.SIZE_64,
isBp = false,
isBpc = false,
}: {
size?: IconSize;
isBp?: boolean;
isBpc?: boolean;
} = {},
): string {
return `https://images.evetech.net/types/${icon_id}/icon${isBp ? '/bp' : isBpc ? '/bpc' : ''}?size=${size}`;
}

View File

@@ -0,0 +1,14 @@
export * from './attribute';
export * from './blueprint';
export * from './category';
export * from './effect';
export * from './group';
export * from './icon';
export * from './market-group';
export * from './meta-group';
export * from './region';
export * from './schematic';
export * from './skill';
export * from './solar-system';
export * from './type';
export * from './unit';

View File

@@ -0,0 +1,53 @@
import fs from 'node:fs';
import { join } from 'node:path';
import type { Unit } from './unit';
import type { SolarSystem } from './solar-system';
import type { Attribute } from './attribute';
import type { Blueprint } from './blueprint';
import type { Category } from './category';
import type { Effect } from './effect';
import type { Group } from './group';
import type { Icon } from './icon';
import type { MarketGroup } from './market-group';
import type { MetaGroup } from './meta-group';
import type { Region } from './region';
import type { Schematic } from './schematic';
import type { Skill } from './skill';
import type { Type } from './type';
const dataSets = {
loaded: false,
dogma_attributes: {} as Record<string, Attribute>,
blueprints: {} as Record<string, Blueprint>,
categories: {} as Record<string, Category>,
dogma_effects: {} as Record<string, Effect>,
groups: {} as Record<string, Group>,
icons: {} as Record<string, Icon>,
market_groups: {} as Record<string, MarketGroup>,
meta_groups: {} as Record<string, MetaGroup>,
regions: {} as Record<string, Region>,
schematics: {} as Record<string, Schematic>,
skills: {} as Record<string, Skill>,
solar_systems: {} as Record<string, SolarSystem>,
types: {} as Record<string, Type>,
units: {} as Record<string, Unit>,
};
export async function loadModels(): Promise<void> {
dataSets.dogma_attributes = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/dogma_attributes.json')).toString());
dataSets.blueprints = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/blueprints.json')).toString());
dataSets.categories = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/categories.json')).toString());
dataSets.dogma_effects = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/dogma_effects.json')).toString());
dataSets.groups = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/groups.json')).toString());
dataSets.icons = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/icons.json')).toString());
dataSets.market_groups = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/market_groups.json')).toString());
dataSets.meta_groups = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/meta_groups.json')).toString());
dataSets.regions = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/regions.json')).toString());
dataSets.schematics = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/schematics.json')).toString());
dataSets.skills = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/skills.json')).toString());
dataSets.solar_systems = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/solar_systems.json')).toString());
dataSets.types = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/types.json')).toString());
dataSets.units = JSON.parse(fs.readFileSync(join(__dirname, './data/reference-data/units.json')).toString());
dataSets.loaded = true;
}
export { dataSets };

View File

@@ -0,0 +1,24 @@
import type { LocalizedString } from './shared-types';
import { dataSets, loadModels } from './loadModels';
export interface MarketGroup {
readonly market_group_id: number;
readonly parent_group_id: number;
readonly name: LocalizedString;
readonly description: LocalizedString;
readonly child_market_group_ids: number[];
readonly icon_id: number;
readonly has_types: boolean;
}
export function getMarketGroup(market_group_id: number): MarketGroup {
if (!dataSets.loaded) loadModels();
const data = dataSets.market_groups[String(market_group_id)];
if (!data) throw new Error(`Market group ID ${market_group_id} not found in reference data`);
return data;
}
export async function getAllChildMarketGroups(marketGroup: MarketGroup): Promise<MarketGroup[]> {
const children = await Promise.all(marketGroup.child_market_group_ids.map((id) => getMarketGroup(id)));
return children.concat(...(await Promise.all(children.map((child) => getAllChildMarketGroups(child)))));
}

View File

@@ -0,0 +1,17 @@
import type { LocalizedString } from './shared-types';
import { dataSets, loadModels } from './loadModels';
export interface MetaGroup {
readonly meta_group_id: number;
readonly name: LocalizedString;
readonly type_ids: number[];
readonly icon_id?: number;
readonly icon_suffix?: string;
}
export function getMetaGroup(meta_group_id: number): MetaGroup {
if (!dataSets.loaded) loadModels();
const data = dataSets.meta_groups[String(meta_group_id)];
if (!data) throw new Error(`Meta group ID ${meta_group_id} not found in reference data`);
return data;
}

View File

@@ -0,0 +1,24 @@
import type { LocalizedString, Position } from './shared-types';
import { dataSets, loadModels } from './loadModels';
export interface Region {
readonly region_id: number;
readonly center: Position;
readonly description_id: number;
readonly faction_id: number;
readonly max: Position;
readonly min: Position;
readonly name_id: number;
readonly wormhole_class_id?: number;
readonly nebula_id?: number;
readonly universe_id: string;
readonly description: LocalizedString;
readonly name: LocalizedString;
}
export function getRegion(region_id: number): Region {
if (!dataSets.loaded) loadModels();
const data = dataSets.regions[String(region_id)];
if (!data) throw new Error(`Region ID ${region_id} not found in reference data`);
return data;
}

View File

@@ -0,0 +1,43 @@
import type { LocalizedString, TypeIDQuantity } from './shared-types';
import { getType, type Type } from './type';
import { dataSets, loadModels } from './loadModels';
export interface Schematic {
readonly schematic_id: number;
readonly cycle_time: number;
readonly name: LocalizedString;
readonly materials: { [type_id: string]: TypeIDQuantity };
readonly products: { [type_id: string]: TypeIDQuantity };
readonly pin_type_ids: number[];
}
export function getSchematic(schematic_id: number): Schematic {
if (!dataSets.loaded) loadModels();
const data = dataSets.schematics[String(schematic_id)];
if (!data) throw new Error(`Schematic ID ${schematic_id} not found in reference data`);
return data;
}
export function getMaterialQuantities(schematic: Schematic): {
type: Type;
quantity: number;
}[] {
return Object.entries(schematic.materials).map(([type_id, { quantity }]) => ({
type: getType(Number(type_id)),
quantity,
}));
}
export function getProductQuantities(schematic: Schematic): {
type: Type;
quantity: number;
}[] {
return Object.entries(schematic.products).map(([type_id, { quantity }]) => ({
type: getType(Number(type_id)),
quantity,
}));
}
export function getPinTypes(schematic: Schematic): Type[] {
return schematic.pin_type_ids.map(getType);
}

View File

@@ -0,0 +1,56 @@
import type { Type } from './type';
export interface LocalizedString {
de?: string;
en?: string;
es?: string;
fr?: string;
ja?: string;
ko?: string;
ru?: string;
zh?: string;
}
export interface TypeIDQuantity {
type_id: number;
quantity: number;
}
export interface TypeQuantity {
type: Type;
quantity: number;
}
export interface AttributeIDValue {
attribute_id: number;
value: number;
}
export interface EffectIDDefault {
effect_id: number;
is_default: boolean;
}
export interface MaterialIDQuantity {
material_type_id: number;
quantity: number;
}
export interface BlueprintTypeIDActivity {
blueprint_type_id: number;
blueprint_activity: ActivityType;
}
export enum ActivityType {
MANUFACTURING = 'manufacturing',
RESEARCH_MATERIAL = 'research_material',
RESEARCH_TIME = 'research_time',
COPYING = 'copying',
INVENTION = 'invention',
}
export interface Position {
x: number;
y: number;
z: number;
}

View File

@@ -0,0 +1,47 @@
import { type Attribute, getAttribute } from './attribute';
import { dataSets, loadModels } from './loadModels';
export interface Skill {
readonly type_id: number;
readonly primary_dogma_attribute_id: number;
readonly secondary_dogma_attribute_id: number;
readonly primary_character_attribute_id: number;
readonly secondary_character_attribute_id: number;
readonly training_time_multiplier: number;
readonly required_skills?: { [skill_type_id: string]: number }; // skill_type_id : level
}
export function getSkill(type_id: number): Skill {
if (!dataSets.loaded) loadModels();
const data = dataSets.skills[String(type_id)];
if (!data) throw new Error(`Skill ID ${type_id} not found in reference data`);
return data;
}
export function getPrimaryDogmaAttribute(skill: Skill): Attribute {
return getAttribute(skill.primary_dogma_attribute_id);
}
export function getSecondaryDogmaAttribute(skill: Skill): Attribute {
return getAttribute(skill.secondary_dogma_attribute_id);
}
export function getPrimaryCharacterAttribute(skill: Skill): Attribute {
return getAttribute(skill.primary_character_attribute_id);
}
export function getSecondaryCharacterAttribute(skill: Skill): Attribute {
return getAttribute(skill.secondary_character_attribute_id);
}
export function getPrerequisites(skill: Skill): { skill: Skill; level: number }[] {
if (!skill.required_skills) return [];
return Object.entries(skill.required_skills).map(([skill_type_id, level]) => ({
skill: getSkill(parseInt(skill_type_id)),
level,
}));
}
export function skillpointsAtLevel(skill: Skill, level: number): number {
return Math.pow(2, 2.5 * (level - 1)) * 250 * skill.training_time_multiplier;
}

View File

@@ -0,0 +1,42 @@
import { dataSets, loadModels } from './loadModels';
export interface SolarSystem {
readonly regionID: number;
readonly constellationID: number;
readonly solarSystemID: number;
readonly solarSystemName: string;
readonly x: number;
readonly y: number;
readonly z: number;
readonly xMin: number;
readonly xMax: number;
readonly yMin: number;
readonly yMax: number;
readonly zMin: number;
readonly zMax: number;
readonly luminosity: number;
readonly border: boolean;
readonly fringe: boolean;
readonly corridor: boolean;
readonly hub: boolean;
readonly international: boolean;
readonly regional: boolean;
readonly security: number;
readonly factionID: number;
readonly radius: number;
readonly sunTypeID: number;
readonly securityClass: string;
}
export function getSolarSystem(solarSystemID: number): SolarSystem {
if (!dataSets.loaded) loadModels();
const data = dataSets.solar_systems[String(solarSystemID)] as any;
if (!data) throw new Error(`Solar System ID ${solarSystemID} not found in reference data`);
return {
...data,
security: parseFloat(data.security),
radius: parseFloat(data.radius),
sunTypeID: parseInt(data.sun_type_id, 10),
securityClass: data.security_class ?? 'nullsec', // Default to 'nullsec' if security_class is not present
};
}

View File

@@ -0,0 +1,224 @@
import type {
ActivityType,
AttributeIDValue,
BlueprintTypeIDActivity,
EffectIDDefault,
LocalizedString,
MaterialIDQuantity,
} from './shared-types';
import { IconSize } from './icon';
import { getUnit, type Unit } from './unit';
import { CommonAttribute, getAttribute, type Attribute } from './attribute';
import { getGroup, type Group } from './group';
import { getMetaGroup, type MetaGroup } from './meta-group';
import { dataSets, loadModels } from './loadModels';
interface Masteries {
'0': number[];
'1': number[];
'2': number[];
'3': number[];
'4': number[];
}
interface Bonus {
bonus: number;
bonus_text: LocalizedString;
importance: number;
unit_id: number;
}
interface Traits {
misc_bonuses: { [level: string]: Bonus };
role_bonuses: { [level: string]: Bonus };
types: { [skill_type_id: string]: { [order: string]: Bonus } };
}
export interface Type {
readonly type_id: number;
readonly name: LocalizedString;
readonly description: LocalizedString;
readonly published: boolean;
readonly group_id?: number;
readonly base_price?: number;
readonly capacity?: number;
readonly faction_id?: number;
readonly graphic_id?: number;
readonly market_group_id?: number;
readonly mass?: number;
readonly masteries?: Masteries;
readonly meta_group_id?: number;
readonly portion_size?: number;
readonly race_id?: number;
readonly radius?: number;
readonly sof_faction_name?: string;
readonly sound_id?: number;
readonly traits?: Traits;
readonly volume?: number;
readonly dogma_attributes?: {
[attribute_id: string]: AttributeIDValue;
};
readonly dogma_effects?: { [effect_id: string]: EffectIDDefault };
readonly packaged_volume?: number;
readonly type_materials?: { [type_id: string]: MaterialIDQuantity };
readonly required_skills?: { [skill_type_id: string]: number }; // skill_type_id : level
readonly type_variations?: { [meta_group_id: string]: number[] }; // meta_group_id : type_ids[]
readonly produced_by_blueprints?: {
[blueprint_type_id: string]: BlueprintTypeIDActivity;
}; // blueprint_type_id : blueprint_activity
readonly buildable_pin_type_ids?: number[];
readonly is_ore?: boolean;
readonly ore_variations?: { [variant: string]: number }; // variant : type_id
readonly produced_by_schematic_ids?: number[];
readonly used_by_schematic_ids?: number[];
readonly is_blueprint?: boolean;
}
export function getType(type_id: number): Type {
if (!dataSets.loaded) loadModels();
const data = dataSets.types[String(type_id)];
if (!data) throw new Error(`Type ID ${type_id} not found in reference data`);
return data;
}
export function getTypeIconUrl(type: Type, size: IconSize = IconSize.SIZE_64) {
return `https://images.evetech.net/types/${type.type_id}/icon${type.is_blueprint ? '/bp' : ''}?size=${size}`;
}
export function getSkillBonuses(type: Type): {
skill: Type;
bonuses: {
bonus: number;
bonus_text: LocalizedString;
importance: number;
unit: Unit;
}[];
}[] {
if (!type.traits) return [];
const skillBonuses: {
skill: Type;
bonuses: {
bonus: number;
bonus_text: LocalizedString;
importance: number;
unit: Unit;
}[];
}[] = [];
for (const skill_type_id in type.traits.types) {
skillBonuses.push({
skill: getType(Number(skill_type_id)),
bonuses: Object.keys(type.traits.types[skill_type_id]).map((order) => {
const bonus = type.traits!.types[skill_type_id][order];
return {
bonus: bonus.bonus,
bonus_text: bonus.bonus_text,
importance: bonus.importance,
unit: getUnit(bonus.unit_id),
};
}),
});
}
return skillBonuses;
}
export function getRoleBonuses(type: Type): {
bonus: number;
bonus_text: LocalizedString;
importance: number;
unit: Unit;
}[] {
if (!type.traits || !type.traits.role_bonuses) return [];
return Object.values(type.traits.role_bonuses).map((bonus) => ({
bonus: bonus.bonus,
bonus_text: bonus.bonus_text,
importance: bonus.importance,
unit: bonus.unit_id ? getUnit(bonus.unit_id) : undefined,
}));
}
export function eveRefLink(type_id: number) {
return `https://everef.net/types/${type_id}`;
}
export function renderTypeEveRefLink(type: Type, locale: string = 'en') {
return `[${type.name[locale] ?? type.name.en}](${eveRefLink(type.type_id)})`;
}
export function eveTycoonLink(type_id: number) {
return `https://evetycoon.com/market/${type_id}`;
}
export function getTypeAttributes(type: Type): any[] {
if (!type.dogma_attributes) return [];
Object.keys(type.dogma_attributes).map((attribute_id) => ({
attribute: getAttribute(Number(attribute_id)),
value: type.dogma_attributes![attribute_id].value,
}));
}
export function typeHasAnyAttribute(type: Type, attribute_ids: CommonAttribute[]): boolean {
if (!type.dogma_attributes) return false;
for (const attribute_id of attribute_ids) {
if (type.dogma_attributes[attribute_id]) return true;
}
return false;
}
export function getTypeSkills(type: Type): {
skill: Type;
level: number;
}[] {
if (!type.required_skills) return [];
return Object.keys(type.required_skills).map((skill_type_id) => ({
skill: getType(Number(skill_type_id)),
level: type.required_skills![skill_type_id],
}));
}
export function typeGetAttribute(
type: Type,
attribute_id: number,
): {
attribute: Attribute;
value: number;
} {
if (!type.dogma_attributes || !type.dogma_attributes[attribute_id]) return null;
return {
attribute: getAttribute(attribute_id),
value: type.dogma_attributes[attribute_id].value,
};
}
export function getTypeBlueprints(type: Type): {
blueprint: Type;
activity: ActivityType;
}[] {
if (!type.produced_by_blueprints) return [];
return Object.values(type.produced_by_blueprints).map((blueprint) => ({
blueprint: getType(blueprint.blueprint_type_id),
activity: blueprint.blueprint_activity,
}));
}
export function getTypeSchematics(type: Type): Type[] {
return type.produced_by_schematic_ids?.map((schematic_id) => getType(schematic_id)) ?? [];
}
export function getTypeGroup(type: Type): Group {
if (!type.group_id) return null;
return getGroup(type.group_id);
}
export function getTypeVariants(type: Type): {
metaGroup: MetaGroup;
types: Type[];
}[] {
return Object.entries(type.type_variations || {}).map(([meta_group_id, variant_ids]) => ({
metaGroup: getMetaGroup(Number(meta_group_id)),
types: variant_ids.map((type_id) => getType(type_id)),
}));
}
export function typeHasAttributes(type: Type): boolean {
return type.dogma_attributes && Object.keys(type.dogma_attributes).length > 0;
}

View File

@@ -0,0 +1,66 @@
import { convertMillisecondsToTimeString, convertSecondsToTimeString } from '@/utils/markdown';
import { getGroup, renderGroupEveRefLink } from './group';
import type { LocalizedString } from './shared-types';
import { getType, renderTypeEveRefLink } from './type';
import { dataSets, loadModels } from './loadModels';
const sizeMap = {
1: 'Small',
2: 'Medium',
3: 'Large',
4: 'X-Large',
};
export interface Unit {
readonly unit_id: number;
readonly display_name: string;
readonly description: LocalizedString;
readonly name: LocalizedString;
}
export function getUnit(unit_id: number): Unit {
if (!dataSets.loaded) loadModels();
const unit = dataSets.units[String(unit_id)];
if (!unit) throw new Error(`Unit ID ${unit_id} not found in reference data`);
return unit;
}
export function renderUnit(unit: Unit, value: number, locale: string = 'en'): string {
switch (unit.unit_id) {
case 108: // inverse percentage
case 111: // Inverse percentage
return [(1 - value).toFixed(2), unit.display_name ?? ''].join(' ');
case 3: // seconds
return `${convertSecondsToTimeString(value)}`;
case 101: // milliseconds
return `${convertMillisecondsToTimeString(value)}`;
case 117: // size class
return sizeMap[value] ?? 'Unknown';
case 141: // hardpoints
return value + '';
case 120: // calibration
return value + ' pts';
case 116: // typeID
return getType(value).name[locale] ?? 'Unknown';
case 10: // m/s
return `${value} m/s`;
case 11: // meters per second squared
return `${value} m/s²`;
case 9: // cubic meters
return `${value}`;
case 8: // square meters
return `${value}`;
case 12: // reciprocal meters
return `${value} m⁻¹`;
case 128: // megabits per second
return `${value} Mbps`;
case 115: // groupID
return renderGroupEveRefLink(getGroup(value), locale) ?? 'Unknown';
default:
return [value, unit.display_name ?? ''].join(' ');
}
}
export function isUnitInversePercentage(unit: Unit): boolean {
return unit.unit_id == 108 || unit.unit_id == 111;
}

View File

@@ -0,0 +1,240 @@
import { Elysia, redirect } from 'elysia';
import { eveAuthPlugin, extractCharacterInfoFromToken, extractScopesFromToken, refresh } from '@/oauth';
import { ESI_SCOPE, type eveAuthDb } from './auth.types';
export interface EVEAuthAPIOptions {
clientId: string;
clientSecret: string;
callbackUrl: string;
database: eveAuthDb;
basePath?: string;
defaultScopes?: ESI_SCOPE | ESI_SCOPE[];
scopeSets?: { [key: string]: ESI_SCOPE[] };
responsePage?: {
success: string;
error: string;
};
}
/**
* EVE Online OAuth API for Elysia framework.
* Provides endpoints to handle OAuth flow and manage character scopes.
*/
export function eveAuthAPI(options: EVEAuthAPIOptions) {
const { clientId, clientSecret, callbackUrl, database, basePath = '/auth', scopeSets } = options;
const successPage = options.responsePage?.success || 'EVE OAuth successful! You can close this window.';
const errorPage = options.responsePage?.error || 'EVE OAuth failed. Please try again.';
const defaultScopes = options.defaultScopes
? Array.isArray(options.defaultScopes)
? options.defaultScopes
: [options.defaultScopes]
: [ESI_SCOPE.publicData];
return (
new Elysia()
.use(eveAuthPlugin({ clientId, clientSecret, callbackUrl }))
// Display success page
.get(`${basePath}/success`, () => successPage)
// Display error page
.get(`${basePath}/error`, () => errorPage)
// OAuth callback
.get(`${basePath}/callback`, async ({ cookie, redirect, eveAuthValidateRequest }) => {
try {
const tokens = await eveAuthValidateRequest();
const { id: characterId, name: characterName } = extractCharacterInfoFromToken(tokens);
if (cookie.characterId && cookie.characterId.value !== characterId) {
throw 'Character ID mismatch';
}
const clientId = cookie.clientId?.value as string;
if (!clientId) {
throw 'Missing Client ID cookie';
}
let user = await database.getUserByClientId(clientId);
if (!user) {
// create user if not exists
if (!(user = await database.createUser(clientId))) {
throw 'Failed to create user';
}
}
let character = await database.getCharacterByUserAndCharacterId(user.id, characterId);
if (!character) {
// create character if not exists
if (!(character = await database.createCharacter(user.id, characterId, characterName, tokens))) {
throw 'Failed to create character';
}
} else {
// update tokens if character exists
if (!(await database.updateCharacterTokens(character.id, tokens))) {
throw 'Failed to update character tokens';
}
}
if (!user.main_character_id) {
// set main character if not set, but do not error if it fails
await database.setCharacterAsMain(user.id, characterId);
}
cookie.userId.set({ value: user.id, maxAge: 30 * 24 * 60 * 60, path: '/' }); // 30 days
return redirect(`${basePath}/success`, 302);
} catch (error) {
console.error(`EVE Discord OAuth callback failed`, error);
return redirect(`${basePath}/error`, 302);
} finally {
cookie.characterId?.remove();
cookie.clientId?.remove();
}
})
// Initiates OAuth with default scopes
.get(`${basePath}/clientId/:clientId`, async ({ params, eveAuthRedirect, cookie }) => {
const clientId = params.clientId;
cookie.clientId.set({ value: clientId, maxAge: 600, path: '/' }); // 10 minutes
return eveAuthRedirect(defaultScopes);
})
// Adds scopes by merging existing scopes with the specified scopes
.get(`${basePath}/clientId/:clientId/addScopes/:characterId/:scopes`, async ({ params, eveAuthRedirect, cookie }) => {
const clientId = params.clientId;
const characterId = parseInt(params.characterId);
const scopes = params.scopes.split(',');
const user = await database.getUserByClientId(clientId);
if (!user) {
throw 'User not found';
}
const character = await database.getCharacterByUserAndCharacterId(user.id, characterId);
if (!character) {
throw 'Character not found';
}
const existingScopes = extractScopesFromToken({ access_token: character.access_token });
const mergedScopes = Array.from(new Set([...existingScopes, ...scopes])) as ESI_SCOPE[];
cookie.clientId.set({ value: clientId, maxAge: 600, path: '/' }); // 10 minutes
cookie.characterId.set({ value: characterId, maxAge: 600, path: '/' }); // 10 minutes
return eveAuthRedirect(mergedScopes);
})
// Add scope set to character by merging existing scopes with the set
.get(`${basePath}/clientId/:clientId/addSet/:characterId/:set`, async ({ params, eveAuthRedirect, cookie }) => {
const clientId = params.clientId;
const characterId = parseInt(params.characterId);
const setName = params.set;
const scopes = scopeSets?.[setName];
if (!scopes) {
throw `Scope set "${setName}" not found`;
}
const user = await database.getUserByClientId(clientId);
if (!user) {
throw 'User not found';
}
const character = await database.getCharacterByUserAndCharacterId(user.id, characterId);
if (!character) {
throw 'Character not found';
}
const existingScopes = extractScopesFromToken({ access_token: character.access_token });
const mergedScopes = Array.from(new Set([...existingScopes, ...scopes]));
cookie.clientId.set({ value: clientId, maxAge: 600, path: '/' }); // 10 minutes
cookie.characterId.set({ value: characterId, maxAge: 600, path: '/' }); // 10 minutes
return eveAuthRedirect(mergedScopes);
})
// Replaces scopes by requesting only the specified scopes
.get(`${basePath}/clientId/:clientId/setScopes/:characterId/:scopes`, async ({ params, eveAuthRedirect, cookie }) => {
const clientId = params.clientId;
const characterId = parseInt(params.characterId);
const scopes = params.scopes.split(',') as ESI_SCOPE[];
cookie.clientId.set({ value: clientId, maxAge: 600, path: '/' }); // 10 minutes
cookie.characterId.set({ value: characterId, maxAge: 600, path: '/' }); // 10 minutes
return eveAuthRedirect(scopes);
})
// remove scopes by refreshing tokens without the specified scopes
.get(`${basePath}/clientId/:clientId/remScopes/:characterId/:scopes`, async ({ params, eveAuthRedirect, cookie }) => {
const clientId = params.clientId;
const characterId = parseInt(params.characterId);
const scopes = params.scopes.split(',');
const user = await database.getUserByClientId(clientId);
if (!user) {
throw 'User not found';
}
const character = await database.getCharacterByUserAndCharacterId(user.id, characterId);
if (!character) {
throw 'Character not found';
}
const existingScopes = extractScopesFromToken({ access_token: character.access_token });
const mergedScopes = existingScopes.filter((scope) => !scopes.includes(scope));
const refreshedTokens = await refresh(
{ refresh_token: character.refresh_token },
{
clientId,
clientSecret,
scopes: mergedScopes,
},
);
if (!refreshedTokens) {
throw 'Failed to refresh tokens with removed scopes';
}
if (!(await database.updateCharacterTokens(character.id, refreshedTokens))) {
throw 'Failed to update character tokens after removing scopes';
}
return redirect(`${basePath}/success`, 302);
})
// Remove scope set from character by refreshing tokens without the specified scopes
.get(`${basePath}/clientId/:clientId/remSet/:characterId/:set`, async ({ params, eveAuthRedirect, cookie }) => {
const clientId = params.clientId;
const characterId = parseInt(params.characterId);
const setName = params.set;
const scopes = scopeSets?.[setName];
if (!scopes) {
throw `Scope set "${setName}" not found`;
}
const user = await database.getUserByClientId(clientId);
if (!user) {
throw 'User not found';
}
const character = await database.getCharacterByUserAndCharacterId(user.id, characterId);
if (!character) {
throw 'Character not found';
}
const existingScopes = extractScopesFromToken({ access_token: character.access_token });
const mergedScopes = existingScopes.filter((scope) => !scopes.includes(scope));
const refreshedTokens = await refresh(
{ refresh_token: character.refresh_token },
{
clientId,
clientSecret,
scopes: mergedScopes,
},
);
if (!refreshedTokens) {
throw 'Failed to refresh tokens with removed scopes';
}
if (!(await database.updateCharacterTokens(character.id, refreshedTokens))) {
throw 'Failed to update character tokens after removing scopes';
}
return redirect(`${basePath}/success`, 302);
})
);
}

View File

@@ -0,0 +1,126 @@
/**
* Minimal database interface for EVE Online OAuth integration.
* Implement this interface to manage users and their associated characters.
*/
export const EVE_JWKS_URL = 'https://login.eveonline.com/oauth/jwks';
export const EVE_ISSUER = 'login.eveonline.com';
export const EVE_AUDIENCE = 'eveonline';
export const DATA_SOURCE = 'tranquility';
export interface EveTokens {
access_token: string;
expires_in: number;
refresh_token: string;
}
export interface User {
id: number;
main_character_id?: number; // references Character.id
}
export interface Character {
id: number;
access_token: string;
refresh_token: string;
}
export interface eveAuthDb {
createUser(client_id: string): Promise<User>;
createCharacter(userId: number, characterId: number, characterName: string, tokens: EveTokens): Promise<Character>;
updateCharacterTokens(id: number, tokens: EveTokens): Promise<boolean>;
getUserByClientId(client_id: string): Promise<User | null>;
getCharacterByUserAndCharacterId(userId: number, characterId: number): Promise<Character | null>;
setCharacterAsMain(userId: number, characterId: number): Promise<boolean>;
}
export interface PluginOptions {
clientId: string;
clientSecret: string;
callbackUrl: string;
}
export interface AuthOptions extends PluginOptions {
scopes?: ESI_SCOPE | ESI_SCOPE[];
}
export interface ApiOptions extends PluginOptions {
database: eveAuthDb;
basePath?: string;
defaultScopes?: ESI_SCOPE | ESI_SCOPE[];
scopeSets?: { [key: string]: ESI_SCOPE[] };
responsePage?: {
success: string;
error: string;
};
}
export enum ESI_SCOPE {
'publicData' = 'publicData',
'esi-alliances.read_contacts.v1' = 'esi-alliances.read_contacts.v1',
'esi-assets.read_assets.v1' = 'esi-assets.read_assets.v1',
'esi-assets.read_corporation_assets.v1' = 'esi-assets.read_corporation_assets.v1',
'esi-calendar.read_calendar_events.v1' = 'esi-calendar.read_calendar_events.v1',
'esi-calendar.respond_calendar_events.v1' = 'esi-calendar.respond_calendar_events.v1',
'esi-characters.read_agents_research.v1' = 'esi-characters.read_agents_research.v1',
'esi-characters.read_blueprints.v1' = 'esi-characters.read_blueprints.v1',
'esi-characters.read_contacts.v1' = 'esi-characters.read_contacts.v1',
'esi-characters.read_corporation_roles.v1' = 'esi-characters.read_corporation_roles.v1',
'esi-characters.read_fatigue.v1' = 'esi-characters.read_fatigue.v1',
'esi-characters.read_freelance_jobs.v1' = 'esi-characters.read_freelance_jobs.v1',
'esi-characters.read_fw_stats.v1' = 'esi-characters.read_fw_stats.v1',
'esi-characters.read_loyalty.v1' = 'esi-characters.read_loyalty.v1',
'esi-characters.read_medals.v1' = 'esi-characters.read_medals.v1',
'esi-characters.read_notifications.v1' = 'esi-characters.read_notifications.v1',
'esi-characters.read_standings.v1' = 'esi-characters.read_standings.v1',
'esi-characters.read_titles.v1' = 'esi-characters.read_titles.v1',
'esi-characters.write_contacts.v1' = 'esi-characters.write_contacts.v1',
'esi-clones.read_clones.v1' = 'esi-clones.read_clones.v1',
'esi-clones.read_implants.v1' = 'esi-clones.read_implants.v1',
'esi-contracts.read_character_contracts.v1' = 'esi-contracts.read_character_contracts.v1',
'esi-contracts.read_corporation_contracts.v1' = 'esi-contracts.read_corporation_contracts.v1',
'esi-corporations.read_blueprints.v1' = 'esi-corporations.read_blueprints.v1',
'esi-corporations.read_contacts.v1' = 'esi-corporations.read_contacts.v1',
'esi-corporations.read_container_logs.v1' = 'esi-corporations.read_container_logs.v1',
'esi-corporations.read_corporation_membership.v1' = 'esi-corporations.read_corporation_membership.v1',
'esi-corporations.read_divisions.v1' = 'esi-corporations.read_divisions.v1',
'esi-corporations.read_facilities.v1' = 'esi-corporations.read_facilities.v1',
'esi-corporations.read_freelance_jobs.v1' = 'esi-corporations.read_freelance_jobs.v1',
'esi-corporations.read_fw_stats.v1' = 'esi-corporations.read_fw_stats.v1',
'esi-corporations.read_medals.v1' = 'esi-corporations.read_medals.v1',
'esi-corporations.read_projects.v1' = 'esi-corporations.read_projects.v1',
'esi-corporations.read_standings.v1' = 'esi-corporations.read_standings.v1',
'esi-corporations.read_starbases.v1' = 'esi-corporations.read_starbases.v1',
'esi-corporations.read_structures.v1' = 'esi-corporations.read_structures.v1',
'esi-corporations.read_titles.v1' = 'esi-corporations.read_titles.v1',
'esi-corporations.track_members.v1' = 'esi-corporations.track_members.v1',
'esi-fittings.read_fittings.v1' = 'esi-fittings.read_fittings.v1',
'esi-fittings.write_fittings.v1' = 'esi-fittings.write_fittings.v1',
'esi-fleets.read_fleet.v1' = 'esi-fleets.read_fleet.v1',
'esi-fleets.write_fleet.v1' = 'esi-fleets.write_fleet.v1',
'esi-industry.read_character_jobs.v1' = 'esi-industry.read_character_jobs.v1',
'esi-industry.read_character_mining.v1' = 'esi-industry.read_character_mining.v1',
'esi-industry.read_corporation_jobs.v1' = 'esi-industry.read_corporation_jobs.v1',
'esi-industry.read_corporation_mining.v1' = 'esi-industry.read_corporation_mining.v1',
'esi-killmails.read_corporation_killmails.v1' = 'esi-killmails.read_corporation_killmails.v1',
'esi-killmails.read_killmails.v1' = 'esi-killmails.read_killmails.v1',
'esi-location.read_location.v1' = 'esi-location.read_location.v1',
'esi-location.read_online.v1' = 'esi-location.read_online.v1',
'esi-location.read_ship_type.v1' = 'esi-location.read_ship_type.v1',
'esi-mail.organize_mail.v1' = 'esi-mail.organize_mail.v1',
'esi-mail.read_mail.v1' = 'esi-mail.read_mail.v1',
'esi-mail.send_mail.v1' = 'esi-mail.send_mail.v1',
'esi-markets.read_character_orders.v1' = 'esi-markets.read_character_orders.v1',
'esi-markets.read_corporation_orders.v1' = 'esi-markets.read_corporation_orders.v1',
'esi-markets.structure_markets.v1' = 'esi-markets.structure_markets.v1',
'esi-planets.manage_planets.v1' = 'esi-planets.manage_planets.v1',
'esi-planets.read_customs_offices.v1' = 'esi-planets.read_customs_offices.v1',
'esi-search.search_structures.v1' = 'esi-search.search_structures.v1',
'esi-skills.read_skillqueue.v1' = 'esi-skills.read_skillqueue.v1',
'esi-skills.read_skills.v1' = 'esi-skills.read_skills.v1',
'esi-ui.open_window.v1' = 'esi-ui.open_window.v1',
'esi-ui.write_waypoint.v1' = 'esi-ui.write_waypoint.v1',
'esi-universe.read_structures.v1' = 'esi-universe.read_structures.v1',
'esi-wallet.read_character_wallet.v1' = 'esi-wallet.read_character_wallet.v1',
'esi-wallet.read_corporation_wallets.v1' = 'esi-wallet.read_corporation_wallets.v1',
}

View File

@@ -0,0 +1,96 @@
import * as oauth from '@star-kitten/util/oauth';
import jwkToPem from 'jwk-to-pem';
import { jwtDecode } from 'jwt-decode';
import type { AuthOptions, ESI_SCOPE, EveTokens } from './auth.types';
const EVE_OAUTH_BASE_URL = 'https://login.eveonline.com';
export async function createAuthorizationURL(options: AuthOptions) {
return oauth.createAuthorizationURL(
`${EVE_OAUTH_BASE_URL}/v2/oauth/authorize/`,
options.callbackUrl,
options.clientId,
options.scopes || 'publicData',
);
}
export async function validateCode(clientId: string, clientSecret: string, code: string): Promise<EveTokens> {
try {
return oauth.validateCode<EveTokens>(code, {
clientId: clientId,
url: `${EVE_OAUTH_BASE_URL}/v2/oauth/token`,
auth: {
clientSecret: clientSecret,
type: 'basic',
},
});
} catch (error) {
console.error(`failed to validate EVE authorization code`, error);
return null;
}
}
let eveAuthPublicKey: any; // cache the public key for EVE Online's OAuth2 provider
export async function verify(token: string) {
if (!eveAuthPublicKey) {
try {
const eveJWKS = (await (await fetch(`${EVE_OAUTH_BASE_URL}/oauth/jwks`)).json()) as { keys: any[] };
eveAuthPublicKey = jwkToPem(eveJWKS.keys[0]);
} catch (err) {
console.error(`failed to get EVE Auth public keys`, err);
return;
}
}
return oauth.verify(token, eveAuthPublicKey);
}
export async function refresh(
{ refresh_token }: { refresh_token: string },
{ clientId, clientSecret, scopes }: { clientId: string; clientSecret: string; scopes?: ESI_SCOPE | ESI_SCOPE[] },
): Promise<EveTokens> {
try {
const options = {
clientId: clientId,
url: `${EVE_OAUTH_BASE_URL}/v2/oauth/token`,
scope: scopes,
auth: {
clientSecret: clientSecret,
type: 'basic' as const,
},
};
return oauth.refresh<EveTokens>(refresh_token, options);
} catch (error) {
console.error(`failed to refresh EVE tokens`, error);
return null;
}
}
export function extractCharacterInfoFromToken({ access_token }: { access_token?: string }) {
if (!access_token) return undefined;
const payload = jwtDecode<any>(access_token);
return {
id: parseInt(payload.sub!.split(':')[2]),
name: payload.name,
};
}
export function extractScopesFromToken({ access_token }: { access_token?: string }) {
if (!access_token) return [];
const payload = jwtDecode<any>(access_token);
return payload.scp as ESI_SCOPE[];
}
export function isValidToken({ access_token }: { access_token?: string }) {
if (!access_token) return false;
const payload = jwtDecode<any>(access_token);
const now = Math.floor(Date.now() / 1000);
return payload.exp > now;
}
export function tokenHasScopes({ access_token }: { access_token?: string }, scopes: ESI_SCOPE | ESI_SCOPE[]) {
if (!access_token) return false;
const tokenScopes = extractScopesFromToken({ access_token });
const requiredScopes = Array.isArray(scopes) ? scopes : [scopes];
return requiredScopes.every((scope) => tokenScopes.includes(scope));
}

View File

@@ -0,0 +1,4 @@
export * from './eve-auth';
export * from './plugin';
export * from './api';
export * from './auth.types';

View File

@@ -0,0 +1,52 @@
import { Elysia } from 'elysia';
import { createAuthorizationURL, validateCode, verify } from './eve-auth';
import { ESI_SCOPE, type PluginOptions } from './auth.types';
/**
* EVE Online OAuth plugin for Elysia framework.
* Injects methods into the Elysia context to redirect users to EVE's
* OAuth authorization page and to validate OAuth callback requests.
*/
export function eveAuthPlugin({ clientId, clientSecret, callbackUrl }: PluginOptions) {
return new Elysia().derive({ as: 'global' }, ({ cookie, redirect, query, status }) => {
const eveAuthRedirect = async (scope?: ESI_SCOPE | ESI_SCOPE[]) => {
const { url, state } = await createAuthorizationURL({
clientId,
clientSecret,
callbackUrl,
scopes: scope || ESI_SCOPE.publicData,
});
cookie.state.set({ value: state, maxAge: 600, path: '/' });
return redirect(url.href, 302);
};
const eveAuthValidateRequest = async () => {
const code = query.code as string;
const state = query.state as string;
const cookieState = cookie.state?.value;
if (!code || !state || !cookieState || state !== cookieState) {
throw status(401);
}
try {
const tokens = await validateCode(clientId, clientSecret, code);
const decoded = await verify(tokens.access_token);
if (!decoded) {
throw 'Invalid token';
}
return tokens;
} catch (error) {
console.error(`EVE OAuth validation failed`, error);
throw status(401);
} finally {
cookie.state?.remove();
}
};
return {
eveAuthRedirect,
eveAuthValidateRequest,
};
});
}

View File

@@ -0,0 +1,7 @@
import attributeOrders from '@data/hoboleaks-sde/attributeorders.json';
export const attributeOrdering = {
'11': attributeOrders['11'],
'87': attributeOrders['87'],
'default': attributeOrders.default,
};

View File

@@ -0,0 +1,25 @@
const base_url = 'https://evetycoon.com/api/v1';
export interface Price {
buyVolume: number;
sellVolume: number;
buyOrders: number;
sellOrders: number;
buyOutliers: number;
sellOutliers: number;
buyThreshold: number;
sellThreshold: number;
buyAvgFivePercent: number;
sellAvgFivePercent: number;
maxBuy: number;
minSell: number;
}
enum Region {
TheForge = 10000002,
}
export const fetchPrice = async (type_id: number, region_id: number = Region.TheForge): Promise<Price> => {
const response = await fetch(`${base_url}/market/stats/${region_id}/${type_id}`);
return (await response.json()) as Price;
};

2
packages/eve/src/third-party/index.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export * as evetycoon from './evetycoon';
export * as janice from './janice';

407
packages/eve/src/third-party/janice.ts vendored Normal file
View File

@@ -0,0 +1,407 @@
/**
* 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;
}
};

View File

@@ -0,0 +1,2 @@
export * from './markdown';
export * from './typeSearch';

View File

@@ -0,0 +1,403 @@
import { describe, it, expect, beforeAll } from 'bun:test';
import { cleanText, convertMillisecondsToTimeString, convertSecondsToTimeString, coloredTextCodeBlock } from './markdown';
import * as path from 'path';
import * as fs from 'fs';
// Load test fixtures
const basePath = path.join(__dirname, '../../fixtures/markdown');
const markupFixturesPath = path.join(basePath, 'test-data-markup.json');
const timeFixturesPath = path.join(basePath, 'test-data-time.json');
const colorFixturesPath = path.join(basePath, 'test-data-colors.json');
let markupFixtures: any;
let timeFixtures: any;
let colorFixtures: any;
describe('cleanText', () => {
beforeAll(() => {
// Fixtures are already loaded above
markupFixtures = JSON.parse(fs.readFileSync(markupFixturesPath, 'utf8'));
timeFixtures = JSON.parse(fs.readFileSync(timeFixturesPath, 'utf8'));
colorFixtures = JSON.parse(fs.readFileSync(colorFixturesPath, 'utf8'));
});
it('should handle basic bold markup', () => {
const input = markupFixtures.boldMarkup.complete;
const result = cleanText(input);
expect(result).toBe('**bold text**');
});
it('should handle incomplete bold markup - open only', () => {
const input = markupFixtures.boldMarkup.openOnly;
const result = cleanText(input);
expect(result).toBe('**bold text**');
});
it('should handle incomplete bold markup - close only', () => {
const input = markupFixtures.boldMarkup.closeOnly;
const result = cleanText(input);
expect(result).toBe('**bold text**');
});
it('should handle basic italic markup', () => {
const input = markupFixtures.italicMarkup.complete;
const result = cleanText(input);
expect(result).toBe('*italic text*');
});
it('should handle incomplete italic markup - open only', () => {
const input = markupFixtures.italicMarkup.openOnly;
const result = cleanText(input);
expect(result).toBe('*italic text*');
});
it('should handle incomplete italic markup - close only', () => {
const input = markupFixtures.italicMarkup.closeOnly;
const result = cleanText(input);
expect(result).toBe('*italic text*');
});
it('should remove color tags with hex colors', () => {
const input = markupFixtures.colorTags.hex6;
const result = cleanText(input);
expect(result).toBe('colored text');
});
it('should remove color tags with hex colors (8-digit)', () => {
const input = markupFixtures.colorTags.hex8;
const result = cleanText(input);
expect(result).toBe('colored text');
});
it('should remove color tags with named colors', () => {
const input = markupFixtures.colorTags.namedColor;
const result = cleanText(input);
expect(result).toBe('colored text');
});
it('should respect max length parameter', () => {
const longText = 'a'.repeat(100);
const input = `<b>${longText}</b>`;
const result = cleanText(input, 50);
expect(result.length).toBeLessThanOrEqual(53); // Account for ** markup + truncation
expect(result).toContain('**');
});
it('should use default max length when not specified', () => {
const veryLongText = 'a'.repeat(2000);
const input = `<b>${veryLongText}</b>`;
const result = cleanText(input);
expect(result.length).toBeLessThanOrEqual(1003); // Account for ** markup
});
it('should handle empty input', () => {
const result = cleanText('');
expect(result).toBe('');
});
it('should handle whitespace-only input', () => {
const result = cleanText(' \n\t ');
expect(result).toBe('');
});
it('should trim whitespace from input', () => {
const input = ' <b>text</b> ';
const result = cleanText(input);
expect(result).toBe('**text**');
});
it('should handle multiple bold tags', () => {
const input = markupFixtures.boldMarkup.multiple;
const result = cleanText(input);
expect(result).toBe('**first** and **second**');
});
it('should handle multiple italic tags', () => {
const input = markupFixtures.italicMarkup.multiple;
const result = cleanText(input);
expect(result).toBe('*first* and *second*');
});
it('should handle multiple color tags', () => {
const input = markupFixtures.colorTags.multiple;
const result = cleanText(input);
expect(result).toBe('first and second');
});
it('should handle nested markup', () => {
const input = markupFixtures.boldMarkup.nested;
const result = cleanText(input);
expect(result).toContain('**');
});
it('should handle empty tags', () => {
const result1 = cleanText(markupFixtures.boldMarkup.empty);
const result2 = cleanText(markupFixtures.italicMarkup.empty);
const result3 = cleanText(markupFixtures.colorTags.empty);
// The regex doesn't match empty content between tags
expect(result1).toBe('<b></b>'); // No match, so unchanged
expect(result2).toBe('<i></i>'); // No match, so unchanged
expect(result3).toBe('');
});
});
describe('convertMillisecondsToTimeString', () => {
beforeAll(() => {
// Fixtures are already loaded above
markupFixtures = JSON.parse(fs.readFileSync(markupFixturesPath, 'utf8'));
timeFixtures = JSON.parse(fs.readFileSync(timeFixturesPath, 'utf8'));
colorFixtures = JSON.parse(fs.readFileSync(colorFixturesPath, 'utf8'));
});
it('should handle zero milliseconds', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.zero);
expect(result).toBe(timeFixtures.expected.zero);
});
it('should handle one second', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.oneSecond);
expect(result).toBe(timeFixtures.expected.oneSecond);
});
it('should handle one minute', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.oneMinute);
expect(result).toBe(timeFixtures.expected.oneMinute);
});
it('should handle one hour', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.oneHour);
expect(result).toBe(timeFixtures.expected.oneHour);
});
it('should handle complex time (1h 1m 1.5s)', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.complex);
expect(result).toBe(timeFixtures.expected.complexMs);
});
it('should handle large time values (24h)', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.daysWorthMs);
expect(result).toBe(timeFixtures.expected.daysMs);
});
it('should handle fractional seconds', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.fractionalSeconds);
expect(result).toBe(timeFixtures.expected.fractionalSeconds);
});
it('should handle small fractions', () => {
const result = convertMillisecondsToTimeString(timeFixtures.milliseconds.smallFraction);
expect(result).toBe(timeFixtures.expected.smallFraction);
});
it('should handle negative values', () => {
const result = convertMillisecondsToTimeString(-1000);
expect(result).toBe('-1.0s');
});
});
describe('convertSecondsToTimeString', () => {
beforeAll(() => {
// Fixtures are already loaded above
markupFixtures = JSON.parse(fs.readFileSync(markupFixturesPath, 'utf8'));
timeFixtures = JSON.parse(fs.readFileSync(timeFixturesPath, 'utf8'));
colorFixtures = JSON.parse(fs.readFileSync(colorFixturesPath, 'utf8'));
});
it('should handle zero seconds', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.zero);
expect(result).toBe('0s');
});
it('should handle one second', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.oneSecond);
expect(result).toBe('1s');
});
it('should handle one minute', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.oneMinute);
expect(result).toBe('1m');
});
it('should handle one hour', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.oneHour);
expect(result).toBe('1h');
});
it('should handle complex time (1h 1m 1s)', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.complex);
expect(result).toBe(timeFixtures.expected.complexSec);
});
it('should handle large time values (24h)', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.daysWorthSec);
expect(result).toBe(timeFixtures.expected.daysSec);
});
it('should handle fractional input (should floor to integer seconds)', () => {
const result = convertSecondsToTimeString(timeFixtures.seconds.fractionalInput);
expect(result).toBe('1h 1m 1.5s'); // Function doesn't actually floor, it preserves fractional seconds
});
it('should handle negative values', () => {
const result = convertSecondsToTimeString(-60);
expect(result).toBe('0s'); // Current implementation doesn't handle negatives properly
});
it('should not include seconds when there are hours and minutes but no remainder seconds', () => {
const result = convertSecondsToTimeString(3660); // 1h 1m 0s
expect(result).toBe('1h 1m');
});
});
describe('coloredTextCodeBlock', () => {
beforeAll(() => {
// Fixtures are already loaded above
markupFixtures = JSON.parse(fs.readFileSync(markupFixturesPath, 'utf8'));
timeFixtures = JSON.parse(fs.readFileSync(timeFixturesPath, 'utf8'));
colorFixtures = JSON.parse(fs.readFileSync(colorFixturesPath, 'utf8'));
});
it('should create red colored text block', () => {
const input = colorFixtures.testText.simple;
const result = coloredTextCodeBlock(input, 'red');
expect(result).toBe(colorFixtures.expected.red.simple);
});
it('should create blue colored text block', () => {
const input = colorFixtures.testText.simple;
const result = coloredTextCodeBlock(input, 'blue');
expect(result).toBe(colorFixtures.expected.blue.simple);
});
it('should create green colored text block', () => {
const input = colorFixtures.testText.simple;
const result = coloredTextCodeBlock(input, 'green');
expect(result).toBe(colorFixtures.expected.green.simple);
});
it('should create yellow colored text block', () => {
const input = colorFixtures.testText.simple;
const result = coloredTextCodeBlock(input, 'yellow');
expect(result).toBe(colorFixtures.expected.yellow.simple);
});
it('should handle empty text with red color', () => {
const result = coloredTextCodeBlock('', 'red');
expect(result).toBe(colorFixtures.expected.red.empty);
});
it('should handle empty text with blue color', () => {
const result = coloredTextCodeBlock('', 'blue');
expect(result).toBe(colorFixtures.expected.blue.empty);
});
it('should handle empty text with green color', () => {
const result = coloredTextCodeBlock('', 'green');
expect(result).toBe(colorFixtures.expected.green.empty);
});
it('should handle empty text with yellow color', () => {
const result = coloredTextCodeBlock('', 'yellow');
expect(result).toBe(colorFixtures.expected.yellow.empty);
});
it('should handle multiline text', () => {
const input = colorFixtures.testText.multiline;
const result = coloredTextCodeBlock(input, 'red');
expect(result).toContain('Line 1\nLine 2\nLine 3');
expect(result).toContain('```ansi');
expect(result).toContain('\u001B[2;31m');
});
it('should handle special characters', () => {
const input = colorFixtures.testText.withSpecialChars;
const result = coloredTextCodeBlock(input, 'green');
expect(result).toContain(input);
expect(result).toContain('```ansi');
expect(result).toContain('\u001B[2;36m');
});
it('should handle unicode characters', () => {
const input = colorFixtures.testText.unicode;
const result = coloredTextCodeBlock(input, 'yellow');
expect(result).toContain(input);
expect(result).toContain('```ansi');
expect(result).toContain('\u001B[2;33m');
});
it('should handle code-like text', () => {
const input = colorFixtures.testText.code;
const result = coloredTextCodeBlock(input, 'blue');
expect(result).toContain(input);
expect(result).toContain('```ansi');
expect(result).toContain('\u001B[2;32m\u001B[2;36m\u001B[2;34m');
});
it('should return original text for invalid color', () => {
const input = 'test text';
// TypeScript should prevent this, but testing runtime behavior
const result = coloredTextCodeBlock(input, 'invalid' as any);
expect(result).toBe(input);
});
});
describe('Edge cases and error handling', () => {
beforeAll(() => {
// Fixtures are already loaded above
markupFixtures = JSON.parse(fs.readFileSync(markupFixturesPath, 'utf8'));
timeFixtures = JSON.parse(fs.readFileSync(timeFixturesPath, 'utf8'));
colorFixtures = JSON.parse(fs.readFileSync(colorFixturesPath, 'utf8'));
});
it('should handle malformed markup gracefully', () => {
const input = '<b>unclosed bold <i>nested italic</b> text</i>';
const result = cleanText(input);
expect(result).toContain('**');
expect(result).toContain('*');
});
it('should handle deeply nested markup', () => {
const input = '<b><i><color=red><a href=showinfo:587>Deep Nesting</a></color></i></b>';
const result = cleanText(input);
expect(result).toBe('***Deep Nesting***');
});
it('should handle very large time values', () => {
const largeValue = 1000 * 60 * 60 * 24 * 365; // 1 year in ms
const result = convertMillisecondsToTimeString(largeValue);
expect(result).toContain('h');
expect(result.length).toBeGreaterThan(0);
});
it('should handle very small time values', () => {
const result = convertMillisecondsToTimeString(1);
expect(result).toBe('0.0s');
});
it('should handle text with no markup', () => {
const input = 'Plain text with no markup';
const result = cleanText(input);
expect(result).toBe(input);
});
it('should handle only whitespace in markup', () => {
const input = '<b> </b>';
const result = cleanText(input);
expect(result).toBe('** **');
});
it('should handle EVE links with very large IDs', () => {
const input = '<a href=showinfo:999999999>Large ID Item</a>';
const result = cleanText(input);
expect(result).toBe('Large ID Item');
});
it('should handle color tags with various hex formats', () => {
const testCases = [
{ input: '<color=0xABC123>hex with 0x</color>', expected: 'hex with 0x' },
{ input: '<color=ABC123>hex without 0x</color>', expected: 'hex without 0x' },
{ input: '<color=0xABC12345>8-digit hex</color>', expected: '8-digit hex' },
];
testCases.forEach(({ input, expected }) => {
const result = cleanText(input);
expect(result).toBe(expected);
});
});
});

View File

@@ -0,0 +1,105 @@
import { truncateText } from '@star-kitten/util/text.js';
export function cleanText(input: string, maxLength: number = 1000): string {
return truncateText(replaceBoldTextMarkup(replaceItalicTextMarkup(removeColorTags(removeLinks(input.trim())))), maxLength);
}
function replaceBoldTextMarkup(input: string): string {
// replace all <b>name</b>, <b>name, and name</b> with **name** using regex
const regex = /<b>([^<]*)<\/b>|<b>([^<]*)|([^<]*)<\/b>/g;
return input.replace(regex, (match, p1, p2, p3) => {
if (p1) return `**${p1}**`;
if (p2) return `**${p2}**`;
if (p3) return `**${p3}**`;
return match;
});
}
function replaceItalicTextMarkup(input: string): string {
// replace all <i>name</i>, <i>name, and name</i> with *name* using regex
const regex = /<i>([^<]*)<\/i>|<i>([^<]*)|([^<]*)<\/i>/g;
return input.replace(regex, (match, p1, p2, p3) => {
if (p1) return `*${p1}*`;
if (p2) return `*${p2}*`;
if (p3) return `*${p3}*`;
return match;
});
}
function removeColorTags(input: string): string {
const regex = /<color=(?:0x)?([0-9a-fA-F]{6,8}|[a-zA-Z]+)>(.*?)<\/color>/g;
return input.replace(regex, '$2');
}
function convertToDiscordLinks(input: string): string {
const regex = /<a href=showinfo:(\d+)>(.*?)<\/a>/g;
return input.replace(regex, (match, number, text) => {
const eveRefLink = `https://everef.net/types/${number}`;
return `[${text}](${eveRefLink})`;
});
}
function removeLinks(input: string): string {
const regex = /<a href=showinfo:(\d+)>(.*?)<\/a>/g;
return input.replace(regex, (match, number, text) => {
return text;
});
}
export function convertMillisecondsToTimeString(milliseconds: number): string {
const totalSeconds = milliseconds / 1000;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const parts: string[] = [];
if (hours > 0) {
parts.push(`${hours}h`);
}
if (minutes > 0) {
parts.push(`${minutes}m`);
}
if (secs > 0 || parts.length === 0) {
// Include seconds if it's the only part
parts.push(`${secs.toFixed(1)}s`);
}
return parts.join(' ');
}
export function convertSecondsToTimeString(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
const parts: string[] = [];
if (hours > 0) {
parts.push(`${hours}h`);
}
if (minutes > 0) {
parts.push(`${minutes}m`);
}
if (secs > 0 || parts.length === 0) {
// Include seconds if it's the only part
parts.push(`${secs}s`);
}
return parts.join(' ');
}
export function coloredTextCodeBlock(text: string, color: 'green' | 'blue' | 'red' | 'yellow'): string {
switch (color) {
case 'red':
return '```ansi\n' + text + '```\n';
case 'blue':
return '```ansi\n' + text + '```\n';
case 'yellow':
return '```ansi\n' + text + '```\n';
case 'green':
return '```ansi\n' + text + '```\n';
default:
return text;
}
}

View File

@@ -0,0 +1,73 @@
import fs from 'node:fs';
import { chain } from 'stream-chain';
import { parser } from 'stream-json';
import { streamObject } from 'stream-json/streamers/StreamObject';
import { create, insert, search } from '@orama/orama';
import { normalize } from '@star-kitten/util/text';
import { getType, type Type } from '@/models/type';
const db = create({
schema: {
type_id: 'number',
name: {
en: 'string',
de: 'string',
fr: 'string',
ru: 'string',
ja: 'string',
zh: 'string',
},
},
});
export async function initializeTypeSearch() {
return new Promise((resolve, reject) => {
const pipeline = chain([fs.createReadStream('./data/reference-data/types.json'), parser(), streamObject(), (data) => data]);
pipeline.on('data', async ({ value }) => {
if (value && value.market_group_id && value.published) {
try {
await addType(value);
} catch (e) {
reject(e);
}
}
});
pipeline.on('error', reject);
pipeline.on('end', resolve);
});
}
const addType = async (type: Type) =>
await insert(db, {
type_id: type.type_id,
name: type.name,
});
export async function typeSearch(name: string) {
let now = Date.now();
const normalizedName = normalize(name);
if (normalizedName.length > 100) return null;
const results = await search(db, {
term: normalizedName,
limit: 1,
tolerance: 0,
});
if (!results || results.count === 0) return null;
now = Date.now();
const type = await getType(results.hits[0].document.type_id);
return type;
}
export async function typeSearchAutoComplete(name: string) {
const normalizedName = normalize(name);
if (normalizedName.length > 100) return null;
const results = await search(db, {
term: normalizedName,
});
if (!results || results.count === 0) return null;
return results.hits.map((hit) => ({
name: hit.document.name.en,
value: hit.document.name.en,
}));
}

View File

@@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"lib": ["ESNext"],
"typeRoots": ["src/types", "./node_modules/@types"],
"paths": {
"@/*": ["./src/*"],
"@data/*": ["./data/*"],
},
"emitDeclarationOnly": true,
"noEmit": false,
"noEmitOnError": false,
"declaration": true,
"outDir": "dist/types",
"rootDir": ".",
"allowImportingTsExtensions": true
},
"include": ["src", "fixtures"],
"exclude": ["node_modules", "build", "**/*.test.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: [
'./src/**/*.ts',
'!./src/**/*.test.ts',
],
platform: 'node',
dts: true,
minify: false,
sourcemap: true,
unbundle: true,
external: ['bun:sqlite', 'bun'],
},
]);