Task #26 Phase 2 Complete — Core Architecture Files created: - conf.yml: Blueprint manifest with all paths configured - admin/controller.php: Admin settings controller (BYOK key, webhook, interval) - admin/view.blade.php: Admin UI with Trinity-inspired styling - controllers/ModpackAPIController.php: Client API with all 4 platform integrations - routes/client.php: Client route for manual version checks - views/server/wrapper.tsx: React component for server overview page - database/migrations: Per-server tracking table Platform Support (all implemented): - CurseForge (BYOK API key) - Modrinth (open, no key) - Technic (open, no key) - FTB/modpacks.ch (open, no key) Detection Strategy: 1. Egg Variables (MODPACK_PLATFORM, MODPACK_ID, platform-specific vars) 2. File fingerprinting via DaemonFileRepository (manifest.json, modrinth.index.json) 3. Manual override via admin UI Next: Phase 3 - Testing on Dev Panel (64.50.188.128) Signed-off-by: Claude (Chronicler #62) <claude@firefrostgaming.com>
316 lines
11 KiB
PHP
316 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Pterodactyl\BlueprintFramework\Extensions\modpackchecker\controllers;
|
|
|
|
use Pterodactyl\Http\Controllers\Controller;
|
|
use Pterodactyl\Models\Server;
|
|
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
|
|
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class ModpackAPIController extends Controller
|
|
{
|
|
public function __construct(
|
|
private DaemonFileRepository $fileRepository,
|
|
private BlueprintExtensionLibrary $blueprint
|
|
) {}
|
|
|
|
/**
|
|
* Manual version check triggered from React frontend
|
|
*/
|
|
public function manualCheck(Request $request, Server $server): JsonResponse
|
|
{
|
|
// 1. Try Egg Variables first (most reliable)
|
|
$platform = $this->getEggVariable($server, 'MODPACK_PLATFORM');
|
|
$modpackId = $this->getEggVariable($server, 'MODPACK_ID');
|
|
|
|
// Also check platform-specific variables
|
|
if (empty($modpackId)) {
|
|
if ($curseforgeId = $this->getEggVariable($server, 'CURSEFORGE_ID')) {
|
|
$platform = 'curseforge';
|
|
$modpackId = $curseforgeId;
|
|
} elseif ($modrinthId = $this->getEggVariable($server, 'MODRINTH_PROJECT_ID')) {
|
|
$platform = 'modrinth';
|
|
$modpackId = $modrinthId;
|
|
} elseif ($ftbId = $this->getEggVariable($server, 'FTB_MODPACK_ID')) {
|
|
$platform = 'ftb';
|
|
$modpackId = $ftbId;
|
|
} elseif ($technicSlug = $this->getEggVariable($server, 'TECHNIC_SLUG')) {
|
|
$platform = 'technic';
|
|
$modpackId = $technicSlug;
|
|
}
|
|
}
|
|
|
|
// 2. Fallback to file fingerprinting if not set
|
|
if (empty($platform) || empty($modpackId)) {
|
|
$detected = $this->detectFromFiles($server);
|
|
if (!empty($detected['platform'])) {
|
|
$platform = $detected['platform'];
|
|
$modpackId = $detected['id'] ?? null;
|
|
}
|
|
}
|
|
|
|
// 3. If still nothing, return unknown
|
|
if (empty($platform) || empty($modpackId)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'status' => 'unknown',
|
|
'message' => 'Could not detect modpack. Set MODPACK_PLATFORM and MODPACK_ID in startup variables.',
|
|
]);
|
|
}
|
|
|
|
// 4. Query the appropriate API
|
|
$versionData = $this->checkVersion($platform, $modpackId);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'server_uuid' => $server->uuid,
|
|
'platform' => $platform,
|
|
'modpack_id' => $modpackId,
|
|
'modpack_name' => $versionData['name'] ?? 'Unknown',
|
|
'current_version' => $versionData['current'] ?? 'Unknown',
|
|
'latest_version' => $versionData['latest'] ?? 'Unknown',
|
|
'status' => $versionData['status'] ?? 'unknown',
|
|
'error' => $versionData['error'] ?? null,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get an egg variable value for a server
|
|
*/
|
|
private function getEggVariable(Server $server, string $name): ?string
|
|
{
|
|
$variable = $server->variables()
|
|
->whereHas('variable', function ($query) use ($name) {
|
|
$query->where('env_variable', $name);
|
|
})
|
|
->first();
|
|
|
|
return $variable?->variable_value;
|
|
}
|
|
|
|
/**
|
|
* Attempt to detect modpack from files
|
|
*/
|
|
private function detectFromFiles(Server $server): array
|
|
{
|
|
// Try CurseForge manifest.json
|
|
try {
|
|
$content = $this->fileRepository->setServer($server)->getContent('/manifest.json');
|
|
$json = json_decode($content, true);
|
|
if (isset($json['projectID'])) {
|
|
return [
|
|
'platform' => 'curseforge',
|
|
'id' => (string) $json['projectID'],
|
|
];
|
|
}
|
|
} catch (\Exception $e) {
|
|
// File doesn't exist or Wings unreachable
|
|
}
|
|
|
|
// Try Modrinth modrinth.index.json
|
|
try {
|
|
$content = $this->fileRepository->setServer($server)->getContent('/modrinth.index.json');
|
|
$json = json_decode($content, true);
|
|
if (isset($json['name'])) {
|
|
// Modrinth index doesn't contain project ID directly
|
|
// We detect it's Modrinth but need manual ID
|
|
return [
|
|
'platform' => 'modrinth',
|
|
'id' => null,
|
|
'name' => $json['name'] ?? null,
|
|
];
|
|
}
|
|
} catch (\Exception $e) {
|
|
// File doesn't exist
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Check version against platform API
|
|
*/
|
|
private function checkVersion(string $platform, string $modpackId): array
|
|
{
|
|
return match ($platform) {
|
|
'curseforge' => $this->checkCurseForge($modpackId),
|
|
'modrinth' => $this->checkModrinth($modpackId),
|
|
'technic' => $this->checkTechnic($modpackId),
|
|
'ftb' => $this->checkFTB($modpackId),
|
|
default => ['status' => 'error', 'error' => 'Unknown platform: ' . $platform],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check CurseForge API
|
|
*/
|
|
private function checkCurseForge(string $modpackId): array
|
|
{
|
|
$apiKey = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
|
|
|
|
if (empty($apiKey)) {
|
|
return [
|
|
'status' => 'error',
|
|
'error' => 'CurseForge API key not configured. Add it in Admin > Extensions > ModpackChecker.',
|
|
];
|
|
}
|
|
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'x-api-key' => $apiKey,
|
|
'Accept' => 'application/json',
|
|
])->timeout(10)->get("https://api.curseforge.com/v1/mods/{$modpackId}");
|
|
|
|
if ($response->status() === 401 || $response->status() === 403) {
|
|
return ['status' => 'error', 'error' => 'Invalid CurseForge API key.'];
|
|
}
|
|
|
|
if ($response->status() === 404) {
|
|
return ['status' => 'error', 'error' => 'Modpack not found or delisted.'];
|
|
}
|
|
|
|
if (!$response->successful()) {
|
|
return ['status' => 'error', 'error' => 'CurseForge API error: ' . $response->status()];
|
|
}
|
|
|
|
$data = $response->json();
|
|
$name = $data['data']['name'] ?? 'Unknown';
|
|
$latestFile = $data['data']['latestFiles'][0] ?? null;
|
|
$latestVersion = $latestFile['displayName'] ?? 'Unknown';
|
|
|
|
return [
|
|
'name' => $name,
|
|
'latest' => $latestVersion,
|
|
'status' => 'up_to_date', // Can't determine current without server-side tracking
|
|
];
|
|
} catch (\Exception $e) {
|
|
return ['status' => 'error', 'error' => 'Failed to connect to CurseForge: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check Modrinth API
|
|
*/
|
|
private function checkModrinth(string $modpackId): array
|
|
{
|
|
try {
|
|
// First get project info
|
|
$projectResponse = Http::withHeaders([
|
|
'User-Agent' => 'ModpackChecker/1.0.0 (firefrostgaming.com)',
|
|
])->timeout(10)->get("https://api.modrinth.com/v2/project/{$modpackId}");
|
|
|
|
if ($projectResponse->status() === 404) {
|
|
return ['status' => 'error', 'error' => 'Modpack not found on Modrinth.'];
|
|
}
|
|
|
|
if (!$projectResponse->successful()) {
|
|
return ['status' => 'error', 'error' => 'Modrinth API error: ' . $projectResponse->status()];
|
|
}
|
|
|
|
$projectData = $projectResponse->json();
|
|
$name = $projectData['title'] ?? 'Unknown';
|
|
|
|
// Get versions
|
|
$versionResponse = Http::withHeaders([
|
|
'User-Agent' => 'ModpackChecker/1.0.0 (firefrostgaming.com)',
|
|
])->timeout(10)->get("https://api.modrinth.com/v2/project/{$modpackId}/version");
|
|
|
|
if (!$versionResponse->successful()) {
|
|
return [
|
|
'name' => $name,
|
|
'latest' => 'Unknown',
|
|
'status' => 'error',
|
|
'error' => 'Could not fetch versions.',
|
|
];
|
|
}
|
|
|
|
$versions = $versionResponse->json();
|
|
$latestVersion = $versions[0]['version_number'] ?? 'Unknown';
|
|
|
|
return [
|
|
'name' => $name,
|
|
'latest' => $latestVersion,
|
|
'status' => 'up_to_date',
|
|
];
|
|
} catch (\Exception $e) {
|
|
return ['status' => 'error', 'error' => 'Failed to connect to Modrinth: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check Technic API
|
|
*/
|
|
private function checkTechnic(string $slug): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(10)
|
|
->get("https://api.technicpack.net/modpack/{$slug}?build=1");
|
|
|
|
if ($response->status() === 404) {
|
|
return ['status' => 'error', 'error' => 'Modpack not found on Technic.'];
|
|
}
|
|
|
|
if (!$response->successful()) {
|
|
return ['status' => 'error', 'error' => 'Technic API error: ' . $response->status()];
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
if (isset($data['error'])) {
|
|
return ['status' => 'error', 'error' => $data['error']];
|
|
}
|
|
|
|
$name = $data['displayName'] ?? $data['name'] ?? 'Unknown';
|
|
$latestVersion = $data['version'] ?? 'Unknown';
|
|
|
|
return [
|
|
'name' => $name,
|
|
'latest' => $latestVersion,
|
|
'status' => 'up_to_date',
|
|
];
|
|
} catch (\Exception $e) {
|
|
return ['status' => 'error', 'error' => 'Failed to connect to Technic: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check FTB (modpacks.ch) API
|
|
*/
|
|
private function checkFTB(string $modpackId): array
|
|
{
|
|
try {
|
|
$response = Http::timeout(10)
|
|
->get("https://api.modpacks.ch/public/modpack/{$modpackId}");
|
|
|
|
if ($response->status() === 404) {
|
|
return ['status' => 'error', 'error' => 'Modpack not found on FTB.'];
|
|
}
|
|
|
|
if (!$response->successful()) {
|
|
return ['status' => 'error', 'error' => 'FTB API error: ' . $response->status()];
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
if (isset($data['status']) && $data['status'] === 'error') {
|
|
return ['status' => 'error', 'error' => $data['message'] ?? 'Unknown FTB error'];
|
|
}
|
|
|
|
$name = $data['name'] ?? 'Unknown';
|
|
$versions = $data['versions'] ?? [];
|
|
$latestVersion = !empty($versions) ? ($versions[0]['name'] ?? 'Unknown') : 'Unknown';
|
|
|
|
return [
|
|
'name' => $name,
|
|
'latest' => $latestVersion,
|
|
'status' => 'up_to_date',
|
|
];
|
|
} catch (\Exception $e) {
|
|
return ['status' => 'error', 'error' => 'Failed to connect to FTB: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
}
|