> /dev/null 2>&1 * * HOW IT WORKS: * 1. Finds all servers with MODPACK_PLATFORM egg variable set * 2. Loops through each server, checking the appropriate API * 3. Stores results in modpackchecker_servers database table * 4. Dashboard badges read from this table (never calling APIs directly) * * RATE LIMITING: * Each API call is followed by a 2-second sleep to avoid rate limits. * For 50 servers, a full check takes ~2 minutes. Adjust sleep() if needed. * * PLATFORM SUPPORT: * - CurseForge: Requires API key in settings (configured via admin panel) * - Modrinth: Public API, no key needed (uses User-Agent) * - FTB: Public API via modpacks.ch * - Technic: Public API, uses slug instead of numeric ID * * DATABASE TABLE: modpackchecker_servers * This table caches the latest check results. See migration file for schema. * Uses updateOrInsert for upsert behavior - first run creates, subsequent update. * * EXAMPLE OUTPUT: * Starting modpack update check... * Found 12 servers with modpack configuration * Checking: ATM9 Server (a1b2c3d4-...) * 🟠 UPDATE AVAILABLE: All The Mods 9 - 0.2.60 * Checking: Creative Server (e5f6g7h8-...) * Skipping - missing platform or modpack ID * Checking: Vanilla Server (i9j0k1l2-...) * 🟢 Up to date: Adrenaserver - 1.7.0 * Modpack update check complete! * * ERROR HANDLING: * If an API call fails, the error is logged to the database and the command * continues to the next server. One failure doesn't stop the whole batch. * * DEPENDENCIES: * - Database table: modpackchecker_servers (from migration) * - Settings: modpackchecker::curseforge_api_key (for CurseForge only) * * @package Pterodactyl\Console\Commands * @author Firefrost Gaming / Frostystyle * @version 1.0.0 * @see ModpackAPIController.php (provides getStatus endpoint for badges) * @see UpdateBadge.tsx (React component that displays the results) * ============================================================================= */ namespace Pterodactyl\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Pterodactyl\Models\Server; /** * Artisan command to check all servers for modpack updates. * * Run manually with: php artisan modpackchecker:check * Or schedule in app/Console/Kernel.php: * $schedule->command('modpackchecker:check')->everyFourHours(); */ class CheckModpackUpdates extends Command { /** * The console command signature. * * @var string */ protected $signature = 'modpackchecker:check'; /** * The console command description (shown in `php artisan list`). * * @var string */ protected $description = 'Check all servers for modpack updates'; /** * Execute the console command. * * Main entry point. Finds all servers with modpack configuration * and checks each one against its platform's API. * * @return int Exit code (0 = success) */ public function handle(): int { $this->info('Starting modpack update check...'); // Get all servers that have modpack variables set $servers = Server::whereHas('variables', function ($q) { $q->where('env_variable', 'MODPACK_PLATFORM'); })->get(); $this->info("Found {$servers->count()} servers with modpack configuration"); foreach ($servers as $server) { $this->checkServer($server); // Rate limiting - sleep between checks sleep(2); } $this->info('Modpack update check complete!'); return 0; } /** * Check a single server for modpack updates. * * This method: * 1. Retrieves modpack configuration from egg variables * 2. Calls the appropriate platform API * 3. Compares current vs latest version * 4. Stores results in database cache * * If any step fails, the error is logged to the database * and the method returns (doesn't throw). * * @param Server $server The server to check * @return void */ private function checkServer(Server $server): void { $this->line("Checking: {$server->name} ({$server->uuid})"); try { // Get platform and modpack ID from variables $platform = $this->getVariable($server, 'MODPACK_PLATFORM'); $modpackId = $this->getVariable($server, 'MODPACK_ID'); if (!$platform || !$modpackId) { $this->warn(" Skipping - missing platform or modpack ID"); return; } // Get latest version from API $latestData = $this->fetchLatestVersion($platform, $modpackId); if (!$latestData) { $this->error(" Failed to fetch version data"); $this->updateDatabase($server, [ 'platform' => $platform, 'modpack_id' => $modpackId, 'error_message' => 'Failed to fetch version data', 'update_available' => false, ]); return; } // Get current version (from variable or file - for now just use variable) $currentVersion = $this->getVariable($server, 'MODPACK_CURRENT_VERSION'); // Determine if update is available $updateAvailable = $currentVersion && $currentVersion !== $latestData['version']; $this->updateDatabase($server, [ 'platform' => $platform, 'modpack_id' => $modpackId, 'modpack_name' => $latestData['name'], 'current_version' => $currentVersion, 'latest_version' => $latestData['version'], 'update_available' => $updateAvailable, 'error_message' => null, ]); $status = $updateAvailable ? '🟠 UPDATE AVAILABLE' : '🟢 Up to date'; $this->info(" {$status}: {$latestData['name']} - {$latestData['version']}"); } catch (\Exception $e) { $this->error(" Error: {$e->getMessage()}"); $this->updateDatabase($server, [ 'error_message' => $e->getMessage(), 'update_available' => false, ]); } } /** * Get an egg variable value from a server. * * Egg variables are the startup parameters configured in the server's * egg. For modpack checking, we look for: * - MODPACK_PLATFORM: Which API to query * - MODPACK_ID: The modpack identifier for that platform * - MODPACK_CURRENT_VERSION: What's currently installed (optional) * * @param Server $server The server to query * @param string $name The variable name (e.g., "MODPACK_PLATFORM") * @return string|null The variable value, or null if not set */ private function getVariable(Server $server, string $name): ?string { $variable = $server->variables() ->where('env_variable', $name) ->first(); return $variable?->server_value; } /** * Route to the appropriate platform API based on platform name. * * This is just a dispatcher - actual API logic is in the check* methods. * * @param string $platform The platform name (modrinth, curseforge, ftb, technic) * @param string $modpackId The platform-specific modpack identifier * @return array|null Contains [name, version] or null on failure */ private 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 => null, }; } private function checkModrinth(string $projectId): ?array { $response = Http::withHeaders([ 'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0', ])->get("https://api.modrinth.com/v2/project/{$projectId}"); if (!$response->successful()) return null; $project = $response->json(); $versionResponse = Http::withHeaders([ 'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0', ])->get("https://api.modrinth.com/v2/project/{$projectId}/version"); $versions = $versionResponse->json(); return [ 'name' => $project['title'] ?? 'Unknown', 'version' => $versions[0]['version_number'] ?? 'Unknown', ]; } private function checkCurseForge(string $modpackId): ?array { $apiKey = DB::table('settings') ->where('key', 'like', '%modpackchecker::curseforge_api_key%') ->value('value'); if (!$apiKey) return null; $response = Http::withHeaders([ 'x-api-key' => $apiKey, ])->get("https://api.curseforge.com/v1/mods/{$modpackId}"); if (!$response->successful()) return null; $data = $response->json()['data'] ?? []; return [ 'name' => $data['name'] ?? 'Unknown', 'version' => $data['latestFiles'][0]['displayName'] ?? 'Unknown', ]; } private function checkFTB(string $modpackId): ?array { $response = Http::get("https://api.modpacks.ch/public/modpack/{$modpackId}"); if (!$response->successful()) return null; $data = $response->json(); return [ 'name' => $data['name'] ?? 'Unknown', 'version' => $data['versions'][0]['name'] ?? 'Unknown', ]; } private function checkTechnic(string $slug): ?array { $response = Http::get("https://api.technicpack.net/modpack/{$slug}?build=1"); if (!$response->successful()) return null; $data = $response->json(); return [ 'name' => $data['displayName'] ?? $data['name'] ?? 'Unknown', 'version' => $data['version'] ?? 'Unknown', ]; } /** * Store or update the modpack check results in the database. * * Uses updateOrInsert for upsert behavior: * - First check: Creates a new row * - Subsequent checks: Updates the existing row * * The server_id column is the unique key for matching. * * This cache table is read by ModpackAPIController::getStatus() * which powers the dashboard badges. * * @param Server $server The server being checked * @param array $data The data to store (platform, version info, errors, etc.) * @return void */ private function updateDatabase(Server $server, array $data): void { DB::table('modpackchecker_servers')->updateOrInsert( ['server_id' => $server->id], array_merge($data, [ 'server_uuid' => $server->uuid, 'last_checked' => now(), 'updated_at' => now(), ]) ); } }