const username = process.env.HUEVO_USER || ""; const password = process.env.HUEVO_PASSWORD || ""; const profile = process.env.HUEVO_PROFILE || "/opt/huevo-clicker/profile"; const CLICKS_MIN = 190; const CLICKS_MAX = 240; const CLICKS_PER_SEC = 15.5; const MIN_GAP_MS = 8_000; const WAIT_PAD_MS = 300; if (!username || !password) { console.error("HUEVO_USER and HUEVO_PASSWORD are required"); process.exit(1); } function log(message: string) { console.log(`${new Date().toISOString()} ${message}`); } function batchSize() { return CLICKS_MIN + Math.floor(Math.random() * (CLICKS_MAX - CLICKS_MIN + 1)); } function waitMsFor(clicks: number) { return Math.max(MIN_GAP_MS, Math.ceil((clicks / CLICKS_PER_SEC) * 1000) + WAIT_PAD_MS); } function openView() { return new Bun.WebView({ width: 1280, height: 900, dataStore: { directory: profile }, backend: { type: "chrome", url: false, path: "/usr/bin/google-chrome-stable", argv: ["--no-sandbox", "--disable-dev-shm-usage"], stderr: "ignore", }, }); } async function pageState(view: Bun.WebView) { return view.evaluate(`(() => ({ url: location.href, challenge: ((document.body && document.body.innerText) || "").includes("Проверка браузера"), login: !!document.getElementById("login-form"), clicker: !!document.getElementById("ck-zone"), }))()`) as Promise<{ url: string; challenge: boolean; login: boolean; clicker: boolean }>; } async function submitLogin(view: Bun.WebView) { await view.evaluate(`(() => { const form = document.getElementById("login-form"); if (!form) return false; form.username.value = ${JSON.stringify(username)}; form.password.value = ${JSON.stringify(password)}; const remember = form.querySelector("input[name=remember]"); if (remember) remember.checked = true; form.requestSubmit(); return true; })()`); } async function ensureSession(view: Bun.WebView) { await view.navigate("https://huevo.su/clicker"); let submitted = false; for (let i = 0; i < 40; i++) { let state: { url: string; challenge: boolean; login: boolean; clicker: boolean }; try { state = await pageState(view); } catch { await Bun.sleep(400); continue; } if (state.clicker) return; if (state.login) { if (!submitted) { await submitLogin(view); submitted = true; log("login submitted"); } } else if (!state.challenge) { submitted = false; try { await view.navigate("https://huevo.su/clicker"); } catch { // navigation already in flight } } await Bun.sleep(400); } throw new Error("clicker session was not established"); } async function sendClicks(view: Bun.WebView, clicks: number) { return view.evaluate(`(async () => { const response = await fetch("/api/clicker", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "clicks=" + ${clicks}, }); const text = await response.text(); let data = null; try { data = JSON.parse(text); } catch {} return { status: response.status, login: !!document.getElementById("login-form"), data, text: text.slice(0, 300), }; })()`) as Promise<{ status: number; login: boolean; data: null | { ok?: boolean; error?: string; retry?: number; message?: string; counted?: number; clicks?: number; balance_text?: string; day_coins?: number; cap?: number; earned?: number; capped?: boolean; }; text: string; }>; } log(`huevo clicker started, bun ${Bun.version}, batch ${CLICKS_MIN}-${CLICKS_MAX}`); for (;;) { const view = openView(); try { await ensureSession(view); log("session ready"); for (;;) { const clicks = batchSize(); const waitMs = waitMsFor(clicks); await Bun.sleep(waitMs); let result: Awaited>; try { result = await sendClicks(view, clicks); } catch (error) { log(`request failed: ${error instanceof Error ? error.message : String(error)}`); break; } if (result.login || result.status === 401 || result.status === 403) { log(`auth lost (http ${result.status}), logging in again`); await ensureSession(view); continue; } const data = result.data; if (!data) { log(`http ${result.status} non-json: ${result.text.replace(/\s+/g, " ").slice(0, 180)}`); continue; } if (data.ok) { log( `ok sent=${clicks} counted=${data.counted} wait=${waitMs}ms clicks=${data.clicks} balance=${data.balance_text} today=${data.day_coins}/${data.cap} earned=${data.earned ?? 0}`, ); continue; } if (data.error === "slow") { const retryMs = Math.ceil((data.retry || 5) * 1000) + WAIT_PAD_MS; log(`slow, retry in ${Math.round(retryMs / 1000)}s`); await Bun.sleep(retryMs); continue; } if (data.error === "full" || data.capped) { log("daily cap reached, pause 15m"); await Bun.sleep(15 * 60 * 1000); continue; } log(`rejected ${data.error || "unknown"} ${data.message || ""}`.trim()); } } catch (error) { log(`restart: ${error instanceof Error ? error.message : String(error)}`); } finally { try { view.close(); } catch {} } await Bun.sleep(5_000); }