user-scanner 1.0.8.1__py3-none-any.whl → 1.0.9.1__py3-none-any.whl
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.
- user_scanner/__main__.py +86 -61
- user_scanner/cli/printer.py +117 -0
- user_scanner/core/orchestrator.py +91 -58
- user_scanner/core/result.py +57 -8
- user_scanner/core/utils.py +9 -0
- user_scanner/creator/hashnode.py +3 -6
- user_scanner/creator/itch_io.py +15 -4
- user_scanner/dev/bitbucket.py +34 -0
- user_scanner/dev/github.py +1 -1
- user_scanner/dev/leetcode.py +35 -0
- user_scanner/dev/sourceforge.py +30 -0
- user_scanner/social/discord.py +4 -4
- user_scanner/social/twitch.py +83 -0
- user_scanner/version.json +1 -1
- {user_scanner-1.0.8.1.dist-info → user_scanner-1.0.9.1.dist-info}/METADATA +22 -10
- {user_scanner-1.0.8.1.dist-info → user_scanner-1.0.9.1.dist-info}/RECORD +19 -13
- {user_scanner-1.0.8.1.dist-info → user_scanner-1.0.9.1.dist-info}/WHEEL +0 -0
- {user_scanner-1.0.8.1.dist-info → user_scanner-1.0.9.1.dist-info}/entry_points.txt +0 -0
- {user_scanner-1.0.8.1.dist-info → user_scanner-1.0.9.1.dist-info}/licenses/LICENSE +0 -0
user_scanner/__main__.py
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import time
|
|
3
3
|
import re
|
|
4
|
-
from user_scanner.
|
|
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
|
|
7
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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,78 +37,108 @@ def load_categories() -> Dict[str, Path]:
|
|
|
39
37
|
return categories
|
|
40
38
|
|
|
41
39
|
|
|
42
|
-
def
|
|
43
|
-
|
|
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
|
-
|
|
52
|
-
if func:
|
|
53
|
-
try:
|
|
54
|
-
result = func(username)
|
|
55
|
-
reason = ""
|
|
71
|
+
site_name = get_site_name(module)
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
if not func:
|
|
74
|
+
return Result.error(f"{site_name} has no validate_ function", site_name=site_name, username=username)
|
|
75
|
+
|
|
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)
|
|
59
82
|
|
|
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
83
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
while i != print_queue:
|
|
74
|
-
lock.wait()
|
|
84
|
+
def run_module_single(module, username: str, printer: Printer, last: bool = True) -> List[Result]:
|
|
85
|
+
result = worker_single(module, username)
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
category = find_category(module)
|
|
88
|
+
if category:
|
|
89
|
+
result.update(category=category)
|
|
79
90
|
|
|
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)
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
# Just executes as if it was a thread
|
|
83
|
-
worker_single(module, username, print_queue)
|
|
97
|
+
return [result]
|
|
84
98
|
|
|
85
99
|
|
|
86
|
-
def run_checks_category(category_path:Path, username:str, verbose=False):
|
|
87
|
-
global print_queue
|
|
88
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
|
-
|
|
105
|
+
if printer.is_console:
|
|
106
|
+
print(f"\n{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}")
|
|
107
|
+
|
|
108
|
+
results = []
|
|
92
109
|
|
|
93
|
-
|
|
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)
|
|
94
115
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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)
|
|
100
122
|
|
|
101
|
-
|
|
102
|
-
t.join()
|
|
123
|
+
return results
|
|
103
124
|
|
|
104
125
|
|
|
105
|
-
def run_checks(username):
|
|
106
|
-
|
|
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}")
|
|
107
129
|
|
|
108
|
-
|
|
109
|
-
run_checks_category(category_path, username)
|
|
110
|
-
print()
|
|
130
|
+
results = []
|
|
111
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)
|
|
112
137
|
|
|
113
|
-
|
|
138
|
+
return results
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def make_request(url: str, **kwargs) -> httpx.Response:
|
|
114
142
|
"""Simple wrapper to **httpx.get** that predefines headers and timeout"""
|
|
115
143
|
if not "headers" in kwargs:
|
|
116
144
|
kwargs["headers"] = {
|
|
@@ -124,7 +152,9 @@ def make_get_request(url: str, **kwargs) -> httpx.Response:
|
|
|
124
152
|
if not "timeout" in kwargs:
|
|
125
153
|
kwargs["timeout"] = 5.0
|
|
126
154
|
|
|
127
|
-
|
|
155
|
+
method = kwargs.pop("method", "GET")
|
|
156
|
+
|
|
157
|
+
return httpx.request(method.upper(), url, **kwargs)
|
|
128
158
|
|
|
129
159
|
|
|
130
160
|
def generic_validate(url: str, func: Callable[[httpx.Response], AnyResult], **kwargs) -> AnyResult:
|
|
@@ -132,10 +162,12 @@ def generic_validate(url: str, func: Callable[[httpx.Response], AnyResult], **kw
|
|
|
132
162
|
A generic validate function that makes a request and executes the provided function on the response.
|
|
133
163
|
"""
|
|
134
164
|
try:
|
|
135
|
-
response =
|
|
136
|
-
|
|
165
|
+
response = make_request(url, **kwargs)
|
|
166
|
+
result = func(response)
|
|
167
|
+
result.url = url
|
|
168
|
+
return result
|
|
137
169
|
except Exception as e:
|
|
138
|
-
return Result.error(e)
|
|
170
|
+
return Result.error(e, url=url)
|
|
139
171
|
|
|
140
172
|
|
|
141
173
|
def status_validate(url: str, available: int | List[int], taken: int | List[int], **kwargs) -> Result:
|
|
@@ -162,6 +194,7 @@ def status_validate(url: str, available: int | List[int], taken: int | List[int]
|
|
|
162
194
|
|
|
163
195
|
return generic_validate(url, inner, **kwargs)
|
|
164
196
|
|
|
197
|
+
|
|
165
198
|
def generate_permutations(username, pattern, limit=None):
|
|
166
199
|
"""
|
|
167
200
|
Generate all order-based permutations of characters in `pattern`
|
user_scanner/core/result.py
CHANGED
|
@@ -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
|
|
user_scanner/creator/hashnode.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import httpx
|
|
2
2
|
from user_scanner.core.result import Result
|
|
3
|
+
from user_scanner.core.orchestrator import generic_validate
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def validate_hashnode(user):
|
|
@@ -18,9 +19,7 @@ def validate_hashnode(user):
|
|
|
18
19
|
'Referer': "https://hashnode.com/signup",
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
response = httpx.post(url, json=payload, headers=headers, timeout=3.0)
|
|
23
|
-
|
|
22
|
+
def process(response):
|
|
24
23
|
if response.status_code == 200:
|
|
25
24
|
data = response.json()
|
|
26
25
|
|
|
@@ -35,9 +34,7 @@ def validate_hashnode(user):
|
|
|
35
34
|
else:
|
|
36
35
|
return Result.error("Invalid status code")
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
return Result.error(e)
|
|
40
|
-
|
|
37
|
+
return generic_validate(url, process, method="POST", json=payload, headers=headers, timeout=3.0)
|
|
41
38
|
|
|
42
39
|
if __name__ == "__main__":
|
|
43
40
|
user = input("Username?: ").strip()
|
user_scanner/creator/itch_io.py
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import re
|
|
2
|
+
from user_scanner.core.orchestrator import status_validate, Result
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
def validate_itch_io(user):
|
|
5
|
-
|
|
5
|
+
def validate_itch_io(user: str) -> Result:
|
|
6
|
+
if not (2 <= len(user) <= 25):
|
|
7
|
+
return Result.error("Length must be 2-25 characters.")
|
|
8
|
+
|
|
9
|
+
if not re.match(r'^[a-z0-9_-]+$', user):
|
|
10
|
+
|
|
11
|
+
if re.search(r'[A-Z]', user):
|
|
12
|
+
return Result.error("Use lowercase letters only.")
|
|
13
|
+
|
|
14
|
+
return Result.error("Only use lowercase letters, numbers, underscores, and hyphens.")
|
|
15
|
+
|
|
16
|
+
url = f"https://itch.io/profile/{user}"
|
|
6
17
|
|
|
7
18
|
return status_validate(url, 404, 200, follow_redirects=True)
|
|
8
19
|
|
|
@@ -16,4 +27,4 @@ if __name__ == "__main__":
|
|
|
16
27
|
elif result == 0:
|
|
17
28
|
print("Unavailable!")
|
|
18
29
|
else:
|
|
19
|
-
print("Error occurred!")
|
|
30
|
+
print(f"Error occurred! Reason: {result.get_reason()}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from user_scanner.core.orchestrator import status_validate, Result
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def validate_bitbucket(user: str) -> Result:
|
|
6
|
+
if not (1 <= len(user) <= 30):
|
|
7
|
+
return Result.error("Length must be 1-30 characters.")
|
|
8
|
+
|
|
9
|
+
if not re.match(r'^[a-z0-9][a-z0-9_-]*$', user):
|
|
10
|
+
|
|
11
|
+
if re.search(r'[A-Z]', user):
|
|
12
|
+
return Result.error("Use lowercase letters only.")
|
|
13
|
+
|
|
14
|
+
return Result.error("Only use lowercase letters, numbers, hyphens, and underscores.")
|
|
15
|
+
|
|
16
|
+
url = f"https://bitbucket.org/{user}/"
|
|
17
|
+
|
|
18
|
+
return status_validate(url, 404, [200, 302], follow_redirects=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
user = input("Username?: ").strip()
|
|
23
|
+
result = validate_bitbucket(user)
|
|
24
|
+
|
|
25
|
+
if result == 1:
|
|
26
|
+
print("Available!")
|
|
27
|
+
elif result == 0:
|
|
28
|
+
print("Unavailable!")
|
|
29
|
+
else:
|
|
30
|
+
print(f"Error occurred! Reason: {result.get_reason()}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
user_scanner/dev/github.py
CHANGED
|
@@ -29,7 +29,7 @@ def validate_github(user):
|
|
|
29
29
|
|
|
30
30
|
if response.status_code == 422:
|
|
31
31
|
if GITHUB_INVALID_MSG in response.text:
|
|
32
|
-
return Result.error("Cannot start/end with hyphen or use double hyphens")
|
|
32
|
+
return Result.error("Cannot start/end with hyphen or use double hyphens, underscores")
|
|
33
33
|
|
|
34
34
|
return Result.taken()
|
|
35
35
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from user_scanner.core.orchestrator import status_validate, Result
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def validate_leetcode(user: str) -> Result:
|
|
6
|
+
if not (3 <= len(user) <= 30):
|
|
7
|
+
return Result.error("Length must be between 3 and 30 characters")
|
|
8
|
+
|
|
9
|
+
if not re.match(r'^[a-zA-Z0-9._-]+$', user):
|
|
10
|
+
return Result.error("Can only use letters, numbers, underscores, periods, or hyphens")
|
|
11
|
+
|
|
12
|
+
url = f"https://leetcode.com/u/{user}/"
|
|
13
|
+
|
|
14
|
+
headers = {
|
|
15
|
+
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36",
|
|
16
|
+
'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",
|
|
17
|
+
'Accept-Encoding': "identity",
|
|
18
|
+
'upgrade-insecure-requests': "1",
|
|
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_leetcode(user)
|
|
29
|
+
|
|
30
|
+
if result == 1:
|
|
31
|
+
print("Available!")
|
|
32
|
+
elif result == 0:
|
|
33
|
+
print("Unavailable!")
|
|
34
|
+
else:
|
|
35
|
+
print(f"Error occurred! Reason: {result.get_reason()}")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from user_scanner.core.orchestrator import status_validate, Result
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def validate_sourceforge(user: str) -> Result:
|
|
6
|
+
if not (3 <= len(user) <= 30):
|
|
7
|
+
return Result.error("Length must be 3-30 characters.")
|
|
8
|
+
|
|
9
|
+
if not re.match(r'^[a-z0-9-]+$', user):
|
|
10
|
+
|
|
11
|
+
if re.search(r'[A-Z]', user):
|
|
12
|
+
return Result.error("Use lowercase letters only.")
|
|
13
|
+
|
|
14
|
+
return Result.error("Only use lowercase letters, numbers, and dashes.")
|
|
15
|
+
|
|
16
|
+
url = f"https://sourceforge.net/u/{user}/"
|
|
17
|
+
|
|
18
|
+
return status_validate(url, 404, 200, follow_redirects=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
user = input("Username?: ").strip()
|
|
23
|
+
result = validate_sourceforge(user)
|
|
24
|
+
|
|
25
|
+
if result == 1:
|
|
26
|
+
print("Available!")
|
|
27
|
+
elif result == 0:
|
|
28
|
+
print("Unavailable!")
|
|
29
|
+
else:
|
|
30
|
+
print(f"Error occurred! Reason: {result.get_reason()}")
|
user_scanner/social/discord.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import httpx
|
|
2
2
|
from user_scanner.core.result import Result
|
|
3
|
+
from user_scanner.core.orchestrator import generic_validate
|
|
3
4
|
|
|
4
5
|
def validate_discord(user):
|
|
5
6
|
url = "https://discord.com/api/v9/unique-username/username-attempt-unauthed"
|
|
@@ -15,8 +16,7 @@ def validate_discord(user):
|
|
|
15
16
|
|
|
16
17
|
data = {"username": user}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
response = httpx.post(url, headers=headers, json=data, timeout=3.0)
|
|
19
|
+
def process(response):
|
|
20
20
|
if response.status_code == 200:
|
|
21
21
|
status = response.json().get("taken")
|
|
22
22
|
if status is True:
|
|
@@ -24,8 +24,8 @@ def validate_discord(user):
|
|
|
24
24
|
elif status is False:
|
|
25
25
|
return Result.available()
|
|
26
26
|
return Result.error("Invalid status code")
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
return generic_validate(url, process, method="POST", json=data, headers=headers, timeout=3.0)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import httpx
|
|
4
|
+
from user_scanner.core.orchestrator import generic_validate, Result
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_twitch(user: str) -> Result:
|
|
8
|
+
if not (4 <= len(user) <= 25):
|
|
9
|
+
return Result.error("Username must be between 4 and 25 characters long")
|
|
10
|
+
|
|
11
|
+
if not re.match(r"^[a-zA-Z0-9]+$", user):
|
|
12
|
+
return Result.error("Username can only contain alphanumeric characters (a-z, 0-9)")
|
|
13
|
+
|
|
14
|
+
url = "https://gql.twitch.tv/gql"
|
|
15
|
+
|
|
16
|
+
payload = [
|
|
17
|
+
{
|
|
18
|
+
"operationName": "ChannelLayout",
|
|
19
|
+
"variables": {
|
|
20
|
+
"channelLogin": user,
|
|
21
|
+
"includeIsDJ": True
|
|
22
|
+
},
|
|
23
|
+
"extensions": {
|
|
24
|
+
"persistedQuery": {
|
|
25
|
+
"version": 1,
|
|
26
|
+
"sha256Hash": "4c361fa1874dc8f6a49e62b56aa1032eccb31311bdb653918a924f96a8b2d1a6"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
headers = {
|
|
33
|
+
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36",
|
|
34
|
+
'Accept-Encoding': "identity",
|
|
35
|
+
'Content-Type': "application/json",
|
|
36
|
+
'sec-ch-ua-platform': "\"Android\"",
|
|
37
|
+
'accept-language': "en-US",
|
|
38
|
+
'client-id': "kimne78kx3ncx6brgo4mv6wki5h1ko",
|
|
39
|
+
'client-version': "7bb0442d-1175-4ab5-9d32-b1f370536cbf",
|
|
40
|
+
'origin': "https://m.twitch.tv",
|
|
41
|
+
'referer': "https://m.twitch.tv/",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def process(response: httpx.Response) -> Result:
|
|
45
|
+
if response.status_code != 200:
|
|
46
|
+
return Result.error(f"Unexpected status code: {response.status_code}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
data = response.json()
|
|
50
|
+
except json.JSONDecodeError as e:
|
|
51
|
+
return Result.error(f"Failed to decode JSON response: {e}")
|
|
52
|
+
|
|
53
|
+
user_data = data[0].get('data', {}).get('user', {})
|
|
54
|
+
typename = user_data.get('__typename')
|
|
55
|
+
|
|
56
|
+
if typename == "User":
|
|
57
|
+
return Result.taken()
|
|
58
|
+
elif typename == "UserDoesNotExist":
|
|
59
|
+
return Result.available()
|
|
60
|
+
else:
|
|
61
|
+
return Result.error("Unexpected GraphQL response structure or type.")
|
|
62
|
+
|
|
63
|
+
return generic_validate(
|
|
64
|
+
url,
|
|
65
|
+
process,
|
|
66
|
+
headers=headers,
|
|
67
|
+
method='POST',
|
|
68
|
+
content=json.dumps(payload),
|
|
69
|
+
follow_redirects=False
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
user = input("Twitch Username?: ").strip()
|
|
75
|
+
result = validate_twitch(user)
|
|
76
|
+
|
|
77
|
+
if result == 1:
|
|
78
|
+
print("Available!")
|
|
79
|
+
elif result == 0:
|
|
80
|
+
print("Unavailable!")
|
|
81
|
+
else:
|
|
82
|
+
reason = result.get_reason()
|
|
83
|
+
print(f"Error occurred! Reason: {reason}")
|
user_scanner/version.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: user-scanner
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9.1
|
|
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
|

|
|
17
17
|
<p align="center">
|
|
18
|
-
<img src="https://img.shields.io/badge/Version-1.0.
|
|
18
|
+
<img src="https://img.shields.io/badge/Version-1.0.9.1-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
|
-
|
|
67
|
+
```
|
|
66
68
|
|
|
69
|
+
Also, the output file and format can be specified: <br>
|
|
70
|
+
|
|
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
|
|
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
user_scanner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
user_scanner/__main__.py,sha256=
|
|
3
|
-
user_scanner/version.json,sha256=
|
|
2
|
+
user_scanner/__main__.py,sha256=jmfWuPNToix9UtLNh7IYrm-M22QBdSPtmMHv4ZY66Bc,5253
|
|
3
|
+
user_scanner/version.json,sha256=xMl1CJDy9wpMkJKVCDUR1Vh7d-oyLU4gx1symSWOLIY,49
|
|
4
4
|
user_scanner/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
user_scanner/cli/banner.py,sha256=3t6owaDArERlvpcszA1Yi3dtksvh8a9tLyrxRowTC40,1499
|
|
6
|
+
user_scanner/cli/printer.py,sha256=H8rNw0ewG3G0JquKnMLW8PbHmFcALaEZZNUAsAUScHg,4027
|
|
6
7
|
user_scanner/community/__init__.py,sha256=5EzlM991pJqvqIRc05_QV5BureJZ7wiCRm1AyEY6pms,12
|
|
7
8
|
user_scanner/community/coderlegion.py,sha256=W_bdjzdFPRgUrNFFlylvToSJ4AzaFCtTsUy_MRVDdSo,451
|
|
8
9
|
user_scanner/community/stackoverflow.py,sha256=MTL8O0TLHkjVbugBh1pLxELJLU3hkX_YEHjGjaKTJi4,1007
|
|
9
10
|
user_scanner/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
user_scanner/core/orchestrator.py,sha256=
|
|
11
|
-
user_scanner/core/result.py,sha256=
|
|
11
|
+
user_scanner/core/orchestrator.py,sha256=nfe0KEcT2U_MB48OgmuvQ0tHvQdnJm8VVi06QxiuJMU,7059
|
|
12
|
+
user_scanner/core/result.py,sha256=8qrIXO5jg6OjWkLtEq25lx_b1hLgtDFugdDyrJX4vcU,3300
|
|
13
|
+
user_scanner/core/utils.py,sha256=v3XLUXmknf9zl_JBOmnss3280SrEWBdPcz-zq3S8lak,249
|
|
12
14
|
user_scanner/creator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
15
|
user_scanner/creator/devto.py,sha256=mIACmG1a4eoctywxb5p04sI0YVi3dsjCRw9YVOFBEKQ,435
|
|
14
|
-
user_scanner/creator/hashnode.py,sha256=
|
|
15
|
-
user_scanner/creator/itch_io.py,sha256=
|
|
16
|
+
user_scanner/creator/hashnode.py,sha256=NEIpSyf0zbcZ_QNjU3C7F5oApvVpUQOd_oQuughM-Qc,1403
|
|
17
|
+
user_scanner/creator/itch_io.py,sha256=2a8UVh-_OaWQPcSUHUuijDGpWDxsR8DoCcU1BdTRqqs,854
|
|
16
18
|
user_scanner/creator/kaggle.py,sha256=QaXIG02OGxvQZEvwHm50RKNd7joxGOq0Ht3cFfrYEiU,445
|
|
17
19
|
user_scanner/creator/medium.py,sha256=NIOYnk8_ASD0kYfKqs8t6uZZTV4D-5-ZxyHMzOMMOuI,1015
|
|
18
20
|
user_scanner/creator/patreon.py,sha256=g-r85pxirf0ihK3STyGYPIzp59MB7JH64Opb4wq1fyU,461
|
|
19
21
|
user_scanner/creator/producthunt.py,sha256=p0HoIIVhmv9bBkelhfzRYudUFoyk_qeT66-hPpHEFqk,1938
|
|
20
22
|
user_scanner/dev/__init__.py,sha256=qUR0eLwN-gO6oKk-1cmCVT4G_AxUHHMgpV3wJ7URXi4,7
|
|
23
|
+
user_scanner/dev/bitbucket.py,sha256=qAIlFCmMaNTUx2-a5wJKHjbQjERcJt0zKHJmjLAeXr4,876
|
|
21
24
|
user_scanner/dev/codeberg.py,sha256=Z6nV0_8xZhMiCcNn9Hn79VVh6y0ar9fqL7KS2b7IaDo,447
|
|
22
25
|
user_scanner/dev/cratesio.py,sha256=mJnlLJoMLlQ7f_95QD7LgH1xCj-e6FooOFkpYypBfG4,724
|
|
23
26
|
user_scanner/dev/dockerhub.py,sha256=sPEnomGiPM2mKv2HsA-9WxaXHjzz21A6ox3IXK1etLc,643
|
|
24
|
-
user_scanner/dev/github.py,sha256=
|
|
27
|
+
user_scanner/dev/github.py,sha256=9Q4G84WTAeWfNliApKdRFl1MJLfHvDPJ09mwr8P1ePo,1702
|
|
25
28
|
user_scanner/dev/gitlab.py,sha256=kMDSd74XbofmJocfS4Fd9DxPryIHBMek3N_5c7Z_AJQ,1351
|
|
26
29
|
user_scanner/dev/huggingface.py,sha256=hDanOZ45LeUg3hrN0CYrBnBnLqHCYtOWS0_HCvAbmDw,454
|
|
27
30
|
user_scanner/dev/launchpad.py,sha256=N58ioX_dEHq2uwyyGrWnDKWwbqK9_RiuBQ1uWR5cDfg,799
|
|
31
|
+
user_scanner/dev/leetcode.py,sha256=PTJcgp1W3fzLDK_Jy_VvRjKnLftLYMJaw3kfMjHqt9c,1246
|
|
28
32
|
user_scanner/dev/npmjs.py,sha256=k-DhFqGJWDoQ79EzR8hmVrJk07AfJfPUWnIYuKc2G6w,713
|
|
29
33
|
user_scanner/dev/replit.py,sha256=SI_i2l4w9tm2kBX4-cONBAT8dSynXoGEP4zcU8ngnh0,442
|
|
34
|
+
user_scanner/dev/sourceforge.py,sha256=Kt8MmpCgB1tNwYRI9PYOZzIrL1VfnpzeNC43DcbZlbI,850
|
|
30
35
|
user_scanner/donation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
36
|
user_scanner/donation/buymeacoffee.py,sha256=86LGyChv_UKQFp2D7nIoK1B-FCAAbbfabS8NA9yLp5k,459
|
|
32
37
|
user_scanner/donation/liberapay.py,sha256=njClxpbRLZQ_L2-lUYCY6QFnF4IcwfCJPCIg1iEqo7M,1120
|
|
@@ -39,7 +44,7 @@ user_scanner/gaming/roblox.py,sha256=Qs51jLgKh-Ehqlco_j8CFtJ4CLVoZeBwPugDvAyLw3Q
|
|
|
39
44
|
user_scanner/gaming/steam.py,sha256=l8xk_p9aiYQWCPoogQnO1iwkfojPhg6yd76OZHhKN50,740
|
|
40
45
|
user_scanner/social/__init__.py,sha256=jaCkFwX1uYtF0ENifVwF8OfHrYYUTm64B9wlBq9BBfQ,9
|
|
41
46
|
user_scanner/social/bluesky.py,sha256=11Y_vRj3txEDQqoD0iANgSWVSB8L87OotPQZquhneR0,1994
|
|
42
|
-
user_scanner/social/discord.py,sha256=
|
|
47
|
+
user_scanner/social/discord.py,sha256=S_6ItjRcxip_L60UJ2rdLRFf4eXT7fMN7roCKA-lDfc,1193
|
|
43
48
|
user_scanner/social/instagram.py,sha256=GgmKGvi3meKdZ_nQJbJSBZDJTEKSoE6Cn4_VARmo62I,953
|
|
44
49
|
user_scanner/social/mastodon.py,sha256=qISx-gUsddC8lFMcmERA4N0YAnXyS1Jq2Xgg7XE4sL4,450
|
|
45
50
|
user_scanner/social/pinterest.py,sha256=JIJ-HPtMoGvxW7NQzm02lChFKMmE6k6GxFoUZ6OvCec,784
|
|
@@ -48,12 +53,13 @@ user_scanner/social/snapchat.py,sha256=XEW_W4jEBX4AiHREcfHGstt97Ez3GI-3bKSzhtMyn
|
|
|
48
53
|
user_scanner/social/soundcloud.py,sha256=e2yU1w2fnH1EhzYed0kxgcqgWz0YoCQQFf6yKqhRPjM,1246
|
|
49
54
|
user_scanner/social/telegram.py,sha256=9IS-0pghMifNRmj62NcxCOvn23Hvg0AJJcuhCa_aXD4,765
|
|
50
55
|
user_scanner/social/threads.py,sha256=rK8Gm_riDdr0djo23tk38fNVVEBuC6nj2iTXvWrqXeE,951
|
|
56
|
+
user_scanner/social/twitch.py,sha256=blsh5sMT7miF5-xqVXYLieTILzkop2PsWqv9HhP8G40,2509
|
|
51
57
|
user_scanner/social/x.py,sha256=sAnboHHZN2DWyKeds46GLZHxGG-G_bjzfVNIkblSHx8,1406
|
|
52
58
|
user_scanner/social/youtube.py,sha256=UPu584teg75P7FT05RFG3nobbHgPmzjr-ZwyN2sw6gw,1980
|
|
53
59
|
user_scanner/utils/update.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
60
|
user_scanner/utils/version.py,sha256=mPh24EwITyXgD3AMgbflRL180pS0JfrvuJdnoErOU34,623
|
|
55
|
-
user_scanner-1.0.
|
|
56
|
-
user_scanner-1.0.
|
|
57
|
-
user_scanner-1.0.
|
|
58
|
-
user_scanner-1.0.
|
|
59
|
-
user_scanner-1.0.
|
|
61
|
+
user_scanner-1.0.9.1.dist-info/entry_points.txt,sha256=XqU3kssYZ0vXaPy5qYUOTCu4u-48Xie7QWFpBCYc7Nc,59
|
|
62
|
+
user_scanner-1.0.9.1.dist-info/licenses/LICENSE,sha256=XH1QyQG68zo1opDIZHTHcTAbe9XMzewvTaFTukcN9vc,1061
|
|
63
|
+
user_scanner-1.0.9.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
64
|
+
user_scanner-1.0.9.1.dist-info/METADATA,sha256=NXrrk_8W1d8k7DbXvyAhMYq9QFCL-opRBVicqcOQVDI,5697
|
|
65
|
+
user_scanner-1.0.9.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|