commit 4c62e945354fc266c840c4e903c3205b50fc0c12 Author: Azimkin Date: Thu Oct 15 07:46:16 2026 +0000 Добавить бота копилки huEVO. Сессия через Bun.WebView, в каждом интервале уходит 190–240 кликов с паузой под лимит сервера. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25cc836 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +env +profile/ diff --git a/huevo-clicker.service b/huevo-clicker.service new file mode 100644 index 0000000..d9bd45b --- /dev/null +++ b/huevo-clicker.service @@ -0,0 +1,17 @@ +[Unit] +Description=huEVO clicker +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/huevo-clicker +EnvironmentFile=/opt/huevo-clicker/env +Environment=HOME=/root +ExecStart=/usr/local/bin/bun /opt/huevo-clicker/index.ts +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..1929c56 --- /dev/null +++ b/index.ts @@ -0,0 +1,194 @@ +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); +}