pryscraper/signup_automator.py
cryptorugmunch a7c30b12cd
Some checks failed
CI / lint (push) Failing after 2s
CI / typecheck (push) Failing after 2s
CI / test (push) Failing after 2s
CI / Secret scan (gitleaks) (push) Failing after 1s
CI / Security audit (bandit) (push) Failing after 2s
chore(lint): auto-fix 253 of 283 ruff issues (F401, I001, E402, RUF100, UP037, SIM105)
Mass ruff auto-fix:
  - ruff check --fix: 109 issues fixed (F401 unused imports,
    I001 unsorted imports, UP037 quoted annotations, SIM105
    suppressible exception, RUF100 unused-noqa)
  - ruff check --fix --unsafe-fixes: 22 additional issues
  - ruff format: 70 files reformatted
  - Manual pass: fix 16 misplaced import httpx lines
  - Manual pass: fix remaining E402 (import-after-docstring)

Result: 283 errors -> 30 errors.

The remaining 30 are real issues that need manual review:
  5 F401 unused-import (likely auto-generated stubs)
  5 F821 undefined-name (real bugs in code that references
    redis/pydantic/LLMRegistry without imports)
  3 BLE001 (the compliance LLM fallback is intentional; the
    other two are real)
  3 RUF012 mutable-class-default
  3 SIM105, 3 SIM117, 2 E722, 2 E741
  1 B007, 1 B025, 1 E402, 1 RUF200 (pyproject.toml issue)

Tests: 436/437 pass (1 pre-existing SSE sandbox failure).
format check + import sort: now clean.
make ci: still gated on the 30 remaining real issues.
Follow-up: triage the 30 issues file-by-file.
2026-07-02 21:51:25 +02:00

340 lines
11 KiB
Python

# SPDX-License-Identifier: BSL-1.1
# Copyright (c) 2026 Rug Munch Media LLC
#
# Part of Pry — Stealth / Anti-Detection Module
# Licensed under Business Source License 1.1 — see LICENSE-BSL-STEALTH.
# Change Date: 2029-01-01 (converts to MIT).
"""Pry — Signup automator with identity generation, email/SMS verification."""
import asyncio
import json
import logging
import random
import re
import string
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, ClassVar
import httpx
logger = logging.getLogger(__name__)
PROFILES_DIR = Path(__file__).parent / "profiles"
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
class ProfileGenerator:
"""Generate realistic human identities for account registration."""
FIRST_NAMES: ClassVar[list[str]] = [
"James",
"Mary",
"John",
"Patricia",
"Robert",
"Jennifer",
"Michael",
"Linda",
"William",
"Elizabeth",
"David",
"Barbara",
"Richard",
"Susan",
"Joseph",
"Jessica",
"Thomas",
"Sarah",
"Christopher",
"Karen",
]
LAST_NAMES: ClassVar[list[str]] = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Hernandez",
"Lopez",
"Gonzalez",
"Wilson",
"Anderson",
"Thomas",
"Taylor",
"Moore",
"Jackson",
"Martin",
]
DOMAINS: ClassVar[list[str]] = [
"gmail.com",
"yahoo.com",
"outlook.com",
"hotmail.com",
"protonmail.com",
"icloud.com",
"aol.com",
"mail.com",
]
STREETS: ClassVar[list[str]] = [
"Oak",
"Maple",
"Cedar",
"Pine",
"Elm",
"Birch",
"Walnut",
"Cherry",
"Ash",
"Willow",
]
STREET_TYPES: ClassVar[list[str]] = [
"St",
"Ave",
"Rd",
"Dr",
"Ln",
"Blvd",
"Way",
"Ct",
"Pl",
"Cir",
]
CITIES: ClassVar[list[str]] = [
"New York",
"Los Angeles",
"Chicago",
"Houston",
"Phoenix",
"Philadelphia",
"San Antonio",
"San Diego",
"Dallas",
"Austin",
"Portland",
"Seattle",
"Denver",
"Boston",
"Nashville",
]
STATES: ClassVar[list[str]] = [
"NY",
"CA",
"IL",
"TX",
"AZ",
"PA",
"TX",
"CA",
"TX",
"TX",
"OR",
"WA",
"CO",
"MA",
"TN",
]
JOBS: ClassVar[list[str]] = [
"Software Engineer",
"Teacher",
"Nurse",
"Accountant",
"Marketing Manager",
"Data Analyst",
"Graphic Designer",
"Project Manager",
"Sales Associate",
"Customer Support",
"HR Coordinator",
"Financial Analyst",
]
def generate(self, locale: str = "US") -> dict[str, Any]:
idx = random.randint(0, len(self.FIRST_NAMES) - 1)
first = self.FIRST_NAMES[idx]
last = random.choice(self.LAST_NAMES)
username = (first + last + str(random.randint(10, 9999))).lower()
email = f"{username}@{random.choice(self.DOMAINS)}"
state_idx = random.randint(0, len(self.CITIES) - 1) if locale == "US" else 0
profile = {
"first_name": first,
"last_name": last,
"full_name": f"{first} {last}",
"email": email,
"username": username,
"password": self._generate_password(),
"phone": self._generate_phone(),
"address": {
"street": f"{random.randint(100, 9999)} {random.choice(self.STREETS)} {random.choice(self.STREET_TYPES)}",
"city": self.CITIES[state_idx],
"state": self.STATES[state_idx],
"zip": f"{random.randint(10000, 99999)}",
"country": "US",
},
"dob": f"{random.randint(1970, 2002)}-{random.randint(1, 12):02d}-{random.randint(1, 28):02d}",
"job_title": random.choice(self.JOBS),
"created_at": datetime.now(UTC).isoformat(),
"profile_id": self._generate_id(),
}
return profile
def _generate_password(self, length: int = 16) -> str:
chars = string.ascii_letters + string.digits + "!@#$%^&*"
return "".join(random.choice(chars) for _ in range(length))
def _generate_phone(self) -> str:
return f"+1{random.randint(200, 999)}{random.randint(100, 999)}{random.randint(1000, 9999)}"
def _generate_id(self) -> str:
import uuid
return uuid.uuid4().hex[:12]
def save_profile(self, profile: dict[str, Any]) -> str:
pid = profile["profile_id"]
path = PROFILES_DIR / f"{pid}.json"
path.write_text(json.dumps(profile, indent=2))
return pid
def load_profile(self, profile_id: str) -> dict[str, Any] | None:
path = PROFILES_DIR / f"{profile_id}.json"
if path.exists():
return json.loads(path.read_text())
return None
class EmailVerifier:
"""Handle email verification links via temp mail or IMAP."""
def __init__(self, temp_mail_api_key: str = "", imap_config: dict[str, Any] | None = None):
self.temp_mail_api_key = temp_mail_api_key
self.imap_config = imap_config or {}
async def create_temp_inbox(self) -> dict[str, Any]:
"""Create a temporary email inbox for receiving verification links."""
from client import get_client
client = await get_client()
try:
resp = await client.get("https://api.mail.tm/accounts", timeout=10)
domains = resp.json().get("hydra:member", [])
domain = domains[0]["domain"] if domains else "@mail.tm"
import uuid
name = f"pry_{uuid.uuid4().hex[:8]}"
pw = uuid.uuid4().hex[:16]
r = await client.post(
"https://api.mail.tm/accounts",
json={"address": f"{name}{domain}", "password": pw},
timeout=10,
)
if r.is_success:
token_r = await client.post(
"https://api.mail.tm/token",
json={"address": f"{name}{domain}", "password": pw},
timeout=10,
)
token = token_r.json().get("token", "")
return {
"email": f"{name}{domain}",
"password": pw,
"token": token,
"provider": "mail.tm",
}
except (httpx.HTTPError, httpx.RequestError) as e:
logger.warning("temp_mail_failed", extra={"error": str(e)[:80]})
return {"email": "", "error": "Temp mail unavailable"}
async def check_verification_link(
self, token: str, max_wait: int = 120, check_interval: int = 5
) -> str | None:
"""Poll temp inbox for verification link."""
from client import get_client
client = await get_client()
headers = {"Authorization": f"Bearer {token}"}
for _ in range(max_wait // check_interval):
try:
r = await client.get("https://api.mail.tm/messages", headers=headers, timeout=10)
if r.is_success:
messages = r.json().get("hydra:member", [])
for msg in messages:
msg_id = msg.get("id", "")
mr = await client.get(
f"https://api.mail.tm/messages/{msg_id}", headers=headers, timeout=10
)
if mr.is_success:
html = (
mr.json().get("html", [""])[0]
if isinstance(mr.json().get("html"), list)
else str(mr.json())
)
links = re.findall(r'https?://[^\s"<>]+', html)
for link in links:
if (
"verify" in link.lower()
or "confirm" in link.lower()
or "activate" in link.lower()
):
return link
except (httpx.HTTPError, httpx.RequestError):
pass
await asyncio.sleep(check_interval)
return None
class SMSVerifier:
"""Handle SMS verification via Twilio or SMS-activate."""
async def send_sms(
self, phone: str, message: str, provider: str = "twilio", **creds: Any
) -> dict[str, Any]:
if provider == "twilio":
return await self._twilio_send(phone, message, creds)
return {"success": False, "error": f"Unknown SMS provider: {provider}"}
async def _twilio_send(self, phone: str, message: str, creds: dict) -> dict[str, Any]:
from client import get_client
client = await get_client()
import base64
auth = base64.b64encode(
f"{creds.get('account_sid', '')}:{creds.get('auth_token', '')}".encode()
).decode()
resp = await client.post(
f"https://api.twilio.com/2010-04-01/Accounts/{creds.get('account_sid', '')}/Messages.json",
data={"To": phone, "From": creds.get("from_number", ""), "Body": message},
headers={"Authorization": f"Basic {auth}"},
timeout=15,
)
return {"success": resp.is_success, "status": resp.status_code}
async def request_activation_number(self, service: str, country: str = "us") -> dict[str, Any]:
"""Get a phone number for SMS activation via SMS-activate.org."""
from client import get_client
api_key = self._get_key()
if not api_key:
return {"success": False, "error": "SMS-activate API key required"}
client = await get_client()
resp = await client.get(
f"https://sms-activate.org/stubs/handler_api.php?api_key={api_key}&action=getNumber&service={service}&country={country}",
timeout=15,
)
data = resp.text
if "ACCESS_NUMBER" in data:
parts = data.split(":")
return {"success": True, "activation_id": parts[1], "phone": f"+{parts[2]}"}
return {"success": False, "error": data}
def _get_key(self) -> str:
import os
return os.getenv("PRY_SMS_ACTIVATE_KEY", "")