@fudrouter/fsrouter 0.6.80 → 0.6.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env python3
2
+ """Flashloop account auto-signup via Camoufox (anti-fingerprint) + Fsmail email verification.
3
+
4
+ Referral code: UWZKVP (hardcoded in URL)
5
+
6
+ Outputs JSON lines to stdout:
7
+ {"step": "..."} — progress update
8
+ {"status": "success", "email": "...", "password": "..."} — final result
9
+ {"status": "error", "error": "..."} — failure
10
+ """
11
+
12
+ import sys
13
+ import json
14
+ import argparse
15
+ import time
16
+ import random
17
+ import string
18
+ import re
19
+ import urllib.request
20
+ import urllib.parse
21
+ import urllib.error
22
+ from pathlib import Path
23
+
24
+ REFERRAL_CODE = "UWZKVP"
25
+ SIGNUP_URL = "https://www.flashloop.app/onboarding"
26
+
27
+ # ── Stdout JSON helpers ────────────────────────────────────────────────────────
28
+ def emit(obj):
29
+ print(json.dumps(obj), flush=True)
30
+
31
+ def log_step(msg):
32
+ emit({"step": msg})
33
+
34
+ def success(email, password):
35
+ emit({"status": "success", "email": email, "password": password})
36
+
37
+ def die(msg):
38
+ emit({"status": "error", "error": msg})
39
+ sys.exit(1)
40
+
41
+ # ── Fsmail helpers ─────────────────────────────────────────────────────────────
42
+ def fsmail_request(base_url, api_key, path, method="GET", data=None):
43
+ url = base_url.rstrip("/") + "/api" + path
44
+ req = urllib.request.Request(url, method=method)
45
+ req.add_header("Authorization", f"Bearer {api_key}")
46
+ req.add_header("Content-Type", "application/json")
47
+ req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
48
+ if data:
49
+ req.data = json.dumps(data).encode()
50
+ with urllib.request.urlopen(req, timeout=15) as resp:
51
+ return json.loads(resp.read())
52
+
53
+ def create_fsmail_inbox(base_url, api_key):
54
+ """Create a random inbox and return (email, alias)."""
55
+ rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
56
+ alias = f"fl{rand}"
57
+ domain = None
58
+
59
+ # Get available domains
60
+ try:
61
+ domains_data = fsmail_request(base_url, api_key, "/domains")
62
+ domains = domains_data.get("domains", [])
63
+ if domains:
64
+ domain = domains[0].get("name", domains[0].get("domain", ""))
65
+ except Exception:
66
+ pass
67
+
68
+ if not domain:
69
+ die("Tidak ada domain tersedia di Fsmail")
70
+
71
+ email = f"{alias}@{domain}"
72
+
73
+ try:
74
+ fsmail_request(base_url, api_key, "/inboxes", method="POST",
75
+ data={"alias": alias, "domain": domain})
76
+ except Exception as e:
77
+ log_step(f"Warning: inbox creation: {e}")
78
+
79
+ return email, alias
80
+
81
+ def wait_for_flashloop_email():
82
+ """Wait for Flashloop verification email and extract OTP code."""
83
+ log_step(f"Menunggu email verifikasi Flashloop...")
84
+ deadline = time.time() + timeout
85
+ seen_ids = set()
86
+
87
+ while time.time() < deadline:
88
+ try:
89
+ data = fsmail_request(base_url, api_key, f"/inboxes/{urllib.parse.quote(alias)}/messages")
90
+ messages = data.get("messages", [])
91
+ for msg in messages:
92
+ msg_id = msg.get("id", "")
93
+ subject = msg.get("subject", "")
94
+ if msg_id in seen_ids:
95
+ continue
96
+ seen_ids.add(msg_id)
97
+
98
+ subj_lower = subject.lower()
99
+ is_flashloop = (
100
+ "flashloop" in subj_lower or
101
+ "verify" in subj_lower or
102
+ "confirm" in subj_lower or
103
+ "code" in subj_lower or
104
+ "otp" in subj_lower
105
+ )
106
+ if is_flashloop:
107
+ # Fetch full message
108
+ try:
109
+ full = fsmail_request(base_url, api_key,
110
+ f"/inboxes/{urllib.parse.quote(alias)}/messages/{urllib.parse.quote(msg_id)}")
111
+ body = full.get("body", "") or full.get("text", "") or full.get("html", "")
112
+
113
+ # Extract OTP code (usually 4-8 digits)
114
+ otp_match = re.search(r'\b(\d{4,8})\b', body)
115
+ if otp_match:
116
+ code = otp_match.group(1)
117
+ log_step(f"Kode verifikasi ditemukan: {code}")
118
+ return code
119
+
120
+ # Try extracting from subject
121
+ otp_subj = re.search(r'\b(\d{4,8})\b', subject)
122
+ if otp_subj:
123
+ code = otp_subj.group(1)
124
+ log_step(f"Kode verifikasi dari subject: {code}")
125
+ return code
126
+
127
+ # Try extracting verification link
128
+ link_match = re.search(r'https?://[^\s"<>]+verify[^\s"<>]*', body)
129
+ if not link_match:
130
+ link_match = re.search(r'https?://[^\s"<>]+confirm[^\s"<>]*', body)
131
+ if link_match:
132
+ link = link_match.group(0)
133
+ log_step(f"Link verifikasi ditemukan")
134
+ return {"type": "link", "url": link}
135
+
136
+ log_step(f"Email ditemukan tapi tidak bisa extract kode/link")
137
+ return body # Return raw body for manual parsing
138
+ except Exception as e:
139
+ log_step(f"Error fetching message: {e}")
140
+ except Exception:
141
+ pass
142
+
143
+ time.sleep(3)
144
+
145
+ die("Timeout menunggu email verifikasi Flashloop")
146
+
147
+ # ── Password generator ─────────────────────────────────────────────────────────
148
+ def gen_password(length=14):
149
+ chars = string.ascii_letters + string.digits + "!@#$%"
150
+ while True:
151
+ pwd = ''.join(random.choices(chars, k=length))
152
+ if (any(c.isupper() for c in pwd) and
153
+ any(c.islower() for c in pwd) and
154
+ any(c.isdigit() for c in pwd)):
155
+ return pwd
156
+
157
+ # ── Main ───────────────────────────────────────────────────────────────────────
158
+ def main():
159
+ parser = argparse.ArgumentParser()
160
+ parser.add_argument("--email", required=True)
161
+ parser.add_argument("--password", required=True)
162
+ parser.add_argument("--fsmail-url", required=True)
163
+ parser.add_argument("--fsmail-key", required=True)
164
+ parser.add_argument("--profiles-dir", required=True)
165
+ parser.add_argument("--headless", action="store_true", default=True)
166
+ parser.add_argument("--delay", type=float, default=0)
167
+ args = parser.parse_args()
168
+
169
+ if args.delay > 0:
170
+ log_step(f"Delay {args.delay}s...")
171
+ time.sleep(args.delay)
172
+
173
+ email = args.email
174
+ password = args.password
175
+ alias = email.split("@")[0]
176
+ log_step(f"Email: {email}")
177
+
178
+ # Launch browser
179
+ log_step("Meluncurkan browser...")
180
+ try:
181
+ from camoufox.sync_api import Camoufox
182
+ except ImportError:
183
+ die("Camoufox tidak terinstall. Jalankan: pip install camoufox && python -m camoufox fetch")
184
+
185
+ launch_kwargs = dict(headless=args.headless, os="windows", locale="en-US")
186
+
187
+ def _make_camoufox(kw):
188
+ try:
189
+ return Camoufox(**kw)
190
+ except TypeError:
191
+ kw.pop("os", None)
192
+ try:
193
+ return Camoufox(**kw)
194
+ except TypeError:
195
+ kw.pop("locale", None)
196
+ return Camoufox(**kw)
197
+
198
+ browser_ctx = _make_camoufox(dict(launch_kwargs))
199
+
200
+ with browser_ctx as browser:
201
+ page = browser.new_page()
202
+
203
+ # Step 1: Go to signup page with referral code
204
+ log_step("Membuka halaman signup Flashloop...")
205
+ page.goto(SIGNUP_URL, wait_until="networkidle", timeout=30000)
206
+ time.sleep(5)
207
+
208
+ # Step 2: Click "Continue with Email"
209
+ log_step("Klik 'Continue with Email'...")
210
+ try:
211
+ email_btn = page.locator('a:has-text("Continue with Email"), button:has-text("Continue with Email")').first
212
+ email_btn.wait_for(timeout=15000)
213
+ email_btn.click()
214
+ time.sleep(2)
215
+ except Exception as e:
216
+ die(f"Tidak bisa klik 'Continue with Email': {e}")
217
+
218
+ # Step 3: Fill signup form
219
+ log_step("Mengisi form signup...")
220
+ try:
221
+ # Email field
222
+ email_input = page.locator('input[placeholder*="Email"], input[type="email"], input[name="email"]').first
223
+ email_input.wait_for(timeout=10000)
224
+ email_input.fill(email)
225
+ time.sleep(0.5)
226
+
227
+ # Password field
228
+ pwd_input = page.locator('input[placeholder*="Password"], input[type="password"], input[name="password"]').first
229
+ pwd_input.fill(password)
230
+ time.sleep(0.5)
231
+ except Exception as e:
232
+ die(f"Gagal mengisi form: {e}")
233
+
234
+ # Step 4: Click "Create Account"
235
+ log_step("Membuat akun...")
236
+ try:
237
+ create_btn = page.locator('button:has-text("Create Account"), button:has-text("Sign Up"), button:has-text("Register")').first
238
+ create_btn.click()
239
+ time.sleep(3)
240
+ except Exception as e:
241
+ die(f"Gagal klik 'Create Account': {e}")
242
+
243
+ # Step 5: Handle post-signup (referral code + optional verification)
244
+ log_step("Menunggu halaman setelah signup...")
245
+ time.sleep(3)
246
+
247
+ # Check for referral code input: "GOT A REFERRAL CODE?"
248
+ try:
249
+ page_text_lower = page.content().lower()
250
+ if "referral" in page_text_lower or "bonus credits" in page_text_lower:
251
+ log_step("Halaman referral terdeteksi, memasukkan kode UWZKVP...")
252
+ # Try multiple locator strategies
253
+ ref_input = None
254
+ for selector in [
255
+ 'input[placeholder*="referral" i]',
256
+ 'input[placeholder*="code" i]',
257
+ 'input[name*="referral" i]',
258
+ 'input[name*="code" i]',
259
+ 'input[type="text"]',
260
+ 'input:not([type="password"]):not([type="hidden"]):not([type="email"])',
261
+ ]:
262
+ try:
263
+ el = page.locator(selector).first
264
+ if el.is_visible(timeout=1000):
265
+ ref_input = el
266
+ break
267
+ except Exception:
268
+ continue
269
+
270
+ if ref_input:
271
+ ref_input.fill(REFERRAL_CODE)
272
+ time.sleep(0.5)
273
+ # Try to find and click Apply Code button
274
+ for btn_text in ["Apply Code", "Apply", "Submit", "Continue"]:
275
+ try:
276
+ btn = page.locator(f'button:has-text("{btn_text}")').first
277
+ if btn.is_visible(timeout=1000):
278
+ btn.click()
279
+ log_step(f"Klik '{btn_text}' - kode referral diterapkan!")
280
+ break
281
+ except Exception:
282
+ continue
283
+ time.sleep(3)
284
+ else:
285
+ log_step("Input referral tidak ditemukan, lanjut...")
286
+ except Exception as e:
287
+ log_step(f"Referral step tidak ditemukan atau sudah di-skip: {e}")
288
+
289
+ # Check if already logged in (pricing page or dashboard)
290
+ current_url = page.url
291
+ if "pricing" in current_url or "dashboard" in current_url or "app" in current_url:
292
+ log_step(f"Sudah masuk! URL: {current_url}")
293
+ success(email, password)
294
+ else:
295
+ # Check for OTP input
296
+ otp_inputs = page.locator('input[maxlength="1"], input[placeholder*="code"], input[placeholder*="Code"], input[type="tel"], input[name*="code"], input[name*="otp"]')
297
+
298
+ if otp_inputs.count() > 0:
299
+ log_step("Halaman OTP terdeteksi, menunggu kode dari email...")
300
+ result = wait_for_flashloop_email()
301
+
302
+ if isinstance(result, dict) and result.get("type") == "link":
303
+ log_step("Membuka link verifikasi...")
304
+ page.goto(result["url"], wait_until="domcontentloaded", timeout=30000)
305
+ time.sleep(5)
306
+ elif isinstance(result, str) and len(result) <= 8 and result.isdigit():
307
+ log_step(f"Memasukkan kode OTP: {result}")
308
+ inputs = page.locator('input[maxlength="1"], input[placeholder*="code"], input[type="tel"]')
309
+ for i, digit in enumerate(result):
310
+ if i < inputs.count():
311
+ inputs.nth(i).fill(digit)
312
+ time.sleep(0.2)
313
+ time.sleep(2)
314
+ try:
315
+ verify_btn = page.locator('button:has-text("Verify"), button:has-text("Confirm"), button:has-text("Submit")').first
316
+ verify_btn.click(timeout=5000)
317
+ time.sleep(3)
318
+ except Exception:
319
+ pass
320
+ else:
321
+ log_step("Tidak ada kode verifikasi, lanjut...")
322
+ else:
323
+ log_step("Tidak ada OTP, menunggu sebentar...")
324
+ time.sleep(5)
325
+
326
+ # Final check
327
+ log_step("Menunggu halaman akhir...")
328
+ time.sleep(5)
329
+ current_url = page.url
330
+ log_step(f"URL akhir: {current_url}")
331
+
332
+ if "onboarding" not in current_url or "login" not in current_url:
333
+ success(email, password)
334
+ else:
335
+ die(f"Signup gagal. URL: {current_url}")
336
+
337
+ if __name__ == "__main__":
338
+ main()