user-scanner 1.0.10.2__py3-none-any.whl → 1.1.0__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 +241 -129
- user_scanner/core/email_orchestrator.py +78 -0
- user_scanner/core/formatter.py +27 -0
- user_scanner/core/helpers.py +194 -2
- user_scanner/core/orchestrator.py +24 -112
- user_scanner/core/result.py +38 -13
- user_scanner/email_scan/adult/pornhub.py +62 -0
- user_scanner/email_scan/adult/xnxx.py +46 -0
- user_scanner/email_scan/adult/xvideos.py +50 -0
- user_scanner/email_scan/dev/.ruff_cache/.gitignore +2 -0
- user_scanner/email_scan/dev/.ruff_cache/0.14.10/10328336453267387919 +0 -0
- user_scanner/email_scan/dev/.ruff_cache/CACHEDIR.TAG +1 -0
- user_scanner/email_scan/dev/bitbucket.py +33 -0
- user_scanner/email_scan/dev/github.py +72 -0
- user_scanner/email_scan/dev/huggingface.py +37 -0
- user_scanner/email_scan/shopping/__init__.py +0 -0
- user_scanner/email_scan/shopping/ebay.py.lock +97 -0
- user_scanner/email_scan/shopping/flipkart.py +52 -0
- user_scanner/email_scan/social/__init__.py +0 -0
- user_scanner/email_scan/social/facebook.py +96 -0
- user_scanner/email_scan/social/instagram.py +48 -0
- user_scanner/email_scan/social/mastodon.py +57 -0
- user_scanner/email_scan/social/x.py +41 -0
- user_scanner/user_scan/community/lemmy.py +30 -0
- user_scanner/user_scan/creator/__init__.py +0 -0
- user_scanner/user_scan/creator/gumroad.py +22 -0
- user_scanner/{creator → user_scan/creator}/producthunt.py +13 -4
- user_scanner/user_scan/donation/__init__.py +0 -0
- user_scanner/user_scan/gaming/__init__.py +0 -0
- user_scanner/{gaming → user_scan/gaming}/roblox.py +15 -5
- user_scanner/version.json +1 -1
- user_scanner-1.1.0.dist-info/METADATA +239 -0
- user_scanner-1.1.0.dist-info/RECORD +94 -0
- user_scanner/cli/printer.py +0 -117
- user_scanner/config.json +0 -1
- user_scanner-1.0.10.2.dist-info/METADATA +0 -172
- user_scanner-1.0.10.2.dist-info/RECORD +0 -72
- /user_scanner/{creator → email_scan}/__init__.py +0 -0
- /user_scanner/{donation → email_scan/adult}/__init__.py +0 -0
- /user_scanner/{gaming → email_scan/dev}/__init__.py +0 -0
- /user_scanner/{community → user_scan/community}/__init__.py +0 -0
- /user_scanner/{community → user_scan/community}/coderlegion.py +0 -0
- /user_scanner/{community → user_scan/community}/hackernews.py +0 -0
- /user_scanner/{community → user_scan/community}/stackoverflow.py +0 -0
- /user_scanner/{creator → user_scan/creator}/devto.py +0 -0
- /user_scanner/{creator → user_scan/creator}/hashnode.py +0 -0
- /user_scanner/{creator → user_scan/creator}/itch_io.py +0 -0
- /user_scanner/{creator → user_scan/creator}/kaggle.py +0 -0
- /user_scanner/{creator → user_scan/creator}/medium.py +0 -0
- /user_scanner/{creator → user_scan/creator}/patreon.py +0 -0
- /user_scanner/{creator → user_scan/creator}/substack.py +0 -0
- /user_scanner/{creator → user_scan/creator}/twitch.py +0 -0
- /user_scanner/{dev → user_scan/dev}/__init__.py +0 -0
- /user_scanner/{dev → user_scan/dev}/bitbucket.py +0 -0
- /user_scanner/{dev → user_scan/dev}/codeberg.py +0 -0
- /user_scanner/{dev → user_scan/dev}/cratesio.py +0 -0
- /user_scanner/{dev → user_scan/dev}/dockerhub.py +0 -0
- /user_scanner/{dev → user_scan/dev}/github.py +0 -0
- /user_scanner/{dev → user_scan/dev}/gitlab.py +0 -0
- /user_scanner/{dev → user_scan/dev}/huggingface.py +0 -0
- /user_scanner/{dev → user_scan/dev}/launchpad.py +0 -0
- /user_scanner/{dev → user_scan/dev}/leetcode.py +0 -0
- /user_scanner/{dev → user_scan/dev}/npmjs.py +0 -0
- /user_scanner/{dev → user_scan/dev}/replit.py +0 -0
- /user_scanner/{dev → user_scan/dev}/sourceforge.py +0 -0
- /user_scanner/{donation → user_scan/donation}/buymeacoffee.py +0 -0
- /user_scanner/{donation → user_scan/donation}/liberapay.py +0 -0
- /user_scanner/{gaming → user_scan/gaming}/chess_com.py +0 -0
- /user_scanner/{gaming → user_scan/gaming}/lichess.py +0 -0
- /user_scanner/{gaming → user_scan/gaming}/minecraft.py +0 -0
- /user_scanner/{gaming → user_scan/gaming}/monkeytype.py +0 -0
- /user_scanner/{gaming → user_scan/gaming}/osu.py +0 -0
- /user_scanner/{gaming → user_scan/gaming}/steam.py +0 -0
- /user_scanner/{social → user_scan/social}/__init__.py +0 -0
- /user_scanner/{social → user_scan/social}/bluesky.py +0 -0
- /user_scanner/{social → user_scan/social}/discord.py +0 -0
- /user_scanner/{social → user_scan/social}/instagram.py +0 -0
- /user_scanner/{social → user_scan/social}/mastodon.py +0 -0
- /user_scanner/{social → user_scan/social}/pinterest.py +0 -0
- /user_scanner/{social → user_scan/social}/reddit.py +0 -0
- /user_scanner/{social → user_scan/social}/snapchat.py +0 -0
- /user_scanner/{social → user_scan/social}/soundcloud.py +0 -0
- /user_scanner/{social → user_scan/social}/telegram.py +0 -0
- /user_scanner/{social → user_scan/social}/threads.py +0 -0
- /user_scanner/{social → user_scan/social}/tiktok.py +0 -0
- /user_scanner/{social → user_scan/social}/x.py +0 -0
- /user_scanner/{social → user_scan/social}/youtube.py +0 -0
- {user_scanner-1.0.10.2.dist-info → user_scanner-1.1.0.dist-info}/WHEEL +0 -0
- {user_scanner-1.0.10.2.dist-info → user_scanner-1.1.0.dist-info}/entry_points.txt +0 -0
- {user_scanner-1.0.10.2.dist-info → user_scanner-1.1.0.dist-info}/licenses/LICENSE +0 -0
user_scanner/core/helpers.py
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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,
|
|
11
|
-
from
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
114
|
-
for
|
|
115
|
-
result.update(category
|
|
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
|
-
|
|
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
|
|
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
|
|
136
|
-
|
|
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)
|
user_scanner/core/result.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from colorama import Fore, Style
|
|
2
3
|
|
|
3
4
|
DEBUG_MSG = """Result {{
|
|
4
5
|
status: {status},
|
|
@@ -6,6 +7,7 @@ DEBUG_MSG = """Result {{
|
|
|
6
7
|
username: "{username}",
|
|
7
8
|
site_name: "{site_name}",
|
|
8
9
|
category: "{category}",
|
|
10
|
+
is_email: "{is_email}"
|
|
9
11
|
}}"""
|
|
10
12
|
|
|
11
13
|
JSON_TEMPLATE = """{{
|
|
@@ -21,12 +23,10 @@ CSV_TEMPLATE = "{username},{category},{site_name},{status},{reason}"
|
|
|
21
23
|
|
|
22
24
|
def humanize_exception(e: Exception) -> str:
|
|
23
25
|
msg = str(e).lower()
|
|
24
|
-
|
|
25
26
|
if "10054" in msg:
|
|
26
27
|
return "Connection closed by remote server"
|
|
27
28
|
if "11001" in msg:
|
|
28
29
|
return "Could not resolve hostname"
|
|
29
|
-
|
|
30
30
|
return str(e)
|
|
31
31
|
|
|
32
32
|
|
|
@@ -35,22 +35,29 @@ class Status(Enum):
|
|
|
35
35
|
AVAILABLE = 1
|
|
36
36
|
ERROR = 2
|
|
37
37
|
|
|
38
|
+
def to_label(self, is_email=False):
|
|
39
|
+
if self == Status.ERROR:
|
|
40
|
+
return "Error"
|
|
41
|
+
if is_email:
|
|
42
|
+
return "Registered" if self == Status.TAKEN else "Not Registered"
|
|
43
|
+
return "Taken" if self == Status.TAKEN else "Available"
|
|
44
|
+
|
|
38
45
|
def __str__(self):
|
|
39
|
-
return
|
|
46
|
+
return self.to_label(is_email=False)
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class Result:
|
|
43
50
|
def __init__(self, status: Status, reason: str | Exception | None = None, **kwargs):
|
|
44
51
|
self.status = status
|
|
45
52
|
self.reason = reason
|
|
46
|
-
|
|
47
53
|
self.username = None
|
|
48
54
|
self.site_name = None
|
|
49
55
|
self.category = None
|
|
56
|
+
self.is_email = False
|
|
50
57
|
self.update(**kwargs)
|
|
51
58
|
|
|
52
59
|
def update(self, **kwargs):
|
|
53
|
-
for field in ("username", "site_name", "category"):
|
|
60
|
+
for field in ("username", "site_name", "category", "is_email"):
|
|
54
61
|
if field in kwargs and kwargs[field] is not None:
|
|
55
62
|
setattr(self, field, kwargs[field])
|
|
56
63
|
|
|
@@ -72,7 +79,6 @@ class Result:
|
|
|
72
79
|
status = Status(i)
|
|
73
80
|
except ValueError:
|
|
74
81
|
return cls(Status.ERROR, "Invalid status. Please contact maintainers.")
|
|
75
|
-
|
|
76
82
|
return cls(status, reason if status == Status.ERROR else None)
|
|
77
83
|
|
|
78
84
|
def to_number(self) -> int:
|
|
@@ -86,24 +92,27 @@ class Result:
|
|
|
86
92
|
return ""
|
|
87
93
|
if isinstance(self.reason, str):
|
|
88
94
|
return self.reason
|
|
89
|
-
# Format the exception
|
|
90
95
|
msg = humanize_exception(self.reason)
|
|
91
96
|
return f"{type(self.reason).__name__}: {msg.capitalize()}"
|
|
92
97
|
|
|
93
98
|
def as_dict(self) -> dict:
|
|
94
99
|
return {
|
|
95
|
-
"status": self.status,
|
|
100
|
+
"status": self.status.to_label(self.is_email),
|
|
96
101
|
"reason": self.get_reason(),
|
|
97
102
|
"username": self.username,
|
|
98
103
|
"site_name": self.site_name,
|
|
99
|
-
"category": self.category
|
|
104
|
+
"category": self.category,
|
|
105
|
+
"is_email": self.is_email
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
def debug(self) -> str:
|
|
103
109
|
return DEBUG_MSG.format(**self.as_dict())
|
|
104
110
|
|
|
105
111
|
def to_json(self) -> str:
|
|
106
|
-
|
|
112
|
+
msg = JSON_TEMPLATE.format(**self.as_dict())
|
|
113
|
+
if self.is_email:
|
|
114
|
+
msg = msg.replace("\t\"username\":", "\t\"email\":")
|
|
115
|
+
return msg
|
|
107
116
|
|
|
108
117
|
def to_csv(self) -> str:
|
|
109
118
|
return CSV_TEMPLATE.format(**self.as_dict())
|
|
@@ -114,12 +123,28 @@ class Result:
|
|
|
114
123
|
def __eq__(self, other):
|
|
115
124
|
if isinstance(other, Status):
|
|
116
125
|
return self.status == other
|
|
117
|
-
|
|
118
126
|
if isinstance(other, Result):
|
|
119
127
|
return self.status == other.status
|
|
120
|
-
|
|
121
128
|
if isinstance(other, int):
|
|
122
129
|
return self.to_number() == other
|
|
123
|
-
|
|
124
130
|
return NotImplemented
|
|
125
131
|
|
|
132
|
+
def get_console_output(self) -> str:
|
|
133
|
+
site_name = self.site_name
|
|
134
|
+
username = self.username
|
|
135
|
+
status_text = self.status.to_label(self.is_email)
|
|
136
|
+
|
|
137
|
+
if self.is_email:
|
|
138
|
+
color = Fore.GREEN if self == Status.TAKEN else Fore.RED
|
|
139
|
+
icon = "[✔]" if self == Status.TAKEN else "[✘]"
|
|
140
|
+
else:
|
|
141
|
+
color = Fore.GREEN if self == Status.AVAILABLE else Fore.RED
|
|
142
|
+
icon = "[✔]" if self == Status.AVAILABLE else "[✘]"
|
|
143
|
+
|
|
144
|
+
if self == Status.AVAILABLE or self == Status.TAKEN:
|
|
145
|
+
return f" {color}{icon} {site_name} ({username}): {status_text}{Style.RESET_ALL}"
|
|
146
|
+
elif self == Status.ERROR:
|
|
147
|
+
reason = f" ({self.get_reason()})" if self.has_reason() else ""
|
|
148
|
+
return f" {Fore.YELLOW}[!] {site_name} ({username}): {status_text}{reason}{Style.RESET_ALL}"
|
|
149
|
+
|
|
150
|
+
return ""
|
|
@@ -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=10) 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)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from user_scanner.core.result import Result
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async def _check(email: str) -> Result:
|
|
6
|
+
url = "https://www.xvideos.com/account/checkemail"
|
|
7
|
+
params = {'email': email}
|
|
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
|
+
'Accept': "application/json, text/javascript, */*; q=0.01",
|
|
12
|
+
'Accept-Encoding': "identity",
|
|
13
|
+
'X-Requested-With': "XMLHttpRequest",
|
|
14
|
+
'sec-ch-ua-platform': "\"Android\"",
|
|
15
|
+
'sec-ch-ua': "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
|
|
16
|
+
'sec-ch-ua-mobile': "?1",
|
|
17
|
+
'Sec-Fetch-Site': "same-origin",
|
|
18
|
+
'Sec-Fetch-Mode': "cors",
|
|
19
|
+
'Sec-Fetch-Dest': "empty",
|
|
20
|
+
'Referer': "https://www.xvideos.com/",
|
|
21
|
+
'Accept-Language': "en-US,en;q=0.9"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async with httpx.AsyncClient(http2=True, timeout=3) as client:
|
|
25
|
+
try:
|
|
26
|
+
response = await client.get(url, params=params, headers=headers)
|
|
27
|
+
|
|
28
|
+
if response.status_code == 429:
|
|
29
|
+
return Result.error("Rate limited, wait for a few minutes")
|
|
30
|
+
|
|
31
|
+
if response.status_code != 200:
|
|
32
|
+
return Result.error(f"HTTP Error: {response.status_code}")
|
|
33
|
+
|
|
34
|
+
data = response.json()
|
|
35
|
+
|
|
36
|
+
exists_bool = data.get("result")
|
|
37
|
+
|
|
38
|
+
if exists_bool is True:
|
|
39
|
+
return Result.available()
|
|
40
|
+
elif exists_bool is False:
|
|
41
|
+
return Result.taken()
|
|
42
|
+
else:
|
|
43
|
+
return Result.error("Unexpected error, report it via GitHub issues")
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return Result.error(e)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def validate_xvideos(email: str) -> Result:
|
|
50
|
+
return await _check(email)
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Signature: 8a477f597d28d172789f06886806bc55
|