fix: ModpackChecker Phase 3 complete - working end-to-end pipeline
PHASE 3 COMPLETE - All systems operational on Dev Panel
Changes:
- Renamed controllers/ to Controllers/ (PSR-4 case sensitivity fix)
- Updated namespace to use capital C in Controllers
- Fixed getEggVariable() method to use correct Pterodactyl model structure
- Changed from whereHas('variable'...) to direct where('env_variable'...)
- Changed return from variable_value to server_value
- Updated routes/client.php with correct namespace
- Updated wrapper.tsx with correct API path (/api/client/extensions/...)
- Added build.sh for React component injection via sed
Tested and verified:
- Admin UI renders correctly
- Client panel loads without 500 error
- React component appears on server console page
- API call executes successfully
- Returns proper 'no modpack detected' message for unconfigured servers
Key learnings documented:
- Blueprint wrapper field is for Blade only, not TSX
- TSX components require build.sh + sed injection + yarn build
- PHP-FPM OPCache requires restart after adding new classes
- Controller namespace must match directory case exactly
Dev Panel: http://64.50.188.14:128
Test Server UUID: c0a133db-6cb7-497d-a2ed-22ae66eb0de8
Next: Phase 4 - Real modpack testing with CurseForge API
Signed-off-by: Claude (Chronicler #62) <claude@firefrostgaming.com>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
<?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)) {
|
||||
$modpackId = match($platform) {
|
||||
'curseforge' => $this->getEggVariable($server, 'CURSEFORGE_ID'),
|
||||
'modrinth' => $this->getEggVariable($server, 'MODRINTH_PROJECT_ID'),
|
||||
'ftb' => $this->getEggVariable($server, 'FTB_MODPACK_ID'),
|
||||
'technic' => $this->getEggVariable($server, 'TECHNIC_SLUG'),
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
// 2. If no egg variables, try file detection
|
||||
if (empty($platform) || empty($modpackId)) {
|
||||
$detected = $this->detectFromFiles($server);
|
||||
$platform = $platform ?: ($detected['platform'] ?? null);
|
||||
$modpackId = $modpackId ?: ($detected['modpack_id'] ?? null);
|
||||
}
|
||||
|
||||
// 3. If still nothing, return helpful error
|
||||
if (empty($platform) || empty($modpackId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Could not detect modpack. Set MODPACK_PLATFORM and MODPACK_ID in startup variables.',
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. Check the appropriate API
|
||||
try {
|
||||
$versionData = match($platform) {
|
||||
'curseforge' => $this->checkCurseForge($modpackId),
|
||||
'modrinth' => $this->checkModrinth($modpackId),
|
||||
'ftb' => $this->checkFTB($modpackId),
|
||||
'technic' => $this->checkTechnic($modpackId),
|
||||
default => throw new \Exception("Unknown platform: {$platform}")
|
||||
};
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'platform' => $platform,
|
||||
'modpack_id' => $modpackId,
|
||||
'modpack_name' => $versionData['name'] ?? 'Unknown',
|
||||
'latest_version' => $versionData['version'] ?? 'Unknown',
|
||||
'status' => 'checked',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'platform' => $platform,
|
||||
'modpack_id' => $modpackId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an egg variable value for a server
|
||||
*/
|
||||
private function getEggVariable(Server $server, string $name): ?string
|
||||
{
|
||||
$variable = $server->variables()
|
||||
->where('env_variable', $name)
|
||||
->first();
|
||||
return $variable?->server_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to detect modpack from files
|
||||
*/
|
||||
private function detectFromFiles(Server $server): array
|
||||
{
|
||||
try {
|
||||
// Try CurseForge manifest.json
|
||||
$manifest = $this->readServerFile($server, 'manifest.json');
|
||||
if ($manifest) {
|
||||
$data = json_decode($manifest, true);
|
||||
if (isset($data['manifestType']) && $data['manifestType'] === 'minecraftModpack') {
|
||||
return [
|
||||
'platform' => 'curseforge',
|
||||
'modpack_id' => $data['projectID'] ?? null,
|
||||
'name' => $data['name'] ?? null,
|
||||
'version' => $data['version'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Try Modrinth modrinth.index.json
|
||||
$modrinthIndex = $this->readServerFile($server, 'modrinth.index.json');
|
||||
if ($modrinthIndex) {
|
||||
$data = json_decode($modrinthIndex, true);
|
||||
if (isset($data['formatVersion'])) {
|
||||
return [
|
||||
'platform' => 'modrinth',
|
||||
'modpack_id' => $data['dependencies']['minecraft'] ?? null,
|
||||
'name' => $data['name'] ?? null,
|
||||
'version' => $data['versionId'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// File detection failed, return empty
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from the server via Wings
|
||||
*/
|
||||
private function readServerFile(Server $server, string $path): ?string
|
||||
{
|
||||
try {
|
||||
$this->fileRepository->setServer($server);
|
||||
return $this->fileRepository->getContent($path);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check CurseForge API for latest version
|
||||
*/
|
||||
private function checkCurseForge(string $modpackId): array
|
||||
{
|
||||
$apiKey = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
throw new \Exception('CurseForge API key not configured');
|
||||
}
|
||||
|
||||
$response = Http::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();
|
||||
$mod = $data['data'] ?? [];
|
||||
|
||||
return [
|
||||
'name' => $mod['name'] ?? 'Unknown',
|
||||
'version' => $mod['latestFiles'][0]['displayName'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Modrinth API for latest version
|
||||
*/
|
||||
private function checkModrinth(string $projectId): array
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
|
||||
])->get("https://api.modrinth.com/v2/project/{$projectId}/version");
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Modrinth API request failed: ' . $response->status());
|
||||
}
|
||||
|
||||
$versions = $response->json();
|
||||
$latest = $versions[0] ?? [];
|
||||
|
||||
// Get project name
|
||||
$projectResponse = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
|
||||
])->get("https://api.modrinth.com/v2/project/{$projectId}");
|
||||
|
||||
$project = $projectResponse->json();
|
||||
|
||||
return [
|
||||
'name' => $project['title'] ?? 'Unknown',
|
||||
'version' => $latest['version_number'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check FTB (modpacks.ch) API for latest version
|
||||
*/
|
||||
private function checkFTB(string $modpackId): array
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->get("https://api.modpacks.ch/public/modpack/{$modpackId}");
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('FTB API request failed: ' . $response->status());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
return [
|
||||
'name' => $data['name'] ?? 'Unknown',
|
||||
'version' => $data['versions'][0]['name'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Technic API for latest version
|
||||
*/
|
||||
private function checkTechnic(string $slug): array
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->get("https://api.technicpack.net/modpack/{$slug}?build=1");
|
||||
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user