repo: init

This commit is contained in:
2025-04-12 19:38:54 +07:00
commit 5189ed1472
13 changed files with 536 additions and 0 deletions

178
.gitignore vendored Normal file
View File

@ -0,0 +1,178 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Castorsrm
config.json
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

31
.swcrc Normal file
View File

@ -0,0 +1,31 @@
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"jsx": false,
"dynamicImport": false,
"privateMethod": false,
"functionBind": false,
"exportDefaultFrom": false,
"exportNamespaceFrom": false,
"decorators": false,
"decoratorsBeforeExport": false,
"topLevelAwait": true,
"importMeta": false
},
"minify": {
"compress": {
"unused": true
},
"mangle": true
},
"transform": null,
"target": "esnext",
"loose": false,
"externalHelpers": false,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
"keepClassNames": false
},
"minify": true
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 tretrauit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Castorsrm
Rice shirt rice money :(
## Installation
This project uses Bun for dependency management and Node.js (or Bun on UNIX platforms) for the app execution.
```bash
git clone https://git.tretrauit.me/tretrauit/castorsrm
cd castorsrm
bun install
bun run dev
# or execute "bun run dev-bun" to start the application with Bun instead
# Note that Bun will not work correctly with Playwright on Windows.
```
## Configuration
On the first launch, it'll generate a `config.json` file. You can change the application settings by editing that file.
## License
[MIT](./LICENSE)

BIN
bun.lockb Normal file

Binary file not shown.

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "castorsrm",
"version": "0.1.0",
"module": "src/index.ts",
"type": "module",
"scripts": {
"dev": "node --env-file=.env --import @swc-node/register/esm-register ./src/index.ts",
"dev-bun": "bun ./src/index.ts",
"start": "node ./dist/index.js",
"start-bun": "bun ./dist/index.js",
"build": "swc ./src -d dist"
},
"devDependencies": {
"@swc-node/register": "^1.10.10",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.11.20",
"@types/bun": "latest",
"pino-pretty": "^13.0.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"patchright": "^1.51.3",
"pino": "^9.6.0",
"playwright": "^1.51.1"
},
"trustedDependencies": [
"@swc/core"
]
}

46
src/config.ts Normal file
View File

@ -0,0 +1,46 @@
class Config {
playwright: {
browser: string;
headless: boolean;
cdp: string;
url: string;
} = {
browser: "chromium",
headless: true,
cdp: "ws://127.0.0.1:9222",
url: "https://www.twitch.tv/",
};
proxy: {
mode: string,
count: number,
} = {
mode: "reflect4",
count: 0,
};
logger: {
level: string,
} = {
level: "info",
};
static fromJSON(json: string) {
const obj = JSON.parse(json);
const config = new Config();
for (const [k, v] of Object.entries(obj)) {
if (k in config) {
// @ts-ignore
config[k] = v;
} else {
console.warn(`Unknown key '${k}' in configuration file. Ignoring it.`);
}
}
return config;
}
toJSON() {
const obj: any = {};
for (const [k, v] of Object.entries(this)) {
obj[k] = v;
}
return JSON.stringify(obj, null, 4);
}
}
export default Config;

12
src/constants.ts Normal file
View File

@ -0,0 +1,12 @@
const REFLECT4_SERVERS = [
"https://www.blockaway.net",
"https://www.croxyproxy.com",
"https://www.croxyproxy.rocks",
"https://www.croxy.network",
"https://www.croxy.org",
"https://www.youtubeunblocked.live",
"https://www.croxyproxy.net",
"https://proxyium.com"
];
export { REFLECT4_SERVERS };

79
src/index.ts Normal file
View File

@ -0,0 +1,79 @@
import { chromium, type Browser, type LaunchOptions } from "patchright";
import fs from 'node:fs';
import * as reflect4 from "./proxy/reflect4";
import logger from "./logger";
import Config from "./config";
const version = "0.1.0";
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.");
let config = new Config();
if (fs.existsSync("config.json")) {
logger.info("Reading configuration from 'config.json'...");
const text = await fs.promises.readFile("config.json", "utf-8");
config = Config.fromJSON(text);
} else {
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.level = process.env.LOG_LEVEL || config.logger.level;
logger.info(`Logger level set to '${logger.level}'`);
let browser: Browser;
let launchOptions: LaunchOptions = {
headless: config.playwright.headless,
}
logger.info("Launching browser...");
logger.debug(`Launch options: ${JSON.stringify(launchOptions)}`);
switch (config.playwright.browser) {
case "chromium":
logger.info("Using Chromium as the browser provider.");
logger.warn("Chromium is not supported by Twitch. Use at your own risk.");
browser = await chromium.launch({ ...launchOptions });
break;
case "chrome":
logger.info("Using Google Chrome as the browser provider.");
browser = await chromium.launch({ ...launchOptions, channel: "chrome" });
break;
case "cdp":
logger.info("Using Chrome DevTools Protocol (CDP) for browser connection.");
browser = await chromium.connectOverCDP(config.playwright.cdp);
break;
default:
logger.warn(`Unsupported browser channel: '${config.playwright.browser}'`);
logger.warn("Castorsrm will try to launch the browser anyway, but it may not work as expected.");
browser = await chromium.launch({ ...launchOptions, channel: config.playwright.browser });
break;
}
logger.info("Browser launched successfully, spawning proxies");
if (config.proxy.mode === "reflect4") {
const context = await browser.newContext();
const tasks: Array<Promise<void>> = [];
logger.info(`Spawning ${config.proxy.count} proxies...`);
for (let i = 0; i < config.proxy.count; i++) {
logger.debug(`Spawning proxy ${i + 1}...`);
tasks.push(reflect4.spawn(context, config.playwright.url));
}
await Promise.all(tasks);
logger.info("All proxies spawned successfully.");
}
process.on("SIGINT", () => {
logger.info("Received SIGINT. Closing browser...");
});
process.on("SIGINT", async () => {
logger.info("Received SIGINT. Closing browser...");
for (const context of browser.contexts()) {
await context.close();
}
await browser.close();
process.exit();
});

15
src/logger.ts Normal file
View File

@ -0,0 +1,15 @@
import { type Logger, pino } from "pino";
import pretty from "pino-pretty";
const stream = pretty({
colorize: true,
translateTime: "SYS:standard",
ignore: "hostname,pid",
});
const logger: Logger<never> = pino({
name: "castorsrm",
level: "info",
}, stream);
export default logger;

43
src/proxy/reflect4.ts Normal file
View File

@ -0,0 +1,43 @@
import { type BrowserContext, type Locator } from "patchright";
import logger from "../logger";
import * as twitch from "../website/twitch";
import * as constants from "../constants";
async function spawn(context: BrowserContext, targetUrl: string) {
const spawnId = btoa(Math.random().toString()).substring(4,10);
const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)];
logger.debug(`[${spawnId}] Using reflect4 server: ${server}`);
const page = await context.newPage();
await page.goto(server);
let targetInput: Locator | null = null;
const allInput = await page.locator("input").all();
for (const input of allInput) {
const placeholder = await input.getAttribute("placeholder");
if (placeholder?.includes("URL")) {
targetInput = input;
break;
}
}
if (!targetInput) {
logger.error(`[${spawnId}] Failed to find input field for URL input`);
return;
}
await targetInput.fill(targetUrl);
await targetInput.press("Enter");
// Keep-alive the page open for 5 minutes then refresh
if (targetUrl.startsWith("https://www.twitch.tv/")) {
twitch.keepAlive(page, spawnId);
} else {
logger.warn(`[${spawnId}] Unsupported URL: ${targetUrl}`);
logger.warn(`[${spawnId}] Will try to keep alive, but no guarantees`);
while (true) {
await page.waitForTimeout(5 * 60 * 1000); // 5 minutes
await page.reload();
logger.debug(`[${spawnId}] Reloaded page`);
}
}
await page.close();
logger.info(`[${spawnId}] Proxy with the server ${server} closed`);
}
export { spawn };

28
src/website/twitch.ts Normal file
View File

@ -0,0 +1,28 @@
import type { Page } from "patchright";
import logger from "../logger";
async function keepAlive(page: Page, spawnId: string = "unknown") {
try {
let waitTime = 0;
while (true) {
// Wait for a random time between 1 and 11 seconds
const timeout = 1000 + Math.floor(Math.random() * 60 * 1000) % 10000;
logger.debug(`[${spawnId}] Waiting for ${timeout / 1000} seconds...`);
await page.waitForTimeout(timeout);
waitTime += timeout;
if ((await page.locator(".ScCoreButton-sc-ocjdkq-0.ggPgVz").all()).length > 0) {
logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
}
if (waitTime > 5 * 60 * 1000) {
logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`);
await page.reload({timeout: 0, waitUntil: "domcontentloaded"});
waitTime = 0;
}
}
} catch (e) {
logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`);
}
}
export { keepAlive };

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"noImplicitAny": false
}
}