/** * server.js * Express API server (replaces api.php + proxy_check.php) * All Telegram features have been removed. * Token-based ext login has been removed — extension works without login. */ const express = require("express"); const cors = require("cors"); const crypto = require("crypto"); const path = require("path"); const https = require("https"); const http = require("http"); const fs = require("fs"); const Database = require("./database"); const gitSync = require("./git-sync"); // ─── Git sync: pull data from GitHub before starting ──────────── gitSync.initSync(); const app = express(); const PORT = process.env.PORT || 7860; // 7860 = HF Spaces default const db = new Database(path.join(__dirname, "data")); // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Auto-migrate / sync hit counters on startup db.migrateOldHitCounters(); db.autoSyncHitCounters(); // ─── Helpers ──────────────────────────────────────────────────── function apiResponse(res, success, data = {}, error = null, status = 200) { const response = { success, ...data }; if (error) response.error = error; return res.status(status).json(response); } function getInput(req, key, defaultVal = null) { return req.query[key] || req.body?.[key] || defaultVal; } function sanitize(input, maxLen = 500) { if (!input) return ""; input = String(input) .trim() .replace(/<[^>]*>/g, ""); return input.length > maxLen ? input.substring(0, maxLen) : input; } function generateToken(length = 15) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let token = ""; for (let i = 0; i < length; i++) { token += chars[crypto.randomInt(0, chars.length)]; } return token; } function generateLicenseKey(version) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let random = ""; for (let i = 0; i < 20; i++) { random += chars[crypto.randomInt(0, chars.length)]; } return `USAGI-${version.replace(/\./g, "")}-${random}`; } function requireToken(req, res) { const token = getInput(req, "token"); if (!token) return apiResponse(res, false, {}, "Token required", 400); if (token.length !== 15) return apiResponse(res, false, {}, "Invalid token format (must be 15 chars)", 400); const user = db.getUserByToken(token); if (!user) return apiResponse(res, false, {}, "Invalid token", 401); if (user.banned) return apiResponse(res, false, {}, "Account suspended", 403); return user; } // ─── Feedback helpers (concurrent-safe) ────────────────────────── const feedbackFile = path.join(__dirname, "feedback.json"); function readFeedback() { try { if (!fs.existsSync(feedbackFile)) return {}; return JSON.parse(fs.readFileSync(feedbackFile, "utf-8")) || {}; } catch (e) { return {}; } } function writeFeedback(data) { const tmp = feedbackFile + ".tmp." + process.pid; try { fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8"); fs.renameSync(tmp, feedbackFile); return true; } catch (e) { try { fs.unlinkSync(tmp); } catch (_) {} return false; } } // ─── Routes ───────────────────────────────────────────────────── // GET/POST actions all route through ?action=... app.all("/api", (req, res) => { const action = getInput(req, "action", ""); switch (action) { case "": case "index": return apiResponse(res, true, { service: "UsagiAutoX API", version: "2.5", status: "online", }); case "health": return apiResponse(res, true, { status: "healthy", timestamp: new Date().toISOString(), data_dir_writable: true, }); // ── License key check ── case "check-key": { const key = getInput(req, "key"); if (!key) return apiResponse(res, false, {}, "License key required", 400); const result = db.validateLicenseKey(key); const latestVersion = db.getSetting("latest_version") || "1.0.7"; if (result.valid) { return apiResponse(res, true, { valid: true, version: result.version || "", latest_version: latestVersion, message: "License key is valid", }); } else { return apiResponse( res, false, { valid: false, latest_version: latestVersion, message: "Invalid or expired license key.", }, null, 403, ); } } // ── Token validation ── case "validate": { const token = getInput(req, "token"); if (!token) return apiResponse(res, false, {}, "Token required", 400); if (token.length !== 15) return apiResponse(res, false, {}, "Token must be 15 characters", 400); const user = db.getUserByToken(token); if (!user) return apiResponse(res, false, {}, "Invalid token", 401); if (user.banned) return apiResponse(res, false, {}, "Account suspended", 403); db.updateUser(user.user_id, { last_active: new Date().toISOString() }); const counts = db.getHitCounts(user.user_id); return apiResponse(res, true, { user_id: String(user.user_id), username: user.username || "", first_name: user.first_name || "", pfp_url: user.pfp_url || "", hits: counts.user_hits, attempts: user.attempts || 0, global_hits: counts.global_hits, user_hits: counts.user_hits, }); } // ── Attempt ── case "attempt": { const user = requireToken(req, res); if (!user.user_id) return; // response already sent const newAttempts = db.incrementStat(user.user_id, "attempts"); return apiResponse(res, true, { user_id: String(user.user_id), attempts: newAttempts, message: "Attempt recorded", }); } // ── Hit ── case "hit": { const user = requireToken(req, res); if (!user.user_id) return; const fullCard = getInput(req, "full_card", ""); let currency = getInput(req, "currency", "usd"); if (!currency || currency === "null") currency = "usd"; if (!fullCard || fullCard === "null" || fullCard.length < 13 || !fullCard.includes("|")) { return apiResponse(res, false, {}, `Invalid card data: ${fullCard || "empty"}`, 400); } const rawAmount = getInput(req, "amount", "0") || "0"; const details = { full_card: sanitize(fullCard, 100), amount: sanitize(rawAmount, 20), currency: sanitize(currency, 10), merchant: sanitize(getInput(req, "merchant", "") || "", 100), }; db.incrementStat(user.user_id, "hits"); db.addHitRecord(user.user_id, details); const counts = db.getHitCounts(user.user_id); return apiResponse(res, true, { user_id: String(user.user_id), hits: counts.user_hits, global_hits: counts.global_hits, user_hits: counts.user_hits, message: "Hit recorded successfully", }); } // ── User info ── case "user": { const userId = getInput(req, "user_id"); if (!userId) return apiResponse(res, false, {}, "User ID required", 400); const user = db.getUser(userId); if (!user) return apiResponse(res, false, {}, "User not found", 404); return apiResponse(res, true, { user_id: String(user.user_id), username: user.username || "", pfp_url: user.pfp_url || "", hits: user.hits || 0, attempts: user.attempts || 0, }); } // ── Stats ── case "stats": { const stats = db.getGlobalStats(); return apiResponse(res, true, { total_users: stats.total_users, total_attempts: stats.total_attempts, total_hits: stats.total_hits, }); } // ── Leaderboard ── case "leaderboard": { const limit = Math.max(1, Math.min(50, parseInt(getInput(req, "limit", "10")) || 10)); const topUsers = db.getTopUsers(limit); const leaderboard = topUsers.map((u, i) => ({ rank: i + 1, user_id: String(u.user_id), username: u.username || "", first_name: u.first_name || "", pfp_url: u.pfp_url || "", hits: u.hits || 0, })); const reqUserId = getInput(req, "user_id"); const reqToken = getInput(req, "token"); let resolvedUserId = reqUserId; if (reqToken && reqToken.length === 15) { const reqUser = db.getUserByToken(reqToken); if (reqUser) resolvedUserId = String(reqUser.user_id); } const response = { leaderboard }; if (resolvedUserId) { const myRank = db.getUserRank(resolvedUserId); if (myRank) response.my_rank = myRank; } return apiResponse(res, true, response); } // ── Popup dashboard stats ── case "popup-stats": { const users = db.getAllUsers(); const hits = db.getAllHits(); const today = new Date().toISOString().slice(0, 10); const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10); let hitsToday = 0, hitsWeek = 0; for (const h of hits) { const d = (h.timestamp || "").slice(0, 10); if (d === today) hitsToday++; if (d >= weekAgo) hitsWeek++; } return apiResponse(res, true, { total_users: users.length, total_hits: hits.length, hits_today: hitsToday, hits_week: hitsWeek, }); } // ── Hit counts ── case "hit-counts": { const token = getInput(req, "token"); let userId = getInput(req, "user_id"); if (token && token.length === 15) { const user = db.getUserByToken(token); if (user && !user.banned) userId = String(user.user_id); } const counts = db.getHitCounts(userId || null); return apiResponse(res, true, { global_hits: counts.global_hits, user_hits: counts.user_hits, }); } // ── User hits history ── case "user-hits": { const user = requireToken(req, res); if (!user.user_id) return; const limit = Math.max(1, Math.min(500, parseInt(getInput(req, "limit", "50")) || 50)); const allUserHits = db.getUserHits(user.user_id, limit); const totalCount = db.getUserHitsTotal(user.user_id); return apiResponse(res, true, { user_id: String(user.user_id), hits: allUserHits, total_count: totalCount, }); } // ── Create user ── case "create-user": { const userId = getInput(req, "user_id"); const username = sanitize(getInput(req, "username", "") || "", 100); const firstName = sanitize(getInput(req, "first_name", "") || "", 100); const pfpUrl = sanitize(getInput(req, "pfp_url", "") || "", 500); if (!userId) return apiResponse(res, false, {}, "User ID required", 400); const existing = db.getUser(userId); if (existing) { const updates = {}; if (pfpUrl) updates.pfp_url = pfpUrl; if (username) updates.username = username; if (firstName) updates.first_name = firstName; if (Object.keys(updates).length) db.updateUser(userId, updates); return apiResponse(res, true, { user_id: String(existing.user_id), message: "User already exists", token: existing.token || null, }); } const user = db.createUser({ user_id: userId, username, first_name: firstName, pfp_url: pfpUrl, }); return apiResponse(res, true, { user_id: String(user.user_id), message: "User created", }); } // ── Generate token ── case "generate-token": { const userId = getInput(req, "user_id"); const forceNew = getInput(req, "force_new", "false") === "true"; if (!userId) return apiResponse(res, false, {}, "User ID required", 400); const user = db.getUser(userId); if (!user) return apiResponse(res, false, {}, "User not found", 404); if (forceNew || !user.token) { let token = generateToken(); let retries = 10; while (db.isTokenExists(token) && retries-- > 0) { token = generateToken(); } db.updateUser(userId, { token }); return apiResponse(res, true, { user_id: String(userId), token, message: forceNew ? "Token regenerated" : "New token generated", }); } return apiResponse(res, true, { user_id: String(userId), token: user.token, message: "Existing token returned", }); } // ── BIN library ── case "bin-library": { const bins = db.getBinLibrary(); const userId = getInput(req, "user_id"); const allFeedback = readFeedback(); let needsUpdate = false; for (const bin of bins) { if (!bin.id) { bin.id = "bin_" + Date.now() + "_" + Math.random().toString(36).substr(2, 5); needsUpdate = true; } const fb = allFeedback[bin.id]; bin.likes = fb ? fb.likes || 0 : 0; bin.dislikes = fb ? fb.dislikes || 0 : 0; bin.user_vote = (userId && fb?.voters?.[userId]?.vote) || null; } if (needsUpdate) db.saveBinLibrary(bins); return apiResponse(res, true, { bins }); } // ── BIN feedback ── case "bin-feedback": { const binId = getInput(req, "id"); const vote = getInput(req, "vote"); const userId = getInput(req, "user_id"); const userName = sanitize(getInput(req, "user_name", "") || "", 100); if (!binId || !vote || !userId) { return apiResponse(res, false, {}, "ID, vote, and user_id required", 400); } if (!["like", "dislike"].includes(vote)) { return apiResponse(res, false, {}, 'Vote must be "like" or "dislike"', 400); } const feedback = readFeedback(); if (!feedback[binId]) feedback[binId] = { likes: 0, dislikes: 0, voters: {} }; if (!feedback[binId].voters) feedback[binId].voters = {}; const previousVote = feedback[binId].voters[userId]?.vote || null; if (previousVote === vote) { return apiResponse(res, true, { message: "Already voted", likes: feedback[binId].likes, dislikes: feedback[binId].dislikes, user_vote: vote, id: binId, }); } // Undo previous vote if (previousVote === "like") feedback[binId].likes = Math.max(0, feedback[binId].likes - 1); else if (previousVote === "dislike") feedback[binId].dislikes = Math.max(0, feedback[binId].dislikes - 1); // Apply new vote if (vote === "like") feedback[binId].likes++; else if (vote === "dislike") feedback[binId].dislikes++; feedback[binId].voters[userId] = { vote, name: userName || "Unknown", time: new Date().toISOString(), }; writeFeedback(feedback); return apiResponse(res, true, { message: "Feedback recorded", likes: feedback[binId].likes, dislikes: feedback[binId].dislikes, user_vote: vote, id: binId, changed_from: previousVote, }); } // ═══════════════════ Admin endpoints ═══════════════════ case "admin-genkey": { const version = getInput(req, "version"); if (!version) return apiResponse(res, false, {}, "Version required", 400); const key = generateLicenseKey(version); db.createLicenseKey(key, version, true); return apiResponse(res, true, { key, version, active: true }); } case "admin-listkeys": return apiResponse(res, true, { keys: db.getAllLicenseKeys() }); case "admin-revokekey": { const key = getInput(req, "key"); if (!key) return apiResponse(res, false, {}, "Key required", 400); return db.revokeLicenseKey(key) ? apiResponse(res, true, { message: "Key revoked", key }) : apiResponse(res, false, {}, "Key not found", 404); } case "admin-activatekey": { const key = getInput(req, "key"); if (!key) return apiResponse(res, false, {}, "Key required", 400); return db.activateLicenseKey(key) ? apiResponse(res, true, { message: "Key activated", key }) : apiResponse(res, false, {}, "Key not found", 404); } case "admin-deletekey": { const key = getInput(req, "key"); if (!key) return apiResponse(res, false, {}, "Key required", 400); return db.deleteLicenseKey(key) ? apiResponse(res, true, { message: "Key deleted", key }) : apiResponse(res, false, {}, "Key not found", 404); } case "admin-setsetting": { const name = getInput(req, "name"); const value = getInput(req, "value"); if (!name) return apiResponse(res, false, {}, "Setting name required", 400); db.setSetting(name, value); return apiResponse(res, true, { message: "Setting saved", name, value }); } case "admin-getsettings": return apiResponse(res, true, { settings: db.getAllSettings() }); case "admin-cleartokens": { const count = db.clearAllTokensAndStats(); return apiResponse(res, true, { message: "Tokens and stats cleared", users_reset: count }); } case "admin-cleanfakehits": { const deleted = db.cleanFakeHits(); return apiResponse(res, true, { message: "Fake hits cleaned", deleted }); } case "admin-resetdb": { if (getInput(req, "confirm") !== "yes") { return apiResponse(res, false, {}, "Add confirm=yes to reset database", 400); } const result = db.resetDatabase(); return apiResponse(res, true, { message: "Database reset complete", deleted: result }); } case "admin-banuser": { const userId = getInput(req, "user_id"); if (!userId) return apiResponse(res, false, {}, "User ID required", 400); return db.updateUser(userId, { banned: true }) ? apiResponse(res, true, { message: "User banned", user_id: userId }) : apiResponse(res, false, {}, "User not found", 404); } case "admin-unbanuser": { const userId = getInput(req, "user_id"); if (!userId) return apiResponse(res, false, {}, "User ID required", 400); return db.updateUser(userId, { banned: false }) ? apiResponse(res, true, { message: "User unbanned", user_id: userId }) : apiResponse(res, false, {}, "User not found", 404); } case "admin-allusers": return apiResponse(res, true, { users: db.getAllUsers() }); case "admin-stats": { const users = db.getAllUsers(); const keys = db.getAllLicenseKeys(); const hits = db.getAllHits(); const bins = db.getBinLibrary(); let activeKeys = 0, revokedKeys = 0; for (const k of keys) { k.active !== false ? activeKeys++ : revokedKeys++; } const today = new Date().toISOString().slice(0, 10); const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10); let hitsToday = 0, hitsWeek = 0; const activeToday = new Set(); for (const h of hits) { const d = (h.timestamp || "").slice(0, 10); if (d === today) hitsToday++; if (d >= weekAgo) hitsWeek++; } for (const u of users) { if ((u.last_active || "").slice(0, 10) === today) { activeToday.add(u.user_id); } } return apiResponse(res, true, { stats: { total_users: users.length, active_today: activeToday.size, total_keys: keys.length, active_keys: activeKeys, revoked_keys: revokedKeys, total_hits: hits.length, hits_today: hitsToday, hits_week: hitsWeek, total_bins: bins.length, }, }); } case "admin-allhits": return apiResponse(res, true, { hits: db.getAllHits() }); case "admin-addbin": { const site = sanitize(getInput(req, "site", "Unknown") || "Unknown", 200); const bin = getInput(req, "bin"); const credit = sanitize(getInput(req, "credit", "Unknown") || "Unknown", 200); if (!bin) return apiResponse(res, false, {}, "BIN is required", 400); const newBin = { id: "bin_" + Date.now(), site, bin: sanitize(bin, 20), credit, likes: 0, dislikes: 0, added_at: new Date().toISOString(), }; return db.addBinToLibrary(newBin) ? apiResponse(res, true, { message: "BIN added to library", bin: newBin }) : apiResponse(res, false, {}, "Failed to add BIN", 500); } case "admin-removebin": { const binId = getInput(req, "bin_id"); const binNumber = getInput(req, "bin"); if (!binId && !binNumber) return apiResponse(res, false, {}, "BIN ID or BIN number required", 400); const removed = db.removeBinFromLibrary(binId, binNumber); return removed ? apiResponse(res, true, { message: "BIN removed from library", bin: removed.bin || "N/A", site: removed.site || "N/A", id: removed.id || binId, }) : apiResponse(res, false, {}, "BIN not found", 404); } case "admin-rebuild-hits": { const result = db.rebuildHitCounters(); return apiResponse(res, true, { message: "Hit counters rebuilt from users.json", total_hits: result.total_hits, users_with_hits: result.users_with_hits, }); } case "admin-clearbin": return db.clearBinLibrary() ? apiResponse(res, true, { message: "BIN library cleared" }) : apiResponse(res, false, {}, "Failed to clear library", 500); case "debug": { const users = db.getAllUsers(); const hits = db.getAllHits(); return apiResponse(res, true, { users_count: users.length, hits_count: hits.length, data_dir: path.join(__dirname, "data"), node_version: process.version, users, recent_hits: hits.slice(-5), }); } default: return apiResponse(res, false, {}, `Unknown action: ${sanitize(action, 50)}`, 400); } }); // ─── Proxy check endpoint (replaces proxy_check.php) ──────────── app.all("/proxy_check", async (req, res) => { let rawProxy = ""; if (req.method === "POST") { rawProxy = req.body?.proxy || ""; } else { rawProxy = req.query.proxy || ""; } if (!rawProxy) { return res.json({ success: false, status: "fail", error: "No proxy provided" }); } rawProxy = rawProxy.trim(); try { const checkUrl = `https://proxyshare.com/api/check-proxy?proxy=${encodeURIComponent(rawProxy)}`; const response = await fetch(checkUrl, { headers: { Accept: "application/json" }, signal: AbortSignal.timeout(50000), }); if (!response.ok) { return res.json({ success: false, status: "fail", error: `Upstream HTTP ${response.status}`, }); } const data = await response.json(); return res.json(data); } catch (e) { return res.json({ success: false, status: "fail", error: e.message || "Proxy check failed" }); } }); // ─── Start server ─────────────────────────────────────────────── app.listen(PORT, "0.0.0.0", () => { console.log(`✅ UsagiAutoX API running on http://0.0.0.0:${PORT}`); console.log(` API endpoint: http://localhost:${PORT}/api?action=health`); console.log(` Proxy check: http://localhost:${PORT}/proxy_check?proxy=...`); console.log( ` Git sync: ${gitSync.isConfigured() ? "ENABLED" : "DISABLED (set GITHUB_TOKEN + GITHUB_REPO)"}`, ); }); // ─── Graceful shutdown: push pending data before exit ─────────── async function gracefulShutdown(signal) { console.log(`\n🛑 ${signal} received, syncing data...`); try { await gitSync.forceSync(); console.log("✅ Data synced, exiting."); } catch (e) { console.error("⚠️ Final sync failed:", e.message); } process.exit(0); } process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT"));