├── user_scanner ├── __init__.py ├── cli │ ├── __init__.py │ └── banner.py ├── utils │ ├── update.py │ └── version.py ├── core │ ├── __init__.py │ ├── result.py │ └── orchestrator.py ├── creator │ ├── __init__.py │ ├── devto.py │ ├── itch_io.py │ ├── kaggle.py │ ├── patreon.py │ ├── medium.py │ ├── hashnode.py │ └── producthunt.py ├── donation │ ├── __init__.py │ ├── buymeacoffee.py │ └── liberapay.py ├── gaming │ ├── __init__.py │ ├── osu.py │ ├── minecraft.py │ ├── steam.py │ ├── chess_com.py │ ├── monkeytype.py │ └── roblox.py ├── dev │ ├── __init__.py │ ├── replit.py │ ├── codeberg.py │ ├── huggingface.py │ ├── dockerhub.py │ ├── npmjs.py │ ├── cratesio.py │ ├── launchpad.py │ ├── gitlab.py │ └── github.py ├── social │ ├── __init__.py │ ├── mastodon.py │ ├── telegram.py │ ├── pinterest.py │ ├── reddit.py │ ├── threads.py │ ├── instagram.py │ ├── discord.py │ ├── snapchat.py │ ├── soundcloud.py │ ├── x.py │ ├── youtube.py │ └── bluesky.py ├── community │ ├── __init__.py │ ├── coderlegion.py │ └── stackoverflow.py ├── version.json └── __main__.py ├── requirements.txt ├── .gitignore ├── pyproject.toml ├── LICENSE ├── README.md └── CONTRIBUTING.md /user_scanner/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user_scanner/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user_scanner/utils/update.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user_scanner/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user_scanner/creator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user_scanner/donation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user_scanner/gaming/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx 2 | colorama 3 | -------------------------------------------------------------------------------- /user_scanner/dev/__init__.py: -------------------------------------------------------------------------------- 1 | # tech 2 | -------------------------------------------------------------------------------- /user_scanner/social/__init__.py: -------------------------------------------------------------------------------- 1 | # social 2 | -------------------------------------------------------------------------------- /user_scanner/community/__init__.py: -------------------------------------------------------------------------------- 1 | # community 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | __pycache__/ 4 | rm_cache.py 5 | -------------------------------------------------------------------------------- /user_scanner/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.8.1", 3 | "version_type": "pypi" 4 | } 5 | -------------------------------------------------------------------------------- /user_scanner/creator/devto.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_devto(user): 5 | url = f"https://dev.to/{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_devto(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/creator/itch_io.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_itch_io(user): 5 | url = f"https://{user}.itch.io" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_itch_io(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/dev/replit.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_replit(user): 5 | url = f"https://replit.com/@{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_replit(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/creator/kaggle.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_kaggle(user): 5 | url = f"https://www.kaggle.com/{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_kaggle(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/dev/codeberg.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_codeberg(user): 5 | url = f"https://codeberg.org/{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_codeberg(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/gaming/osu.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_osu(user): 5 | url = f"https://osu.ppy.sh/users/{user}" 6 | 7 | return status_validate(url, 404, [200, 302], follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_osu(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/dev/huggingface.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_huggingface(user): 5 | url = f"https://huggingface.co/{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_huggingface(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") -------------------------------------------------------------------------------- /user_scanner/social/mastodon.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_mastodon(user): 5 | url = f"https://mastodon.social/@{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_mastodon(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occured!") 20 | -------------------------------------------------------------------------------- /user_scanner/community/coderlegion.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_coderlegion(user): 5 | url = f"https://coderlegion.com/user/{user}" 6 | 7 | return status_validate(url, 404, 200, timeout=15.0) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_coderlegion(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occured!") 20 | -------------------------------------------------------------------------------- /user_scanner/creator/patreon.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_patreon(user): 5 | url = f"https://www.patreon.com/{user}" 6 | 7 | return status_validate(url, 404, 200, timeout=15.0, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_patreon(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occured!") 20 | -------------------------------------------------------------------------------- /user_scanner/donation/buymeacoffee.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_buymeacoffee(user): 5 | url = f"https://buymeacoffee.com/{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_buymeacoffee(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/gaming/minecraft.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_minecraft(user): 5 | url = f"https://api.mojang.com/minecraft/profile/lookup/name/{user}" 6 | 7 | return status_validate(url, 404, 200, follow_redirects=True) 8 | 9 | 10 | if __name__ == "__main__": 11 | user = input("Username?: ").strip() 12 | result = validate_minecraft(user) 13 | 14 | if result == 1: 15 | print("Available!") 16 | elif result == 0: 17 | print("Unavailable!") 18 | else: 19 | print("Error occurred!") 20 | -------------------------------------------------------------------------------- /user_scanner/utils/version.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | _SCRIPT_DIR = Path(__file__).resolve().parent 5 | VERSION_FILE = _SCRIPT_DIR.parent / "version.json" 6 | 7 | 8 | def load_local_version(): 9 | try: 10 | data = json.loads(VERSION_FILE.read_text()) 11 | return data.get("version", "0.0.0"), data.get("version_type", "local") 12 | except FileNotFoundError: 13 | return "N/A", "file_missing" 14 | except json.JSONDecodeError: 15 | return "N/A", "json_error" 16 | except Exception: 17 | return "N/A", "error" 18 | 19 | 20 | if __name__ == "__main__": 21 | version, version_type = load_local_version() 22 | print(f"Version: {version}, Type: {version_type}") 23 | -------------------------------------------------------------------------------- /user_scanner/dev/dockerhub.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_dockerhub(user): 5 | url = f"https://hub.docker.com/v2/users/{user}/" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", 9 | 'Accept': "application/json", 10 | } 11 | 12 | return status_validate(url, 404, 200, headers=headers) 13 | 14 | 15 | if __name__ == "__main__": 16 | user = input("Username?: ").strip() 17 | result = validate_dockerhub(user) 18 | 19 | if result == 1: 20 | print("Available!") 21 | elif result == 0: 22 | print("Unavailable!") 23 | else: 24 | print("Error occurred!") 25 | -------------------------------------------------------------------------------- /user_scanner/dev/npmjs.py: -------------------------------------------------------------------------------- 1 | import re 2 | from user_scanner.core.orchestrator import status_validate, Result 3 | 4 | 5 | def validate_npmjs(user): 6 | if re.match(r'^[^a-zA-Z0-9_-]', user): 7 | return Result.error("Username cannot start with a period") 8 | 9 | if re.search(r'[A-Z]', user): 10 | return Result.error("Username cannot contain uppercase letters.") 11 | 12 | url = f"https://www.npmjs.com/~{user}" 13 | 14 | 15 | return status_validate(url, 404, 200, timeout=3.0, follow_redirects=True) 16 | 17 | 18 | if __name__ == "__main__": 19 | user = input("Username?: ").strip() 20 | result = validate_npmjs(user) 21 | 22 | if result == 1: 23 | print("Available!") 24 | elif result == 0: 25 | print("Unavailable!") 26 | else: 27 | print(f"Error occurred! Reason: {result}") 28 | -------------------------------------------------------------------------------- /user_scanner/dev/cratesio.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_cratesio(user): 5 | url = f"https://crates.io/api/v1/users/{user}" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 9 | 'Accept': "application/json", 10 | 'Referer': "https://crates.io/", 11 | 'sec-fetch-mode': "cors", 12 | } 13 | 14 | return status_validate(url, 404, 200, headers=headers) 15 | 16 | 17 | if __name__ == "__main__": 18 | user = input("Username?: ").strip() 19 | result = validate_cratesio(user) 20 | 21 | if result == 1: 22 | print("Available!") 23 | elif result == 0: 24 | print("Unavailable!") 25 | else: 26 | print("Error occurred!") 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "user-scanner" 7 | version = "1.0.8.1" 8 | description = "Check username availability across multiple popular platforms" 9 | readme = "README.md" 10 | license = {file = "LICENSE"} 11 | authors = [ 12 | {name = "Kaif", email = "kafcodec@gmail.com"} 13 | ] 14 | dependencies = [ 15 | "httpx", 16 | "colorama" 17 | ] 18 | 19 | requires-python = ">=3.7" 20 | keywords = ["username", "checker", "availability", "social", "tech", "python", "user-scanner"] 21 | 22 | [project.urls] 23 | Homepage = "https://github.com/kaifcodec/user-scanner" 24 | 25 | [project.scripts] 26 | user-scanner = "user_scanner.__main__:main" 27 | 28 | [tool.flit.sdist] 29 | exclude = ["tests/", "docs/", ".github/", ".git", "rm_pycache.py", "che.py", ".gitignore"] 30 | -------------------------------------------------------------------------------- /user_scanner/gaming/steam.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_steam(user): 6 | url = f"https://steamcommunity.com/id/{user}/" 7 | 8 | def process(response): 9 | if response.status_code == 200: 10 | if "Error" in response.text: 11 | return Result.available() 12 | else: 13 | return Result.taken() 14 | 15 | return Result.error("Invalid status code") 16 | 17 | return generic_validate(url, process) 18 | 19 | 20 | if __name__ == "__main__": 21 | user = input("Username?: ").strip() 22 | result = validate_steam(user) 23 | 24 | if result == 1: 25 | print("Available!") 26 | elif result == 0: 27 | print("Unavailable!") 28 | else: 29 | print("Error occurred!") 30 | -------------------------------------------------------------------------------- /user_scanner/social/telegram.py: -------------------------------------------------------------------------------- 1 | import re 2 | from user_scanner.core.orchestrator import generic_validate 3 | from user_scanner.core.result import Result 4 | 5 | 6 | def validate_telegram(user: str) -> int: 7 | url = f"https://t.me/{user}" 8 | 9 | def process(r): 10 | if r.status_code == 200: 11 | if re.search(r']*class="tgme_page_extra"[^>]*>', r.text): 12 | return Result.taken() 13 | else: 14 | return Result.available() 15 | return Result.error() 16 | 17 | return generic_validate(url, process, follow_redirects=True) 18 | 19 | 20 | if __name__ == "__main__": 21 | user = input("Username?: ").strip() 22 | result = validate_telegram(user) 23 | 24 | if result == 1: 25 | print("Available!") 26 | elif result == 0: 27 | print("Unavailable!") 28 | else: 29 | print("Error occured!") 30 | -------------------------------------------------------------------------------- /user_scanner/social/pinterest.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | def validate_pinterest(user): 5 | url = f"https://www.pinterest.com/{user}/" 6 | 7 | def process(response): 8 | if response.status_code == 200: 9 | if "User not found." in response.text: 10 | return Result.available() 11 | else: 12 | return Result.taken() 13 | else: 14 | return Result.error("Invalid status code") 15 | 16 | return generic_validate(url, process, follow_redirects=True) 17 | 18 | 19 | if __name__ == "__main__": 20 | user = input("Username?: ").strip() 21 | result = validate_pinterest(user) 22 | 23 | if result == 1: 24 | print("Available!") 25 | elif result == 0: 26 | print("Unavailable!") 27 | else: 28 | print("Error occured!") 29 | -------------------------------------------------------------------------------- /user_scanner/dev/launchpad.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_launchpad(user): 5 | url = f"https://launchpad.net/~{user}" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 9 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9", 10 | 'Accept-Encoding': "gzip, deflate, br, zstd", 11 | 'Upgrade-Insecure-Requests': "1", 12 | } 13 | 14 | return status_validate(url, 404, 200, headers=headers, follow_redirects=True) 15 | 16 | 17 | if __name__ == "__main__": 18 | user = input("Username?: ").strip() 19 | result = validate_launchpad(user) 20 | 21 | if result == 1: 22 | print("Available!") 23 | elif result == 0: 24 | print("Unavailable!") 25 | else: 26 | print("Error occurred!") 27 | -------------------------------------------------------------------------------- /user_scanner/social/reddit.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_reddit(user): 6 | url = f"https://www.reddit.com/user/{user}/" 7 | 8 | def process(response): 9 | if response.status_code == 200: 10 | if "Sorry, nobody on Reddit goes by that name." in response.text: 11 | return Result.available() 12 | else: 13 | return Result.taken() 14 | else: 15 | return Result.error() 16 | 17 | return generic_validate(url, process, follow_redirects=True) 18 | 19 | 20 | if __name__ == "__main__": 21 | user = input("Username?: ").strip() 22 | result = validate_reddit(user) 23 | 24 | if result == 1: 25 | print("Available!") 26 | elif result == 0: 27 | print("Unavailable!") 28 | else: 29 | print("Error occured!") 30 | -------------------------------------------------------------------------------- /user_scanner/social/threads.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_instagram(user): 5 | url = f"https://www.threads.com/api/v1/users/web_profile_info/?username={user}" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 9 | 'X-IG-App-ID': "936619743392459", 10 | 'Accept': "application/json, text/javascript, */*; q=0.01", 11 | 'Accept-Encoding': "gzip, deflate, br", 12 | 'Accept-Language': "en-US,en;q=0.9", 13 | 'X-Requested-With': "XMLHttpRequest", 14 | 'Referer': f"https://www.instagram.com/{user}/", 15 | } 16 | 17 | return status_validate(url, 404, 200, headers=headers) 18 | 19 | 20 | if __name__ == "__main__": 21 | user = input("Username?: ").strip() 22 | result = validate_instagram(user) 23 | 24 | if result == 1: 25 | print("Available!") 26 | elif result == 0: 27 | print("Unavailable!") 28 | else: 29 | print("Error occured!") 30 | -------------------------------------------------------------------------------- /user_scanner/social/instagram.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_instagram(user): 5 | url = f"https://www.instagram.com/api/v1/users/web_profile_info/?username={user}" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 9 | 'X-IG-App-ID': "936619743392459", 10 | 'Accept': "application/json, text/javascript, */*; q=0.01", 11 | 'Accept-Encoding': "gzip, deflate, br", 12 | 'Accept-Language': "en-US,en;q=0.9", 13 | 'X-Requested-With': "XMLHttpRequest", 14 | 'Referer': f"https://www.instagram.com/{user}/", 15 | } 16 | 17 | return status_validate(url, 404, 200, headers=headers) 18 | 19 | 20 | if __name__ == "__main__": 21 | user = input("Username?: ").strip() 22 | result = validate_instagram(user) 23 | 24 | if result == 1: 25 | print("Available!") 26 | elif result == 0: 27 | print("Unavailable!") 28 | else: 29 | print("Error occured!") 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kaif 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /user_scanner/community/stackoverflow.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | def validate_stackoverflow(user: str) -> Result: 5 | url = f"https://stackoverflow.com/users/filter?search={user}" 6 | 7 | def process(response): 8 | if response.status_code == 200: 9 | text = response.text 10 | 11 | if "No users matched your search." in text: 12 | return Result.available() 13 | 14 | pattern = f'>{user}<' 15 | if pattern in text: 16 | return Result.taken() 17 | 18 | return Result.available() 19 | 20 | return Result.error("Unexpected status code from Stack Overflow") 21 | 22 | return generic_validate(url, process) 23 | 24 | 25 | if __name__ == "__main__": 26 | user = input("Username?: ").strip() 27 | result = validate_stackoverflow(user) 28 | 29 | if result == Result.available(): 30 | print("Available!") 31 | elif result == Result.taken(): 32 | print("Unavailable!") 33 | else: 34 | msg = result.get_reason() 35 | print("Error occurred!" + msg) 36 | -------------------------------------------------------------------------------- /user_scanner/creator/medium.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_medium(user): 6 | url = f"https://medium.com/@{user}" 7 | 8 | headers = { 9 | 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", 10 | 'Accept': "text/html", 11 | } 12 | 13 | def process(response): 14 | if response.status_code == 200: 15 | html_text = response.text 16 | 17 | username_tag = f'property="profile:username" content="{user}"' 18 | 19 | if username_tag in html_text: 20 | return Result.taken() 21 | else: 22 | return Result.available() 23 | return Result.error() 24 | 25 | return generic_validate(url, process, headers=headers) 26 | 27 | 28 | if __name__ == "__main__": 29 | user = input("Username?: ").strip() 30 | result = validate_medium(user) 31 | 32 | if result == 1: 33 | print("Available!") 34 | elif result == 0: 35 | print("Unavailable!") 36 | else: 37 | print("Error occurred!") 38 | -------------------------------------------------------------------------------- /user_scanner/donation/liberapay.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_liberapay(user): 5 | url = f"https://en.liberapay.com/{user}" 6 | 7 | headers = { 8 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", 9 | "accept-language": "en-Us,pt;q=0.6", 10 | "cache-control": "no-cache", 11 | "pragma": "no-cache", 12 | "priority": "u=0, i", 13 | "sec-ch-ua": '"Chromium";v="142", "Brave";v="142", "Not_A Brand";v="99"', 14 | "sec-ch-ua-mobile": "?0", 15 | "sec-ch-ua-platform": '"Windows"', 16 | "sec-fetch-dest": "document", 17 | "sec-fetch-mode": "navigate", 18 | "sec-fetch-site": "none", 19 | "sec-fetch-user": "?1", 20 | "sec-gpc": "1", 21 | "upgrade-insecure-requests": "1", 22 | } 23 | 24 | return status_validate(url, 404, 200, headers=headers, follow_redirects=True) 25 | 26 | 27 | if __name__ == "__main__": 28 | user = input("Username?: ").strip() 29 | result = validate_liberapay(user) 30 | 31 | if result == 1: 32 | print("Available!") 33 | elif result == 0: 34 | print("Unavailable!") 35 | else: 36 | print("Error occurred!") 37 | -------------------------------------------------------------------------------- /user_scanner/social/discord.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from user_scanner.core.result import Result 3 | 4 | def validate_discord(user): 5 | url = "https://discord.com/api/v9/unique-username/username-attempt-unauthed" 6 | 7 | headers = { 8 | "authority": "discord.com", 9 | "accept": "/", 10 | "accept-language": "en-GB,en-US;q=0.9,en;q=0.8", 11 | "content-type": "application/json", 12 | "origin": "https://discord.com", 13 | "referer": "https://discord.com/register" 14 | } 15 | 16 | data = {"username": user} 17 | 18 | try: 19 | response = httpx.post(url, headers=headers, json=data, timeout=3.0) 20 | if response.status_code == 200: 21 | status = response.json().get("taken") 22 | if status is True: 23 | return Result.taken() 24 | elif status is False: 25 | return Result.available() 26 | return Result.error("Invalid status code") 27 | except Exception as e: 28 | return Result.error(e) 29 | 30 | 31 | if __name__ == "__main__": 32 | user = input("Username?: ").strip() 33 | result = validate_discord(user) 34 | 35 | if result == 1: 36 | print("Available!") 37 | elif result == 0: 38 | print("Unavailable!") 39 | else: 40 | print("Error occured!") 41 | -------------------------------------------------------------------------------- /user_scanner/gaming/chess_com.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_chess_com(user): 6 | url = f"https://www.chess.com/callback/user/valid?username={user}" 7 | 8 | headers = { 9 | 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 10 | 'Accept': "application/json, text/plain, */*", 11 | 'Accept-Encoding': "identity", 12 | 'Accept-Language': "en-US,en;q=0.9", 13 | } 14 | 15 | def process(response): 16 | if response.status_code == 200: 17 | data = response.json() 18 | if data.get('valid') is True: 19 | # 'valid': true means the username is NOT taken 20 | return Result.available() 21 | elif data.get('valid') is False: 22 | # 'valid': false means the username IS taken 23 | return Result.taken() 24 | return Result.error("Invalid status code") 25 | 26 | return generic_validate(url, process, headers=headers) 27 | 28 | 29 | if __name__ == "__main__": 30 | user = input("Username?: ").strip() 31 | result = validate_chess_com(user) 32 | 33 | if result == 1: 34 | print("Available!") 35 | elif result == 0: 36 | print("Unavailable!") 37 | else: 38 | print("Error occured!") 39 | -------------------------------------------------------------------------------- /user_scanner/social/snapchat.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate 2 | 3 | 4 | def validate_snapchat(user): 5 | url = f"https://www.snapchat.com/@{user}" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 9 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 10 | 'Accept-Encoding': "gzip, deflate, br, zstd", 11 | 'sec-ch-ua': "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"", 12 | 'sec-ch-ua-mobile': "?1", 13 | 'sec-ch-ua-platform': "\"Android\"", 14 | 'upgrade-insecure-requests': "1", 15 | 'sec-fetch-site': "none", 16 | 'sec-fetch-mode': "navigate", 17 | 'sec-fetch-user': "?1", 18 | 'sec-fetch-dest': "document", 19 | 'accept-language': "en-US,en;q=0.9", 20 | 'priority': "u=0, i" 21 | } 22 | 23 | return status_validate(url, 404, 200, headers=headers, follow_redirects=True) 24 | 25 | 26 | if __name__ == "__main__": 27 | user = input("Username?: ").strip() 28 | result = validate_snapchat(user) 29 | 30 | if result == 1: 31 | print("Available!") 32 | elif result == 0: 33 | print("Unavailable!") 34 | else: 35 | print("Error occured!") 36 | -------------------------------------------------------------------------------- /user_scanner/social/soundcloud.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_soundcloud(user): 6 | url = f"https://soundcloud.com/{user}" 7 | 8 | headers = { 9 | 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", 10 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 11 | } 12 | 13 | def process(response): 14 | if response.status_code == 404: 15 | return Result.available() 16 | 17 | if response.status_code == 200: 18 | text = response.text 19 | 20 | if f'soundcloud://users:{user}' in text: 21 | return Result.taken() 22 | if f'"username":"{user}"' in text: 23 | return Result.taken() 24 | if 'soundcloud://users:' in text and '"username":"' in text: 25 | return Result.taken() 26 | 27 | return Result.available() 28 | 29 | return Result.error() 30 | 31 | return generic_validate(url, process, headers=headers, follow_redirects=True) 32 | 33 | 34 | if __name__ == "__main__": 35 | user = input("Username?: ").strip() 36 | result = validate_soundcloud(user) 37 | 38 | if result == 1: 39 | print("Available!") 40 | elif result == 0: 41 | print("Unavailable!") 42 | else: 43 | print("Error occured!") 44 | -------------------------------------------------------------------------------- /user_scanner/cli/banner.py: -------------------------------------------------------------------------------- 1 | import json 2 | from colorama import Fore, Style, init 3 | from ..utils.version import load_local_version 4 | 5 | 6 | version, version_type = load_local_version() 7 | init(autoreset=True) 8 | C_RED = Fore.RED + Style.BRIGHT 9 | C_CYAN = Fore.CYAN + Style.BRIGHT 10 | C_GREEN = Fore.GREEN + Style.BRIGHT 11 | C_WHITE = Fore.WHITE + Style.BRIGHT 12 | C_MAGENTA = Fore.MAGENTA + Style.BRIGHT 13 | BANNER_ASCII = fr"""{C_CYAN} _ _ ___ ___ _ __ ___ ___ __ _ _ __ _ __ ___ _ __ 14 | | | | / __|/ _ \ '__|____/ __|/ __/ _` | '_ \| '_ \ / _ \ '__| 15 | | |_| \__ \ __/ | |_____\__ \ (_| (_| | | | | | | | __/ | 16 | \__,_|___/\___|_| |___/\___\__,_|_| |_|_| |_|\___|_| Version: {version} 17 | {Style.RESET_ALL}""".strip() 18 | 19 | INFO_BOX = f"""{C_MAGENTA} ╔════════════════════════════════════════╗ 20 | ║ {C_RED}♚ {C_GREEN}Project Name{C_WHITE} : UserScanner {C_MAGENTA}║ 21 | ║ {C_RED}♚ {C_GREEN}Author{C_WHITE} : Kaif {C_MAGENTA}║ 22 | ║ {C_RED}♚ {C_GREEN}Github{C_WHITE} : github.com/kaifcodec {C_MAGENTA}║ 23 | ║ {C_RED}♚ {C_GREEN}Email{C_WHITE} : kaifcodec@gmail.com {C_MAGENTA}║ 24 | ══════════════════════════════════════════{Style.RESET_ALL}""".strip() 25 | 26 | 27 | def print_banner(): 28 | print(BANNER_ASCII) 29 | print(INFO_BOX) 30 | print(" ") 31 | 32 | 33 | if __name__ == "__main__": 34 | print_banner() 35 | -------------------------------------------------------------------------------- /user_scanner/dev/gitlab.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_gitlab(user): 6 | url = f"https://gitlab.com/users/{user}/exists" 7 | 8 | headers = { 9 | 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", 10 | 'Accept': "application/json, text/plain, */*", 11 | 'X-Requested-With': "XMLHttpRequest", 12 | 'Referer': "https://gitlab.com/users/sign_up", 13 | } 14 | 15 | def process(response): 16 | if response.status_code == 200: 17 | data = response.json() 18 | if 'exists' in data: 19 | # Corrected: Compare against Python boolean True/False 20 | # AVAILABLE (return 1) if "exists": true 21 | if data['exists'] is False: 22 | return Result.available() 23 | # UNAVAILABLE (return 0) if "exists": false 24 | elif data['exists'] is True: 25 | return Result.taken() 26 | return Result.error("Invalid status code") 27 | 28 | return generic_validate(url, process, headers=headers) 29 | 30 | 31 | if __name__ == "__main__": 32 | user = input("Username?: ").strip() 33 | result = validate_gitlab(user) 34 | 35 | if result == 1: 36 | print("Available!") 37 | elif result == 0: 38 | print("Unavailable!") 39 | else: 40 | print("Error occurred!") 41 | -------------------------------------------------------------------------------- /user_scanner/gaming/monkeytype.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | def validate_monkeytype(user: str) -> int: 5 | 6 | url = f"https://api.monkeytype.com/users/checkName/{user}" 7 | 8 | headers = { 9 | "User-Agent": ( 10 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 11 | "AppleWebKit/537.36 (KHTML, like Gecko) " 12 | "Chrome/128.0.0.0 Safari/537.36" 13 | ), 14 | "Accept": "application/json, text/plain, */*", 15 | "Accept-Encoding": "identity", 16 | "Accept-Language": "en-US,en;q=0.9", 17 | } 18 | 19 | def process(response): 20 | if response.status_code == 200: 21 | data = response.json() 22 | # Expected shape: 23 | # { "message": "string", "data": { "available": true/false } } 24 | payload = data.get("data", {}) 25 | available = payload.get("available") 26 | 27 | if available is True: 28 | return Result.available() 29 | elif available is False: 30 | return Result.taken() 31 | return Result.error("Invalid status code") 32 | 33 | return generic_validate(url, process, headers=headers) 34 | 35 | 36 | if __name__ == "__main__": 37 | user = input("Username?: ").strip() 38 | result = validate_monkeytype(user) 39 | 40 | if result == 1: 41 | print("Available!") 42 | elif result == 0: 43 | print("Unavailable!") 44 | else: 45 | print("Error occurred!") 46 | -------------------------------------------------------------------------------- /user_scanner/creator/hashnode.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_hashnode(user): 6 | url = "https://hashnode.com/utility/ajax/check-username" 7 | 8 | payload = { 9 | "username": user, 10 | "name": "Dummy Dummy" 11 | } 12 | 13 | headers = { 14 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 15 | 'Accept': "application/json", 16 | 'Content-Type': "application/json", 17 | 'Origin': "https://hashnode.com", 18 | 'Referer': "https://hashnode.com/signup", 19 | } 20 | 21 | try: 22 | response = httpx.post(url, json=payload, headers=headers, timeout=3.0) 23 | 24 | if response.status_code == 200: 25 | data = response.json() 26 | 27 | if 'status' in data: 28 | if data['status'] == 1: 29 | return Result.available() 30 | elif data['status'] == 0: 31 | return Result.taken() 32 | 33 | return Result.error("Status not found") 34 | 35 | else: 36 | return Result.error("Invalid status code") 37 | 38 | except Exception as e: 39 | return Result.error(e) 40 | 41 | 42 | if __name__ == "__main__": 43 | user = input("Username?: ").strip() 44 | result = validate_hashnode(user) 45 | 46 | if result == 1: 47 | print("Available!") 48 | elif result == 0: 49 | print("Unavailable!") 50 | else: 51 | print("Error occurred!") 52 | -------------------------------------------------------------------------------- /user_scanner/social/x.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.result import Result 2 | from user_scanner.core.orchestrator import generic_validate 3 | 4 | def validate_x(user): 5 | url = "https://api.twitter.com/i/users/username_available.json" 6 | 7 | params = { 8 | "username": user, 9 | "full_name": "John Doe", 10 | "email": "johndoe07@gmail.com" 11 | } 12 | 13 | headers = { 14 | "Authority": "api.twitter.com", 15 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 16 | } 17 | 18 | def process(response): 19 | status = response.status_code 20 | 21 | if status in [401, 403, 429]: 22 | return Result.error() 23 | 24 | elif status == 200: 25 | data = response.json() 26 | if data.get('valid') is True: 27 | return Result.available() 28 | elif data.get('reason') == 'taken': 29 | return Result.taken() 30 | elif (data.get('reason') == "improper_format" or data.get('reason') == "invalid_username"): 31 | return Result.error(f"X says: {data.get('desc')}") 32 | 33 | return Result.error() 34 | 35 | return generic_validate(url, process, params=params, headers=headers) 36 | 37 | if __name__ == "__main__": 38 | user = input("Username?: ").strip() 39 | result = validate_x(user) 40 | 41 | if result == 1: 42 | print("Available!") 43 | elif result == 0: 44 | print("Unavailable!") 45 | else: 46 | print("Error occured!") 47 | -------------------------------------------------------------------------------- /user_scanner/gaming/roblox.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_roblox(user): 6 | # official api 7 | url = f"https://users.roblox.com/v1/users/search?keyword={user}&limit=10" 8 | 9 | def process(response): 10 | search_results = response.json() # api response 11 | 12 | if response.status_code == 429: 13 | return Result.error("Too many requests") 14 | 15 | if response.status_code == 400: 16 | error = search_results["errors"][0] #Api states theres always an error 17 | if error["code"] == 6: 18 | return Result.error("Username is too short") 19 | if error["code"] == 5: 20 | return Result.error("Username was filtered") 21 | #Shouldn't be able to reach this 22 | return Result.error("Invalid username") 23 | 24 | # iterates through the entries in the search results 25 | for entry in search_results["data"]: 26 | # .lower() so casing from the API doesn't matter 27 | if entry["name"].lower() == user.lower(): # if a username matches the user 28 | return Result.taken() 29 | return Result.available() 30 | 31 | return generic_validate(url, process, follow_redirects=True) 32 | 33 | 34 | if __name__ == "__main__": 35 | user = input("Username?: ").strip() 36 | result = validate_roblox(user) 37 | 38 | if result == 1: 39 | print("Available!") 40 | elif result == 0: 41 | print("Unavailable!") 42 | else: 43 | print("Error occurred!") 44 | -------------------------------------------------------------------------------- /user_scanner/dev/github.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate, Result 2 | 3 | 4 | def validate_github(user): 5 | url = f"https://github.com/signup_check/username?value={user}" 6 | 7 | headers = { 8 | 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", 9 | 'Accept-Encoding': "gzip, deflate, br, zstd", 10 | 'sec-ch-ua-platform': "\"Linux\"", 11 | 'sec-ch-ua': "\"Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Google Chrome\";v=\"140\"", 12 | 'sec-ch-ua-mobile': "?0", 13 | 'sec-fetch-site': "same-origin", 14 | 'sec-fetch-mode': "cors", 15 | 'sec-fetch-dest': "empty", 16 | 'referer': "https://github.com/signup?source=form-home-signup&user_email=", 17 | 'accept-language': "en-US,en;q=0.9", 18 | 'priority': "u=1, i" 19 | } 20 | 21 | GITHUB_INVALID_MSG = ( 22 | "Username may only contain alphanumeric characters or single hyphens, " 23 | "and cannot begin or end with a hyphen." 24 | ) 25 | 26 | def process(response): 27 | if response.status_code == 200: 28 | return Result.available() 29 | 30 | if response.status_code == 422: 31 | if GITHUB_INVALID_MSG in response.text: 32 | return Result.error("Cannot start/end with hyphen or use double hyphens") 33 | 34 | return Result.taken() 35 | 36 | return Result.error("Unexpected GitHub response report it via issues") 37 | 38 | return generic_validate(url, process, headers=headers) 39 | 40 | 41 | if __name__ == "__main__": 42 | user = input("Username?: ").strip() 43 | result = validate_github(user) 44 | 45 | if result == 1: 46 | print("Available!") 47 | elif result == 0: 48 | print("Unavailable!") 49 | else: 50 | print("Error occured!") 51 | -------------------------------------------------------------------------------- /user_scanner/creator/producthunt.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate, Result 2 | 3 | 4 | def validate_youtube(user) -> Result: 5 | url = f"https://m.youtube.com/@{user}" 6 | headers = { 7 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36", 8 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 9 | 'Accept-Encoding': "identity", 10 | 'sec-ch-dpr': "2.75", 11 | 'sec-ch-viewport-width': "980", 12 | 'sec-ch-ua': "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"", 13 | 'sec-ch-ua-mobile': "?1", 14 | 'sec-ch-ua-full-version': "\"143.0.7499.52\"", 15 | 'sec-ch-ua-arch': "\"\"", 16 | 'sec-ch-ua-platform': "\"Android\"", 17 | 'sec-ch-ua-platform-version': "\"15.0.0\"", 18 | 'sec-ch-ua-bitness': "\"\"", 19 | 'sec-ch-ua-wow64': "?0", 20 | 'sec-ch-ua-full-version-list': "\"Google Chrome\";v=\"143.0.7499.52\", \"Chromium\";v=\"143.0.7499.52\", \"Not A(Brand\";v=\"24.0.0.0\"", 21 | 'sec-ch-ua-form-factors': "\"Mobile\"", 22 | 'upgrade-insecure-requests': "1", 23 | 'x-browser-channel': "stable", 24 | 'x-browser-year': "2025", 25 | 'x-browser-copyright': "Copyright 2025 Google LLC. All Rights reserved.", 26 | 'sec-fetch-site': "none", 27 | 'sec-fetch-mode': "navigate", 28 | 'sec-fetch-user': "?1", 29 | 'sec-fetch-dest': "document", 30 | 'accept-language': "en-US,en;q=0.9", 31 | 'priority': "u=0, i" 32 | } 33 | 34 | return status_validate(url, 404, 200, headers=headers) 35 | 36 | 37 | if __name__ == "__main__": 38 | user = input("Username?: ").strip() 39 | result = validate_youtube(user) 40 | 41 | if result == 1: 42 | print("Available!") 43 | elif result == 0: 44 | print("Unavailable!") 45 | else: 46 | reason = result.get_reason() 47 | print(f"Error occurred! Reason: {reason}") 48 | -------------------------------------------------------------------------------- /user_scanner/social/youtube.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import status_validate, Result 2 | 3 | 4 | def validate_youtube(user) -> Result: 5 | url = f"https://m.youtube.com/@{user}" 6 | headers = { 7 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36", 8 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 9 | 'Accept-Encoding': "identity", 10 | 'sec-ch-dpr': "2.75", 11 | 'sec-ch-viewport-width': "980", 12 | 'sec-ch-ua': "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"", 13 | 'sec-ch-ua-mobile': "?1", 14 | 'sec-ch-ua-full-version': "\"143.0.7499.52\"", 15 | 'sec-ch-ua-arch': "\"\"", 16 | 'sec-ch-ua-platform': "\"Android\"", 17 | 'sec-ch-ua-platform-version': "\"15.0.0\"", 18 | 'sec-ch-ua-model': "\"I2404\"", 19 | 'sec-ch-ua-bitness': "\"\"", 20 | 'sec-ch-ua-wow64': "?0", 21 | 'sec-ch-ua-full-version-list': "\"Google Chrome\";v=\"143.0.7499.52\", \"Chromium\";v=\"143.0.7499.52\", \"Not A(Brand\";v=\"24.0.0.0\"", 22 | 'sec-ch-ua-form-factors': "\"Mobile\"", 23 | 'upgrade-insecure-requests': "1", 24 | 'x-browser-channel': "stable", 25 | 'x-browser-year': "2025", 26 | 'x-browser-copyright': "Copyright 2025 Google LLC. All Rights reserved.", 27 | 'sec-fetch-site': "none", 28 | 'sec-fetch-mode': "navigate", 29 | 'sec-fetch-user': "?1", 30 | 'sec-fetch-dest': "document", 31 | 'accept-language': "en-US,en;q=0.9", 32 | 'priority': "u=0, i" 33 | } 34 | 35 | 36 | return status_validate(url, 404, 200, headers=headers) 37 | 38 | 39 | if __name__ == "__main__": 40 | user = input("Username?: ").strip() 41 | result = validate_youtube(user) 42 | 43 | if result == 1: 44 | print("Available!") 45 | elif result == 0: 46 | print("Unavailable!") 47 | else: 48 | reason = result.get_reason() 49 | print(f"Error occurred! Reason: {reason}") 50 | 51 | -------------------------------------------------------------------------------- /user_scanner/social/bluesky.py: -------------------------------------------------------------------------------- 1 | from user_scanner.core.orchestrator import generic_validate 2 | from user_scanner.core.result import Result 3 | 4 | 5 | def validate_bluesky(user): 6 | handle = user if user.endswith('.bsky.social') else f"{user}.bsky.social" 7 | url = "https://bsky.social/xrpc/com.atproto.temp.checkHandleAvailability" 8 | 9 | headers = { 10 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 11 | 'Accept-Encoding': "gzip", 12 | 'atproto-accept-labelers': "did:plc:ar7c4by46qjdydhdevvrndac;redact", 13 | 'sec-ch-ua-platform': "\"Android\"", 14 | 'sec-ch-ua': "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"", 15 | 'sec-ch-ua-mobile': "?1", 16 | 'origin': "https://bsky.app", 17 | 'sec-fetch-site': "cross-site", 18 | 'sec-fetch-mode': "cors", 19 | 'sec-fetch-dest': "empty", 20 | 'referer': "https://bsky.app/", 21 | 'accept-language': "en-US,en;q=0.9", 22 | } 23 | 24 | params = { 25 | 'handle': handle, 26 | } 27 | 28 | def process(response): 29 | if response.status_code == 200: 30 | data = response.json() 31 | result_type = data.get('result', {}).get('$type') 32 | 33 | if result_type == "com.atproto.temp.checkHandleAvailability#resultAvailable": 34 | return Result.available() 35 | elif result_type == "com.atproto.temp.checkHandleAvailability#resultUnavailable": 36 | return Result.taken() 37 | elif response.status_code == 400: 38 | return Result.error("Username can only contain letters, numbers, hyphens (no leading/trailing)") 39 | 40 | return Result.error("Invalid status code!") 41 | 42 | return generic_validate(url, process, headers=headers, params=params, timeout=15.0) 43 | 44 | 45 | if __name__ == "__main__": 46 | user = input("Username?: ").strip() 47 | result = validate_bluesky(user) 48 | 49 | if result == 1: 50 | print("Available!") 51 | elif result == 0: 52 | print("Unavailable!") 53 | else: 54 | print("Error occured!") 55 | -------------------------------------------------------------------------------- /user_scanner/core/result.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Literal 3 | 4 | 5 | def humanize_exception(e: Exception) -> str: 6 | msg = str(e).lower() 7 | 8 | if "10054" in msg: 9 | return "Connection closed by remote server" 10 | if "11001" in msg: 11 | return "Could not resolve hostname" 12 | 13 | return str(e) 14 | 15 | 16 | class Status(Enum): 17 | TAKEN = 0 18 | AVAILABLE = 1 19 | ERROR = 2 20 | 21 | 22 | class Result: 23 | def __init__(self, status: Status, reason: str | Exception | None = None): 24 | self.status = status 25 | self.reason = reason 26 | 27 | @classmethod 28 | def taken(cls): 29 | return cls(Status.TAKEN) 30 | 31 | @classmethod 32 | def available(cls): 33 | return cls(Status.AVAILABLE) 34 | 35 | @classmethod 36 | def error(cls, reason: str | Exception | None = None): 37 | return cls(Status.ERROR, reason) 38 | 39 | @classmethod 40 | def from_number(cls, i: int, reason: str | Exception | None = None): 41 | try: 42 | status = Status(i) 43 | except ValueError: 44 | return cls(Status.ERROR, "Invalid status. Please contact maintainers.") 45 | 46 | return cls(status, reason if status == Status.ERROR else None) 47 | 48 | def to_number(self) -> int: 49 | return self.status.value 50 | 51 | def has_reason(self) -> bool: 52 | return self.reason != None 53 | 54 | def get_reason(self) -> str: 55 | if self.reason == None: 56 | return "" 57 | if isinstance(self.reason, str): 58 | return self.reason 59 | #Format the exception 60 | msg = humanize_exception(self.reason) 61 | return f"{type(self.reason).__name__}: {msg.capitalize()}" 62 | 63 | def __str__(self): 64 | return self.get_reason() 65 | 66 | def __eq__(self, other): 67 | if isinstance(other, Status): 68 | return self.status == other 69 | 70 | if isinstance(other, Result): 71 | return self.status == other.status 72 | 73 | if isinstance(other, int): 74 | return self.to_number() == other 75 | 76 | return NotImplemented 77 | 78 | 79 | AnyResult = Literal[0, 1, 2] | Result 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # User Scanner 2 | 3 | ![1000136215](https://github.com/user-attachments/assets/49ec8d24-665b-4115-8525-01a8d0ca2ef4) 4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | --- 14 | 15 | Scan a username across multiple social, developer, and creator platforms to see if it’s available. 16 | Perfect for finding a **unique username** across GitHub, Twitter, Reddit, Instagram, and more, all in one command. 17 | 18 | 19 | ### Features 20 | 21 | - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**. 22 | - ✅ Clear **Available / Taken / Error** output for each platform. 23 | - ✅ Robust error handling: It prints the exact reason (e.g. Cannot use underscores, hyphens at the start/end) 24 | - ✅ Fully modular: add new platform modules easily. 25 | - ✅ Wildcard-based username permutations for automatic variation generation using provided suffix 26 | - ✅ Command-line interface ready: works directly after `pip install` 27 | - ✅ Can be used as username OSINT tool. 28 | - ✅ Very low and lightweight dependencies, can be run on any machine. 29 | --- 30 | 31 | ### Installation 32 | 33 | ```bash 34 | pip install user-scanner 35 | ``` 36 | 37 | --- 38 | 39 | ### Usage 40 | 41 | Scan a username across all platforms: 42 | 43 | ```bash 44 | user-scanner -u 45 | ``` 46 | Optionally, scan a specific category or single module: 47 | 48 | ```bash 49 | user-scanner -u -c dev 50 | user-scanner -l # Lists all available modules 51 | user-scanner -u -m github 52 | user-scanner -u -p 53 | 54 | ``` 55 | 56 | Generate multiple username variations by appending a suffix: 57 | 58 | ```bash 59 | user-scanner -u -p 60 | 61 | ``` 62 | Optionally, scan a specific category or single module with limit: 63 | 64 | ```bash 65 | user-scanner -u -p -c dev 66 | user-scanner -u -p -m github 67 | user-scanner -u -p -s # limit generation of usernames 68 | user-scanner -u -p -d #delay to avoid rate-limits 69 | ``` 70 | 71 | --- 72 | ### Screenshot: 73 | 74 | - Note*: New modules are constantly getting added so this might have only limited, outdated output: 75 | 76 | 77 | 1000140392 78 | 79 | 80 | --- 81 | 82 | 1000140393 83 | 84 | 85 | ### Contributing: 86 | 87 | Modules are organized by category: 88 | 89 | ``` 90 | user_scanner/ 91 | ├── dev/ # Developer platforms (GitHub, GitLab, etc.) 92 | ├── social/ # Social platforms (Twitter/X, Reddit, Instagram, etc.) 93 | ├── creator/ # Creator platforms (Hashnode, Dev.to, Medium, etc.) 94 | ├── community/ # Community platforms (forums, niche sites) 95 | ├── gaming/ # Gaming sites (chess.com, roblox, monkeytype etc.) 96 | ├── donation/ # Donation taking sites (buymeacoffe.com, similar...) 97 | ``` 98 | 99 | **Module guidelines:** 100 | This project contains small "validator" modules that check whether a username exists on a given platform. Each validator is a single function that returns a Result object (see `core/orchestrator.py`). 101 | 102 | Result semantics: 103 | - Result.available() → `available` 104 | - Result.taken() → `taken` 105 | - Result.error(message: Optional[str]) → `error`, blocked, unknown, or request failure (include short diagnostic message when helpful) 106 | 107 | Follow this document when adding or updating validators. 108 | 109 | See [CONTRIBUTING.md](CONTRIBUTING.md) for examples. 110 | 111 | --- 112 | 113 | ### Dependencies: 114 | - [httpx](https://pypi.org/project/httpx/) 115 | - [colorama](https://pypi.org/project/colorama/) 116 | 117 | --- 118 | 119 | ### License 120 | 121 | This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details. 122 | 123 | 124 | --- 125 | 126 | ### Star History 127 | 128 | 129 | 130 | 131 | 132 | Star History Chart 133 | 134 | 135 | -------------------------------------------------------------------------------- /user_scanner/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time 3 | import re 4 | from user_scanner.core.orchestrator import run_checks, load_modules , generate_permutations, load_categories 5 | from colorama import Fore, Style 6 | from .cli import banner 7 | from .cli.banner import print_banner 8 | 9 | MAX_PERMUTATIONS_LIMIT = 100 # To prevent excessive generation 10 | 11 | def list_modules(category=None): 12 | categories = load_categories() 13 | categories_to_list = [category] if category else categories.keys() 14 | 15 | for cat_name in categories_to_list: 16 | path = categories[cat_name] 17 | modules = load_modules(path) 18 | print(Fore.MAGENTA + 19 | f"\n== {cat_name.upper()} SITES =={Style.RESET_ALL}") 20 | for module in modules: 21 | site_name = module.__name__.split(".")[-1] 22 | print(f" - {site_name}") 23 | 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser( 27 | prog="user-scanner", 28 | description="Scan usernames across multiple platforms." 29 | ) 30 | parser.add_argument( 31 | "-u", "--username", help="Username to scan across platforms" 32 | ) 33 | parser.add_argument( 34 | "-c", "--category", choices=load_categories().keys(), 35 | help="Scan all platforms in a category" 36 | ) 37 | parser.add_argument( 38 | "-m", "--module", help="Scan a single specific module across all categories" 39 | ) 40 | parser.add_argument( 41 | "-l", "--list", action="store_true", help="List all available modules by category" 42 | ) 43 | parser.add_argument( 44 | "-v", "--verbose", action="store_true", help="Enable verbose output" 45 | ) 46 | 47 | parser.add_argument( 48 | "-p", "--permute",type=str,help="Generate username permutations using a string pattern (e.g -p 234)" 49 | ) 50 | parser.add_argument( 51 | "-s", "--stop",type=int,default=MAX_PERMUTATIONS_LIMIT,help="Limit the number of username permutations generated" 52 | ) 53 | 54 | parser.add_argument( 55 | "-d", "--delay",type=float,default=0,help="Delay in seconds between requests (recommended: 1-2 seconds)" 56 | ) 57 | 58 | args = parser.parse_args() 59 | 60 | if args.list: 61 | list_modules(args.category) 62 | return 63 | 64 | if not args.username: 65 | parser.print_help() 66 | return 67 | 68 | # Special username checks before run 69 | if (args.module == "x" or args.category == "social"): 70 | if re.search(r"[^a-zA-Z0-9._-]", args.username): 71 | print( 72 | Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. X (Twitter) doesn't support these." + Style.RESET_ALL) 73 | if (args.module == "bluesky" or args.category == "social"): 74 | if re.search(r"[^a-zA-Z0-9\.-]", args.username): 75 | print( 76 | Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. Bluesky will throw error. (Supported: only hyphens and digits)" + Style.RESET_ALL + "\n") 77 | print_banner() 78 | 79 | if args.permute and args.delay == 0: 80 | print( 81 | Fore.YELLOW 82 | + "[!] Warning: You're generating multiple usernames with NO delay between requests. " 83 | "This may trigger rate limits or IP bans. Use --delay 1 or higher. (Use only if the sites throw errors otherwise ignore)\n" 84 | + Style.RESET_ALL) 85 | 86 | usernames = [args.username] # Default single username list 87 | 88 | #Added permutation support , generate all possible permutation of given sequence. 89 | if args.permute: 90 | usernames = generate_permutations(args.username, args.permute , args.stop) 91 | print(Fore.CYAN + f"[+] Generated {len(usernames)} username permutations" + Style.RESET_ALL) 92 | 93 | 94 | 95 | if args.module and "." in args.module: 96 | args.module = args.module.replace(".", "_") 97 | 98 | 99 | if args.module: 100 | # Single module search across all categories 101 | found = False 102 | for cat_path in load_categories().values(): 103 | modules = load_modules(cat_path) 104 | for module in modules: 105 | site_name = module.__name__.split(".")[-1] 106 | if site_name.lower() == args.module.lower(): 107 | from user_scanner.core.orchestrator import run_module_single 108 | for name in usernames: # <-- permutation support here 109 | run_module_single(module, name) 110 | if args.delay > 0: 111 | time.sleep(args.delay) 112 | found = True 113 | if not found: 114 | print( 115 | Fore.RED + f"[!] Module '{args.module}' not found in any category." + Style.RESET_ALL) 116 | elif args.category: 117 | # Category-wise scan 118 | category_package = load_categories().get(args.category) 119 | from user_scanner.core.orchestrator import run_checks_category 120 | 121 | for name in usernames: # <-- permutation support here 122 | run_checks_category(category_package, name, args.verbose) 123 | if args.delay > 0: 124 | time.sleep(args.delay) 125 | else: 126 | # Full scan 127 | for name in usernames: 128 | run_checks(name) 129 | if args.delay > 0: 130 | time.sleep(args.delay) 131 | 132 | 133 | if __name__ == "__main__": 134 | main() 135 | -------------------------------------------------------------------------------- /user_scanner/core/orchestrator.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import importlib.util 3 | from colorama import Fore, Style 4 | import threading 5 | from itertools import permutations 6 | import httpx 7 | from pathlib import Path 8 | from user_scanner.core.result import Result, AnyResult 9 | from typing import Callable, Dict, List 10 | 11 | lock = threading.Condition() 12 | # Basically which thread is the one to print 13 | print_queue = 0 14 | 15 | 16 | def load_modules(category_path: Path): 17 | modules = [] 18 | for file in category_path.glob("*.py"): 19 | if file.name == "__init__.py": 20 | continue 21 | spec = importlib.util.spec_from_file_location(file.stem, str(file)) 22 | module = importlib.util.module_from_spec(spec) 23 | spec.loader.exec_module(module) 24 | 25 | modules.append(module) 26 | return modules 27 | 28 | 29 | def load_categories() -> Dict[str, Path]: 30 | root = Path(__file__).resolve().parent.parent # Should be user_scanner 31 | categories = {} 32 | 33 | for subfolder in root.iterdir(): 34 | if subfolder.is_dir() and \ 35 | not subfolder.name.lower() in ["cli", "utils", "core"] and \ 36 | not "__" in subfolder.name: # Removes __pycache__ 37 | categories[subfolder.name] = subfolder.resolve() 38 | 39 | return categories 40 | 41 | 42 | def worker_single(module, username, i): 43 | global print_queue 44 | 45 | func = next((getattr(module, f) for f in dir(module) 46 | if f.startswith("validate_") and callable(getattr(module, f))), None) 47 | site_name = module.__name__.split('.')[-1].capitalize().replace("_", ".") 48 | if site_name == "X": 49 | site_name = "X (Twitter)" 50 | 51 | output = "" 52 | if func: 53 | try: 54 | result = func(username) 55 | reason = "" 56 | 57 | if isinstance(result, Result) and result.has_reason(): 58 | reason = f" ({result.get_reason()})" 59 | 60 | if result == 1: 61 | output = f" {Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}" 62 | elif result == 0: 63 | output = f" {Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}" 64 | else: 65 | output = f" {Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}" 66 | except Exception as e: 67 | output = f" {Fore.YELLOW}[!] {site_name}: Exception - {e}{Style.RESET_ALL}" 68 | else: 69 | output = f" {Fore.YELLOW}[!] {site_name} has no validate_ function{Style.RESET_ALL}" 70 | 71 | with lock: 72 | # Waits for in-order printing 73 | while i != print_queue: 74 | lock.wait() 75 | 76 | print(output) 77 | print_queue += 1 78 | lock.notify_all() 79 | 80 | 81 | def run_module_single(module, username): 82 | # Just executes as if it was a thread 83 | worker_single(module, username, print_queue) 84 | 85 | 86 | def run_checks_category(category_path:Path, username:str, verbose=False): 87 | global print_queue 88 | 89 | modules = load_modules(category_path) 90 | category_name = category_path.stem.capitalize() 91 | print(f"{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}") 92 | 93 | print_queue = 0 94 | 95 | threads = [] 96 | for i, module in enumerate(modules): 97 | t = threading.Thread(target=worker_single, args=(module, username, i)) 98 | threads.append(t) 99 | t.start() 100 | 101 | for t in threads: 102 | t.join() 103 | 104 | 105 | def run_checks(username): 106 | print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}\n") 107 | 108 | for category_path in load_categories().values(): 109 | run_checks_category(category_path, username) 110 | print() 111 | 112 | 113 | def make_get_request(url: str, **kwargs) -> httpx.Response: 114 | """Simple wrapper to **httpx.get** that predefines headers and timeout""" 115 | if not "headers" in kwargs: 116 | kwargs["headers"] = { 117 | 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", 118 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 119 | 'Accept-Encoding': "gzip, deflate, br", 120 | 'Accept-Language': "en-US,en;q=0.9", 121 | 'sec-fetch-dest': "document", 122 | } 123 | 124 | if not "timeout" in kwargs: 125 | kwargs["timeout"] = 5.0 126 | 127 | return httpx.get(url, **kwargs) 128 | 129 | 130 | def generic_validate(url: str, func: Callable[[httpx.Response], AnyResult], **kwargs) -> AnyResult: 131 | """ 132 | A generic validate function that makes a request and executes the provided function on the response. 133 | """ 134 | try: 135 | response = make_get_request(url, **kwargs) 136 | return func(response) 137 | except Exception as e: 138 | return Result.error(e) 139 | 140 | 141 | def status_validate(url: str, available: int | List[int], taken: int | List[int], **kwargs) -> Result: 142 | """ 143 | Function that takes a **url** and **kwargs** for the request and 144 | checks if the request status matches the availabe or taken. 145 | **Available** and **Taken** must either be whole numbers or lists of whole numbers. 146 | """ 147 | def inner(response: httpx.Response): 148 | # Checks if a number is equal or is contained inside 149 | def contains(a, b): return (isinstance(a, list) and b in a) or (a == b) 150 | status = response.status_code 151 | available_value = contains(available, status) 152 | taken_value = contains(taken, status) 153 | 154 | if available_value and taken_value: 155 | # Can't be both available and taken 156 | return Result.error("Invalid status match. Report this on Github.") 157 | elif available_value: 158 | return Result.available() 159 | elif taken_value: 160 | return Result.taken() 161 | return Result.error("Status didn't match. Report this on Github.") 162 | 163 | return generic_validate(url, inner, **kwargs) 164 | 165 | def generate_permutations(username, pattern, limit=None): 166 | """ 167 | Generate all order-based permutations of characters in `pattern` 168 | appended after `username`. 169 | """ 170 | permutations_set = {username} 171 | 172 | chars = list(pattern) 173 | 174 | # generate permutations of length 1 → len(chars) 175 | for r in range(1, len(chars) + 1): 176 | for combo in permutations(chars, r): 177 | permutations_set.add(username + ''.join(combo)) 178 | if limit and len(permutations_set) >= limit: 179 | return list(permutations_set)[:limit] 180 | 181 | return sorted(permutations_set) 182 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to user-scanner 2 | 3 | Thanks for contributing! This guide explains how to add or modify platform validators correctly, and it includes the orchestrator helpers (generic_validate and status_validate) used to keep validators small and consistent. 4 | 5 | --- 6 | 7 | ## Overview 8 | 9 | This project contains small "validator" modules that check whether a username exists on a given platform. Each validator is a single function that returns a Result object (see core/orchestrator.py). 10 | 11 | Result semantics: 12 | - Result.available() → available 13 | - Result.taken() → taken 14 | - Result.error(message: Optional[str]) → error, blocked, unknown, or request failure (include short diagnostic message when helpful) 15 | 16 | Follow this document when adding or updating validators. 17 | 18 | --- 19 | 20 | ## Folder structure 21 | 22 | - `social/` -> Social media platforms (Instagram, Reddit, X, etc.) 23 | - `dev/` -> Developer platforms (GitHub, GitLab, Kaggle, etc.) 24 | - `community/` -> Miscellaneous or community-specific platforms 25 | - Add new directories for new categories as needed. 26 | 27 | Example: 28 | ``` 29 | user_scanner/ 30 | ├── social/ 31 | | └── reddit.py 32 | | ... 33 | ├── dev/ 34 | | └── launchpad.py 35 | | ... 36 | └── core/ 37 | └── orchestrator.py 38 | ... 39 | ``` 40 | 41 | Place each new module in the most relevant folder. 42 | 43 | --- 44 | 45 | ## Module naming 46 | 47 | - File name must be the platform name in lowercase (no spaces or special characters). 48 | - Examples: `github.py`, `reddit.py`, `x.py`, `pinterest.py` 49 | 50 | --- 51 | 52 | ## Validator function 53 | 54 | Each module must expose exactly one validator function named: 55 | 56 | ```python 57 | def validate_(user: str) -> Result: 58 | ... 59 | ``` 60 | 61 | Rules: 62 | - Single parameter: the username (str). 63 | - Return a Result object (use Result.available(), Result.taken(), or Result.error(msg)). 64 | - Keep the function synchronous unless you are implementing an optional async variant; prefer sync for consistency. 65 | - Prefer using the orchestrator helpers (see below) so validators stay small and consistent. 66 | 67 | --- 68 | 69 | ## Orchestrator helpers 70 | 71 | To keep validators DRY, the repository provides helper functions in `core/orchestrator.py`. Use these where appropriate. 72 | 73 | 1. generic_validate 74 | - Purpose: Run a request for a given URL and let a small callback (processor) inspect the httpx.Response and return a Result. 75 | - Typical signature (example — consult the actual orchestrator implementation for exact parameter names): 76 | - `generic_validate(url: str, processor: Callable[[httpx.Response], Result], headers: Optional[dict] = None, timeout: float = 5.0, follow_redirects: bool = False) -> Result` 77 | - Processor function signature: 78 | - def process(response) -> Result 79 | - Must return Result.available(), Result.taken(), or Result.error("message") 80 | - Use case: Sites that return 200 for both found and not-found states and require checking the HTML body for a unique "not found" string (or other content inspection). 81 | 82 | ### Example `github.py` module: 83 | - This example shows how to use `generic_validate()` and how to return Result values with optional error messages. 84 | 85 | ```python 86 | from user_scanner.core.orchestrator import generic_validate, Result 87 | 88 | 89 | def validate_github(user): 90 | url = f"https://github.com/signup_check/username?value={user}" 91 | 92 | headers = { 93 | 'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", 94 | 'Accept-Encoding': "gzip, deflate, br, zstd", 95 | 'sec-ch-ua-platform': "\"Linux\"", 96 | 'sec-ch-ua': "\"Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Google Chrome\";v=\"140\"", 97 | 'sec-ch-ua-mobile': "?0", 98 | 'sec-fetch-site': "same-origin", 99 | 'sec-fetch-mode': "cors", 100 | 'sec-fetch-dest': "empty", 101 | 'referer': "https://github.com/signup?source=form-home-signup&user_email=", 102 | 'accept-language': "en-US,en;q=0.9", 103 | 'priority': "u=1, i" 104 | } 105 | 106 | GITHUB_INVALID_MSG = ( 107 | "Username may only contain alphanumeric characters or single hyphens, " 108 | "and cannot begin or end with a hyphen." 109 | ) 110 | 111 | def process(response): 112 | if response.status_code == 200: 113 | return Result.available() 114 | 115 | if response.status_code == 422: 116 | if GITHUB_INVALID_MSG in response.text: 117 | return Result.error("Cannot start/end with hyphen or use double hyphens") 118 | 119 | return Result.taken() 120 | 121 | return Result.error("Unexpected GitHub response — report it via issues") 122 | 123 | return generic_validate(url, process, headers=headers) 124 | 125 | 126 | if __name__ == "__main__": 127 | user = input("Username?: ").strip() 128 | result = validate_github(user) 129 | 130 | # Inspect Result (example usage; adapt to actual Result API in orchestrator) 131 | if result == Result.available(): 132 | print("Available!") 133 | elif result == Result.taken(): 134 | print("Unavailable!") 135 | else: 136 | # Result.error can carry a message; show it when present 137 | msg = getattr(result, "message", None) 138 | print("Error occurred!" + (f" {msg}" if msg else "")) 139 | ``` 140 | 141 | 2. status_validate 142 | - Purpose: Simple helper for sites where availability can be determined purely from HTTP status codes (e.g., 404 = available, 200 = taken). 143 | - Typical signature (example): 144 | - `status_validate(url: str, available_status: int, taken_status: int, headers: Optional[dict] = None, timeout: float = 5.0, follow_redirects: bool = False) -> Result` 145 | - Use case: Sites that reliably return 404 for missing profiles and 200 for existing ones. 146 | 147 | ### Example `launchpad.py` module: 148 | 149 | ```python 150 | from user_scanner.core.orchestrator import status_validate 151 | 152 | def validate_launchpad(user: str): 153 | """ 154 | Uses status_validate because Launchpad returns 404 for non-existing users 155 | and 200 for existing ones. 156 | """ 157 | url = f"https://launchpad.net/~{user}" 158 | 159 | headers = { 160 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 161 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9", 162 | "Accept-Encoding": "gzip, deflate, br, zstd", 163 | "Upgrade-Insecure-Requests": "1", 164 | } 165 | 166 | # available_status=404, taken_status=200 167 | return status_validate(url, 404, 200, headers=headers, follow_redirects=True) 168 | ``` 169 | 170 | Note: The exact parameter names and behavior of the orchestrator functions are defined in `core/orchestrator.py`. Use this CONTRIBUTING guide as a reference for when to use each helper; inspect the implementation for exact types and helper methods on Result. 171 | 172 | --- 173 | 174 | ## HTTP requests & headers 175 | 176 | - The orchestrator centralizes httpx usage and exception handling. Validators should avoid making raw httpx requests themselves unless there's a specific reason. 177 | - When providing headers, include a User-Agent and reasonable Accept headers. 178 | - Timeouts should be reasonable (3–10 seconds). The orchestrator will usually expose a timeout parameter. 179 | 180 | Example common headers: 181 | ```py 182 | headers = { 183 | 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36", 184 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9", 185 | 'Accept-Encoding': "gzip, deflate, br, zstd", 186 | 'Upgrade-Insecure-Requests': "1", 187 | } 188 | 189 | ``` 190 | 191 | --- 192 | 193 | ## Return values and error handling 194 | 195 | - Always return a Result object: 196 | - Result.available() 197 | - Result.taken() 198 | - Result.error("short diagnostic message") 199 | - The orchestrator will capture network errors (httpx.ConnectError, httpx.TimeoutException, etc.) and should return Result.error(...) for those cases. If you implement direct requests inside a validator, follow the same pattern: 200 | 201 | ```python 202 | except (httpx.ConnectError, httpx.TimeoutException): 203 | return Result.error("network error") 204 | except Exception: 205 | return Result.error("unexpected error") 206 | ``` 207 | 208 | - Prefer returning meaningful error messages with Result.error to help debugging and triage in issues. 209 | 210 | --- 211 | 212 | ## Style & linting 213 | 214 | - Follow PEP8. 215 | - Use type hints for validator signatures. 216 | - Keep code readable and small. 217 | - Add docstrings to explain non-obvious heuristics. 218 | - Run linters and formatters before opening a PR (pre-commit is recommended). 219 | 220 | --- 221 | 222 | ## Pull request checklist 223 | 224 | Before opening a PR: 225 | - [x] Add the new validator file in the appropriate folder. 226 | - [x] Prefer using `generic_validate` or `status_validate` where applicable. 227 | - [x] Ensure imports are valid and package can be imported. 228 | 229 | When opening the PR: 230 | - Describe the approach, any heuristics used, and potential edge cases. 231 | - If the platform has rate limits or anti-bot measures, note them and recommend a testing approach. 232 | - If you return Result.error with a message, include why and any reproducible steps if available. 233 | 234 | --- 235 | 236 | ## When to implement custom logic 237 | 238 | - Use `status_validate` when availability is determined by HTTP status codes (e.g., 404 vs 200). 239 | - Use `generic_validate` when you need to inspect response content or headers and decide availability via a short callback that returns a Result. 240 | - If a platform requires API keys, OAuth, or heavy JS rendering, document it in the PR and consider an "advanced" module that can be enabled separately. 241 | 242 | --- 243 | 244 | Thank you for contributing! 245 | --------------------------------------------------------------------------------