merged libraries into one

This commit is contained in:
JB
2026-01-01 22:07:16 -05:00
parent a6642ac829
commit 6e31d40d49
185 changed files with 383 additions and 4013 deletions

View 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/`);
}

View 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]);
}

View 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);
}

View 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),
});
}

View 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;
}
}

View 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';

View 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);
}

View 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,
};

View 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;
}

View 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;
}