diff --git a/user_scanner/email_scan/adult/motherless.py b/user_scanner/email_scan/adult/motherless.py new file mode 100644 index 00000000..d71194d4 --- /dev/null +++ b/user_scanner/email_scan/adult/motherless.py @@ -0,0 +1,46 @@ +import httpx +from user_scanner.core.result import Result + + +async def _check(email: str) -> Result: + show_url = "https://motherless.xxx" + url = "https://motherless.xxx/register/checkemail" + + headers = { + "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36", + "X-Requested-With": "XMLHttpRequest", + "Origin": show_url, + "Referer": show_url + "/register", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + } + + try: + async with httpx.AsyncClient(http2=True, timeout=15.0) as client: + response = await client.post(url, data={"email": email}, headers=headers) + + if response.status_code == 429: + return Result.error("Rate limited, wait for a few minutes") + + if response.status_code != 200: + return Result.error(f"HTTP Error: {response.status_code}") + + body = response.text + + # The field check reuses "not-available" for both existing accounts + # and rejected input (e.g. "Invalid Host."). + if 'class="not-available"' in body: + if "invalid" in body.lower(): + return Result.error("Email address rejected by Motherless") + return Result.taken(url=show_url) + + if 'class="available"' in body: + return Result.available(url=show_url) + + return Result.error("Unexpected response body, report it via GitHub issues") + + except Exception as e: + return Result.error(e) + + +async def validate_motherless(email: str) -> Result: + return await _check(email) diff --git a/user_scanner/user_scan/adult/motherless.py b/user_scanner/user_scan/adult/motherless.py new file mode 100644 index 00000000..c4ebea44 --- /dev/null +++ b/user_scanner/user_scan/adult/motherless.py @@ -0,0 +1,75 @@ +import re +from user_scanner.core.orchestrator import generic_validate, make_request, Result + +CHECK_URL = "https://motherless.xxx/register/checkusername" +HEADERS = { + "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36", + "X-Requested-With": "XMLHttpRequest", + "Origin": "https://motherless.xxx", + "Referer": "https://motherless.xxx/register", +} + + +def validate_motherless(user): + show_url = f"https://motherless.xxx/m/{user}" + + def process(response): + if response.status_code != 200: + return Result.error(f"Unexpected status: {response.status_code}", url=show_url) + + body = response.text + + # The field check reuses "not-available" for both taken names and + # rejected input (e.g. "Username is invalid."). + if 'class="not-available"' in body: + if "invalid" in body.lower(): + return Result.error("Username rejected by Motherless", url=show_url) + return Result.taken(extra=_fetch_profile(show_url), url=show_url) + + if 'class="available"' in body: + return Result.available(url=show_url) + + return Result.error("Unexpected response body, report it via GitHub issues", url=show_url) + + return generic_validate( + CHECK_URL, process, show_url=show_url, method="POST", data={"username": user}, headers=HEADERS + ) + + +def _fetch_profile(profile_url: str) -> dict: + """The checkusername endpoint only reports availability, so pull the public + member page for metadata. Best-effort: a failed fetch yields no extra.""" + try: + response = make_request(profile_url) + if response.status_code != 200: + return {} + return _extract_profile(response.text) + except Exception: + return {} + + +def _extract_profile(html_text: str) -> dict: + extra = {} + + # The public member page renders two field blocks: "profile-stats" rows + # (Label: value) and "profile-member-info" rows + # (Label value). Pull every row rather than a + # fixed subset so whatever the member fills in is captured. Zero counts are + # dropped as noise. + for row in re.finditer(r'