Files
firefrost-services/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php
Claude (Chronicler #83 - The Compiler) 9415c1b984 v1.1.0 Priority 1+2b: file ID comparison + manifest version extraction
- Migration: adds current_file_id, latest_file_id, is_ignored columns
- ModpackApiService: all 4 platforms now return file_id in response
- CheckModpackUpdates: file ID comparison (preferred) with string fallback
- detectCurseForge: extracts manifest['version'] as installed_version
- Cron skips is_ignored servers
- Detection priority: manifest version > egg var > DB record > seed latest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:38:41 -05:00

212 lines
7.7 KiB
PHP

<?php
/**
* =============================================================================
* MODPACK VERSION CHECKER - API SERVICE
* =============================================================================
*
* Centralized service for all modpack platform API interactions.
* Used by both the Controller (manual checks) and Command (cron checks).
*
* SUPPORTED PLATFORMS:
* - Modrinth: Public API with User-Agent requirement
* - CurseForge: Requires API key (configured in admin panel)
* - FTB: Public API via modpacks.ch
* - Technic: Public API with dynamic build number caching
*
* WHY A SERVICE?
* DRY principle - both the manual check button and the cron job need
* the same API logic. Centralizing it here means:
* - One place to fix bugs
* - One place to add new platforms
* - Consistent error handling
* - Shared caching (e.g., Technic build number)
*
* @package Pterodactyl\BlueprintFramework\Extensions\modpackchecker
* @author Firefrost Gaming / Frostystyle <dev@firefrostgaming.com>
* @version 1.0.0
* =============================================================================
*/
namespace Pterodactyl\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
use Exception;
class ModpackApiService
{
public function __construct(
private BlueprintExtensionLibrary $blueprint
) {}
/**
* Fetch the latest version info for a modpack from its platform API.
*
* @param string $platform Platform name: modrinth, curseforge, ftb, technic
* @param string $modpackId Platform-specific modpack identifier
* @return array Contains: name, version
* @throws Exception If platform unknown or API call fails
*/
public function fetchLatestVersion(string $platform, string $modpackId): array
{
return match($platform) {
'modrinth' => $this->checkModrinth($modpackId),
'curseforge' => $this->checkCurseForge($modpackId),
'ftb' => $this->checkFTB($modpackId),
'technic' => $this->checkTechnic($modpackId),
default => throw new Exception("Unknown platform: {$platform}")
};
}
/**
* Query Modrinth API for latest modpack version.
*
* NO API KEY REQUIRED - uses User-Agent for identification.
* Rate limit: 300 requests/minute (generous)
*
* @param string $projectId Modrinth project ID or slug
* @return array Contains: name, version
* @throws Exception If API request fails
*/
private function checkModrinth(string $projectId): array
{
$headers = ['User-Agent' => 'FirefrostGaming/ModpackChecker/1.0'];
$response = Http::timeout(10)->withHeaders($headers)
->get("https://api.modrinth.com/v2/project/{$projectId}");
if (!$response->successful()) {
throw new Exception('Modrinth API request failed: ' . $response->status());
}
$project = $response->json();
$versionResponse = Http::timeout(10)->withHeaders($headers)
->get("https://api.modrinth.com/v2/project/{$projectId}/version");
if (!$versionResponse->successful()) {
throw new Exception('Modrinth versions API failed: ' . $versionResponse->status());
}
$versions = $versionResponse->json();
$latestVer = $versions[0] ?? null;
return [
'name' => $project['title'] ?? 'Unknown',
'version' => $latestVer['version_number'] ?? 'Unknown',
'file_id' => $latestVer ? ($latestVer['id'] ?? null) : null,
];
}
/**
* Query CurseForge API for latest modpack version.
*
* REQUIRES API KEY - configured in admin panel.
* Rate limit: ~1000 requests/day for personal keys.
*
* @param string $modpackId CurseForge project ID (numeric)
* @return array Contains: name, version
* @throws Exception If API key missing or request fails
*/
private function checkCurseForge(string $modpackId): array
{
$apiKey = trim($this->blueprint->dbGet('modpackchecker', 'curseforge_api_key') ?? '');
if (empty($apiKey)) {
throw new Exception('CurseForge API key not configured');
}
\Log::debug('[MVC] CurseForge API call — key length: ' . strlen($apiKey) . ', modpack: ' . $modpackId);
$response = Http::timeout(10)->withHeaders([
'x-api-key' => $apiKey,
'Accept' => 'application/json',
])->get("https://api.curseforge.com/v1/mods/{$modpackId}");
if (!$response->successful()) {
throw new Exception('CurseForge API request failed: ' . $response->status());
}
$data = $response->json()['data'] ?? [];
$latestFile = $data['latestFiles'][0] ?? null;
return [
'name' => $data['name'] ?? 'Unknown',
'version' => $latestFile['displayName'] ?? 'Unknown',
'file_id' => $latestFile ? (string) $latestFile['id'] : null,
];
}
/**
* Query Feed The Beast (FTB) API for latest modpack version.
*
* NO API KEY REQUIRED - modpacks.ch is public.
*
* @param string $modpackId FTB modpack ID (numeric)
* @return array Contains: name, version
* @throws Exception If API request fails
*/
private function checkFTB(string $modpackId): array
{
$response = Http::timeout(10)->get("https://api.modpacks.ch/public/modpack/{$modpackId}");
if (!$response->successful()) {
throw new Exception('FTB API request failed: ' . $response->status());
}
$data = $response->json();
$latestVer = $data['versions'][0] ?? null;
return [
'name' => $data['name'] ?? 'Unknown',
'version' => $latestVer['name'] ?? 'Unknown',
'file_id' => $latestVer ? (string) ($latestVer['id'] ?? null) : null,
];
}
/**
* Query Technic Platform API for latest modpack version.
*
* NO API KEY REQUIRED - but requires dynamic build number.
* The build number is cached for 12 hours to reduce API calls.
*
* "RV-Ready" approach: Technic blocks old build numbers, so we
* dynamically fetch the current stable launcher build.
*
* @param string $slug Technic modpack slug (URL-friendly name)
* @return array Contains: name, version
* @throws Exception If API request fails
*/
private function checkTechnic(string $slug): array
{
// Cache the build number for 12 hours to prevent rate limits
$latestBuild = Cache::remember('modpackchecker_technic_build', 43200, function () {
$versionResponse = Http::timeout(10)->get('https://api.technicpack.net/launcher/version/stable4');
return $versionResponse->successful()
? ($versionResponse->json('build') ?? 999)
: 999;
});
$response = Http::timeout(10)->withHeaders([
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
'Accept' => 'application/json',
])->get("https://api.technicpack.net/modpack/{$slug}?build={$latestBuild}");
if (!$response->successful()) {
throw new Exception('Technic API request failed: ' . $response->status());
}
$data = $response->json();
return [
'name' => $data['displayName'] ?? $data['name'] ?? 'Unknown',
'version' => $data['version'] ?? 'Unknown',
'file_id' => $data['version'] ?? null,
];
}
}