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
JavaScript
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®ion=${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;