forked from 14790897/auto-read-liunxdo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bypasscf.js
498 lines (462 loc) · 17.8 KB
/
bypasscf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
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,登录失败");
}
//真正执行阅读脚本
const externalScriptPath = path.join(
dirname(fileURLToPath(import.meta.url)),
"index.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/400", {
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) {
// 使用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 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"), // 点击登录按钮触发跳转
]); //注意如果登录失败,这里会一直等待跳转,导致脚本执行失败 这点四个月之前你就发现了结果今天又遇到(有个用户遇到了https://linux.do/t/topic/169209/82),但是你没有在这个报错你提示我8.5
} catch (error) {
throw new Error(
`Navigation timed out in login.请检查用户名密码是否正确(注意密码中是否有特殊字符,需要外面加上双引号指明这是字符串,如果密码里面有双引号则需要转义)(注意GitHub action不需要增加处理,也不需要加引号),失败用户 ${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>© 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`
);
});