├── 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 | 
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 |
78 |
79 |
80 | ---
81 |
82 |
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 |
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 |
--------------------------------------------------------------------------------