refactor(modpackchecker): Batch 2 fixes - centralized service, rate limiting, schema fixes
NEW: app/Services/ModpackApiService.php
- Centralized API logic for all 4 platforms
- Technic build number cached for 12 hours (RV-Ready)
- Single source of truth for API calls
Controller (ModpackAPIController.php):
- Now uses injected ModpackApiService instead of duplicated code
- Added RateLimiter: 2 requests/minute per server on manualCheck()
- Returns 429 with countdown when rate limited
- Removed 400+ lines of duplicated API code
Console Command (CheckModpackUpdates.php):
- FIXED: updateDatabase() now uses server_uuid (not server_id)
- FIXED: status column uses strings ('update_available', 'up_to_date', 'error')
- FIXED: Technic API now uses dynamic build via service
- Now uses injected ModpackApiService
SECURITY:
- Rate limiting prevents API key abuse via button spam
- Technic build caching reduces external API calls
Reviewed by: Gemini AI (Architecture Consultant)
Signed-off-by: Claude (Chronicler #63) <claude@firefrostgaming.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
<?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 Illuminate\Support\Facades\DB;
|
||||
use Exception;
|
||||
|
||||
class ModpackApiService
|
||||
{
|
||||
/**
|
||||
* 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::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::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();
|
||||
|
||||
return [
|
||||
'name' => $project['title'] ?? 'Unknown',
|
||||
'version' => $versions[0]['version_number'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = DB::table('settings')
|
||||
->where('key', 'modpackchecker::curseforge_api_key')
|
||||
->value('value');
|
||||
|
||||
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()['data'] ?? [];
|
||||
|
||||
return [
|
||||
'name' => $data['name'] ?? 'Unknown',
|
||||
'version' => $data['latestFiles'][0]['displayName'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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::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',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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::get('https://api.technicpack.net/launcher/version/stable4');
|
||||
return $versionResponse->successful()
|
||||
? ($versionResponse->json('build') ?? 999)
|
||||
: 999;
|
||||
});
|
||||
|
||||
$response = Http::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',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user