manualCheck() * - GET /api/client/extensions/modpackchecker/status -> getStatus() * * @package Pterodactyl\BlueprintFramework\Extensions\modpackchecker * @author Firefrost Gaming / Frostystyle * @version 1.0.0 * @see ModpackApiService.php (centralized API logic) * @see CheckModpackUpdates.php (cron command that populates the cache) * ============================================================================= */ namespace Pterodactyl\Http\Controllers; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Models\Server; use Pterodactyl\Repositories\Wings\DaemonFileRepository; use Pterodactyl\Services\ModpackApiService; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\RateLimiter; class ModpackAPIController extends Controller { public function __construct( private DaemonFileRepository $fileRepository, private ModpackApiService $apiService ) {} /** * Manual version check triggered from the server console UI. * * Rate limited to 2 requests per minute per server to prevent API abuse. * * @param Request $request The incoming HTTP request * @param Server $server The server to check * @return JsonResponse */ public function manualCheck(Request $request, Server $server): JsonResponse { // Rate Limiting: Max 2 requests per minute per server $limitKey = 'modpack_check_' . $server->uuid; if (RateLimiter::tooManyAttempts($limitKey, 2)) { $seconds = RateLimiter::availableIn($limitKey); return response()->json([ 'success' => false, 'message' => "Too many requests. Please wait {$seconds} seconds before checking again." ], 429); } RateLimiter::hit($limitKey, 60); // 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 using the unified Service try { $versionData = $this->apiService->fetchLatestVersion($platform, $modpackId); return response()->json([ 'success' => true, 'platform' => $platform, 'modpack_id' => $modpackId, 'modpack_name' => $versionData['name'], 'latest_version' => $versionData['version'], 'status' => 'checked', ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'platform' => $platform, 'modpack_id' => $modpackId, 'error' => $e->getMessage(), ]); } } /** * Retrieve an egg variable value for a specific server. * * @param Server $server The server to query * @param string $name The environment variable name * @return string|null The variable's value, or null if not set */ private function getEggVariable(Server $server, string $name): ?string { $variable = $server->variables() ->where('env_variable', $name) ->first(); return $variable?->server_value; } /** * Attempt to detect modpack platform and ID by reading server files. * * Fallback method when egg variables aren't set. * * @param Server $server The server to scan * @return array Contains: platform, modpack_id (all nullable) */ 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 game server via the Wings daemon. * * @param Server $server The server whose files we're reading * @param string $path Relative path from server root * @return string|null File contents, or null if unreadable */ private function readServerFile(Server $server, string $path): ?string { try { $this->fileRepository->setServer($server); return $this->fileRepository->getContent($path); } catch (\Exception $e) { return null; } } /** * Get cached update status for all of a user's servers. * * THIS IS THE DASHBOARD BADGE ENDPOINT. * * CRITICAL: This method ONLY reads from the local database cache. * It NEVER makes external API calls. * * @param Request $request The incoming HTTP request * @return JsonResponse Keyed by server_uuid */ public function getStatus(Request $request): JsonResponse { $user = $request->user(); // Get all server UUIDs the user has access to $serverUuids = $user->accessibleServers()->pluck('uuid')->toArray(); // Query our cache table for these servers $statuses = DB::table('modpackchecker_servers') ->whereIn('server_uuid', $serverUuids) ->get() ->keyBy('server_uuid'); $result = []; foreach ($statuses as $uuid => $status) { $result[$uuid] = [ 'update_available' => $status->status === 'update_available', 'modpack_name' => $status->modpack_name, 'current_version' => $status->current_version, 'latest_version' => $status->latest_version, ]; } return response()->json($result); } }