@fudrouter/fsrouter 0.6.92 → 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.
@@ -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("&amp;", "&").replace("&quot;", '"').replace("&#39;", "'")
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()