Добавить бота копилки huEVO.
Сессия через Bun.WebView, в каждом интервале уходит 190–240 кликов с паузой под лимит сервера. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
env
|
||||
profile/
|
||||
17
huevo-clicker.service
Normal file
17
huevo-clicker.service
Normal file
@@ -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
|
||||
194
index.ts
Normal file
194
index.ts
Normal file
@@ -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<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);
|
||||
}
|
||||
Reference in New Issue
Block a user