PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_NUM, ]); $stmt = $pdo->query($sql); $count = $stmt ? $stmt->fetchColumn() : false; if ($count === false || !is_numeric($count)) { throw new RuntimeException('HL_ONLINE_COUNT_SQL did not return a numeric value.'); } $usersOnline = max(0, min(65535, (int) $count)); } catch (Throwable $e) { if (!$reportOfflineOnDbError) { fwrite(STDERR, "Database error: {$e->getMessage()}\n"); exit(1); } fwrite(STDERR, "Database error; reporting offline because HL_REPORT_OFFLINE_ON_DB_ERROR=1: {$e->getMessage()}\n"); $usersOnline = 0; $status = 'offline'; } $endpoint = $apiBase.'/hotels/'.rawurlencode($slug).'/presence'; $payload = json_encode([ 'users_online' => $usersOnline, 'status' => $status, ], JSON_THROW_ON_ERROR); if ($dryRun) { echo "Dry run: would POST to {$endpoint}\n"; echo $payload."\n"; exit(0); } if (!extension_loaded('curl')) { fwrite(STDERR, "PHP cURL extension is not enabled.\n"); exit(1); } $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer '.$apiKey, 'Content-Type: application/json', 'Accept: application/json', 'User-Agent: HabboLibre-Presence-Reporter/1.2', ], CURLOPT_POSTFIELDS => $payload, CURLOPT_CONNECTTIMEOUT => $connectTimeoutSeconds, CURLOPT_TIMEOUT => $timeoutSeconds, ]); $body = curl_exec($ch); $httpStatus = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); curl_close($ch); if ($body === false || $httpStatus < 200 || $httpStatus >= 300) { fwrite(STDERR, "HabboLibre API error HTTP {$httpStatus}: ".($error ?: $body)."\n"); appendHabboLibreLog($logFile, $status, $usersOnline, $httpStatus); exit(1); } // Append a heartbeat line to the optional diagnostic log so the owner can // verify cron is firing without checking HabboLibre. appendHabboLibreLog($logFile, $status, $usersOnline, $httpStatus); $response = json_decode($body, true); $reportedAt = is_array($response) ? ($response['reported_at'] ?? null) : null; $freshForSeconds = is_array($response) ? ($response['fresh_for_seconds'] ?? null) : null; echo "Reported {$usersOnline} users online to HabboLibre"; if ($reportedAt) { echo " at {$reportedAt}"; } if ($freshForSeconds) { echo " (fresh for {$freshForSeconds}s)"; } echo "\n"; function appendHabboLibreLog(string $path, string $status, int $usersOnline, int $httpStatus): void { if ($path === '') { return; } $line = sprintf( "%s | %s | users_online=%d | http=%d\n", date('c'), $status, $usersOnline, $httpStatus, ); @file_put_contents($path, $line, FILE_APPEND | LOCK_EX); } /** @return resource|null */ function acquireHabboLibreLock(string $slug, string $path) { $safeSlug = preg_replace('/[^a-zA-Z0-9_-]/', '_', $slug) ?: 'hotel'; $path = $path !== '' ? $path : sys_get_temp_dir()."/habbolibre-presence-{$safeSlug}.lock"; $handle = @fopen($path, 'c'); if (!$handle) { fwrite(STDERR, "Could not open lock file {$path}; continuing without lock.\n"); return null; } if (!flock($handle, LOCK_EX | LOCK_NB)) { echo "Another HabboLibre presence reporter run is still active; skipping.\n"; exit(0); } return $handle; } function envFlag(string $key, bool $default = false): bool { $value = getenv($key); if ($value === false || $value === '') { return $default; } return in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true); } function envInt(string $key, int $default, int $min, int $max): int { $value = getenv($key); if ($value === false || $value === '' || !is_numeric($value)) { return $default; } return max($min, min($max, (int) $value)); } function loadHabboLibreEnv(string $path): void { if (!is_readable($path)) { return; } $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($lines === false) { return; } foreach ($lines as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') { continue; } [$key, $value] = array_pad(explode('=', $line, 2), 2, ''); $key = trim($key); $value = trim(trim($value), "\"'"); if ($key === '' || getenv($key) !== false) { continue; } putenv($key.'='.$value); $_ENV[$key] = $value; } }