Compare commits

...

12 Commits

Author SHA1 Message Date
bb9a31cc78 fix: close the page if an error occurred while automating
Prevent massive memory leak...
2025-04-20 07:23:40 +07:00
c6b571c68d chore: move pino-pretty to dependencies
Sacrifice minimal performance for readability :cat_smirk:
2025-04-14 08:43:02 +07:00
a63e92e91f chore: prevent launching if proxy count is 0 2025-04-14 01:23:09 +07:00
92cb93b4cb chore: version in constants 2025-04-14 01:10:52 +07:00
6cf4096064 chore: fix running dist package in nodejs
So we have to add ".js" to the import name...
2025-04-13 13:29:36 +07:00
358fe53be9 feat: add change_viewport option 2025-04-13 00:43:55 +07:00
50c5927be7 fix: check consent button every second 2025-04-13 00:34:57 +07:00
cb427a9fa4 feat: random device resolution & accept cookies 2025-04-13 00:31:57 +07:00
2ad2f9993e feat: catch all errors 2025-04-12 21:58:19 +07:00
27b4f4a9cc feat: wait for 15s before execute the keepAlive strategy 2025-04-12 21:48:25 +07:00
65d801b248 chore: remove one sigint message 2025-04-12 21:46:49 +07:00
be8a9650a1 fix: await keepAlive
I forgot...
2025-04-12 21:46:26 +07:00
8 changed files with 78 additions and 26 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
# Castorsrm # Castorsrm
config.json config.json
build/
# Logs # Logs

View File

@ -2,6 +2,13 @@
Rice shirt rice money :( Rice shirt rice money :(
## Notice
> [!WARNING]
> This software is provided by the author as is, without any warranty. Use at your own risk
The view buffed should be around 1/2 (or 2/5) of the amount of proxy you chosen in the worst case, and possibly near the amount in the best case.
## Installation ## Installation
This project uses Bun for dependency management and Node.js (or Bun on UNIX platforms) for the app execution. This project uses Bun for dependency management and Node.js (or Bun on UNIX platforms) for the app execution.

View File

@ -8,14 +8,13 @@
"dev-bun": "bun ./src/index.ts", "dev-bun": "bun ./src/index.ts",
"start": "node ./dist/index.js", "start": "node ./dist/index.js",
"start-bun": "bun ./dist/index.js", "start-bun": "bun ./dist/index.js",
"build": "swc ./src -d dist" "build": "swc ./src -d dist --strip-leading-paths"
}, },
"devDependencies": { "devDependencies": {
"@swc-node/register": "^1.10.10", "@swc-node/register": "^1.10.10",
"@swc/cli": "^0.6.0", "@swc/cli": "^0.6.0",
"@swc/core": "^1.11.20", "@swc/core": "^1.11.20",
"@types/bun": "latest", "@types/bun": "latest"
"pino-pretty": "^13.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"
@ -23,6 +22,7 @@
"dependencies": { "dependencies": {
"patchright": "^1.51.3", "patchright": "^1.51.3",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"playwright": "^1.51.1" "playwright": "^1.51.1"
}, },
"trustedDependencies": [ "trustedDependencies": [

View File

@ -1,11 +1,13 @@
class Config { class Config {
playwright: { playwright: {
browser: string; browser: string;
change_viewport: boolean;
headless: boolean; headless: boolean;
cdp: string; cdp: string;
url: string; url: string;
} = { } = {
browser: "chromium", browser: "chromium",
change_viewport: true,
headless: true, headless: true,
cdp: "ws://127.0.0.1:9222", cdp: "ws://127.0.0.1:9222",
url: "https://www.twitch.tv/", url: "https://www.twitch.tv/",

View File

@ -1,3 +1,4 @@
const VERSION = "0.1.0";
const REFLECT4_SERVERS = [ const REFLECT4_SERVERS = [
"https://www.blockaway.net", "https://www.blockaway.net",
"https://www.croxyproxy.com", "https://www.croxyproxy.com",
@ -9,4 +10,4 @@ const REFLECT4_SERVERS = [
"https://proxyium.com" "https://proxyium.com"
]; ];
export { REFLECT4_SERVERS }; export { REFLECT4_SERVERS, VERSION };

View File

@ -1,12 +1,11 @@
import { chromium, type Browser, type LaunchOptions } from "patchright"; import { chromium, type Browser, type LaunchOptions } from "patchright";
import fs from 'node:fs'; import fs from 'node:fs';
import * as reflect4 from "./proxy/reflect4"; import * as reflect4 from "./proxy/reflect4.js";
import logger from "./logger"; import logger from "./logger.js";
import Config from "./config"; import Config from "./config.js";
import { VERSION } from "./constants.js";
const version = "0.1.0"; logger.info(`Castorsrm v${VERSION}`)
logger.info(`Castorsrm v${version} - https://github.com/teppyboy/castorsrm`)
logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk."); logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk.");
let config = new Config(); let config = new Config();
@ -16,9 +15,16 @@ if (fs.existsSync("config.json")) {
config = Config.fromJSON(text); config = Config.fromJSON(text);
} else { } else {
logger.info("No configuration file found. Using the default configuration."); logger.info("No configuration file found. Using the default configuration.");
await fs.promises.writeFile("config.json", config.toJSON());
logger.info("Default configuration file 'config.json' created."); logger.info("Default configuration file 'config.json' created.");
} }
// Write the new config file in case we updated something.
await fs.promises.writeFile("config.json", config.toJSON());
// Validate configuration
if (config.proxy.count < 1) {
logger.error("Proxy count must be greater than 0.");
process.exit(1);
}
logger.level = process.env.LOG_LEVEL || config.logger.level; logger.level = process.env.LOG_LEVEL || config.logger.level;
logger.info(`Logger level set to '${logger.level}'`); logger.info(`Logger level set to '${logger.level}'`);
@ -59,16 +65,23 @@ if (config.proxy.mode === "reflect4") {
logger.info(`Spawning ${config.proxy.count} proxies...`); logger.info(`Spawning ${config.proxy.count} proxies...`);
for (let i = 0; i < config.proxy.count; i++) { for (let i = 0; i < config.proxy.count; i++) {
logger.debug(`Spawning proxy ${i + 1}...`); logger.debug(`Spawning proxy ${i + 1}...`);
tasks.push(reflect4.spawn(context, config.playwright.url)); tasks.push((async () => {
const spawnId = `${i + 1}`;
while (true) {
try {
await reflect4.spawn(context, config.playwright.url, config.playwright.change_viewport, spawnId);
} catch (e) {
logger.error(`[${spawnId}] Error while running: ${e}`);
}
logger.warn(`[${spawnId}] Restarting in 3 seconds...`);
await new Promise(resolve => setTimeout(resolve, 3 * 1000));
}
})());
} }
await Promise.all(tasks); await Promise.all(tasks);
logger.info("All proxies spawned successfully."); logger.info("All proxies spawned successfully.");
} }
process.on("SIGINT", () => {
logger.info("Received SIGINT. Closing browser...");
});
process.on("SIGINT", async () => { process.on("SIGINT", async () => {
logger.info("Received SIGINT. Closing browser..."); logger.info("Received SIGINT. Closing browser...");
for (const context of browser.contexts()) { for (const context of browser.contexts()) {

View File

@ -1,14 +1,26 @@
import { type BrowserContext, type Locator } from "patchright"; import { devices, type BrowserContext, type Locator } from "patchright";
import logger from "../logger"; import logger from "../logger.js";
import * as twitch from "../website/twitch"; import * as twitch from "../website/twitch.js";
import * as constants from "../constants"; import * as constants from "../constants.js";
async function spawn(context: BrowserContext, targetUrl: string) { async function spawn(context: BrowserContext, targetUrl: string, changeViewport: boolean = false, spawnId: string = "unknown") {
const spawnId = btoa(Math.random().toString()).substring(4,10);
const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)]; const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)];
logger.debug(`[${spawnId}] Using reflect4 server: ${server}`); logger.debug(`[${spawnId}] Using reflect4 server: ${server}`);
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server); if (changeViewport) {
logger.debug(`[${spawnId}] Changing viewport size...`);
const deviceName = Object.keys(devices)[Math.floor(Math.random() * Object.keys(devices).length)];
const device = devices[deviceName];
logger.debug(`[${spawnId}] Using device: ${deviceName}`);
await page.setViewportSize(device.viewport);
}
try {
await page.goto(server);
} catch (e) {
logger.error(`[${spawnId}] Error while navigating to proxy website: ${e}`);
await page.close();
throw e;
}
let targetInput: Locator | null = null; let targetInput: Locator | null = null;
const allInput = await page.locator("input").all(); const allInput = await page.locator("input").all();
for (const input of allInput) { for (const input of allInput) {
@ -20,13 +32,17 @@ async function spawn(context: BrowserContext, targetUrl: string) {
} }
if (!targetInput) { if (!targetInput) {
logger.error(`[${spawnId}] Failed to find input field for URL input`); logger.error(`[${spawnId}] Failed to find input field for URL input`);
return; await page.close();
throw new Error(`Failed to find input field for URL input`);
} }
await targetInput.fill(targetUrl); await targetInput.fill(targetUrl);
await targetInput.press("Enter"); await targetInput.press("Enter");
logger.info(`[${spawnId}] Navigating to ${targetUrl}`);
await page.waitForTimeout(15000); // Wait for 15 second to let the page load
// Keep-alive the page open for 5 minutes then refresh // Keep-alive the page open for 5 minutes then refresh
if (targetUrl.startsWith("https://www.twitch.tv/")) { if (targetUrl.startsWith("https://www.twitch.tv/")) {
twitch.keepAlive(page, spawnId); logger.info(`[${spawnId}] Twitch URL detected, using Twitch mode...`);
await twitch.keepAlive(page, spawnId);
} else { } else {
logger.warn(`[${spawnId}] Unsupported URL: ${targetUrl}`); logger.warn(`[${spawnId}] Unsupported URL: ${targetUrl}`);
logger.warn(`[${spawnId}] Will try to keep alive, but no guarantees`); logger.warn(`[${spawnId}] Will try to keep alive, but no guarantees`);

View File

@ -1,5 +1,13 @@
import type { Page } from "patchright"; import type { Page } from "patchright";
import logger from "../logger"; import logger from "../logger.js";
async function checkConsentButton(page: Page, spawnId: string = "unknown") {
const consentButton = page.locator("button[data-a-target='consent-banner-accept']");
if ((await consentButton.all()).length > 0) {
logger.debug(`[${spawnId}] Consent button found, clicking it...`);
await consentButton.click();
}
}
async function keepAlive(page: Page, spawnId: string = "unknown") { async function keepAlive(page: Page, spawnId: string = "unknown") {
try { try {
@ -13,7 +21,10 @@ async function keepAlive(page: Page, spawnId: string = "unknown") {
if ((await page.locator(".ScCoreButton-sc-ocjdkq-0.ggPgVz").all()).length > 0) { if ((await page.locator(".ScCoreButton-sc-ocjdkq-0.ggPgVz").all()).length > 0) {
logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`); logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"}); await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
waitTime = 0;
continue;
} }
await checkConsentButton(page, spawnId);
if (waitTime > 5 * 60 * 1000) { if (waitTime > 5 * 60 * 1000) {
logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`); logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"}); await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
@ -21,6 +32,7 @@ async function keepAlive(page: Page, spawnId: string = "unknown") {
} }
} }
} catch (e) { } catch (e) {
await page.close();
logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`); logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`);
} }
} }