user-scanner 1.0.8.1__tar.gz → 1.0.9.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 (60) hide show
  1. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/PKG-INFO +22 -10
  2. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/README.md +21 -9
  3. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/pyproject.toml +1 -1
  4. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/__main__.py +86 -61
  5. user_scanner-1.0.9.0/user_scanner/cli/printer.py +117 -0
  6. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/core/orchestrator.py +86 -55
  7. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/core/result.py +57 -8
  8. user_scanner-1.0.9.0/user_scanner/core/utils.py +9 -0
  9. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/version.json +1 -1
  10. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/LICENSE +0 -0
  11. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/__init__.py +0 -0
  12. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/cli/__init__.py +0 -0
  13. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/cli/banner.py +0 -0
  14. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/community/__init__.py +0 -0
  15. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/community/coderlegion.py +0 -0
  16. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/community/stackoverflow.py +0 -0
  17. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/core/__init__.py +0 -0
  18. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/__init__.py +0 -0
  19. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/devto.py +0 -0
  20. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/hashnode.py +0 -0
  21. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/itch_io.py +0 -0
  22. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/kaggle.py +0 -0
  23. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/medium.py +0 -0
  24. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/patreon.py +0 -0
  25. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/creator/producthunt.py +0 -0
  26. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/__init__.py +0 -0
  27. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/codeberg.py +0 -0
  28. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/cratesio.py +0 -0
  29. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/dockerhub.py +0 -0
  30. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/github.py +0 -0
  31. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/gitlab.py +0 -0
  32. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/huggingface.py +0 -0
  33. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/launchpad.py +0 -0
  34. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/npmjs.py +0 -0
  35. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/dev/replit.py +0 -0
  36. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/donation/__init__.py +0 -0
  37. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/donation/buymeacoffee.py +0 -0
  38. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/donation/liberapay.py +0 -0
  39. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/__init__.py +0 -0
  40. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/chess_com.py +0 -0
  41. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/minecraft.py +0 -0
  42. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/monkeytype.py +0 -0
  43. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/osu.py +0 -0
  44. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/roblox.py +0 -0
  45. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/gaming/steam.py +0 -0
  46. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/__init__.py +0 -0
  47. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/bluesky.py +0 -0
  48. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/discord.py +0 -0
  49. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/instagram.py +0 -0
  50. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/mastodon.py +0 -0
  51. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/pinterest.py +0 -0
  52. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/reddit.py +0 -0
  53. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/snapchat.py +0 -0
  54. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/soundcloud.py +0 -0
  55. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/telegram.py +0 -0
  56. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/threads.py +0 -0
  57. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/x.py +0 -0
  58. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/social/youtube.py +0 -0
  59. {user_scanner-1.0.8.1 → user_scanner-1.0.9.0}/user_scanner/utils/update.py +0 -0
  60. {user_scanner-1.0.8.1 → user_scanner-1.0.9.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.8.1
3
+ Version: 1.0.9.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.8.1-blueviolet?style=for-the-badge&logo=github" />
18
+ <img src="https://img.shields.io/badge/Version-1.0.9.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" />
@@ -31,14 +31,16 @@ Perfect for finding a **unique username** across GitHub, Twitter, Reddit, Instag
31
31
 
32
32
  ### Features
33
33
 
34
- - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**.
35
- - ✅ Clear **Available / Taken / Error** output for each platform.
34
+ - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**
35
+ - ✅ Clear **Available / Taken / Error** output for each platform
36
36
  - ✅ Robust error handling: It prints the exact reason (e.g. Cannot use underscores, hyphens at the start/end)
37
- - ✅ Fully modular: add new platform modules easily.
37
+ - ✅ Fully modular: add new platform modules easily
38
38
  - ✅ Wildcard-based username permutations for automatic variation generation using provided suffix
39
+ - ✅ Selection of results format (e.g. json, csv, console (default))
40
+ - ✅ Get the scanning results in preferred format (json/csv) in specified output file (suitable for power users)
39
41
  - ✅ Command-line interface ready: works directly after `pip install`
40
- - ✅ Can be used as username OSINT tool.
41
- - ✅ Very low and lightweight dependencies, can be run on any machine.
42
+ - ✅ Can be used as username OSINT tool
43
+ - ✅ Very low and lightweight dependencies, can be run on any machine
42
44
  ---
43
45
 
44
46
  ### Installation
@@ -62,15 +64,21 @@ Optionally, scan a specific category or single module:
62
64
  user-scanner -u <username> -c dev
63
65
  user-scanner -l # Lists all available modules
64
66
  user-scanner -u <username> -m github
65
- user-scanner -u <username> -p <suffix>
67
+ ```
66
68
 
69
+ Also, the output file and format can be specified: <br>
70
+ \* Errors and warnings will only appear when the format is set to "console"
71
+ ```bash
72
+ user-scanner -u <username> -f console #Default format
73
+ user-scanner -u <username> -f csv
74
+ user-scanner -u <username> -f json
75
+ user-scanner -u <username> -f <format> -o <output-file>
67
76
  ```
68
77
 
69
78
  Generate multiple username variations by appending a suffix:
70
79
 
71
80
  ```bash
72
81
  user-scanner -u <username> -p <suffix>
73
-
74
82
  ```
75
83
  Optionally, scan a specific category or single module with limit:
76
84
 
@@ -78,7 +86,7 @@ Optionally, scan a specific category or single module with limit:
78
86
  user-scanner -u <username> -p <suffix> -c dev
79
87
  user-scanner -u <username> -p <suffix> -m github
80
88
  user-scanner -u <username> -p <suffix> -s <number> # limit generation of usernames
81
- user-scanner -u <username> -p <suffix> -d <seconds> #delay to avoid rate-limits
89
+ user-scanner -u <username> -p <suffix> -d <seconds> # delay to avoid rate-limits (can be 0s-1s)
82
90
  ```
83
91
 
84
92
  ---
@@ -94,6 +102,10 @@ user-scanner -u <username> -p <suffix> -d <seconds> #delay to avoid rate-limits
94
102
 
95
103
  <img width="1080" height="352" alt="1000140393" src="https://github.com/user-attachments/assets/578b248c-2a05-4917-aab3-6372a7c28045" />
96
104
 
105
+ ---
106
+
107
+ <img width="992" height="556" alt="1000141265" src="https://github.com/user-attachments/assets/9babb19f-bc87-4e7b-abe5-c52b8b1b672c" />
108
+
97
109
 
98
110
  ### Contributing:
99
111
 
@@ -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.8.1-blueviolet?style=for-the-badge&logo=github" />
5
+ <img src="https://img.shields.io/badge/Version-1.0.9.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" />
@@ -18,14 +18,16 @@ Perfect for finding a **unique username** across GitHub, Twitter, Reddit, Instag
18
18
 
19
19
  ### Features
20
20
 
21
- - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**.
22
- - ✅ Clear **Available / Taken / Error** output for each platform.
21
+ - ✅ Check usernames across **social networks**, **developer platforms**, and **creator communities**
22
+ - ✅ Clear **Available / Taken / Error** output for each platform
23
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.
24
+ - ✅ Fully modular: add new platform modules easily
25
25
  - ✅ Wildcard-based username permutations for automatic variation generation using provided suffix
26
+ - ✅ Selection of results format (e.g. json, csv, console (default))
27
+ - ✅ Get the scanning results in preferred format (json/csv) in specified output file (suitable for power users)
26
28
  - ✅ 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
+ - ✅ Can be used as username OSINT tool
30
+ - ✅ Very low and lightweight dependencies, can be run on any machine
29
31
  ---
30
32
 
31
33
  ### Installation
@@ -49,15 +51,21 @@ Optionally, scan a specific category or single module:
49
51
  user-scanner -u <username> -c dev
50
52
  user-scanner -l # Lists all available modules
51
53
  user-scanner -u <username> -m github
52
- user-scanner -u <username> -p <suffix>
54
+ ```
53
55
 
56
+ Also, the output file and format can be specified: <br>
57
+ \* Errors and warnings will only appear when the format is set to "console"
58
+ ```bash
59
+ user-scanner -u <username> -f console #Default format
60
+ user-scanner -u <username> -f csv
61
+ user-scanner -u <username> -f json
62
+ user-scanner -u <username> -f <format> -o <output-file>
54
63
  ```
55
64
 
56
65
  Generate multiple username variations by appending a suffix:
57
66
 
58
67
  ```bash
59
68
  user-scanner -u <username> -p <suffix>
60
-
61
69
  ```
62
70
  Optionally, scan a specific category or single module with limit:
63
71
 
@@ -65,7 +73,7 @@ Optionally, scan a specific category or single module with limit:
65
73
  user-scanner -u <username> -p <suffix> -c dev
66
74
  user-scanner -u <username> -p <suffix> -m github
67
75
  user-scanner -u <username> -p <suffix> -s <number> # limit generation of usernames
68
- user-scanner -u <username> -p <suffix> -d <seconds> #delay to avoid rate-limits
76
+ user-scanner -u <username> -p <suffix> -d <seconds> # delay to avoid rate-limits (can be 0s-1s)
69
77
  ```
70
78
 
71
79
  ---
@@ -81,6 +89,10 @@ user-scanner -u <username> -p <suffix> -d <seconds> #delay to avoid rate-limits
81
89
 
82
90
  <img width="1080" height="352" alt="1000140393" src="https://github.com/user-attachments/assets/578b248c-2a05-4917-aab3-6372a7c28045" />
83
91
 
92
+ ---
93
+
94
+ <img width="992" height="556" alt="1000141265" src="https://github.com/user-attachments/assets/9babb19f-bc87-4e7b-abe5-c52b8b1b672c" />
95
+
84
96
 
85
97
  ### Contributing:
86
98
 
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "user-scanner"
7
- version = "1.0.8.1"
7
+ version = "1.0.9.0"
8
8
  description = "Check username availability across multiple popular platforms"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -1,27 +1,16 @@
1
1
  import argparse
2
2
  import time
3
3
  import re
4
- from user_scanner.core.orchestrator import run_checks, load_modules , generate_permutations, load_categories
4
+ from user_scanner.cli import printer
5
+ from user_scanner.core.orchestrator import generate_permutations, load_categories
5
6
  from colorama import Fore, Style
6
- from .cli import banner
7
- from .cli.banner import print_banner
7
+ from user_scanner.cli.banner import print_banner
8
+ from typing import List
9
+ from user_scanner.core.result import Result
10
+ from user_scanner.core.utils import is_last_value
8
11
 
9
12
  MAX_PERMUTATIONS_LIMIT = 100 # To prevent excessive generation
10
13
 
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
14
  def main():
26
15
  parser = argparse.ArgumentParser(
27
16
  prog="user-scanner",
@@ -43,91 +32,127 @@ def main():
43
32
  parser.add_argument(
44
33
  "-v", "--verbose", action="store_true", help="Enable verbose output"
45
34
  )
46
-
35
+
47
36
  parser.add_argument(
48
37
  "-p", "--permute",type=str,help="Generate username permutations using a string pattern (e.g -p 234)"
49
38
  )
50
39
  parser.add_argument(
51
40
  "-s", "--stop",type=int,default=MAX_PERMUTATIONS_LIMIT,help="Limit the number of username permutations generated"
52
41
  )
53
-
42
+
54
43
  parser.add_argument(
55
44
  "-d", "--delay",type=float,default=0,help="Delay in seconds between requests (recommended: 1-2 seconds)"
56
45
  )
57
-
46
+
47
+ parser.add_argument(
48
+ "-f", "--format", choices=["console", "csv", "json"], default="console", help="Specify the output format (default: console)"
49
+ )
50
+
51
+ parser.add_argument(
52
+ "-o", "--output", type=str, help="Specify the output file"
53
+ )
54
+
58
55
  args = parser.parse_args()
59
-
56
+
57
+ Printer = printer.Printer(args.format)
58
+
60
59
  if args.list:
61
- list_modules(args.category)
60
+ Printer.print_modules(args.category)
62
61
  return
63
-
62
+
64
63
  if not args.username:
65
64
  parser.print_help()
66
65
  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
66
 
79
- if args.permute and args.delay == 0:
67
+
68
+ if Printer.is_console:
69
+ print_banner()
70
+
71
+ if args.permute and args.delay == 0 and Printer.is_console:
80
72
  print(
81
73
  Fore.YELLOW
82
74
  + "[!] Warning: You're generating multiple usernames with NO delay between requests. "
83
75
  "This may trigger rate limits or IP bans. Use --delay 1 or higher. (Use only if the sites throw errors otherwise ignore)\n"
84
76
  + Style.RESET_ALL)
85
-
77
+
86
78
  usernames = [args.username] # Default single username list
87
-
79
+
88
80
  #Added permutation support , generate all possible permutation of given sequence.
89
81
  if args.permute:
90
82
  usernames = generate_permutations(args.username, args.permute , args.stop)
91
- print(Fore.CYAN + f"[+] Generated {len(usernames)} username permutations" + Style.RESET_ALL)
83
+ if Printer.is_console:
84
+ print(
85
+ Fore.CYAN + f"[+] Generated {len(usernames)} username permutations" + Style.RESET_ALL)
92
86
 
93
-
94
-
95
87
  if args.module and "." in args.module:
96
88
  args.module = args.module.replace(".", "_")
97
89
 
90
+ def run_all_usernames(func, arg = None) -> List[Result]:
91
+ """
92
+ Executes a function for all given usernames.
93
+ Made in order to simplify main()
94
+ """
95
+ results = []
96
+ print(Printer.get_start())
97
+ for i, name in enumerate(usernames):
98
+ is_last = i == len(usernames) - 1
99
+ if arg == None:
100
+ results.extend(func(name, Printer, is_last))
101
+ else:
102
+ results.extend(func(arg, name, Printer, is_last))
103
+ if args.delay > 0 and not is_last:
104
+ time.sleep(args.delay)
105
+ if Printer.is_json:
106
+ print(Printer.get_end())
107
+ return results
108
+
109
+ results = []
98
110
 
99
111
  if args.module:
100
112
  # Single module search across all categories
101
- found = False
102
- for cat_path in load_categories().values():
103
- modules = load_modules(cat_path)
113
+ from user_scanner.core.orchestrator import run_module_single, find_module
114
+ modules = find_module(args.module)
115
+
116
+ if len(modules) > 0:
104
117
  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:
118
+ results.extend(run_all_usernames(run_module_single, module))
119
+ else:
114
120
  print(
115
121
  Fore.RED + f"[!] Module '{args.module}' not found in any category." + Style.RESET_ALL)
122
+
116
123
  elif args.category:
117
124
  # Category-wise scan
118
125
  category_package = load_categories().get(args.category)
119
126
  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)
127
+ results = run_all_usernames(run_checks_category, category_package)
128
+
125
129
  else:
126
130
  # Full scan
127
- for name in usernames:
128
- run_checks(name)
129
- if args.delay > 0:
130
- time.sleep(args.delay)
131
+ from user_scanner.core.orchestrator import run_checks
132
+ results = run_all_usernames(run_checks)
133
+
134
+ if not args.output:
135
+ return
136
+
137
+ if args.output and Printer.is_console:
138
+ msg = (
139
+ "\n[!] The console format cannot be "
140
+ f"written to file: '{args.output}'."
141
+ )
142
+ print(Fore.RED + msg + Style.RESET_ALL)
143
+ return
144
+
145
+ content = Printer.get_start()
146
+
147
+ for i,result in enumerate(results):
148
+ char = "" if Printer.is_csv or is_last_value(results, i) else ","
149
+ content += "\n" + Printer.get_result_output(result) + char
150
+
151
+ if Printer.is_json:
152
+ content += "\n" + Printer.get_end()
153
+
154
+ with open(args.output, "a", encoding="utf-8") as f:
155
+ f.write(content)
131
156
 
132
157
 
133
158
  if __name__ == "__main__":
@@ -0,0 +1,117 @@
1
+ from colorama import Fore, Style
2
+ from typing import Literal
3
+ from user_scanner.core.result import Result, Status
4
+
5
+ INDENT = " "
6
+ CSV_HEADER = "username,category,site_name,status,url,reason"
7
+
8
+
9
+ def indentate(msg: str, indent: int):
10
+ if indent <= 0:
11
+ return msg
12
+ tabs = INDENT * indent
13
+ return "\n".join([f"{tabs}{line}" for line in msg.split("\n")])
14
+
15
+
16
+ class Printer:
17
+ def __init__(self, format: Literal["console", "csv", "json"]) -> None:
18
+ if not format in ["console", "csv", "json"]:
19
+ raise ValueError(f"Invalid output-format: {format}")
20
+ self.mode: str = format
21
+ self.indent: int = 0
22
+
23
+ @property
24
+ def is_console(self) -> bool:
25
+ return self.mode == "console"
26
+
27
+ @property
28
+ def is_csv(self) -> bool:
29
+ return self.mode == "csv"
30
+
31
+ @property
32
+ def is_json(self) -> bool:
33
+ return self.mode == "json"
34
+
35
+ def get_start(self, json_char: str = "[") -> str:
36
+ if self.is_json:
37
+ self.indent += 1
38
+ return indentate(json_char, self.indent - 1)
39
+ elif self.is_csv:
40
+ return CSV_HEADER
41
+ return ""
42
+
43
+ def get_end(self, json_char: str = "]") -> str:
44
+ if not self.is_json:
45
+ return
46
+ self.indent = max(self.indent - 1, 0)
47
+ return indentate(json_char, self.indent)
48
+
49
+ def get_result_output(self, result: Result) -> str:
50
+ #In principle result should always have this
51
+ site_name = result.site_name
52
+ username = result.username
53
+
54
+ match (result.status, self.mode):
55
+ case (Status.AVAILABLE, "console"):
56
+ return f"{INDENT}{Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}"
57
+
58
+ case (Status.TAKEN, "console"):
59
+ return f"{INDENT}{Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}"
60
+
61
+ case (Status.ERROR, "console"):
62
+ reason = ""
63
+ if isinstance(result, Result) and result.has_reason():
64
+ reason = f" ({result.get_reason()})"
65
+ return f"{INDENT}{Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}"
66
+
67
+ case (_, "json"):
68
+ return indentate(result.to_json().replace("\t", INDENT), self.indent)
69
+
70
+ case (_, "csv"):
71
+ return result.to_csv()
72
+
73
+ return ""
74
+
75
+ def print_modules(self, category: str | None = None):
76
+ from user_scanner.core.orchestrator import load_categories, load_modules
77
+ categories = load_categories()
78
+ categories_to_list = [category] if category else categories.keys()
79
+
80
+ # Print the start
81
+ if self.is_json:
82
+ print(self.get_start("{"))
83
+ elif self.is_csv:
84
+ print("category,site_name")
85
+
86
+ for i, cat_name in enumerate(categories_to_list):
87
+ path = categories[cat_name]
88
+ modules = load_modules(path)
89
+
90
+ # Print for each category
91
+ match self.mode:
92
+ case "console":
93
+ print(Fore.MAGENTA +
94
+ f"\n== {cat_name.upper()} SITES =={Style.RESET_ALL}")
95
+ case "json":
96
+ print(self.get_start(f"\"{cat_name}\": ["))
97
+
98
+ for j, module in enumerate(modules):
99
+ is_last = j == len(modules) - 1
100
+ site_name = module.__name__.split(".")[-1].capitalize()
101
+
102
+ # Print for each site name
103
+ match self.mode:
104
+ case "console":
105
+ print(f"{INDENT}- {site_name}")
106
+ case "json":
107
+ msg = f"\"{site_name}\"" + ("" if is_last else ",")
108
+ print(indentate(msg, self.indent))
109
+ case "csv":
110
+ print(f"{cat_name},{site_name}")
111
+
112
+ if self.is_json:
113
+ is_last = i == len(categories_to_list) - 1
114
+ print(self.get_end("]" if is_last else "],"))
115
+
116
+ if self.is_json:
117
+ print(self.get_end("}"))
@@ -1,16 +1,14 @@
1
1
  import importlib
2
2
  import importlib.util
3
3
  from colorama import Fore, Style
4
- import threading
4
+ from concurrent.futures import ThreadPoolExecutor
5
5
  from itertools import permutations
6
6
  import httpx
7
7
  from pathlib import Path
8
+ from user_scanner.cli.printer import Printer
8
9
  from user_scanner.core.result import Result, AnyResult
9
10
  from typing import Callable, Dict, List
10
-
11
- lock = threading.Condition()
12
- # Basically which thread is the one to print
13
- print_queue = 0
11
+ from user_scanner.core.utils import get_site_name, is_last_value
14
12
 
15
13
 
16
14
  def load_modules(category_path: Path):
@@ -39,75 +37,105 @@ def load_categories() -> Dict[str, Path]:
39
37
  return categories
40
38
 
41
39
 
42
- def worker_single(module, username, i):
43
- global print_queue
40
+ def find_module(name: str):
41
+ name = name.lower()
42
+
43
+ matches = [
44
+ module
45
+ for category_path in load_categories().values()
46
+ for module in load_modules(category_path)
47
+ if module.__name__.split(".")[-1].lower() == name
48
+ ]
49
+
50
+ return matches
51
+
52
+ def find_category(module) -> str | None:
53
+
54
+ module_file = getattr(module, '__file__', None)
55
+ if not module_file:
56
+ return None
57
+
58
+ category = Path(module_file).parent.name.lower()
59
+ categories = load_categories()
60
+ if category in categories:
61
+ return category.capitalize()
62
+
63
+ return None
64
+
65
+
44
66
 
67
+ def worker_single(module, username: str) -> Result:
45
68
  func = next((getattr(module, f) for f in dir(module)
46
69
  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
70
 
51
- output = ""
52
- if func:
53
- try:
54
- result = func(username)
55
- reason = ""
71
+ site_name = get_site_name(module)
56
72
 
57
- if isinstance(result, Result) and result.has_reason():
58
- reason = f" ({result.get_reason()})"
73
+ if not func:
74
+ return Result.error(f"{site_name} has no validate_ function", site_name=site_name, username=username)
59
75
 
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}"
76
+ try:
77
+ result: Result = func(username)
78
+ result.update(site_name=site_name, username=username)
79
+ return result
80
+ except Exception as e:
81
+ return Result.error(e, site_name=site_name, username=username)
70
82
 
71
- with lock:
72
- # Waits for in-order printing
73
- while i != print_queue:
74
- lock.wait()
75
83
 
76
- print(output)
77
- print_queue += 1
78
- lock.notify_all()
84
+ def run_module_single(module, username: str, printer: Printer, last: bool = True) -> List[Result]:
85
+ result = worker_single(module, username)
79
86
 
87
+ category = find_category(module)
88
+ if category:
89
+ result.update(category=category)
80
90
 
81
- def run_module_single(module, username):
82
- # Just executes as if it was a thread
83
- worker_single(module, username, print_queue)
91
+ site_name = get_site_name(module)
92
+ msg = printer.get_result_output(result)
93
+ if last == False and printer.is_json:
94
+ msg += ","
95
+ print(msg)
84
96
 
97
+ return [result]
85
98
 
86
- def run_checks_category(category_path:Path, username:str, verbose=False):
87
- global print_queue
88
99
 
100
+
101
+ def run_checks_category(category_path: Path, username: str, printer: Printer, last: bool = True) -> List[Result]:
89
102
  modules = load_modules(category_path)
103
+
90
104
  category_name = category_path.stem.capitalize()
91
- print(f"{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}")
105
+ if printer.is_console:
106
+ print(f"\n{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}")
92
107
 
93
- print_queue = 0
108
+ results = []
94
109
 
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()
110
+ with ThreadPoolExecutor(max_workers=10) as executor:
111
+ exec_map = executor.map(lambda m: worker_single(m, username), modules)
112
+ for i, result in enumerate(exec_map):
113
+ result.update(category = category_name)
114
+ results.append(result)
100
115
 
101
- for t in threads:
102
- t.join()
116
+ is_last = last and is_last_value(modules, i)
117
+ site_name = get_site_name(modules[i])
118
+ msg = printer.get_result_output(result)
119
+ if is_last == False and printer.is_json:
120
+ msg += ","
121
+ print(msg)
103
122
 
123
+ return results
104
124
 
105
- def run_checks(username):
106
- print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}\n")
107
125
 
108
- for category_path in load_categories().values():
109
- run_checks_category(category_path, username)
110
- print()
126
+ def run_checks(username: str, printer: Printer, last: bool = True) -> List[Result]:
127
+ if printer.is_console:
128
+ print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}")
129
+
130
+ results = []
131
+
132
+ categories = list(load_categories().values())
133
+ for i, category_path in enumerate(categories):
134
+ last_cat: int = last and (i == len(categories) - 1)
135
+ temp = run_checks_category(category_path, username, printer, last_cat)
136
+ results.extend(temp)
137
+
138
+ return results
111
139
 
112
140
 
113
141
  def make_get_request(url: str, **kwargs) -> httpx.Response:
@@ -133,9 +161,11 @@ def generic_validate(url: str, func: Callable[[httpx.Response], AnyResult], **kw
133
161
  """
134
162
  try:
135
163
  response = make_get_request(url, **kwargs)
136
- return func(response)
164
+ result = func(response)
165
+ result.url = url
166
+ return result
137
167
  except Exception as e:
138
- return Result.error(e)
168
+ return Result.error(e, url=url)
139
169
 
140
170
 
141
171
  def status_validate(url: str, available: int | List[int], taken: int | List[int], **kwargs) -> Result:
@@ -162,6 +192,7 @@ def status_validate(url: str, available: int | List[int], taken: int | List[int]
162
192
 
163
193
  return generic_validate(url, inner, **kwargs)
164
194
 
195
+
165
196
  def generate_permutations(username, pattern, limit=None):
166
197
  """
167
198
  Generate all order-based permutations of characters in `pattern`
@@ -1,6 +1,24 @@
1
1
  from enum import Enum
2
2
  from typing import Literal
3
3
 
4
+ DEBUG_MSG = """Result {{
5
+ status: {status},
6
+ reason: "{reason}",
7
+ username: "{username}",
8
+ site_name: "{site_name}",
9
+ category: "{category}",
10
+ }}"""
11
+
12
+ JSON_TEMPLATE = """{{
13
+ \t"username": "{username}",
14
+ \t"category": "{category}",
15
+ \t"site_name": "{site_name}",
16
+ \t"status": "{status}",
17
+ \t"reason": "{reason}"
18
+ }}"""
19
+
20
+ CSV_TEMPLATE = "{username},{category},{site_name},{status},{reason}"
21
+
4
22
 
5
23
  def humanize_exception(e: Exception) -> str:
6
24
  msg = str(e).lower()
@@ -18,23 +36,36 @@ class Status(Enum):
18
36
  AVAILABLE = 1
19
37
  ERROR = 2
20
38
 
39
+ def __str__(self):
40
+ return super().__str__().split(".")[1].capitalize()
41
+
21
42
 
22
43
  class Result:
23
- def __init__(self, status: Status, reason: str | Exception | None = None):
44
+ def __init__(self, status: Status, reason: str | Exception | None = None, **kwargs):
24
45
  self.status = status
25
46
  self.reason = reason
26
47
 
48
+ self.username = None
49
+ self.site_name = None
50
+ self.category = None
51
+ self.update(**kwargs)
52
+
53
+ def update(self, **kwargs):
54
+ for field in ("username", "site_name", "category"):
55
+ if field in kwargs and kwargs[field] is not None:
56
+ setattr(self, field, kwargs[field])
57
+
27
58
  @classmethod
28
- def taken(cls):
29
- return cls(Status.TAKEN)
59
+ def taken(cls, **kwargs):
60
+ return cls(Status.TAKEN, **kwargs)
30
61
 
31
62
  @classmethod
32
- def available(cls):
33
- return cls(Status.AVAILABLE)
63
+ def available(cls, **kwargs):
64
+ return cls(Status.AVAILABLE, **kwargs)
34
65
 
35
66
  @classmethod
36
- def error(cls, reason: str | Exception | None = None):
37
- return cls(Status.ERROR, reason)
67
+ def error(cls, reason: str | Exception | None = None, **kwargs):
68
+ return cls(Status.ERROR, reason, **kwargs)
38
69
 
39
70
  @classmethod
40
71
  def from_number(cls, i: int, reason: str | Exception | None = None):
@@ -56,10 +87,28 @@ class Result:
56
87
  return ""
57
88
  if isinstance(self.reason, str):
58
89
  return self.reason
59
- #Format the exception
90
+ # Format the exception
60
91
  msg = humanize_exception(self.reason)
61
92
  return f"{type(self.reason).__name__}: {msg.capitalize()}"
62
93
 
94
+ def as_dict(self) -> dict:
95
+ return {
96
+ "status": self.status,
97
+ "reason": self.get_reason(),
98
+ "username": self.username,
99
+ "site_name": self.site_name,
100
+ "category": self.category
101
+ }
102
+
103
+ def debug(self) -> str:
104
+ return DEBUG_MSG.format(**self.as_dict())
105
+
106
+ def to_json(self) -> str:
107
+ return JSON_TEMPLATE.format(**self.as_dict())
108
+
109
+ def to_csv(self) -> str:
110
+ return CSV_TEMPLATE.format(**self.as_dict())
111
+
63
112
  def __str__(self):
64
113
  return self.get_reason()
65
114
 
@@ -0,0 +1,9 @@
1
+ def get_site_name(module) -> str:
2
+ name = module.__name__.split('.')[-1].capitalize().replace("_", ".")
3
+ if name == "X":
4
+ return "X (Twitter)"
5
+ return name
6
+
7
+
8
+ def is_last_value(values, i: int) -> bool:
9
+ return i == len(values) - 1
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.0.8.1",
2
+ "version": "1.0.9.0",
3
3
  "version_type": "pypi"
4
4
  }
File without changes