UNPKG

hl-gaming-official-ff-data

Version:

HL Gaming Official Free Fire API client with advanced diagnostics and optional AI-guided remediation

343 lines (314 loc) 15.7 kB
const axios = require("axios"); const EventEmitter = require("events"); const LRU = require("lru-cache"); class HLFFClientError extends Error { constructor(message, code = "HLFF_ERROR", details = null) { super(message); this.name = "HLFFClientError"; this.code = code; this.details = details; } } class HLFFClient extends EventEmitter { constructor(optionsOrApiKey, maybeRegion = "pk", config = {}) { super(); if (typeof optionsOrApiKey === "object" && optionsOrApiKey !== null) { const { apiKey, region = "pk" } = optionsOrApiKey; if (!apiKey) throw new HLFFClientError("API key is required.", "API_KEY_MISSING"); this.apiKey = apiKey; this.region = region; } else { if (!optionsOrApiKey) throw new HLFFClientError("API key is required.", "API_KEY_MISSING"); this.apiKey = optionsOrApiKey; this.region = maybeRegion || "pk"; } this._BASE_URL = config.baseURL || "https://developers2-hlgamingofficial.vercel.app/main/games/freefire/account/api"; this.settings = { timeout: typeof config.timeout === "number" ? config.timeout : 10000, retries: Number.isInteger(config.retries) ? config.retries : 3, retryBackoffBaseMs: config.retryBackoffBaseMs || 300, retryBackoffMaxMs: config.retryBackoffMaxMs || 5000, jitter: config.jitter === false ? false : true, debug: !!config.debug, maxConcurrentRequests: config.maxConcurrentRequests || 6, cacheTTL: config.cacheTTL || 0, cacheMax: config.cacheMax || 500, userAgent: config.userAgent || `hlff-client/2.1.0`, transformResponse: typeof config.transformResponse === "function" ? config.transformResponse : null, axiosInstance: config.axiosInstance || null, ai: Object.assign({ enabled: false, provider: "openai", apiKey: null, model: "gpt-4o-mini", timeout: 5000 }, config.ai || {}) }; this._inFlight = 0; this._queue = []; this.cache = new LRU({ max: this.settings.cacheMax, ttl: this.settings.cacheTTL }); this.axios = this.settings.axiosInstance || axios.create({ baseURL: this._BASE_URL, timeout: this.settings.timeout, headers: { "User-Agent": this.settings.userAgent } }); } async _enqueue(fn) { if (this._inFlight >= this.settings.maxConcurrentRequests) { return new Promise((resolve, reject) => { this._queue.push({ fn, resolve, reject }); }); } this._inFlight++; try { return await fn(); } finally { this._inFlight--; if (this._queue.length) { const next = this._queue.shift(); next.fn().then(next.resolve, next.reject); } } } _cacheKey(method, params) { return `${method}:${JSON.stringify(params)}`; } _sleep(ms) { return new Promise((res) => setTimeout(res, ms)); } _isRetryable(err) { if (!err) return false; if (err.response) { const s = err.response.status; if (s >= 500 || s === 429) return true; return false; } if (err.code === "ECONNABORTED") return true; if (err.request && !err.response) return true; return false; } _analyzeErrorLocal(err, context = {}) { const out = { summary: null, severity: "medium", probableCauses: [], suggestions: [], nextSteps: [], debug: {} }; out.debug.context = context; if (!err) { out.summary = "Unknown error occurred."; out.severity = "low"; out.suggestions.push("Capture full error object and re-run diagnostics."); return out; } out.debug.raw = { name: err.name, message: err.message, code: err.code || null }; if (err.response) { const status = err.response.status; out.debug.status = status; out.debug.headers = err.response.headers || {}; if (status === 401 || status === 403) { out.summary = "Authentication or authorization failure."; out.severity = "high"; out.probableCauses.push("Invalid or missing API key", "Insufficient permissions", "IP/region restrictions"); out.suggestions.push("Verify the API key configured in the client", "Ensure your developer UID and region are correct", "Check whether the API key has been revoked or rate-limited"); out.nextSteps.push("Try a simple curl with the same credentials", "Check the API dashboard for key status"); out.debug.curl = `curl "${this._BASE_URL}?sectionName=AllData&PlayerUid=9351564274&region=${this.region}&useruid=YOUR_USER_UID&api=YOUR_API_KEY"`; return out; } if (status === 404) { out.summary = "Requested resource not found."; out.severity = "low"; out.probableCauses.push("Player UID incorrect", "Wrong section name", "Region mismatch"); out.suggestions.push("Confirm the Player UID", "Try AllData section to confirm account existence", "Verify region"); return out; } if (status === 429) { out.summary = "Rate limit reached."; out.severity = "high"; out.probableCauses.push("Too many requests in short time", "Shared API key across clients"); const retryAfter = err.response.headers && (err.response.headers["retry-after"] || err.response.headers["x-ratelimit-reset"]); if (retryAfter) out.debug.retryAfter = retryAfter; out.suggestions.push("Respect Retry-After header if provided", "Implement exponential backoff and jitter", "Enable local caching"); out.nextSteps.push("Reduce request frequency", "Use unique developer userUid per client when possible"); return out; } if (status >= 500) { out.summary = "Server-side error."; out.severity = "medium"; out.probableCauses.push("Temporary server outage", "Malformed request that triggered server failure"); out.suggestions.push("Retry with exponential backoff", "Log full request and response and contact support if persistent"); out.nextSteps.push("Check API status page and try after a pause"); return out; } out.summary = `API returned status ${status}.`; out.severity = status >= 400 ? "high" : "medium"; out.probableCauses.push("Client or server returned non-success status"); out.suggestions.push("Inspect response body for error details", "Validate request params and API key"); out.nextSteps.push("Capture the response payload and share with support if needed"); out.debug.body = err.response.data; return out; } if (err.code === "ECONNABORTED" || (err.message && err.message.toLowerCase().includes("timeout"))) { out.summary = "Request timed out."; out.severity = "medium"; out.probableCauses.push("Network slowness", "Timeout too low for current conditions"); out.suggestions.push("Increase client timeout setting", "Check network connectivity and DNS", "Retry using jittered backoff"); out.nextSteps.push("Run a network check (ping, traceroute) to the API host"); return out; } if (err.request && !err.response) { out.summary = "No response received from server."; out.severity = "high"; out.probableCauses.push("Network unreachable", "Server blocked the request", "CORS/proxy issues in some environments"); out.suggestions.push("Check internet connectivity and proxy settings", "Confirm the API base URL is reachable", "Try from a different network"); out.nextSteps.push("Run a curl request from same host to verify reachability"); return out; } if (err instanceof SyntaxError || (err.message && err.message.toLowerCase().includes("json"))) { out.summary = "Response parsing error."; out.severity = "medium"; out.probableCauses.push("API returned invalid JSON", "Unexpected response format due to HTML error page"); out.suggestions.push("Log raw response body", "Check if an HTML error page was returned", "Add defensive parsing"); return out; } out.summary = "Unhandled client error."; out.severity = "medium"; out.probableCauses.push("Unknown"); out.suggestions.push("Capture and inspect the full error object", "Enable debug mode to gather more data"); return out; } async _enrichWithAI(localAnalysis, err, context = {}) { if (!this.settings.ai || !this.settings.ai.enabled) return null; const aiCfg = this.settings.ai; if (!aiCfg.apiKey) return null; const prompt = [ "You are a helpful diagnostics assistant for an HTTP API client.", "Provide a concise explanation, probable root causes (ranked), prioritized remediation steps, sample commands/code snippets to verify fixes, and recommended next steps for a developer.", "Return a JSON object with keys: summary, causes (array), remediation (array), verification (array), followUp (array).", "Context:", JSON.stringify({ localAnalysis, errorMessage: err && err.message ? err.message : "", errorName: err && err.name ? err.name : "", context }) ].join("\n\n"); try { const resp = await axios.post("https://api.openai.com/v1/chat/completions", { model: aiCfg.model || "gpt-4o-mini", messages: [{ role: "system", content: "You produce structured JSON responses only." }, { role: "user", content: prompt }], max_tokens: 800, temperature: 0.2 }, { headers: { Authorization: `Bearer ${aiCfg.apiKey}`, "Content-Type": "application/json" }, timeout: aiCfg.timeout || 5000 }); const aiMessage = resp.data && resp.data.choices && resp.data.choices[0] && resp.data.choices[0].message && resp.data.choices[0].message.content; try { const parsed = JSON.parse(aiMessage); return parsed; } catch (parseErr) { return { raw: aiMessage }; } } catch (e) { return { aiError: e.message || String(e) }; } } async _requestWithRetries(params, options = {}) { const { signal, sectionName } = options; const maxAttempts = Math.max(1, this.settings.retries + 1); let attempt = 0; let lastErr = null; while (attempt < maxAttempts) { attempt++; if (signal && signal.aborted) { const err = new HLFFClientError("Request aborted by caller.", "REQUEST_ABORTED"); this.emit("error", { err, sectionName, attempt }); throw err; } try { this.emit("request", { sectionName, attempt, params }); const resp = await this.axios.get("", { params, signal }); this.emit("response", { sectionName, attempt, params, status: resp.status, data: resp.data }); return resp; } catch (err) { lastErr = err; const retryable = this._isRetryable(err); if (!retryable) { const local = this._analyzeErrorLocal(err, { params, attempt }); const ai = await this._enrichWithAI(local, err, { params, attempt }).catch(()=>null); const combined = Object.assign({}, { local }, { ai }); const clientErr = new HLFFClientError(local.summary || err.message || "Request failed", "API_ERROR", combined); this.emit("insight", { combined, err, sectionName, attempt }); this.emit("error", { err: clientErr, sectionName, attempt }); throw clientErr; } if (attempt >= maxAttempts) break; const backoff = Math.min(this.settings.retryBackoffBaseMs * 2 ** (attempt - 1), this.settings.retryBackoffMaxMs) + (this.settings.jitter ? Math.floor(Math.random() * 100) : 0); if (this.settings.debug) this.emit("debug", { attempt, backoff, err: err.message || err.toString() }); await this._sleep(backoff); continue; } } const local = this._analyzeErrorLocal(lastErr, { params, attempts: attempt }); const ai = await this._enrichWithAI(local, lastErr, { params, attempts: attempt }).catch(()=>null); const combined = Object.assign({}, { local }, { ai }); const code = lastErr && lastErr.response ? "API_ERROR" : "NETWORK_ERROR"; const clientErr = new HLFFClientError(local.summary || (lastErr && lastErr.message) || "Request failed after retries", code, combined); this.emit("retriesExhausted", { attempts: attempt, error: clientErr, sectionName }); this.emit("insight", { combined, err: clientErr, sectionName, attempts: attempt }); this.emit("error", { err: clientErr, sectionName, attempts: attempt }); throw clientErr; } async _makeRequest(sectionName, playerUid, userUid, opts = {}) { if (!playerUid) throw new HLFFClientError("Player UID is required.", "MISSING_PLAYER_UID"); if (!userUid) throw new HLFFClientError("User UID is required.", "MISSING_USER_UID"); const region = opts.region || this.region; const params = { sectionName, PlayerUid: playerUid, region, useruid: userUid, api: this.apiKey }; const cacheKey = this._cacheKey(sectionName, params); if (opts.cache !== false && this.settings.cacheTTL > 0) { const cached = this.cache.get(cacheKey); if (cached) return cached; } const task = () => this._requestWithRetries(params, { signal: opts.signal, sectionName }); const resp = await this._enqueue(task); if (!resp || typeof resp.data === "undefined") { const err = new HLFFClientError("Invalid response from API.", "INVALID_RESPONSE"); this.emit("error", { err, sectionName }); throw err; } const out = this.settings.transformResponse ? this.settings.transformResponse(resp.data, { sectionName, params }) : resp.data; if (opts.cache !== false && this.settings.cacheTTL > 0) this.cache.set(cacheKey, out); return out; } getSection(sectionName, playerUid, userUid, options = {}) { return this._makeRequest(sectionName, playerUid, userUid, options); } getAllData(playerUid, userUid, options = {}) { return this.getSection("AllData", playerUid, userUid, options); } getAccountInfo(playerUid, userUid, options = {}) { return this.getSection("AccountInfo", playerUid, userUid, options); } getAccountProfile(playerUid, userUid, options = {}) { return this.getSection("Account Profile Info", playerUid, userUid, options); } getGuildInfo(playerUid, userUid, options = {}) { return this.getSection("Guild Info", playerUid, userUid, options); } getPetInfo(playerUid, userUid, options = {}) { return this.getSection("Pet Info", playerUid, userUid, options); } getSocialInfo(playerUid, userUid, options = {}) { return this.getSection("Social Info", playerUid, userUid, options); } banCheck(playerUid, userUid, options = {}) { return this.getSection("Ban Check", playerUid, userUid, options); } async apiStatus(userUid, options = {}) { try { const data = await this.getSection("AllData", "0", userUid, { cache: false, signal: options.signal }); return { ok: true, info: data }; } catch (err) { if (err instanceof HLFFClientError) return { ok: false, error: err }; return { ok: false, error: new HLFFClientError(err.message || "Status check failed", "STATUS_CHECK_FAILED") }; } } async analyzeError(err, context = {}) { const local = this._analyzeErrorLocal(err, context); const ai = await this._enrichWithAI(local, err, context).catch(()=>null); return { local, ai }; } } module.exports = HLFFClient; module.exports.HLFFClientError = HLFFClientError;