Priority 2 — Date-time seeding:
- fetchFileHistory() for CurseForge, Modrinth, FTB
- seedCurrentVersion() matches release closest to server install date
- Falls back to latest if no history or no install date
Priority 3 — New endpoints:
- GET /servers/{server}/status — zero-click cached status
- GET /servers/{server}/releases — recalibrate dropdown (10 releases)
- POST /servers/{server}/calibrate — save user's version selection
- POST /servers/{server}/ignore — toggle is_ignored flag
Priority 5 — BCC log parsing:
- detectFromBccLog() reads logs/latest.log for BetterCompatibilityChecker
- Extracts modpack name + version from BCC output line
- Skips CHANGE_ME values
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
287 lines
10 KiB
PHP
287 lines
10 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,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Fetch file/version history for date-time seeding.
|
|
* Returns array of ['file_id', 'version', 'display_name', 'release_date'].
|
|
*/
|
|
public function fetchFileHistory(string $platform, string $modpackId, int $limit = 20): array
|
|
{
|
|
return match($platform) {
|
|
'curseforge' => $this->fetchCurseForgeHistory($modpackId, $limit),
|
|
'modrinth' => $this->fetchModrinthHistory($modpackId, $limit),
|
|
'ftb' => $this->fetchFtbHistory($modpackId, $limit),
|
|
default => []
|
|
};
|
|
}
|
|
|
|
private function fetchCurseForgeHistory(string $modpackId, int $limit): array
|
|
{
|
|
$apiKey = trim($this->blueprint->dbGet('modpackchecker', 'curseforge_api_key') ?? '');
|
|
if (empty($apiKey)) return [];
|
|
|
|
$response = Http::timeout(15)->withHeaders([
|
|
'x-api-key' => $apiKey,
|
|
'Accept' => 'application/json',
|
|
])->get("https://api.curseforge.com/v1/mods/{$modpackId}/files", [
|
|
'pageSize' => $limit,
|
|
]);
|
|
|
|
if (!$response->successful()) return [];
|
|
|
|
return collect($response->json()['data'] ?? [])
|
|
->map(fn($f) => [
|
|
'file_id' => (string) $f['id'],
|
|
'version' => $f['displayName'] ?? 'Unknown',
|
|
'display_name' => $f['displayName'] ?? 'Unknown',
|
|
'release_date' => $f['fileDate'] ?? null,
|
|
])
|
|
->toArray();
|
|
}
|
|
|
|
private function fetchModrinthHistory(string $projectId, int $limit): array
|
|
{
|
|
$response = Http::timeout(15)
|
|
->withHeaders(['User-Agent' => 'FirefrostGaming/ModpackChecker/1.0'])
|
|
->get("https://api.modrinth.com/v2/project/{$projectId}/version");
|
|
|
|
if (!$response->successful()) return [];
|
|
|
|
return collect($response->json() ?? [])
|
|
->take($limit)
|
|
->map(fn($v) => [
|
|
'file_id' => $v['id'] ?? null,
|
|
'version' => $v['version_number'] ?? 'Unknown',
|
|
'display_name' => $v['name'] ?? $v['version_number'] ?? 'Unknown',
|
|
'release_date' => $v['date_published'] ?? null,
|
|
])
|
|
->toArray();
|
|
}
|
|
|
|
private function fetchFtbHistory(string $modpackId, int $limit): array
|
|
{
|
|
$response = Http::timeout(15)
|
|
->get("https://api.modpacks.ch/public/modpack/{$modpackId}");
|
|
|
|
if (!$response->successful()) return [];
|
|
|
|
return collect($response->json()['versions'] ?? [])
|
|
->take($limit)
|
|
->map(fn($v) => [
|
|
'file_id' => (string) ($v['id'] ?? ''),
|
|
'version' => $v['name'] ?? 'Unknown',
|
|
'display_name' => $v['name'] ?? 'Unknown',
|
|
'release_date' => isset($v['updated']) ? date('c', $v['updated']) : null,
|
|
])
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
];
|
|
}
|
|
}
|