Initial commit: RobloxVerify automation tool
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.session
|
||||||
|
*.zip
|
||||||
|
haarcascade_frontalface_default.xml
|
||||||
|
logs/
|
||||||
|
result/
|
||||||
|
videos/
|
||||||
|
fix_cv.py
|
||||||
|
telegram_cleaner.py
|
||||||
|
accs.txt
|
||||||
|
proxy.txt
|
||||||
|
login_password_cookie.txt
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# RobloxVerify
|
||||||
|
|
||||||
|
Автоматизация прохождения age verification на Roblox через Persona.
|
||||||
|
|
||||||
|
## Форматы accs.txt
|
||||||
|
|
||||||
|
```
|
||||||
|
login:pass
|
||||||
|
login:pass:cookie
|
||||||
|
login:cookie
|
||||||
|
just_the_cookie_token
|
||||||
|
```
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
# Тестовый режим (пауза между аккаунтами):
|
||||||
|
python main.py --test
|
||||||
|
```
|
||||||
|
|
||||||
|
Требуется Chrome 149+.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Format: login:pass or login:pass:cookie or login:cookie or just-cookie
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Format: login:password:cookie
|
||||||
|
# Example:
|
||||||
|
# user:pass:_|WARNING:-DO-NOT-SHARE-THIS.--(token)
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import threading
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||||
|
|
||||||
|
import undetected_chromedriver as uc
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
|
||||||
|
|
||||||
|
PROXY_FILE = Path("proxy.txt")
|
||||||
|
ACCOUNTS_FILE = Path("accs.txt")
|
||||||
|
LOCAL_BRIDGE_HOST = "127.0.0.1"
|
||||||
|
LOCAL_BRIDGE_PORT = 18080
|
||||||
|
LOG_DIR = Path("logs")
|
||||||
|
VIDEOS_DIR = Path("videos")
|
||||||
|
HAAR_PATH = Path("haarcascade_frontalface_default.xml")
|
||||||
|
|
||||||
|
THREADS = 3
|
||||||
|
TEST_MODE = "--test" in sys.argv
|
||||||
|
|
||||||
|
log: logging.Logger = None
|
||||||
|
RESULT_DIR: Path = None
|
||||||
|
_processed_logins: set = set() # accounts finished (any outcome)
|
||||||
|
|
||||||
|
# Thread-safety primitives
|
||||||
|
_chrome_lock = threading.Lock()
|
||||||
|
_port_locks = [threading.Lock() for _ in range(THREADS)]
|
||||||
|
_port_owners = [None] * THREADS
|
||||||
|
|
||||||
|
|
||||||
|
def load_proxies() -> list[str]:
|
||||||
|
"""Returns list of normalized socks5:// proxy strings."""
|
||||||
|
if not PROXY_FILE.exists():
|
||||||
|
return []
|
||||||
|
proxies = []
|
||||||
|
for line in PROXY_FILE.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
s = line.replace("socks5://", "")
|
||||||
|
if "@" in s:
|
||||||
|
auth, rest = s.split("@", 1)
|
||||||
|
user, pw = auth.split(":", 1)
|
||||||
|
host, port = rest.split(":", 1)
|
||||||
|
proxies.append(f"socks5://{user}:{pw}@{host}:{port}")
|
||||||
|
else:
|
||||||
|
parts = s.split(":")
|
||||||
|
if len(parts) == 4:
|
||||||
|
# host:port:user:pass
|
||||||
|
host, port, user, pw = parts
|
||||||
|
proxies.append(f"socks5://{user}:{pw}@{host}:{port}")
|
||||||
|
elif len(parts) == 2:
|
||||||
|
host, port = parts
|
||||||
|
proxies.append(f"socks5://{host}:{port}")
|
||||||
|
return proxies
|
||||||
|
|
||||||
|
|
||||||
|
def parse_account_line(line: str) -> dict | None:
|
||||||
|
raw = line.strip()
|
||||||
|
result = {"_raw": raw}
|
||||||
|
first = raw.find(":")
|
||||||
|
if first == -1:
|
||||||
|
if len(raw) > 50 or raw.startswith("_|WARNING"):
|
||||||
|
result.update({"login": "", "password": "", "cookie": raw})
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
|
||||||
|
login_part = raw[:first]
|
||||||
|
rest_after_first = raw[first + 1:]
|
||||||
|
|
||||||
|
# cookie-only: login_part выглядит как начало куки
|
||||||
|
if login_part.startswith("_|WARNING"):
|
||||||
|
result.update({"login": "", "password": "", "cookie": raw})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# login:cookie — кука начинается сразу после первого :
|
||||||
|
if rest_after_first.startswith("_|WARNING"):
|
||||||
|
result.update({"login": login_part, "password": "", "cookie": rest_after_first})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# login:pass:cookie или login:pass
|
||||||
|
second = rest_after_first.find(":")
|
||||||
|
if second != -1:
|
||||||
|
result.update({"login": login_part, "password": rest_after_first[:second], "cookie": rest_after_first[second + 1:]})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Без второго : — login:pass или login:cookie (кука без _|WARNING)
|
||||||
|
if len(rest_after_first) > 50:
|
||||||
|
result.update({"login": login_part, "password": "", "cookie": rest_after_first})
|
||||||
|
else:
|
||||||
|
result.update({"login": login_part, "password": rest_after_first, "cookie": ""})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def start_pproxy_bridge(proxy: str, port: int) -> subprocess.Popen | None:
|
||||||
|
remote = proxy
|
||||||
|
s = proxy.replace("socks5://", "", 1)
|
||||||
|
if "@" in s:
|
||||||
|
auth, rest = s.split("@", 1)
|
||||||
|
remote = f"socks5://{rest}#{auth}"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[sys.executable, "-m", "pproxy", "-l", f"http://{LOCAL_BRIDGE_HOST}:{port}", "-r", remote, "-v"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
time.sleep(1.5)
|
||||||
|
if proc.poll() is not None:
|
||||||
|
err = proc.stderr.read() if proc.stderr else ""
|
||||||
|
log.error("pproxy on port %d failed (attempt %d/5): %s", port, attempt + 1, err.strip()[:200])
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
return proc
|
||||||
|
except OSError as e:
|
||||||
|
log.warning("pproxy on port %d attempt %d/5: %s", port, attempt + 1, e)
|
||||||
|
time.sleep(2)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_haar_cascade() -> str:
|
||||||
|
if HAAR_PATH.exists():
|
||||||
|
return str(HAAR_PATH)
|
||||||
|
url = "https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml"
|
||||||
|
log.info("Downloading haarcascade_frontalface_default.xml...")
|
||||||
|
urllib.request.urlretrieve(url, str(HAAR_PATH))
|
||||||
|
return str(HAAR_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def _list_videos() -> list[Path]:
|
||||||
|
video_exts = {".mp4", ".webm", ".avi", ".mov", ".mkv"}
|
||||||
|
if not VIDEOS_DIR.exists():
|
||||||
|
return []
|
||||||
|
return sorted([f for f in VIDEOS_DIR.iterdir() if f.suffix.lower() in video_exts and not f.name.startswith(".")])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_video_size(video_path: Path) -> tuple[int, int]:
|
||||||
|
cap = cv2.VideoCapture(str(video_path))
|
||||||
|
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
cap.release()
|
||||||
|
return w, h
|
||||||
|
|
||||||
|
|
||||||
|
def _get_video_duration(video_path: Path) -> float:
|
||||||
|
cap = cv2.VideoCapture(str(video_path))
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
cap.release()
|
||||||
|
if fps <= 0:
|
||||||
|
return 0
|
||||||
|
return count / fps
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_frames_jpg_b64(video_path: Path, w: int, h: int, step: int = 2) -> list[str]:
|
||||||
|
import cv2
|
||||||
|
import base64
|
||||||
|
cap = cv2.VideoCapture(str(video_path))
|
||||||
|
ow = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
oh = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
idx = 0
|
||||||
|
while True:
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
if idx % step != 0:
|
||||||
|
idx += 1
|
||||||
|
continue
|
||||||
|
idx += 1
|
||||||
|
if (ow, oh) != (w, h):
|
||||||
|
frame = cv2.resize(frame, (w, h))
|
||||||
|
frames.append(frame)
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for frame in frames:
|
||||||
|
_, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
||||||
|
result.append(base64.b64encode(buf.tobytes()).decode())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_video_directions(video_path: Path) -> tuple[dict[str, list[int]], list[float]]:
|
||||||
|
"""Returns (dir_ranges, face_cx_list).
|
||||||
|
dir_ranges: {dir: [start_frame, end_frame]} for center/left/right.
|
||||||
|
face_cx_list: face center X per frame (original coords), -1 if no face.
|
||||||
|
"""
|
||||||
|
haar_src = cv2.objdetect if hasattr(cv2, "objdetect") and hasattr(cv2.objdetect, "CascadeClassifier") else cv2
|
||||||
|
if not hasattr(haar_src, "CascadeClassifier"):
|
||||||
|
raise RuntimeError(
|
||||||
|
"cv2.CascadeClassifier not available. "
|
||||||
|
"Reinstall opencv-python: pip install --force-reinstall opencv-python"
|
||||||
|
)
|
||||||
|
haar_path = _ensure_haar_cascade()
|
||||||
|
face_cascade = haar_src.CascadeClassifier(haar_path)
|
||||||
|
cap = cv2.VideoCapture(str(video_path))
|
||||||
|
fw = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
low = fw * 0.45
|
||||||
|
high = fw * 0.55
|
||||||
|
directions = []
|
||||||
|
cx_list = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
|
faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(60, 60))
|
||||||
|
if len(faces) > 0:
|
||||||
|
x, _, fw_face, _ = faces[0]
|
||||||
|
cx = x + fw_face / 2
|
||||||
|
cx_list.append(cx)
|
||||||
|
if cx < low:
|
||||||
|
directions.append("right")
|
||||||
|
elif cx > high:
|
||||||
|
directions.append("left")
|
||||||
|
else:
|
||||||
|
directions.append("center")
|
||||||
|
else:
|
||||||
|
cx_list.append(-1.0)
|
||||||
|
directions.append("center")
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
ranges: dict[str, list[int]] = {"center": [], "left": [], "right": []}
|
||||||
|
for d in ranges:
|
||||||
|
start = None
|
||||||
|
for i, v in enumerate(directions):
|
||||||
|
if v == d and start is None:
|
||||||
|
start = i
|
||||||
|
elif v != d and start is not None:
|
||||||
|
if i - start >= 5:
|
||||||
|
ranges[d].append([start, i - 1])
|
||||||
|
start = None
|
||||||
|
if start is not None and len(directions) - start >= 5:
|
||||||
|
ranges[d].append([start, len(directions) - 1])
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for d, segs in ranges.items():
|
||||||
|
if segs:
|
||||||
|
result[d] = max(segs, key=lambda s: s[1] - s[0])
|
||||||
|
else:
|
||||||
|
result[d] = [0, 0]
|
||||||
|
return result, cx_list
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger() -> logging.Logger:
|
||||||
|
LOG_DIR.mkdir(exist_ok=True)
|
||||||
|
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
log_file = LOG_DIR / f"session_{ts}.log"
|
||||||
|
logger = logging.getLogger("robverify")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
fh = logging.FileHandler(log_file, encoding="utf-8")
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
sh = logging.StreamHandler(sys.stdout)
|
||||||
|
sh.setFormatter(fmt)
|
||||||
|
logger.addHandler(sh)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def human_type(element, text: str) -> None:
|
||||||
|
for ch in text:
|
||||||
|
element.send_keys(ch)
|
||||||
|
time.sleep(random.uniform(0.03, 0.12))
|
||||||
|
|
||||||
|
|
||||||
|
def save_result(acc: dict, category: str, msg: str = "") -> None:
|
||||||
|
label = f"{acc['login']}:{acc['password']}" if acc['password'] else acc['login']
|
||||||
|
if not label:
|
||||||
|
label = acc['cookie'][:40] + "..."
|
||||||
|
line = f"{label} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||||
|
if msg:
|
||||||
|
line += f" - {msg}"
|
||||||
|
file_path = RESULT_DIR / f"{category}.txt"
|
||||||
|
with open(file_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(line + "\n")
|
||||||
|
log.info("Saved to %s", file_path.name)
|
||||||
|
|
||||||
|
|
||||||
|
def _cookie_login(driver, cookie: str) -> bool:
|
||||||
|
driver.get("https://www.roblox.com")
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
accept = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Accept All']")))
|
||||||
|
accept.click()
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
driver.add_cookie({"name": ".ROBLOSECURITY", "value": cookie, "domain": ".roblox.com"})
|
||||||
|
driver.get("https://www.roblox.com/home")
|
||||||
|
time.sleep(3)
|
||||||
|
return "/login" not in driver.current_url
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def process_account(driver, acc: dict, donor_video: Path | None,
|
||||||
|
frames_b64: list[str] | None = None, w: int = 0, h: int = 0,
|
||||||
|
dir_ranges: dict | None = None, speed_multiplier: float = 1.0) -> bool:
|
||||||
|
login, password, cookie = acc["login"], acc["password"], acc["cookie"]
|
||||||
|
log.info("=== %s ===", login or "cookie-only")
|
||||||
|
|
||||||
|
if cookie:
|
||||||
|
if _cookie_login(driver, cookie):
|
||||||
|
log.info("Logged in via cookie")
|
||||||
|
elif login and password:
|
||||||
|
log.info("Cookie expired, falling back to login:pass")
|
||||||
|
cookie = ""
|
||||||
|
else:
|
||||||
|
log.warning("Cookie expired and no password — skipping")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not cookie:
|
||||||
|
driver.get("https://www.roblox.com/login")
|
||||||
|
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.ID, "login-username")))
|
||||||
|
|
||||||
|
try:
|
||||||
|
accept = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Accept All']")))
|
||||||
|
accept.click()
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
username_input = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "login-username")))
|
||||||
|
ActionChains(driver).move_to_element(username_input).pause(0.3).perform()
|
||||||
|
time.sleep(0.1); username_input.click(); time.sleep(0.1); username_input.clear()
|
||||||
|
human_type(username_input, login)
|
||||||
|
|
||||||
|
password_input = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "login-password")))
|
||||||
|
ActionChains(driver).move_to_element(password_input).pause(0.3).perform()
|
||||||
|
time.sleep(0.1); password_input.click(); time.sleep(0.1); password_input.clear()
|
||||||
|
human_type(password_input, password)
|
||||||
|
|
||||||
|
login_btn = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "login-button")))
|
||||||
|
ActionChains(driver).move_to_element(login_btn).pause(0.2).perform()
|
||||||
|
time.sleep(0.1); login_btn.click()
|
||||||
|
|
||||||
|
WebDriverWait(driver, 30).until(lambda d: "/login" not in d.current_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agree = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.XPATH, "//button[contains(@class, 'modal-button') and contains(@class, 'btn-control-md')]")))
|
||||||
|
agree.click()
|
||||||
|
log.info("Accepted Updated Agreements")
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
driver.get("https://www.roblox.com/my/account#!/info")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "Did we get your age group right" in body:
|
||||||
|
log.info("VERIFIED: already verified (age group banner)")
|
||||||
|
save_result(acc, "verified", "age group banner")
|
||||||
|
return "verified"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
close_btn = driver.find_element(By.XPATH, "//div[@data-state='open' and contains(@class, 'dialog-overlay')]/..//button[contains(@class, 'icon')]")
|
||||||
|
close_btn.click()
|
||||||
|
log.info("Dismissed overlay dialog")
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
camera_btn = WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.XPATH, "//span[text()='Continue with camera']/ancestor::button")))
|
||||||
|
try:
|
||||||
|
camera_btn.click()
|
||||||
|
except Exception:
|
||||||
|
driver.execute_script("arguments[0].click();", camera_btn)
|
||||||
|
log.info("Clicked camera button via JS")
|
||||||
|
|
||||||
|
time.sleep(15)
|
||||||
|
|
||||||
|
for text in ["Sorry, your verification was declined", "Session expired"]:
|
||||||
|
try:
|
||||||
|
el = driver.find_element(By.XPATH, f"//*[contains(text(), '{text}')]")
|
||||||
|
msg = el.text
|
||||||
|
log.info(msg)
|
||||||
|
cat = "expired" if "Session expired" in msg else "declined"
|
||||||
|
save_result(acc, cat, msg)
|
||||||
|
return "Session expired" in msg
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
allow_btn = WebDriverWait(driver, 5).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Continue')] | //button[contains(text(), 'Allow')] | //button[contains(text(), 'Accept')]"))
|
||||||
|
)
|
||||||
|
allow_btn.click()
|
||||||
|
log.info("Accepted Roblox permission dialog")
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception:
|
||||||
|
log.info("No permission dialog to accept")
|
||||||
|
|
||||||
|
if not donor_video:
|
||||||
|
log.info("No donor video — all clear.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
iframe = WebDriverWait(driver, 10).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH, "//iframe[contains(@src, '/widget') or contains(@src, 'inquiry.withpersona.com/widget')]"))
|
||||||
|
)
|
||||||
|
log.info("Persona iframe found")
|
||||||
|
except Exception:
|
||||||
|
log.warning("Persona widget iframe not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if frames_b64 is None and donor_video:
|
||||||
|
w, h = _get_video_size(donor_video)
|
||||||
|
if dir_ranges is None:
|
||||||
|
dir_ranges, _ = _analyze_video_directions(donor_video)
|
||||||
|
frames_b64 = _extract_frames_jpg_b64(donor_video, w, h, step=2)
|
||||||
|
log.info("Extracted %d frames from %s", len(frames_b64), donor_video.name)
|
||||||
|
if dir_ranges is None and donor_video:
|
||||||
|
dir_ranges, _ = _analyze_video_directions(donor_video)
|
||||||
|
log.info("Direction ranges: %s", dir_ranges)
|
||||||
|
|
||||||
|
driver.switch_to.frame(iframe)
|
||||||
|
log.info("Switched to Persona iframe")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "Session expired" in body_text:
|
||||||
|
log.info("Session expired detected in Persona iframe before video flow")
|
||||||
|
save_result(acc, "expired", "in iframe before video")
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
consent = driver.find_element(By.XPATH, "//input[@type='checkbox' and @aria-label='I consent to the processing of my biometric information']")
|
||||||
|
driver.execute_script("arguments[0].click();", consent)
|
||||||
|
log.info("Biometric consent checkbox clicked")
|
||||||
|
time.sleep(0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
all_b64 = ",".join(frames_b64)
|
||||||
|
script = f"""
|
||||||
|
(function() {{
|
||||||
|
if (window.__camReady) return;
|
||||||
|
window.__camReady = true;
|
||||||
|
|
||||||
|
var _frames = '{all_b64}'.split(',');
|
||||||
|
var _canvas, _stream, _idx = 0, _paused = true;
|
||||||
|
var _currentDir = 'center';
|
||||||
|
var _pulseTheta = 0, _pulseFreq = 0.05, _pulseIntensity = 0.003;
|
||||||
|
var _frameInterval = Math.round(33 / {speed_multiplier});
|
||||||
|
var _streamResolve;
|
||||||
|
var _streamPromise = new Promise(function(r) {{ _streamResolve = r; }});
|
||||||
|
var _rotation = 0, _warmth = 0, _filter = 'none', _scaleX = 1, _scaleY = 1;
|
||||||
|
|
||||||
|
function _applyArtifacts(ctx, w, h) {{
|
||||||
|
var imgData = ctx.getImageData(0, 0, w, h);
|
||||||
|
var data = imgData.data;
|
||||||
|
_pulseTheta += _pulseFreq;
|
||||||
|
var pulseDelta = Math.sin(_pulseTheta) * _pulseIntensity;
|
||||||
|
var warmth = _warmth * 255;
|
||||||
|
for (var i = 0; i < data.length; i += 4) {{
|
||||||
|
var noise = (Math.random() - 0.5) * 6;
|
||||||
|
data[i] = Math.min(255, Math.max(0, data[i] + noise + (pulseDelta * 255) + warmth));
|
||||||
|
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + noise));
|
||||||
|
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + noise - warmth));
|
||||||
|
}}
|
||||||
|
ctx.putImageData(imgData, 0, 0);
|
||||||
|
ctx.translate((Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.4);
|
||||||
|
}}
|
||||||
|
|
||||||
|
function _draw() {{
|
||||||
|
if (!_paused) {{
|
||||||
|
_idx = (_idx + 1) % _frames.length;
|
||||||
|
}}
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = function() {{
|
||||||
|
var ctx = _canvas.getContext('2d');
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, _canvas.width, _canvas.height);
|
||||||
|
ctx.filter = _filter;
|
||||||
|
var cx = _canvas.width / 2, cy = _canvas.height / 2;
|
||||||
|
ctx.translate(cx, cy);
|
||||||
|
ctx.rotate(_rotation * Math.PI / 180);
|
||||||
|
ctx.scale(_scaleX, _scaleY);
|
||||||
|
if (_currentDir !== 'center') {{
|
||||||
|
ctx.scale(-1, 1);
|
||||||
|
}}
|
||||||
|
ctx.drawImage(img, -cx, -cy, _canvas.width, _canvas.height);
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
ctx.filter = 'none';
|
||||||
|
_applyArtifacts(ctx, _canvas.width, _canvas.height);
|
||||||
|
setTimeout(_draw, _frameInterval);
|
||||||
|
}};
|
||||||
|
img.src = 'data:image/jpeg;base64,' + _frames[_idx];
|
||||||
|
}}
|
||||||
|
|
||||||
|
_canvas = document.createElement('canvas');
|
||||||
|
_canvas.width = {w};
|
||||||
|
_canvas.height = {h};
|
||||||
|
var img0 = new Image();
|
||||||
|
img0.onload = function() {{
|
||||||
|
var ctx = _canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img0, 0, 0);
|
||||||
|
_applyArtifacts(ctx, _canvas.width, _canvas.height);
|
||||||
|
_stream = _canvas.captureStream(30);
|
||||||
|
_streamResolve(_stream);
|
||||||
|
}};
|
||||||
|
img0.src = 'data:image/jpeg;base64,' + _frames[0];
|
||||||
|
|
||||||
|
window.__startVideo = function() {{
|
||||||
|
_rotation = (Math.random() - 0.5) * 5;
|
||||||
|
_warmth = (Math.random() - 0.5) * 0.08;
|
||||||
|
var blur = (Math.random() * 0.5 + 0.2).toFixed(1) + 'px';
|
||||||
|
var contrast = (1 + (Math.random() - 0.5) * 0.1).toFixed(3);
|
||||||
|
var brightness = (1 + (Math.random() - 0.5) * 0.08).toFixed(3);
|
||||||
|
_filter = 'blur(' + blur + ') contrast(' + contrast + ') brightness(' + brightness + ')';
|
||||||
|
_scaleX = 1 + (Math.random() - 0.5) * 0.08;
|
||||||
|
_scaleY = 1 + (Math.random() - 0.5) * 0.08;
|
||||||
|
_paused = false;
|
||||||
|
_draw();
|
||||||
|
}};
|
||||||
|
window.__seekDirection = function(dir) {{ _currentDir = dir; }};
|
||||||
|
|
||||||
|
if (navigator.mediaDevices) {{
|
||||||
|
navigator.mediaDevices.getUserMedia = function(constraints) {{
|
||||||
|
return _streamPromise;
|
||||||
|
}};
|
||||||
|
navigator.mediaDevices.enumerateDevices = function() {{
|
||||||
|
return Promise.resolve([{{deviceId:'cam',kind:'videoinput',label:'Cam',groupId:'g'}}]);
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
if (navigator.permissions) {{
|
||||||
|
var _pq = navigator.permissions.query.bind(navigator.permissions);
|
||||||
|
navigator.permissions.query = function(d) {{
|
||||||
|
if (d && d.name === 'camera') return Promise.resolve({{state:'granted',onchange:null}});
|
||||||
|
return _pq(d);
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
|
||||||
|
driver.execute_script(script)
|
||||||
|
log.info("Camera override injected (%dx%d)", w, h)
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
for _ in range(20):
|
||||||
|
try:
|
||||||
|
driver.find_element(By.XPATH, "//button[@data-test='button__primary' and @id='button_submit']").click()
|
||||||
|
log.info("Clicked primary Continue")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
for _ in range(20):
|
||||||
|
try:
|
||||||
|
driver.find_element(By.XPATH, "//button[@data-test='selfie-prompt__button--camera']").click()
|
||||||
|
log.info("Clicked selfie Continue")
|
||||||
|
time.sleep(3)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
log.info("Waiting for center instruction...")
|
||||||
|
for _ in range(60):
|
||||||
|
try:
|
||||||
|
alert = driver.find_element(By.XPATH, "//span[@role='alert']")
|
||||||
|
txt = alert.text.lower()
|
||||||
|
if "center" in txt or "центр" in txt:
|
||||||
|
driver.execute_script("window.__startVideo();")
|
||||||
|
log.info("Center instruction detected, video started")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
end_time = time.time() + 120
|
||||||
|
log.info("Verification loop — manual mode")
|
||||||
|
while time.time() < end_time:
|
||||||
|
try:
|
||||||
|
body = driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "Try taking another photo" in body or "couldn't estimate your age" in body:
|
||||||
|
log.info("Photo rejected, restarting browser")
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
return True
|
||||||
|
if "Session expired" in body:
|
||||||
|
log.info("Session expired detected during verification")
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
return True
|
||||||
|
if "estimated your age" in body.lower() and "full access" in body.lower():
|
||||||
|
log.info("VERIFIED: age verified successfully")
|
||||||
|
save_result(acc, "verified", "in iframe")
|
||||||
|
try:
|
||||||
|
ok_btn = driver.find_element(By.XPATH, "//button[@id='complete__button' and @data-test='button__primary']")
|
||||||
|
ok_btn.click()
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
return "verified"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "Session expired" in body_text:
|
||||||
|
log.info("Session expired detected after verification")
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
driver.switch_to.default_content()
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "estimated your age" in body.lower() and "full access" in body.lower():
|
||||||
|
log.info("VERIFIED: age verified successfully")
|
||||||
|
save_result(acc, "verified", "main page")
|
||||||
|
return "verified"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for text in ["Sorry, your verification was declined", "Session expired"]:
|
||||||
|
try:
|
||||||
|
el = driver.find_element(By.XPATH, f"//*[contains(text(), '{text}')]")
|
||||||
|
msg = el.text
|
||||||
|
log.info(msg)
|
||||||
|
cat = "expired" if "Session expired" in msg else "declined"
|
||||||
|
save_result(acc, cat, msg)
|
||||||
|
return "Session expired" in msg
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.info("No decline message — pending (wait for days)")
|
||||||
|
save_result(acc, "pending", "no result in 120s loop")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_files() -> None:
|
||||||
|
if not PROXY_FILE.exists():
|
||||||
|
PROXY_FILE.write_text("# Format: socks5://user:pass@host:port\n# Or: host:port:user:pass\n# Or: host:port\n", encoding="utf-8")
|
||||||
|
log.info("Created %s — fill in your proxy.", PROXY_FILE)
|
||||||
|
if not ACCOUNTS_FILE.exists():
|
||||||
|
ACCOUNTS_FILE.write_text("# Format: login:pass or login:pass:cookie or login:cookie or just-cookie\n", encoding="utf-8")
|
||||||
|
log.info("Created %s — add your accounts.", ACCOUNTS_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def _acquire_port(tid: int) -> int:
|
||||||
|
"""Claim a port slot. Blocks until a slot is free."""
|
||||||
|
slot = tid % THREADS
|
||||||
|
_port_locks[slot].acquire()
|
||||||
|
_port_owners[slot] = threading.current_thread().name
|
||||||
|
return LOCAL_BRIDGE_PORT + slot
|
||||||
|
|
||||||
|
|
||||||
|
def _release_port(port: int) -> None:
|
||||||
|
slot = port - LOCAL_BRIDGE_PORT
|
||||||
|
if 0 <= slot < THREADS:
|
||||||
|
_port_owners[slot] = None
|
||||||
|
_port_locks[slot].release()
|
||||||
|
|
||||||
|
|
||||||
|
def _process_one(tid: int, acc: dict,
|
||||||
|
all_videos: list[Path], all_proxies: list[str],
|
||||||
|
result_dir: Path) -> None:
|
||||||
|
"""Process one account in its own thread."""
|
||||||
|
global log
|
||||||
|
login = acc["login"] or acc["cookie"][:20] + "..."
|
||||||
|
|
||||||
|
proxy = random.choice(all_proxies) if all_proxies else None
|
||||||
|
donor_video = random.choice(all_videos)
|
||||||
|
|
||||||
|
log.info("[T%d] === %s — video: %s%s ===", tid, login, donor_video.name,
|
||||||
|
f" proxy: {proxy}" if proxy else "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fw, fh = _get_video_size(donor_video)
|
||||||
|
dur = _get_video_duration(donor_video)
|
||||||
|
speed_multiplier = 0.5 if dur < 15 else 1.0
|
||||||
|
if speed_multiplier < 1:
|
||||||
|
log.info("Video is %.1fs — slowing playback to %.0f%%", dur, speed_multiplier * 100)
|
||||||
|
dir_ranges, _ = _analyze_video_directions(donor_video)
|
||||||
|
frames_b64 = _extract_frames_jpg_b64(donor_video, fw, fh, step=2)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("[T%d] Video analysis failed for %s: %s", tid, login, e)
|
||||||
|
_processed_logins.add(login)
|
||||||
|
return
|
||||||
|
|
||||||
|
port = _acquire_port(tid)
|
||||||
|
try:
|
||||||
|
pproxy_proc = start_pproxy_bridge(proxy, port) if proxy else None
|
||||||
|
|
||||||
|
max_retries = 15
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
options = uc.ChromeOptions()
|
||||||
|
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
|
if pproxy_proc:
|
||||||
|
options.add_argument(f"--proxy-server=http://{LOCAL_BRIDGE_HOST}:{port}")
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
try:
|
||||||
|
with _chrome_lock:
|
||||||
|
driver = uc.Chrome(options=options, headless=False, version_main=149)
|
||||||
|
result = process_account(driver, acc, donor_video, frames_b64, fw, fh, dir_ranges, speed_multiplier)
|
||||||
|
if result == "verified":
|
||||||
|
log.info("[T%d] Account verified", tid)
|
||||||
|
elif not result:
|
||||||
|
log.info("[T%d] Account done (no retry needed)", tid)
|
||||||
|
else:
|
||||||
|
log.info("[T%d] Session expired, retrying (attempt %d/%d)", tid, attempt + 1, max_retries)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log.error("[T%d] Error: %s", tid, e)
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
if TEST_MODE:
|
||||||
|
input(f"[T{tid}] Press Enter to close browser for {login} and continue...\n")
|
||||||
|
if driver:
|
||||||
|
try:
|
||||||
|
driver.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if pproxy_proc:
|
||||||
|
pproxy_proc.terminate()
|
||||||
|
try:
|
||||||
|
pproxy_proc.wait(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
_release_port(port)
|
||||||
|
|
||||||
|
_processed_logins.add(login)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
global log, RESULT_DIR
|
||||||
|
log = setup_logger()
|
||||||
|
|
||||||
|
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
RESULT_DIR = Path("result") / ts.replace(":", "-")
|
||||||
|
RESULT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
log.info("Results: %s", RESULT_DIR)
|
||||||
|
log.info("Threads: %d", THREADS)
|
||||||
|
|
||||||
|
if TEST_MODE:
|
||||||
|
log.info("TEST MODE enabled — will pause between accounts.")
|
||||||
|
|
||||||
|
ensure_files()
|
||||||
|
|
||||||
|
all_proxies = load_proxies()
|
||||||
|
if PROXY_FILE.exists() and PROXY_FILE.read_text(encoding="utf-8").strip() and not all_proxies:
|
||||||
|
log.warning("Invalid proxy format in %s, running without proxy", PROXY_FILE)
|
||||||
|
|
||||||
|
raw_lines = [l.strip() for l in ACCOUNTS_FILE.read_text(encoding="utf-8").splitlines()
|
||||||
|
if l.strip() and not l.startswith("#")]
|
||||||
|
accounts = []
|
||||||
|
for line in raw_lines:
|
||||||
|
parsed = parse_account_line(line)
|
||||||
|
if parsed:
|
||||||
|
accounts.append(parsed)
|
||||||
|
else:
|
||||||
|
log.warning("Skipping unrecognized line: %s", line)
|
||||||
|
if not accounts:
|
||||||
|
log.error("No valid accounts found in %s (formats: login:pass, login:pass:cookie, login:cookie, or just cookie)", ACCOUNTS_FILE)
|
||||||
|
return
|
||||||
|
|
||||||
|
all_videos = _list_videos()
|
||||||
|
if not all_videos:
|
||||||
|
log.error("No videos in videos/")
|
||||||
|
return
|
||||||
|
log.info("Found %d accounts, %d videos, %d proxies", len(accounts), len(all_videos), len(all_proxies))
|
||||||
|
|
||||||
|
# Process accounts in parallel
|
||||||
|
finished_lines = []
|
||||||
|
with ThreadPoolExecutor(max_workers=THREADS) as pool:
|
||||||
|
fut_map = {}
|
||||||
|
for tid, acc in enumerate(accounts):
|
||||||
|
fut = pool.submit(_process_one, tid, acc,
|
||||||
|
all_videos, all_proxies, RESULT_DIR)
|
||||||
|
fut_map[fut] = acc
|
||||||
|
|
||||||
|
for fut in as_completed(fut_map):
|
||||||
|
acc = fut_map[fut]
|
||||||
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
label = acc["login"] or acc["cookie"][:20]
|
||||||
|
log.error("Thread crashed on %s: %s", label, e)
|
||||||
|
finished_lines.append(acc["_raw"])
|
||||||
|
|
||||||
|
# Remove all processed accounts from accs.txt
|
||||||
|
remaining = [l for l in ACCOUNTS_FILE.read_text(encoding="utf-8").splitlines()
|
||||||
|
if l.strip() not in finished_lines]
|
||||||
|
ACCOUNTS_FILE.write_text("\n".join(remaining) + "\n", encoding="utf-8") if remaining else ACCOUNTS_FILE.write_text("", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Format: socks5://user:pass@host:port
|
||||||
|
# Or: host:port:user:pass
|
||||||
|
# Or: host:port
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
undetected-chromedriver
|
||||||
|
selenium
|
||||||
|
opencv-python
|
||||||
|
pproxy
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import sys, ast, os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
with open("main.py", encoding="utf-8") as f:
|
||||||
|
src = f.read()
|
||||||
|
|
||||||
|
# isolate parse_account_line (no external deps)
|
||||||
|
tree = ast.parse(src)
|
||||||
|
func_def = None
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.FunctionDef) and node.name == "parse_account_line":
|
||||||
|
func_def = node
|
||||||
|
break
|
||||||
|
|
||||||
|
# exec only the function
|
||||||
|
exec(compile(ast.Module([func_def], []), "<parse>", "exec"))
|
||||||
|
|
||||||
|
def test_login_pass():
|
||||||
|
r = parse_account_line("user:pass")
|
||||||
|
assert r["login"] == "user"
|
||||||
|
assert r["password"] == "pass"
|
||||||
|
assert r["cookie"] == ""
|
||||||
|
|
||||||
|
def test_login_pass_cookie():
|
||||||
|
raw = "user:pass:_|WARNING:some-cookie:with:colons"
|
||||||
|
r = parse_account_line(raw)
|
||||||
|
assert r["login"] == "user"
|
||||||
|
assert r["password"] == "pass"
|
||||||
|
assert r["cookie"] == "_|WARNING:some-cookie:with:colons"
|
||||||
|
|
||||||
|
def test_login_cookie():
|
||||||
|
raw = "user:_|WARNING:some-cookie:with:colons"
|
||||||
|
r = parse_account_line(raw)
|
||||||
|
assert r["login"] == "user"
|
||||||
|
assert r["password"] == ""
|
||||||
|
assert r["cookie"] == "_|WARNING:some-cookie:with:colons"
|
||||||
|
|
||||||
|
def test_cookie_only():
|
||||||
|
raw = "_|WARNING:" + "x" * 100
|
||||||
|
r = parse_account_line(raw)
|
||||||
|
assert r["login"] == ""
|
||||||
|
assert r["password"] == ""
|
||||||
|
assert r["cookie"] == raw
|
||||||
|
|
||||||
|
def test_short_line_no_colon():
|
||||||
|
assert parse_account_line("short") is None
|
||||||
|
|
||||||
|
def test_raw_preserved():
|
||||||
|
raw = "user:pass:cookie"
|
||||||
|
r = parse_account_line(raw)
|
||||||
|
assert r["_raw"] == raw
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_login_pass()
|
||||||
|
test_login_pass_cookie()
|
||||||
|
test_login_cookie()
|
||||||
|
test_cookie_only()
|
||||||
|
test_short_line_no_colon()
|
||||||
|
test_raw_preserved()
|
||||||
|
print("All tests passed")
|
||||||
Reference in New Issue
Block a user