auto / bypasscf_likeUser.js
github-actions[bot]
GitHub deploy: cdd1251aafcc7267d5c51954d23cb4565eba35a0
cc9afac
Raw
History Blame Contribute Delete
19 kB
import fs from "fs";
import path from "path";
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import TelegramBot from "node-telegram-bot-api";
dotenv.config();
// 截图保存的文件夹
// const screenshotDir = "screenshots";
// if (!fs.existsSync(screenshotDir)) {
// fs.mkdirSync(screenshotDir);
// }
puppeteer.use(StealthPlugin());
// Load the default .env file
if (fs.existsSync(".env.local")) {
console.log("Using .env.local file to supply config environment variables");
const envConfig = dotenv.parse(fs.readFileSync(".env.local"));
for (const k in envConfig) {
process.env[k] = envConfig[k];
}
} else {
console.log(
"Using .env file to supply config environment variables, you can create a .env.local file to overwrite defaults, it doesn't upload to git"
);
}
// 读取以分钟为单位的运行时间限制
const runTimeLimitMinutes = process.env.RUN_TIME_LIMIT_MINUTES || 20;
// 将分钟转换为毫秒
const runTimeLimitMillis = runTimeLimitMinutes * 60 * 1000;
console.log(
`运行时间限制为:${runTimeLimitMinutes} 分钟 (${runTimeLimitMillis} 毫秒)`
);
// 设置一个定时器,在运行时间到达时终止进程
const shutdownTimer = setTimeout(() => {
console.log("时间到,Reached time limit, shutting down the process...");
process.exit(0); // 退出进程
}, runTimeLimitMillis);
const token = process.env.TELEGRAM_BOT_TOKEN;
const chatId = process.env.TELEGRAM_CHAT_ID;
const specificUser = process.env.SPECIFIC_USER || "14790897";
const maxConcurrentAccounts = 4; // 每批最多同时运行的账号数
const usernames = process.env.USERNAMES.split(",");
const passwords = process.env.PASSWORDS.split(",");
const loginUrl = process.env.WEBSITE || "https://linux.do"; //在GitHub action环境里它不能读取默认环境变量,只能在这里设置默认值
const delayBetweenInstances = 10000;
const totalAccounts = usernames.length; // 总的账号数
const delayBetweenBatches =
runTimeLimitMillis / Math.ceil(totalAccounts / maxConcurrentAccounts);
let bot;
if (token && chatId) {
bot = new TelegramBot(token);
}
function sendToTelegram(message) {
if (!bot) return;
bot
.sendMessage(chatId, message)
.then(() => {
console.log("Telegram message sent successfully");
})
.catch((error) => {
console.error("Error sending Telegram message:", error);
});
}
//随机等待时间
function delayClick(time) {
return new Promise(function (resolve) {
setTimeout(resolve, time);
});
}
(async () => {
try {
if (usernames.length !== passwords.length) {
console.log(usernames.length, usernames, passwords.length, passwords);
throw new Error("用户名和密码的数量不匹配!");
}
// 并发启动浏览器实例进行登录
const loginTasks = usernames.map((username, index) => {
const password = passwords[index];
const delay = (index % maxConcurrentAccounts) * delayBetweenInstances; // 使得每一组内的浏览器可以分开启动
return () => {
// 确保这里返回的是函数
return new Promise((resolve, reject) => {
setTimeout(() => {
launchBrowserForUser(username, password)
.then(resolve)
.catch(reject);
}, delay);
});
};
});
// 依次执行每个批次的任务
for (let i = 0; i < totalAccounts; i += maxConcurrentAccounts) {
console.log(`当前批次:${i + 1} - ${i + maxConcurrentAccounts}`);
// 执行每批次最多 4 个账号
const batch = loginTasks
.slice(i, i + maxConcurrentAccounts)
.map(async (task) => {
const { browser } = await task(); // 运行任务并获取浏览器实例
return browser;
}); // 等待当前批次的任务完成
const browsers = await Promise.all(batch); // Task里面的任务本身是没有进行await的, 所以会继续执行下面的代码
// 如果还有下一个批次,等待指定的时间,同时,如果总共只有一个账号,也需要继续运行
if (i + maxConcurrentAccounts < totalAccounts || i === 0) {
console.log(`等待 ${delayBetweenBatches / 1000} 秒`);
await new Promise((resolve) =>
setTimeout(resolve, delayBetweenBatches)
);
} else {
console.log("没有下一个批次,即将结束");
}
console.log(
`批次 ${
Math.floor(i / maxConcurrentAccounts) + 1
} 完成,关闭浏览器...,浏览器对象:${browsers}`
);
// 关闭所有浏览器实例
for (const browser of browsers) {
await browser.close();
}
}
console.log("所有账号登录操作已完成");
// 等待所有登录操作完成
// await Promise.all(loginTasks);
} catch (error) {
// 错误处理逻辑
console.error("发生错误:", error);
if (token && chatId) {
sendToTelegram(`${error.message}`);
}
}
})();
async function launchBrowserForUser(username, password) {
let browser = null; // 在 try 之外声明 browser 变量
try {
console.log("当前用户:", username);
const browserOptions = {
headless: "auto",
args: ["--no-sandbox", "--disable-setuid-sandbox"], // Linux 需要的安全设置
};
// 如果环境变量不是 'dev',则添加代理配置
// if (process.env.ENVIRONMENT !== "dev") {
// browserOptions["proxy"] = {
// host: "38.154.227.167",
// port: "5868",
// username: "pqxujuyl",
// password: "y1nmb5kjbz9t",
// };
// }
var { connect } = await import("puppeteer-real-browser");
const { page, browser: newBrowser } = await connect(browserOptions);
browser = newBrowser; // 将 browser 初始化
// 启动截图功能
// takeScreenshots(page);
//登录操作
await navigatePage(loginUrl, page, browser);
await delayClick(8000);
// 设置额外的 headers
await page.setExtraHTTPHeaders({
"accept-language": "en-US,en;q=0.9",
});
// 验证 `navigator.webdriver` 属性是否为 undefined
// const isWebDriverUndefined = await page.evaluate(() => {
// return `${navigator.webdriver}`;
// });
// console.log("navigator.webdriver is :", isWebDriverUndefined); // 输出应为 false
page.on("pageerror", (error) => {
console.error(`Page error: ${error.message}`);
});
page.on("error", async (error) => {
// console.error(`Error: ${error.message}`);
// 检查是否是 localStorage 的访问权限错误
if (
error.message.includes(
"Failed to read the 'localStorage' property from 'Window'"
)
) {
console.log("Trying to refresh the page to resolve the issue...");
await page.reload(); // 刷新页面
// 重新尝试你的操作...
}
});
page.on("console", async (msg) => {
// console.log("PAGE LOG:", msg.text());
// 使用一个标志变量来检测是否已经刷新过页面
if (
!page._isReloaded &&
msg.text().includes("the server responded with a status of 429")
) {
// 设置标志变量为 true,表示即将刷新页面
page._isReloaded = true;
//由于油候脚本它这个时候可能会导航到新的网页,会导致直接执行代码报错,所以使用这个来在每个新网页加载之前来执行
await page.evaluateOnNewDocument(() => {
localStorage.setItem("autoLikeEnabled", "false");
});
// 等待一段时间,比如 3 秒
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Retrying now...");
// 尝试刷新页面
// await page.reload();
}
});
// //登录操作
console.log("登录操作");
await login(page, username, password);
// 查找具有类名 "avatar" 的 img 元素验证登录是否成功
const avatarImg = await page.$("img.avatar");
if (avatarImg) {
console.log("找到avatarImg,登录成功");
} else {
console.log("未找到avatarImg,登录失败");
throw new Error("登录失败");
}
//真正执行阅读脚本
const externalScriptPath = path.join(
dirname(fileURLToPath(import.meta.url)),
"index_likeUser.js"
);
const externalScript = fs.readFileSync(externalScriptPath, "utf8");
// 在每个新的文档加载时执行外部脚本
await page.evaluateOnNewDocument(
(...args) => {
const [specificUser, scriptToEval] = args;
localStorage.setItem("read", true);
localStorage.setItem("specificUser", specificUser);
localStorage.setItem("isFirstRun", "false");
console.log("当前点赞用户:", specificUser);
eval(scriptToEval);
},
specificUser,
externalScript
); //变量必须从外部显示的传入, 因为在浏览器上下文它是读取不了的
// 添加一个监听器来监听每次页面加载完成的事件
page.on("load", async () => {
// await page.evaluate(externalScript); //因为这个是在页面加载好之后执行的,而脚本是在页面加载好时刻来判断是否要执行,由于已经加载好了,脚本就不会起作用
});
// 如果是Linuxdo,就导航到我的帖子,但我感觉这里写没什么用,因为外部脚本已经定义好了,不对,这里不会点击按钮,所以不会跳转,需要手动跳转
if (loginUrl == "https://linux.do") {
await page.goto("https://linux.do/t/topic/13716/630", {
waitUntil: "domcontentloaded",
});
} else if (loginUrl == "https://meta.appinn.net") {
await page.goto("https://meta.appinn.net/t/topic/52006", {
waitUntil: "domcontentloaded",
});
} else {
await page.goto(`${loginUrl}/t/topic/1`, {
waitUntil: "domcontentloaded",
});
}
if (token && chatId) {
sendToTelegram(`${username} 登录成功`);
}
return { browser };
} catch (err) {
// throw new Error(err);
console.log("Error in launchBrowserForUser:", err);
if (token && chatId) {
sendToTelegram(`${err.message}`);
}
return { browser }; // 错误时仍然返回 browser
}
}
async function login(page, username, password, retryCount = 3) {
// 使用XPath查询找到包含"登录"或"login"文本的按钮
let loginButtonFound = await page.evaluate(() => {
let loginButton = Array.from(document.querySelectorAll("button")).find(
(button) =>
button.textContent.includes("登录") ||
button.textContent.includes("login")
); // 注意loginButton 变量在外部作用域中是无法被 page.evaluate 内部的代码直接修改的。page.evaluate 的代码是在浏览器环境中执行的,这意味着它们无法直接影响 Node.js 环境中的变量
// 如果没有找到,尝试根据类名查找
if (!loginButton) {
loginButton = document.querySelector(".login-button");
}
if (loginButton) {
loginButton.click();
console.log("Login button clicked.");
return true; // 返回true表示找到了按钮并点击了
} else {
console.log("Login button not found.");
return false; // 返回false表示没有找到按钮
}
});
if (!loginButtonFound) {
if (loginUrl == "https://meta.appinn.net") {
await page.goto("https://meta.appinn.net/t/topic/52006", {
waitUntil: "domcontentloaded",
});
await page.click(".discourse-reactions-reaction-button");
} else {
await page.goto(`${loginUrl}/t/topic/1`, {
waitUntil: "domcontentloaded",
});
await page.click(".discourse-reactions-reaction-button");
}
}
// 等待用户名输入框加载
await page.waitForSelector("#login-account-name");
// 模拟人类在找到输入框后的短暂停顿
await delayClick(500); // 延迟500毫秒
// 清空输入框并输入用户名
await page.click("#login-account-name", { clickCount: 3 });
await page.type("#login-account-name", username, {
delay: 100,
}); // 输入时在每个按键之间添加额外的延迟
// 等待密码输入框加载
await page.waitForSelector("#login-account-password");
// 模拟人类在输入用户名后的短暂停顿
// delayClick; // 清空输入框并输入密码
await page.click("#login-account-password", { clickCount: 3 });
await page.type("#login-account-password", password, {
delay: 100,
});
// 模拟人类在输入完成后思考的短暂停顿
await delayClick(1000);
// 假设登录按钮的ID是'login-button',点击登录按钮
await page.waitForSelector("#login-button");
await page.click("#login-button");
await delayClick(500); // 模拟在点击登录按钮前的短暂停顿
try {
await Promise.all([
page.waitForNavigation({ waitUntil: "domcontentloaded" }), // 等待 页面跳转 DOMContentLoaded 事件
// 去掉上面一行会报错:Error: Execution context was destroyed, most likely because of a navigation. 可能是因为之后没等页面加载完成就执行了脚本
page.click("#login-button", { force: true }), // 点击登录按钮触发跳转
]); //注意如果登录失败,这里会一直等待跳转,导致脚本执行失败 这点四个月之前你就发现了结果今天又遇到(有个用户遇到了https://linux.do/t/topic/169209/82),但是你没有在这个报错你提示我8.5
} catch (error) {
const alertError = await page.$(".alert.alert-error");
if (alertError) {
const alertText = await page.evaluate((el) => el.innerText, alertError); // 使用 evaluate 获取 innerText
if (
alertText.includes("incorrect") ||
alertText.includes("Incorrect ") ||
alertText.includes("不正确")
) {
throw new Error(
`非超时错误,请检查用户名密码是否正确,失败用户 ${username}, 错误信息:${alertText}`
);
} else {
throw new Error(
`非超时错误,也不是密码错误,失败用户 ${username},错误信息:${alertText}`
);
}
} else {
if (retryCount > 0) {
console.log("Retrying login...");
await delayClick(2000); // 增加重试前的延迟
return await login(page, username, password, retryCount - 1);
} else {
throw new Error(
`Navigation timed out in login.超时了,可能是IP质量问题,失败用户 ${username},
${error}`
); //{password}
}
}
}
await delayClick(1000);
}
async function navigatePage(url, page, browser) {
await page.goto(url, { waitUntil: "domcontentloaded" }); //如果使用默认的load,linux下页面会一直加载导致无法继续执行
const startTime = Date.now(); // 记录开始时间
let pageTitle = await page.title(); // 获取当前页面标题
while (pageTitle.includes("Just a moment")) {
console.log("The page is under Cloudflare protection. Waiting...");
await delayClick(2000); // 每次检查间隔2秒
// 重新获取页面标题
pageTitle = await page.title();
// 检查是否超过15秒
if (Date.now() - startTime > 35000) {
console.log("Timeout exceeded, aborting actions.");
await browser.close();
return; // 超时则退出函数
}
}
console.log("页面标题:", pageTitle);
}
// 每秒截图功能
async function takeScreenshots(page) {
let screenshotIndex = 0;
setInterval(async () => {
screenshotIndex++;
const screenshotPath = path.join(
screenshotDir,
`screenshot-${screenshotIndex}.png`
);
try {
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved: ${screenshotPath}`);
} catch (error) {
console.error("Error taking screenshot:", error);
}
}, 1000);
// 注册退出时删除文件夹的回调函数
process.on("exit", () => {
try {
fs.rmdirSync(screenshotDir, { recursive: true });
console.log(`Deleted folder: ${screenshotDir}`);
} catch (error) {
console.error(`Error deleting folder ${screenshotDir}:`, error);
}
});
}
import express from "express";
const healthApp = express();
const HEALTH_PORT = process.env.HEALTH_PORT || 7860;
// 健康探针路由
healthApp.get("/health", (req, res) => {
const memoryUsage = process.memoryUsage();
// 将字节转换为MB
const memoryUsageMB = {
rss: `${(memoryUsage.rss / (1024 * 1024)).toFixed(2)} MB`, // 转换为MB并保留两位小数
heapTotal: `${(memoryUsage.heapTotal / (1024 * 1024)).toFixed(2)} MB`,
heapUsed: `${(memoryUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB`,
external: `${(memoryUsage.external / (1024 * 1024)).toFixed(2)} MB`,
arrayBuffers: `${(memoryUsage.arrayBuffers / (1024 * 1024)).toFixed(2)} MB`,
};
const healthData = {
status: "OK",
timestamp: new Date().toISOString(),
memoryUsage: memoryUsageMB,
uptime: process.uptime().toFixed(2), // 保留两位小数
};
res.status(200).json(healthData);
});
healthApp.get("/", (req, res) => {
res.send(`
<html>
<head>
<title>Auto Read</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
max-width: 600px;
text-align: center;
}
h1 {
color: #007bff;
}
p {
font-size: 18px;
margin: 15px 0;
}
a {
color: #007bff;
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
footer {
margin-top: 20px;
font-size: 14px;
color: #555;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to the Auto Read App</h1>
<p>You can check the server's health at <a href="/health">/health</a>.</p>
<p>GitHub: <a href="https://github.com/14790897/auto-read-liunxdo" target="_blank">https://github.com/14790897/auto-read-liunxdo</a></p>
<footer>&copy; 2024 Auto Read App</footer>
</div>
</body>
</html>
`);
});
healthApp.listen(HEALTH_PORT, () => {
console.log(
`Health check endpoint is running at http://localhost:${HEALTH_PORT}/health`
);
});