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.
340 lines
11 KiB
Python
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", "")
|