@fudrouter/fsrouter 0.6.80 → 0.6.82
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.
- package/dist/open-sse/providers/registry/nous-research.js +279 -0
- package/open-sse/providers/registry/nous-research.js +279 -0
- package/package.json +1 -1
- package/src/automation/cf_token_via_session.py +158 -0
- package/src/automation/cloudflare_signup.py +3689 -0
- package/src/automation/codebuddy_signup.py +636 -0
- package/src/automation/flashloop_signup.py +338 -0
- package/src/automation/kimi_signup.py +401 -0
- package/src/automation/leonardo_signup.py +902 -0
- package/src/automation/openvecta_signup.py +652 -0
- package/src/automation/qoder_signup.py +323 -0
- package/src/automation/test_proxy.py +86 -0
- package/src/automation/weavy_signup.py +1964 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OpenVecta account auto-signup via Camoufox (anti-fingerprint) + Fsmail email verification.
|
|
3
|
+
|
|
4
|
+
Flow:
|
|
5
|
+
1. Navigate to openvecta.com → click "Get started"
|
|
6
|
+
2. Privy modal opens → enter email → receive verification code via Fsmail
|
|
7
|
+
3. Enter verification code → account created (Privy auto-creates Solana wallet)
|
|
8
|
+
4. Navigate to dashboard → API keys tab → create new key
|
|
9
|
+
5. Copy the ov_sk_live_... key
|
|
10
|
+
|
|
11
|
+
Outputs JSON lines to stdout:
|
|
12
|
+
{"step": "..."} — progress update
|
|
13
|
+
{"status": "success", "api_key": "ov_sk_live_...", "email": "..."} — final result
|
|
14
|
+
{"status": "error", "error": "..."} — failure
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import json
|
|
19
|
+
import argparse
|
|
20
|
+
import time
|
|
21
|
+
import random
|
|
22
|
+
import string
|
|
23
|
+
import re
|
|
24
|
+
import urllib.request
|
|
25
|
+
import urllib.parse
|
|
26
|
+
import urllib.error
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Stdout JSON helpers ────────────────────────────────────────────────────────
|
|
31
|
+
def emit(obj):
|
|
32
|
+
print(json.dumps(obj), flush=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def log_step(msg):
|
|
36
|
+
emit({"step": msg})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def success(api_key, email):
|
|
40
|
+
emit({"status": "success", "api_key": api_key, "email": email})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def die(msg):
|
|
44
|
+
emit({"status": "error", "error": msg})
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Fsmail helpers ─────────────────────────────────────────────────────────────
|
|
49
|
+
def fsmail_request(base_url, api_key, path, method="GET", data=None, host_header=None):
|
|
50
|
+
url = base_url.rstrip("/") + "/api" + path
|
|
51
|
+
req = urllib.request.Request(url, method=method)
|
|
52
|
+
req.add_header("Authorization", f"Bearer {api_key}")
|
|
53
|
+
req.add_header("X-API-Key", api_key)
|
|
54
|
+
req.add_header("Content-Type", "application/json")
|
|
55
|
+
req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
|
|
56
|
+
req.add_header("Accept", "application/json, */*")
|
|
57
|
+
if host_header:
|
|
58
|
+
req.add_header("Host", host_header)
|
|
59
|
+
elif "localhost" in base_url or "127.0.0.1" in base_url:
|
|
60
|
+
req.add_header("Host", "fsmail.klipers.site")
|
|
61
|
+
if data:
|
|
62
|
+
req.data = json.dumps(data).encode()
|
|
63
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
64
|
+
return json.loads(resp.read())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_fsmail_inbox(base_url, api_key, email):
|
|
68
|
+
"""Create inbox by splitting email into alias + domain."""
|
|
69
|
+
try:
|
|
70
|
+
alias, domain = email.split("@", 1)
|
|
71
|
+
fsmail_request(base_url, api_key, "/inboxes", method="POST",
|
|
72
|
+
data={"alias": alias, "domain": domain})
|
|
73
|
+
except Exception:
|
|
74
|
+
pass # might already exist
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def wait_for_openvecta_verify_email(base_url, api_key, email, timeout=300):
|
|
78
|
+
"""Wait for OpenVecta/Privy verification email and extract the code or link."""
|
|
79
|
+
log_step(f"Menunggu email verifikasi OpenVecta ({email})...")
|
|
80
|
+
alias = email.split("@")[0]
|
|
81
|
+
deadline = time.time() + timeout
|
|
82
|
+
seen_ids = set()
|
|
83
|
+
while time.time() < deadline:
|
|
84
|
+
try:
|
|
85
|
+
data = fsmail_request(base_url, api_key, f"/inboxes/{urllib.parse.quote(alias)}/messages")
|
|
86
|
+
messages = data.get("messages", [])
|
|
87
|
+
for msg in messages:
|
|
88
|
+
msg_id = msg.get("id", "")
|
|
89
|
+
subject = msg.get("subject", "")
|
|
90
|
+
if msg_id in seen_ids:
|
|
91
|
+
continue
|
|
92
|
+
seen_ids.add(msg_id)
|
|
93
|
+
subj_lower = subject.lower()
|
|
94
|
+
is_ov_email = (
|
|
95
|
+
"openvecta" in subj_lower or
|
|
96
|
+
"privy" in subj_lower or
|
|
97
|
+
"verify" in subj_lower or
|
|
98
|
+
"confirm" in subj_lower or
|
|
99
|
+
"code" in subj_lower or
|
|
100
|
+
"login" in subj_lower or
|
|
101
|
+
"sign in" in subj_lower or
|
|
102
|
+
"magic" in subj_lower or
|
|
103
|
+
"verification" in subj_lower
|
|
104
|
+
)
|
|
105
|
+
if is_ov_email:
|
|
106
|
+
# Fetch full message body
|
|
107
|
+
try:
|
|
108
|
+
full = fsmail_request(base_url, api_key, f"/messages/{urllib.parse.quote(msg_id)}")
|
|
109
|
+
msg_body = full.get("message", full)
|
|
110
|
+
body = msg_body.get("body", msg_body.get("html", msg_body.get("text", "")))
|
|
111
|
+
except Exception:
|
|
112
|
+
body = msg.get("snippet", "")
|
|
113
|
+
|
|
114
|
+
# Look for verification link
|
|
115
|
+
link_patterns = [
|
|
116
|
+
r'https://auth\.privy\.io[^\s\'"<>]+',
|
|
117
|
+
r'https://openvecta\.com[^\s\'"<>]*verify[^\s\'"<>]*',
|
|
118
|
+
r'https://openvecta\.com[^\s\'"<>]*confirm[^\s\'"<>]*',
|
|
119
|
+
r'https://openvecta\.com[^\s\'"<>]*auth[^\s\'"<>]*',
|
|
120
|
+
r'https://[^\s\'"<>]*privy[^\s\'"<>]*',
|
|
121
|
+
]
|
|
122
|
+
for pat in link_patterns:
|
|
123
|
+
links = re.findall(pat, body)
|
|
124
|
+
if links:
|
|
125
|
+
link = links[0].rstrip(".")
|
|
126
|
+
log_step(f"Link verifikasi ditemukan!")
|
|
127
|
+
return {"type": "link", "value": link}
|
|
128
|
+
|
|
129
|
+
# Look for verification code (typically 6 digits)
|
|
130
|
+
code_patterns = [
|
|
131
|
+
r'(?:code|otp|pin|verification)[:\s]*(\d{4,8})',
|
|
132
|
+
r'(\d{6})',
|
|
133
|
+
r'<strong[^>]*>(\d{4,8})</strong>',
|
|
134
|
+
r'<span[^>]*>(\d{6})</span>',
|
|
135
|
+
r'<h1[^>]*>(\d{6})</h1>',
|
|
136
|
+
r'<p[^>]*>\s*(\d{6})\s*</p>',
|
|
137
|
+
]
|
|
138
|
+
for pat in code_patterns:
|
|
139
|
+
codes = re.findall(pat, body, re.IGNORECASE)
|
|
140
|
+
if codes:
|
|
141
|
+
code = codes[0].strip()
|
|
142
|
+
if len(code) >= 4:
|
|
143
|
+
log_step(f"Kode verifikasi ditemukan: {code}")
|
|
144
|
+
return {"type": "code", "value": code}
|
|
145
|
+
|
|
146
|
+
# If we found the email but no link/code, return the body for debugging
|
|
147
|
+
log_step(f"Email ditemukan tapi tidak ada link/kode yang bisa diekstrak")
|
|
148
|
+
return {"type": "unknown", "value": body[:500]}
|
|
149
|
+
except Exception as e:
|
|
150
|
+
log_step(f"Fsmail poll error: {e}")
|
|
151
|
+
time.sleep(5)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Human-like typing ──────────────────────────────────────────────────────────
|
|
156
|
+
def human_type(page, selector, text, delay_min=50, delay_max=150):
|
|
157
|
+
"""Type text with human-like delays."""
|
|
158
|
+
el = page.locator(selector).first
|
|
159
|
+
el.click()
|
|
160
|
+
time.sleep(0.3)
|
|
161
|
+
for char in text:
|
|
162
|
+
el.type(char, delay=random.randint(delay_min, delay_max))
|
|
163
|
+
time.sleep(0.5)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def safe_click(page, selector, timeout=10000):
|
|
167
|
+
"""Click element with retry."""
|
|
168
|
+
try:
|
|
169
|
+
el = page.locator(selector).first
|
|
170
|
+
el.wait_for(state="visible", timeout=timeout)
|
|
171
|
+
el.click()
|
|
172
|
+
time.sleep(1)
|
|
173
|
+
return True
|
|
174
|
+
except Exception as e:
|
|
175
|
+
log_step(f"Click failed ({selector}): {e}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def wait_for_any(page, selectors, timeout=15000):
|
|
180
|
+
"""Wait for any of the selectors to appear, return the first matching selector."""
|
|
181
|
+
deadline = time.time() + timeout / 1000
|
|
182
|
+
while time.time() < deadline:
|
|
183
|
+
for sel in selectors:
|
|
184
|
+
try:
|
|
185
|
+
el = page.locator(sel).first
|
|
186
|
+
if el.count() > 0 and el.is_visible(timeout=500):
|
|
187
|
+
return sel
|
|
188
|
+
except Exception:
|
|
189
|
+
continue
|
|
190
|
+
time.sleep(0.5)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── Main ───────────────────────────────────────────────────────────────────────
|
|
195
|
+
def main():
|
|
196
|
+
parser = argparse.ArgumentParser()
|
|
197
|
+
parser.add_argument("--email", required=True)
|
|
198
|
+
parser.add_argument("--password", required=True, help="Used as placeholder, not for OpenVecta login")
|
|
199
|
+
parser.add_argument("--fsmail-base-url", default="")
|
|
200
|
+
parser.add_argument("--fsmail-api-key", default="")
|
|
201
|
+
parser.add_argument("--fsmail-domain", default="")
|
|
202
|
+
parser.add_argument("--profiles-dir", default="profiles/openvecta")
|
|
203
|
+
parser.add_argument("--headless", action="store_true")
|
|
204
|
+
parser.add_argument("--proxy-server")
|
|
205
|
+
parser.add_argument("--proxy-user")
|
|
206
|
+
parser.add_argument("--proxy-pass")
|
|
207
|
+
parser.add_argument("--stagger-delay", type=int, default=0)
|
|
208
|
+
args = parser.parse_args()
|
|
209
|
+
|
|
210
|
+
email = args.email
|
|
211
|
+
|
|
212
|
+
# Import Camoufox
|
|
213
|
+
try:
|
|
214
|
+
from camoufox.sync_api import Camoufox
|
|
215
|
+
except ImportError:
|
|
216
|
+
die("Camoufox tidak terinstall. Jalankan: pip install camoufox && python -m camoufox fetch")
|
|
217
|
+
|
|
218
|
+
profiles_dir = Path(args.profiles_dir)
|
|
219
|
+
profiles_dir.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
|
|
221
|
+
# Pre-create Fsmail inbox if we have credentials
|
|
222
|
+
fsmail_ok = bool(args.fsmail_base_url and args.fsmail_api_key and args.fsmail_domain)
|
|
223
|
+
if fsmail_ok:
|
|
224
|
+
log_step(f"Membuat inbox Fsmail untuk {email}...")
|
|
225
|
+
try:
|
|
226
|
+
create_fsmail_inbox(args.fsmail_base_url, args.fsmail_api_key, email)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
log_step(f"Fsmail inbox warning: {e}")
|
|
229
|
+
|
|
230
|
+
log_step("Meluncurkan browser Camoufox (anti-fingerprint)...")
|
|
231
|
+
|
|
232
|
+
if args.stagger_delay > 0:
|
|
233
|
+
log_step(f"Stagger delay {args.stagger_delay}s...")
|
|
234
|
+
time.sleep(args.stagger_delay)
|
|
235
|
+
|
|
236
|
+
proxy_dict = None
|
|
237
|
+
if args.proxy_server:
|
|
238
|
+
proxy_dict = {"server": args.proxy_server}
|
|
239
|
+
if args.proxy_user:
|
|
240
|
+
proxy_dict["username"] = args.proxy_user
|
|
241
|
+
if args.proxy_pass:
|
|
242
|
+
proxy_dict["password"] = args.proxy_pass
|
|
243
|
+
|
|
244
|
+
launch_kwargs = dict(
|
|
245
|
+
headless=args.headless,
|
|
246
|
+
os="windows",
|
|
247
|
+
locale="en-US",
|
|
248
|
+
)
|
|
249
|
+
if proxy_dict:
|
|
250
|
+
launch_kwargs["proxy"] = proxy_dict
|
|
251
|
+
launch_kwargs["geoip"] = True
|
|
252
|
+
|
|
253
|
+
def _make_camoufox(kw):
|
|
254
|
+
"""Launch Camoufox, stripping unsupported kwargs one by one."""
|
|
255
|
+
try:
|
|
256
|
+
return Camoufox(**kw)
|
|
257
|
+
except TypeError:
|
|
258
|
+
kw.pop("os", None)
|
|
259
|
+
try:
|
|
260
|
+
return Camoufox(**kw)
|
|
261
|
+
except TypeError:
|
|
262
|
+
kw.pop("locale", None)
|
|
263
|
+
return Camoufox(**kw)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
browser_ctx = _make_camoufox(dict(launch_kwargs))
|
|
267
|
+
except Exception as _pe:
|
|
268
|
+
_ps = str(_pe)
|
|
269
|
+
if proxy_dict and any(k in _ps for k in ("InvalidProxy", "Tunnel connection", "Failed to connect to proxy", "ProxyError")):
|
|
270
|
+
log_step(f"Proxy dead ({proxy_dict.get('server', '?')}) — fallback tanpa proxy")
|
|
271
|
+
launch_kwargs.pop("proxy", None)
|
|
272
|
+
launch_kwargs.pop("geoip", None)
|
|
273
|
+
browser_ctx = _make_camoufox(dict(launch_kwargs))
|
|
274
|
+
else:
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
with browser_ctx as browser:
|
|
278
|
+
page = browser.new_page()
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
# ── Step 1: Navigate to OpenVecta ─────────────────────────────────
|
|
282
|
+
log_step("Membuka openvecta.com...")
|
|
283
|
+
page.goto("https://openvecta.com", wait_until="domcontentloaded", timeout=30000)
|
|
284
|
+
time.sleep(3)
|
|
285
|
+
|
|
286
|
+
# ── Step 2: Click "Get started" to open Privy auth ────────────────
|
|
287
|
+
log_step("Mencari tombol 'Get started'...")
|
|
288
|
+
get_started = page.locator("button:has-text('Get started')").first
|
|
289
|
+
get_started.wait_for(state="visible", timeout=15000)
|
|
290
|
+
get_started.click()
|
|
291
|
+
time.sleep(3)
|
|
292
|
+
|
|
293
|
+
# ── Step 3: Handle Privy auth modal ───────────────────────────────
|
|
294
|
+
log_step("Menunggu modal Privy auth...")
|
|
295
|
+
|
|
296
|
+
# Privy modal may appear as an iframe or overlay
|
|
297
|
+
# Try to find the email input in Privy modal
|
|
298
|
+
privy_email_input = None
|
|
299
|
+
|
|
300
|
+
# Method 1: Look for input in Privy iframe
|
|
301
|
+
for attempt in range(3):
|
|
302
|
+
frames = page.frames
|
|
303
|
+
for frame in frames:
|
|
304
|
+
try:
|
|
305
|
+
url = frame.url or ""
|
|
306
|
+
if "privy" in url:
|
|
307
|
+
log_step(f"Frame Privy ditemukan: {url[:80]}")
|
|
308
|
+
email_input = frame.locator("input[type='email'], input[placeholder*='email' i], input[name='email']").first
|
|
309
|
+
if email_input.count() > 0:
|
|
310
|
+
privy_email_input = email_input
|
|
311
|
+
break
|
|
312
|
+
except Exception:
|
|
313
|
+
continue
|
|
314
|
+
if privy_email_input:
|
|
315
|
+
break
|
|
316
|
+
# Also check main page
|
|
317
|
+
try:
|
|
318
|
+
email_input = page.locator("input[type='email'], input[placeholder*='email' i], input[name='email']").first
|
|
319
|
+
if email_input.count() > 0 and email_input.is_visible(timeout=2000):
|
|
320
|
+
privy_email_input = email_input
|
|
321
|
+
break
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
time.sleep(2)
|
|
325
|
+
|
|
326
|
+
if not privy_email_input:
|
|
327
|
+
# Take screenshot for debugging
|
|
328
|
+
try:
|
|
329
|
+
page.screenshot(path="/tmp/openvecta_no_privy.png")
|
|
330
|
+
log_step("Screenshot: /tmp/openvecta_no_privy.png")
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
die("Tidak dapat menemukan input email Privy. Mungkin UI berubah.")
|
|
334
|
+
|
|
335
|
+
# ── Step 4: Enter email in Privy ──────────────────────────────────
|
|
336
|
+
log_step(f"Memasukkan email: {email}")
|
|
337
|
+
privy_email_input.click()
|
|
338
|
+
time.sleep(0.5)
|
|
339
|
+
privy_email_input.fill("")
|
|
340
|
+
time.sleep(0.3)
|
|
341
|
+
# Type with human-like delays
|
|
342
|
+
for char in email:
|
|
343
|
+
privy_email_input.type(char, delay=random.randint(30, 80))
|
|
344
|
+
time.sleep(1)
|
|
345
|
+
|
|
346
|
+
# Click the submit/continue button
|
|
347
|
+
log_step("Mengklik tombol submit...")
|
|
348
|
+
# Look for submit button in the same context
|
|
349
|
+
submit_clicked = False
|
|
350
|
+
for frame in page.frames:
|
|
351
|
+
try:
|
|
352
|
+
submit_btn = frame.locator("button[type='submit'], button:has-text('Continue'), button:has-text('Sign in'), button:has-text('Log in'), button:has-text('Submit')").first
|
|
353
|
+
if submit_btn.count() > 0 and submit_btn.is_visible(timeout=2000):
|
|
354
|
+
submit_btn.click()
|
|
355
|
+
submit_clicked = True
|
|
356
|
+
break
|
|
357
|
+
except Exception:
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
if not submit_clicked:
|
|
361
|
+
# Try main page
|
|
362
|
+
try:
|
|
363
|
+
submit_btn = page.locator("button[type='submit'], button:has-text('Continue'), button:has-text('Sign in'), button:has-text('Log in')").first
|
|
364
|
+
if submit_btn.count() > 0:
|
|
365
|
+
submit_btn.click()
|
|
366
|
+
submit_clicked = True
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
if not submit_clicked:
|
|
371
|
+
# Try pressing Enter
|
|
372
|
+
page.keyboard.press("Enter")
|
|
373
|
+
|
|
374
|
+
time.sleep(3)
|
|
375
|
+
|
|
376
|
+
# ── Step 5: Wait for verification code/link via Fsmail ────────────
|
|
377
|
+
if not fsmail_ok:
|
|
378
|
+
die("Fsmail credentials not provided. Cannot verify email.")
|
|
379
|
+
|
|
380
|
+
verify_result = wait_for_openvecta_verify_email(
|
|
381
|
+
args.fsmail_base_url, args.fsmail_api_key, email, timeout=300
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if not verify_result:
|
|
385
|
+
try:
|
|
386
|
+
page.screenshot(path="/tmp/openvecta_no_verify.png")
|
|
387
|
+
log_step("Screenshot: /tmp/openvecta_no_verify.png")
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
die("Timeout menunggu email verifikasi OpenVecta.")
|
|
391
|
+
|
|
392
|
+
# ── Step 6: Handle verification ───────────────────────────────────
|
|
393
|
+
if verify_result["type"] == "link":
|
|
394
|
+
log_step("Membuka link verifikasi...")
|
|
395
|
+
page.goto(verify_result["value"], wait_until="domcontentloaded", timeout=30000)
|
|
396
|
+
time.sleep(5)
|
|
397
|
+
elif verify_result["type"] == "code":
|
|
398
|
+
code = verify_result["value"]
|
|
399
|
+
log_step(f"Memasukkan kode verifikasi: {code}")
|
|
400
|
+
# Find code input fields (Privy typically has separate digit inputs or a single input)
|
|
401
|
+
code_entered = False
|
|
402
|
+
|
|
403
|
+
# Method 1: Single code input
|
|
404
|
+
for frame in page.frames:
|
|
405
|
+
try:
|
|
406
|
+
code_input = frame.locator("input[type='text'], input[type='number'], input[type='tel'], input[inputmode='numeric']").first
|
|
407
|
+
if code_input.count() > 0 and code_input.is_visible(timeout=2000):
|
|
408
|
+
code_input.click()
|
|
409
|
+
code_input.fill(code)
|
|
410
|
+
code_entered = True
|
|
411
|
+
break
|
|
412
|
+
except Exception:
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
# Method 2: Multiple single-digit inputs (OTP style)
|
|
416
|
+
if not code_entered:
|
|
417
|
+
for frame in page.frames:
|
|
418
|
+
try:
|
|
419
|
+
digit_inputs = frame.locator("input[maxlength='1']")
|
|
420
|
+
count = digit_inputs.count()
|
|
421
|
+
if count >= 4:
|
|
422
|
+
for i, digit in enumerate(code[:count]):
|
|
423
|
+
if i < count:
|
|
424
|
+
digit_inputs.nth(i).fill(digit)
|
|
425
|
+
time.sleep(0.1)
|
|
426
|
+
code_entered = True
|
|
427
|
+
break
|
|
428
|
+
except Exception:
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
if not code_entered:
|
|
432
|
+
# Try main page
|
|
433
|
+
try:
|
|
434
|
+
code_input = page.locator("input[type='text'], input[type='number']").first
|
|
435
|
+
if code_input.count() > 0:
|
|
436
|
+
code_input.fill(code)
|
|
437
|
+
code_entered = True
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
time.sleep(3)
|
|
442
|
+
|
|
443
|
+
# Click verify/submit button
|
|
444
|
+
for frame in page.frames:
|
|
445
|
+
try:
|
|
446
|
+
verify_btn = frame.locator("button:has-text('Verify'), button:has-text('Confirm'), button:has-text('Submit'), button[type='submit']").first
|
|
447
|
+
if verify_btn.count() > 0 and verify_btn.is_visible(timeout=2000):
|
|
448
|
+
verify_btn.click()
|
|
449
|
+
break
|
|
450
|
+
except Exception:
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
time.sleep(5)
|
|
454
|
+
else:
|
|
455
|
+
log_step(f"Email ditemukan tapi format tidak dikenali: {str(verify_result['value'])[:200]}")
|
|
456
|
+
|
|
457
|
+
# ── Step 7: Wait for dashboard to load ────────────────────────────
|
|
458
|
+
log_step("Menunggu dashboard OpenVecta...")
|
|
459
|
+
time.sleep(5)
|
|
460
|
+
|
|
461
|
+
# Check if we need to navigate to dashboard
|
|
462
|
+
current_url = page.url
|
|
463
|
+
if "dashboard" not in current_url:
|
|
464
|
+
log_step("Navigasi ke dashboard...")
|
|
465
|
+
page.goto("https://openvecta.com/dashboard", wait_until="domcontentloaded", timeout=30000)
|
|
466
|
+
time.sleep(5)
|
|
467
|
+
|
|
468
|
+
# ── Step 8: Find API keys tab and create key ──────────────────────
|
|
469
|
+
log_step("Mencari tab API keys...")
|
|
470
|
+
|
|
471
|
+
# Look for API keys tab/button
|
|
472
|
+
api_keys_clicked = False
|
|
473
|
+
for selector in [
|
|
474
|
+
"text=API keys",
|
|
475
|
+
"text=API Keys",
|
|
476
|
+
"text=Keys",
|
|
477
|
+
"button:has-text('API')",
|
|
478
|
+
"a:has-text('API keys')",
|
|
479
|
+
"[data-tab='keys']",
|
|
480
|
+
"text=api keys",
|
|
481
|
+
]:
|
|
482
|
+
try:
|
|
483
|
+
el = page.locator(selector).first
|
|
484
|
+
if el.count() > 0 and el.is_visible(timeout=3000):
|
|
485
|
+
el.click()
|
|
486
|
+
api_keys_clicked = True
|
|
487
|
+
log_step(f"Tab API keys ditemukan dan diklik")
|
|
488
|
+
time.sleep(2)
|
|
489
|
+
break
|
|
490
|
+
except Exception:
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
if not api_keys_clicked:
|
|
494
|
+
log_step("Tab API keys tidak ditemukan, mencoba screenshot...")
|
|
495
|
+
try:
|
|
496
|
+
page.screenshot(path="/tmp/openvecta_dashboard.png")
|
|
497
|
+
log_step("Screenshot: /tmp/openvecta_dashboard.png")
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
# Click "Create key" button
|
|
502
|
+
log_step("Mencari tombol 'Create key'...")
|
|
503
|
+
create_clicked = False
|
|
504
|
+
for selector in [
|
|
505
|
+
"button:has-text('Create key')",
|
|
506
|
+
"button:has-text('Create Key')",
|
|
507
|
+
"button:has-text('Generate')",
|
|
508
|
+
"button:has-text('New key')",
|
|
509
|
+
"button:has-text('Add key')",
|
|
510
|
+
"text=Create key",
|
|
511
|
+
"text=Generate key",
|
|
512
|
+
]:
|
|
513
|
+
try:
|
|
514
|
+
el = page.locator(selector).first
|
|
515
|
+
if el.count() > 0 and el.is_visible(timeout=3000):
|
|
516
|
+
el.click()
|
|
517
|
+
create_clicked = True
|
|
518
|
+
log_step("Tombol 'Create key' diklik")
|
|
519
|
+
time.sleep(3)
|
|
520
|
+
break
|
|
521
|
+
except Exception:
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
if not create_clicked:
|
|
525
|
+
try:
|
|
526
|
+
page.screenshot(path="/tmp/openvecta_no_create_btn.png")
|
|
527
|
+
log_step("Screenshot: /tmp/openvecta_no_create_btn.png")
|
|
528
|
+
except Exception:
|
|
529
|
+
pass
|
|
530
|
+
die("Tidak dapat menemukan tombol 'Create key'. Mungkin akun belum di-fund atau UI berubah.")
|
|
531
|
+
|
|
532
|
+
# ── Step 9: Copy the generated API key ────────────────────────────
|
|
533
|
+
log_step("Mengambil API key yang dihasilkan...")
|
|
534
|
+
time.sleep(3)
|
|
535
|
+
|
|
536
|
+
api_key = None
|
|
537
|
+
|
|
538
|
+
# Method 1: Look for text matching ov_sk_ pattern on page
|
|
539
|
+
try:
|
|
540
|
+
page_text = page.content()
|
|
541
|
+
ov_matches = re.findall(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', page_text)
|
|
542
|
+
if ov_matches:
|
|
543
|
+
api_key = ov_matches[0]
|
|
544
|
+
log_step(f"API key ditemukan di halaman: {api_key[:20]}...")
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
|
|
548
|
+
# Method 2: Look in code/pre elements
|
|
549
|
+
if not api_key:
|
|
550
|
+
for selector in ["code", "pre", "[class*='key']", "[class*='token']", "[class*='secret']"]:
|
|
551
|
+
try:
|
|
552
|
+
el = page.locator(selector).first
|
|
553
|
+
if el.count() > 0:
|
|
554
|
+
text = el.text_content() or ""
|
|
555
|
+
ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', text)
|
|
556
|
+
if ov_match:
|
|
557
|
+
api_key = ov_match.group(1)
|
|
558
|
+
log_step(f"API key ditemukan di element {selector}")
|
|
559
|
+
break
|
|
560
|
+
except Exception:
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
# Method 3: Look for a copy button and read the nearby text
|
|
564
|
+
if not api_key:
|
|
565
|
+
for selector in [
|
|
566
|
+
"button:has-text('Copy')",
|
|
567
|
+
"button:has-text('copy')",
|
|
568
|
+
"[data-copy]",
|
|
569
|
+
"button[aria-label*='copy' i]",
|
|
570
|
+
]:
|
|
571
|
+
try:
|
|
572
|
+
copy_btn = page.locator(selector).first
|
|
573
|
+
if copy_btn.count() > 0 and copy_btn.is_visible(timeout=2000):
|
|
574
|
+
# Read sibling or parent text
|
|
575
|
+
parent = copy_btn.locator("xpath=..")
|
|
576
|
+
parent_text = parent.text_content() or ""
|
|
577
|
+
ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', parent_text)
|
|
578
|
+
if ov_match:
|
|
579
|
+
api_key = ov_match.group(1)
|
|
580
|
+
break
|
|
581
|
+
# Try grandparent
|
|
582
|
+
gparent = parent.locator("xpath=..")
|
|
583
|
+
gparent_text = gparent.text_content() or ""
|
|
584
|
+
ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', gparent_text)
|
|
585
|
+
if ov_match:
|
|
586
|
+
api_key = ov_match.group(1)
|
|
587
|
+
break
|
|
588
|
+
except Exception:
|
|
589
|
+
continue
|
|
590
|
+
|
|
591
|
+
# Method 4: Try clipboard (click copy button and read clipboard)
|
|
592
|
+
if not api_key:
|
|
593
|
+
try:
|
|
594
|
+
copy_btn = page.locator("button:has-text('Copy')").first
|
|
595
|
+
if copy_btn.count() > 0:
|
|
596
|
+
copy_btn.click()
|
|
597
|
+
time.sleep(1)
|
|
598
|
+
# Try to read from clipboard via JS
|
|
599
|
+
clipboard = page.evaluate("() => navigator.clipboard.readText().catch(() => '')")
|
|
600
|
+
if clipboard and re.match(r'ov_sk_live_', clipboard):
|
|
601
|
+
api_key = clipboard
|
|
602
|
+
except Exception:
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
# Method 5: Scan all text nodes for ov_sk_ pattern
|
|
606
|
+
if not api_key:
|
|
607
|
+
try:
|
|
608
|
+
all_text = page.evaluate("""
|
|
609
|
+
() => {
|
|
610
|
+
const walker = document.createTreeWalker(
|
|
611
|
+
document.body, NodeFilter.SHOW_TEXT, null, false
|
|
612
|
+
);
|
|
613
|
+
const texts = [];
|
|
614
|
+
let node;
|
|
615
|
+
while (node = walker.nextNode()) {
|
|
616
|
+
const t = node.textContent.trim();
|
|
617
|
+
if (t.includes('ov_sk_')) texts.push(t);
|
|
618
|
+
}
|
|
619
|
+
return texts;
|
|
620
|
+
}
|
|
621
|
+
""")
|
|
622
|
+
for text in (all_text or []):
|
|
623
|
+
ov_match = re.search(r'(ov_sk_live_[A-Za-z0-9_\-]{20,})', text)
|
|
624
|
+
if ov_match:
|
|
625
|
+
api_key = ov_match.group(1)
|
|
626
|
+
break
|
|
627
|
+
except Exception:
|
|
628
|
+
pass
|
|
629
|
+
|
|
630
|
+
if not api_key:
|
|
631
|
+
try:
|
|
632
|
+
page.screenshot(path="/tmp/openvecta_no_key.png")
|
|
633
|
+
log_step("Screenshot: /tmp/openvecta_no_key.png")
|
|
634
|
+
except Exception:
|
|
635
|
+
pass
|
|
636
|
+
die("Tidak dapat menemukan API key ov_sk_live_... di halaman. Mungkin akun belum di-fund dengan USDC.")
|
|
637
|
+
|
|
638
|
+
# ── Step 10: Return success ───────────────────────────────────────
|
|
639
|
+
log_step(f"OpenVecta signup berhasil! API key: {api_key[:20]}...")
|
|
640
|
+
success(api_key, email)
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
try:
|
|
644
|
+
page.screenshot(path="/tmp/openvecta_error.png")
|
|
645
|
+
log_step(f"Screenshot error: /tmp/openvecta_error.png")
|
|
646
|
+
except Exception:
|
|
647
|
+
pass
|
|
648
|
+
die(f"Error selama otomatisasi OpenVecta: {str(e)}")
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
if __name__ == "__main__":
|
|
652
|
+
main()
|