diff --git a/user_scanner/user_scan/creator/unsplash.py b/user_scanner/user_scan/creator/unsplash.py new file mode 100644 index 00000000..ba141467 --- /dev/null +++ b/user_scanner/user_scan/creator/unsplash.py @@ -0,0 +1,39 @@ +import re +import html +import httpx +from user_scanner.core.result import Result +from user_scanner.core.orchestrator import make_request + +def validate_unsplash(username: str) -> Result: + """Validate a username on Unsplash.""" + url = f"https://unsplash.com/@{username}" + show_url = f"https://unsplash.com/@{username}" + + try: + # We pass headers={} to use httpx's default user-agent, + # as Unsplash blocks/redirects the default browser user-agent. + response = make_request(url, headers={}, follow_redirects=True) + response_text = response.text + + # Verify status code 200 AND check unique Unsplash strings in the response body to prevent false positives + if response.status_code == 200 and "unsplash" in response_text.lower() and "page not found" not in response_text.lower(): + extra = {} + title_match = re.search(r"(.*?)", response_text, re.IGNORECASE) + if title_match: + title = html.unescape(title_match.group(1).strip()) + title_clean = title.split(" | ")[0] + match = re.search(r"^(.*?)\s*\((?:@" + re.escape(username) + r")\)", title_clean, re.IGNORECASE) + if match: + name = match.group(1).strip() + name = re.sub(r"'\s*s (?:Collections|Photos|Likes|Stats)$", "", name, flags=re.IGNORECASE) + name = re.sub(r"'\''s (?:Collections|Photos|Likes|Stats)$", "", name, flags=re.IGNORECASE) + extra["name"] = name + return Result.taken(extra=extra, url=show_url) + elif response.status_code == 404 or "page not found" in response_text.lower(): + return Result.available(url=show_url) + else: + return Result.error(f"Unexpected status: {response.status_code}", url=show_url) + except httpx.TimeoutException: + return Result.skipped("Connection timed out") + except Exception as e: + return Result.error(e, url=show_url) diff --git a/user_scanner/user_scan/dev/hackerone.py b/user_scanner/user_scan/dev/hackerone.py new file mode 100644 index 00000000..fc2ad116 --- /dev/null +++ b/user_scanner/user_scan/dev/hackerone.py @@ -0,0 +1,33 @@ +import re +import html +import httpx +from user_scanner.core.result import Result +from user_scanner.core import orchestrator + +def validate_hackerone(username: str) -> Result: + """Validate a username on HackerOne.""" + url = f"https://hackerone.com/{username}" + show_url = f"https://hackerone.com/{username}" + try: + response = orchestrator.make_request(url, follow_redirects=True) + response_text = response.text + + # Verify status code 200 AND check unique HackerOne strings in the response body to prevent false positives + if response.status_code == 200 and "hackerone" in response_text.lower() and f"hackerone.com/{username}" in response_text.lower(): + extra = {} + # Extract user bio/description from OpenGraph metadata + bio_match = re.search(r'property="og:description"\s+content="([^"]+)"', response_text, re.IGNORECASE) + if not bio_match: + bio_match = re.search(r'content="([^"]+)"\s+property="og:description"', response_text, re.IGNORECASE) + if bio_match: + bio = html.unescape(bio_match.group(1).strip()) + if bio and bio != "-": + extra["bio"] = bio + return Result.taken(extra=extra, url=show_url) + elif response.status_code == 404 or "page not found" in response_text.lower(): + return Result.available(url=show_url) + return Result.error(f"Unexpected status: {response.status_code}", url=show_url) + except httpx.TimeoutException: + return Result.skipped("Connection timed out") + except Exception as e: + return Result.error(e, url=show_url) diff --git a/user_scanner/user_scan/dev/wix.py b/user_scanner/user_scan/dev/wix.py new file mode 100644 index 00000000..9201f10f --- /dev/null +++ b/user_scanner/user_scan/dev/wix.py @@ -0,0 +1,31 @@ +import re +import html +import httpx +from user_scanner.core.result import Result +from user_scanner.core import orchestrator + +def validate_wix(username: str) -> Result: + """Validate a username on Wix.""" + url = f"https://{username}.wix.com" + show_url = f"https://{username}.wix.com" + try: + response = orchestrator.make_request(url, follow_redirects=True) + response_text = response.text + + # Verify status code 200 AND check unique Wix strings in the response body to prevent false positives + if response.status_code == 200 and "wix" in response_text.lower(): + extra = {} + title_match = re.search(r"(.*?)", response_text, re.IGNORECASE) + if title_match: + extra["title"] = html.unescape(title_match.group(1).strip()) + return Result.taken(extra=extra, url=show_url) + elif response.status_code == 404: + return Result.available(url=show_url) + return Result.error(f"Unexpected status: {response.status_code}", url=show_url) + except httpx.ConnectError: + # If the domain doesn't resolve at all, it usually means the username/subdomain is available + return Result.available(url=show_url) + except httpx.TimeoutException: + return Result.skipped("Connection timed out") + except Exception as e: + return Result.error(e, url=show_url) diff --git a/user_scanner/user_scan/finance/tradingview.py b/user_scanner/user_scan/finance/tradingview.py new file mode 100644 index 00000000..2a77e7cd --- /dev/null +++ b/user_scanner/user_scan/finance/tradingview.py @@ -0,0 +1,35 @@ +import re +import html +import httpx +from user_scanner.core.result import Result +from user_scanner.core import orchestrator + +def validate_tradingview(username: str) -> Result: + """Validate a username on TradingView.""" + url = f"https://www.tradingview.com/u/{username}/" + show_url = f"https://www.tradingview.com/u/{username}/" + try: + response = orchestrator.make_request(url, follow_redirects=True) + response_text = response.text + + # Verify status code 200 AND check unique TradingView strings in the response body to prevent false positives + if response.status_code == 200 and "tradingview" in response_text.lower() and "page not found" not in response_text.lower(): + extra = {} + title_match = re.search(r"(.*?)", response_text, re.IGNORECASE) + if title_match: + title = html.unescape(title_match.group(1).strip()) + # TradingView format: {Name} — Trading Ideas and Scripts — TradingView + # or {Username} — Trading Ideas and Scripts — TradingView + title_parts = title.split(" — ") + if len(title_parts) > 0: + name = title_parts[0].strip() + if name.lower() != username.lower(): + extra["name"] = name + return Result.taken(extra=extra, url=show_url) + elif response.status_code == 404 or "page not found" in response_text.lower(): + return Result.available(url=show_url) + return Result.error(f"Unexpected status: {response.status_code}", url=show_url) + except httpx.TimeoutException: + return Result.skipped("Connection timed out") + except Exception as e: + return Result.error(e, url=show_url)