user-scanner 1.0.6.0__tar.gz → 1.0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/PKG-INFO +27 -6
  2. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/README.md +26 -5
  3. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/pyproject.toml +2 -2
  4. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/__main__.py +63 -44
  5. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/core/orchestrator.py +68 -36
  6. user_scanner-1.0.8.0/user_scanner/core/result.py +79 -0
  7. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/hashnode.py +8 -13
  8. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/itch_io.py +0 -5
  9. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/medium.py +7 -12
  10. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/patreon.py +0 -6
  11. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/producthunt.py +2 -19
  12. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/github.py +19 -2
  13. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/gitlab.py +4 -3
  14. user_scanner-1.0.8.0/user_scanner/dev/huggingface.py +19 -0
  15. user_scanner-1.0.8.0/user_scanner/dev/npmjs.py +27 -0
  16. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/donation/buymeacoffee.py +0 -3
  17. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/chess_com.py +4 -9
  18. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/minecraft.py +0 -5
  19. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/monkeytype.py +4 -10
  20. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/osu.py +0 -5
  21. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/roblox.py +14 -9
  22. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/steam.py +6 -9
  23. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/bluesky.py +7 -13
  24. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/discord.py +6 -9
  25. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/mastodon.py +0 -6
  26. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/pinterest.py +4 -4
  27. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/reddit.py +4 -3
  28. user_scanner-1.0.8.0/user_scanner/social/soundcloud.py +43 -0
  29. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/telegram.py +6 -6
  30. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/x.py +10 -20
  31. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/youtube.py +2 -18
  32. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/version.json +1 -1
  33. user_scanner-1.0.6.0/user_scanner/dev/npmjs.py +0 -43
  34. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/LICENSE +0 -0
  35. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/__init__.py +0 -0
  36. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/cli/__init__.py +0 -0
  37. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/cli/banner.py +0 -0
  38. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/community/__init__.py +0 -0
  39. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/community/coderlegion.py +0 -0
  40. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/core/__init__.py +0 -0
  41. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/__init__.py +0 -0
  42. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/devto.py +0 -0
  43. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/creator/kaggle.py +0 -0
  44. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/__init__.py +0 -0
  45. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/codeberg.py +0 -0
  46. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/cratesio.py +0 -0
  47. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/dockerhub.py +0 -0
  48. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/launchpad.py +0 -0
  49. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/dev/replit.py +0 -0
  50. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/donation/__init__.py +0 -0
  51. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/donation/liberapay.py +0 -0
  52. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/gaming/__init__.py +0 -0
  53. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/__init__.py +0 -0
  54. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/instagram.py +0 -0
  55. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/snapchat.py +0 -0
  56. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/social/threads.py +0 -0
  57. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/utils/update.py +0 -0
  58. {user_scanner-1.0.6.0 → user_scanner-1.0.8.0}/user_scanner/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: user-scanner
3
- Version: 1.0.6.0
3
+ Version: 1.0.8.0
4
4
  Summary: Check username availability across multiple popular platforms
5
5
  Keywords: username,checker,availability,social,tech,python,user-scanner
6
6
  Author-email: Kaif <kafcodec@gmail.com>
@@ -15,7 +15,7 @@ Project-URL: Homepage, https://github.com/kaifcodec/user-scanner
15
15
 
16
16
  ![1000136215](https://github.com/user-attachments/assets/49ec8d24-665b-4115-8525-01a8d0ca2ef4)
17
17
  <p align="center">
18
- <img src="https://img.shields.io/badge/Version-1.0.6.0-blueviolet?style=for-the-badge&logo=github" />
18
+ <img src="https://img.shields.io/badge/Version-1.0.8.0-blueviolet?style=for-the-badge&logo=github" />
19
19
  <img src="https://img.shields.io/github/issues/kaifcodec/user-scanner?style=for-the-badge&logo=github" />
20
20
  <img src="https://img.shields.io/badge/Tested%20on-Termux-black?style=for-the-badge&logo=termux" />
21
21
  <img src="https://img.shields.io/badge/Tested%20on-Windows-cyan?style=for-the-badge&logo=Windows" />
@@ -34,6 +34,7 @@ Perfect for finding a **unique username** across GitHub, Twitter, Reddit, Instag
34
34
  - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**.
35
35
  - ✅ Clear **Available / Taken / Error** output for each platform.
36
36
  - ✅ Fully modular: add new platform modules easily.
37
+ - ✅ Wildcard-based username permutations for automatic variation generation
37
38
  - ✅ Command-line interface ready: works directly after `pip install`.
38
39
  - ✅ Can be used as username OSINT tool.
39
40
  - ✅ Very low and lightweight dependencies, can be run on any machine.
@@ -60,14 +61,37 @@ Optionally, scan a specific category or single module:
60
61
  user-scanner -u <username> -c dev
61
62
  user-scanner -l # Lists all available modules
62
63
  user-scanner -u <username> -m github
64
+ user-scanner -u <username> -p <suffix>
63
65
 
64
66
  ```
67
+
68
+ Generate multiple username variations by appending a suffix:
69
+
70
+ ```bash
71
+ user-scanner -u <username> -p <suffix>
72
+
73
+ ```
74
+ Optionally, scan a specific category or single module with limit:
75
+
76
+ ```bash
77
+ user-scanner -u <username> -p <suffix> -c dev
78
+ user-scanner -u <username> -p <suffix> -m github
79
+ user-scanner -u <username> -p <suffix> -s <number> # limit generation of usernames
80
+ user-scanner -u <username> -p <suffix> -d <seconds> #delay to avoid rate-limits
81
+ ```
82
+
65
83
  ---
66
84
  ### Screenshot:
67
85
 
68
86
  - Note*: New modules are constantly getting added so this might have only limited, outdated output:
69
87
 
70
- <img width="1008" height="568" alt="1000139959" src="https://github.com/user-attachments/assets/467a4aa0-238d-4110-b9a6-d4b96c244432" />
88
+
89
+ <img width="1080" height="770" alt="1000140392" src="https://github.com/user-attachments/assets/4638c8f6-40c6-46f8-ae17-ac65cd199d81" />
90
+
91
+
92
+ ---
93
+
94
+ <img width="1080" height="352" alt="1000140393" src="https://github.com/user-attachments/assets/578b248c-2a05-4917-aab3-6372a7c28045" />
71
95
 
72
96
 
73
97
  ### Contributing:
@@ -94,9 +118,6 @@ user_scanner/
94
118
 
95
119
  See [CONTRIBUTING.md](CONTRIBUTING.md) for examples.
96
120
 
97
- ### 📧 Contact:
98
- - [Email](kaifcodec@gmail.com)
99
-
100
121
  ---
101
122
 
102
123
  ### Dependencies:
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![1000136215](https://github.com/user-attachments/assets/49ec8d24-665b-4115-8525-01a8d0ca2ef4)
4
4
  <p align="center">
5
- <img src="https://img.shields.io/badge/Version-1.0.6.0-blueviolet?style=for-the-badge&logo=github" />
5
+ <img src="https://img.shields.io/badge/Version-1.0.8.0-blueviolet?style=for-the-badge&logo=github" />
6
6
  <img src="https://img.shields.io/github/issues/kaifcodec/user-scanner?style=for-the-badge&logo=github" />
7
7
  <img src="https://img.shields.io/badge/Tested%20on-Termux-black?style=for-the-badge&logo=termux" />
8
8
  <img src="https://img.shields.io/badge/Tested%20on-Windows-cyan?style=for-the-badge&logo=Windows" />
@@ -21,6 +21,7 @@ Perfect for finding a **unique username** across GitHub, Twitter, Reddit, Instag
21
21
  - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**.
22
22
  - ✅ Clear **Available / Taken / Error** output for each platform.
23
23
  - ✅ Fully modular: add new platform modules easily.
24
+ - ✅ Wildcard-based username permutations for automatic variation generation
24
25
  - ✅ Command-line interface ready: works directly after `pip install`.
25
26
  - ✅ Can be used as username OSINT tool.
26
27
  - ✅ Very low and lightweight dependencies, can be run on any machine.
@@ -47,14 +48,37 @@ Optionally, scan a specific category or single module:
47
48
  user-scanner -u <username> -c dev
48
49
  user-scanner -l # Lists all available modules
49
50
  user-scanner -u <username> -m github
51
+ user-scanner -u <username> -p <suffix>
50
52
 
51
53
  ```
54
+
55
+ Generate multiple username variations by appending a suffix:
56
+
57
+ ```bash
58
+ user-scanner -u <username> -p <suffix>
59
+
60
+ ```
61
+ Optionally, scan a specific category or single module with limit:
62
+
63
+ ```bash
64
+ user-scanner -u <username> -p <suffix> -c dev
65
+ user-scanner -u <username> -p <suffix> -m github
66
+ user-scanner -u <username> -p <suffix> -s <number> # limit generation of usernames
67
+ user-scanner -u <username> -p <suffix> -d <seconds> #delay to avoid rate-limits
68
+ ```
69
+
52
70
  ---
53
71
  ### Screenshot:
54
72
 
55
73
  - Note*: New modules are constantly getting added so this might have only limited, outdated output:
56
74
 
57
- <img width="1008" height="568" alt="1000139959" src="https://github.com/user-attachments/assets/467a4aa0-238d-4110-b9a6-d4b96c244432" />
75
+
76
+ <img width="1080" height="770" alt="1000140392" src="https://github.com/user-attachments/assets/4638c8f6-40c6-46f8-ae17-ac65cd199d81" />
77
+
78
+
79
+ ---
80
+
81
+ <img width="1080" height="352" alt="1000140393" src="https://github.com/user-attachments/assets/578b248c-2a05-4917-aab3-6372a7c28045" />
58
82
 
59
83
 
60
84
  ### Contributing:
@@ -81,9 +105,6 @@ user_scanner/
81
105
 
82
106
  See [CONTRIBUTING.md](CONTRIBUTING.md) for examples.
83
107
 
84
- ### 📧 Contact:
85
- - [Email](kaifcodec@gmail.com)
86
-
87
108
  ---
88
109
 
89
110
  ### Dependencies:
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "user-scanner"
7
- version = "1.0.6.0"
7
+ version = "1.0.8.0"
8
8
  description = "Check username availability across multiple popular platforms"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -26,4 +26,4 @@ Homepage = "https://github.com/kaifcodec/user-scanner"
26
26
  user-scanner = "user_scanner.__main__:main"
27
27
 
28
28
  [tool.flit.sdist]
29
- exclude = ["tests/", "docs/", ".github/", ".git", "rm_pycache", "che.py", ".gitignore"]
29
+ exclude = ["tests/", "docs/", ".github/", ".git", "rm_pycache.py", "che.py", ".gitignore"]
@@ -1,38 +1,22 @@
1
1
  import argparse
2
+ import time
2
3
  import re
3
- from user_scanner.core.orchestrator import run_checks, load_modules
4
+ from user_scanner.core.orchestrator import run_checks, load_modules , generate_permutations, load_categories
4
5
  from colorama import Fore, Style
5
6
  from .cli import banner
6
7
  from .cli.banner import print_banner
7
8
 
8
- CATEGORY_MAPPING = {
9
- "dev": "dev",
10
- "social": "social",
11
- "creator": "creator",
12
- "community": "community",
13
- "gaming": "gaming",
14
- "donation": "donation"
15
- }
16
-
9
+ MAX_PERMUTATIONS_LIMIT = 100 # To prevent excessive generation
17
10
 
18
11
  def list_modules(category=None):
19
- from user_scanner import dev, social, creator, community, gaming, donation
20
- packages = {
21
- "dev": dev,
22
- "social": social,
23
- "creator": creator,
24
- "community": community,
25
- "gaming": gaming,
26
- "donation": donation
27
- }
28
-
29
- categories_to_list = [category] if category else packages.keys()
12
+ categories = load_categories()
13
+ categories_to_list = [category] if category else categories.keys()
30
14
 
31
15
  for cat_name in categories_to_list:
32
- package = packages[cat_name]
33
- modules = load_modules(package)
16
+ path = categories[cat_name]
17
+ modules = load_modules(path)
34
18
  print(Fore.MAGENTA +
35
- f"\n== {cat_name.upper()} SITES =={Style.RESET_ALL}")
19
+ f"\n== {cat_name.upper()} SITES =={Style.RESET_ALL}")
36
20
  for module in modules:
37
21
  site_name = module.__name__.split(".")[-1]
38
22
  print(f" - {site_name}")
@@ -47,7 +31,7 @@ def main():
47
31
  "-u", "--username", help="Username to scan across platforms"
48
32
  )
49
33
  parser.add_argument(
50
- "-c", "--category", choices=CATEGORY_MAPPING.keys(),
34
+ "-c", "--category", choices=load_categories().keys(),
51
35
  help="Scan all platforms in a category"
52
36
  )
53
37
  parser.add_argument(
@@ -59,16 +43,28 @@ def main():
59
43
  parser.add_argument(
60
44
  "-v", "--verbose", action="store_true", help="Enable verbose output"
61
45
  )
62
-
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
+
63
58
  args = parser.parse_args()
64
-
65
- if args.module and "." in args.module:
66
- args.module = args.module.replace(".", "_")
67
-
59
+
68
60
  if args.list:
69
61
  list_modules(args.category)
70
62
  return
71
-
63
+
64
+ if not args.username:
65
+ parser.print_help()
66
+ return
67
+
72
68
  # Special username checks before run
73
69
  if (args.module == "x" or args.category == "social"):
74
70
  if re.search(r"[^a-zA-Z0-9._-]", args.username):
@@ -78,37 +74,60 @@ def main():
78
74
  if re.search(r"[^a-zA-Z0-9\.-]", args.username):
79
75
  print(
80
76
  Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. Bluesky will throw error. (Supported: only hyphens and digits)" + Style.RESET_ALL + "\n")
81
- if not args.username:
82
- parser.print_help()
83
- return
84
- else:
85
- print_banner()
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(".", "_")
86
97
 
87
- from user_scanner import dev, social, creator, community, gaming, donation
88
98
 
89
99
  if args.module:
90
100
  # Single module search across all categories
91
- packages = [dev, social, creator, community, gaming, donation]
92
101
  found = False
93
- for package in packages:
94
- modules = load_modules(package)
102
+ for cat_path in load_categories().values():
103
+ modules = load_modules(cat_path)
95
104
  for module in modules:
96
105
  site_name = module.__name__.split(".")[-1]
97
106
  if site_name.lower() == args.module.lower():
98
107
  from user_scanner.core.orchestrator import run_module_single
99
- run_module_single(module, args.username)
108
+ for name in usernames: # <-- permutation support here
109
+ run_module_single(module, name)
110
+ if args.delay > 0:
111
+ time.sleep(args.delay)
100
112
  found = True
101
113
  if not found:
102
114
  print(
103
115
  Fore.RED + f"[!] Module '{args.module}' not found in any category." + Style.RESET_ALL)
104
116
  elif args.category:
105
117
  # Category-wise scan
106
- category_package = eval(CATEGORY_MAPPING[args.category])
118
+ category_package = load_categories().get(args.category)
107
119
  from user_scanner.core.orchestrator import run_checks_category
108
- run_checks_category(category_package, args.username, args.verbose)
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)
109
125
  else:
110
126
  # Full scan
111
- run_checks(args.username)
127
+ for name in usernames:
128
+ run_checks(name)
129
+ if args.delay > 0:
130
+ time.sleep(args.delay)
112
131
 
113
132
 
114
133
  if __name__ == "__main__":
@@ -1,28 +1,43 @@
1
1
  import importlib
2
- import pkgutil
3
2
  from colorama import Fore, Style
4
3
  import threading
5
-
4
+ from itertools import permutations
6
5
  import httpx
7
- from httpx import ConnectError, TimeoutException
6
+ from pathlib import Path
7
+ from user_scanner.core.result import Result, AnyResult
8
+ from typing import Callable, Dict, List
8
9
 
9
10
  lock = threading.Condition()
10
11
  # Basically which thread is the one to print
11
12
  print_queue = 0
12
13
 
13
14
 
14
- def load_modules(package):
15
-
15
+ def load_modules(category_path: Path):
16
16
  modules = []
17
- for _, name, _ in pkgutil.iter_modules(package.__path__, package.__name__ + "."):
18
- try:
19
- module = importlib.import_module(name)
20
- modules.append(module)
21
- except Exception as e:
22
- print(f"Failed to import {name}: {e}")
17
+ for file in category_path.glob("*.py"):
18
+ if file.name == "__init__.py":
19
+ continue
20
+ spec = importlib.util.spec_from_file_location(file.stem, str(file))
21
+ module = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(module)
23
+
24
+ modules.append(module)
23
25
  return modules
24
26
 
25
27
 
28
+ def load_categories() -> Dict[str, Path]:
29
+ root = Path(__file__).resolve().parent.parent # Should be user_scanner
30
+ categories = {}
31
+
32
+ for subfolder in root.iterdir():
33
+ if subfolder.is_dir() and \
34
+ not subfolder.name.lower() in ["cli", "utils", "core"] and \
35
+ not "__" in subfolder.name: # Removes __pycache__
36
+ categories[subfolder.name] = subfolder.resolve()
37
+
38
+ return categories
39
+
40
+
26
41
  def worker_single(module, username, i):
27
42
  global print_queue
28
43
 
@@ -36,12 +51,17 @@ def worker_single(module, username, i):
36
51
  if func:
37
52
  try:
38
53
  result = func(username)
54
+ reason = ""
55
+
56
+ if isinstance(result, Result) and result.has_reason():
57
+ reason = f" ({result.get_reason()})"
58
+
39
59
  if result == 1:
40
- output = f" {Fore.GREEN}[✔] {site_name}: Available{Style.RESET_ALL}"
60
+ output = f" {Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}"
41
61
  elif result == 0:
42
- output = f" {Fore.RED}[✘] {site_name}: Taken{Style.RESET_ALL}"
62
+ output = f" {Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}"
43
63
  else:
44
- output = f" {Fore.YELLOW}[!] {site_name}: Error{Style.RESET_ALL}"
64
+ output = f" {Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}"
45
65
  except Exception as e:
46
66
  output = f" {Fore.YELLOW}[!] {site_name}: Exception - {e}{Style.RESET_ALL}"
47
67
  else:
@@ -62,11 +82,11 @@ def run_module_single(module, username):
62
82
  worker_single(module, username, print_queue)
63
83
 
64
84
 
65
- def run_checks_category(package, username, verbose=False):
85
+ def run_checks_category(category_path:Path, username:str, verbose=False):
66
86
  global print_queue
67
87
 
68
- modules = load_modules(package)
69
- category_name = package.__name__.split('.')[-1].capitalize()
88
+ modules = load_modules(category_path)
89
+ category_name = category_path.stem.capitalize()
70
90
  print(f"{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}")
71
91
 
72
92
  print_queue = 0
@@ -82,18 +102,14 @@ def run_checks_category(package, username, verbose=False):
82
102
 
83
103
 
84
104
  def run_checks(username):
85
- from user_scanner import dev, social, creator, community, gaming, donation
86
-
87
- packages = [dev, social, creator, community, gaming, donation]
88
-
89
105
  print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}\n")
90
106
 
91
- for package in packages:
92
- run_checks_category(package, username)
107
+ for category_path in load_categories().values():
108
+ run_checks_category(category_path, username)
93
109
  print()
94
110
 
95
111
 
96
- def make_get_request(url, **kwargs):
112
+ def make_get_request(url: str, **kwargs) -> httpx.Response:
97
113
  """Simple wrapper to **httpx.get** that predefines headers and timeout"""
98
114
  if not "headers" in kwargs:
99
115
  kwargs["headers"] = {
@@ -110,39 +126,55 @@ def make_get_request(url, **kwargs):
110
126
  return httpx.get(url, **kwargs)
111
127
 
112
128
 
113
- def generic_validate(url, func, **kwargs):
129
+ def generic_validate(url: str, func: Callable[[httpx.Response], AnyResult], **kwargs) -> AnyResult:
114
130
  """
115
131
  A generic validate function that makes a request and executes the provided function on the response.
116
132
  """
117
133
  try:
118
134
  response = make_get_request(url, **kwargs)
119
135
  return func(response)
120
- except (ConnectError, TimeoutException):
121
- return 2
122
- except Exception:
123
- return 2
136
+ except Exception as e:
137
+ return Result.error(e)
124
138
 
125
139
 
126
- def status_validate(url, available, taken, **kwargs):
140
+ def status_validate(url: str, available: int | List[int], taken: int | List[int], **kwargs) -> Result:
127
141
  """
128
142
  Function that takes a **url** and **kwargs** for the request and
129
143
  checks if the request status matches the availabe or taken.
130
144
  **Available** and **Taken** must either be whole numbers or lists of whole numbers.
131
145
  """
132
- def inner(response):
146
+ def inner(response: httpx.Response):
133
147
  # Checks if a number is equal or is contained inside
134
148
  def contains(a, b): return (isinstance(a, list) and b in a) or (a == b)
135
-
136
149
  status = response.status_code
137
150
  available_value = contains(available, status)
138
151
  taken_value = contains(taken, status)
139
152
 
140
153
  if available_value and taken_value:
141
- return 2 # Can't be both available and taken
154
+ # Can't be both available and taken
155
+ return Result.error("Invalid status match. Report this on Github.")
142
156
  elif available_value:
143
- return 1
157
+ return Result.available()
144
158
  elif taken_value:
145
- return 0
146
- return 2
159
+ return Result.taken()
160
+ return Result.error("Status didn't match. Report this on Github.")
147
161
 
148
162
  return generic_validate(url, inner, **kwargs)
163
+
164
+ def generate_permutations(username, pattern, limit=None):
165
+ """
166
+ Generate all order-based permutations of characters in `pattern`
167
+ appended after `username`.
168
+ """
169
+ permutations_set = {username}
170
+
171
+ chars = list(pattern)
172
+
173
+ # generate permutations of length 1 → len(chars)
174
+ for r in range(1, len(chars) + 1):
175
+ for combo in permutations(chars, r):
176
+ permutations_set.add(username + ''.join(combo))
177
+ if limit and len(permutations_set) >= limit:
178
+ return list(permutations_set)[:limit]
179
+
180
+ return sorted(permutations_set)
@@ -0,0 +1,79 @@
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
@@ -1,6 +1,5 @@
1
1
  import httpx
2
- import json
3
- from httpx import ConnectError, TimeoutException
2
+ from user_scanner.core.result import Result
4
3
 
5
4
 
6
5
  def validate_hashnode(user):
@@ -27,21 +26,17 @@ def validate_hashnode(user):
27
26
 
28
27
  if 'status' in data:
29
28
  if data['status'] == 1:
30
- return 1
29
+ return Result.available()
31
30
  elif data['status'] == 0:
32
- return 0
31
+ return Result.taken()
33
32
 
34
- return 2
33
+ return Result.error("Status not found")
35
34
 
36
35
  else:
37
- return 2
38
-
39
- except (ConnectError, TimeoutException):
40
- return 2
41
- except json.JSONDecodeError:
42
- return 2
43
- except Exception:
44
- return 2
36
+ return Result.error("Invalid status code")
37
+
38
+ except Exception as e:
39
+ return Result.error(e)
45
40
 
46
41
 
47
42
  if __name__ == "__main__":
@@ -2,11 +2,6 @@ from user_scanner.core.orchestrator import status_validate
2
2
 
3
3
 
4
4
  def validate_itch_io(user):
5
- """
6
- Checks if a itch.io username is available.
7
- Returns: 1 -> available, 0 -> taken, 2 -> error
8
- """
9
-
10
5
  url = f"https://{user}.itch.io"
11
6
 
12
7
  return status_validate(url, 404, 200, follow_redirects=True)
@@ -1,5 +1,5 @@
1
- import httpx
2
- from httpx import ConnectError, TimeoutException
1
+ from user_scanner.core.orchestrator import generic_validate
2
+ from user_scanner.core.result import Result
3
3
 
4
4
 
5
5
  def validate_medium(user):
@@ -10,24 +10,19 @@ def validate_medium(user):
10
10
  'Accept': "text/html",
11
11
  }
12
12
 
13
- try:
14
- response = httpx.get(url, headers=headers, timeout=3.0)
15
-
13
+ def process(response):
16
14
  if response.status_code == 200:
17
15
  html_text = response.text
18
16
 
19
17
  username_tag = f'property="profile:username" content="{user}"'
20
18
 
21
19
  if username_tag in html_text:
22
- return 0
20
+ return Result.taken()
23
21
  else:
24
- return 1
25
- return 2
22
+ return Result.available()
23
+ return Result.error()
26
24
 
27
- except (ConnectError, TimeoutException):
28
- return 2
29
- except Exception:
30
- return 2
25
+ return generic_validate(url, process, headers=headers)
31
26
 
32
27
 
33
28
  if __name__ == "__main__":
@@ -8,12 +8,6 @@ def validate_patreon(user):
8
8
 
9
9
 
10
10
  if __name__ == "__main__":
11
- try:
12
- import httpx
13
- except ImportError:
14
- print("Error: 'httpx' library is not installed.")
15
- exit()
16
-
17
11
  user = input("Username?: ").strip()
18
12
  result = validate_patreon(user)
19
13