@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,636 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Standalone script to automate CodeBuddy signup & API key generation using Playwright/Camoufox.
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
- from pathlib import Path
13
-
14
- # Patch Playwright's Locator.is_visible to support the timeout argument
15
- try:
16
- from playwright.sync_api import Locator
17
- _orig_is_visible = Locator.is_visible
18
- def _patched_is_visible(self, *args, **kwargs):
19
- timeout = kwargs.pop("timeout", None)
20
- if timeout is None and len(args) > 0:
21
- timeout = args[0]
22
- args = args[1:]
23
- if timeout is not None:
24
- try:
25
- self.wait_for(state="visible", timeout=float(timeout))
26
- return True
27
- except Exception:
28
- return False
29
- return _orig_is_visible(self, *args, **kwargs)
30
- Locator.is_visible = _patched_is_visible
31
- except ImportError:
32
- pass
33
-
34
- # Setup simple stdout logger to not conflict with JSON line prints
35
- logging.basicConfig(level=logging.WARNING)
36
- logger = logging.getLogger("codebuddy_signup")
37
-
38
- CODEBUDDY_LOGIN_URL = "https://www.codebuddy.ai/login?redirect_uri=https://www.codebuddy.ai/home"
39
- CODEBUDDY_API_KEYS_ENDPOINT = "https://www.codebuddy.ai/console/api/client/v1/api-keys"
40
-
41
- GOOGLE_EMAIL_SELECTORS = [
42
- "input[type='email']",
43
- "input[name='identifier']",
44
- "input#identifierId",
45
- ]
46
- GOOGLE_PASSWORD_SELECTORS = [
47
- "input[type='password'][name='Passwd']",
48
- "input[type='password']",
49
- "input[name='password']",
50
- ]
51
- GOOGLE_NEXT_SELECTORS = [
52
- "#identifierNext button",
53
- "#passwordNext button",
54
- "button[jsname='LgbsSe']",
55
- "div[role='button']:has-text('Next')",
56
- "button:has-text('Next')",
57
- "button:has-text('Berikutnya')",
58
- ]
59
-
60
- class CodeBuddyAutomationError(RuntimeError):
61
- pass
62
-
63
- def log_step(msg, *args):
64
- txt = msg % args if args else msg
65
- sys.stdout.write(json.dumps({"step": txt}, ensure_ascii=False) + "\n")
66
- sys.stdout.flush()
67
-
68
- def safe_email_to_dirname(email: str) -> str:
69
- cleaned = (email or "").strip().lower()
70
- cleaned = cleaned.replace("@", "_at_")
71
- cleaned = re.sub(r"[^a-z0-9._-]+", "_", cleaned)
72
- cleaned = cleaned.strip("._-")
73
- return cleaned or "account"
74
-
75
- def get_proxy_country(proxy: dict = None) -> str:
76
- import requests
77
- url = "http://ip-api.com/json/"
78
- proxies = None
79
- if proxy and proxy.get("server"):
80
- server = proxy["server"]
81
- username = proxy.get("username")
82
- password = proxy.get("password")
83
- if username and password:
84
- if "://" in server:
85
- proto, host = server.split("://", 1)
86
- proxy_url = f"{proto}://{username}:{password}@{host}"
87
- else:
88
- proxy_url = f"http://{username}:{password}@{server}"
89
- else:
90
- proxy_url = server
91
- proxies = {
92
- "http": proxy_url,
93
- "https": proxy_url
94
- }
95
-
96
- for attempt in range(2):
97
- try:
98
- response = requests.get(url, proxies=proxies, timeout=10)
99
- if response.status_code == 200:
100
- data = response.json()
101
- if data.get("status") == "success" and data.get("country"):
102
- return data["country"]
103
- except Exception:
104
- pass
105
-
106
- try:
107
- fallback_url = "https://ipapi.co/json/"
108
- response = requests.get(fallback_url, proxies=proxies, timeout=10)
109
- if response.status_code == 200:
110
- data = response.json()
111
- if data.get("country_name"):
112
- return data["country_name"]
113
- except Exception:
114
- pass
115
-
116
- return "United States"
117
-
118
- def test_api_key(api_key: str, proxy: dict = None) -> bool:
119
- url = "https://www.codebuddy.ai/v2/chat/completions"
120
- headers = {
121
- "Content-Type": "application/json",
122
- "Authorization": f"Bearer {api_key}",
123
- }
124
- payload = {
125
- "model": "default-model",
126
- "messages": [
127
- {"role": "system", "content": "You are a helpful assistant."},
128
- {"role": "user", "content": "ping"}
129
- ],
130
- "stream": True
131
- }
132
-
133
- proxies = None
134
- if proxy and proxy.get("server"):
135
- server = proxy["server"]
136
- username = proxy.get("username")
137
- password = proxy.get("password")
138
- if username and password:
139
- if "://" in server:
140
- proto, host = server.split("://", 1)
141
- proxy_url = f"{proto}://{username}:{password}@{host}"
142
- else:
143
- proxy_url = f"http://{username}:{password}@{server}"
144
- else:
145
- proxy_url = server
146
- proxies = {"http": proxy_url, "https": proxy_url}
147
-
148
- import requests
149
- try:
150
- r = requests.post(url, headers=headers, json=payload, proxies=proxies, timeout=10, stream=True)
151
- if r.status_code == 200:
152
- try:
153
- r.close()
154
- except Exception:
155
- pass
156
- return True
157
- except Exception:
158
- pass
159
- return False
160
-
161
- def click_first(page, selectors, timeout_ms: int = 5000) -> bool:
162
- deadline = time.time() + (timeout_ms / 1000.0)
163
- while time.time() < deadline:
164
- for sel in selectors:
165
- try:
166
- loc = page.locator(sel).first
167
- if loc.count() > 0 and loc.is_visible(timeout=400):
168
- try:
169
- loc.click(timeout=1500)
170
- return True
171
- except Exception:
172
- pass
173
- try:
174
- handle = loc.element_handle(timeout=500)
175
- if handle:
176
- page.evaluate("(el) => el.click()", handle)
177
- return True
178
- except Exception:
179
- pass
180
- except Exception:
181
- continue
182
- time.sleep(0.3)
183
- return False
184
-
185
- def fill_first(page, selectors, value: str, timeout_ms: int = 5000) -> bool:
186
- deadline = time.time() + (timeout_ms / 1000.0)
187
- while time.time() < deadline:
188
- for sel in selectors:
189
- try:
190
- loc = page.locator(sel).first
191
- if loc.count() > 0 and loc.is_visible(timeout=400):
192
- loc.fill(value, timeout=1500)
193
- return True
194
- except Exception:
195
- continue
196
- time.sleep(0.3)
197
- return False
198
-
199
- def do_google_login(page, email: str, password: str) -> bool:
200
- log_step("Menunggu form email Google...")
201
- if not fill_first(page, GOOGLE_EMAIL_SELECTORS, email, timeout_ms=10000):
202
- log_step("Form email tidak ditemukan, cek apakah diminta pilih akun.")
203
- try:
204
- account_sel = f"div[data-identifier='{email.lower()}']"
205
- if page.locator(account_sel).count() > 0:
206
- page.locator(account_sel).click()
207
- log_step(f"Memilih akun {email}")
208
- except Exception:
209
- pass
210
- else:
211
- click_first(page, GOOGLE_NEXT_SELECTORS, timeout_ms=3000)
212
-
213
- log_step("Menunggu form password Google...")
214
- if fill_first(page, GOOGLE_PASSWORD_SELECTORS, password, timeout_ms=10000):
215
- click_first(page, GOOGLE_NEXT_SELECTORS, timeout_ms=3000)
216
- log_step("Password diisi dan disubmit.")
217
- return True
218
-
219
- time.sleep(2)
220
- if "codebuddy.ai" in page.url.lower():
221
- log_step("Berhasil auth, redirect ke CodeBuddy.")
222
- return True
223
-
224
- return False
225
-
226
- def perform_login_page_actions(page, email: str) -> None:
227
- iframe_selector = "iframe[title='login-iframe']"
228
- try:
229
- page.wait_for_selector(iframe_selector, timeout=15000)
230
- for _ in range(30):
231
- exists = page.evaluate(f"""() => {{
232
- const iframe = document.querySelector("{iframe_selector}");
233
- if (iframe && iframe.contentDocument) {{
234
- const doc = iframe.contentDocument;
235
- return !!(doc.querySelector("#agree-policy-account") || doc.querySelector("input[type='checkbox']"));
236
- }}
237
- return false;
238
- }}""")
239
- if exists:
240
- break
241
- time.sleep(0.5)
242
- except Exception:
243
- pass
244
-
245
- iframe = page.frame_locator(iframe_selector)
246
-
247
- log_step("Centang checkbox persetujuan...")
248
- try:
249
- page.evaluate(f"""() => {{
250
- const iframe = document.querySelector("{iframe_selector}");
251
- if (iframe && iframe.contentDocument) {{
252
- const doc = iframe.contentDocument;
253
- const checkbox = doc.querySelector("#agree-policy-account") || doc.querySelector("input[type='checkbox']");
254
- if (checkbox) {{
255
- checkbox.checked = true;
256
- checkbox.dispatchEvent(new Event('change', {{ bubbles: true }}));
257
- checkbox.dispatchEvent(new Event('click', {{ bubbles: true }}));
258
- return true;
259
- }}
260
- }}
261
- return false;
262
- }}""")
263
- except Exception:
264
- pass
265
-
266
- time.sleep(2)
267
-
268
- log_step("Klik Sign up with Google...")
269
- google_selector = "#social-google, a[href*='google/login'], a:has-text('Google')"
270
- clicked_google = False
271
- try:
272
- loc = iframe.locator(google_selector).first
273
- if loc.count() > 0:
274
- loc.click(timeout=10000, force=True)
275
- clicked_google = True
276
- except Exception:
277
- pass
278
-
279
- if not clicked_google:
280
- try:
281
- page.evaluate(f"""() => {{
282
- const iframe = document.querySelector("{iframe_selector}");
283
- if (iframe && iframe.contentDocument) {{
284
- const doc = iframe.contentDocument;
285
- const el = doc.querySelector("#social-google") ||
286
- doc.querySelector("a[href*='google/login']") ||
287
- Array.from(doc.querySelectorAll("button, a, div[role='button']"))
288
- .find(e => (e.innerText || '').includes("Google") || (e.textContent || '').includes("Google"));
289
- if (el) {{
290
- el.click();
291
- return true;
292
- }}
293
- }}
294
- return false;
295
- }}""")
296
- except Exception:
297
- pass
298
-
299
- time.sleep(2)
300
-
301
- try:
302
- confirm_btn = page.locator("button:has-text('Confirm')").first
303
- if confirm_btn.is_visible(timeout=3000):
304
- log_step("Klik Confirm pada popup Service Agreement...")
305
- confirm_btn.click(timeout=3000, force=True)
306
- time.sleep(2)
307
- else:
308
- confirm_btn2 = iframe.locator("button:has-text('Confirm')").first
309
- if confirm_btn2.is_visible(timeout=2000):
310
- confirm_btn2.click(timeout=3000, force=True)
311
- time.sleep(2)
312
- except Exception:
313
- pass
314
-
315
- def safe_evaluate(page, js_code: str, arg=None, max_retries: int = 3):
316
- for attempt in range(max_retries):
317
- try:
318
- if arg is not None:
319
- return page.evaluate(js_code, arg)
320
- else:
321
- return page.evaluate(js_code)
322
- except Exception as e:
323
- err_msg = str(e).lower()
324
- if "execution context was destroyed" in err_msg or "context or browser has been closed" in err_msg:
325
- if attempt < max_retries - 1:
326
- time.sleep(3)
327
- try:
328
- page.wait_for_load_state("domcontentloaded", timeout=5000)
329
- except Exception:
330
- pass
331
- continue
332
- raise
333
- raise CodeBuddyAutomationError("Failed to evaluate JS after context destroyed retries")
334
-
335
- def main():
336
- parser = argparse.ArgumentParser(description="CodeBuddy auto-signup tool")
337
- parser.add_argument("--email", required=True)
338
- parser.add_argument("--password", required=True)
339
- parser.add_argument("--proxy-server")
340
- parser.add_argument("--proxy-user")
341
- parser.add_argument("--proxy-pass")
342
- parser.add_argument("--profiles-dir", required=True)
343
- parser.add_argument("--headless", action="store_true", default=False)
344
- args = parser.parse_args()
345
-
346
- profiles_root = Path(args.profiles_dir)
347
- profiles_root.mkdir(parents=True, exist_ok=True)
348
- profile_dir = profiles_root / safe_email_to_dirname(args.email)
349
- profile_dir.mkdir(parents=True, exist_ok=True)
350
-
351
- proxy_dict = None
352
- if args.proxy_server:
353
- proxy_dict = {"server": args.proxy_server}
354
- if args.proxy_user:
355
- proxy_dict["username"] = args.proxy_user
356
- if args.proxy_pass:
357
- proxy_dict["password"] = args.proxy_pass
358
-
359
- try:
360
- from camoufox.sync_api import Camoufox
361
- except ImportError:
362
- sys.stdout.write(json.dumps({"status": "error", "message": "Camoufox package not installed in python environment."}) + "\n")
363
- sys.exit(1)
364
-
365
- kwargs = dict(
366
- headless=args.headless,
367
- persistent_context=True, no_viewport=True,
368
- user_data_dir=str(profile_dir),
369
- humanize=True,
370
- geoip=True,
371
- locale="en-US",
372
- os=("windows", "macos", "linux"),
373
- window=(1280, 800),
374
- firefox_user_prefs={
375
- "network.trr.mode": 5,
376
- }
377
- )
378
- if proxy_dict:
379
- kwargs["proxy"] = {k: v for k, v in proxy_dict.items() if v}
380
-
381
- log_step("Meluncurkan browser...")
382
- ctx_manager = None
383
- try:
384
- try:
385
- ctx_manager = Camoufox(**kwargs)
386
- except TypeError:
387
- for drop in ("window", "os", "geoip", "humanize", "locale"):
388
- kwargs.pop(drop, None)
389
- try:
390
- ctx_manager = Camoufox(**kwargs)
391
- break
392
- except TypeError:
393
- continue
394
- if not ctx_manager:
395
- ctx_manager = Camoufox(**kwargs)
396
-
397
- with ctx_manager as browser:
398
- page = browser.new_page()
399
-
400
- log_step("Membuka halaman login CodeBuddy...")
401
- page.goto(CODEBUDDY_LOGIN_URL, wait_until="domcontentloaded", timeout=45000)
402
- time.sleep(3)
403
-
404
- if "codebuddy.ai" in page.url.lower() and "/login" in page.url.lower():
405
- perform_login_page_actions(page, args.email)
406
- try:
407
- page.wait_for_url(lambda url: "accounts.google.com" in url or ("codebuddy.ai" in url and "/login" not in url), timeout=20000)
408
- except Exception:
409
- pass
410
-
411
- if "accounts.google.com" in page.url.lower():
412
- do_google_login(page, args.email, args.password)
413
- try:
414
- page.wait_for_url("**/codebuddy.ai/**", timeout=30000)
415
- except Exception:
416
- pass
417
-
418
- max_google_loops = 10
419
- for loop_i in range(max_google_loops):
420
- cur_url = page.url.lower()
421
- if "accounts.google.com" not in cur_url and "myaccount.google.com" not in cur_url:
422
- break
423
-
424
- log_step(f"Halaman interstitial Google: {page.url[:80]}...")
425
- try:
426
- page.wait_for_load_state("networkidle", timeout=5000)
427
- except Exception:
428
- pass
429
-
430
- clicked = click_first(page, [
431
- "button:has-text('Saya mengerti')",
432
- "a:has-text('Saya mengerti')",
433
- "button:has-text('I understand')",
434
- "button:has-text('I Understand')",
435
- "button:has-text('Continue')",
436
- "button:has-text('Allow')",
437
- "button:has-text('Accept')",
438
- "button:has-text('Agree')",
439
- "button:has-text('I agree')",
440
- "button:has-text('Next')",
441
- ".VfPpkd-LgbsSe[data-mdc-ripple-is-unbounded]",
442
- "button[jsname='LgbsSe']",
443
- "input[type='submit']",
444
- "button[type='submit']",
445
- ], timeout_ms=4000)
446
-
447
- if clicked:
448
- log_step("Mengklik persetujuan Google, menunggu redirect...")
449
- time.sleep(3)
450
- else:
451
- try:
452
- continue_url = page.evaluate("""() => {
453
- const url = new URL(window.location.href);
454
- return url.searchParams.get('continue') || '';
455
- }""")
456
- if continue_url and "google.com" in continue_url:
457
- page.goto(continue_url, wait_until="domcontentloaded", timeout=15000)
458
- time.sleep(3)
459
- else:
460
- try:
461
- js_clicked = page.evaluate("""() => {
462
- const btns = Array.from(document.querySelectorAll('button,input[type=submit]'));
463
- const target = btns.find(b => {
464
- const t = (b.innerText || b.value || '').trim();
465
- return t === 'Continue' || t === 'Allow' || t === 'Saya mengerti' || t === 'I understand' || t === 'Accept';
466
- });
467
- if (target) { target.click(); return true; }
468
- return false;
469
- }""")
470
- if js_clicked:
471
- time.sleep(3)
472
- continue
473
- except Exception:
474
- pass
475
- time.sleep(3)
476
- except Exception:
477
- time.sleep(3)
478
-
479
- if "no-permission" in page.url.lower() or "login" in page.url.lower():
480
- log_step("Navigasi ke /register/user/complete...")
481
- page.goto("https://www.codebuddy.ai/register/user/complete", wait_until="domcontentloaded", timeout=20000)
482
- time.sleep(3)
483
-
484
- if "register" in page.url.lower():
485
- country = get_proxy_country(proxy_dict)
486
- log_step(f"Mengisi lokasi registrasi ({country})...")
487
- try:
488
- page.evaluate(f"""() => {{
489
- let inputs = Array.from(document.querySelectorAll('input'));
490
- for (let input of inputs) {{
491
- if (input.placeholder && input.placeholder.toLowerCase().includes('location')) {{
492
- input.value = '{country}';
493
- input.dispatchEvent(new Event('input', {{ bubbles: true }}));
494
- input.dispatchEvent(new Event('change', {{ bubbles: true }}));
495
- }}
496
- }}
497
- }}""")
498
- time.sleep(1)
499
- click_first(page, [
500
- "button:has-text('Submit')",
501
- "button:has-text('Complete')",
502
- "button:has-text('Save')",
503
- "button:has-text('Continue')"
504
- ])
505
- time.sleep(5)
506
- except Exception:
507
- pass
508
-
509
- log_step("Menunggu aktivasi trial otomatis...")
510
- try:
511
- with page.expect_response(lambda r: "billing/ide/trial" in r.url, timeout=15000):
512
- pass
513
- time.sleep(2)
514
- except Exception:
515
- pass
516
-
517
- if "home" not in page.url.lower() and "ide" not in page.url.lower() and "profile" not in page.url.lower():
518
- page.goto("https://www.codebuddy.ai/home", wait_until="domcontentloaded", timeout=15000)
519
- time.sleep(4)
520
-
521
- log_step("Mengaktifkan trial IDE secara manual...")
522
- try:
523
- trial_res = safe_evaluate(page, """async () => {
524
- try {
525
- const res = await fetch("https://www.codebuddy.ai/billing/ide/trial", {
526
- method: "POST",
527
- headers: {
528
- "Accept": "application/json, text/plain, */*",
529
- "x-requested-with": "XMLHttpRequest"
530
- }
531
- });
532
- const text = await res.text();
533
- try { return JSON.parse(text); } catch(e) { return {raw: text, status: res.status}; }
534
- } catch(e) {
535
- return {error: e.toString()};
536
- }
537
- }""")
538
- time.sleep(2)
539
- except Exception:
540
- pass
541
-
542
- log_step("Memanggil API internal generate API Key...")
543
- unique_key_name = f"AutoKey_{int(time.time())}"
544
- api_payload = {
545
- "name": unique_key_name,
546
- "expire_in_days": 365,
547
- "user_enterprise_id": "personal-edition-user-id"
548
- }
549
-
550
- result = safe_evaluate(page, f"""async (payload) => {{
551
- try {{
552
- const res = await fetch("{CODEBUDDY_API_KEYS_ENDPOINT}", {{
553
- method: "POST",
554
- headers: {{
555
- "Content-Type": "application/json",
556
- "Accept": "application/json"
557
- }},
558
- body: JSON.stringify(payload)
559
- }});
560
- const text = await res.text();
561
- let data = null;
562
- try {{ data = JSON.parse(text); }} catch(e) {{}}
563
- return {{ok: res.ok, status: res.status, data: data}};
564
- }} catch(e) {{
565
- return {{ok: false, error: e.toString()}};
566
- }}
567
- }}""", api_payload)
568
-
569
- api_key = ""
570
- if result and result.get("ok") and result.get("data"):
571
- data = result["data"]
572
- if isinstance(data, dict):
573
- inner = data.get("data") or {}
574
- api_key = inner.get("key") or inner.get("api_key") or data.get("key") or data.get("api_key") or ""
575
-
576
- if not api_key and result and result.get("data") and isinstance(result["data"], dict):
577
- err_code = result["data"].get("code")
578
- if err_code == 12502:
579
- log_step("Nama key konflik, mencoba generate dengan nama lain...")
580
- retry_name = f"AutoKey_{int(time.time())}_r"
581
- retry_payload = dict(api_payload, name=retry_name)
582
- try:
583
- retry_result = safe_evaluate(page, f"""async (payload) => {{
584
- const res = await fetch("{CODEBUDDY_API_KEYS_ENDPOINT}", {{
585
- method: "POST",
586
- headers: {{"Content-Type": "application/json", "Accept": "application/json"}},
587
- body: JSON.stringify(payload)
588
- }});
589
- const text = await res.text();
590
- let data = null;
591
- try {{ data = JSON.parse(text); }} catch(e) {{}}
592
- return {{ok: res.ok, status: res.status, data: data}};
593
- }}""", retry_payload)
594
- if retry_result and retry_result.get("ok") and retry_result.get("data"):
595
- inner2 = (retry_result["data"].get("data") or {})
596
- api_key = inner2.get("key") or inner2.get("api_key") or ""
597
- except Exception:
598
- pass
599
-
600
- if not api_key:
601
- log_step("Mencari API Key yang sudah ada di list...")
602
- list_result = safe_evaluate(page, f"""async () => {{
603
- const res = await fetch("{CODEBUDDY_API_KEYS_ENDPOINT}?page=1&page_size=10&user_enterprise_id=personal-edition-user-id");
604
- const text = await res.text();
605
- let data = null;
606
- try {{ data = JSON.parse(text); }} catch(e) {{}}
607
- return {{ok: res.ok, status: res.status, data: data}};
608
- }}""")
609
-
610
- if list_result and list_result.get("data") and isinstance(list_result["data"], dict):
611
- inner = list_result["data"].get("data") or {}
612
- items = inner.get("items") or inner.get("list") or []
613
- if items:
614
- masked = items[0].get("masked_key", "")
615
- sys.stdout.write(json.dumps({"status": "error", "message": f"Akun sudah memiliki API key ({masked}), tetapi hanya masked key yang tersedia."}) + "\n")
616
- sys.exit(1)
617
-
618
- if api_key:
619
- log_step("Menguji validitas API Key...")
620
- if test_api_key(api_key, proxy_dict):
621
- log_step("API Key valid dan siap digunakan!")
622
- sys.stdout.write(json.dumps({"status": "success", "api_key": api_key}) + "\n")
623
- sys.exit(0)
624
- else:
625
- sys.stdout.write(json.dumps({"status": "error", "message": "API Key berhasil digenerate tetapi gagal tes fungsionalitas chat."}) + "\n")
626
- sys.exit(1)
627
- else:
628
- sys.stdout.write(json.dumps({"status": "error", "message": "Gagal generate API Key dari CodeBuddy."}) + "\n")
629
- sys.exit(1)
630
-
631
- except Exception as e:
632
- sys.stdout.write(json.dumps({"status": "error", "message": str(e)}) + "\n")
633
- sys.exit(1)
634
-
635
- if __name__ == "__main__":
636
- main()