user-scanner 1.0.10.3__py3-none-any.whl → 1.1.0.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.
Files changed (92) hide show
  1. user_scanner/__main__.py +241 -129
  2. user_scanner/core/email_orchestrator.py +78 -0
  3. user_scanner/core/formatter.py +27 -0
  4. user_scanner/core/helpers.py +194 -2
  5. user_scanner/core/orchestrator.py +24 -112
  6. user_scanner/core/result.py +51 -18
  7. user_scanner/email_scan/adult/pornhub.py +62 -0
  8. user_scanner/email_scan/adult/xnxx.py +46 -0
  9. user_scanner/email_scan/adult/xvideos.py +50 -0
  10. user_scanner/email_scan/community/stackoverflow.py +40 -0
  11. user_scanner/email_scan/creator/__init__.py +0 -0
  12. user_scanner/email_scan/creator/gumroad.py +82 -0
  13. user_scanner/email_scan/creator/patreon.py +58 -0
  14. user_scanner/email_scan/dev/__init__.py +0 -0
  15. user_scanner/email_scan/dev/bitbucket.py +33 -0
  16. user_scanner/email_scan/dev/github.py +72 -0
  17. user_scanner/email_scan/dev/huggingface.py +37 -0
  18. user_scanner/email_scan/gaming/__init__.py +0 -0
  19. user_scanner/email_scan/gaming/chess_com.py +47 -0
  20. user_scanner/email_scan/shopping/__init__.py +0 -0
  21. user_scanner/email_scan/shopping/flipkart.py +52 -0
  22. user_scanner/email_scan/social/__init__.py +0 -0
  23. user_scanner/email_scan/social/facebook.py +96 -0
  24. user_scanner/email_scan/social/instagram.py +48 -0
  25. user_scanner/email_scan/social/mastodon.py +57 -0
  26. user_scanner/email_scan/social/x.py +41 -0
  27. user_scanner/user_scan/community/lemmy.py +30 -0
  28. user_scanner/user_scan/creator/__init__.py +0 -0
  29. user_scanner/user_scan/creator/gumroad.py +22 -0
  30. user_scanner/user_scan/donation/__init__.py +0 -0
  31. user_scanner/user_scan/gaming/__init__.py +0 -0
  32. user_scanner/{gaming → user_scan/gaming}/roblox.py +15 -5
  33. user_scanner/version.json +1 -1
  34. user_scanner-1.1.0.1.dist-info/METADATA +239 -0
  35. user_scanner-1.1.0.1.dist-info/RECORD +98 -0
  36. user_scanner/cli/printer.py +0 -117
  37. user_scanner-1.0.10.3.dist-info/METADATA +0 -172
  38. user_scanner-1.0.10.3.dist-info/RECORD +0 -72
  39. /user_scanner/{creator → email_scan}/__init__.py +0 -0
  40. /user_scanner/{donation → email_scan/adult}/__init__.py +0 -0
  41. /user_scanner/{gaming → email_scan/community}/__init__.py +0 -0
  42. /user_scanner/{community → user_scan/community}/__init__.py +0 -0
  43. /user_scanner/{community → user_scan/community}/coderlegion.py +0 -0
  44. /user_scanner/{community → user_scan/community}/hackernews.py +0 -0
  45. /user_scanner/{community → user_scan/community}/stackoverflow.py +0 -0
  46. /user_scanner/{creator → user_scan/creator}/devto.py +0 -0
  47. /user_scanner/{creator → user_scan/creator}/hashnode.py +0 -0
  48. /user_scanner/{creator → user_scan/creator}/itch_io.py +0 -0
  49. /user_scanner/{creator → user_scan/creator}/kaggle.py +0 -0
  50. /user_scanner/{creator → user_scan/creator}/medium.py +0 -0
  51. /user_scanner/{creator → user_scan/creator}/patreon.py +0 -0
  52. /user_scanner/{creator → user_scan/creator}/producthunt.py +0 -0
  53. /user_scanner/{creator → user_scan/creator}/substack.py +0 -0
  54. /user_scanner/{creator → user_scan/creator}/twitch.py +0 -0
  55. /user_scanner/{dev → user_scan/dev}/__init__.py +0 -0
  56. /user_scanner/{dev → user_scan/dev}/bitbucket.py +0 -0
  57. /user_scanner/{dev → user_scan/dev}/codeberg.py +0 -0
  58. /user_scanner/{dev → user_scan/dev}/cratesio.py +0 -0
  59. /user_scanner/{dev → user_scan/dev}/dockerhub.py +0 -0
  60. /user_scanner/{dev → user_scan/dev}/github.py +0 -0
  61. /user_scanner/{dev → user_scan/dev}/gitlab.py +0 -0
  62. /user_scanner/{dev → user_scan/dev}/huggingface.py +0 -0
  63. /user_scanner/{dev → user_scan/dev}/launchpad.py +0 -0
  64. /user_scanner/{dev → user_scan/dev}/leetcode.py +0 -0
  65. /user_scanner/{dev → user_scan/dev}/npmjs.py +0 -0
  66. /user_scanner/{dev → user_scan/dev}/replit.py +0 -0
  67. /user_scanner/{dev → user_scan/dev}/sourceforge.py +0 -0
  68. /user_scanner/{donation → user_scan/donation}/buymeacoffee.py +0 -0
  69. /user_scanner/{donation → user_scan/donation}/liberapay.py +0 -0
  70. /user_scanner/{gaming → user_scan/gaming}/chess_com.py +0 -0
  71. /user_scanner/{gaming → user_scan/gaming}/lichess.py +0 -0
  72. /user_scanner/{gaming → user_scan/gaming}/minecraft.py +0 -0
  73. /user_scanner/{gaming → user_scan/gaming}/monkeytype.py +0 -0
  74. /user_scanner/{gaming → user_scan/gaming}/osu.py +0 -0
  75. /user_scanner/{gaming → user_scan/gaming}/steam.py +0 -0
  76. /user_scanner/{social → user_scan/social}/__init__.py +0 -0
  77. /user_scanner/{social → user_scan/social}/bluesky.py +0 -0
  78. /user_scanner/{social → user_scan/social}/discord.py +0 -0
  79. /user_scanner/{social → user_scan/social}/instagram.py +0 -0
  80. /user_scanner/{social → user_scan/social}/mastodon.py +0 -0
  81. /user_scanner/{social → user_scan/social}/pinterest.py +0 -0
  82. /user_scanner/{social → user_scan/social}/reddit.py +0 -0
  83. /user_scanner/{social → user_scan/social}/snapchat.py +0 -0
  84. /user_scanner/{social → user_scan/social}/soundcloud.py +0 -0
  85. /user_scanner/{social → user_scan/social}/telegram.py +0 -0
  86. /user_scanner/{social → user_scan/social}/threads.py +0 -0
  87. /user_scanner/{social → user_scan/social}/tiktok.py +0 -0
  88. /user_scanner/{social → user_scan/social}/x.py +0 -0
  89. /user_scanner/{social → user_scan/social}/youtube.py +0 -0
  90. {user_scanner-1.0.10.3.dist-info → user_scanner-1.1.0.1.dist-info}/WHEEL +0 -0
  91. {user_scanner-1.0.10.3.dist-info → user_scanner-1.1.0.1.dist-info}/entry_points.txt +0 -0
  92. {user_scanner-1.0.10.3.dist-info → user_scanner-1.1.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,15 @@
1
+ import importlib
2
+ import importlib.util
3
+ from itertools import permutations
4
+ from types import ModuleType
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+ import random
8
+ import threading
9
+ import httpx
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+
12
+
1
13
  def get_site_name(module) -> str:
2
14
  name = module.__name__.split('.')[-1].capitalize().replace("_", ".")
3
15
  if name == "X":
@@ -5,5 +17,185 @@ def get_site_name(module) -> str:
5
17
  return name
6
18
 
7
19
 
8
- def is_last_value(values, i: int) -> bool:
9
- return i == len(values) - 1
20
+ def load_modules(category_path: Path) -> List[ModuleType]:
21
+ modules = []
22
+ for file in category_path.glob("*.py"):
23
+ if file.name == "__init__.py":
24
+ continue
25
+ spec = importlib.util.spec_from_file_location(file.stem, str(file))
26
+ if spec is None or spec.loader is None:
27
+ continue
28
+ module = importlib.util.module_from_spec(spec)
29
+ spec.loader.exec_module(module)
30
+
31
+ modules.append(module)
32
+ return modules
33
+
34
+
35
+ def load_categories(is_email: bool = False) -> Dict[str, Path]:
36
+ folder_name = "email_scan" if is_email else "user_scan"
37
+ root = Path(__file__).resolve().parent.parent / folder_name
38
+ categories = {}
39
+
40
+ for subfolder in root.iterdir():
41
+ if subfolder.is_dir() and \
42
+ subfolder.name.lower() not in ["cli", "utils", "core"] and \
43
+ "__" not in subfolder.name: # Removes __pycache__
44
+ categories[subfolder.name] = subfolder.resolve()
45
+
46
+ return categories
47
+
48
+
49
+ def find_module(name: str, is_email: bool = False) -> List[ModuleType]:
50
+ name = name.lower()
51
+
52
+ return [
53
+ module
54
+ for category_path in load_categories(is_email).values()
55
+ for module in load_modules(category_path)
56
+ if module.__name__.split(".")[-1].lower() == name
57
+ ]
58
+
59
+
60
+ def find_category(module: ModuleType) -> str | None:
61
+
62
+ module_file = getattr(module, '__file__', None)
63
+ if not module_file:
64
+ return None
65
+
66
+ category = Path(module_file).parent.name.lower()
67
+ if category in load_categories(False) or category in load_categories(True):
68
+ return category.capitalize()
69
+
70
+ return None
71
+
72
+
73
+ def generate_permutations(username: str, pattern: str, limit: int | None = None, is_email: bool = False) -> List[str]:
74
+ """
75
+ Generate all order-based permutations of characters in `pattern`
76
+ appended after `username`.
77
+ """
78
+
79
+ if limit and limit <= 0:
80
+ return []
81
+
82
+ permutations_set = {username}
83
+ chars = list(pattern)
84
+
85
+ domain = ""
86
+ if is_email:
87
+ username, domain = username.strip().split("@")
88
+
89
+ # generate permutations of length 1 → len(chars)
90
+ for r in range(len(chars)):
91
+ for combo in permutations(chars, r):
92
+ new = username + ''.join(combo)
93
+ if is_email:
94
+ new += "@" + domain
95
+ permutations_set.add(new)
96
+ if limit and len(permutations_set) >= limit:
97
+ return sorted(permutations_set)
98
+
99
+ return sorted(permutations_set)
100
+
101
+
102
+ def validate_proxies(proxy_list: List[str], timeout: int = 5, max_workers: int = 50) -> List[str]:
103
+ """Validate proxies by testing them against google.com. Returns list of working proxies."""
104
+ working_proxies = []
105
+
106
+ def test_proxy(proxy: str) -> Optional[str]:
107
+ try:
108
+ with httpx.Client(proxy=proxy, timeout=timeout) as client:
109
+ response = client.get("https://www.google.com")
110
+ if response.status_code == 200:
111
+ return proxy
112
+ except Exception:
113
+ pass
114
+ return None
115
+
116
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
117
+ futures = {executor.submit(test_proxy, proxy): proxy for proxy in proxy_list}
118
+ for future in as_completed(futures):
119
+ result = future.result()
120
+ if result:
121
+ working_proxies.append(result)
122
+
123
+ return working_proxies
124
+
125
+
126
+ class ProxyManager:
127
+ """Thread-safe proxy manager that loads and rotates proxies from a file."""
128
+
129
+ def __init__(self, proxy_file: str):
130
+ self.proxies: list[str] = []
131
+ self.current_index = 0
132
+ self.lock = threading.Lock()
133
+ self._load_proxies(proxy_file)
134
+
135
+ def _load_proxies(self, proxy_file: str) -> None:
136
+ """Load proxies from a text file. Supports http://, https://, and socks5:// proxies."""
137
+ try:
138
+ with open(proxy_file, 'r', encoding='utf-8') as f:
139
+ for line in f:
140
+ line = line.strip()
141
+ if line and not line.startswith('#'):
142
+ # Add protocol if not present
143
+ if not line.startswith(('http://', 'https://', 'socks5://')):
144
+ line = 'http://' + line
145
+ self.proxies.append(line)
146
+
147
+ if not self.proxies:
148
+ raise ValueError("No valid proxies found in file")
149
+
150
+ except FileNotFoundError:
151
+ raise FileNotFoundError(f"Proxy file not found: {proxy_file}")
152
+ except Exception as e:
153
+ raise Exception(f"Error loading proxies: {e}")
154
+
155
+ def get_next_proxy(self) -> Optional[str]:
156
+ """Get the next proxy in rotation (round-robin)."""
157
+ if not self.proxies:
158
+ return None
159
+
160
+ with self.lock:
161
+ proxy = self.proxies[self.current_index]
162
+ self.current_index = (self.current_index + 1) % len(self.proxies)
163
+ return proxy
164
+
165
+ def get_random_proxy(self) -> Optional[str]:
166
+ """Get a random proxy from the list."""
167
+ if not self.proxies:
168
+ return None
169
+ return random.choice(self.proxies)
170
+
171
+ def count(self) -> int:
172
+ """Return the number of loaded proxies."""
173
+ return len(self.proxies)
174
+
175
+
176
+ # Global proxy manager instance
177
+ _proxy_manager: Optional[ProxyManager] = None
178
+
179
+
180
+ def set_proxy_manager(proxy_file: Optional[str]) -> None:
181
+ """Initialize the global proxy manager with a proxy file."""
182
+ global _proxy_manager
183
+ if proxy_file:
184
+ _proxy_manager = ProxyManager(proxy_file)
185
+ else:
186
+ _proxy_manager = None
187
+
188
+
189
+ def get_proxy() -> Optional[str]:
190
+ """Get the next proxy from the global proxy manager."""
191
+ if _proxy_manager:
192
+ return _proxy_manager.get_next_proxy()
193
+ return None
194
+
195
+
196
+ def get_proxy_count() -> int:
197
+ """Get the count of loaded proxies."""
198
+ if _proxy_manager:
199
+ return _proxy_manager.count()
200
+ return 0
201
+
@@ -1,72 +1,14 @@
1
- import importlib
2
- import importlib.util
3
1
  from colorama import Fore, Style
4
2
  from concurrent.futures import ThreadPoolExecutor
5
- from itertools import permutations
6
3
  import httpx
7
4
  from pathlib import Path
8
- from user_scanner.cli.printer import Printer
9
5
  from user_scanner.core.result import Result
10
- from typing import Callable, Dict, List
11
- from user_scanner.core.helpers import get_site_name, is_last_value
6
+ from typing import Callable, List
7
+ from types import ModuleType
8
+ from user_scanner.core.helpers import find_category, get_site_name, load_categories, load_modules, get_proxy
12
9
 
13
10
 
14
- def load_modules(category_path: Path):
15
- modules = []
16
- for file in category_path.glob("*.py"):
17
- if file.name == "__init__.py":
18
- continue
19
- spec = importlib.util.spec_from_file_location(file.stem, str(file))
20
- if spec is None or spec.loader is None:
21
- continue
22
- module = importlib.util.module_from_spec(spec)
23
- spec.loader.exec_module(module)
24
-
25
- modules.append(module)
26
- return modules
27
-
28
-
29
- def load_categories() -> Dict[str, Path]:
30
- root = Path(__file__).resolve().parent.parent # Should be user_scanner
31
- categories = {}
32
-
33
- for subfolder in root.iterdir():
34
- if subfolder.is_dir() and \
35
- subfolder.name.lower() not in ["cli", "utils", "core"] and \
36
- "__" not in subfolder.name: # Removes __pycache__
37
- categories[subfolder.name] = subfolder.resolve()
38
-
39
- return categories
40
-
41
-
42
- def find_module(name: str):
43
- name = name.lower()
44
-
45
- matches = [
46
- module
47
- for category_path in load_categories().values()
48
- for module in load_modules(category_path)
49
- if module.__name__.split(".")[-1].lower() == name
50
- ]
51
-
52
- return matches
53
-
54
- def find_category(module) -> str | None:
55
-
56
- module_file = getattr(module, '__file__', None)
57
- if not module_file:
58
- return None
59
-
60
- category = Path(module_file).parent.name.lower()
61
- categories = load_categories()
62
- if category in categories:
63
- return category.capitalize()
64
-
65
- return None
66
-
67
-
68
-
69
- def worker_single(module, username: str) -> Result:
11
+ def _worker_single(module: ModuleType, username: str) -> Result:
70
12
  func = next((getattr(module, f) for f in dir(module)
71
13
  if f.startswith("validate_") and callable(getattr(module, f))), None)
72
14
 
@@ -81,60 +23,43 @@ def worker_single(module, username: str) -> Result:
81
23
  return result
82
24
  except Exception as e:
83
25
  return Result.error(e, site_name=site_name, username=username)
84
-
85
-
86
- def run_module_single(module, username: str, printer: Printer, last: bool = True) -> List[Result]:
87
- result = worker_single(module, username)
26
+
27
+ def run_user_module(module: ModuleType, username: str) -> List[Result]:
28
+ result = _worker_single(module, username)
88
29
 
89
30
  category = find_category(module)
90
31
  if category:
91
32
  result.update(category=category)
92
33
 
93
- get_site_name(module)
94
- msg = printer.get_result_output(result)
95
- if not last and printer.is_json:
96
- msg += ","
97
- print(msg)
34
+ print(result.get_console_output())
98
35
 
99
36
  return [result]
100
37
 
101
38
 
102
-
103
- def run_checks_category(category_path: Path, username: str, printer: Printer, last: bool = True) -> List[Result]:
104
- modules = load_modules(category_path)
105
-
39
+ def run_user_category(category_path: Path, username: str) -> List[Result]:
106
40
  category_name = category_path.stem.capitalize()
107
- if printer.is_console:
108
- print(f"\n{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}")
41
+ print(f"\n{Fore.MAGENTA}== {category_name} SITES =={Style.RESET_ALL}")
109
42
 
110
43
  results = []
44
+ modules = load_modules(category_path)
111
45
 
112
46
  with ThreadPoolExecutor(max_workers=10) as executor:
113
- exec_map = executor.map(lambda m: worker_single(m, username), modules)
114
- for i, result in enumerate(exec_map):
115
- result.update(category = category_name)
47
+ exec_map = executor.map(lambda m: _worker_single(m, username), modules)
48
+ for result in exec_map:
49
+ result.update(category=category_name)
116
50
  results.append(result)
117
51
 
118
- is_last = last and is_last_value(modules, i)
119
- get_site_name(modules[i])
120
- msg = printer.get_result_output(result)
121
- if not is_last and printer.is_json:
122
- msg += ","
123
- print(msg)
52
+ print(result.get_console_output())
124
53
 
125
54
  return results
126
55
 
127
56
 
128
- def run_checks(username: str, printer: Printer, last: bool = True) -> List[Result]:
129
- if printer.is_console:
130
- print(f"\n{Fore.CYAN} Checking username: {username}{Style.RESET_ALL}")
131
-
57
+ def run_user_full(username: str) -> List[Result]:
132
58
  results = []
133
59
 
134
60
  categories = list(load_categories().values())
135
- for i, category_path in enumerate(categories):
136
- last_cat = last and (i == len(categories) - 1)
137
- temp = run_checks_category(category_path, username, printer, last_cat)
61
+ for category_path in categories:
62
+ temp = run_user_category(category_path, username)
138
63
  results.extend(temp)
139
64
 
140
65
  return results
@@ -154,6 +79,12 @@ def make_request(url: str, **kwargs) -> httpx.Response:
154
79
  if "timeout" not in kwargs:
155
80
  kwargs["timeout"] = 5.0
156
81
 
82
+ # Add proxy if available and not already set
83
+ if "proxy" not in kwargs:
84
+ proxy = get_proxy()
85
+ if proxy:
86
+ kwargs["proxy"] = proxy
87
+
157
88
  method = kwargs.pop("method", "GET")
158
89
 
159
90
  return httpx.request(method.upper(), url, **kwargs)
@@ -194,22 +125,3 @@ def status_validate(url: str, available: int | List[int], taken: int | List[int]
194
125
  return Result.error("Status didn't match. Report this on Github.")
195
126
 
196
127
  return generic_validate(url, inner, **kwargs)
197
-
198
-
199
- def generate_permutations(username, pattern, limit=None):
200
- """
201
- Generate all order-based permutations of characters in `pattern`
202
- appended after `username`.
203
- """
204
- permutations_set = {username}
205
-
206
- chars = list(pattern)
207
-
208
- # generate permutations of length 1 → len(chars)
209
- for r in range(1, len(chars) + 1):
210
- for combo in permutations(chars, r):
211
- permutations_set.add(username + ''.join(combo))
212
- if limit and len(permutations_set) >= limit:
213
- return list(permutations_set)[:limit]
214
-
215
- return sorted(permutations_set)
@@ -1,11 +1,14 @@
1
1
  from enum import Enum
2
2
 
3
+ from colorama import Fore, Style
4
+
3
5
  DEBUG_MSG = """Result {{
4
6
  status: {status},
5
7
  reason: "{reason}",
6
8
  username: "{username}",
7
9
  site_name: "{site_name}",
8
10
  category: "{category}",
11
+ is_email: "{is_email}"
9
12
  }}"""
10
13
 
11
14
  JSON_TEMPLATE = """{{
@@ -21,12 +24,10 @@ CSV_TEMPLATE = "{username},{category},{site_name},{status},{reason}"
21
24
 
22
25
  def humanize_exception(e: Exception) -> str:
23
26
  msg = str(e).lower()
24
-
25
27
  if "10054" in msg:
26
28
  return "Connection closed by remote server"
27
29
  if "11001" in msg:
28
30
  return "Could not resolve hostname"
29
-
30
31
  return str(e)
31
32
 
32
33
 
@@ -35,32 +36,39 @@ class Status(Enum):
35
36
  AVAILABLE = 1
36
37
  ERROR = 2
37
38
 
39
+ def to_label(self, is_email=False):
40
+ if self == Status.ERROR:
41
+ return "Error"
42
+ if is_email:
43
+ return "Registered" if self == Status.TAKEN else "Not Registered"
44
+ return "Taken" if self == Status.TAKEN else "Available"
45
+
38
46
  def __str__(self):
39
- return super().__str__().split(".")[1].capitalize()
47
+ return self.to_label(is_email=False)
40
48
 
41
49
 
42
50
  class Result:
43
51
  def __init__(self, status: Status, reason: str | Exception | None = None, **kwargs):
44
52
  self.status = status
45
53
  self.reason = reason
46
-
47
54
  self.username = None
48
55
  self.site_name = None
49
56
  self.category = None
57
+ self.is_email = False
50
58
  self.update(**kwargs)
51
59
 
52
60
  def update(self, **kwargs):
53
- for field in ("username", "site_name", "category"):
61
+ for field in ("username", "site_name", "category", "is_email"):
54
62
  if field in kwargs and kwargs[field] is not None:
55
63
  setattr(self, field, kwargs[field])
56
64
 
57
65
  @classmethod
58
- def taken(cls, **kwargs):
59
- return cls(Status.TAKEN, **kwargs)
66
+ def taken(cls, reason: str | Exception | None = None, **kwargs):
67
+ return cls(Status.TAKEN, reason, **kwargs)
60
68
 
61
69
  @classmethod
62
- def available(cls, **kwargs):
63
- return cls(Status.AVAILABLE, **kwargs)
70
+ def available(cls, reason: str | Exception | None = None, **kwargs):
71
+ return cls(Status.AVAILABLE, reason, **kwargs)
64
72
 
65
73
  @classmethod
66
74
  def error(cls, reason: str | Exception | None = None, **kwargs):
@@ -72,8 +80,7 @@ class Result:
72
80
  status = Status(i)
73
81
  except ValueError:
74
82
  return cls(Status.ERROR, "Invalid status. Please contact maintainers.")
75
-
76
- return cls(status, reason if status == Status.ERROR else None)
83
+ return cls(status, reason)
77
84
 
78
85
  def to_number(self) -> int:
79
86
  return self.status.value
@@ -86,24 +93,27 @@ class Result:
86
93
  return ""
87
94
  if isinstance(self.reason, str):
88
95
  return self.reason
89
- # Format the exception
90
96
  msg = humanize_exception(self.reason)
91
97
  return f"{type(self.reason).__name__}: {msg.capitalize()}"
92
98
 
93
99
  def as_dict(self) -> dict:
94
100
  return {
95
- "status": self.status,
101
+ "status": self.status.to_label(self.is_email),
96
102
  "reason": self.get_reason(),
97
103
  "username": self.username,
98
104
  "site_name": self.site_name,
99
- "category": self.category
105
+ "category": self.category,
106
+ "is_email": self.is_email,
100
107
  }
101
108
 
102
109
  def debug(self) -> str:
103
110
  return DEBUG_MSG.format(**self.as_dict())
104
111
 
105
112
  def to_json(self) -> str:
106
- return JSON_TEMPLATE.format(**self.as_dict())
113
+ msg = JSON_TEMPLATE.format(**self.as_dict())
114
+ if self.is_email:
115
+ msg = msg.replace('\t"username":', '\t"email":')
116
+ return msg
107
117
 
108
118
  def to_csv(self) -> str:
109
119
  return CSV_TEMPLATE.format(**self.as_dict())
@@ -114,12 +124,35 @@ class Result:
114
124
  def __eq__(self, other):
115
125
  if isinstance(other, Status):
116
126
  return self.status == other
117
-
118
127
  if isinstance(other, Result):
119
128
  return self.status == other.status
120
-
121
129
  if isinstance(other, int):
122
130
  return self.to_number() == other
123
-
124
131
  return NotImplemented
125
132
 
133
+ def get_output_color(self) -> str:
134
+ if self == Status.ERROR:
135
+ return Fore.YELLOW
136
+ elif self.is_email:
137
+ return Fore.GREEN if self == Status.TAKEN else Fore.RED
138
+ else:
139
+ return Fore.GREEN if self == Status.AVAILABLE else Fore.RED
140
+
141
+ def get_output_icon(self) -> str:
142
+ if self == Status.ERROR:
143
+ return "[!]"
144
+ elif self.is_email:
145
+ return "[✔]" if self == Status.TAKEN else "[✘]"
146
+ else:
147
+ return "[✔]" if self == Status.AVAILABLE else "[✘]"
148
+
149
+ def get_console_output(self) -> str:
150
+ site_name = self.site_name
151
+ username = self.username
152
+ status_text = self.status.to_label(self.is_email)
153
+
154
+ color = self.get_output_color()
155
+ icon = self.get_output_icon()
156
+
157
+ reason = f" ({self.get_reason()})" if self.has_reason() else ""
158
+ return f" {color}{icon} {site_name} ({username}): {status_text}{reason}{Style.RESET_ALL}"
@@ -0,0 +1,62 @@
1
+ import httpx
2
+ import re
3
+ from user_scanner.core.result import Result
4
+
5
+ async def _check(email: str) -> Result:
6
+ base_url = "https://www.pornhub.com"
7
+ check_api = f"{base_url}/api/v1/user/create_account_check"
8
+
9
+ headers = {
10
+ "user-agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36",
11
+ "x-requested-with": "XMLHttpRequest",
12
+ "origin": base_url,
13
+ "referer": base_url + "/",
14
+ "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
15
+ }
16
+
17
+ async with httpx.AsyncClient(http2=True, follow_redirects=True, timeout=3) as client:
18
+ try:
19
+ landing_resp = await client.get(base_url, headers=headers)
20
+ token_match = re.search(r'var\s+token\s*=\s*"([^"]+)"', landing_resp.text)
21
+
22
+ if not token_match:
23
+ return Result.error("Failed to extract dynamic token from HTML")
24
+
25
+ token = token_match.group(1)
26
+
27
+ params = {"token": token}
28
+ payload = {
29
+ "check_what": "email",
30
+ "email": email
31
+ }
32
+
33
+ response = await client.post(
34
+ check_api,
35
+ params=params,
36
+ headers=headers,
37
+ data=payload
38
+ )
39
+
40
+ if response.status_code == 429:
41
+ return Result.error("Rate limited, wait for a few minutes")
42
+
43
+ if response.status_code != 200:
44
+ return Result.error(f"HTTP Error: {response.status_code}")
45
+
46
+ data = response.json()
47
+ status = data.get("email")
48
+ error_msg = data.get("error_message", "")
49
+
50
+ if status == "create_account_passed":
51
+ return Result.available()
52
+ elif "already in use" in error_msg.lower() or status != "create_account_passed":
53
+ return Result.taken()
54
+ else:
55
+ return Result.error(f"Unexpected API response: {status}")
56
+
57
+ except Exception as e:
58
+ return Result.error(e)
59
+
60
+
61
+ async def validate_pornhub(email: str) -> Result:
62
+ return await _check(email)
@@ -0,0 +1,46 @@
1
+ import httpx
2
+ from user_scanner.core.result import Result
3
+
4
+ async def _check(email: str) -> Result:
5
+ url = "https://www.xnxx.com/account/checkemail"
6
+ params = {'email': email}
7
+ headers = {
8
+ 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36",
9
+ 'Accept': "application/json, text/javascript, */*; q=0.01",
10
+ 'Accept-Encoding': "identity",
11
+ 'X-Requested-With': "XMLHttpRequest",
12
+ 'sec-ch-ua-platform': '"Android"',
13
+ 'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
14
+ 'sec-ch-ua-mobile': "?1",
15
+ 'Sec-Fetch-Site': "same-origin",
16
+ 'Sec-Fetch-Mode': "cors",
17
+ 'Sec-Fetch-Dest': "empty",
18
+ 'Referer': "https://www.xnxx.com/",
19
+ 'Accept-Language': "en-US,en;q=0.9"
20
+ }
21
+
22
+ async with httpx.AsyncClient(http2=True) as client:
23
+ try:
24
+ response = await client.get(url, params=params, headers=headers)
25
+
26
+ if response.status_code == 429:
27
+ return Result.error("Rate limited wait for few minutes")
28
+
29
+ if response.status_code != 200:
30
+ return Result.error(f"HTTP Error: {response.status_code}")
31
+
32
+ data = response.json()
33
+ exists_bool = data.get("result")
34
+
35
+ if exists_bool is True:
36
+ return Result.available()
37
+ elif exists_bool is False:
38
+ return Result.taken()
39
+ else:
40
+ return Result.error("Unexpected error, report it via GitHub issues")
41
+
42
+ except Exception as e:
43
+ return Result.error(e)
44
+
45
+ async def validate_xnxx(email: str) -> Result:
46
+ return await _check(email)