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 CHANGED
@@ -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,78 +37,108 @@ 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)
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
- with lock:
72
- # Waits for in-order printing
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
- print(output)
77
- print_queue += 1
78
- lock.notify_all()
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
- def run_module_single(module, username):
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
- 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}")
107
+
108
+ results = []
92
109
 
93
- print_queue = 0
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
- 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()
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
- for t in threads:
102
- t.join()
123
+ return results
103
124
 
104
125
 
105
- def run_checks(username):
106
- print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}\n")
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
- for category_path in load_categories().values():
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
- def make_get_request(url: str, **kwargs) -> httpx.Response:
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
- return httpx.get(url, **kwargs)
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 = make_get_request(url, **kwargs)
136
- return func(response)
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`
@@ -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,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
- try:
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
- except Exception as e:
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()
@@ -1,8 +1,19 @@
1
- from user_scanner.core.orchestrator import status_validate
1
+ import re
2
+ from user_scanner.core.orchestrator import status_validate, Result
2
3
 
3
4
 
4
- def validate_itch_io(user):
5
- url = f"https://{user}.itch.io"
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
+
@@ -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()}")
@@ -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
- try:
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
- except Exception as e:
28
- return Result.error(e)
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,4 +1,4 @@
1
1
  {
2
- "version": "1.0.8.1",
2
+ "version": "1.0.9.1",
3
3
  "version_type": "pypi"
4
4
  }
@@ -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.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
  ![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.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
- user-scanner -u <username> -p <suffix>
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=r_8gq8wSRs3U7yUkTAv4e4MPopX0MUf_EbwedBTPs44,5082
3
- user_scanner/version.json,sha256=_1eU-X0UN6AjXfkZNFG3CCPFc6S2N4WQmSrbnp5Dvog,49
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=8dLzV-0zUafZxMLIGAT65Tp4YJtvboelR6eL9M22_4M,6124
11
- user_scanner/core/result.py,sha256=HWz9JjRk74shAdM56ZtdG6hIp4dgcahqYSnpOp3uCak,1933
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=vlXsJfIG_5ShEK2xPLDl2bi6X-d08iTeoqfKeQLaB1g,1363
15
- user_scanner/creator/itch_io.py,sha256=JsFkFzBfJI18DeuSYJIOPGEV_9v7t-jtwmBYCA9W2P8,440
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=km0RMd4cS5sY8IUKDKoNC1oQeCj57ld4HAjOlo1w4ms,1689
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=z-oIqT416ydnZUkq481rz6NTq5yc_BYu-P_Z79uR-Jw,1150
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.8.1.dist-info/entry_points.txt,sha256=XqU3kssYZ0vXaPy5qYUOTCu4u-48Xie7QWFpBCYc7Nc,59
56
- user_scanner-1.0.8.1.dist-info/licenses/LICENSE,sha256=XH1QyQG68zo1opDIZHTHcTAbe9XMzewvTaFTukcN9vc,1061
57
- user_scanner-1.0.8.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
58
- user_scanner-1.0.8.1.dist-info/METADATA,sha256=e14tVl6oz9iSik_h3jmapma4fdyyv3YYdWaYh-82sfY,5153
59
- user_scanner-1.0.8.1.dist-info/RECORD,,
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,,