Files
huevo-clicker/index.ts
Azimkin 4c62e94535 Добавить бота копилки huEVO.
Сессия через Bun.WebView, в каждом интервале уходит 190–240 кликов с паузой под лимит сервера.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 07:46:16 +00:00

195 lines
5.4 KiB
TypeScript

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<ReturnType<typeof sendClicks>>;
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);
}