merged libraries into one
This commit is contained in:
30
packages/lib/src/eve/esi/alliance.ts
Normal file
30
packages/lib/src/eve/esi/alliance.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { esiFetch } from './fetch';
|
||||
|
||||
// PUBLIC APIS ---------------------------------------------------------------
|
||||
|
||||
interface AllianceData {
|
||||
creator_corporation_id: number;
|
||||
creator_id: number;
|
||||
date_founded: string;
|
||||
executor_corporation_id: number;
|
||||
faction_id: number;
|
||||
name: string;
|
||||
ticker: string;
|
||||
}
|
||||
|
||||
export async function getAllianceData(id: number) {
|
||||
return await esiFetch<Partial<AllianceData>>(`/alliances/${id}/`);
|
||||
}
|
||||
|
||||
export async function getAllianceCorporations(id: number) {
|
||||
return await esiFetch<number[]>(`/alliances/${id}/corporations/`);
|
||||
}
|
||||
|
||||
interface AllianceIcons {
|
||||
px128x128: string;
|
||||
px64x64: string;
|
||||
}
|
||||
|
||||
export async function getAllianceIcons(id: number) {
|
||||
return await esiFetch<Partial<AllianceIcons>>(`/alliances/${id}/icons/`);
|
||||
}
|
||||
102
packages/lib/src/eve/esi/auth.ts
Normal file
102
packages/lib/src/eve/esi/auth.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { encodeBase64urlNoPadding } from '@oslojs/encoding';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import jwkToPem from 'jwk-to-pem';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { options } from './options';
|
||||
|
||||
export interface EveTokens {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
function generateState(): string {
|
||||
const randomValues = new Uint8Array(32);
|
||||
crypto.getRandomValues(randomValues);
|
||||
return encodeBase64urlNoPadding(randomValues);
|
||||
}
|
||||
|
||||
export async function createAuthorizationURL(scopes: string[] | string = 'publicData') {
|
||||
const state = generateState();
|
||||
const url = new URL('https://login.eveonline.com/v2/oauth/authorize/');
|
||||
url.searchParams.set('response_type', 'code');
|
||||
url.searchParams.set('redirect_uri', options.callback_url);
|
||||
url.searchParams.set('client_id', options.client_id);
|
||||
url.searchParams.set('state', state);
|
||||
url.searchParams.set('scope', Array.isArray(scopes) ? scopes.join(' ') : scopes);
|
||||
return {
|
||||
url,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
export async function validateAuthorizationCode(code: string): Promise<EveTokens> {
|
||||
try {
|
||||
const response = await fetch('https://login.eveonline.com/v2/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${Buffer.from(`${options.client_id}:${options.client_secret}`).toString('base64')}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
}),
|
||||
});
|
||||
return (await response.json()) as EveTokens;
|
||||
} catch (error) {
|
||||
console.error(`failed to validate EVE authorization code`, error);
|
||||
throw `${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
// cache the public key for EVE Online's OAuth2 provider
|
||||
let eveAuthPublicKey: any;
|
||||
export async function validateToken(token: string) {
|
||||
if (!eveAuthPublicKey) {
|
||||
try {
|
||||
const eveJWKS = (await (await fetch('https://login.eveonline.com/oauth/jwks')).json()) as { keys: any[] };
|
||||
eveAuthPublicKey = jwkToPem(eveJWKS.keys[0]);
|
||||
} catch (err) {
|
||||
console.error(`failed to get EVE Auth public keys`, err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, eveAuthPublicKey);
|
||||
return decoded;
|
||||
} catch (err) {
|
||||
console.error(`failed to validate EVE token`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function refresh(
|
||||
{ refresh_token }: { refresh_token: string },
|
||||
scopes?: string[] | string,
|
||||
): Promise<EveTokens> {
|
||||
const params = {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token,
|
||||
scope: '' as string | string[],
|
||||
};
|
||||
|
||||
if (scopes) {
|
||||
params['scope'] = Array.isArray(scopes) ? scopes.join(' ') : scopes;
|
||||
}
|
||||
|
||||
const response = await fetch('https://login.eveonline.com/v2/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${Buffer.from(`${options.client_id}:${options.client_secret}`).toString('base64')}`,
|
||||
},
|
||||
body: new URLSearchParams(params),
|
||||
});
|
||||
return (await response.json()) as EveTokens;
|
||||
}
|
||||
|
||||
export function characterIdFromToken(token: string) {
|
||||
const payload = jwtDecode(token);
|
||||
return parseInt(payload.sub!.split(':')[2]);
|
||||
}
|
||||
381
packages/lib/src/eve/esi/character.ts
Normal file
381
packages/lib/src/eve/esi/character.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import { CharacterHelper, type Character } from '@/eve/db';
|
||||
import { esiFetch } from './fetch';
|
||||
import { tokenHasScopes } from './scopes';
|
||||
|
||||
// PUBLIC APIS ---------------------------------------------------------------
|
||||
|
||||
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) {
|
||||
return esiFetch<Partial<CharacterData>>(`/characters/${id}/`);
|
||||
}
|
||||
|
||||
export interface CharacterAffiliations {
|
||||
alliance_id: number;
|
||||
character_id: number;
|
||||
corporation_id: number;
|
||||
faction_id: number;
|
||||
}
|
||||
|
||||
export function getCharacterAffiliations(ids: number[]) {
|
||||
return esiFetch<Partial<CharacterAffiliations>[]>(`/characters/affiliation/`, undefined, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
})[0] as Partial<CharacterAffiliations>;
|
||||
}
|
||||
|
||||
export interface CharacterPortraits {
|
||||
px128x128: string;
|
||||
px256x256: string;
|
||||
px512x512: string;
|
||||
px64x64: string;
|
||||
}
|
||||
|
||||
export function getCharacterPortraits(id: number) {
|
||||
return esiFetch<Partial<CharacterPortraits>>(`/characters/${id}/portrait/`);
|
||||
}
|
||||
|
||||
export interface CharacterCorporationHistory {
|
||||
corporation_id: number;
|
||||
is_deleted: boolean;
|
||||
record_id: number;
|
||||
start_date: string;
|
||||
}
|
||||
|
||||
export function getCharacterCorporationHistory(id: number) {
|
||||
return esiFetch<Partial<CharacterCorporationHistory>[]>(`/characters/${id}/corporationhistory/`);
|
||||
}
|
||||
|
||||
export function getPortraitURL(id: number) {
|
||||
return `https://images.evetech.net/characters/${id}/portrait`;
|
||||
}
|
||||
|
||||
// PRIVATE APIS --------------------------------------------------------------
|
||||
|
||||
export interface CharacterRoles {
|
||||
roles: string[];
|
||||
roles_at_base: string[];
|
||||
roles_at_hq: string[];
|
||||
roles_at_other: string[];
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_corporation_roles.v1
|
||||
export function getCharacterRoles(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_corporation_roles.v1')) return null;
|
||||
return esiFetch<Partial<CharacterRoles>>(`/characters/${character.eveID}/roles/`, character);
|
||||
}
|
||||
|
||||
export interface CharacterTitles {
|
||||
titles: {
|
||||
name: string;
|
||||
title_id: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_titles.v1
|
||||
export function getCharacterTitles(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_titles.v1')) return null;
|
||||
return esiFetch<Partial<CharacterTitles>>(`/characters/${character.eveID}/titles/`, character);
|
||||
}
|
||||
|
||||
export interface CharacterStandings {
|
||||
from_id: number;
|
||||
from_type: 'agent' | 'npc_corp' | 'faction';
|
||||
standing: number;
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_standings.v1
|
||||
export function getCharacterStandings(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_standings.v1')) return null;
|
||||
return esiFetch<Partial<CharacterStandings>[]>(`/characters/${character.eveID}/standings/`, character);
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
is_read: boolean;
|
||||
sender_id: number;
|
||||
sender_type: 'character' | 'corporation' | 'alliance' | 'faction' | 'system';
|
||||
text: string;
|
||||
timestamp: string;
|
||||
type:
|
||||
| 'character'
|
||||
| 'corporation'
|
||||
| 'alliance'
|
||||
| 'faction'
|
||||
| 'inventory'
|
||||
| 'industry'
|
||||
| 'loyalty'
|
||||
| 'skills'
|
||||
| 'sov'
|
||||
| 'structures'
|
||||
| 'war';
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_notifications.v1
|
||||
export function getCharacterNotifications(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_notifications.v1')) return null;
|
||||
return esiFetch<Partial<Notification>[]>(`/characters/${character.eveID}/notifications/`, character);
|
||||
}
|
||||
|
||||
export interface ContactNotification {
|
||||
message: string;
|
||||
notification_id: number;
|
||||
send_date: string;
|
||||
sender_character_id: number;
|
||||
standing_level: number;
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_notifications.v1
|
||||
export function getCharacterContactNotifications(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_notifications.v1')) return null;
|
||||
return esiFetch<Partial<ContactNotification>[]>(`/characters/${character.eveID}/notifications/contacts`, character);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_medals.v1
|
||||
export function getCharacterMedals(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_medals.v1')) return null;
|
||||
return esiFetch<Partial<Medals>[]>(`/characters/${character.eveID}/medals/`, character);
|
||||
}
|
||||
|
||||
export interface JumpFatigue {
|
||||
jump_fatigue_expire_date: string;
|
||||
last_jump_date: string;
|
||||
last_update_date: string;
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_fatigue.v1
|
||||
export function getCharacterJumpFatigue(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_fatigue.v1')) return null;
|
||||
return esiFetch<Partial<JumpFatigue>>(`/characters/${character.eveID}/fatigue/`, character);
|
||||
}
|
||||
|
||||
export interface Blueprint {
|
||||
item_id: number;
|
||||
location_flag: string;
|
||||
location_id: number;
|
||||
material_efficiency: number;
|
||||
quantity: number;
|
||||
runs: number;
|
||||
time_efficiency: number;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_blueprints.v1
|
||||
export function getCharacterBlueprints(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_blueprints.v1')) return null;
|
||||
return esiFetch<Partial<Blueprint>[]>(`/characters/${character.eveID}/blueprints/`, character);
|
||||
}
|
||||
|
||||
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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-characters.read_agents_research.v1')) return null;
|
||||
return esiFetch<Partial<AgentResearch>[]>(`/characters/${character.eveID}/agents_research/`, character);
|
||||
}
|
||||
|
||||
// CLONES --------------------------------------------------------------------
|
||||
|
||||
export interface Clones {
|
||||
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;
|
||||
}
|
||||
|
||||
// required scope: esi-clones.read_clones.v1
|
||||
export function getCharacterClones(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-clones.read_clones.v1')) return null;
|
||||
return esiFetch<Partial<Clones>>(`/characters/${character.eveID}/clones/`, character);
|
||||
}
|
||||
|
||||
// required scope: esi-clones.read_implants.v1
|
||||
export function getCharacterImplants(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-clones.read_implants.v1')) return null;
|
||||
return esiFetch<number[]>(`/characters/${character.eveID}/implants/`, character);
|
||||
}
|
||||
|
||||
// ASSETS --------------------------------------------------------------------
|
||||
|
||||
export interface Asset {
|
||||
is_blueprint_copy: boolean;
|
||||
is_singleton: boolean;
|
||||
item_id: number;
|
||||
location_flag: string;
|
||||
location_id: number;
|
||||
location_type: 'station' | 'solar_system' | 'other';
|
||||
quantity: number;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
// required scope: esi-assets.read_assets.v1
|
||||
export function getCharacterAssets(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-assets.read_assets.v1')) return null;
|
||||
return esiFetch<Partial<Asset>[]>(`/characters/${character.eveID}/assets/`, character);
|
||||
}
|
||||
|
||||
export interface AssetLocation {
|
||||
item_id: number;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
// required scope: esi-assets.read_assets.v1
|
||||
export function getCharacterAssetLocations(character: Character, ids: number[]) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-assets.read_assets.v1')) return null;
|
||||
return esiFetch<Partial<AssetLocation>[]>(`/characters/${character.eveID}/assets/locations/`, character, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
});
|
||||
}
|
||||
|
||||
export interface AssetNames {
|
||||
item_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// required scope: esi-assets.read_assets.v1
|
||||
export function getCharacterAssetNames(character: Character, ids: number[]) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-assets.read_assets.v1')) return null;
|
||||
return esiFetch<Partial<AssetNames>[]>(`/characters/${character.eveID}/assets/names/`, character, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
});
|
||||
}
|
||||
|
||||
// WALLET --------------------------------------------------------------------
|
||||
|
||||
// required scope: esi-wallet.read_character_wallet.v1
|
||||
export function getCharacterWallet(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-wallet.read_character_wallet.v1')) return null;
|
||||
return esiFetch<number>(`/characters/${character.eveID}/wallet/`, character);
|
||||
}
|
||||
|
||||
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(character: Character, fromId: number) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-wallet.read_character_wallet.v1')) return null;
|
||||
return esiFetch<Partial<WalletTransaction>[]>(`/characters/${character.eveID}/wallet/transactions/`, character, {
|
||||
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(character: Character, page: number = 1) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-wallet.read_character_wallet.v1')) return null;
|
||||
return esiFetch<Partial<WalletJournalEntry>[]>(`/characters/${character.eveID}/wallet/journal/?page=${page}`, character);
|
||||
}
|
||||
|
||||
// LOCATION --------------------------------------------------
|
||||
|
||||
export interface Location {
|
||||
solar_system_id: number;
|
||||
station_id: number;
|
||||
structure_id: number;
|
||||
}
|
||||
|
||||
// required scope: esi-location.read_location.v1
|
||||
export function getCharacterLocation(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-location.read_location.v1')) return null;
|
||||
return esiFetch<Partial<Location>>(`/characters/${character.eveID}/location/`, character);
|
||||
}
|
||||
|
||||
export interface Online {
|
||||
last_login: string;
|
||||
last_logout: string;
|
||||
logins: number;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
// required scope: esi-location.read_online.v1
|
||||
export function getCharacterOnline(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-location.read_online.v1')) return null;
|
||||
return esiFetch<Partial<Online>>(`/characters/${character.eveID}/online/`, character);
|
||||
}
|
||||
|
||||
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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-location.read_ship_type.v1')) return null;
|
||||
return esiFetch<Partial<CurrentShip>>(`/characters/${character.eveID}/ship/`, character);
|
||||
}
|
||||
97
packages/lib/src/eve/esi/corporation.ts
Normal file
97
packages/lib/src/eve/esi/corporation.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { CharacterHelper, type Character } from '@/eve/db';
|
||||
import { esiFetch } from './fetch';
|
||||
|
||||
// PUBLIC APIS ---------------------------------------------------------------
|
||||
|
||||
interface CorporationData {
|
||||
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(id: number) {
|
||||
return await esiFetch<Partial<CorporationData>>(`/corporations/${id}/`);
|
||||
}
|
||||
|
||||
interface AllianceHistory {
|
||||
alliance_id: number;
|
||||
is_deleted: boolean;
|
||||
record_id: number;
|
||||
start_date: string;
|
||||
}
|
||||
|
||||
export async function getCorporationAllianceHistory(id: number) {
|
||||
return await esiFetch<Partial<AllianceHistory>[]>(`/corporations/${id}/alliancehistory/`);
|
||||
}
|
||||
|
||||
interface CorporationIcons {
|
||||
px256x256: string;
|
||||
px128x128: string;
|
||||
px64x64: string;
|
||||
}
|
||||
|
||||
export async function getCorporationIcons(id: number) {
|
||||
return await esiFetch<Partial<CorporationIcons>>(`/corporations/${id}/icons/`);
|
||||
}
|
||||
|
||||
// ASSETS -------------------------------------------------------------------
|
||||
|
||||
export interface AssetData {
|
||||
is_blueprint_copy: boolean;
|
||||
is_singleton: boolean;
|
||||
item_id: number;
|
||||
location_flag: string;
|
||||
location_id: number;
|
||||
location_type: string;
|
||||
quantity: number;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
// required scope: esi-assets.read_corporation_assets.v1
|
||||
export async function getCorporationAssets(id: number, character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-assets.read_corporation_assets.v1')) return null;
|
||||
return await esiFetch<Partial<AssetData>[]>(`/corporations/${id}/assets/`, character);
|
||||
}
|
||||
|
||||
export interface AssetLocation {
|
||||
item_id: number;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
// required scope: esi-assets.read_corporation_assets.v1
|
||||
export async function getCorporationAssetLocations(id: number, character: Character, ids: number[]) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-assets.read_corporation_assets.v1')) return null;
|
||||
return await esiFetch<Partial<AssetLocation>[]>(`/corporations/${id}/assets/locations/`, character, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
});
|
||||
}
|
||||
|
||||
export interface AssetNames {
|
||||
item_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// required scope: esi-assets.read_corporation_assets.v1
|
||||
export async function getCorporationAssetNames(id: number, character: Character, ids: number[]) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-assets.read_corporation_assets.v1')) return null;
|
||||
return await esiFetch<Partial<AssetNames>[]>(`/corporations/${id}/assets/names/`, character, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
});
|
||||
}
|
||||
92
packages/lib/src/eve/esi/fetch.ts
Normal file
92
packages/lib/src/eve/esi/fetch.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { type Character, CharacterHelper } from '@/eve/db/models';
|
||||
import { options } from './options';
|
||||
import { ESI_LATEST_URL } from './scopes';
|
||||
|
||||
const cache = new Map<string, CacheItem>();
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
noCache?: boolean;
|
||||
cacheDuration?: number; // default 30 minutes
|
||||
}
|
||||
|
||||
interface CacheItem {
|
||||
expires: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
function cleanCache() {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of cache) {
|
||||
if (value.expires < now) {
|
||||
cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(cleanCache, 1000 * 60 * 15); // clean cache every 15 minutes
|
||||
|
||||
const defaultCacheDuration = 1000 * 60 * 30; // 30 minutes
|
||||
|
||||
export async function esiFetch<T>(
|
||||
path: string,
|
||||
character?: Character,
|
||||
{ method = 'GET', body, noCache = false, cacheDuration = defaultCacheDuration }: Partial<RequestOptions> = {},
|
||||
) {
|
||||
try {
|
||||
const headers = {
|
||||
'User-Agent': options.user_agent,
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
if (character) {
|
||||
// check if the token is expired
|
||||
if (!CharacterHelper.hasValidToken(character)) {
|
||||
await CharacterHelper.refreshTokens(character);
|
||||
if (!CharacterHelper.hasValidToken(character)) {
|
||||
throw new Error(`Failed to refresh token for character: ${character.eveID}`);
|
||||
}
|
||||
}
|
||||
|
||||
headers['Authorization'] = `Bearer ${character.accessToken}`;
|
||||
}
|
||||
|
||||
const init: RequestInit = {
|
||||
headers,
|
||||
method: method || 'GET',
|
||||
body: body || undefined,
|
||||
};
|
||||
|
||||
const url = new URL(`${ESI_LATEST_URL}${path.startsWith('/') ? path : '/' + path}`);
|
||||
url.searchParams.set('datasource', 'tranquility');
|
||||
|
||||
if (!noCache && init.method === 'GET') {
|
||||
const cached = cache.get(url.href);
|
||||
if (cached && cached?.expires > Date.now()) {
|
||||
return cached.data as T;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, init);
|
||||
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, {
|
||||
expires: Math.max(
|
||||
(res.headers.get('expires') && new Date(Number(res.headers.get('expires') || '')).getTime()) || 0,
|
||||
Date.now() + cacheDuration,
|
||||
),
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return data as T;
|
||||
} catch (err) {
|
||||
console.error(`ESI request failure at ${path} | ${JSON.stringify(err)}`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
12
packages/lib/src/eve/esi/index.ts
Normal file
12
packages/lib/src/eve/esi/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './scopes';
|
||||
export * as CharacterAPI from './character';
|
||||
export * as CorporationAPI from './corporation';
|
||||
export * as AllianceAPI from './alliance';
|
||||
export * as auth from './auth';
|
||||
export * from './auth';
|
||||
export * from './fetch';
|
||||
export * from './skills';
|
||||
export * from './options';
|
||||
export * from './mail';
|
||||
export * from './character';
|
||||
export * from './alliance';
|
||||
130
packages/lib/src/eve/esi/mail.ts
Normal file
130
packages/lib/src/eve/esi/mail.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { esiFetch } from './fetch';
|
||||
import { CharacterHelper, type Character } from '@/eve/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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.read_mail.v1')) return null;
|
||||
return esiFetch<MailHeader[]>(`/characters/${character.eveID}/mail/`, character);
|
||||
}
|
||||
|
||||
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(character: Character, mail: SendMail) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.send_mail.v1')) return null;
|
||||
return esiFetch(`/characters/${character.eveID}/mail/`, character, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(mail),
|
||||
});
|
||||
}
|
||||
|
||||
// requires scope: esi-mail.read_mail.v1
|
||||
export function deleteMail(character: Character, mailID: number) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.organize_mail.v1')) return null;
|
||||
return esiFetch(`/characters/${character.eveID}/mail/${mailID}/`, character, {
|
||||
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(character: Character, mailID: number) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.read_mail.v1')) return null;
|
||||
return esiFetch<Mail>(`/characters/${character.eveID}/mail/${mailID}/`, character);
|
||||
}
|
||||
|
||||
export interface MailMetadata {
|
||||
labels: string[]; // labels array
|
||||
read: boolean; // read boolean
|
||||
}
|
||||
|
||||
// requires scope: esi-mail.organize_mail.v1
|
||||
export function updateMailMetadata(character: Character, mailID: number, metadata: MailMetadata) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.organize_mail.v1')) return null;
|
||||
return esiFetch(`/characters/${character.eveID}/mail/${mailID}/`, character, {
|
||||
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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.read_mail.v1')) return null;
|
||||
return esiFetch<MailLabels>(`/characters/${character.eveID}/mail/labels/`, character);
|
||||
}
|
||||
|
||||
export interface CreateMailLabel {
|
||||
color: number; // color integer
|
||||
name: string; // name string
|
||||
}
|
||||
|
||||
// requires scope: esi-mail.organize_mail.v1
|
||||
export function createMailLabel(character: Character, label: CreateMailLabel) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.organize_mail.v1')) return null;
|
||||
return esiFetch(`/characters/${character.eveID}/mail/labels/`, character, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(label),
|
||||
});
|
||||
}
|
||||
|
||||
// requires scope: esi-mail.organize_mail.v1
|
||||
export function deleteMailLabel(character: Character, labelID: number) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.organize_mail.v1')) return null;
|
||||
return esiFetch(`/characters/${character.eveID}/mail/labels/${labelID}/`, character, {
|
||||
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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-mail.read_mail.v1')) return null;
|
||||
return esiFetch<MailingList[]>(`/characters/${character.eveID}/mail/lists/`, character);
|
||||
}
|
||||
18
packages/lib/src/eve/esi/options.ts
Normal file
18
packages/lib/src/eve/esi/options.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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,
|
||||
};
|
||||
91
packages/lib/src/eve/esi/scopes.ts
Normal file
91
packages/lib/src/eve/esi/scopes.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
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 ESI_LATEST_URL = 'https://esi.evetech.net/latest';
|
||||
export const DATA_SOURCE = 'tranquility';
|
||||
|
||||
export function joinScopes(...scopes: string[]) {
|
||||
return scopes.join(' ');
|
||||
}
|
||||
|
||||
export enum SCOPES {
|
||||
PUBLIC_DATA = 'publicData',
|
||||
CALENDAR_RESPOND_CALENDAR_EVENTS = 'esi-calendar.respond_calendar_events.v1',
|
||||
CALENDAR_READ_CALENDAR_EVENTS = 'esi-calendar.read_calendar_events.v1',
|
||||
LOCATION_READ_LOCATION = 'esi-location.read_location.v1',
|
||||
LOCATION_READ_SHIP_TYPE = 'esi-location.read_ship_type.v1',
|
||||
MAIL_ORGANIZE_MAIL = 'esi-mail.organize_mail.v1',
|
||||
MAIL_READ_MAIL = 'esi-mail.read_mail.v1',
|
||||
MAIL_SEND_MAIL = 'esi-mail.send_mail.v1',
|
||||
SKILLS_READ_SKILLS = 'esi-skills.read_skills.v1',
|
||||
SKILLS_READ_SKILLQUEUE = 'esi-skills.read_skillqueue.v1',
|
||||
WALLET_READ_CHARACTER_WALLET = 'esi-wallet.read_character_wallet.v1',
|
||||
WALLET_READ_CORPORATION_WALLET = 'esi-wallet.read_corporation_wallet.v1',
|
||||
SEARCH_SEARCH_STRUCTURES = 'esi-search.search_structures.v1',
|
||||
CLONES_READ_CLONES = 'esi-clones.read_clones.v1',
|
||||
CHARACTERS_READ_CONTACTS = 'esi-characters.read_contacts.v1',
|
||||
UNIVERSE_READ_STRUCTURES = 'esi-universe.read_structures.v1',
|
||||
KILLMAILS_READ_KILLMAILS = 'esi-killmails.read_killmails.v1',
|
||||
CORPORATIONS_READ_CORPORATION_MEMBERSHIP = 'esi-corporations.read_corporation_membership.v1',
|
||||
ASSETS_READ_ASSETS = 'esi-assets.read_assets.v1',
|
||||
PLANETS_MANAGE_PLANETS = 'esi-planets.manage_planets.v1',
|
||||
FLEETS_READ_FLEET = 'esi-fleets.read_fleet.v1',
|
||||
FLEETS_WRITE_FLEET = 'esi-fleets.write_fleet.v1',
|
||||
UI_OPEN_WINDOW = 'esi-ui.open_window.v1',
|
||||
UI_WRITE_WAYPOINT = 'esi-ui.write_waypoint.v1',
|
||||
CHARACTERS_WRITE_CONTACTS = 'esi-characters.write_contacts.v1',
|
||||
FITTINGS_READ_FITTINGS = 'esi-fittings.read_fittings.v1',
|
||||
FITTINGS_WRITE_FITTINGS = 'esi-fittings.write_fittings.v1',
|
||||
MARKETS_STRUCTURE_MARKETS = 'esi-markets.structure_markets.v1',
|
||||
CORPORATIONS_READ_STRUCTURES = 'esi-corporations.read_structures.v1',
|
||||
CHARACTERS_READ_LOYALTY = 'esi-characters.read_loyalty.v1',
|
||||
CHARACTERS_READ_OPPORTUNITIES = 'esi-characters.read_opportunities.v1',
|
||||
CHARACTERS_READ_CHAT_CHANNELS = 'esi-characters.read_chat_channels.v1',
|
||||
CHARACTERS_READ_MEDALS = 'esi-characters.read_medals.v1',
|
||||
CHARACTERS_READ_STANDINGS = 'esi-characters.read_standings.v1',
|
||||
CHARACTERS_READ_AGENTS_RESEARCH = 'esi-characters.read_agents_research.v1',
|
||||
INDUSTRY_READ_CHARACTER_JOBS = 'esi-industry.read_character_jobs.v1',
|
||||
MARKETS_READ_CHARACTER_ORDERS = 'esi-markets.read_character_orders.v1',
|
||||
CHARACTERS_READ_BLUEPRINTS = 'esi-characters.read_blueprints.v1',
|
||||
CHARACTERS_READ_CORPORATION_ROLES = 'esi-characters.read_corporation_roles.v1',
|
||||
LOCATION_READ_ONLINE = 'esi-location.read_online.v1',
|
||||
CONTRACTS_READ_CHARACTER_CONTRACTS = 'esi-contracts.read_character_contracts.v1',
|
||||
CLONES_READ_IMPLANTS = 'esi-clones.read_implants.v1',
|
||||
CHARACTERS_READ_FATIGUE = 'esi-characters.read_fatigue.v1',
|
||||
KILLMAILS_READ_CORPORATION_KILLMAILS = 'esi-killmails.read_corporation_killmails.v1',
|
||||
CORPORATIONS_TRACK_MEMBERS = 'esi-corporations.track_members.v1',
|
||||
WALLET_READ_CORPORATION_WALLETS = 'esi-wallet.read_corporation_wallets.v1',
|
||||
CHARACTERS_READ_NOTIFICATIONS = 'esi-characters.read_notifications.v1',
|
||||
CORPORATIONS_READ_DIVISIONS = 'esi-corporations.read_divisions.v1',
|
||||
CORPORATIONS_READ_CONTACTS = 'esi-corporations.read_contacts.v1',
|
||||
ASSETS_READ_CORPORATION_ASSETS = 'esi-assets.read_corporation_assets.v1',
|
||||
CORPORATIONS_READ_TITLES = 'esi-corporations.read_titles.v1',
|
||||
CORPORATIONS_READ_BLUEPRINTS = 'esi-corporations.read_blueprints.v1',
|
||||
CONTRACTS_READ_CORPORATION_CONTRACTS = 'esi-contracts.read_corporation_contracts.v1',
|
||||
CORPORATIONS_READ_STANDINGS = 'esi-corporations.read_standings.v1',
|
||||
CORPORATIONS_READ_STARBASES = 'esi-corporations.read_starbases.v1',
|
||||
INDUSTRY_READ_CORPORATION_JOBS = 'esi-industry.read_corporation_jobs.v1',
|
||||
MARKETS_READ_CORPORATION_ORDERS = 'esi-markets.read_corporation_orders.v1',
|
||||
CORPORATIONS_READ_CONTAINER_LOGS = 'esi-corporations.read_container_logs.v1',
|
||||
INDUSTRY_READ_CHARACTER_MINING = 'esi-industry.read_character_mining.v1',
|
||||
INDUSTRY_READ_CORPORATION_MINING = 'esi-industry.read_corporation_mining.v1',
|
||||
PLANETS_READ_CUSTOMS_OFFICES = 'esi-planets.read_customs_offices.v1',
|
||||
CORPORATIONS_READ_FACILITIES = 'esi-corporations.read_facilities.v1',
|
||||
CORPORATIONS_READ_MEDALS = 'esi-corporations.read_medals.v1',
|
||||
CHARACTERS_READ_TITLES = 'esi-characters.read_titles.v1',
|
||||
ALLIANCES_READ_CONTACTS = 'esi-alliances.read_contacts.v1',
|
||||
CHARACTERS_READ_FW_STATS = 'esi-characters.read_fw_stats.v1',
|
||||
CORPORATIONS_READ_FW_STATS = 'esi-corporations.read_fw_stats.v1',
|
||||
}
|
||||
|
||||
export function tokenHasScopes(access_token: string, ...scopes: string[]) {
|
||||
let tokenScopes = getScopesFromToken(access_token);
|
||||
return scopes.every((scope) => tokenScopes.includes(scope));
|
||||
}
|
||||
|
||||
export function getScopesFromToken(access_token: string) {
|
||||
const decoded = jwtDecode(access_token) as { scp: string[] | string; };
|
||||
return typeof decoded.scp === 'string' ? [decoded.scp] : decoded.scp;
|
||||
}
|
||||
66
packages/lib/src/eve/esi/skills.ts
Normal file
66
packages/lib/src/eve/esi/skills.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { CharacterHelper, type Character } from '@/eve/db/models';
|
||||
import { esiFetch } from './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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-skills.read_skills.v1')) return null;
|
||||
return esiFetch<CharacterAttributes>(`/characters/${character.eveID}/attributes`, character);
|
||||
}
|
||||
|
||||
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(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-skills.read_skillqueue.v1')) return null;
|
||||
return esiFetch<SkillQueueItem[]>(`/characters/${character.eveID}/skillqueue`, character);
|
||||
}
|
||||
|
||||
export interface APISkill {
|
||||
active_skill_level: number;
|
||||
skill_id: number;
|
||||
skillpoints_in_skill: number;
|
||||
trained_skill_level: number;
|
||||
}
|
||||
|
||||
export interface CharacterSkills {
|
||||
skills: APISkill[]; // max 1000
|
||||
total_sp: number;
|
||||
unallocated_sp?: number;
|
||||
}
|
||||
|
||||
// required scope: esi-skills.read_skills.v1
|
||||
export function getCharacterSkills(character: Character) {
|
||||
if (!CharacterHelper.hasScope(character, 'esi-skills.read_skills.v1')) return null;
|
||||
return esiFetch<CharacterSkills>(`/characters/${character.eveID}/skills`, character);
|
||||
}
|
||||
|
||||
export function calculateTrainingPercentage(queuedSkill: SkillQueueItem) {
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user