From 9de08a236334dc6c1e973b7dee5a4a5e4f8bb1db Mon Sep 17 00:00:00 2001 From: Vamato <62358492+VamatoHD@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:46:23 +0000 Subject: [PATCH 1/6] feat: json output --- user_scanner/__main__.py | 74 ++++++++++++++++--------- user_scanner/cli/printer.py | 90 +++++++++++++++++++++++++++++++ user_scanner/core/orchestrator.py | 53 +++++++++--------- user_scanner/core/result.py | 3 ++ 4 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 user_scanner/cli/printer.py diff --git a/user_scanner/__main__.py b/user_scanner/__main__.py index c50bdb9..e89dbbf 100644 --- a/user_scanner/__main__.py +++ b/user_scanner/__main__.py @@ -1,10 +1,10 @@ import argparse import time import re +from user_scanner.cli import printer from user_scanner.core.orchestrator import run_checks, load_modules , generate_permutations, load_categories from colorama import Fore, Style -from .cli import banner -from .cli.banner import print_banner +from user_scanner.cli.banner import print_banner MAX_PERMUTATIONS_LIMIT = 100 # To prevent excessive generation @@ -54,6 +54,10 @@ def main(): parser.add_argument( "-d", "--delay",type=float,default=0,help="Delay in seconds between requests (recommended: 1-2 seconds)" ) + + parser.add_argument( + "-o", "--output-format", choices=["console", "csv", "json"], default="console", help="Specify output format (default: console)" + ) args = parser.parse_args() @@ -64,19 +68,22 @@ def main(): if not args.username: parser.print_help() return - - # Special username checks before run - if (args.module == "x" or args.category == "social"): - if re.search(r"[^a-zA-Z0-9._-]", args.username): - print( - Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. X (Twitter) doesn't support these." + Style.RESET_ALL) - if (args.module == "bluesky" or args.category == "social"): - if re.search(r"[^a-zA-Z0-9\.-]", args.username): - print( - Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. Bluesky will throw error. (Supported: only hyphens and digits)" + Style.RESET_ALL + "\n") - print_banner() + + Printer = printer.Printer(args.output_format) + + if Printer.is_console: + print_banner() + # Special username checks before run + if (args.module == "x" or args.category == "social"): + if re.search(r"[^a-zA-Z0-9._-]", args.username): + print( + Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. X (Twitter) doesn't support these." + Style.RESET_ALL) + if (args.module == "bluesky" or args.category == "social"): + if re.search(r"[^a-zA-Z0-9\.-]", args.username): + print( + Fore.RED + f"[!] Username '{args.username}' contains unsupported special characters. Bluesky will throw error. (Supported: only hyphens and digits)" + Style.RESET_ALL + "\n") - if args.permute and args.delay == 0: + if args.permute and args.delay == 0 and Printer.is_console: print( Fore.YELLOW + "[!] Warning: You're generating multiple usernames with NO delay between requests. " @@ -88,13 +95,14 @@ def main(): #Added permutation support , generate all possible permutation of given sequence. if args.permute: usernames = generate_permutations(args.username, args.permute , args.stop) - print(Fore.CYAN + f"[+] Generated {len(usernames)} username permutations" + Style.RESET_ALL) + if Printer.is_console: + print( + Fore.CYAN + f"[+] Generated {len(usernames)} username permutations" + Style.RESET_ALL) - - if args.module and "." in args.module: args.module = args.module.replace(".", "_") + is_last = lambda x: x == len(usernames) - 1 if args.module: # Single module search across all categories @@ -105,10 +113,15 @@ def main(): site_name = module.__name__.split(".")[-1] if site_name.lower() == args.module.lower(): from user_scanner.core.orchestrator import run_module_single - for name in usernames: # <-- permutation support here - run_module_single(module, name) - if args.delay > 0: + + Printer.print_json_start() + + for i, name in enumerate(usernames): # <-- permutation support here + run_module_single(module, name, Printer, is_last(i)) + if args.delay > 0 and not is_last(i): time.sleep(args.delay) + + Printer.print_json_end() found = True if not found: print( @@ -118,17 +131,26 @@ def main(): category_package = load_categories().get(args.category) from user_scanner.core.orchestrator import run_checks_category - for name in usernames: # <-- permutation support here - run_checks_category(category_package, name, args.verbose) - if args.delay > 0: + Printer.print_json_start() + + for i, name in enumerate(usernames): # <-- permutation support here + run_checks_category(category_package, name, Printer, is_last(i)) + if args.delay > 0 and not is_last(i): time.sleep(args.delay) + + Printer.print_json_end() + else: # Full scan - for name in usernames: - run_checks(name) - if args.delay > 0: + Printer.print_json_start() + + for i, name in enumerate(usernames): + run_checks(name, Printer, is_last(i)) + if args.delay > 0 and not is_last(i): time.sleep(args.delay) + Printer.print_json_end() + if __name__ == "__main__": main() diff --git a/user_scanner/cli/printer.py b/user_scanner/cli/printer.py new file mode 100644 index 0000000..029c649 --- /dev/null +++ b/user_scanner/cli/printer.py @@ -0,0 +1,90 @@ +from colorama import Fore, Style +from typing import Literal +from user_scanner.core.result import Result, Status + +JSON_TEMPLATE = """{{ +\t"site_name": "{site_name}", +\t"username": "{username}", +\t"result": "{result}" +}}""" + +JSON_TEMPLATE_ERROR = """{{ +\t"site_name": "{site_name}", +\t"username": "{username}", +\t"result": "Error", +\t"reason": "{reason}" +}}""" + + +def identate(msg: str, ident: int): + tabs = "\t" * ident + return "\n".join([f"{tabs}{line}" for line in msg.split("\n")]) + + +class Printer: + def __init__(self, output_format: Literal["console", "csv", "json"]) -> None: + if not output_format in ["console", "csv", "json"]: + raise ValueError(f"Invalid output-format: {output_format}") + self.mode: str = output_format + self.ident: int = 0 + + @property + def is_console(self) -> bool: + return self.mode == "console" + + @property + def is_csv(self) -> bool: + return self.mode == "csv" + + @property + def is_json(self) -> bool: + return self.mode == "json" + + def print_json_start(self) -> None: + if not self.is_json: + return + self.ident += 1 + print(identate("[", self.ident - 1)) + + def print_json_end(self) -> None: + if not self.is_json: + return + self.ident = max(self.ident - 1, 0) + print(identate("]", self.ident)) + + def get_result_output(self, site_name: str, username: str, result: Result) -> str: + if result == None: + result = Result.error("Invalid return value: None") + + if isinstance(result, int): + result = Result.from_number(result) + + match (result.status, self.mode): + case (Status.AVAILABLE, "console"): + return f" {Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}" + + case (Status.TAKEN, "console"): + return f" {Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}" + + case (Status.ERROR, "console"): + reason = "" + if isinstance(result, Result) and result.has_reason(): + reason = f" ({result.get_reason()})" + return f" {Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}" + + case (Status.AVAILABLE, "json") | (Status.TAKEN, "json"): + msg = identate(JSON_TEMPLATE, self.ident).format( + site_name=site_name, + username=username, + result=str(result.status) + ) + return msg + + case (Status.ERROR, "json"): + msg = identate(JSON_TEMPLATE_ERROR, self.ident).format( + site_name=site_name, + username=username, + reason=result.get_reason() + ) + return msg + diff --git a/user_scanner/core/orchestrator.py b/user_scanner/core/orchestrator.py index 5e9e3cc..e615793 100644 --- a/user_scanner/core/orchestrator.py +++ b/user_scanner/core/orchestrator.py @@ -5,6 +5,7 @@ from itertools import permutations import httpx from pathlib import Path +from user_scanner.cli.printer import Printer from user_scanner.core.result import Result, AnyResult from typing import Callable, Dict, List @@ -39,7 +40,7 @@ def load_categories() -> Dict[str, Path]: return categories -def worker_single(module, username, i): +def worker_single(module, username: str, i: int, printer: Printer, last: bool = True) -> AnyResult: global print_queue func = next((getattr(module, f) for f in dir(module) @@ -52,49 +53,46 @@ def worker_single(module, username, i): if func: try: result = func(username) - reason = "" - - if isinstance(result, Result) and result.has_reason(): - reason = f" ({result.get_reason()})" - - if result == 1: - output = f" {Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}" - elif result == 0: - output = f" {Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}" - else: - output = f" {Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}" + output = printer.get_result_output(site_name, username, result) + if last == False and printer.is_json: + output += "," except Exception as e: - output = f" {Fore.YELLOW}[!] {site_name}: Exception - {e}{Style.RESET_ALL}" + if Printer.is_console: + output = f" {Fore.YELLOW}[!] {site_name}: Exception - {e}{Style.RESET_ALL}" else: - output = f" {Fore.YELLOW}[!] {site_name} has no validate_ function{Style.RESET_ALL}" + if Printer.is_console: + output = f" {Fore.YELLOW}[!] {site_name} has no validate_ function{Style.RESET_ALL}" with lock: # Waits for in-order printing while i != print_queue: lock.wait() - print(output) + if output != "": + print(output) print_queue += 1 lock.notify_all() -def run_module_single(module, username): +def run_module_single(module, username: str, printer: Printer, last: bool = True) -> AnyResult: # Just executes as if it was a thread - worker_single(module, username, print_queue) + return worker_single(module, username, print_queue, printer, last) -def run_checks_category(category_path:Path, username:str, verbose=False): +def run_checks_category(category_path: Path, username: str, printer: Printer, last: bool = True): global print_queue modules = load_modules(category_path) - category_name = category_path.stem.capitalize() - print(f"{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}") + if printer.is_console: + category_name = category_path.stem.capitalize() + print(f"\n{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}") print_queue = 0 threads = [] for i, module in enumerate(modules): - t = threading.Thread(target=worker_single, args=(module, username, i)) + last_thread = last and (i == len(modules) - 1) + t = threading.Thread(target=worker_single, args=(module, username, i, printer, last_thread)) threads.append(t) t.start() @@ -102,13 +100,14 @@ def run_checks_category(category_path:Path, username:str, verbose=False): t.join() -def run_checks(username): - print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}\n") - - for category_path in load_categories().values(): - run_checks_category(category_path, username) - print() +def run_checks(username: str, printer: Printer, last:bool = True): + if printer.is_console: + print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}") + categories = list(load_categories().values()) + for i, category_path in enumerate(categories): + last_cat: int = last and (i == len(categories) - 1) + run_checks_category(category_path, username, printer, last_cat) def make_get_request(url: str, **kwargs) -> httpx.Response: """Simple wrapper to **httpx.get** that predefines headers and timeout""" diff --git a/user_scanner/core/result.py b/user_scanner/core/result.py index ea55b47..15365a3 100644 --- a/user_scanner/core/result.py +++ b/user_scanner/core/result.py @@ -18,6 +18,9 @@ class Status(Enum): AVAILABLE = 1 ERROR = 2 + def __str__(self): + return super().__str__().split(".")[1].capitalize() + class Result: def __init__(self, status: Status, reason: str | Exception | None = None): From 3fe49d044241408731147bb6c6a6a944d4bb85fe Mon Sep 17 00:00:00 2001 From: Vamato <62358492+VamatoHD@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:56:12 +0000 Subject: [PATCH 2/6] feat: csv output --- user_scanner/__main__.py | 12 ++++++------ user_scanner/cli/printer.py | 31 +++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/user_scanner/__main__.py b/user_scanner/__main__.py index e89dbbf..e951a17 100644 --- a/user_scanner/__main__.py +++ b/user_scanner/__main__.py @@ -114,14 +114,14 @@ def main(): if site_name.lower() == args.module.lower(): from user_scanner.core.orchestrator import run_module_single - Printer.print_json_start() + Printer.print_start() for i, name in enumerate(usernames): # <-- permutation support here run_module_single(module, name, Printer, is_last(i)) if args.delay > 0 and not is_last(i): time.sleep(args.delay) - Printer.print_json_end() + Printer.print_end() found = True if not found: print( @@ -131,25 +131,25 @@ def main(): category_package = load_categories().get(args.category) from user_scanner.core.orchestrator import run_checks_category - Printer.print_json_start() + Printer.print_start() for i, name in enumerate(usernames): # <-- permutation support here run_checks_category(category_package, name, Printer, is_last(i)) if args.delay > 0 and not is_last(i): time.sleep(args.delay) - Printer.print_json_end() + Printer.print_end() else: # Full scan - Printer.print_json_start() + Printer.print_start() for i, name in enumerate(usernames): run_checks(name, Printer, is_last(i)) if args.delay > 0 and not is_last(i): time.sleep(args.delay) - Printer.print_json_end() + Printer.print_end() if __name__ == "__main__": diff --git a/user_scanner/cli/printer.py b/user_scanner/cli/printer.py index 029c649..dbe443c 100644 --- a/user_scanner/cli/printer.py +++ b/user_scanner/cli/printer.py @@ -15,6 +15,9 @@ \t"reason": "{reason}" }}""" +CSV_HEADER = "site_name,username,result,reason" +CSV_TEMPLATE = "{site_name},{username},{result},{reason}" + def identate(msg: str, ident: int): tabs = "\t" * ident @@ -40,13 +43,14 @@ def is_csv(self) -> bool: def is_json(self) -> bool: return self.mode == "json" - def print_json_start(self) -> None: - if not self.is_json: - return - self.ident += 1 - print(identate("[", self.ident - 1)) + def print_start(self) -> None: + if self.is_json: + self.ident += 1 + print(identate("[", self.ident - 1)) + elif self.is_csv: + print(CSV_HEADER) - def print_json_end(self) -> None: + def print_end(self) -> None: if not self.is_json: return self.ident = max(self.ident - 1, 0) @@ -73,18 +77,25 @@ def get_result_output(self, site_name: str, username: str, result: Result) -> st return f" {Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}" case (Status.AVAILABLE, "json") | (Status.TAKEN, "json"): - msg = identate(JSON_TEMPLATE, self.ident).format( + return identate(JSON_TEMPLATE, self.ident).format( site_name=site_name, username=username, result=str(result.status) ) - return msg case (Status.ERROR, "json"): - msg = identate(JSON_TEMPLATE_ERROR, self.ident).format( + return identate(JSON_TEMPLATE_ERROR, self.ident).format( + site_name=site_name, + username=username, + reason=result.get_reason() + ) + + case (_, "csv"): + return CSV_TEMPLATE.format( site_name=site_name, username=username, + result=str(result.status), reason=result.get_reason() ) - return msg + return "" From 536ffd0fc5343a6da0f2ef62d15a08094bd29abc Mon Sep 17 00:00:00 2001 From: Vamato <62358492+VamatoHD@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:19:35 +0000 Subject: [PATCH 3/6] dry: cleanup __main__ --- user_scanner/__main__.py | 63 +++++++++++++------------------ user_scanner/core/orchestrator.py | 13 +++++++ 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/user_scanner/__main__.py b/user_scanner/__main__.py index e951a17..2454d03 100644 --- a/user_scanner/__main__.py +++ b/user_scanner/__main__.py @@ -2,7 +2,7 @@ import time import re from user_scanner.cli import printer -from user_scanner.core.orchestrator import run_checks, load_modules , generate_permutations, load_categories +from user_scanner.core.orchestrator import load_modules , generate_permutations, load_categories from colorama import Fore, Style from user_scanner.cli.banner import print_banner @@ -21,7 +21,6 @@ def list_modules(category=None): site_name = module.__name__.split(".")[-1] print(f" - {site_name}") - def main(): parser = argparse.ArgumentParser( prog="user-scanner", @@ -102,54 +101,44 @@ def main(): if args.module and "." in args.module: args.module = args.module.replace(".", "_") - is_last = lambda x: x == len(usernames) - 1 + def run_all_usernames(func, arg = None): + """ + Executes a function for all given usernames. + Made in order to simplify main() + """ + Printer.print_start() + for i, name in enumerate(usernames): + is_last = i == len(usernames) - 1 + if arg == None: + func(name, Printer, is_last) + else: + func(arg, name, Printer, is_last) + if args.delay > 0 and not is_last: + time.sleep(args.delay) + Printer.print_end() if args.module: # Single module search across all categories - found = False - for cat_path in load_categories().values(): - modules = load_modules(cat_path) - for module in modules: - site_name = module.__name__.split(".")[-1] - if site_name.lower() == args.module.lower(): - from user_scanner.core.orchestrator import run_module_single + from user_scanner.core.orchestrator import run_module_single, find_module + modules = find_module(args.module) - Printer.print_start() - - for i, name in enumerate(usernames): # <-- permutation support here - run_module_single(module, name, Printer, is_last(i)) - if args.delay > 0 and not is_last(i): - time.sleep(args.delay) - - Printer.print_end() - found = True - if not found: + if len(modules) > 0: + for module in modules: + run_all_usernames(run_module_single, module) + else: print( Fore.RED + f"[!] Module '{args.module}' not found in any category." + Style.RESET_ALL) + elif args.category: # Category-wise scan category_package = load_categories().get(args.category) from user_scanner.core.orchestrator import run_checks_category - - Printer.print_start() - - for i, name in enumerate(usernames): # <-- permutation support here - run_checks_category(category_package, name, Printer, is_last(i)) - if args.delay > 0 and not is_last(i): - time.sleep(args.delay) - - Printer.print_end() + run_all_usernames(run_checks_category, category_package) else: # Full scan - Printer.print_start() - - for i, name in enumerate(usernames): - run_checks(name, Printer, is_last(i)) - if args.delay > 0 and not is_last(i): - time.sleep(args.delay) - - Printer.print_end() + from user_scanner.core.orchestrator import run_checks + run_all_usernames(run_checks) if __name__ == "__main__": diff --git a/user_scanner/core/orchestrator.py b/user_scanner/core/orchestrator.py index e615793..5e652bc 100644 --- a/user_scanner/core/orchestrator.py +++ b/user_scanner/core/orchestrator.py @@ -40,6 +40,19 @@ def load_categories() -> Dict[str, Path]: return categories +def find_module(name: str): + name = name.lower() + + matches = [ + module + for category_path in load_categories().values() + for module in load_modules(category_path) + if module.__name__.split(".")[-1].lower() == name + ] + + return matches + + def worker_single(module, username: str, i: int, printer: Printer, last: bool = True) -> AnyResult: global print_queue From 1638c0dbd71cf5a744509119f3d1c3ee885e796b Mon Sep 17 00:00:00 2001 From: Vamato <62358492+VamatoHD@users.noreply.github.com> Date: Sat, 13 Dec 2025 13:10:15 +0000 Subject: [PATCH 4/6] fix: converted indent into two spaces --- user_scanner/cli/printer.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/user_scanner/cli/printer.py b/user_scanner/cli/printer.py index dbe443c..92f08fe 100644 --- a/user_scanner/cli/printer.py +++ b/user_scanner/cli/printer.py @@ -2,25 +2,30 @@ from typing import Literal from user_scanner.core.result import Result, Status +INDENT = " " + JSON_TEMPLATE = """{{ \t"site_name": "{site_name}", \t"username": "{username}", \t"result": "{result}" -}}""" +}}""".replace("\t", INDENT) JSON_TEMPLATE_ERROR = """{{ \t"site_name": "{site_name}", \t"username": "{username}", \t"result": "Error", \t"reason": "{reason}" -}}""" +}}""".replace("\t", INDENT) + CSV_HEADER = "site_name,username,result,reason" CSV_TEMPLATE = "{site_name},{username},{result},{reason}" -def identate(msg: str, ident: int): - tabs = "\t" * ident +def indentate(msg: str, indent: int): + if indent <= 0: + return msg + tabs = INDENT * indent return "\n".join([f"{tabs}{line}" for line in msg.split("\n")]) @@ -29,7 +34,7 @@ def __init__(self, output_format: Literal["console", "csv", "json"]) -> None: if not output_format in ["console", "csv", "json"]: raise ValueError(f"Invalid output-format: {output_format}") self.mode: str = output_format - self.ident: int = 0 + self.indent: int = 0 @property def is_console(self) -> bool: @@ -45,16 +50,16 @@ def is_json(self) -> bool: def print_start(self) -> None: if self.is_json: - self.ident += 1 - print(identate("[", self.ident - 1)) + self.indent += 1 + print(indentate("[", self.indent - 1)) elif self.is_csv: print(CSV_HEADER) def print_end(self) -> None: if not self.is_json: return - self.ident = max(self.ident - 1, 0) - print(identate("]", self.ident)) + self.indent = max(self.indent - 1, 0) + print(indentate("]", self.indent)) def get_result_output(self, site_name: str, username: str, result: Result) -> str: if result == None: @@ -65,26 +70,26 @@ def get_result_output(self, site_name: str, username: str, result: Result) -> st match (result.status, self.mode): case (Status.AVAILABLE, "console"): - return f" {Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}" + return f"{INDENT}{Fore.GREEN}[✔] {site_name} ({username}): Available{Style.RESET_ALL}" case (Status.TAKEN, "console"): - return f" {Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}" + return f"{INDENT}{Fore.RED}[✘] {site_name} ({username}): Taken{Style.RESET_ALL}" case (Status.ERROR, "console"): reason = "" if isinstance(result, Result) and result.has_reason(): reason = f" ({result.get_reason()})" - return f" {Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}" + return f"{INDENT}{Fore.YELLOW}[!] {site_name} ({username}): Error{reason}{Style.RESET_ALL}" case (Status.AVAILABLE, "json") | (Status.TAKEN, "json"): - return identate(JSON_TEMPLATE, self.ident).format( + return indentate(JSON_TEMPLATE, self.indent).format( site_name=site_name, username=username, result=str(result.status) ) case (Status.ERROR, "json"): - return identate(JSON_TEMPLATE_ERROR, self.ident).format( + return indentate(JSON_TEMPLATE_ERROR, self.indent).format( site_name=site_name, username=username, reason=result.get_reason() From 1dca09f16623cf369fe5cdcc0f887817ab6e6863 Mon Sep 17 00:00:00 2001 From: Vamato <62358492+VamatoHD@users.noreply.github.com> Date: Sat, 13 Dec 2025 13:28:16 +0000 Subject: [PATCH 5/6] update: README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98c13a7..8605c40 100644 --- a/README.md +++ b/README.md @@ -49,15 +49,20 @@ Optionally, scan a specific category or single module: user-scanner -u -c dev user-scanner -l # Lists all available modules user-scanner -u -m github -user-scanner -u -p +``` +Also, the output format can be specified:
+\* Errors and warnings will only appear when the format is set to "console" +```bash +user-scanner -u -o console #Default format +user-scanner -u -o csv +user-scanner -u -o json ``` Generate multiple username variations by appending a suffix: ```bash user-scanner -u -p - ``` Optionally, scan a specific category or single module with limit: From fa8ae8ef0ed9b7d8be5eb81eaf83732ebe6ba71f Mon Sep 17 00:00:00 2001 From: Vamato <62358492+VamatoHD@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:26:33 +0000 Subject: [PATCH 6/6] feat: formatted list argument --- user_scanner/__main__.py | 18 +++---------- user_scanner/cli/printer.py | 52 ++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/user_scanner/__main__.py b/user_scanner/__main__.py index 2454d03..eeff161 100644 --- a/user_scanner/__main__.py +++ b/user_scanner/__main__.py @@ -8,19 +8,6 @@ MAX_PERMUTATIONS_LIMIT = 100 # To prevent excessive generation -def list_modules(category=None): - categories = load_categories() - categories_to_list = [category] if category else categories.keys() - - for cat_name in categories_to_list: - path = categories[cat_name] - modules = load_modules(path) - print(Fore.MAGENTA + - f"\n== {cat_name.upper()} SITES =={Style.RESET_ALL}") - for module in modules: - site_name = module.__name__.split(".")[-1] - print(f" - {site_name}") - def main(): parser = argparse.ArgumentParser( prog="user-scanner", @@ -60,15 +47,16 @@ def main(): args = parser.parse_args() + Printer = printer.Printer(args.output_format) + if args.list: - list_modules(args.category) + Printer.print_modules(args.category) return if not args.username: parser.print_help() return - Printer = printer.Printer(args.output_format) if Printer.is_console: print_banner() diff --git a/user_scanner/cli/printer.py b/user_scanner/cli/printer.py index 92f08fe..ea641ff 100644 --- a/user_scanner/cli/printer.py +++ b/user_scanner/cli/printer.py @@ -48,18 +48,18 @@ def is_csv(self) -> bool: def is_json(self) -> bool: return self.mode == "json" - def print_start(self) -> None: + def print_start(self, json_char: str = "[") -> None: if self.is_json: self.indent += 1 - print(indentate("[", self.indent - 1)) + print(indentate(json_char, self.indent - 1)) elif self.is_csv: print(CSV_HEADER) - def print_end(self) -> None: + def print_end(self, json_char: str = "]") -> None: if not self.is_json: return self.indent = max(self.indent - 1, 0) - print(indentate("]", self.indent)) + print(indentate(json_char, self.indent)) def get_result_output(self, site_name: str, username: str, result: Result) -> str: if result == None: @@ -104,3 +104,47 @@ def get_result_output(self, site_name: str, username: str, result: Result) -> st ) return "" + + def print_modules(self, category: str | None = None): + from user_scanner.core.orchestrator import load_categories, load_modules + categories = load_categories() + categories_to_list = [category] if category else categories.keys() + + # Print the start + if self.is_json: + self.print_start("{") + elif self.is_csv: + print("category,site_name") + + for i, cat_name in enumerate(categories_to_list): + path = categories[cat_name] + modules = load_modules(path) + + # Print for each category + match self.mode: + case "console": + print(Fore.MAGENTA + + f"\n== {cat_name.upper()} SITES =={Style.RESET_ALL}") + case "json": + self.print_start(f"\"{cat_name}\": [") + + for j, module in enumerate(modules): + is_last = j == len(modules) - 1 + site_name = module.__name__.split(".")[-1].capitalize() + + # Print for each site name + match self.mode: + case "console": + print(f"{INDENT}- {site_name}") + case "json": + msg = f"\"{site_name}\"" + ("" if is_last else ",") + print(indentate(msg, self.indent)) + case "csv": + print(f"{cat_name},{site_name}") + + if self.is_json: + is_last = i == len(categories_to_list) - 1 + self.print_end("]" if is_last else "],") + + if self.is_json: + self.print_end("}")