@fudrouter/fsrouter 0.6.79 → 0.6.81
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 +1 -0
- package/dist/open-sse/shared/machineId.js +2 -1
- package/open-sse/providers/registry/nous-research.js +1 -0
- package/open-sse/shared/machineId.js +2 -1
- 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,902 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import argparse
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
import socket
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
# Patch Playwright's Locator.is_visible
|
|
16
|
+
try:
|
|
17
|
+
from playwright.sync_api import Locator
|
|
18
|
+
_orig_is_visible = Locator.is_visible
|
|
19
|
+
def _patched_is_visible(self, *args, **kwargs):
|
|
20
|
+
timeout = kwargs.pop("timeout", None)
|
|
21
|
+
if timeout is None and len(args) > 0: timeout = args[0]
|
|
22
|
+
if timeout is not None:
|
|
23
|
+
try:
|
|
24
|
+
self.wait_for(state="visible", timeout=float(timeout))
|
|
25
|
+
return True
|
|
26
|
+
except Exception: return False
|
|
27
|
+
return _orig_is_visible(self, *args, **kwargs)
|
|
28
|
+
Locator.is_visible = _patched_is_visible
|
|
29
|
+
except ImportError: pass
|
|
30
|
+
|
|
31
|
+
START_TIME = time.time()
|
|
32
|
+
def log_step(message: str, *args):
|
|
33
|
+
elapsed = int(time.time() - START_TIME)
|
|
34
|
+
duration = f"{elapsed}s" if elapsed < 60 else f"{elapsed // 60}m {elapsed % 60}s"
|
|
35
|
+
full_msg = message % args if args else message
|
|
36
|
+
print(json.dumps({"step": f"{full_msg} [{duration}]"}), flush=True)
|
|
37
|
+
|
|
38
|
+
def safe_email_to_dirname(email: str) -> str:
|
|
39
|
+
cleaned = (email or "").strip().lower().replace("@", "_at_")
|
|
40
|
+
return re.sub(r"[^a-z0-9._-]+", "_", cleaned).strip("._-")
|
|
41
|
+
|
|
42
|
+
def click_first(page, selectors, timeout_ms: int = 8000) -> bool:
|
|
43
|
+
deadline = time.time() + (timeout_ms / 1000.0)
|
|
44
|
+
while time.time() < deadline:
|
|
45
|
+
for sel in selectors:
|
|
46
|
+
try:
|
|
47
|
+
loc = page.locator(sel).first
|
|
48
|
+
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
49
|
+
loc.click(timeout=2000)
|
|
50
|
+
return True
|
|
51
|
+
except Exception: continue
|
|
52
|
+
time.sleep(0.3)
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def fill_first(page, selectors, value: str, timeout_ms: int = 8000) -> bool:
|
|
56
|
+
deadline = time.time() + (timeout_ms / 1000.0)
|
|
57
|
+
while time.time() < deadline:
|
|
58
|
+
for sel in selectors:
|
|
59
|
+
try:
|
|
60
|
+
loc = page.locator(sel).first
|
|
61
|
+
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
62
|
+
loc.fill(value, timeout=2000)
|
|
63
|
+
return True
|
|
64
|
+
except Exception: continue
|
|
65
|
+
time.sleep(0.3)
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def _react_invoke_click(page, text_marker: str) -> bool:
|
|
69
|
+
try:
|
|
70
|
+
return page.evaluate("""(marker) => {
|
|
71
|
+
const btn = Array.from(document.querySelectorAll('button, [role="button"], a')).find(el =>
|
|
72
|
+
(el.innerText || '').toLowerCase().includes(marker.toLowerCase()) && !/Cancel|Batal/i.test(el.innerText)
|
|
73
|
+
);
|
|
74
|
+
if (!btn) return false;
|
|
75
|
+
const propsKey = Object.keys(btn).find(k => k.startsWith('__reactProps$'));
|
|
76
|
+
if (propsKey && btn[propsKey] && typeof btn[propsKey].onClick === 'function') {
|
|
77
|
+
btn[propsKey].onClick({ preventDefault:()=>{}, stopPropagation:()=>{}, target:btn, type:'click' });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}""", text_marker)
|
|
82
|
+
except Exception: return False
|
|
83
|
+
|
|
84
|
+
def _bypass_canva_modals(page):
|
|
85
|
+
try:
|
|
86
|
+
page.evaluate("""() => {
|
|
87
|
+
const sels = ['[role="dialog"]', '.modal', '[aria-modal="true"]', 'div:has(> button[aria-label="Close"])'];
|
|
88
|
+
sels.forEach(s => document.querySelectorAll(s).forEach(e => e.remove()));
|
|
89
|
+
document.querySelectorAll('[class*="backdrop"], [class*="overlay"]').forEach(e => e.remove());
|
|
90
|
+
}""")
|
|
91
|
+
except Exception: pass
|
|
92
|
+
|
|
93
|
+
import urllib.request
|
|
94
|
+
|
|
95
|
+
def get_db_path() -> Path:
|
|
96
|
+
return Path.home() / ".9router-v2" / "db" / "data.sqlite"
|
|
97
|
+
|
|
98
|
+
def iso_to_unix(iso_str: str) -> int:
|
|
99
|
+
try:
|
|
100
|
+
clean = iso_str.replace("Z", "+00:00")
|
|
101
|
+
from datetime import datetime, timezone
|
|
102
|
+
dt = datetime.fromisoformat(clean)
|
|
103
|
+
return int(dt.timestamp())
|
|
104
|
+
except Exception:
|
|
105
|
+
return int(time.time())
|
|
106
|
+
|
|
107
|
+
def load_settings_db() -> dict:
|
|
108
|
+
db_path = get_db_path()
|
|
109
|
+
if not db_path.exists(): return {}
|
|
110
|
+
try:
|
|
111
|
+
conn = sqlite3.connect(str(db_path))
|
|
112
|
+
conn.row_factory = sqlite3.Row
|
|
113
|
+
cursor = conn.cursor()
|
|
114
|
+
cursor.execute("SELECT data FROM settings WHERE id = 1")
|
|
115
|
+
row = cursor.fetchone()
|
|
116
|
+
conn.close()
|
|
117
|
+
if row: return json.loads(row["data"])
|
|
118
|
+
except Exception: pass
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
def extract_otp_canva(text: str, html: str = "", subject: str = "") -> tuple:
|
|
122
|
+
"""Extract 6-digit OTP or passwordless URL from Canva email."""
|
|
123
|
+
parts = [subject or "", text or "", html or ""]
|
|
124
|
+
haystack = "\n".join(filter(None, parts))
|
|
125
|
+
# Labeled code
|
|
126
|
+
m = re.search(r'(?:verification\s*code|code\s*(?:is|:)|one[-\s]?time)\s*[:#-]?\s*([0-9]{4,8})\b', haystack, re.IGNORECASE)
|
|
127
|
+
if m: return m.group(1), ""
|
|
128
|
+
# Canva passwordless link
|
|
129
|
+
m_url = re.search(r'https://www\.canva\.com/passwordless/[^\s"<>]+', haystack)
|
|
130
|
+
if m_url: return "", m_url.group(0).replace("&", "&")
|
|
131
|
+
# Loose 6-digit
|
|
132
|
+
m_loose = re.search(r'(?<![0-9])([0-9]{6})(?![0-9])', haystack)
|
|
133
|
+
if m_loose: return m_loose.group(1), ""
|
|
134
|
+
return "", ""
|
|
135
|
+
|
|
136
|
+
def sync_ammail_messages(email: str, since_ts: int, settings: dict):
|
|
137
|
+
db_path = get_db_path()
|
|
138
|
+
if not db_path.exists(): return
|
|
139
|
+
api_key = settings.get("ammail_api_key")
|
|
140
|
+
base_url = settings.get("ammail_base_url")
|
|
141
|
+
fallback_url = settings.get("ammail_cf_workers_dev_url")
|
|
142
|
+
if not api_key or (not base_url and not fallback_url): return
|
|
143
|
+
alias = email.split("@")[0]
|
|
144
|
+
domain = email.split("@")[1] if "@" in email else ""
|
|
145
|
+
urls = [u.rstrip("/") for u in [base_url, fallback_url] if u]
|
|
146
|
+
messages = []
|
|
147
|
+
for base in urls:
|
|
148
|
+
try:
|
|
149
|
+
req = urllib.request.Request(f"{base}/api/inboxes/{alias}/messages",
|
|
150
|
+
headers={"X-API-Key": api_key, "Accept": "application/json",
|
|
151
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"})
|
|
152
|
+
with urllib.request.urlopen(req, timeout=10) as res:
|
|
153
|
+
data = json.loads(res.read().decode("utf-8"))
|
|
154
|
+
messages = data.get("messages", [])
|
|
155
|
+
if messages: break
|
|
156
|
+
except Exception: continue
|
|
157
|
+
for msg in messages:
|
|
158
|
+
msg_id = msg.get("id")
|
|
159
|
+
if not msg_id: continue
|
|
160
|
+
try:
|
|
161
|
+
conn = sqlite3.connect(str(db_path))
|
|
162
|
+
cursor = conn.cursor()
|
|
163
|
+
cursor.execute("SELECT 1 FROM ammailOtps WHERE messageShortId = ?", (msg_id,))
|
|
164
|
+
if cursor.fetchone(): conn.close(); continue
|
|
165
|
+
conn.close()
|
|
166
|
+
except Exception: continue
|
|
167
|
+
full_msg = None
|
|
168
|
+
for base in urls:
|
|
169
|
+
try:
|
|
170
|
+
req = urllib.request.Request(f"{base}/api/messages/{msg_id}",
|
|
171
|
+
headers={"X-API-Key": api_key, "Accept": "application/json",
|
|
172
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"})
|
|
173
|
+
with urllib.request.urlopen(req, timeout=10) as res:
|
|
174
|
+
data = json.loads(res.read().decode("utf-8"))
|
|
175
|
+
full_msg = data.get("message")
|
|
176
|
+
if full_msg: break
|
|
177
|
+
except Exception: continue
|
|
178
|
+
if not full_msg: continue
|
|
179
|
+
body_text = str(full_msg.get("text") or msg.get("snippet") or "")
|
|
180
|
+
body_html = str(full_msg.get("html") or "")
|
|
181
|
+
from_data = full_msg.get("from") or msg.get("from") or {}
|
|
182
|
+
sender = str(from_data.get("address") or from_data.get("name") or "")
|
|
183
|
+
subject = str(full_msg.get("subject") or msg.get("subject") or "")
|
|
184
|
+
received_at = iso_to_unix(str(full_msg.get("receivedAt") or msg.get("receivedAt") or ""))
|
|
185
|
+
otp_code, verify_url = extract_otp_canva(body_text, body_html, subject)
|
|
186
|
+
try:
|
|
187
|
+
conn = sqlite3.connect(str(db_path))
|
|
188
|
+
cursor = conn.cursor()
|
|
189
|
+
cursor.execute("""INSERT OR IGNORE INTO ammailOtps
|
|
190
|
+
(address, alias, domain, sender, subject, otpCode, verifyUrl,
|
|
191
|
+
bodyText, bodyHtml, messageShortId, rawEventJson, receivedAt, usedAt)
|
|
192
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)""",
|
|
193
|
+
(email, alias, domain, sender, subject, otp_code, verify_url,
|
|
194
|
+
body_text, body_html, msg_id, json.dumps(full_msg), received_at))
|
|
195
|
+
conn.commit(); conn.close()
|
|
196
|
+
except Exception: pass
|
|
197
|
+
|
|
198
|
+
def wait_for_otp_from_db(email: str, since_ts: int, settings: dict, timeout: int = 180) -> tuple:
|
|
199
|
+
db_path = get_db_path()
|
|
200
|
+
if not db_path.exists():
|
|
201
|
+
log_step(f"DB tidak ditemukan: {db_path}")
|
|
202
|
+
return "", ""
|
|
203
|
+
deadline = time.time() + timeout
|
|
204
|
+
alias = email.split('@')[0]
|
|
205
|
+
last_log = 0
|
|
206
|
+
while time.time() < deadline:
|
|
207
|
+
try: sync_ammail_messages(email, since_ts, settings)
|
|
208
|
+
except Exception: pass
|
|
209
|
+
try:
|
|
210
|
+
conn = sqlite3.connect(str(db_path))
|
|
211
|
+
conn.row_factory = sqlite3.Row
|
|
212
|
+
cursor = conn.cursor()
|
|
213
|
+
cursor.execute(
|
|
214
|
+
"SELECT id, otpCode, verifyUrl FROM ammailOtps WHERE LOWER(address) = ? AND receivedAt >= ? AND usedAt = 0 ORDER BY receivedAt DESC LIMIT 1",
|
|
215
|
+
(email.lower(), since_ts))
|
|
216
|
+
row = cursor.fetchone()
|
|
217
|
+
if row:
|
|
218
|
+
cursor.execute("UPDATE ammailOtps SET usedAt = ? WHERE id = ?", (int(time.time()), row["id"]))
|
|
219
|
+
conn.commit(); conn.close()
|
|
220
|
+
return row["otpCode"] or "", row["verifyUrl"] or ""
|
|
221
|
+
conn.close()
|
|
222
|
+
except Exception: pass
|
|
223
|
+
# Emit heartbeat setiap 5 detik agar timer UI terus jalan
|
|
224
|
+
now = time.time()
|
|
225
|
+
if now - last_log >= 5:
|
|
226
|
+
remaining = int(deadline - now)
|
|
227
|
+
log_step(f"Menunggu OTP Canva ({alias}@...) — sisa {remaining}s")
|
|
228
|
+
last_log = now
|
|
229
|
+
time.sleep(2)
|
|
230
|
+
return "", ""
|
|
231
|
+
|
|
232
|
+
def enroll_canva_via_email(page, invite_link: str, email: str, password: str, settings: dict = None) -> bool:
|
|
233
|
+
if settings is None: settings = {}
|
|
234
|
+
# Buat inbox di ammail sebelum mulai (kalau belum ada)
|
|
235
|
+
api_key = settings.get("ammail_api_key")
|
|
236
|
+
base_url = (settings.get("ammail_base_url") or settings.get("ammail_cf_workers_dev_url") or "").rstrip("/")
|
|
237
|
+
if api_key and base_url:
|
|
238
|
+
alias = email.split("@")[0]
|
|
239
|
+
domain = email.split("@")[1] if "@" in email else ""
|
|
240
|
+
try:
|
|
241
|
+
body_bytes = json.dumps({"alias": alias, "domain": domain}).encode("utf-8")
|
|
242
|
+
req = urllib.request.Request(
|
|
243
|
+
f"{base_url}/api/inboxes",
|
|
244
|
+
data=body_bytes,
|
|
245
|
+
method="POST"
|
|
246
|
+
)
|
|
247
|
+
req.add_header("X-API-Key", api_key)
|
|
248
|
+
req.add_header("Content-Type", "application/json")
|
|
249
|
+
req.add_header("Accept", "application/json")
|
|
250
|
+
req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
|
251
|
+
with urllib.request.urlopen(req, timeout=10) as res:
|
|
252
|
+
res_data = json.loads(res.read().decode())
|
|
253
|
+
addr = (res_data.get("inbox") or {}).get("address", email)
|
|
254
|
+
log_step(f"Inbox siap: {addr}")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
log_step(f"[WARN] Inbox ammail: {e}")
|
|
257
|
+
|
|
258
|
+
log_step("Membuka Canva invite link...")
|
|
259
|
+
page.goto(invite_link, wait_until="domcontentloaded", timeout=60000)
|
|
260
|
+
time.sleep(2)
|
|
261
|
+
_bypass_canva_modals(page)
|
|
262
|
+
click_first(page, ["button:has-text('Accept all cookies')", "button:has-text('Accept')"], timeout_ms=3000)
|
|
263
|
+
|
|
264
|
+
log_step("Mengisi email...")
|
|
265
|
+
# Step 1: Klik tombol "Continue with email" — Canva hide field di balik button ini
|
|
266
|
+
email_btn_selectors = [
|
|
267
|
+
"button[aria-label='Continue with email']",
|
|
268
|
+
"button[aria-label='Sign up with email']",
|
|
269
|
+
"button[aria-label='Log in with email']",
|
|
270
|
+
"button[aria-label='Lanjutkan dengan email']",
|
|
271
|
+
"button:has-text('Continue with email')",
|
|
272
|
+
"button:has-text('Sign up with email')",
|
|
273
|
+
"button:has-text('Use email')",
|
|
274
|
+
"button:has-text('Lanjutkan dengan email')",
|
|
275
|
+
]
|
|
276
|
+
email_btn = None
|
|
277
|
+
deadline_btn = time.time() + 20
|
|
278
|
+
while time.time() < deadline_btn and email_btn is None:
|
|
279
|
+
for sel in email_btn_selectors:
|
|
280
|
+
try:
|
|
281
|
+
loc = page.locator(sel).first
|
|
282
|
+
if loc.count() > 0 and loc.is_visible(timeout=400):
|
|
283
|
+
email_btn = loc; break
|
|
284
|
+
except Exception: continue
|
|
285
|
+
if email_btn is None: time.sleep(0.8)
|
|
286
|
+
|
|
287
|
+
if email_btn:
|
|
288
|
+
try:
|
|
289
|
+
email_btn.click(force=True, timeout=5000)
|
|
290
|
+
except Exception:
|
|
291
|
+
_react_invoke_click(page, "Continue with email")
|
|
292
|
+
|
|
293
|
+
# Step 2: Isi email — Canva pakai input[name='username'][inputmode='email']
|
|
294
|
+
email_input_selectors = [
|
|
295
|
+
"input[name='username'][inputmode='email']",
|
|
296
|
+
"input[autocomplete='username'][inputmode='email']",
|
|
297
|
+
"input[type='email']",
|
|
298
|
+
"input[name='email']",
|
|
299
|
+
"input[inputmode='email']",
|
|
300
|
+
"input[placeholder*='email' i]",
|
|
301
|
+
"input[autocomplete='email']",
|
|
302
|
+
"input[data-testid*='email' i]",
|
|
303
|
+
"input[aria-label*='email' i]",
|
|
304
|
+
"input[autocomplete='username']",
|
|
305
|
+
"input:not([type='password']):not([type='hidden']):not([type='checkbox'])",
|
|
306
|
+
]
|
|
307
|
+
email_input = None
|
|
308
|
+
deadline_in = time.time() + 20
|
|
309
|
+
while time.time() < deadline_in and email_input is None:
|
|
310
|
+
for sel in email_input_selectors:
|
|
311
|
+
try:
|
|
312
|
+
loc = page.locator(sel).first
|
|
313
|
+
if loc.count() > 0 and loc.is_visible(timeout=500):
|
|
314
|
+
email_input = loc; break
|
|
315
|
+
except Exception: continue
|
|
316
|
+
if email_input is None:
|
|
317
|
+
time.sleep(0.8)
|
|
318
|
+
log_step(f"Menunggu email input... URL: {page.url[:50]}")
|
|
319
|
+
|
|
320
|
+
if email_input is None:
|
|
321
|
+
log_step("Email field tidak ditemukan!")
|
|
322
|
+
if "/home" in page.url: return True
|
|
323
|
+
else:
|
|
324
|
+
email_input.fill(email)
|
|
325
|
+
|
|
326
|
+
# OTP timestamp SETELAH submit (dari leoapi-main)
|
|
327
|
+
otp_since_ts = int(time.time())
|
|
328
|
+
click_first(page, ["button[type='submit']", "button:has-text('Continue')", "button:has-text('Lanjutkan')"], timeout_ms=8000)
|
|
329
|
+
|
|
330
|
+
deadline = time.time() + 150
|
|
331
|
+
otp_filled = False
|
|
332
|
+
|
|
333
|
+
while time.time() < deadline:
|
|
334
|
+
_bypass_canva_modals(page)
|
|
335
|
+
cur_url = page.url.lower()
|
|
336
|
+
|
|
337
|
+
# SUCCESS: Dashboard reached
|
|
338
|
+
if "canva.com" in cur_url and not any(x in cur_url for x in ["/signup", "/login", "/otp", "/brand/join"]):
|
|
339
|
+
log_step(f"Canva page tercapai: {cur_url[:60]} [DONE]")
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
# Juga sukses jika OTP sudah diisi dan sudah di canva.com (apapun page-nya)
|
|
343
|
+
if otp_filled and "canva.com" in cur_url:
|
|
344
|
+
log_step(f"Canva OTP sukses — page: {cur_url[:60]} [DONE]")
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
# Password (existing account)
|
|
348
|
+
pw_input = page.locator("input[type='password']").first
|
|
349
|
+
if pw_input.count() > 0 and pw_input.is_visible(timeout=300):
|
|
350
|
+
log_step("Mengisi password...")
|
|
351
|
+
pw_input.fill(password)
|
|
352
|
+
click_first(page, ["button[type='submit']", "button:has-text('Log in')"])
|
|
353
|
+
time.sleep(5); continue
|
|
354
|
+
|
|
355
|
+
# Name (new account) — Canva pakai input[autocomplete='name'], bukan input[name='firstName']
|
|
356
|
+
name_input = page.locator(
|
|
357
|
+
"input[autocomplete='name'], input[name='firstName'], input[name='name'], input[name='fullName']"
|
|
358
|
+
).first
|
|
359
|
+
if name_input.count() > 0 and name_input.is_visible(timeout=300):
|
|
360
|
+
log_step("Mengisi nama akun baru...")
|
|
361
|
+
name_input.fill("Amstream User")
|
|
362
|
+
click_first(page, ["button[type='submit']", "button:has-text('Continue')", "button:has-text('Create account')"])
|
|
363
|
+
time.sleep(5); continue
|
|
364
|
+
|
|
365
|
+
# OTP screen — adopt leoapi-main strict selectors + text marker fallback
|
|
366
|
+
STRICT_OTP = [
|
|
367
|
+
"input[autocomplete='one-time-code']",
|
|
368
|
+
"input[name='code']", "input[name='otp']",
|
|
369
|
+
"input[name='verificationCode']", "input[name='verification_code']",
|
|
370
|
+
"input[id*='otp' i]:not([id*='no_otp' i])",
|
|
371
|
+
"input[data-testid*='otp' i]",
|
|
372
|
+
"input[aria-label*='code' i]:not([aria-label*='country' i])",
|
|
373
|
+
]
|
|
374
|
+
OTP_TEXT = ["Check your email", "Enter the code", "Verification code", "We sent a code",
|
|
375
|
+
"We've sent", "Cek email", "Masukkan kode", "Enter the verification"]
|
|
376
|
+
|
|
377
|
+
otp_loc = None
|
|
378
|
+
otp_type = None
|
|
379
|
+
# Box-style (6 single-char inputs)
|
|
380
|
+
boxes = page.locator("input[maxlength='1'], input[data-testid*='code-input' i]")
|
|
381
|
+
if boxes.count() >= 4 and boxes.first.is_visible(timeout=300):
|
|
382
|
+
otp_type, otp_loc = "box", boxes
|
|
383
|
+
else:
|
|
384
|
+
for sel in STRICT_OTP:
|
|
385
|
+
try:
|
|
386
|
+
loc = page.locator(sel).first
|
|
387
|
+
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
388
|
+
otp_type, otp_loc = "single", loc; break
|
|
389
|
+
except Exception: continue
|
|
390
|
+
# Text marker fallback
|
|
391
|
+
if not otp_loc and not otp_filled:
|
|
392
|
+
try:
|
|
393
|
+
body = page.inner_text("body", timeout=1000)
|
|
394
|
+
if any(m.lower() in body.lower() for m in OTP_TEXT):
|
|
395
|
+
for sel in STRICT_OTP:
|
|
396
|
+
loc = page.locator(sel).first
|
|
397
|
+
if loc.count() > 0:
|
|
398
|
+
otp_type, otp_loc = "single", loc; break
|
|
399
|
+
except Exception: pass
|
|
400
|
+
|
|
401
|
+
if otp_loc and not otp_filled:
|
|
402
|
+
log_step(f"OTP screen terdeteksi — polling ammail ({email.split('@')[0]})...")
|
|
403
|
+
otp_code, verify_url = wait_for_otp_from_db(email, otp_since_ts, settings)
|
|
404
|
+
if otp_code:
|
|
405
|
+
digits = re.sub(r"\D", "", otp_code)
|
|
406
|
+
log_step(f"OTP didapat: {otp_code}, mengisi...")
|
|
407
|
+
if otp_type == "box":
|
|
408
|
+
for i in range(min(boxes.count(), len(digits))): boxes.nth(i).fill(digits[i])
|
|
409
|
+
else:
|
|
410
|
+
otp_loc.fill(digits)
|
|
411
|
+
otp_filled = True
|
|
412
|
+
time.sleep(1)
|
|
413
|
+
click_first(page, ["button[type='submit']", "button:has-text('Verify')", "button:has-text('Continue')"])
|
|
414
|
+
time.sleep(4)
|
|
415
|
+
elif verify_url:
|
|
416
|
+
log_step("Magic link didapat, navigasi...")
|
|
417
|
+
page.goto(verify_url)
|
|
418
|
+
otp_filled = True
|
|
419
|
+
time.sleep(4)
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
# Join Team confirmation page (AFTER auth, different from /brand/join invite page)
|
|
423
|
+
# /brand/join?token=... = auth page, teams/accept or teams/join = post-auth confirmation
|
|
424
|
+
if otp_filled and ("teams/accept" in cur_url or "teams/join" in cur_url or ("/teams/" in cur_url and "join" not in cur_url)):
|
|
425
|
+
log_step("Halaman konfirmasi Join Team, mengklik...")
|
|
426
|
+
if not click_first(page, ["button:has-text('Join team')", "button:has-text('Join the team')", "button:has-text('Gabung')", "button:has-text('Accept')"], timeout_ms=3000):
|
|
427
|
+
_react_invoke_click(page, "Join")
|
|
428
|
+
time.sleep(3)
|
|
429
|
+
|
|
430
|
+
# Log current state setiap 10 detik — tampilkan fase + URL pendek
|
|
431
|
+
elapsed = int(time.time() - START_TIME)
|
|
432
|
+
if elapsed % 10 < 2:
|
|
433
|
+
short = page.url.replace("https://","").replace("www.","")[:55]
|
|
434
|
+
phase = "OTP dikirim" if otp_filled else ("Menunggu OTP" if otp_loc else "Inisialisasi")
|
|
435
|
+
log_step(f"[{phase}] {short}")
|
|
436
|
+
|
|
437
|
+
time.sleep(2)
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
def _click_canva_authorize_v2(page, email: str) -> bool:
|
|
441
|
+
primary = ["button:has-text('Allow')", "button:has-text('Authorize')", "button:has-text('Continue')", "button:has-text('Allow access')"]
|
|
442
|
+
target = None
|
|
443
|
+
for s in primary:
|
|
444
|
+
loc = page.locator(s).first
|
|
445
|
+
if loc.count() > 0: target = loc; break
|
|
446
|
+
|
|
447
|
+
url_before = page.url
|
|
448
|
+
if target:
|
|
449
|
+
try:
|
|
450
|
+
target.click(timeout=3000)
|
|
451
|
+
time.sleep(2)
|
|
452
|
+
if page.url != url_before: return True
|
|
453
|
+
except Exception: pass
|
|
454
|
+
return _react_invoke_click(page, "Allow") or _react_invoke_click(page, "Continue")
|
|
455
|
+
|
|
456
|
+
def run_automation(email, password, invite_link, proxy=None, headless=True, skip_canva=False):
|
|
457
|
+
settings = load_settings_db()
|
|
458
|
+
if skip_canva: settings["skip_canva"] = True
|
|
459
|
+
import asyncio
|
|
460
|
+
from camoufox.sync_api import Camoufox
|
|
461
|
+
|
|
462
|
+
profile_dir = Path(f"profiles/{safe_email_to_dirname(email)}")
|
|
463
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
464
|
+
|
|
465
|
+
# Fix async event loop conflicts (dari leoapi-main)
|
|
466
|
+
try:
|
|
467
|
+
asyncio.set_event_loop(None)
|
|
468
|
+
if hasattr(asyncio, "events") and hasattr(asyncio.events, "_set_running_loop"):
|
|
469
|
+
asyncio.events._set_running_loop(None)
|
|
470
|
+
except Exception: pass
|
|
471
|
+
|
|
472
|
+
kwargs = dict(
|
|
473
|
+
headless=headless,
|
|
474
|
+
persistent_context=True,
|
|
475
|
+
user_data_dir=str(profile_dir),
|
|
476
|
+
humanize=True,
|
|
477
|
+
geoip=True,
|
|
478
|
+
locale="en-US",
|
|
479
|
+
os=("windows", "macos", "linux"),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if proxy:
|
|
483
|
+
p_url = urlparse(proxy)
|
|
484
|
+
kwargs["proxy"] = {"server": f"{p_url.scheme}://{p_url.hostname}:{p_url.port}"}
|
|
485
|
+
if p_url.username: kwargs["proxy"]["username"] = p_url.username
|
|
486
|
+
if p_url.password: kwargs["proxy"]["password"] = p_url.password
|
|
487
|
+
|
|
488
|
+
# Graceful TypeError fallback (dari leoapi-main)
|
|
489
|
+
ctx = None
|
|
490
|
+
try:
|
|
491
|
+
ctx = Camoufox(**kwargs)
|
|
492
|
+
except TypeError:
|
|
493
|
+
for drop in ("os", "geoip", "humanize"):
|
|
494
|
+
kwargs.pop(drop, None)
|
|
495
|
+
try:
|
|
496
|
+
ctx = Camoufox(**kwargs)
|
|
497
|
+
break
|
|
498
|
+
except TypeError:
|
|
499
|
+
continue
|
|
500
|
+
if ctx is None:
|
|
501
|
+
kwargs.pop("locale", None)
|
|
502
|
+
ctx = Camoufox(**kwargs)
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
with ctx as browser:
|
|
506
|
+
page = browser.new_page()
|
|
507
|
+
|
|
508
|
+
# 1. Canva Enroll
|
|
509
|
+
if not settings.get("skip_canva"):
|
|
510
|
+
if not enroll_canva_via_email(page, invite_link, email, password, settings):
|
|
511
|
+
sys.stdout.write(json.dumps({"status": "error", "message": "Gagal pendaftaran Canva — lihat step log"}) + "\n")
|
|
512
|
+
sys.stdout.flush()
|
|
513
|
+
return False
|
|
514
|
+
# Emit canva_enrolled agar Node update DB
|
|
515
|
+
sys.stdout.write(json.dumps({"canva_enrolled": True}) + "\n")
|
|
516
|
+
sys.stdout.flush()
|
|
517
|
+
else:
|
|
518
|
+
log_step("Skip Canva enrollment as requested.")
|
|
519
|
+
|
|
520
|
+
# 2. Leonardo Signup
|
|
521
|
+
# Intercept /api/auth/get-session untuk capture JWT langsung dari response
|
|
522
|
+
captured_jwt = {"value": ""}
|
|
523
|
+
def _find_jwt_in(obj, depth=0):
|
|
524
|
+
if depth > 6: return ""
|
|
525
|
+
if isinstance(obj, str) and obj.startswith("eyJ") and len(obj) > 100: return obj
|
|
526
|
+
if isinstance(obj, dict):
|
|
527
|
+
for v in obj.values():
|
|
528
|
+
r = _find_jwt_in(v, depth+1)
|
|
529
|
+
if r: return r
|
|
530
|
+
if isinstance(obj, list):
|
|
531
|
+
for v in obj:
|
|
532
|
+
r = _find_jwt_in(v, depth+1)
|
|
533
|
+
if r: return r
|
|
534
|
+
return ""
|
|
535
|
+
def handle_response(response):
|
|
536
|
+
try:
|
|
537
|
+
if "leonardo.ai/api/auth/get-session" not in response.url: return
|
|
538
|
+
if response.status != 200: return
|
|
539
|
+
body = response.text()
|
|
540
|
+
if not body or body.strip() == 'null': return
|
|
541
|
+
import json as _json
|
|
542
|
+
data = _json.loads(body)
|
|
543
|
+
jwt = _find_jwt_in(data)
|
|
544
|
+
if jwt:
|
|
545
|
+
captured_jwt["value"] = jwt
|
|
546
|
+
log_step(f"JWT captured dari get-session intercept!")
|
|
547
|
+
except Exception: pass
|
|
548
|
+
# Di Camoufox persistent_context, 'browser' IS BrowserContext
|
|
549
|
+
browser.on("response", handle_response)
|
|
550
|
+
|
|
551
|
+
log_step("Membuka Leonardo AI — login page...")
|
|
552
|
+
page.goto("https://app.leonardo.ai/auth/login", wait_until="domcontentloaded", timeout=60000)
|
|
553
|
+
time.sleep(3)
|
|
554
|
+
page.screenshot(path="leonardo_login_initial.png")
|
|
555
|
+
|
|
556
|
+
# Setup popup listener SEBELUM klik (event-driven)
|
|
557
|
+
canva_popup = {"page": None}
|
|
558
|
+
def on_popup(popup_page):
|
|
559
|
+
canva_popup["page"] = popup_page
|
|
560
|
+
log_step(f"Popup terdeteksi: {popup_page.url[:60]}")
|
|
561
|
+
browser.on("page", on_popup)
|
|
562
|
+
|
|
563
|
+
log_step("Fokus & Enter pada tombol 'Canva'...")
|
|
564
|
+
sels = [
|
|
565
|
+
"button:has-text('Canva')",
|
|
566
|
+
"a:has-text('Canva')",
|
|
567
|
+
"button[data-slot='button']:has-text('Canva')",
|
|
568
|
+
"div[role='button']:has-text('Canva')"
|
|
569
|
+
]
|
|
570
|
+
focused = False
|
|
571
|
+
for sel in sels:
|
|
572
|
+
try:
|
|
573
|
+
loc = page.locator(sel).first
|
|
574
|
+
if loc.count() > 0 and loc.is_visible(timeout=3000):
|
|
575
|
+
loc.focus(timeout=3000)
|
|
576
|
+
page.keyboard.press("Enter")
|
|
577
|
+
focused = True
|
|
578
|
+
break
|
|
579
|
+
except: continue
|
|
580
|
+
|
|
581
|
+
if not focused:
|
|
582
|
+
log_step("GAGAL: Tombol login Canva tidak bisa difokus!")
|
|
583
|
+
page.screenshot(path="leonardo_login_failed_focus.png")
|
|
584
|
+
|
|
585
|
+
time.sleep(5)
|
|
586
|
+
page.screenshot(path="leonardo_after_click.png")
|
|
587
|
+
log_step(f"Interaction selesai. URL sekarang: {page.url[:60]}")
|
|
588
|
+
time.sleep(10)
|
|
589
|
+
log_step(f"Pages: {[p.url[:50] for p in browser.pages]}")
|
|
590
|
+
|
|
591
|
+
# Handle berdasarkan hasilnya
|
|
592
|
+
auth_page = None
|
|
593
|
+
if canva_popup["page"]:
|
|
594
|
+
auth_page = canva_popup["page"]
|
|
595
|
+
log_step(f"OAuth popup terdeteksi: {auth_page.url[:60]}")
|
|
596
|
+
# Jika about:blank, tunggu navigasi
|
|
597
|
+
if "about:blank" in auth_page.url:
|
|
598
|
+
try:
|
|
599
|
+
auth_page.wait_for_load_state("domcontentloaded", timeout=10000)
|
|
600
|
+
log_step(f"Popup url berubah: {auth_page.url[:60]}")
|
|
601
|
+
except: pass
|
|
602
|
+
_click_canva_authorize_v2(auth_page, email)
|
|
603
|
+
time.sleep(5)
|
|
604
|
+
|
|
605
|
+
# 2. Cek apakah main page redirect (inline OAuth)
|
|
606
|
+
elif "canva.com" in page.url:
|
|
607
|
+
auth_page = page
|
|
608
|
+
log_step(f"OAuth inline di main page: {page.url[:60]}")
|
|
609
|
+
_click_canva_authorize_v2(page, email)
|
|
610
|
+
time.sleep(5)
|
|
611
|
+
|
|
612
|
+
# 3. Scan semua pages (termasuk yang mungkin baru terbuka tapi telat)
|
|
613
|
+
else:
|
|
614
|
+
log_step("Scan pages untuk Canva...")
|
|
615
|
+
for pg in browser.pages:
|
|
616
|
+
if "canva.com" in pg.url and pg != page:
|
|
617
|
+
auth_page = pg
|
|
618
|
+
log_step(f"OAuth popup via scan: {pg.url[:60]}")
|
|
619
|
+
_click_canva_authorize_v2(pg, email)
|
|
620
|
+
time.sleep(5)
|
|
621
|
+
break
|
|
622
|
+
else:
|
|
623
|
+
# Coba klik lagi dengan JS jika belum ada popup
|
|
624
|
+
log_step("Belum ada redirect. Mencoba klik via JS...")
|
|
625
|
+
page.evaluate("Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('Canva'))?.click()")
|
|
626
|
+
time.sleep(10)
|
|
627
|
+
for pg in browser.pages:
|
|
628
|
+
if "canva.com" in pg.url and pg != page:
|
|
629
|
+
auth_page = pg
|
|
630
|
+
log_step(f"OAuth popup via scan (setelah JS click): {pg.url[:60]}")
|
|
631
|
+
_click_canva_authorize_v2(pg, email)
|
|
632
|
+
time.sleep(5)
|
|
633
|
+
break
|
|
634
|
+
else:
|
|
635
|
+
log_step(f"PERINGATAN: Tidak ada redirect ke Canva! URL={page.url}")
|
|
636
|
+
|
|
637
|
+
# Tunggu Leonardo dashboard atau onboarding (max 90s)
|
|
638
|
+
log_step("Menunggu redirect ke Leonardo dashboard/onboarding...")
|
|
639
|
+
deadline_leo = time.time() + 90
|
|
640
|
+
last_log_leo = 0
|
|
641
|
+
canva_btn_retry = 0
|
|
642
|
+
while time.time() < deadline_leo:
|
|
643
|
+
cur = page.url.lower()
|
|
644
|
+
|
|
645
|
+
# Inline OAuth redirect di main page — handle
|
|
646
|
+
if "canva.com" in cur and page != auth_page:
|
|
647
|
+
log_step(f"Main page redirect ke Canva OAuth: {page.url[:60]}")
|
|
648
|
+
_click_canva_authorize_v2(page, email)
|
|
649
|
+
auth_page = page
|
|
650
|
+
time.sleep(5)
|
|
651
|
+
continue
|
|
652
|
+
|
|
653
|
+
# Sukses: sudah di luar /auth/
|
|
654
|
+
if "app.leonardo.ai" in cur and "/auth/" not in cur:
|
|
655
|
+
log_step(f"Leonardo post-auth page: {page.url[:60]}")
|
|
656
|
+
|
|
657
|
+
# Handle onboarding/survey pages — klik Continue/Skip
|
|
658
|
+
if any(x in cur for x in ["/onboarding", "/survey", "/welcome", "/get-started"]):
|
|
659
|
+
log_step("Onboarding page — cari tombol Continue/Skip...")
|
|
660
|
+
for sel in ["button:has-text('Continue')", "button:has-text('Skip')", "button:has-text('Get started')", "button:has-text('Next')", "button[type='submit']"]:
|
|
661
|
+
try:
|
|
662
|
+
loc = page.locator(sel).first
|
|
663
|
+
if loc.count() > 0 and loc.is_visible(timeout=1000):
|
|
664
|
+
loc.click(timeout=3000)
|
|
665
|
+
time.sleep(2)
|
|
666
|
+
break
|
|
667
|
+
except Exception: pass
|
|
668
|
+
time.sleep(2)
|
|
669
|
+
# Cek apakah masih onboarding
|
|
670
|
+
if any(x in page.url.lower() for x in ["/onboarding", "/survey", "/welcome", "/get-started"]):
|
|
671
|
+
time.sleep(3)
|
|
672
|
+
continue
|
|
673
|
+
break
|
|
674
|
+
# Cek popup auth Canva baru (deteksi broad: any canva.com page)
|
|
675
|
+
for pg in browser.pages:
|
|
676
|
+
if "canva.com" in pg.url and pg != auth_page and pg != page:
|
|
677
|
+
log_step(f"OAuth popup baru: {pg.url[:60]}")
|
|
678
|
+
_click_canva_authorize_v2(pg, email)
|
|
679
|
+
auth_page = pg
|
|
680
|
+
# Retry klik "Continue with Canva" jika stuck di /auth/login > 15s
|
|
681
|
+
if "/auth/login" in cur and canva_btn_retry < 2:
|
|
682
|
+
elapsed_stuck = time.time() - (deadline_leo - 90)
|
|
683
|
+
if elapsed_stuck > 15 * (canva_btn_retry + 1):
|
|
684
|
+
log_step(f"Stuck di /auth/login, retry klik Continue with Canva ({canva_btn_retry+1}/2)...")
|
|
685
|
+
try:
|
|
686
|
+
page.goto("https://app.leonardo.ai/auth/login", wait_until="domcontentloaded", timeout=30000)
|
|
687
|
+
time.sleep(2)
|
|
688
|
+
click_first(page, ["button:has-text('Continue with Canva')"], timeout_ms=10000)
|
|
689
|
+
time.sleep(5)
|
|
690
|
+
# Cek popup setelah retry
|
|
691
|
+
for pg in browser.pages:
|
|
692
|
+
if "canva.com" in pg.url and pg != page:
|
|
693
|
+
log_step(f"Popup setelah retry: {pg.url[:60]}")
|
|
694
|
+
_click_canva_authorize_v2(pg, email)
|
|
695
|
+
auth_page = pg
|
|
696
|
+
time.sleep(5)
|
|
697
|
+
break
|
|
698
|
+
else:
|
|
699
|
+
if "canva.com" in page.url:
|
|
700
|
+
_click_canva_authorize_v2(page, email)
|
|
701
|
+
auth_page = page
|
|
702
|
+
time.sleep(5)
|
|
703
|
+
except Exception as _e:
|
|
704
|
+
log_step(f"Retry gagal: {_e}")
|
|
705
|
+
canva_btn_retry += 1
|
|
706
|
+
# Heartbeat setiap 5 detik
|
|
707
|
+
now = time.time()
|
|
708
|
+
if now - last_log_leo >= 5:
|
|
709
|
+
short = page.url.replace("https://","").replace("www.","")[:50]
|
|
710
|
+
log_step(f"Menunggu Leonardo post-auth... [{short}]")
|
|
711
|
+
last_log_leo = now
|
|
712
|
+
time.sleep(2)
|
|
713
|
+
|
|
714
|
+
# Tunggu Leonardo load penuh (pastikan session tersimpan di storage)
|
|
715
|
+
log_step(f"Leonardo page: {page.url[:60]}")
|
|
716
|
+
log_step("Tunggu networkidle + session cookie set...")
|
|
717
|
+
try:
|
|
718
|
+
page.wait_for_load_state("networkidle", timeout=10000)
|
|
719
|
+
except: pass
|
|
720
|
+
time.sleep(5)
|
|
721
|
+
|
|
722
|
+
# Navigate ke /image-generation untuk force session cookie di-set
|
|
723
|
+
log_step("Navigate ke image-generation untuk force session cookie...")
|
|
724
|
+
try:
|
|
725
|
+
page.goto("https://app.leonardo.ai/ai-generations", wait_until="domcontentloaded", timeout=30000)
|
|
726
|
+
time.sleep(5)
|
|
727
|
+
page.wait_for_load_state("networkidle", timeout=10000)
|
|
728
|
+
except: pass
|
|
729
|
+
time.sleep(3)
|
|
730
|
+
|
|
731
|
+
# Extract Leonardo cookies — pakai semua cookies dari context
|
|
732
|
+
log_step("Mengekstrak cookies Leonardo...")
|
|
733
|
+
all_cookies = page.context.cookies()
|
|
734
|
+
log_step(f"Total cookies: {len(all_cookies)}")
|
|
735
|
+
leo_cookies = [c for c in all_cookies if "leonardo.ai" in (c.get("domain") or "")]
|
|
736
|
+
|
|
737
|
+
if not leo_cookies:
|
|
738
|
+
leo_cookies = all_cookies
|
|
739
|
+
cookie_str = "; ".join(f"{c['name']}={c['value']}" for c in leo_cookies)
|
|
740
|
+
|
|
741
|
+
# === LOGIKA DARI LEOAPI-MAIN (updated untuk better-auth) ===
|
|
742
|
+
# Leonardo migrasi dari next-auth ke better-auth
|
|
743
|
+
# Token ada di: __Secure-better-auth.session_data.* atau session_token
|
|
744
|
+
SESSION_TOKEN_NAMES = [
|
|
745
|
+
# better-auth (baru — Leonardo pakai ini sekarang)
|
|
746
|
+
"__Secure-better-auth.session_data.0",
|
|
747
|
+
"__Secure-better-auth.session_data.1",
|
|
748
|
+
"better-auth.session_token",
|
|
749
|
+
"__Secure-better-auth.session_token",
|
|
750
|
+
# next-auth (lama — fallback)
|
|
751
|
+
"__Secure-next-auth.session-token",
|
|
752
|
+
"next-auth.session-token",
|
|
753
|
+
"__Secure-authjs.session-token",
|
|
754
|
+
"authjs.session-token",
|
|
755
|
+
]
|
|
756
|
+
cookie_map = {}
|
|
757
|
+
for item in cookie_str.split(";"):
|
|
758
|
+
item = item.strip()
|
|
759
|
+
if "=" in item:
|
|
760
|
+
k, v = item.split("=", 1)
|
|
761
|
+
cookie_map[k.strip()] = v.strip()
|
|
762
|
+
|
|
763
|
+
# 1. Cek session-token langsung dari cookie
|
|
764
|
+
session_token = ""
|
|
765
|
+
for name in SESSION_TOKEN_NAMES:
|
|
766
|
+
if name in cookie_map:
|
|
767
|
+
session_token = cookie_map[name]
|
|
768
|
+
log_step(f"Session token ditemukan di cookie: {name} (len={len(session_token)})")
|
|
769
|
+
break
|
|
770
|
+
|
|
771
|
+
# 2. Jika tidak ada, coba POST /api/auth/session dengan CSRF
|
|
772
|
+
if not session_token:
|
|
773
|
+
log_step("Session token tidak ada di cookie, coba via /api/auth/session...")
|
|
774
|
+
try:
|
|
775
|
+
CSRF_NAMES = [
|
|
776
|
+
"__Host-next-auth.csrf-token", "__Secure-next-auth.csrf-token",
|
|
777
|
+
"next-auth.csrf-token", "__Host-authjs.csrf-token",
|
|
778
|
+
"__Secure-authjs.csrf-token", "authjs.csrf-token"
|
|
779
|
+
]
|
|
780
|
+
csrf_raw = ""
|
|
781
|
+
for n in CSRF_NAMES:
|
|
782
|
+
if n in cookie_map:
|
|
783
|
+
csrf_raw = cookie_map[n]; break
|
|
784
|
+
csrf = csrf_raw.split("|")[0] if "|" in csrf_raw else csrf_raw
|
|
785
|
+
|
|
786
|
+
import urllib.request as _ur, urllib.parse as _up
|
|
787
|
+
_headers = {
|
|
788
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
789
|
+
"Cookie": cookie_str, "Accept": "application/json",
|
|
790
|
+
"Content-Type": "application/json",
|
|
791
|
+
"Origin": "https://app.leonardo.ai",
|
|
792
|
+
"Referer": "https://app.leonardo.ai/",
|
|
793
|
+
}
|
|
794
|
+
if csrf:
|
|
795
|
+
body = json.dumps({"csrfToken": csrf}).encode()
|
|
796
|
+
_req = _ur.Request("https://app.leonardo.ai/api/auth/session", data=body, headers=_headers, method="POST")
|
|
797
|
+
else:
|
|
798
|
+
_req = _ur.Request("https://app.leonardo.ai/api/auth/session", headers=_headers)
|
|
799
|
+
with _ur.urlopen(_req, timeout=15) as _resp:
|
|
800
|
+
_data = json.loads(_resp.read().decode())
|
|
801
|
+
# Cari JWT dari response
|
|
802
|
+
def _walk_jwt(obj, depth=0):
|
|
803
|
+
if depth > 6: return ""
|
|
804
|
+
if isinstance(obj, str) and obj.startswith("eyJ") and len(obj) > 100: return obj
|
|
805
|
+
if isinstance(obj, dict):
|
|
806
|
+
for k, v in obj.items():
|
|
807
|
+
kl = k.lower()
|
|
808
|
+
if "cf_access_token" in kl: continue
|
|
809
|
+
if kl in {"idtoken","accesstoken","id_token","access_token","token"} or "token" in kl or isinstance(v,(dict,list)):
|
|
810
|
+
r = _walk_jwt(v, depth+1)
|
|
811
|
+
if r: return r
|
|
812
|
+
if isinstance(obj, list):
|
|
813
|
+
for v in obj:
|
|
814
|
+
r = _walk_jwt(v, depth+1)
|
|
815
|
+
if r: return r
|
|
816
|
+
return ""
|
|
817
|
+
session_token = _walk_jwt(_data)
|
|
818
|
+
if session_token:
|
|
819
|
+
log_step(f"JWT ditemukan via /api/auth/session (len={len(session_token)})")
|
|
820
|
+
except Exception as e:
|
|
821
|
+
log_step(f"/api/auth/session gagal: {e}")
|
|
822
|
+
|
|
823
|
+
# 3. Fallback: ambil dari intercept (captured dari response listener)
|
|
824
|
+
if not session_token:
|
|
825
|
+
session_token = captured_jwt.get("value", "")
|
|
826
|
+
if session_token:
|
|
827
|
+
log_step(f"Pakai JWT dari intercept (len={len(session_token)})")
|
|
828
|
+
|
|
829
|
+
if not session_token:
|
|
830
|
+
log_step("GAGAL: next-auth.session-token tidak ditemukan di semua metode!")
|
|
831
|
+
log_step(f"Cookies tersedia: {list(cookie_map.keys())[:10]}")
|
|
832
|
+
|
|
833
|
+
if not cookie_str and not session_token:
|
|
834
|
+
sys.stdout.write(json.dumps({"status": "error", "message": "Gagal extract session Leonardo"}) + "\n")
|
|
835
|
+
sys.stdout.flush()
|
|
836
|
+
return False
|
|
837
|
+
|
|
838
|
+
sys.stdout.write(json.dumps({
|
|
839
|
+
"status": "success",
|
|
840
|
+
"cookie": cookie_str,
|
|
841
|
+
"jwt": session_token,
|
|
842
|
+
"balance": 150,
|
|
843
|
+
"left_team": False,
|
|
844
|
+
}) + "\n")
|
|
845
|
+
sys.stdout.flush()
|
|
846
|
+
return True
|
|
847
|
+
|
|
848
|
+
finally:
|
|
849
|
+
try:
|
|
850
|
+
asyncio.set_event_loop(None)
|
|
851
|
+
if hasattr(asyncio, "events") and hasattr(asyncio.events, "_set_running_loop"):
|
|
852
|
+
asyncio.events._set_running_loop(None)
|
|
853
|
+
except Exception: pass
|
|
854
|
+
|
|
855
|
+
if __name__ == "__main__":
|
|
856
|
+
parser = argparse.ArgumentParser()
|
|
857
|
+
parser.add_argument("--email", required=True)
|
|
858
|
+
parser.add_argument("--password", required=True)
|
|
859
|
+
# Node kirim --invite-link (hyphen), dulu kita pakai --invite_link
|
|
860
|
+
parser.add_argument("--invite-link", "--invite_link", dest="invite_link", default="")
|
|
861
|
+
parser.add_argument("--profiles-dir", "--profiles_dir", dest="profiles_dir", default="")
|
|
862
|
+
parser.add_argument("--signup-method", "--signup_method", dest="signup_method", default="email")
|
|
863
|
+
parser.add_argument("--skip-canva", "--skip_canva", dest="skip_canva", action="store_true")
|
|
864
|
+
parser.add_argument("--canva-delay", "--canva_delay", dest="canva_delay", type=int, default=0)
|
|
865
|
+
parser.add_argument("--canva-headless", "--canva_headless", dest="canva_headless", action="store_true")
|
|
866
|
+
parser.add_argument("--leave-canva-team", "--leave_canva_team", dest="leave_canva_team", action="store_true")
|
|
867
|
+
parser.add_argument("--headless", action="store_true")
|
|
868
|
+
# Proxy args dari Node
|
|
869
|
+
parser.add_argument("--proxy", default=None)
|
|
870
|
+
parser.add_argument("--proxy-server", "--proxy_server", dest="proxy_server", default=None)
|
|
871
|
+
parser.add_argument("--proxy-user", "--proxy_user", dest="proxy_user", default=None)
|
|
872
|
+
parser.add_argument("--proxy-pass", "--proxy_pass", dest="proxy_pass", default=None)
|
|
873
|
+
args = parser.parse_args()
|
|
874
|
+
|
|
875
|
+
# Build proxy string dari proxy-server/user/pass jika ada
|
|
876
|
+
proxy = args.proxy
|
|
877
|
+
if not proxy and args.proxy_server:
|
|
878
|
+
if args.proxy_user and args.proxy_pass:
|
|
879
|
+
from urllib.parse import urlparse as _up
|
|
880
|
+
p = _up(args.proxy_server)
|
|
881
|
+
proxy = f"{p.scheme}://{args.proxy_user}:{args.proxy_pass}@{p.hostname}:{p.port}"
|
|
882
|
+
else:
|
|
883
|
+
proxy = args.proxy_server
|
|
884
|
+
|
|
885
|
+
if args.canva_delay > 0:
|
|
886
|
+
import random
|
|
887
|
+
delay = random.randint(1, args.canva_delay)
|
|
888
|
+
log_step(f"Pre-Canva delay {delay}s...")
|
|
889
|
+
time.sleep(delay)
|
|
890
|
+
|
|
891
|
+
try:
|
|
892
|
+
success = run_automation(args.email, args.password, args.invite_link, proxy, args.headless, skip_canva=args.skip_canva)
|
|
893
|
+
if not success:
|
|
894
|
+
# Emit status:error agar Node handler mark sebagai failed (bukan hanya exit code 0)
|
|
895
|
+
sys.stdout.write(json.dumps({"status": "error", "message": "Automation gagal tanpa detail — cek log"}) + "\n")
|
|
896
|
+
sys.stdout.flush()
|
|
897
|
+
except Exception as e:
|
|
898
|
+
log_step(f"ERROR: {str(e)}")
|
|
899
|
+
import traceback
|
|
900
|
+
sys.stderr.write(traceback.format_exc())
|
|
901
|
+
sys.stdout.write(json.dumps({"status": "error", "message": str(e)}) + "\n")
|
|
902
|
+
sys.stdout.flush()
|