From 0d0a6454e05771facbb970429992eb7153b2404d Mon Sep 17 00:00:00 2001 From: Bruno Leonardo Michels Date: Sat, 18 Jul 2026 21:27:40 -0300 Subject: [PATCH 1/2] feat: add Motherless email and username scan modules Add email-based check via /register/checkemail (email_scan/adult) and a username check via /register/checkusername (user_scan/adult), both using the site's field-availability endpoints on the current motherless.xxx domain. Hits key off the not-available/available response class, with an "invalid" guard separating rejected input from real matches. --- user_scanner/email_scan/adult/motherless.py | 46 +++++++++++++++++++++ user_scanner/user_scan/adult/motherless.py | 35 ++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 user_scanner/email_scan/adult/motherless.py create mode 100644 user_scanner/user_scan/adult/motherless.py 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..d86e733e --- /dev/null +++ b/user_scanner/user_scan/adult/motherless.py @@ -0,0 +1,35 @@ +from user_scanner.core.orchestrator import generic_validate, 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(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 + ) From c8252962a63b38577e41f6f4f7502e0be0160400 Mon Sep 17 00:00:00 2001 From: Bruno Leonardo Michels Date: Sun, 19 Jul 2026 09:11:14 -0300 Subject: [PATCH 2/2] feat: extract profile metadata in Motherless username scan The checkusername endpoint only reports availability, so on a hit fetch the public member page and capture every field it renders via Result.taken(extra=...): the profile-stats block (joined, last seen, profile views, sexuality, favorite porn, website, upload/friend/subscriber counts, etc.), the member-info block (gender, relationship, location) and the avatar. --- user_scanner/user_scan/adult/motherless.py | 44 +++++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/user_scanner/user_scan/adult/motherless.py b/user_scanner/user_scan/adult/motherless.py index d86e733e..c4ebea44 100644 --- a/user_scanner/user_scan/adult/motherless.py +++ b/user_scanner/user_scan/adult/motherless.py @@ -1,4 +1,5 @@ -from user_scanner.core.orchestrator import generic_validate, Result +import re +from user_scanner.core.orchestrator import generic_validate, make_request, Result CHECK_URL = "https://motherless.xxx/register/checkusername" HEADERS = { @@ -23,7 +24,7 @@ def process(response): if 'class="not-available"' in body: if "invalid" in body.lower(): return Result.error("Username rejected by Motherless", url=show_url) - return Result.taken(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) @@ -33,3 +34,42 @@ def process(response): 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'
\s*([^<]+?):\s*\s*(.*?)\s*
', html_text, re.DOTALL): + label = re.sub(r"\s+", " ", row.group(1)).strip() + value = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", row.group(2))).strip() + if value and value != "0": + extra[label] = value + + for row in re.finditer(r'
\s*([^<]+?)\s*\s*(.*?)\s*', html_text, re.DOTALL): + label = re.sub(r"\s+", " ", row.group(1)).strip() + value = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", row.group(2))).strip() + if value: + extra[label] = value + + avatar = re.search(r'og:image"\s+content="([^"]+)"', html_text, re.IGNORECASE) + if avatar: + extra["avatar"] = avatar.group(1) + + return extra