eve-indy-job-tracker/functions.php

795 lines
24 KiB
PHP
Executable file

<?php
require_once __DIR__ . "/session_bootstrap.php";
define("ESI_CLIENT_ID", "YOUR-EVE-CLIENT-ID");
// Replace with your ESI client secret from EVE Developer Portal
define("ESI_CLIENT_SECRET", "YOUR-EVE-CLIENT-SECRET");
function fetch_character_jobs($character_id, &$access_token)
{
// Check cache first
$cache_key = "character_jobs_{$character_id}";
$cache = get_cache_data($cache_key);
if ($cache !== null) {
return $cache;
}
$esi_url = "https://esi.evetech.net/latest/characters/{$character_id}/industry/jobs/?include_completed=false";
$headers = [
"Authorization: Bearer $access_token",
"Accept: application/json",
];
$response = esi_call($esi_url, $headers);
// Token expired? Try to refresh it.
if (
$response["http_code"] === 401 &&
isset($_SESSION["characters"][$character_id]["refresh_token"])
) {
$new_tokens = refresh_token(
$_SESSION["characters"][$character_id]["refresh_token"]
);
if (!empty($new_tokens["access_token"])) {
// Save refreshed token
$_SESSION["characters"][$character_id]["access_token"] =
$new_tokens["access_token"];
$access_token = $new_tokens["access_token"];
$headers[0] = "Authorization: Bearer $access_token";
// Retry the API call
$response = esi_call($esi_url, $headers);
} else {
error_log(
"ERROR: Token refresh failed for character ID $character_id."
);
return []; // Avoid crashing page
}
}
// Still bad? Bail out
if ($response["http_code"] !== 200) {
error_log(
"ERROR: ESI call failed after token refresh for character ID $character_id. HTTP {$response["http_code"]}"
);
return [];
}
$char_jobs = json_decode($response["body"], true);
if (!is_array($char_jobs)) {
$char_jobs = [];
}
// Skip corporation jobs if no character jobs (faster load)
if (empty($char_jobs)) {
// Cache empty results for 60 seconds (shorter time for empty results)
set_cache_data($cache_key, [], 60);
return [];
}
// Fetch corporation jobs if possible
$corp_jobs = [];
$character_info = fetch_character_info($character_id, $access_token);
if (isset($character_info["corporation_id"])) {
$corp_jobs = fetch_corporation_jobs(
$character_info["corporation_id"],
$access_token
);
}
// Filter out corp jobs where character_id is not in the $_SESSION["characters"]
$filtered_corp_jobs = [];
foreach ($corp_jobs as $job) {
if (
isset($job["installer_id"]) &&
$job["installer_id"] == $character_id
) {
$filtered_corp_jobs[] = $job;
}
}
$all_jobs = array_merge($char_jobs, $filtered_corp_jobs);
// If still no jobs after merging, return empty results
if (empty($all_jobs)) {
// Cache empty results for 60 seconds (shorter time for empty results)
set_cache_data($cache_key, [], 60);
return [];
}
// Extract IDs and fetch names in batch
$blueprint_ids = array_unique(array_column($all_jobs, "blueprint_type_id"));
$system_ids = array_unique(array_column($all_jobs, "system_id"));
// Fetch names in parallel using promises
$blueprint_names = [];
$system_names = [];
// Optimize by caching character name
$character_name = $_SESSION["characters"][$character_id]["name"] ?? "Unknown";
// Pre-define activity mapping
$activity_map = [
1 => "Manufacturing",
3 => "TE Research",
4 => "ME Research",
5 => "Copying",
7 => "Reverse Engineering",
8 => "Invention",
9 => "Reaction",
11 => "Reaction",
];
// Process blueprint and system IDs in smaller batches for better performance
if (!empty($blueprint_ids)) {
$blueprint_names = fetch_type_names($blueprint_ids);
}
if (!empty($system_ids)) {
$system_names = fetch_system_names($system_ids);
}
$results = [];
$current_time = time();
foreach ($all_jobs as $job) {
$start_time = format_time($job["start_date"]);
$end_time = isset($job["end_date"])
? format_time($job["end_date"])
: "";
$end_timestamp = isset($job["end_date"])
? strtotime($job["end_date"])
: null;
$time_left =
$end_timestamp && $job["status"] !== "delivered"
? max(0, $end_timestamp - $current_time)
: "Completed";
$system_name = $system_names[$job["system_id"]] ?? "Private Structure";
$blueprint_name =
$blueprint_names[$job["blueprint_type_id"]] ??
$job["blueprint_type_id"];
$results[] = [
"character" =>
$character_name .
(isset($job["installer_id"]) &&
$job["installer_id"] != $character_id
? " (Corp)"
: ""),
"blueprint" => $blueprint_name,
"activity" =>
$activity_map[$job["activity_id"]] ??
"Activity {$job["activity_id"]}",
"status" => $job["status"],
"location" => $system_name,
"start_time" => $start_time,
"end_time" => $end_time,
"end_time_unix" => $end_timestamp,
"time_left" => is_numeric($time_left)
? gmdate("H:i:s", $time_left)
: $time_left,
];
}
// Cache results with dynamic TTL based on the nearest job completion
$min_time_left = PHP_INT_MAX;
foreach ($results as $job) {
if (is_numeric($job["end_time_unix"]) && $job["end_time_unix"] > $current_time) {
$time_until_completion = $job["end_time_unix"] - $current_time;
if ($time_until_completion < $min_time_left) {
$min_time_left = $time_until_completion;
}
}
}
// Cache until the next job completes (min 60 seconds, max 300 seconds)
$cache_ttl = ($min_time_left < PHP_INT_MAX)
? min(max(60, $min_time_left), 300)
: 300;
set_cache_data($cache_key, $results, $cache_ttl);
return $results;
}
function fetch_corporation_jobs($corporation_id, $access_token)
{
// Check cache first
$cache_key = "corporation_jobs_{$corporation_id}";
$cache = get_cache_data($cache_key);
if ($cache !== null) {
return $cache;
}
$esi_url = "https://esi.evetech.net/latest/corporations/{$corporation_id}/industry/jobs/?include_completed=false";
$headers = [
"Authorization: Bearer $access_token",
"Accept: application/json",
];
$response = esi_call($esi_url, $headers);
if ($response["http_code"] !== 200) {
error_log("Corp job fetch failed: " . $response["body"]);
return [];
}
$jobs = json_decode($response["body"], true);
$results = is_array($jobs) ? $jobs : [];
// Cache the results for 5 minutes
set_cache_data($cache_key, $results, 300);
return $results;
}
function fetch_character_info($character_id, $access_token)
{
// Check cache first
$cache_key = "character_info_{$character_id}";
$cache = get_cache_data($cache_key);
if ($cache !== null) {
return $cache;
}
$url = "https://esi.evetech.net/latest/characters/{$character_id}/";
$headers = [
"Authorization: Bearer $access_token",
"Accept: application/json",
];
$response = esi_call($url, $headers);
$info = json_decode($response["body"], true);
if (is_array($info)) {
// Cache character info for 1 hour (rarely changes)
set_cache_data($cache_key, $info, 3600);
}
return $info;
}
function format_time($iso_string)
{
static $cache = [];
// Use cached result if available
if (isset($cache[$iso_string])) {
return $cache[$iso_string];
}
$dt = DateTime::createFromFormat(DateTime::ATOM, $iso_string);
$result = $dt ? $dt->format("M j, Y H:i") : $iso_string;
// Cache the result
$cache[$iso_string] = $result;
return $result;
}
function fetch_type_names($type_ids) {
if (empty($type_ids)) {
return [];
}
// Check if cleanup is needed
$last_cleanup = get_last_cleanup_time();
$thirty_days_ago = strtotime("-30 days");
if (!$last_cleanup || strtotime($last_cleanup) < $thirty_days_ago) {
// Call populate_cache.php to refresh the cache
$populate_cache_script = __DIR__ . "/populate_cache.php";
if (file_exists($populate_cache_script)) {
exec("php " . escapeshellarg($populate_cache_script));
}
update_last_cleanup_time(); // Update the last cleanup timestamp
}
// Load cache
$cache_file = __DIR__ . "/cache/blueprint_cache.json";
$cache = file_exists($cache_file) ? json_decode(file_get_contents($cache_file), true) : [];
// Check cache first
$cached_names = [];
$ids_to_fetch = [];
foreach ($type_ids as $id) {
if (isset($cache[$id])) {
$cached_names[$id] = $cache[$id];
} else {
$ids_to_fetch[] = $id;
}
}
// Fetch missing IDs from API
if (!empty($ids_to_fetch)) {
$url = "https://esi.evetech.net/latest/universe/names/";
$chunks = array_chunk($ids_to_fetch, 1000); // Split into chunks of 1000 IDs
foreach ($chunks as $chunk) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array_values($chunk)));
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 200) {
$data = json_decode($response, true);
foreach ($data as $entry) {
if (isset($entry["id"], $entry["name"])) {
$cached_names[$entry["id"]] = $entry["name"];
$cache[$entry["id"]] = $entry["name"]; // Update cache
}
}
} else {
error_log("Failed to fetch type names. HTTP Code: $http_code");
}
}
}
// Save updated cache
file_put_contents($cache_file, json_encode($cache, JSON_PRETTY_PRINT));
return $cached_names;
}
function populate_blueprint_cache() {
echo "Fetching all type IDs...\n";
$all_type_ids = get_all_type_ids();
if (empty($all_type_ids)) {
echo "No type IDs retrieved.\n";
return;
}
echo "Filtering blueprint IDs...\n";
$blueprint_ids = filter_blueprint_ids($all_type_ids);
if (empty($blueprint_ids)) {
echo "No blueprint IDs found.\n";
return;
}
echo "Fetching blueprint names...\n";
$cached_data = [];
$chunks = array_chunk($blueprint_ids, 1000);
foreach ($chunks as $chunk) {
$batch_names = fetch_type_names($chunk);
foreach ($batch_names as $id => $name) {
$cached_data[$id] = $name;
}
usleep(250000);
}
// Save to JSON cache file
$cache_dir = __DIR__ . "/cache";
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0775, true);
}
$cache_file = $cache_dir . "/blueprint_cache.json";
file_put_contents(
$cache_file,
json_encode($cached_data, JSON_PRETTY_PRINT)
);
echo "Cache populated with " . count($cached_data) . " blueprint names.\n";
}
function get_all_type_ids() {
$all_ids = [];
$page = 1;
do {
$url = "https://esi.evetech.net/latest/universe/types/?page=$page";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
echo "Failed to fetch type IDs on page $page. HTTP Code: $http_code\n";
break;
}
$ids = json_decode($response, true);
if (empty($ids)) {
break;
}
$all_ids = array_merge($all_ids, $ids);
$page++;
usleep(250000); // Avoid API rate limit
} while (true);
return $all_ids;
}
function filter_blueprint_ids($ids) {
$blueprint_ids = [];
$chunks = array_chunk($ids, 1000);
foreach ($chunks as $chunk) {
$names = fetch_type_names($chunk);
foreach ($names as $id => $name) {
if (str_ends_with($name, "Blueprint")) {
$blueprint_ids[] = $id;
}
}
usleep(250000);
}
return $blueprint_ids;
}
function fetch_system_names($system_ids)
{
if (empty($system_ids)) {
return [];
}
// Generate a cache key based on the sorted system IDs
sort($system_ids);
$cache_key = "system_names_" . md5(json_encode($system_ids));
$cache = get_cache_data($cache_key);
if ($cache !== null) {
return $cache;
}
// Check if we already have some of these system names cached individually
$names = [];
$ids_to_fetch = [];
foreach ($system_ids as $id) {
$individual_cache_key = "system_name_$id";
$cached_name = get_cache_data($individual_cache_key);
if ($cached_name !== null) {
$names[$id] = $cached_name;
} else {
$ids_to_fetch[] = $id;
}
}
// If we have all system names from individual caches, return them
if (empty($ids_to_fetch)) {
// Still cache the combined result
set_cache_data($cache_key, $names, 86400);
return $names;
}
// Only fetch the IDs we don't already have cached
$url = "https://esi.evetech.net/latest/universe/names/";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt(
$ch,
CURLOPT_POSTFIELDS,
json_encode(array_values($ids_to_fetch))
);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if (is_array($data)) {
foreach ($data as $entry) {
if (isset($entry["id"]) && isset($entry["name"])) {
$names[$entry["id"]] = $entry["name"];
// Cache individual system names
$individual_cache_key = "system_name_{$entry["id"]}";
set_cache_data($individual_cache_key, $entry["name"], 86400 * 7); // Cache for a week
}
}
}
// Cache system names for 24 hours (they don't change)
set_cache_data($cache_key, $names, 86400);
return $names;
}
function refresh_token($refresh_token)
{
// Initialize cURL
$ch = curl_init("https://login.eveonline.com/v2/oauth/token");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Basic " . base64_encode(ESI_CLIENT_ID . ":" . ESI_CLIENT_SECRET),
"Content-Type: application/x-www-form-urlencoded",
]);
curl_setopt(
$ch,
CURLOPT_POSTFIELDS,
http_build_query([
"grant_type" => "refresh_token",
"refresh_token" => $refresh_token,
])
);
// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Get HTTP status code
$curl_error = curl_error($ch); // Capture cURL error if any
curl_close($ch);
// Handle cURL errors
if ($response === false) {
error_log("cURL error: $curl_error");
return [];
}
// Handle HTTP errors
if ($http_code !== 200) {
error_log("ERROR: Token refresh failed. HTTP $http_code. Response: $response");
// Specific handling for common HTTP errors
if ($http_code === 400) {
error_log("Bad Request: Check the refresh token or request parameters.");
} elseif ($http_code === 401) {
error_log("Unauthorized: Check the client ID and secret.");
} elseif ($http_code === 429) {
error_log("Rate limit exceeded. Retry after delay.");
} elseif ($http_code >= 500) {
error_log("Server error. Retry after a delay.");
}
return [];
}
// Decode the response
$data = json_decode($response, true);
// Validate the response
if (!isset($data["access_token"])) {
error_log("ERROR: Token refresh failed. Invalid response: $response");
return [];
}
// Save the new refresh token if provided
if (isset($data["refresh_token"])) {
$_SESSION["characters"][$character_id]["refresh_token"] = $data["refresh_token"];
}
return $data;
}
function esi_call($url, $headers)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$body = substr($response, $header_size);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
"http_code" => $http_code,
"body" => $body,
];
}
function is_token_expired($access_token, $buffer_time = 300)
{
// Split the token into its three parts (header, payload, signature)
$parts = explode(".", $access_token);
// Validate that the token has exactly three parts
if (count($parts) !== 3) {
error_log("Invalid JWT format: $access_token");
return true; // Assume expired if the format is invalid
}
// Decode the payload (second part of the JWT)
$payload = json_decode(base64_decode($parts[1]), true);
// Validate that the payload is a valid JSON object
if (!is_array($payload)) {
error_log("Malformed JWT payload: " . base64_decode($parts[1]));
return true; // Assume expired if the payload is malformed
}
// Check if the "exp" (expiration) claim exists
if (!isset($payload["exp"])) {
error_log("JWT missing 'exp' claim: " . json_encode($payload));
return true; // Assume expired if the expiration claim is missing
}
// Compare the expiration time with the current time, considering the buffer time
// This prevents tokens from expiring during a user session by refreshing them early
return $payload["exp"] < (time() + $buffer_time);
}
function fetch_character_blueprints($character_id, $access_token)
{
// Check cache first
$cache_key = "character_blueprints_{$character_id}";
$cache = get_cache_data($cache_key);
if ($cache !== null) {
return $cache;
}
$esi_url = "https://esi.evetech.net/latest/characters/{$character_id}/blueprints/";
$headers = [
"Authorization: Bearer $access_token",
"Accept: application/json",
];
$response = esi_call($esi_url, $headers);
if ($response["http_code"] !== 200) {
error_log(
"Blueprint fetch failed for character ID $character_id: " .
$response["body"]
);
return [];
}
$blueprints = json_decode($response["body"], true);
$results = [];
foreach ($blueprints as $bp) {
$results[] = [
"blueprint_type_id" => $bp["type_id"],
"blueprint_name" => $bp["type_id"], // Placeholder, replace with actual name lookup if needed
"material_efficiency" => $bp["material_efficiency"],
"time_efficiency" => $bp["time_efficiency"],
"runs" => $bp["runs"],
"quantity" => $bp["quantity"] ?? 1,
];
}
// Cache the results for 10 minutes (blueprints change less frequently)
set_cache_data($cache_key, $results, 600);
return $results;
}
function get_cached_name($id) {
$cache_file = __DIR__ . "/cache/blueprint_cache.json";
if (!file_exists($cache_file)) {
return null;
}
$cache = json_decode(file_get_contents($cache_file), true);
return $cache[$id]["name"] ?? null;
}
function cache_name($id, $name) {
$cache_file = __DIR__ . "/cache/blueprint_cache.json";
$cache = file_exists($cache_file) ? json_decode(file_get_contents($cache_file), true) : [];
$cache[$id] = [
"name" => $name,
"last_updated" => gmdate("Y-m-d\TH:i:s\Z")
];
file_put_contents($cache_file, json_encode($cache, JSON_PRETTY_PRINT));
}
function clean_cache($expiry_days = 30) {
$cache_file = __DIR__ . "/cache/blueprint_cache.json";
if (!file_exists($cache_file)) {
return;
}
$cache = json_decode(file_get_contents($cache_file), true);
$expiry_time = strtotime("-$expiry_days days");
foreach ($cache as $id => $entry) {
if (strtotime($entry["last_updated"]) < $expiry_time) {
unset($cache[$id]);
}
}
file_put_contents($cache_file, json_encode($cache, JSON_PRETTY_PRINT));
}
function get_last_cleanup_time() {
$file = __DIR__ . "/cache/last_cleanup.json";
if (!file_exists($file)) {
return null;
}
$data = json_decode(file_get_contents($file), true);
return $data["last_cleanup"] ?? null;
}
function update_last_cleanup_time() {
$file = __DIR__ . "/cache/last_cleanup.json";
$data = ["last_cleanup" => gmdate("Y-m-d\TH:i:s\Z")];
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
}
// Cache functions
function get_cache_data($key) {
static $cache_data = null;
static $loaded = false;
$cache_dir = __DIR__ . "/cache";
$cache_file = $cache_dir . "/api_cache.json";
// Load cache data only once per request
if (!$loaded) {
if (file_exists($cache_file)) {
$cache_data = json_decode(file_get_contents($cache_file), true) ?: [];
} else {
$cache_data = [];
}
$loaded = true;
}
if (!isset($cache_data[$key]) || $cache_data[$key]['expires'] < time()) {
return null;
}
return $cache_data[$key]['data'];
}
function set_cache_data($key, $data, $ttl = 300) {
static $cache_data = null;
static $cache_modified = false;
static $loaded = false;
$cache_dir = __DIR__ . "/cache";
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
$cache_file = $cache_dir . "/api_cache.json";
// Load cache data only once per request
if (!$loaded) {
if (file_exists($cache_file)) {
$cache_data = json_decode(file_get_contents($cache_file), true) ?: [];
} else {
$cache_data = [];
}
$loaded = true;
}
if (!is_array($cache_data)) {
$cache_data = [];
}
// Clean expired cache entries (only occasionally to improve performance)
if (rand(1, 10) === 1) {
foreach ($cache_data as $cache_key => $cache_entry) {
if ($cache_entry['expires'] < time()) {
unset($cache_data[$cache_key]);
$cache_modified = true;
}
}
}
// Update cache with new data
$cache_data[$key] = [
'data' => $data,
'expires' => time() + $ttl
];
$cache_modified = true;
// Use register_shutdown_function to write cache only once at the end of the request
if ($cache_modified && !isset($GLOBALS['cache_shutdown_registered'])) {
$GLOBALS['cache_shutdown_registered'] = true;
register_shutdown_function(function() use ($cache_file) {
global $cache_data;
if (is_array($cache_data)) {
file_put_contents($cache_file, json_encode($cache_data));
}
});
}
}
?>