@fudrouter/fsrouter 0.6.91 → 0.6.93
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/handlers/chatCore.js +16 -0
- package/dist/routes/oauth/[provider]/[action]/route.js +6 -0
- package/package.json +10 -2
- package/public/.vite/manifest.json +89 -89
- package/public/assets/EndpointPageClient.kiCCiZeY.js +6 -0
- package/public/assets/Loading.BUhrzmZs.js +1 -0
- package/public/assets/NoAuthProxyCard.BI22GO9q.js +89 -0
- package/public/assets/index.DZiGd5-F.js +89 -0
- package/public/assets/page.1oqivCfq.js +1 -0
- package/public/assets/page.B5R9sdV7.js +1 -0
- package/public/assets/page.B5x2tCz5.js +1 -0
- package/public/assets/page.BFjNdqva.js +1 -0
- package/public/assets/page.BGs8R5av.js +1 -0
- package/public/assets/page.BLO61tLD.js +2 -0
- package/public/assets/page.BLWAx8lB.js +1 -0
- package/public/assets/page.BMcCQehz.js +6 -0
- package/public/assets/page.BS1sQouT.js +1 -0
- package/public/assets/page.BUWLUkXc.js +1 -0
- package/public/assets/page.BWaWJ_9t.js +1 -0
- package/public/assets/page.BYhmIkhw.js +1 -0
- package/public/assets/page.Buhl7l9y.js +1 -0
- package/public/assets/page.CBlkUp7t.js +1 -0
- package/public/assets/page.CG2arj7j.js +4 -0
- package/public/assets/page.CLgmM6Q_.js +5 -0
- package/public/assets/page.CLu_guvG.js +1 -0
- package/public/assets/page.COCdcdEM.js +1 -0
- package/public/assets/page.C_zhg1wq.js +1 -0
- package/public/assets/page.CtaqVBbT.js +6 -0
- package/public/assets/page.Cx0GMnA6.js +5 -0
- package/public/assets/page.D47UJLch.js +2 -0
- package/public/assets/page.D9ZFXJ3P.js +1 -0
- package/public/assets/page.D9kdvCh4.js +1 -0
- package/public/assets/page.DO1WoRkx.js +23 -0
- package/public/assets/page.DU201OUE.js +1 -0
- package/public/assets/page.DXxD-0Vl.js +31 -0
- package/public/assets/page.DaJXSZm4.js +1 -0
- package/public/assets/page.Dkeyd3nc.js +1 -0
- package/public/assets/page.DwJ35Irb.js +1 -0
- package/public/assets/page.DzLEPjRK.js +64 -0
- package/public/assets/page.VAojhnMr.js +1 -0
- package/public/assets/page.WBkwLufg.js +1 -0
- package/public/assets/page.laQQR26l.js +1 -0
- package/public/assets/page.nzF601n5.js +1 -0
- package/public/assets/page.tzSMuTXr.js +1 -0
- package/public/assets/page.xCigoHGB.js +1 -0
- package/public/index.html +4 -4
- package/scripts/postinstall.cjs +90 -0
- package/src/automation/cf_token_via_session.py +0 -158
- package/src/automation/cloudflare_signup.py +0 -3689
- package/src/automation/codebuddy_signup.py +0 -636
- package/src/automation/flashloop_signup.py +0 -338
- package/src/automation/kimi_signup.py +0 -401
- package/src/automation/leonardo_signup.py +0 -902
- package/src/automation/openvecta_signup.py +0 -652
- package/src/automation/qoder_signup.py +0 -323
- package/src/automation/test_proxy.py +0 -86
- package/src/automation/weavy_signup.py +0 -1964
|
@@ -1,1964 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Standalone script to automate Weavy auto-signup via Figma OAuth and Ammail.
|
|
3
|
-
Outputs step logs and final results to stdout as JSON lines.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import sys
|
|
7
|
-
import json
|
|
8
|
-
import argparse
|
|
9
|
-
import time
|
|
10
|
-
import logging
|
|
11
|
-
import re
|
|
12
|
-
import sqlite3
|
|
13
|
-
import urllib.request
|
|
14
|
-
import urllib.parse
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
-
|
|
18
|
-
# Patch Playwright's Locator.is_visible to support the timeout argument
|
|
19
|
-
try:
|
|
20
|
-
from playwright.sync_api import Locator
|
|
21
|
-
_orig_is_visible = Locator.is_visible
|
|
22
|
-
def _patched_is_visible(self, *args, **kwargs):
|
|
23
|
-
timeout = kwargs.pop("timeout", None)
|
|
24
|
-
if timeout is None and len(args) > 0:
|
|
25
|
-
timeout = args[0]
|
|
26
|
-
args = args[1:]
|
|
27
|
-
if timeout is not None:
|
|
28
|
-
try:
|
|
29
|
-
self.wait_for(state="visible", timeout=float(timeout))
|
|
30
|
-
return True
|
|
31
|
-
except Exception:
|
|
32
|
-
return False
|
|
33
|
-
return _orig_is_visible(self, *args, **kwargs)
|
|
34
|
-
Locator.is_visible = _patched_is_visible
|
|
35
|
-
except ImportError:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
# Setup logger
|
|
39
|
-
logging.basicConfig(level=logging.WARNING)
|
|
40
|
-
logger = logging.getLogger("weavy_signup")
|
|
41
|
-
|
|
42
|
-
# URLs
|
|
43
|
-
FIGMA_HOME_URL = "https://www.figma.com/"
|
|
44
|
-
FIGMA_SIGNUP_URL = "https://www.figma.com/signup"
|
|
45
|
-
WEAVY_SIGNIN_URL = "https://app.weavy.ai/signin"
|
|
46
|
-
|
|
47
|
-
# Selector banks
|
|
48
|
-
FIGMA_GET_STARTED_SELECTORS = (
|
|
49
|
-
"a:has-text('Get started for free'):visible",
|
|
50
|
-
"a:has-text('Get started'):visible",
|
|
51
|
-
"button:has-text('Get started'):visible",
|
|
52
|
-
"button:has-text('Get started for free'):visible",
|
|
53
|
-
"a:text('Get started for free'):visible",
|
|
54
|
-
"a:text('Get started'):visible",
|
|
55
|
-
"button:text('Get started'):visible",
|
|
56
|
-
"button:text('Get started for free'):visible",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
FIGMA_EMAIL_INPUT_SELECTORS = (
|
|
60
|
-
"input[type='email']",
|
|
61
|
-
"input[name='email']",
|
|
62
|
-
"input[id='email']",
|
|
63
|
-
"input[placeholder*='email' i]",
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
FIGMA_CONTINUE_EMAIL_SELECTORS = (
|
|
67
|
-
"button:has-text('Continue with email')",
|
|
68
|
-
"button:has-text('Continue with Email')",
|
|
69
|
-
"button[type='submit']",
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
FIGMA_PASSWORD_SELECTORS = (
|
|
73
|
-
"input[type='password']",
|
|
74
|
-
"input[name='password']",
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
FIGMA_SUBMIT_SELECTORS = (
|
|
78
|
-
"button[type='submit']",
|
|
79
|
-
"button:has-text('Create account')",
|
|
80
|
-
"button:has-text('Continue')",
|
|
81
|
-
"button:has-text('Next')",
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
WEAVY_FIGMA_LOGIN_SELECTORS = (
|
|
85
|
-
"button:has-text('Log in with Figma')",
|
|
86
|
-
"button[aria-label*='Figma' i]",
|
|
87
|
-
"a:has-text('Log in with Figma')",
|
|
88
|
-
"div[role='button']:has-text('Log in with Figma')",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
FIGMA_OAUTH_ALLOW_SELECTORS = (
|
|
92
|
-
"button:has-text('Allow access')",
|
|
93
|
-
"button:text('Allow')",
|
|
94
|
-
"button:has-text('Authorize')",
|
|
95
|
-
"button:has-text('Approve')",
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
class WeavyAutomationError(RuntimeError):
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
def log_step(msg, *args):
|
|
102
|
-
txt = msg % args if args else msg
|
|
103
|
-
sys.stdout.write(json.dumps({"step": txt}, ensure_ascii=False) + "\n")
|
|
104
|
-
sys.stdout.flush()
|
|
105
|
-
|
|
106
|
-
def safe_email_to_dirname(email: str) -> str:
|
|
107
|
-
cleaned = (email or "").strip().lower()
|
|
108
|
-
cleaned = cleaned.replace("@", "_at_")
|
|
109
|
-
cleaned = re.sub(r"[^a-z0-9._-]+", "_", cleaned)
|
|
110
|
-
cleaned = cleaned.strip("._-")
|
|
111
|
-
return cleaned or "account"
|
|
112
|
-
|
|
113
|
-
def get_db_path() -> Path:
|
|
114
|
-
# 9router default db path
|
|
115
|
-
return Path.home() / ".9router-v2" / "db" / "data.sqlite"
|
|
116
|
-
|
|
117
|
-
def load_settings_db() -> Dict[str, Any]:
|
|
118
|
-
db_path = get_db_path()
|
|
119
|
-
if not db_path.exists():
|
|
120
|
-
return {}
|
|
121
|
-
try:
|
|
122
|
-
conn = sqlite3.connect(str(db_path))
|
|
123
|
-
conn.row_factory = sqlite3.Row
|
|
124
|
-
cursor = conn.cursor()
|
|
125
|
-
cursor.execute("SELECT data FROM settings WHERE id = 1")
|
|
126
|
-
row = cursor.fetchone()
|
|
127
|
-
conn.close()
|
|
128
|
-
if row:
|
|
129
|
-
return json.loads(row["data"])
|
|
130
|
-
except Exception as e:
|
|
131
|
-
logger.warning(f"Error loading settings from DB: {e}")
|
|
132
|
-
return {}
|
|
133
|
-
|
|
134
|
-
# --- Ammail Sync / OTP logic ---
|
|
135
|
-
def iso_to_unix(iso_str: str) -> int:
|
|
136
|
-
from datetime import datetime
|
|
137
|
-
try:
|
|
138
|
-
clean = iso_str.replace("Z", "+00:00")
|
|
139
|
-
dt = datetime.fromisoformat(clean)
|
|
140
|
-
return int(dt.timestamp())
|
|
141
|
-
except Exception as e:
|
|
142
|
-
logger.warning(f"Error parsing date {iso_str}: {e}")
|
|
143
|
-
return int(time.time())
|
|
144
|
-
|
|
145
|
-
def extract_otp_py(text: str, html: str = "", subject: str = "") -> tuple:
|
|
146
|
-
parts = [subject or "", text or "", html or ""]
|
|
147
|
-
haystack = "\n".join(filter(None, parts))
|
|
148
|
-
|
|
149
|
-
clean_text = re.sub(r"<style[\s\S]*?</style>", " ", haystack, flags=re.IGNORECASE)
|
|
150
|
-
clean_text = re.sub(r"<script[\s\S]*?</script>", " ", clean_text, flags=re.IGNORECASE)
|
|
151
|
-
clean_text = re.sub(r"<[^>]+>", " ", clean_text)
|
|
152
|
-
clean_text = re.sub(r"\s+", " ", clean_text).strip()
|
|
153
|
-
|
|
154
|
-
# Figma verification link pattern
|
|
155
|
-
match = re.search(r"https://[a-zA-Z0-9.-]*figma\.com/[^\s\"'>]+", clean_text)
|
|
156
|
-
verify_url = match.group(0) if match else None
|
|
157
|
-
|
|
158
|
-
# Try HTML fallback if no link found in plain text
|
|
159
|
-
if not verify_url and html:
|
|
160
|
-
match = re.search(r'href=["\'](https://[a-zA-Z0-9.-]*figma\.com/[^"\']+)["\']', html)
|
|
161
|
-
if match:
|
|
162
|
-
verify_url = match.group(1)
|
|
163
|
-
|
|
164
|
-
if verify_url:
|
|
165
|
-
verify_url = verify_url.replace("&", "&").replace(""", '"').replace("'", "'")
|
|
166
|
-
|
|
167
|
-
return "", verify_url
|
|
168
|
-
|
|
169
|
-
def sync_ammail_messages(email: str, since_ts: int, settings: dict):
|
|
170
|
-
db_path = get_db_path()
|
|
171
|
-
if not db_path.exists():
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
api_key = settings.get("ammail_api_key")
|
|
175
|
-
base_url = settings.get("ammail_base_url")
|
|
176
|
-
fallback_url = settings.get("ammail_cf_workers_dev_url")
|
|
177
|
-
|
|
178
|
-
if not api_key or (not base_url and not fallback_url):
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
alias = email.split("@")[0]
|
|
182
|
-
domain = email.split("@")[1] if "@" in email else ""
|
|
183
|
-
|
|
184
|
-
urls_to_try = []
|
|
185
|
-
if base_url:
|
|
186
|
-
urls_to_try.append(base_url.rstrip("/"))
|
|
187
|
-
if fallback_url:
|
|
188
|
-
urls_to_try.append(fallback_url.rstrip("/"))
|
|
189
|
-
|
|
190
|
-
messages = []
|
|
191
|
-
for base in urls_to_try:
|
|
192
|
-
url = f"{base}/api/inboxes/{alias}/messages"
|
|
193
|
-
req = urllib.request.Request(
|
|
194
|
-
url,
|
|
195
|
-
headers={
|
|
196
|
-
"X-API-Key": api_key,
|
|
197
|
-
"Accept": "application/json",
|
|
198
|
-
"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"
|
|
199
|
-
}
|
|
200
|
-
)
|
|
201
|
-
try:
|
|
202
|
-
with urllib.request.urlopen(req, timeout=10) as res:
|
|
203
|
-
data = json.loads(res.read().decode("utf-8"))
|
|
204
|
-
messages = data.get("messages", [])
|
|
205
|
-
if messages:
|
|
206
|
-
break
|
|
207
|
-
except Exception as e:
|
|
208
|
-
logger.warning(f"Failed to fetch inbox messages from {url}: {e}")
|
|
209
|
-
|
|
210
|
-
if not messages:
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
for msg in messages:
|
|
214
|
-
msg_id = msg.get("id")
|
|
215
|
-
if not msg_id:
|
|
216
|
-
continue
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
conn = sqlite3.connect(str(db_path))
|
|
220
|
-
cursor = conn.cursor()
|
|
221
|
-
cursor.execute("SELECT 1 FROM ammailOtps WHERE messageShortId = ?", (msg_id,))
|
|
222
|
-
exists = cursor.fetchone()
|
|
223
|
-
conn.close()
|
|
224
|
-
if exists:
|
|
225
|
-
continue
|
|
226
|
-
except Exception as e:
|
|
227
|
-
logger.warning(f"Error checking message existence: {e}")
|
|
228
|
-
continue
|
|
229
|
-
|
|
230
|
-
full_msg = None
|
|
231
|
-
for base in urls_to_try:
|
|
232
|
-
url = f"{base}/api/messages/{msg_id}"
|
|
233
|
-
req = urllib.request.Request(
|
|
234
|
-
url,
|
|
235
|
-
headers={
|
|
236
|
-
"X-API-Key": api_key,
|
|
237
|
-
"Accept": "application/json",
|
|
238
|
-
"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"
|
|
239
|
-
}
|
|
240
|
-
)
|
|
241
|
-
try:
|
|
242
|
-
with urllib.request.urlopen(req, timeout=10) as res:
|
|
243
|
-
data = json.loads(res.read().decode("utf-8"))
|
|
244
|
-
full_msg = data.get("message")
|
|
245
|
-
if full_msg:
|
|
246
|
-
break
|
|
247
|
-
except Exception as e:
|
|
248
|
-
logger.warning(f"Failed to fetch full message {msg_id} from {url}: {e}")
|
|
249
|
-
|
|
250
|
-
if not full_msg:
|
|
251
|
-
continue
|
|
252
|
-
|
|
253
|
-
body_text = str(full_msg.get("text") or msg.get("snippet") or "")
|
|
254
|
-
body_html = str(full_msg.get("html") or "")
|
|
255
|
-
from_data = full_msg.get("from") or msg.get("from") or {}
|
|
256
|
-
sender = str(from_data.get("address") or from_data.get("name") or "")
|
|
257
|
-
subject = str(full_msg.get("subject") or msg.get("subject") or "")
|
|
258
|
-
received_at_str = full_msg.get("receivedAt") or msg.get("receivedAt") or ""
|
|
259
|
-
received_at = iso_to_unix(received_at_str)
|
|
260
|
-
|
|
261
|
-
otp_code, verify_url = extract_otp_py(body_text, body_html, subject)
|
|
262
|
-
|
|
263
|
-
try:
|
|
264
|
-
conn = sqlite3.connect(str(db_path))
|
|
265
|
-
cursor = conn.cursor()
|
|
266
|
-
cursor.execute(
|
|
267
|
-
"""
|
|
268
|
-
INSERT INTO ammailOtps (
|
|
269
|
-
address, alias, domain, sender, subject, otpCode, verifyUrl,
|
|
270
|
-
bodyText, bodyHtml, messageShortId, rawEventJson, receivedAt, usedAt
|
|
271
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
272
|
-
""",
|
|
273
|
-
(
|
|
274
|
-
email, alias, domain, sender, subject, otp_code, verify_url,
|
|
275
|
-
body_text, body_html, msg_id, json.dumps(full_msg), received_at
|
|
276
|
-
)
|
|
277
|
-
)
|
|
278
|
-
conn.commit()
|
|
279
|
-
conn.close()
|
|
280
|
-
log_step(f"Sync: Tersinkronisasi email verifikasi Figma: {subject}")
|
|
281
|
-
except Exception as e:
|
|
282
|
-
logger.warning(f"Error storing synced OTP to SQLite: {e}")
|
|
283
|
-
|
|
284
|
-
def wait_for_otp_from_db(email: str, since_ts: int, settings: dict, timeout: int = 180) -> tuple:
|
|
285
|
-
db_path = get_db_path()
|
|
286
|
-
if not db_path.exists():
|
|
287
|
-
log_step(f"Database file tidak ditemukan di {db_path}, menunggu OTP dialihkan...")
|
|
288
|
-
return "", ""
|
|
289
|
-
|
|
290
|
-
deadline = time.time() + timeout
|
|
291
|
-
while time.time() < deadline:
|
|
292
|
-
try:
|
|
293
|
-
sync_ammail_messages(email, since_ts, settings)
|
|
294
|
-
except Exception as e:
|
|
295
|
-
logger.warning(f"Error syncing Ammail messages: {e}")
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
conn = sqlite3.connect(str(db_path))
|
|
299
|
-
conn.row_factory = sqlite3.Row
|
|
300
|
-
cursor = conn.cursor()
|
|
301
|
-
cursor.execute(
|
|
302
|
-
"SELECT id, otpCode, verifyUrl FROM ammailOtps WHERE LOWER(address) = ? AND receivedAt >= ? AND usedAt = 0 ORDER BY receivedAt DESC LIMIT 1",
|
|
303
|
-
(email.lower(), since_ts)
|
|
304
|
-
)
|
|
305
|
-
row = cursor.fetchone()
|
|
306
|
-
if row:
|
|
307
|
-
otp_id = row["id"]
|
|
308
|
-
otp_code = row["otpCode"]
|
|
309
|
-
verify_url = row["verifyUrl"]
|
|
310
|
-
now_unix = int(time.time())
|
|
311
|
-
cursor.execute("UPDATE ammailOtps SET usedAt = ? WHERE id = ?", (now_unix, otp_id))
|
|
312
|
-
conn.commit()
|
|
313
|
-
conn.close()
|
|
314
|
-
return otp_code, verify_url
|
|
315
|
-
conn.close()
|
|
316
|
-
except Exception as e:
|
|
317
|
-
logger.warning(f"Error querying SQLite: {e}")
|
|
318
|
-
time.sleep(2)
|
|
319
|
-
return "", ""
|
|
320
|
-
|
|
321
|
-
# --- 2Captcha Solvers ---
|
|
322
|
-
class CaptchaSolverError(RuntimeError):
|
|
323
|
-
pass
|
|
324
|
-
|
|
325
|
-
def solve_funcaptcha(
|
|
326
|
-
api_key: str,
|
|
327
|
-
public_key: str,
|
|
328
|
-
page_url: str,
|
|
329
|
-
surl: str,
|
|
330
|
-
data_blob: Optional[str] = None,
|
|
331
|
-
user_agent: Optional[str] = None,
|
|
332
|
-
proxy_str: Optional[str] = None,
|
|
333
|
-
proxy_type: Optional[str] = None,
|
|
334
|
-
*,
|
|
335
|
-
timeout: float = 180.0,
|
|
336
|
-
poll_interval: float = 5.0,
|
|
337
|
-
) -> str:
|
|
338
|
-
if not api_key:
|
|
339
|
-
raise CaptchaSolverError("2Captcha API key kosong")
|
|
340
|
-
|
|
341
|
-
submit_payload = {
|
|
342
|
-
"key": api_key,
|
|
343
|
-
"method": "funcaptcha",
|
|
344
|
-
"publickey": public_key,
|
|
345
|
-
"pageurl": page_url,
|
|
346
|
-
"surl": surl,
|
|
347
|
-
"json": 1,
|
|
348
|
-
}
|
|
349
|
-
if data_blob:
|
|
350
|
-
submit_payload["data[blob]"] = data_blob
|
|
351
|
-
if user_agent:
|
|
352
|
-
submit_payload["userAgent"] = user_agent
|
|
353
|
-
if proxy_str:
|
|
354
|
-
submit_payload["proxy"] = proxy_str
|
|
355
|
-
submit_payload["proxytype"] = proxy_type or "HTTP"
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
r = requests_post("https://2captcha.com/in.php", data=submit_payload)
|
|
359
|
-
data = json.loads(r)
|
|
360
|
-
except Exception as exc:
|
|
361
|
-
raise CaptchaSolverError(f"2Captcha submit gagal: {exc}") from exc
|
|
362
|
-
|
|
363
|
-
if str(data.get("status")) != "1":
|
|
364
|
-
raise CaptchaSolverError(f"2Captcha submit ditolak: {data.get('request') or data}")
|
|
365
|
-
|
|
366
|
-
task_id = str(data.get("request") or "").strip()
|
|
367
|
-
if not task_id:
|
|
368
|
-
raise CaptchaSolverError("2Captcha tidak return task id")
|
|
369
|
-
|
|
370
|
-
log_step("FunCaptcha submitted ke 2Captcha id=%s, polling...", task_id)
|
|
371
|
-
deadline = time.time() + timeout
|
|
372
|
-
while time.time() < deadline:
|
|
373
|
-
time.sleep(poll_interval)
|
|
374
|
-
try:
|
|
375
|
-
res_url = f"https://2captcha.com/res.php?key={api_key}&action=get&id={task_id}&json=1"
|
|
376
|
-
r = requests_get(res_url)
|
|
377
|
-
res_data = json.loads(r)
|
|
378
|
-
except Exception as exc:
|
|
379
|
-
logger.warning("[captcha] Poll error (will retry): %s", exc)
|
|
380
|
-
continue
|
|
381
|
-
|
|
382
|
-
status = str(res_data.get("status"))
|
|
383
|
-
request_val = str(res_data.get("request") or "")
|
|
384
|
-
|
|
385
|
-
if status == "1":
|
|
386
|
-
log_step("FunCaptcha %s solved!", task_id)
|
|
387
|
-
return request_val
|
|
388
|
-
|
|
389
|
-
if request_val == "CAPCHA_NOT_READY":
|
|
390
|
-
continue
|
|
391
|
-
|
|
392
|
-
raise CaptchaSolverError(f"2Captcha task gagal: {request_val}")
|
|
393
|
-
|
|
394
|
-
raise CaptchaSolverError(f"Timeout {int(timeout)}s menunggu solve FunCaptcha dari 2Captcha")
|
|
395
|
-
|
|
396
|
-
def requests_get(url: str) -> str:
|
|
397
|
-
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
398
|
-
with urllib.request.urlopen(req, timeout=30) as response:
|
|
399
|
-
return response.read().decode("utf-8")
|
|
400
|
-
|
|
401
|
-
def requests_post(url: str, data: dict) -> str:
|
|
402
|
-
encoded_data = urllib.parse.urlencode(data).encode("utf-8")
|
|
403
|
-
req = urllib.request.Request(url, data=encoded_data, headers={"User-Agent": "Mozilla/5.0"})
|
|
404
|
-
with urllib.request.urlopen(req, timeout=30) as response:
|
|
405
|
-
return response.read().decode("utf-8")
|
|
406
|
-
|
|
407
|
-
# --- Helper routines ---
|
|
408
|
-
_DEBUG_PATH = "/tmp/9router_debug.png"
|
|
409
|
-
_debug_page = None
|
|
410
|
-
|
|
411
|
-
def update_debug_screenshot(page):
|
|
412
|
-
"""Safely update /tmp/9router_debug.png."""
|
|
413
|
-
if page:
|
|
414
|
-
try:
|
|
415
|
-
page.screenshot(path=_DEBUG_PATH, timeout=2000)
|
|
416
|
-
except Exception:
|
|
417
|
-
pass
|
|
418
|
-
|
|
419
|
-
import time
|
|
420
|
-
_orig_sleep = time.sleep
|
|
421
|
-
|
|
422
|
-
def _patched_sleep(seconds):
|
|
423
|
-
global _debug_page
|
|
424
|
-
if _debug_page is None:
|
|
425
|
-
_orig_sleep(seconds)
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
end_time = time.time() + seconds
|
|
429
|
-
while time.time() < end_time:
|
|
430
|
-
remaining = end_time - time.time()
|
|
431
|
-
chunk = min(0.4, remaining)
|
|
432
|
-
if chunk <= 0:
|
|
433
|
-
break
|
|
434
|
-
_orig_sleep(chunk)
|
|
435
|
-
# Skip screenshots during short sleeps to avoid overhead
|
|
436
|
-
if seconds > 1.0:
|
|
437
|
-
try:
|
|
438
|
-
update_debug_screenshot(_debug_page)
|
|
439
|
-
except Exception:
|
|
440
|
-
pass
|
|
441
|
-
|
|
442
|
-
time.sleep = _patched_sleep
|
|
443
|
-
|
|
444
|
-
def click_first(page, selectors, timeout_ms: int = 8000) -> bool:
|
|
445
|
-
deadline = time.time() + (timeout_ms / 1000.0)
|
|
446
|
-
while time.time() < deadline:
|
|
447
|
-
for sel in selectors:
|
|
448
|
-
try:
|
|
449
|
-
loc = page.locator(sel).first
|
|
450
|
-
count = loc.count()
|
|
451
|
-
visible = False
|
|
452
|
-
if count > 0:
|
|
453
|
-
visible = loc.is_visible(timeout=300)
|
|
454
|
-
log_step("click_first check: sel='%s', count=%d, visible=%s", sel, count, visible)
|
|
455
|
-
if count > 0 and visible:
|
|
456
|
-
try:
|
|
457
|
-
loc.scroll_into_view_if_needed(timeout=600)
|
|
458
|
-
loc.click(timeout=2000, force=True)
|
|
459
|
-
update_debug_screenshot(page)
|
|
460
|
-
return True
|
|
461
|
-
except Exception as e_click:
|
|
462
|
-
log_step("click_first standard click failed, trying evaluate click: %s", str(e_click))
|
|
463
|
-
handle = loc.element_handle(timeout=300)
|
|
464
|
-
if handle:
|
|
465
|
-
page.evaluate("(el) => el.click()", handle)
|
|
466
|
-
update_debug_screenshot(page)
|
|
467
|
-
return True
|
|
468
|
-
except Exception as e:
|
|
469
|
-
log_step("click_first exception: sel='%s' err='%s'", sel, str(e))
|
|
470
|
-
continue
|
|
471
|
-
time.sleep(0.3)
|
|
472
|
-
return False
|
|
473
|
-
|
|
474
|
-
def fill_first(page, selectors, value: str, timeout_ms: int = 8000) -> bool:
|
|
475
|
-
deadline = time.time() + (timeout_ms / 1000.0)
|
|
476
|
-
while time.time() < deadline:
|
|
477
|
-
for sel in selectors:
|
|
478
|
-
try:
|
|
479
|
-
loc = page.locator(sel).first
|
|
480
|
-
count = loc.count()
|
|
481
|
-
visible = False
|
|
482
|
-
if count > 0:
|
|
483
|
-
visible = loc.is_visible(timeout=300)
|
|
484
|
-
log_step("fill_first check: sel='%s', count=%d, visible=%s", sel, count, visible)
|
|
485
|
-
if count > 0 and visible:
|
|
486
|
-
try:
|
|
487
|
-
loc.fill("", timeout=500)
|
|
488
|
-
except Exception:
|
|
489
|
-
pass
|
|
490
|
-
loc.fill(value, timeout=2000)
|
|
491
|
-
update_debug_screenshot(page)
|
|
492
|
-
return True
|
|
493
|
-
except Exception as e:
|
|
494
|
-
log_step("fill_first exception: sel='%s' err='%s'", sel, str(e))
|
|
495
|
-
continue
|
|
496
|
-
time.sleep(0.3)
|
|
497
|
-
return False
|
|
498
|
-
|
|
499
|
-
def complete_figma_onboarding(page, email: str) -> None:
|
|
500
|
-
log_step("Menyelesaikan onboarding Figma...")
|
|
501
|
-
deadline = time.time() + 35.0
|
|
502
|
-
while time.time() < deadline:
|
|
503
|
-
url = page.url
|
|
504
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
505
|
-
log_step("Onboarding selesai, berada di dashboard Figma.")
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
try:
|
|
509
|
-
name_input = page.locator("input[name='name'], input[placeholder*='name' i], input[type='text']").first
|
|
510
|
-
if name_input.count() > 0 and name_input.is_visible(timeout=500):
|
|
511
|
-
val = name_input.input_value(timeout=500)
|
|
512
|
-
if not val:
|
|
513
|
-
name_input.fill(email.split("@")[0], timeout=1000)
|
|
514
|
-
time.sleep(0.5)
|
|
515
|
-
except Exception:
|
|
516
|
-
pass
|
|
517
|
-
|
|
518
|
-
clicked = False
|
|
519
|
-
for text in ("Continue", "Skip", "Next", "Start for free", "Maybe later", "Design", "Personal use", "Start collaborating"):
|
|
520
|
-
try:
|
|
521
|
-
btn = page.locator(f"button:has-text('{text}'), a:has-text('{text}'), div[role='button']:has-text('{text}')").first
|
|
522
|
-
if btn.count() > 0 and btn.is_visible(timeout=500):
|
|
523
|
-
btn.click(timeout=1000)
|
|
524
|
-
log_step(f"Klik tombol onboarding: {text}")
|
|
525
|
-
clicked = True
|
|
526
|
-
time.sleep(1.5)
|
|
527
|
-
break
|
|
528
|
-
except Exception:
|
|
529
|
-
continue
|
|
530
|
-
|
|
531
|
-
update_debug_screenshot(page)
|
|
532
|
-
if not clicked:
|
|
533
|
-
time.sleep(1.0)
|
|
534
|
-
|
|
535
|
-
_debug_context = None
|
|
536
|
-
|
|
537
|
-
def start_debug_screenshots(page):
|
|
538
|
-
"""Start capturing screenshots synchronously."""
|
|
539
|
-
global _debug_page
|
|
540
|
-
_debug_page = page
|
|
541
|
-
update_debug_screenshot(page)
|
|
542
|
-
|
|
543
|
-
def stop_debug_screenshots():
|
|
544
|
-
"""Stop capturing screenshots and clean up temp screenshot file."""
|
|
545
|
-
global _debug_page
|
|
546
|
-
_debug_page = None
|
|
547
|
-
try:
|
|
548
|
-
import os
|
|
549
|
-
if os.path.exists(_DEBUG_PATH):
|
|
550
|
-
os.remove(_DEBUG_PATH)
|
|
551
|
-
except Exception:
|
|
552
|
-
pass
|
|
553
|
-
|
|
554
|
-
def save_debug_screenshots(context, profiles_dir: str, email: str, prefix="failure"):
|
|
555
|
-
try:
|
|
556
|
-
debug_dir = Path(profiles_dir).parent / "weavy_debug" / safe_email_to_dirname(email)
|
|
557
|
-
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
558
|
-
pages = getattr(context, "pages", []) or []
|
|
559
|
-
for i, pg in enumerate(pages):
|
|
560
|
-
try:
|
|
561
|
-
url = pg.url or "empty"
|
|
562
|
-
ts = int(time.time())
|
|
563
|
-
sanitized_url = re.sub(r"[^a-zA-Z0-9.-]", "_", url)[:50]
|
|
564
|
-
screenshot_path = debug_dir / f"{prefix}_page_{i}_{sanitized_url}_{ts}.png"
|
|
565
|
-
html_path = debug_dir / f"{prefix}_page_{i}_{sanitized_url}_{ts}.html"
|
|
566
|
-
|
|
567
|
-
log_step(f"Mengambil screenshot halaman {i} (URL: {url[:60]})...")
|
|
568
|
-
pg.screenshot(path=str(screenshot_path), full_page=True, timeout=10000)
|
|
569
|
-
try:
|
|
570
|
-
html_path.write_text(pg.content() or "", encoding="utf-8")
|
|
571
|
-
except Exception:
|
|
572
|
-
pass
|
|
573
|
-
log_step(f"Screenshot disimpan di: {screenshot_path}")
|
|
574
|
-
except Exception as e:
|
|
575
|
-
logger.warning(f"Failed to screenshot page {i}: {e}")
|
|
576
|
-
except Exception as e:
|
|
577
|
-
logger.warning(f"Error in debug screenshot function: {e}")
|
|
578
|
-
|
|
579
|
-
def save_immediate_debug(page, email: str, name: str):
|
|
580
|
-
try:
|
|
581
|
-
debug_dir = Path("/home/data/Project/9router/profiles").parent / "weavy_debug" / safe_email_to_dirname(email)
|
|
582
|
-
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
583
|
-
ts = int(time.time())
|
|
584
|
-
screenshot_path = debug_dir / f"{name}_{ts}.png"
|
|
585
|
-
html_path = debug_dir / f"{name}_{ts}.html"
|
|
586
|
-
|
|
587
|
-
log_step(f"Mengambil screenshot diagnostik '{name}'...")
|
|
588
|
-
page.screenshot(path=str(screenshot_path), full_page=True, timeout=10000)
|
|
589
|
-
try:
|
|
590
|
-
html_path.write_text(page.content() or "", encoding="utf-8")
|
|
591
|
-
except Exception:
|
|
592
|
-
pass
|
|
593
|
-
log_step(f"Screenshot diagnostik '{name}' disimpan di: {screenshot_path}")
|
|
594
|
-
except Exception as e:
|
|
595
|
-
logger.warning(f"Failed to save immediate debug {name}: {e}")
|
|
596
|
-
|
|
597
|
-
# --- Cloudflare bypass helper ---
|
|
598
|
-
def is_on_turnstile_page(page) -> bool:
|
|
599
|
-
try:
|
|
600
|
-
title = page.title() or ""
|
|
601
|
-
if "just a moment" in title.lower() or "security verification" in title.lower():
|
|
602
|
-
return True
|
|
603
|
-
except Exception:
|
|
604
|
-
pass
|
|
605
|
-
|
|
606
|
-
# Check if the Cloudflare Turnstile token is already populated.
|
|
607
|
-
# If it is, then the challenge has been resolved, so we are not blocked.
|
|
608
|
-
try:
|
|
609
|
-
token = page.evaluate("() => { const el = document.getElementsByName('cf-turnstile-response')[0] || document.getElementById('cf-turnstile-response'); return el ? el.value : null; }")
|
|
610
|
-
if token is not None:
|
|
611
|
-
return len(token.strip()) == 0
|
|
612
|
-
except Exception:
|
|
613
|
-
pass
|
|
614
|
-
|
|
615
|
-
for sel in [
|
|
616
|
-
"text=Just a moment",
|
|
617
|
-
"text=Verifying you are human",
|
|
618
|
-
"text=Verify you are human",
|
|
619
|
-
"text=Checking your browser",
|
|
620
|
-
"#challenge-form",
|
|
621
|
-
"#cf-challenge-running",
|
|
622
|
-
]:
|
|
623
|
-
try:
|
|
624
|
-
loc = page.locator(sel).first
|
|
625
|
-
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
626
|
-
return True
|
|
627
|
-
except Exception:
|
|
628
|
-
continue
|
|
629
|
-
|
|
630
|
-
try:
|
|
631
|
-
for f in page.frames:
|
|
632
|
-
url = f.url or ""
|
|
633
|
-
if ("challenges.cloudflare.com" in url or "turnstile" in url) and "challenge-platform" in url:
|
|
634
|
-
# If there's an iframe, we still check the response token.
|
|
635
|
-
# If the token is populated, then Turnstile is resolved.
|
|
636
|
-
token = page.evaluate("() => { const el = document.getElementsByName('cf-turnstile-response')[0]; return el ? el.value : ''; }")
|
|
637
|
-
if token and len(token.strip()) > 0:
|
|
638
|
-
return False
|
|
639
|
-
return True
|
|
640
|
-
except Exception:
|
|
641
|
-
pass
|
|
642
|
-
return False
|
|
643
|
-
|
|
644
|
-
def try_click_turnstile_checkbox(page) -> bool:
|
|
645
|
-
target_frame = None
|
|
646
|
-
try:
|
|
647
|
-
for f in page.frames:
|
|
648
|
-
url = f.url or ""
|
|
649
|
-
if "challenges.cloudflare.com" in url or "turnstile" in url:
|
|
650
|
-
target_frame = f
|
|
651
|
-
break
|
|
652
|
-
except Exception:
|
|
653
|
-
pass
|
|
654
|
-
|
|
655
|
-
if target_frame:
|
|
656
|
-
# Check if the iframe itself is actually visible (so we don't click invisible/background widgets)
|
|
657
|
-
try:
|
|
658
|
-
frame_element = page.locator("iframe[src*='challenges.cloudflare.com'], iframe[src*='turnstile']").first
|
|
659
|
-
if frame_element.count() > 0 and not frame_element.is_visible(timeout=500):
|
|
660
|
-
log_step("Turnstile iframe terdeteksi tetapi invisible. Melewati klik.")
|
|
661
|
-
return False
|
|
662
|
-
except Exception:
|
|
663
|
-
pass
|
|
664
|
-
|
|
665
|
-
# Check if the checkbox inside the iframe is already checked
|
|
666
|
-
for cb_sel in [
|
|
667
|
-
"input[type='checkbox']",
|
|
668
|
-
"[role='checkbox']",
|
|
669
|
-
]:
|
|
670
|
-
try:
|
|
671
|
-
box = target_frame.locator(cb_sel).first
|
|
672
|
-
if box.count() > 0:
|
|
673
|
-
checked = box.get_attribute("aria-checked") or box.get_attribute("checked")
|
|
674
|
-
if checked == "true" or checked == "checked" or box.is_checked():
|
|
675
|
-
log_step("Turnstile checkbox sudah ter-centang/checked. Melewati klik.")
|
|
676
|
-
return True
|
|
677
|
-
except Exception:
|
|
678
|
-
pass
|
|
679
|
-
|
|
680
|
-
for cb_sel in [
|
|
681
|
-
"input[type='checkbox']",
|
|
682
|
-
"label.cb-lb input",
|
|
683
|
-
"[role='checkbox']",
|
|
684
|
-
"div.ctp-checkbox-label",
|
|
685
|
-
]:
|
|
686
|
-
try:
|
|
687
|
-
box = target_frame.locator(cb_sel).first
|
|
688
|
-
if box.count() > 0:
|
|
689
|
-
box.click(timeout=3000)
|
|
690
|
-
log_step(f"Turnstile checkbox di-click via target_frame.locator({cb_sel})")
|
|
691
|
-
return True
|
|
692
|
-
except Exception:
|
|
693
|
-
continue
|
|
694
|
-
|
|
695
|
-
try:
|
|
696
|
-
handle = target_frame.frame_element()
|
|
697
|
-
bbox = handle.bounding_box() if handle else None
|
|
698
|
-
if bbox:
|
|
699
|
-
origin_x = bbox["x"]
|
|
700
|
-
origin_y = bbox["y"]
|
|
701
|
-
iframe_w = bbox["width"]
|
|
702
|
-
iframe_h = bbox["height"]
|
|
703
|
-
|
|
704
|
-
target_x = origin_x + 28
|
|
705
|
-
target_y = origin_y + 32
|
|
706
|
-
page.mouse.move(target_x, target_y, steps=10)
|
|
707
|
-
time.sleep(0.3)
|
|
708
|
-
page.mouse.click(target_x, target_y)
|
|
709
|
-
log_step(f"Turnstile checkbox di-click via mouse di coordinate ({target_x}, {target_y})")
|
|
710
|
-
return True
|
|
711
|
-
except Exception as e_mouse:
|
|
712
|
-
logger.debug(f"Turnstile mouse click error: {e_mouse}")
|
|
713
|
-
|
|
714
|
-
for iframe_sel in [
|
|
715
|
-
"iframe[src*='challenges.cloudflare.com']",
|
|
716
|
-
"iframe[src*='turnstile']",
|
|
717
|
-
"iframe[title*='Cloudflare' i]",
|
|
718
|
-
]:
|
|
719
|
-
for cb_sel in [
|
|
720
|
-
"input[type='checkbox']",
|
|
721
|
-
"label.cb-lb input",
|
|
722
|
-
"[role='checkbox']",
|
|
723
|
-
]:
|
|
724
|
-
try:
|
|
725
|
-
box = page.frame_locator(iframe_sel).locator(cb_sel).first
|
|
726
|
-
if box.count() > 0:
|
|
727
|
-
box.click(timeout=3000)
|
|
728
|
-
log_step(f"Turnstile checkbox di-click via frame_locator fallback")
|
|
729
|
-
return True
|
|
730
|
-
except Exception:
|
|
731
|
-
continue
|
|
732
|
-
return False
|
|
733
|
-
|
|
734
|
-
def wait_for_cf_clearance(page, timeout: float = 30.0):
|
|
735
|
-
if not is_on_turnstile_page(page):
|
|
736
|
-
return True
|
|
737
|
-
|
|
738
|
-
log_step("Halaman Turnstile terdeteksi, menunggu resolve...")
|
|
739
|
-
deadline = time.time() + timeout
|
|
740
|
-
click_attempts = 0
|
|
741
|
-
max_click_attempts = 4
|
|
742
|
-
next_click_at = time.time() + 5.0
|
|
743
|
-
|
|
744
|
-
while time.time() < deadline:
|
|
745
|
-
time.sleep(2.0)
|
|
746
|
-
if not is_on_turnstile_page(page):
|
|
747
|
-
log_step("Turnstile selesai (resolved otomatis).")
|
|
748
|
-
try:
|
|
749
|
-
page.wait_for_load_state("networkidle", timeout=5000)
|
|
750
|
-
except Exception:
|
|
751
|
-
pass
|
|
752
|
-
return True
|
|
753
|
-
|
|
754
|
-
now = time.time()
|
|
755
|
-
if click_attempts < max_click_attempts and now >= next_click_at:
|
|
756
|
-
click_attempts += 1
|
|
757
|
-
log_step(f"Mencoba klik Turnstile checkbox (Attempt {click_attempts}/{max_click_attempts})...")
|
|
758
|
-
try_click_turnstile_checkbox(page)
|
|
759
|
-
next_click_at = now + 8.0
|
|
760
|
-
time.sleep(2.0)
|
|
761
|
-
if not is_on_turnstile_page(page):
|
|
762
|
-
log_step("Turnstile selesai setelah di-klik.")
|
|
763
|
-
try:
|
|
764
|
-
page.wait_for_load_state("networkidle", timeout=5000)
|
|
765
|
-
except Exception:
|
|
766
|
-
pass
|
|
767
|
-
return True
|
|
768
|
-
|
|
769
|
-
log_step("Turnstile challenge timeout.")
|
|
770
|
-
return False
|
|
771
|
-
|
|
772
|
-
def solve_and_inject_figma_captcha(page, captured_urls, settings, proxy_server=None, proxy_user=None, proxy_pass=None):
|
|
773
|
-
log_step("Menunggu captcha Arkose Labs terintercept...")
|
|
774
|
-
deadline = time.time() + 15.0
|
|
775
|
-
frame_url = None
|
|
776
|
-
while time.time() < deadline:
|
|
777
|
-
for u in captured_urls:
|
|
778
|
-
if "dataExchangeBlob=" in u:
|
|
779
|
-
frame_url = u
|
|
780
|
-
break
|
|
781
|
-
if frame_url:
|
|
782
|
-
break
|
|
783
|
-
time.sleep(0.5)
|
|
784
|
-
|
|
785
|
-
if not frame_url:
|
|
786
|
-
log_step("Tidak ada Arkose frame URL terintercept dengan dataExchangeBlob.")
|
|
787
|
-
return False
|
|
788
|
-
|
|
789
|
-
log_step("Captcha Figma terdeteksi. Memulai proses pemecahan...")
|
|
790
|
-
api_key = settings.get("codebuddy_2captcha_api_key")
|
|
791
|
-
if not api_key:
|
|
792
|
-
raise WeavyAutomationError("2Captcha API key tidak ditemukan di settings database")
|
|
793
|
-
|
|
794
|
-
user_agent = None
|
|
795
|
-
try:
|
|
796
|
-
user_agent = page.evaluate("navigator.userAgent")
|
|
797
|
-
except Exception:
|
|
798
|
-
pass
|
|
799
|
-
|
|
800
|
-
proxy_str = None
|
|
801
|
-
proxy_type = None
|
|
802
|
-
|
|
803
|
-
parsed = urllib.parse.urlparse(frame_url)
|
|
804
|
-
query_params = urllib.parse.parse_qs(parsed.query)
|
|
805
|
-
data_blob_raw = query_params.get("dataExchangeBlob", [None])[0]
|
|
806
|
-
data_blob = data_blob_raw
|
|
807
|
-
public_key = query_params.get("publicKey", ["A207F8A1-ED09-4325-ACE6-C8E26A458FBA"])[0]
|
|
808
|
-
|
|
809
|
-
log_step(f"Public Key: {public_key}")
|
|
810
|
-
if data_blob:
|
|
811
|
-
log_step(f"Data Blob Length (raw): {len(data_blob_raw)}")
|
|
812
|
-
log_step(f"Data Blob preview: {data_blob[:80]}...")
|
|
813
|
-
else:
|
|
814
|
-
log_step("Data Blob is empty/None")
|
|
815
|
-
|
|
816
|
-
# Set page_url to the top-level page URL of Figma where the captcha is loaded
|
|
817
|
-
target_page_url = page.url if "figma.com" in page.url else "https://www.figma.com/signup"
|
|
818
|
-
log_step(f"Menggunakan page_url untuk 2Captcha: {target_page_url}")
|
|
819
|
-
|
|
820
|
-
try:
|
|
821
|
-
token = solve_funcaptcha(
|
|
822
|
-
api_key=api_key,
|
|
823
|
-
public_key=public_key,
|
|
824
|
-
page_url=target_page_url,
|
|
825
|
-
surl="https://figma-api.arkoselabs.com",
|
|
826
|
-
data_blob=data_blob,
|
|
827
|
-
user_agent=None,
|
|
828
|
-
proxy_str=proxy_str,
|
|
829
|
-
proxy_type=proxy_type,
|
|
830
|
-
)
|
|
831
|
-
log_step("Captcha berhasil di-solve!")
|
|
832
|
-
except CaptchaSolverError as e:
|
|
833
|
-
raise WeavyAutomationError(f"Gagal menyelesaikan captcha: {e}")
|
|
834
|
-
|
|
835
|
-
log_step("Captcha solved. Mengirimkan token ke Figma...")
|
|
836
|
-
captcha_frame = None
|
|
837
|
-
for f in page.frames:
|
|
838
|
-
if "verify.figma.com/arkose_frame.html" in f.url:
|
|
839
|
-
captcha_frame = f
|
|
840
|
-
break
|
|
841
|
-
|
|
842
|
-
if captcha_frame:
|
|
843
|
-
captcha_frame.evaluate(
|
|
844
|
-
f"""
|
|
845
|
-
parent.postMessage(JSON.stringify({{
|
|
846
|
-
eventId: "challenge-complete",
|
|
847
|
-
payload: {{
|
|
848
|
-
sessionToken: "{token}"
|
|
849
|
-
}}
|
|
850
|
-
}}), "*");
|
|
851
|
-
"""
|
|
852
|
-
)
|
|
853
|
-
else:
|
|
854
|
-
log_step("Frame captcha tidak ditemukan, injeksi token via main page...")
|
|
855
|
-
page.evaluate(
|
|
856
|
-
f"""
|
|
857
|
-
window.postMessage(JSON.stringify({{
|
|
858
|
-
eventId: "challenge-complete",
|
|
859
|
-
payload: {{
|
|
860
|
-
sessionToken: "{token}"
|
|
861
|
-
}}
|
|
862
|
-
}}), "*");
|
|
863
|
-
"""
|
|
864
|
-
)
|
|
865
|
-
time.sleep(5.0)
|
|
866
|
-
return True
|
|
867
|
-
|
|
868
|
-
# --- Main execution flow ---
|
|
869
|
-
def run_automation(args, settings):
|
|
870
|
-
email = args.email
|
|
871
|
-
password = args.password
|
|
872
|
-
profiles_dir = Path(args.profiles_dir)
|
|
873
|
-
profile_dir = (profiles_dir / safe_email_to_dirname(email)).resolve()
|
|
874
|
-
start_time = int(time.time()) - 15
|
|
875
|
-
|
|
876
|
-
try:
|
|
877
|
-
from camoufox.sync_api import Camoufox
|
|
878
|
-
from camoufox.addons import DefaultAddons
|
|
879
|
-
except ImportError as e:
|
|
880
|
-
raise WeavyAutomationError(
|
|
881
|
-
"Camoufox not installed. Run in venv: pip install camoufox && python -m camoufox fetch"
|
|
882
|
-
) from e
|
|
883
|
-
|
|
884
|
-
if getattr(args, "clean", False) and profile_dir.exists():
|
|
885
|
-
try:
|
|
886
|
-
import shutil
|
|
887
|
-
shutil.rmtree(str(profile_dir), ignore_errors=True)
|
|
888
|
-
log_step("Konteks profile browser lama dibersihkan (profile dir deleted).")
|
|
889
|
-
except Exception as e_clean:
|
|
890
|
-
logger.debug(f"Failed to delete profile dir: {e_clean}")
|
|
891
|
-
|
|
892
|
-
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
893
|
-
launch_kwargs = dict(
|
|
894
|
-
headless=args.headless,
|
|
895
|
-
persistent_context=True, no_viewport=True,
|
|
896
|
-
user_data_dir=str(profile_dir),
|
|
897
|
-
humanize=True,
|
|
898
|
-
geoip=True,
|
|
899
|
-
locale="en-US",
|
|
900
|
-
os=("windows", "macos", "linux"),
|
|
901
|
-
window=(1920, 1080),
|
|
902
|
-
exclude_addons=[DefaultAddons.UBO],
|
|
903
|
-
firefox_user_prefs={
|
|
904
|
-
"network.trr.mode": 5,
|
|
905
|
-
},
|
|
906
|
-
no_viewport=True,
|
|
907
|
-
)
|
|
908
|
-
|
|
909
|
-
if args.proxy_server:
|
|
910
|
-
proxy_dict = {"server": args.proxy_server}
|
|
911
|
-
if args.proxy_user:
|
|
912
|
-
proxy_dict["username"] = args.proxy_user
|
|
913
|
-
if args.proxy_pass:
|
|
914
|
-
proxy_dict["password"] = args.proxy_pass
|
|
915
|
-
launch_kwargs["proxy"] = proxy_dict
|
|
916
|
-
log_step(f"Menggunakan proxy: {args.proxy_server}")
|
|
917
|
-
|
|
918
|
-
log_step("Meluncurkan browser...")
|
|
919
|
-
|
|
920
|
-
# Try launching with adaptive kwargs for camoufox API versions
|
|
921
|
-
browser = None
|
|
922
|
-
try:
|
|
923
|
-
browser = Camoufox(**launch_kwargs)
|
|
924
|
-
except TypeError:
|
|
925
|
-
for drop in ("window", "os", "geoip", "humanize"):
|
|
926
|
-
launch_kwargs.pop(drop, None)
|
|
927
|
-
try:
|
|
928
|
-
browser = Camoufox(**launch_kwargs)
|
|
929
|
-
break
|
|
930
|
-
except TypeError:
|
|
931
|
-
continue
|
|
932
|
-
if not browser:
|
|
933
|
-
launch_kwargs.pop("locale", None)
|
|
934
|
-
browser = Camoufox(**launch_kwargs)
|
|
935
|
-
|
|
936
|
-
browser_ctx = browser.__enter__()
|
|
937
|
-
try:
|
|
938
|
-
global _debug_context
|
|
939
|
-
context = getattr(browser_ctx, "context", None) or browser_ctx
|
|
940
|
-
_debug_context = context
|
|
941
|
-
|
|
942
|
-
# Block Google One Tap requests to prevent click interception overlays
|
|
943
|
-
try:
|
|
944
|
-
context.route("**/gsi/**", lambda route: route.abort())
|
|
945
|
-
except Exception as e_route:
|
|
946
|
-
logger.debug(f"Failed to setup route block: {e_route}")
|
|
947
|
-
|
|
948
|
-
# Tutup semua tab lama dari run sebelumnya (kecuali yang terakhir)
|
|
949
|
-
try:
|
|
950
|
-
existing_pages = context.pages
|
|
951
|
-
if len(existing_pages) > 1:
|
|
952
|
-
for old_page in existing_pages[:-1]:
|
|
953
|
-
try:
|
|
954
|
-
old_page.close()
|
|
955
|
-
except Exception:
|
|
956
|
-
pass
|
|
957
|
-
except Exception:
|
|
958
|
-
pass
|
|
959
|
-
|
|
960
|
-
# Clear cookies if --clean flag is provided
|
|
961
|
-
if getattr(args, "clean", False):
|
|
962
|
-
try:
|
|
963
|
-
context.clear_cookies()
|
|
964
|
-
log_step("Konteks browser dibersihkan (cookies cleared).")
|
|
965
|
-
except Exception as e_cookies:
|
|
966
|
-
logger.debug(f"Clear cookies error: {e_cookies}")
|
|
967
|
-
|
|
968
|
-
page = context.new_page()
|
|
969
|
-
start_debug_screenshots(page)
|
|
970
|
-
|
|
971
|
-
# Set up global request listener on context to capture Arkose Labs / FunCaptcha URLs and Weavy Auth JWT
|
|
972
|
-
captured_urls = []
|
|
973
|
-
captured_jwt = [None]
|
|
974
|
-
captured_firebase_api_key = [None]
|
|
975
|
-
def handle_request(req):
|
|
976
|
-
url = req.url
|
|
977
|
-
if "arkose" in url.lower() or "verify" in url.lower() or "dataexchange" in url.lower():
|
|
978
|
-
log_step(f"Intercepted request: {url[:100]}")
|
|
979
|
-
captured_urls.append(url)
|
|
980
|
-
if "api.weavy.ai" in url:
|
|
981
|
-
try:
|
|
982
|
-
headers = req.all_headers()
|
|
983
|
-
auth = headers.get("authorization")
|
|
984
|
-
if auth and auth.startswith("Bearer "):
|
|
985
|
-
token = auth.split(" ", 1)[1]
|
|
986
|
-
if token and len(token) > 20:
|
|
987
|
-
captured_jwt[0] = token
|
|
988
|
-
except Exception:
|
|
989
|
-
pass
|
|
990
|
-
# Capture Firebase API key from identitytoolkit or securetoken requests
|
|
991
|
-
if not captured_firebase_api_key[0] and ("identitytoolkit.googleapis.com" in url or "securetoken.googleapis.com" in url):
|
|
992
|
-
try:
|
|
993
|
-
import urllib.parse as _up
|
|
994
|
-
qs = _up.parse_qs(_up.urlparse(url).query)
|
|
995
|
-
key = qs.get("key", [None])[0]
|
|
996
|
-
if key and key.startswith("AIza"):
|
|
997
|
-
captured_firebase_api_key[0] = key
|
|
998
|
-
except Exception:
|
|
999
|
-
pass
|
|
1000
|
-
|
|
1001
|
-
context.on("request", handle_request)
|
|
1002
|
-
|
|
1003
|
-
already_registered = getattr(args, "gsuite", False)
|
|
1004
|
-
|
|
1005
|
-
if not getattr(args, "gsuite", False):
|
|
1006
|
-
# 1. Check if already logged in to Figma
|
|
1007
|
-
log_step("Membuka Figma untuk mengecek status login...")
|
|
1008
|
-
page.goto("https://www.figma.com/")
|
|
1009
|
-
try:
|
|
1010
|
-
page.wait_for_load_state("networkidle", timeout=6000)
|
|
1011
|
-
except Exception:
|
|
1012
|
-
pass
|
|
1013
|
-
|
|
1014
|
-
url = page.url
|
|
1015
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
1016
|
-
log_step("Figma sudah login (halaman dashboard). Melewati login & register.")
|
|
1017
|
-
already_registered = True
|
|
1018
|
-
|
|
1019
|
-
if not already_registered:
|
|
1020
|
-
signup_success = False
|
|
1021
|
-
fallback_to_login = False
|
|
1022
|
-
|
|
1023
|
-
for sa_attempt in range(1, 4):
|
|
1024
|
-
log_step(f"Memulai pendaftaran Figma (Percobaan {sa_attempt}/3)...")
|
|
1025
|
-
if sa_attempt > 1:
|
|
1026
|
-
try:
|
|
1027
|
-
context.clear_cookies()
|
|
1028
|
-
log_step("Cookies dibersihkan untuk pendaftaran baru.")
|
|
1029
|
-
except Exception:
|
|
1030
|
-
pass
|
|
1031
|
-
|
|
1032
|
-
try:
|
|
1033
|
-
captured_urls.clear()
|
|
1034
|
-
# Reset browser storage state to prevent telemetry tracking across retries
|
|
1035
|
-
try:
|
|
1036
|
-
context.clear_cookies()
|
|
1037
|
-
page.goto("about:blank")
|
|
1038
|
-
page.evaluate("() => { try { localStorage.clear(); sessionStorage.clear(); } catch(e){} }")
|
|
1039
|
-
log_step("Browser storage (cookies, localStorage, sessionStorage) dibersihkan.")
|
|
1040
|
-
except Exception as e_clear:
|
|
1041
|
-
logger.debug(f"Failed to clear storage: {e_clear}")
|
|
1042
|
-
|
|
1043
|
-
log_step("Navigasi langsung ke Figma signup page...")
|
|
1044
|
-
page.goto(FIGMA_SIGNUP_URL)
|
|
1045
|
-
try:
|
|
1046
|
-
page.wait_for_load_state("networkidle", timeout=6000)
|
|
1047
|
-
except Exception:
|
|
1048
|
-
pass
|
|
1049
|
-
|
|
1050
|
-
time.sleep(2.0)
|
|
1051
|
-
|
|
1052
|
-
# Close cookie banner if present
|
|
1053
|
-
try:
|
|
1054
|
-
cookie_btn = page.locator("button:has-text('Allow all cookies')").first
|
|
1055
|
-
if cookie_btn.count() > 0 and cookie_btn.is_visible():
|
|
1056
|
-
log_step("Cookie banner terdeteksi. Menutup cookie banner...")
|
|
1057
|
-
cookie_btn.click(timeout=2000, force=True)
|
|
1058
|
-
time.sleep(1.0)
|
|
1059
|
-
except Exception:
|
|
1060
|
-
pass
|
|
1061
|
-
|
|
1062
|
-
# Remove Google One Tap widget to prevent interception
|
|
1063
|
-
try:
|
|
1064
|
-
page.evaluate("() => { const el = document.getElementById('google_one_tap'); if (el) el.remove(); }")
|
|
1065
|
-
except Exception:
|
|
1066
|
-
pass
|
|
1067
|
-
|
|
1068
|
-
log_step("Mengisi email Figma untuk register...")
|
|
1069
|
-
if not fill_first(page, FIGMA_EMAIL_INPUT_SELECTORS, email, timeout_ms=8000):
|
|
1070
|
-
raise WeavyAutomationError("Email input figma tidak ditemukan")
|
|
1071
|
-
|
|
1072
|
-
# Check if password field is already visible (single-step signup form)
|
|
1073
|
-
password_visible = False
|
|
1074
|
-
for sel in FIGMA_PASSWORD_SELECTORS:
|
|
1075
|
-
try:
|
|
1076
|
-
loc = page.locator(sel).first
|
|
1077
|
-
if loc.count() > 0 and loc.is_visible(timeout=500):
|
|
1078
|
-
password_visible = True
|
|
1079
|
-
break
|
|
1080
|
-
except Exception:
|
|
1081
|
-
pass
|
|
1082
|
-
|
|
1083
|
-
if not password_visible:
|
|
1084
|
-
# Click Continue to show password (multi-step signup form)
|
|
1085
|
-
if not click_first(page, FIGMA_CONTINUE_EMAIL_SELECTORS, timeout_ms=5000):
|
|
1086
|
-
raise WeavyAutomationError("Tombol continue email figma tidak ditemukan")
|
|
1087
|
-
time.sleep(2.0)
|
|
1088
|
-
|
|
1089
|
-
log_step("Mengisi password Figma untuk register...")
|
|
1090
|
-
if not fill_first(page, FIGMA_PASSWORD_SELECTORS, password, timeout_ms=8000):
|
|
1091
|
-
raise WeavyAutomationError("Password input figma tidak ditemukan")
|
|
1092
|
-
|
|
1093
|
-
# Try to prefill Name if asked
|
|
1094
|
-
try:
|
|
1095
|
-
name_loc = page.locator("input[name='name']").first
|
|
1096
|
-
if name_loc.count() > 0 and name_loc.is_visible(timeout=500):
|
|
1097
|
-
name_loc.fill("Jane Doe")
|
|
1098
|
-
except Exception:
|
|
1099
|
-
pass
|
|
1100
|
-
|
|
1101
|
-
log_step("Mengirimkan pendaftaran Figma...")
|
|
1102
|
-
if not click_first(page, FIGMA_SUBMIT_SELECTORS, timeout_ms=5000):
|
|
1103
|
-
page.keyboard.press("Enter")
|
|
1104
|
-
|
|
1105
|
-
time.sleep(4.0)
|
|
1106
|
-
|
|
1107
|
-
# CHECK IF EMAIL ALREADY EXISTS ON FIGMA SIGNUP PAGE
|
|
1108
|
-
try:
|
|
1109
|
-
err_msg = page.locator("text=already exists, text=already registered, text=Choose another email, text=Log in instead, text=incorrect password").first
|
|
1110
|
-
if err_msg.count() > 0 and err_msg.is_visible(timeout=1500):
|
|
1111
|
-
log_step("Email sudah terdaftar di Figma. Beralih ke flow login...")
|
|
1112
|
-
fallback_to_login = True
|
|
1113
|
-
break
|
|
1114
|
-
except Exception:
|
|
1115
|
-
pass
|
|
1116
|
-
|
|
1117
|
-
save_immediate_debug(page, email, "post_submit")
|
|
1118
|
-
|
|
1119
|
-
# Check if autologged in on submit
|
|
1120
|
-
url = page.url
|
|
1121
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
1122
|
-
log_step("Terdeteksi akun Figma sudah terdaftar dan masuk dashboard.")
|
|
1123
|
-
signup_success = True
|
|
1124
|
-
break
|
|
1125
|
-
|
|
1126
|
-
log_step("Menunggu validasi captcha Arkose Labs atau redirect...")
|
|
1127
|
-
deadline = time.time() + 20.0
|
|
1128
|
-
has_captcha = False
|
|
1129
|
-
start_btn_clicked = False
|
|
1130
|
-
puzzle_found_frame = None
|
|
1131
|
-
loop_start = time.time()
|
|
1132
|
-
|
|
1133
|
-
while time.time() < deadline:
|
|
1134
|
-
url = page.url
|
|
1135
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
1136
|
-
signup_success = True
|
|
1137
|
-
break
|
|
1138
|
-
|
|
1139
|
-
if "figma.com/verify" in url or "verify?fuid=" in url:
|
|
1140
|
-
log_step("Halaman verifikasi email Figma terdeteksi.")
|
|
1141
|
-
break
|
|
1142
|
-
|
|
1143
|
-
# Check if figma captcha iframe is visible
|
|
1144
|
-
try:
|
|
1145
|
-
captcha_frame_visible = False
|
|
1146
|
-
for f in page.frames:
|
|
1147
|
-
if "verify.figma.com/arkose_frame.html" in f.url:
|
|
1148
|
-
frame_element = page.locator("iframe[src*='verify.figma.com']").first
|
|
1149
|
-
if frame_element.count() > 0 and frame_element.is_visible(timeout=300):
|
|
1150
|
-
captcha_frame_visible = True
|
|
1151
|
-
break
|
|
1152
|
-
if captcha_frame_visible:
|
|
1153
|
-
has_captcha = True
|
|
1154
|
-
break
|
|
1155
|
-
except Exception:
|
|
1156
|
-
pass
|
|
1157
|
-
|
|
1158
|
-
# Look for puzzle frame or Start puzzle button
|
|
1159
|
-
all_frames = [page] + list(page.frames)
|
|
1160
|
-
for f in all_frames:
|
|
1161
|
-
try:
|
|
1162
|
-
for sel in [
|
|
1163
|
-
"button:has-text('Start puzzle')",
|
|
1164
|
-
"button:has-text('Start')",
|
|
1165
|
-
"[class*='puzzle']",
|
|
1166
|
-
"text=Verification",
|
|
1167
|
-
]:
|
|
1168
|
-
loc = f.locator(sel).first
|
|
1169
|
-
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
1170
|
-
puzzle_found_frame = f
|
|
1171
|
-
break
|
|
1172
|
-
if puzzle_found_frame:
|
|
1173
|
-
break
|
|
1174
|
-
except Exception:
|
|
1175
|
-
continue
|
|
1176
|
-
|
|
1177
|
-
# Click Start puzzle if found
|
|
1178
|
-
if puzzle_found_frame and not start_btn_clicked:
|
|
1179
|
-
try:
|
|
1180
|
-
start_btn = puzzle_found_frame.locator("button:has-text('Start puzzle')").first
|
|
1181
|
-
if start_btn.count() == 0:
|
|
1182
|
-
start_btn = puzzle_found_frame.locator("button:has-text('Start')").first
|
|
1183
|
-
if start_btn.count() > 0 and start_btn.is_visible(timeout=1000):
|
|
1184
|
-
log_step("Klik 'Start puzzle' pada pendaftaran...")
|
|
1185
|
-
start_btn.click(timeout=5000)
|
|
1186
|
-
start_btn_clicked = True
|
|
1187
|
-
time.sleep(3.0)
|
|
1188
|
-
save_immediate_debug(page, email, "after_reg_puzzle_click")
|
|
1189
|
-
except Exception as e_click:
|
|
1190
|
-
logger.debug(f"Gagal klik Start puzzle pendaftaran: {e_click}")
|
|
1191
|
-
|
|
1192
|
-
# Fallback check if captcha is preloaded and we've been waiting for more than 7 seconds on the signup page
|
|
1193
|
-
if time.time() > loop_start + 7.0 and any("dataExchangeBlob=" in u for u in captured_urls):
|
|
1194
|
-
has_captcha = True
|
|
1195
|
-
break
|
|
1196
|
-
|
|
1197
|
-
time.sleep(0.5)
|
|
1198
|
-
update_debug_screenshot(page)
|
|
1199
|
-
|
|
1200
|
-
if signup_success:
|
|
1201
|
-
break
|
|
1202
|
-
|
|
1203
|
-
if has_captcha and not ("figma.com/verify" in page.url or "verify?fuid=" in page.url):
|
|
1204
|
-
solve_and_inject_figma_captcha(
|
|
1205
|
-
page=page,
|
|
1206
|
-
captured_urls=captured_urls,
|
|
1207
|
-
settings=settings,
|
|
1208
|
-
proxy_server=args.proxy_server,
|
|
1209
|
-
proxy_user=args.proxy_user,
|
|
1210
|
-
proxy_pass=args.proxy_pass
|
|
1211
|
-
)
|
|
1212
|
-
|
|
1213
|
-
save_immediate_debug(page, email, "pre_verification_wait")
|
|
1214
|
-
log_step("Menunggu email verifikasi Figma dari Ammail...")
|
|
1215
|
-
since_ts = start_time
|
|
1216
|
-
_, verify_url = wait_for_otp_from_db(email, since_ts, settings, timeout=180)
|
|
1217
|
-
if not verify_url:
|
|
1218
|
-
raise WeavyAutomationError("Link verifikasi Figma tidak ditemukan di email (Timeout)")
|
|
1219
|
-
|
|
1220
|
-
log_step(f"Membuka link verifikasi: {verify_url}")
|
|
1221
|
-
page.goto(verify_url)
|
|
1222
|
-
try:
|
|
1223
|
-
page.wait_for_load_state("load", timeout=12000)
|
|
1224
|
-
except Exception:
|
|
1225
|
-
pass
|
|
1226
|
-
time.sleep(4.0)
|
|
1227
|
-
|
|
1228
|
-
complete_figma_onboarding(page, email)
|
|
1229
|
-
signup_success = True
|
|
1230
|
-
break
|
|
1231
|
-
except Exception as e_sa:
|
|
1232
|
-
log_step(f"Percobaan pendaftaran {sa_attempt} gagal: {e_sa}")
|
|
1233
|
-
if sa_attempt == 3:
|
|
1234
|
-
raise e_sa
|
|
1235
|
-
|
|
1236
|
-
if fallback_to_login:
|
|
1237
|
-
# Try to log in
|
|
1238
|
-
log_step("Membuka Figma login page...")
|
|
1239
|
-
page.goto("https://www.figma.com/login")
|
|
1240
|
-
try:
|
|
1241
|
-
page.wait_for_load_state("networkidle", timeout=6000)
|
|
1242
|
-
except Exception:
|
|
1243
|
-
pass
|
|
1244
|
-
|
|
1245
|
-
log_step("Mengisi email Figma...")
|
|
1246
|
-
if fill_first(page, FIGMA_EMAIL_INPUT_SELECTORS, email, timeout_ms=8000):
|
|
1247
|
-
# Check if password field is already visible (single-step login form)
|
|
1248
|
-
password_visible = False
|
|
1249
|
-
for sel in FIGMA_PASSWORD_SELECTORS:
|
|
1250
|
-
try:
|
|
1251
|
-
loc = page.locator(sel).first
|
|
1252
|
-
if loc.count() > 0 and loc.is_visible(timeout=500):
|
|
1253
|
-
password_visible = True
|
|
1254
|
-
break
|
|
1255
|
-
except Exception:
|
|
1256
|
-
pass
|
|
1257
|
-
|
|
1258
|
-
if not password_visible:
|
|
1259
|
-
# Click Continue to show password (multi-step login form)
|
|
1260
|
-
click_first(page, FIGMA_CONTINUE_EMAIL_SELECTORS, timeout_ms=5000)
|
|
1261
|
-
time.sleep(2.0)
|
|
1262
|
-
|
|
1263
|
-
log_step("Mengisi password Figma...")
|
|
1264
|
-
fill_first(page, FIGMA_PASSWORD_SELECTORS, password, timeout_ms=8000)
|
|
1265
|
-
|
|
1266
|
-
log_step("Mengirimkan login Figma...")
|
|
1267
|
-
if not click_first(page, FIGMA_SUBMIT_SELECTORS, timeout_ms=5000):
|
|
1268
|
-
page.keyboard.press("Enter")
|
|
1269
|
-
|
|
1270
|
-
time.sleep(4.0)
|
|
1271
|
-
|
|
1272
|
-
# Check if login succeeded
|
|
1273
|
-
url = page.url
|
|
1274
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
1275
|
-
log_step("Login Figma sukses.")
|
|
1276
|
-
already_registered = True
|
|
1277
|
-
else:
|
|
1278
|
-
# Wait a bit or check for captcha
|
|
1279
|
-
log_step("Menunggu redirect login atau captcha...")
|
|
1280
|
-
deadline = time.time() + 15.0
|
|
1281
|
-
has_captcha = False
|
|
1282
|
-
while time.time() < deadline:
|
|
1283
|
-
url = page.url
|
|
1284
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
1285
|
-
already_registered = True
|
|
1286
|
-
break
|
|
1287
|
-
has_captcha = any("dataExchangeBlob=" in u for u in captured_urls)
|
|
1288
|
-
if has_captcha:
|
|
1289
|
-
break
|
|
1290
|
-
time.sleep(0.5)
|
|
1291
|
-
|
|
1292
|
-
if has_captcha and not already_registered:
|
|
1293
|
-
log_step("Captcha terdeteksi saat login. Memulai solver...")
|
|
1294
|
-
solve_and_inject_figma_captcha(
|
|
1295
|
-
page=page,
|
|
1296
|
-
captured_urls=captured_urls,
|
|
1297
|
-
settings=settings,
|
|
1298
|
-
proxy_server=args.proxy_server,
|
|
1299
|
-
proxy_user=args.proxy_user,
|
|
1300
|
-
proxy_pass=args.proxy_pass
|
|
1301
|
-
)
|
|
1302
|
-
time.sleep(5.0)
|
|
1303
|
-
url = page.url
|
|
1304
|
-
if "/files" in url or "/dashboard" in url or "canvas" in url:
|
|
1305
|
-
already_registered = True
|
|
1306
|
-
log_step("Login Figma sukses setelah captcha solved.")
|
|
1307
|
-
|
|
1308
|
-
# Re-launch clean tab for oauth
|
|
1309
|
-
try:
|
|
1310
|
-
page.close()
|
|
1311
|
-
except Exception:
|
|
1312
|
-
pass
|
|
1313
|
-
|
|
1314
|
-
page = context.new_page()
|
|
1315
|
-
start_debug_screenshots(page)
|
|
1316
|
-
try:
|
|
1317
|
-
page.on("dialog", lambda dialog: dialog.dismiss())
|
|
1318
|
-
except Exception:
|
|
1319
|
-
pass
|
|
1320
|
-
|
|
1321
|
-
# 5. Weavy Signup Phase
|
|
1322
|
-
log_step("Membuka halaman Weavy signin...")
|
|
1323
|
-
try:
|
|
1324
|
-
page.goto(WEAVY_SIGNIN_URL, timeout=30000)
|
|
1325
|
-
except Exception as e_goto:
|
|
1326
|
-
logger.debug(f"Weavy signin page.goto timeout/error (expected if Cloudflare blocks): {e_goto}")
|
|
1327
|
-
# Tunggu Cloudflare Turnstile challenge selesai (Camoufox auto-solve)
|
|
1328
|
-
wait_for_cf_clearance(page, timeout=30.0)
|
|
1329
|
-
time.sleep(2.0)
|
|
1330
|
-
|
|
1331
|
-
url = page.url
|
|
1332
|
-
if "weavy" in url and "signin" not in url and "oauth" not in url:
|
|
1333
|
-
log_step(f"Terdeteksi sudah masuk ke dashboard Weavy: {url}")
|
|
1334
|
-
else:
|
|
1335
|
-
if getattr(args, "gsuite", False):
|
|
1336
|
-
# -----------------
|
|
1337
|
-
# Direct Google Flow on Weavy (completely bypassing Figma)
|
|
1338
|
-
# -----------------
|
|
1339
|
-
log_step("Mengklik tombol Log in with Google di Weavy...")
|
|
1340
|
-
btn_locator = None
|
|
1341
|
-
wait_deadline = time.time() + 25.0
|
|
1342
|
-
while time.time() < wait_deadline:
|
|
1343
|
-
for sel in [
|
|
1344
|
-
"button:has-text('Log in with Google')",
|
|
1345
|
-
"button[aria-label*='Google' i]",
|
|
1346
|
-
"a:has-text('Log in with Google')",
|
|
1347
|
-
"div[role='button']:has-text('Log in with Google')",
|
|
1348
|
-
]:
|
|
1349
|
-
try:
|
|
1350
|
-
loc = page.locator(sel).first
|
|
1351
|
-
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
1352
|
-
btn_locator = loc
|
|
1353
|
-
break
|
|
1354
|
-
except Exception:
|
|
1355
|
-
continue
|
|
1356
|
-
if btn_locator:
|
|
1357
|
-
break
|
|
1358
|
-
time.sleep(0.5)
|
|
1359
|
-
|
|
1360
|
-
if not btn_locator:
|
|
1361
|
-
raise WeavyAutomationError("Tombol Log in with Google tidak ditemukan")
|
|
1362
|
-
|
|
1363
|
-
with page.expect_popup() as popup_info:
|
|
1364
|
-
btn_locator.click(timeout=5000)
|
|
1365
|
-
popup = popup_info.value
|
|
1366
|
-
popup.wait_for_load_state()
|
|
1367
|
-
time.sleep(2.0)
|
|
1368
|
-
|
|
1369
|
-
# Handle Google popup directly
|
|
1370
|
-
log_step("Menangani Google OAuth popup...")
|
|
1371
|
-
google_deadline = time.time() + 90.0
|
|
1372
|
-
filled_email = False
|
|
1373
|
-
filled_pass = False
|
|
1374
|
-
clicked_account = False
|
|
1375
|
-
popup_oauth_done = False
|
|
1376
|
-
while time.time() < google_deadline:
|
|
1377
|
-
# Check if MAIN page already redirected (postMessage / server-side callback worked)
|
|
1378
|
-
try:
|
|
1379
|
-
main_url = page.url or ""
|
|
1380
|
-
if "app.weavy.ai" in main_url and "signin" not in main_url and "oauth" not in main_url:
|
|
1381
|
-
log_step(f"Halaman utama sudah di dashboard: {main_url}")
|
|
1382
|
-
popup_oauth_done = True
|
|
1383
|
-
try:
|
|
1384
|
-
if not popup.is_closed():
|
|
1385
|
-
popup.close()
|
|
1386
|
-
except Exception:
|
|
1387
|
-
pass
|
|
1388
|
-
break
|
|
1389
|
-
except Exception:
|
|
1390
|
-
pass
|
|
1391
|
-
|
|
1392
|
-
if popup.is_closed():
|
|
1393
|
-
log_step("Popup Google tertutup.")
|
|
1394
|
-
break
|
|
1395
|
-
try:
|
|
1396
|
-
popup_url = popup.url or ""
|
|
1397
|
-
log_step(f"[popup] {popup_url[:100]}")
|
|
1398
|
-
|
|
1399
|
-
# Popup reached Weavy dashboard — OAuth callback processed successfully
|
|
1400
|
-
if ("app.weavy.ai" in popup_url and
|
|
1401
|
-
"signin" not in popup_url and
|
|
1402
|
-
"callback" not in popup_url and
|
|
1403
|
-
"oauth" not in popup_url):
|
|
1404
|
-
log_step(f"Popup mencapai Weavy dashboard: {popup_url}")
|
|
1405
|
-
popup_oauth_done = True
|
|
1406
|
-
try:
|
|
1407
|
-
popup.close()
|
|
1408
|
-
except Exception:
|
|
1409
|
-
pass
|
|
1410
|
-
break
|
|
1411
|
-
|
|
1412
|
-
# Check if Google account is disabled / speedbumped
|
|
1413
|
-
if "speedbump/disabled" in popup_url or "disabled" in popup_url or "disabled" in (popup.title() or "").lower():
|
|
1414
|
-
raise WeavyAutomationError(f"Akun Google ({email}) dinonaktifkan/disabled oleh Google.")
|
|
1415
|
-
|
|
1416
|
-
# Account chooser
|
|
1417
|
-
if not clicked_account and (
|
|
1418
|
-
"accountchooser" in popup_url or
|
|
1419
|
-
"Choose an account" in (popup.title() or "")
|
|
1420
|
-
):
|
|
1421
|
-
clicked = popup.evaluate(f"""
|
|
1422
|
-
() => {{
|
|
1423
|
-
const email = '{email}';
|
|
1424
|
-
const lis = document.querySelectorAll('li[data-identifier], li.JDAKTe');
|
|
1425
|
-
for (const li of lis) {{
|
|
1426
|
-
const id = li.getAttribute('data-identifier') || '';
|
|
1427
|
-
if (id === email || li.textContent.includes(email)) {{
|
|
1428
|
-
li.click(); return true;
|
|
1429
|
-
}}
|
|
1430
|
-
}}
|
|
1431
|
-
const all = document.querySelectorAll('[data-email], [data-identifier]');
|
|
1432
|
-
for (const el of all) {{
|
|
1433
|
-
const v = el.getAttribute('data-email') || el.getAttribute('data-identifier') || '';
|
|
1434
|
-
if (v === email) {{ el.click(); return true; }}
|
|
1435
|
-
}}
|
|
1436
|
-
const rows = document.querySelectorAll('ul li');
|
|
1437
|
-
if (rows.length > 0) {{ rows[0].click(); return true; }}
|
|
1438
|
-
return false;
|
|
1439
|
-
}}
|
|
1440
|
-
""")
|
|
1441
|
-
if clicked:
|
|
1442
|
-
log_step(f"JS klik akun '{email}' di account chooser...")
|
|
1443
|
-
clicked_account = True
|
|
1444
|
-
time.sleep(3.0)
|
|
1445
|
-
continue
|
|
1446
|
-
|
|
1447
|
-
# Email input
|
|
1448
|
-
if not filled_email:
|
|
1449
|
-
email_in = popup.locator("input[type='email'], input[name='identifier']").first
|
|
1450
|
-
if email_in.count() > 0 and email_in.is_visible(timeout=500):
|
|
1451
|
-
log_step("Mengisi email GSuite di popup Google...")
|
|
1452
|
-
email_in.fill(email)
|
|
1453
|
-
popup.keyboard.press("Enter")
|
|
1454
|
-
filled_email = True
|
|
1455
|
-
time.sleep(3.0)
|
|
1456
|
-
continue
|
|
1457
|
-
|
|
1458
|
-
# Password input
|
|
1459
|
-
if filled_email and not filled_pass:
|
|
1460
|
-
pass_in = popup.locator("input[type='password'], input[name='Passwd']").first
|
|
1461
|
-
if pass_in.count() > 0 and pass_in.is_visible(timeout=500):
|
|
1462
|
-
log_step("Mengisi password GSuite di popup Google...")
|
|
1463
|
-
pass_in.click(timeout=2000)
|
|
1464
|
-
pass_in.fill(password)
|
|
1465
|
-
time.sleep(0.5)
|
|
1466
|
-
# Try clicking the Next button explicitly (more reliable than Enter)
|
|
1467
|
-
next_clicked = False
|
|
1468
|
-
for next_sel in [
|
|
1469
|
-
"button:has-text('Next')",
|
|
1470
|
-
"button[type='submit']",
|
|
1471
|
-
"#passwordNext",
|
|
1472
|
-
"div[id='passwordNext'] button",
|
|
1473
|
-
]:
|
|
1474
|
-
try:
|
|
1475
|
-
nb = popup.locator(next_sel).first
|
|
1476
|
-
if nb.count() > 0 and nb.is_visible(timeout=500):
|
|
1477
|
-
nb.click(timeout=3000)
|
|
1478
|
-
next_clicked = True
|
|
1479
|
-
log_step(f"Klik tombol '{next_sel}' setelah isi password")
|
|
1480
|
-
break
|
|
1481
|
-
except Exception:
|
|
1482
|
-
pass
|
|
1483
|
-
if not next_clicked:
|
|
1484
|
-
popup.keyboard.press("Enter")
|
|
1485
|
-
filled_pass = True
|
|
1486
|
-
time.sleep(4.0)
|
|
1487
|
-
continue
|
|
1488
|
-
|
|
1489
|
-
# Google post-login challenges (phone, 2FA, identity verify)
|
|
1490
|
-
if filled_pass:
|
|
1491
|
-
popup_url_now = popup.url or ""
|
|
1492
|
-
# "Try another way" or "Continue" on challenge page
|
|
1493
|
-
for challenge_sel in [
|
|
1494
|
-
"button:has-text('Try another way')",
|
|
1495
|
-
"button:has-text('Skip')",
|
|
1496
|
-
"button:has-text('Not now')",
|
|
1497
|
-
"button:has-text('Remind me later')",
|
|
1498
|
-
"button:has-text('Continue')",
|
|
1499
|
-
"button:has-text('Done')",
|
|
1500
|
-
"a:has-text('Skip')",
|
|
1501
|
-
]:
|
|
1502
|
-
try:
|
|
1503
|
-
cb = popup.locator(challenge_sel).first
|
|
1504
|
-
if cb.count() > 0 and cb.is_visible(timeout=300):
|
|
1505
|
-
log_step(f"Google challenge: klik '{challenge_sel}'")
|
|
1506
|
-
cb.click(timeout=3000)
|
|
1507
|
-
time.sleep(2.0)
|
|
1508
|
-
break
|
|
1509
|
-
except Exception:
|
|
1510
|
-
pass
|
|
1511
|
-
|
|
1512
|
-
# "I understand" workspace terms
|
|
1513
|
-
understand_btn = popup.locator(
|
|
1514
|
-
"button:has-text('I understand'), "
|
|
1515
|
-
"button:has-text('I Understand')"
|
|
1516
|
-
).first
|
|
1517
|
-
if understand_btn.count() > 0 and understand_btn.is_visible(timeout=500):
|
|
1518
|
-
log_step("Klik 'I understand' di Google Workspace terms...")
|
|
1519
|
-
understand_btn.click(timeout=3000)
|
|
1520
|
-
time.sleep(2.0)
|
|
1521
|
-
continue
|
|
1522
|
-
|
|
1523
|
-
# Handle any remaining Continue/Allow/Next button
|
|
1524
|
-
for btn_text in ["Continue", "Allow", "Next", "Accept", "Confirm"]:
|
|
1525
|
-
try:
|
|
1526
|
-
b = popup.locator(f"button:has-text('{btn_text}')").first
|
|
1527
|
-
if b.count() > 0 and b.is_visible(timeout=300):
|
|
1528
|
-
log_step(f"Klik '{btn_text}' di popup Google...")
|
|
1529
|
-
b.click(timeout=3000)
|
|
1530
|
-
time.sleep(2.0)
|
|
1531
|
-
break
|
|
1532
|
-
except Exception:
|
|
1533
|
-
pass
|
|
1534
|
-
|
|
1535
|
-
except WeavyAutomationError as e_wa:
|
|
1536
|
-
raise e_wa
|
|
1537
|
-
except Exception as e_popup:
|
|
1538
|
-
logger.debug(f"Google popup handler: {e_popup}")
|
|
1539
|
-
time.sleep(1.0)
|
|
1540
|
-
|
|
1541
|
-
# Log final popup URL before it closes (for debugging)
|
|
1542
|
-
try:
|
|
1543
|
-
if not popup.is_closed():
|
|
1544
|
-
log_step(f"[popup final URL] {popup.url}")
|
|
1545
|
-
except Exception:
|
|
1546
|
-
pass
|
|
1547
|
-
|
|
1548
|
-
# After popup closes/completes, wait for main page to update via Firebase postMessage
|
|
1549
|
-
if not popup_oauth_done:
|
|
1550
|
-
log_step("Popup Google selesai. Menunggu Firebase auth state update di halaman utama...")
|
|
1551
|
-
|
|
1552
|
-
# Phase 1: Watch for natural redirect (Firebase postMessage → Weavy router)
|
|
1553
|
-
# Don't navigate! Let Firebase SDK process the popup result first.
|
|
1554
|
-
phase1_deadline = time.time() + 25.0
|
|
1555
|
-
phase1_done = False
|
|
1556
|
-
while time.time() < phase1_deadline:
|
|
1557
|
-
try:
|
|
1558
|
-
current = page.url or ""
|
|
1559
|
-
if "app.weavy.ai" in current and "signin" not in current and "oauth" not in current:
|
|
1560
|
-
log_step(f"Firebase auth redirect terdeteksi: {current}")
|
|
1561
|
-
phase1_done = True
|
|
1562
|
-
break
|
|
1563
|
-
except Exception:
|
|
1564
|
-
pass
|
|
1565
|
-
time.sleep(1.5)
|
|
1566
|
-
update_debug_screenshot(page)
|
|
1567
|
-
|
|
1568
|
-
if phase1_done:
|
|
1569
|
-
pass # Already on dashboard
|
|
1570
|
-
else:
|
|
1571
|
-
# Phase 2: Force navigate (fallback for cases where postMessage didn't trigger redirect)
|
|
1572
|
-
log_step("Halaman utama tidak redirect otomatis. Coba force navigate...")
|
|
1573
|
-
for nav_attempt in range(1, 4):
|
|
1574
|
-
try:
|
|
1575
|
-
current = page.url or ""
|
|
1576
|
-
if "app.weavy.ai" in current and "signin" not in current and "oauth" not in current:
|
|
1577
|
-
log_step(f"Berhasil masuk: {current}")
|
|
1578
|
-
break
|
|
1579
|
-
log_step(f"Force navigate attempt {nav_attempt}/3 ke app.weavy.ai...")
|
|
1580
|
-
page.goto("https://app.weavy.ai/", timeout=20000)
|
|
1581
|
-
wait_for_cf_clearance(page, timeout=15.0)
|
|
1582
|
-
time.sleep(5.0)
|
|
1583
|
-
update_debug_screenshot(page)
|
|
1584
|
-
except Exception as e_nav:
|
|
1585
|
-
logger.debug(f"Nav attempt {nav_attempt} error: {e_nav}")
|
|
1586
|
-
time.sleep(2.0)
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
else:
|
|
1590
|
-
# -----------------
|
|
1591
|
-
# Regular Figma Flow
|
|
1592
|
-
# -----------------
|
|
1593
|
-
log_step("Mengklik tombol Log in with Figma...")
|
|
1594
|
-
btn_locator = None
|
|
1595
|
-
wait_deadline = time.time() + 25.0
|
|
1596
|
-
while time.time() < wait_deadline:
|
|
1597
|
-
for sel in WEAVY_FIGMA_LOGIN_SELECTORS:
|
|
1598
|
-
try:
|
|
1599
|
-
loc = page.locator(sel).first
|
|
1600
|
-
if loc.count() > 0 and loc.is_visible(timeout=300):
|
|
1601
|
-
btn_locator = loc
|
|
1602
|
-
break
|
|
1603
|
-
except Exception:
|
|
1604
|
-
continue
|
|
1605
|
-
if btn_locator:
|
|
1606
|
-
break
|
|
1607
|
-
time.sleep(0.5)
|
|
1608
|
-
|
|
1609
|
-
btn_clicked = False
|
|
1610
|
-
if btn_locator:
|
|
1611
|
-
try:
|
|
1612
|
-
btn_locator.click(timeout=5000)
|
|
1613
|
-
btn_clicked = True
|
|
1614
|
-
except Exception:
|
|
1615
|
-
pass
|
|
1616
|
-
|
|
1617
|
-
# Tunggu navigasi/transisi URL setelah klik (bisa ke figma.com atau Weavy dashboard)
|
|
1618
|
-
log_step("Menunggu transisi URL setelah klik login...")
|
|
1619
|
-
nav_deadline = time.time() + 15.0
|
|
1620
|
-
while time.time() < nav_deadline:
|
|
1621
|
-
url = page.url
|
|
1622
|
-
if "figma.com" in url or ("weavy" in url and "signin" not in url and "oauth" not in url):
|
|
1623
|
-
break
|
|
1624
|
-
time.sleep(0.5)
|
|
1625
|
-
update_debug_screenshot(page)
|
|
1626
|
-
|
|
1627
|
-
url = page.url
|
|
1628
|
-
if not btn_clicked and "figma.com" not in url and not ("weavy" in url and "signin" not in url):
|
|
1629
|
-
raise WeavyAutomationError("Tombol Log in with Figma tidak ditemukan")
|
|
1630
|
-
|
|
1631
|
-
if "weavy" in url and "signin" not in url and "oauth" not in url:
|
|
1632
|
-
log_step(f"Sudah di dashboard Weavy: {url}")
|
|
1633
|
-
else:
|
|
1634
|
-
save_immediate_debug(page, email, "after_login_click")
|
|
1635
|
-
|
|
1636
|
-
# Handle figma login with email/password (if not logged in)
|
|
1637
|
-
ready_deadline = time.time() + 10.0
|
|
1638
|
-
while time.time() < ready_deadline:
|
|
1639
|
-
if "figma.com/login" in page.url or "figma.com/signup" in page.url:
|
|
1640
|
-
break
|
|
1641
|
-
time.sleep(0.5)
|
|
1642
|
-
update_debug_screenshot(page)
|
|
1643
|
-
|
|
1644
|
-
current_url = page.url
|
|
1645
|
-
if "figma.com/login" in current_url or "figma.com/signup" in current_url:
|
|
1646
|
-
log_step(f"Diarahkan ke halaman login Figma: {current_url}. Mengisi email/password...")
|
|
1647
|
-
if not fill_first(page, FIGMA_EMAIL_INPUT_SELECTORS, email, timeout_ms=8000):
|
|
1648
|
-
raise WeavyAutomationError("Email input figma tidak ditemukan")
|
|
1649
|
-
if not click_first(page, FIGMA_CONTINUE_EMAIL_SELECTORS, timeout_ms=5000):
|
|
1650
|
-
raise WeavyAutomationError("Tombol continue email figma tidak ditemukan")
|
|
1651
|
-
time.sleep(2.0)
|
|
1652
|
-
if not fill_first(page, FIGMA_PASSWORD_SELECTORS, password, timeout_ms=8000):
|
|
1653
|
-
raise WeavyAutomationError("Password input figma tidak ditemukan")
|
|
1654
|
-
if not click_first(page, FIGMA_SUBMIT_SELECTORS, timeout_ms=5000):
|
|
1655
|
-
page.keyboard.press("Enter")
|
|
1656
|
-
time.sleep(4.0)
|
|
1657
|
-
|
|
1658
|
-
# Handle Figma anti-bot verification puzzle (dalam iframe)
|
|
1659
|
-
log_step("Menunggu captcha figma atau redirect ke dashboard...")
|
|
1660
|
-
puzzle_deadline = time.time() + 25.0
|
|
1661
|
-
puzzle_found_frame = None
|
|
1662
|
-
start_btn_clicked = False
|
|
1663
|
-
has_captcha = False
|
|
1664
|
-
|
|
1665
|
-
while time.time() < puzzle_deadline:
|
|
1666
|
-
# Check if already authorized or redirected
|
|
1667
|
-
current_url = page.url
|
|
1668
|
-
if "weavy" in current_url and "signin" not in current_url and "oauth" not in current_url:
|
|
1669
|
-
break
|
|
1670
|
-
|
|
1671
|
-
# Handle Figma blocked page (anti-bot false positive during OAuth redirection)
|
|
1672
|
-
if "figma.com/blocked.html" in current_url:
|
|
1673
|
-
log_step("Figma blocked page terdeteksi saat OAuth. Mengulangi login Figma...")
|
|
1674
|
-
page.goto("https://www.figma.com/login")
|
|
1675
|
-
try:
|
|
1676
|
-
page.wait_for_load_state("networkidle", timeout=6000)
|
|
1677
|
-
except Exception:
|
|
1678
|
-
pass
|
|
1679
|
-
|
|
1680
|
-
log_step("Mengisi email Figma...")
|
|
1681
|
-
if fill_first(page, FIGMA_EMAIL_INPUT_SELECTORS, email, timeout_ms=8000):
|
|
1682
|
-
click_first(page, FIGMA_CONTINUE_EMAIL_SELECTORS, timeout_ms=5000)
|
|
1683
|
-
time.sleep(2.0)
|
|
1684
|
-
log_step("Mengisi password Figma...")
|
|
1685
|
-
fill_first(page, FIGMA_PASSWORD_SELECTORS, password, timeout_ms=8000)
|
|
1686
|
-
log_step("Mengirimkan login Figma...")
|
|
1687
|
-
if not click_first(page, FIGMA_SUBMIT_SELECTORS, timeout_ms=5000):
|
|
1688
|
-
page.keyboard.press("Enter")
|
|
1689
|
-
time.sleep(5.0)
|
|
1690
|
-
|
|
1691
|
-
# Navigate back to Weavy login to retry OAuth handshake
|
|
1692
|
-
log_step("Kembali ke Weavy signin untuk retry OAuth handshake...")
|
|
1693
|
-
page.goto(WEAVY_SIGNIN_URL)
|
|
1694
|
-
try:
|
|
1695
|
-
page.wait_for_load_state("networkidle", timeout=8000)
|
|
1696
|
-
except Exception:
|
|
1697
|
-
pass
|
|
1698
|
-
wait_for_cf_clearance(page, timeout=30.0)
|
|
1699
|
-
click_first(page, WEAVY_FIGMA_LOGIN_SELECTORS, timeout_ms=8000)
|
|
1700
|
-
time.sleep(4.0)
|
|
1701
|
-
continue
|
|
1702
|
-
|
|
1703
|
-
# Check for figma oauth allow button (meaning captcha was bypassed or solved)
|
|
1704
|
-
try:
|
|
1705
|
-
allow_loc = page.locator("button:text('Allow access'), button:text('Allow')").first
|
|
1706
|
-
if allow_loc.count() > 0 and allow_loc.is_visible(timeout=500):
|
|
1707
|
-
break
|
|
1708
|
-
except Exception:
|
|
1709
|
-
pass
|
|
1710
|
-
|
|
1711
|
-
# Look for puzzle frame or Start puzzle button
|
|
1712
|
-
all_frames = [page] + list(page.frames)
|
|
1713
|
-
for f in all_frames:
|
|
1714
|
-
try:
|
|
1715
|
-
for sel in [
|
|
1716
|
-
"button:has-text('Start puzzle')",
|
|
1717
|
-
"button:has-text('Start')",
|
|
1718
|
-
"[class*='puzzle']",
|
|
1719
|
-
"text=Verification",
|
|
1720
|
-
]:
|
|
1721
|
-
loc = f.locator(sel).first
|
|
1722
|
-
if loc.count() > 0 and loc.is_visible(timeout=500):
|
|
1723
|
-
puzzle_found_frame = f
|
|
1724
|
-
break
|
|
1725
|
-
if puzzle_found_frame:
|
|
1726
|
-
break
|
|
1727
|
-
except Exception:
|
|
1728
|
-
continue
|
|
1729
|
-
|
|
1730
|
-
# If we found the puzzle frame and haven't clicked the button yet
|
|
1731
|
-
if puzzle_found_frame and not start_btn_clicked:
|
|
1732
|
-
try:
|
|
1733
|
-
start_btn = puzzle_found_frame.locator("button:has-text('Start puzzle')").first
|
|
1734
|
-
if start_btn.count() == 0:
|
|
1735
|
-
start_btn = puzzle_found_frame.locator("button:has-text('Start')").first
|
|
1736
|
-
if start_btn.count() > 0 and start_btn.is_visible(timeout=1000):
|
|
1737
|
-
log_step("Klik 'Start puzzle'...")
|
|
1738
|
-
start_btn.click(timeout=5000)
|
|
1739
|
-
start_btn_clicked = True
|
|
1740
|
-
time.sleep(3.0)
|
|
1741
|
-
save_immediate_debug(page, email, "after_puzzle_click")
|
|
1742
|
-
except Exception as e_click:
|
|
1743
|
-
logger.debug(f"Gagal mengklik Start puzzle: {e_click}")
|
|
1744
|
-
|
|
1745
|
-
# Check if any dataExchangeBlob is captured
|
|
1746
|
-
has_captcha = any("dataExchangeBlob=" in u for u in captured_urls)
|
|
1747
|
-
if has_captcha:
|
|
1748
|
-
break
|
|
1749
|
-
|
|
1750
|
-
# Check if "Reload Captcha" is shown (captcha loading failed)
|
|
1751
|
-
try:
|
|
1752
|
-
for f in all_frames:
|
|
1753
|
-
reload_btn = f.locator("button:has-text('Reload Captcha')").first
|
|
1754
|
-
if reload_btn.count() > 0 and reload_btn.is_visible(timeout=500):
|
|
1755
|
-
log_step("Tombol 'Reload Captcha' terdeteksi. Mengklik reload...")
|
|
1756
|
-
reload_btn.click(timeout=3000)
|
|
1757
|
-
time.sleep(3.0)
|
|
1758
|
-
break
|
|
1759
|
-
except Exception:
|
|
1760
|
-
pass
|
|
1761
|
-
|
|
1762
|
-
time.sleep(1.0)
|
|
1763
|
-
update_debug_screenshot(page)
|
|
1764
|
-
|
|
1765
|
-
if has_captcha:
|
|
1766
|
-
solve_and_inject_figma_captcha(
|
|
1767
|
-
page=page,
|
|
1768
|
-
captured_urls=captured_urls,
|
|
1769
|
-
settings=settings,
|
|
1770
|
-
proxy_server=args.proxy_server,
|
|
1771
|
-
proxy_user=args.proxy_user,
|
|
1772
|
-
proxy_pass=args.proxy_pass
|
|
1773
|
-
)
|
|
1774
|
-
log_step("Menunggu verifikasi captcha selesai...")
|
|
1775
|
-
time.sleep(5.0)
|
|
1776
|
-
|
|
1777
|
-
# Handle Figma OAuth Allow access (for standard flow)
|
|
1778
|
-
log_step("Mengklik Allow access pada Figma OAuth (jika ada)...")
|
|
1779
|
-
if click_first(page, FIGMA_OAUTH_ALLOW_SELECTORS, timeout_ms=15000):
|
|
1780
|
-
log_step("Figma OAuth allow access di-klik.")
|
|
1781
|
-
else:
|
|
1782
|
-
log_step("Tombol Allow access Figma OAuth tidak ditemukan. Mungkin auto-authorize?")
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
log_step("Menunggu redirect ke Weavy dashboard...")
|
|
1787
|
-
deadline = time.time() + 90.0
|
|
1788
|
-
redirected = False
|
|
1789
|
-
while time.time() < deadline:
|
|
1790
|
-
url = page.url
|
|
1791
|
-
if "weavy" in url and "signin" not in url and "oauth" not in url:
|
|
1792
|
-
redirected = True
|
|
1793
|
-
break
|
|
1794
|
-
time.sleep(1.0)
|
|
1795
|
-
update_debug_screenshot(page)
|
|
1796
|
-
|
|
1797
|
-
if not redirected:
|
|
1798
|
-
raise WeavyAutomationError(f"Gagal redirect ke Weavy dashboard. URL akhir: {page.url}")
|
|
1799
|
-
|
|
1800
|
-
log_step("Berhasil masuk Weavy dashboard. Mengekstrak cookies...")
|
|
1801
|
-
cookies_list = page.context.cookies()
|
|
1802
|
-
weavy_cookies = []
|
|
1803
|
-
for c in cookies_list:
|
|
1804
|
-
domain = c.get("domain") or ""
|
|
1805
|
-
if "weavy.ai" in domain:
|
|
1806
|
-
weavy_cookies.append(f"{c.get('name')}={c.get('value')}")
|
|
1807
|
-
|
|
1808
|
-
cookies_str = "; ".join(weavy_cookies)
|
|
1809
|
-
if not cookies_str:
|
|
1810
|
-
cookies_str = "; ".join(f"{c.get('name')}={c.get('value')}" for c in cookies_list)
|
|
1811
|
-
|
|
1812
|
-
# Extract Firebase refresh token from IndexedDB (Firebase SDK v9+ stores here, not localStorage)
|
|
1813
|
-
firebase_refresh_token = None
|
|
1814
|
-
try:
|
|
1815
|
-
fb_refresh = page.evaluate("""
|
|
1816
|
-
() => new Promise((resolve) => {
|
|
1817
|
-
try {
|
|
1818
|
-
// Firebase v9+ stores auth in IndexedDB
|
|
1819
|
-
const req = indexedDB.open('firebaseLocalStorageDb');
|
|
1820
|
-
req.onsuccess = (e) => {
|
|
1821
|
-
try {
|
|
1822
|
-
const db = e.target.result;
|
|
1823
|
-
const storeName = db.objectStoreNames.contains('firebaseLocalStorage')
|
|
1824
|
-
? 'firebaseLocalStorage' : db.objectStoreNames[0];
|
|
1825
|
-
if (!storeName) { resolve(null); return; }
|
|
1826
|
-
const tx = db.transaction(storeName, 'readonly');
|
|
1827
|
-
const store = tx.objectStore(storeName);
|
|
1828
|
-
const getAllReq = store.getAll();
|
|
1829
|
-
getAllReq.onsuccess = (e) => {
|
|
1830
|
-
const items = e.target.result || [];
|
|
1831
|
-
for (const item of items) {
|
|
1832
|
-
const v = item.value || item;
|
|
1833
|
-
if (v && v.stsTokenManager && v.stsTokenManager.refreshToken) {
|
|
1834
|
-
resolve(v.stsTokenManager.refreshToken);
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
resolve(null);
|
|
1839
|
-
};
|
|
1840
|
-
getAllReq.onerror = () => resolve(null);
|
|
1841
|
-
} catch(e2) { resolve(null); }
|
|
1842
|
-
};
|
|
1843
|
-
req.onerror = () => {
|
|
1844
|
-
// Fallback: try localStorage (Firebase v8 compat)
|
|
1845
|
-
try {
|
|
1846
|
-
for (const key of Object.keys(localStorage || {})) {
|
|
1847
|
-
if (key.startsWith('firebase:authUser:')) {
|
|
1848
|
-
const u = JSON.parse(localStorage.getItem(key) || '{}');
|
|
1849
|
-
const rt = (u && u.stsTokenManager && u.stsTokenManager.refreshToken);
|
|
1850
|
-
if (rt) { resolve(rt); return; }
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
} catch(e3) {}
|
|
1854
|
-
resolve(null);
|
|
1855
|
-
};
|
|
1856
|
-
} catch(e) { resolve(null); }
|
|
1857
|
-
})
|
|
1858
|
-
""")
|
|
1859
|
-
if fb_refresh:
|
|
1860
|
-
firebase_refresh_token = fb_refresh
|
|
1861
|
-
log_step("Firebase refresh token berhasil ditangkap dari IndexedDB!")
|
|
1862
|
-
else:
|
|
1863
|
-
log_step("Firebase refresh token tidak ditemukan di IndexedDB/localStorage.")
|
|
1864
|
-
except Exception as e_fb:
|
|
1865
|
-
logger.debug(f"Failed to extract Firebase refresh token: {e_fb}")
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
# Also try to capture Firebase API key from page meta/script if not yet captured
|
|
1869
|
-
if not captured_firebase_api_key[0]:
|
|
1870
|
-
try:
|
|
1871
|
-
fb_key = page.evaluate("""
|
|
1872
|
-
() => {
|
|
1873
|
-
// Try various global config locations
|
|
1874
|
-
if (window.__FIREBASE_CONFIG__ && window.__FIREBASE_CONFIG__.apiKey) return window.__FIREBASE_CONFIG__.apiKey;
|
|
1875
|
-
if (window._env && window._env.FIREBASE_API_KEY) return window._env.FIREBASE_API_KEY;
|
|
1876
|
-
if (window.NEXT_PUBLIC_FIREBASE_API_KEY) return window.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
1877
|
-
// Scan meta tags
|
|
1878
|
-
const metas = document.querySelectorAll('meta[name*="firebase"], meta[name*="FIREBASE"]');
|
|
1879
|
-
for (const m of metas) {
|
|
1880
|
-
const v = m.getAttribute('content') || '';
|
|
1881
|
-
if (v.startsWith('AIza')) return v;
|
|
1882
|
-
}
|
|
1883
|
-
return null;
|
|
1884
|
-
}
|
|
1885
|
-
""")
|
|
1886
|
-
if fb_key:
|
|
1887
|
-
captured_firebase_api_key[0] = fb_key
|
|
1888
|
-
except Exception:
|
|
1889
|
-
pass
|
|
1890
|
-
|
|
1891
|
-
balance = 150.0
|
|
1892
|
-
|
|
1893
|
-
# Success result output
|
|
1894
|
-
sys.stdout.write(json.dumps({
|
|
1895
|
-
"status": "success",
|
|
1896
|
-
"cookie": cookies_str,
|
|
1897
|
-
"balance": balance,
|
|
1898
|
-
"jwt": captured_jwt[0] or "",
|
|
1899
|
-
"firebase_refresh_token": firebase_refresh_token or "",
|
|
1900
|
-
"firebase_api_key": captured_firebase_api_key[0] or ""
|
|
1901
|
-
}, ensure_ascii=False) + "\n")
|
|
1902
|
-
sys.stdout.flush()
|
|
1903
|
-
except Exception as e:
|
|
1904
|
-
try:
|
|
1905
|
-
save_debug_screenshots(context, args.profiles_dir, args.email, prefix="fail")
|
|
1906
|
-
except Exception:
|
|
1907
|
-
pass
|
|
1908
|
-
raise e
|
|
1909
|
-
finally:
|
|
1910
|
-
browser.__exit__(None, None, None)
|
|
1911
|
-
|
|
1912
|
-
def main():
|
|
1913
|
-
parser = argparse.ArgumentParser(description="Weavy Auto-signup Standalone")
|
|
1914
|
-
parser.add_argument("--email", required=True)
|
|
1915
|
-
parser.add_argument("--password", required=True)
|
|
1916
|
-
parser.add_argument("--profiles-dir", required=True)
|
|
1917
|
-
parser.add_argument("--headless", action="store_true")
|
|
1918
|
-
parser.add_argument("--proxy-server")
|
|
1919
|
-
parser.add_argument("--proxy-user")
|
|
1920
|
-
parser.add_argument("--proxy-pass")
|
|
1921
|
-
parser.add_argument("--gsuite", action="store_true")
|
|
1922
|
-
parser.add_argument("--clean", action="store_true")
|
|
1923
|
-
args = parser.parse_args()
|
|
1924
|
-
|
|
1925
|
-
# Auto-detect GSuite: jika domain email BUKAN domain ammail → langsung GSuite (Google OAuth di Weavy)
|
|
1926
|
-
email_lower = (args.email or "").strip().lower()
|
|
1927
|
-
settings = load_settings_db()
|
|
1928
|
-
|
|
1929
|
-
if not args.gsuite:
|
|
1930
|
-
email_domain = email_lower.split("@")[-1] if "@" in email_lower else ""
|
|
1931
|
-
|
|
1932
|
-
# Kumpulkan semua known ammail domains dari settings
|
|
1933
|
-
known_ammail_domains = set()
|
|
1934
|
-
for key in ("ammail_default_domain", "ammail_cf_domain"):
|
|
1935
|
-
d = (settings.get(key) or "").strip().lower()
|
|
1936
|
-
if d:
|
|
1937
|
-
known_ammail_domains.add(d)
|
|
1938
|
-
|
|
1939
|
-
if email_domain and email_domain not in known_ammail_domains:
|
|
1940
|
-
# Domain tidak ada di ammail → ini akun eksternal (Google/GSuite)
|
|
1941
|
-
log_step(f"Domain '{email_domain}' bukan domain ammail → Google OAuth (GSuite) flow.")
|
|
1942
|
-
args.gsuite = True
|
|
1943
|
-
else:
|
|
1944
|
-
log_step(f"Domain '{email_domain}' adalah domain ammail → Figma signup flow.")
|
|
1945
|
-
|
|
1946
|
-
try:
|
|
1947
|
-
run_automation(args, settings)
|
|
1948
|
-
except Exception as exc:
|
|
1949
|
-
if _debug_context:
|
|
1950
|
-
try:
|
|
1951
|
-
save_debug_screenshots(_debug_context, args.profiles_dir, args.email, prefix="fail")
|
|
1952
|
-
except Exception:
|
|
1953
|
-
pass
|
|
1954
|
-
sys.stdout.write(json.dumps({
|
|
1955
|
-
"status": "error",
|
|
1956
|
-
"message": str(exc)
|
|
1957
|
-
}, ensure_ascii=False) + "\n")
|
|
1958
|
-
sys.stdout.flush()
|
|
1959
|
-
sys.exit(1)
|
|
1960
|
-
finally:
|
|
1961
|
-
stop_debug_screenshots()
|
|
1962
|
-
|
|
1963
|
-
if __name__ == "__main__":
|
|
1964
|
-
main()
|