@fudrouter/fsrouter 0.6.79 → 0.6.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/open-sse/providers/registry/nous-research.js +1 -0
- package/dist/open-sse/shared/machineId.js +2 -1
- package/open-sse/providers/registry/nous-research.js +1 -0
- package/open-sse/shared/machineId.js +2 -1
- package/package.json +1 -1
- package/src/automation/cf_token_via_session.py +158 -0
- package/src/automation/cloudflare_signup.py +3689 -0
- package/src/automation/codebuddy_signup.py +636 -0
- package/src/automation/flashloop_signup.py +338 -0
- package/src/automation/kimi_signup.py +401 -0
- package/src/automation/leonardo_signup.py +902 -0
- package/src/automation/openvecta_signup.py +652 -0
- package/src/automation/qoder_signup.py +323 -0
- package/src/automation/test_proxy.py +86 -0
- package/src/automation/weavy_signup.py +1964 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Standalone script to automate Qoder login via Google OAuth and retrieve device token.
|
|
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 uuid
|
|
13
|
+
import hashlib
|
|
14
|
+
import base64
|
|
15
|
+
import urllib.request
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Setup simple stdout logger to not conflict with JSON line prints
|
|
21
|
+
logging.basicConfig(level=logging.WARNING)
|
|
22
|
+
logger = logging.getLogger("qoder_signup")
|
|
23
|
+
|
|
24
|
+
class QoderAutomationError(RuntimeError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def log_step(msg, *args):
|
|
28
|
+
txt = msg % args if args else msg
|
|
29
|
+
sys.stdout.write(json.dumps({"step": txt}, ensure_ascii=False) + "\n")
|
|
30
|
+
sys.stdout.flush()
|
|
31
|
+
|
|
32
|
+
def safe_email_to_dirname(email: str) -> str:
|
|
33
|
+
cleaned = (email or "").strip().lower()
|
|
34
|
+
cleaned = cleaned.replace("@", "_at_")
|
|
35
|
+
cleaned = re.sub(r"[^a-z0-9._-]+", "_", cleaned)
|
|
36
|
+
cleaned = cleaned.strip("._-")
|
|
37
|
+
return cleaned or "account"
|
|
38
|
+
|
|
39
|
+
def base64Url(buf: bytes) -> str:
|
|
40
|
+
return base64.urlsafe_b64encode(buf).decode("utf-8").replace("=", "")
|
|
41
|
+
|
|
42
|
+
def poll_device_token(nonce: str, verifier: str) -> dict:
|
|
43
|
+
url = f"https://openapi.qoder.sh/api/v1/deviceToken/poll?nonce={urllib.parse.quote(nonce)}&verifier={urllib.parse.quote(verifier)}&challenge_method=S256"
|
|
44
|
+
headers = {
|
|
45
|
+
"Accept": "application/json",
|
|
46
|
+
"User-Agent": "Go-http-client/2.0",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Poll up to 60 seconds
|
|
50
|
+
deadline = time.time() + 60.0
|
|
51
|
+
while time.time() < deadline:
|
|
52
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
53
|
+
try:
|
|
54
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
55
|
+
status = resp.status
|
|
56
|
+
if status == 200:
|
|
57
|
+
res_data = json.loads(resp.read().decode("utf-8"))
|
|
58
|
+
if "token" in res_data:
|
|
59
|
+
return res_data
|
|
60
|
+
except urllib.error.HTTPError as he:
|
|
61
|
+
# 202/404 means authorization pending
|
|
62
|
+
if he.code in (202, 404):
|
|
63
|
+
pass
|
|
64
|
+
else:
|
|
65
|
+
log_step(f"Polling HTTP Error: {he.code}")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
log_step(f"Polling Network Error: {e}")
|
|
68
|
+
|
|
69
|
+
time.sleep(2.0)
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
def fetch_user_info(access_token: str) -> dict:
|
|
73
|
+
url = "https://openapi.qoder.sh/api/v1/userinfo"
|
|
74
|
+
headers = {
|
|
75
|
+
"Authorization": f"Bearer {access_token}",
|
|
76
|
+
"Accept": "application/json",
|
|
77
|
+
"User-Agent": "Go-http-client/2.0",
|
|
78
|
+
}
|
|
79
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
82
|
+
if resp.status == 200:
|
|
83
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
parser = argparse.ArgumentParser(description="Qoder Google auto-login tool")
|
|
90
|
+
parser.add_argument("--email", required=True)
|
|
91
|
+
parser.add_argument("--password", required=True)
|
|
92
|
+
parser.add_argument("--proxy-server")
|
|
93
|
+
parser.add_argument("--proxy-user")
|
|
94
|
+
parser.add_argument("--proxy-pass")
|
|
95
|
+
parser.add_argument("--profiles-dir", required=True)
|
|
96
|
+
parser.add_argument("--headless", action="store_true", default=False)
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
|
|
99
|
+
# Step 1: Initiate local device flow PKCE/nonce parameters
|
|
100
|
+
verifier = base64Url(os.urandom(32))
|
|
101
|
+
challenge = base64Url(hashlib.sha256(verifier.encode("utf-8")).digest())
|
|
102
|
+
nonce = str(uuid.uuid4())
|
|
103
|
+
machine_id = str(uuid.uuid4())
|
|
104
|
+
|
|
105
|
+
client_id = "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb"
|
|
106
|
+
url = f"https://qoder.com/device/selectAccounts?challenge={challenge}&challenge_method=S256&machine_id={machine_id}&nonce={nonce}&client_id={client_id}"
|
|
107
|
+
log_step(f"Meluncurkan flow otorisasi Qoder...")
|
|
108
|
+
|
|
109
|
+
# Step 2: Set up profile dir
|
|
110
|
+
profiles_root = Path(args.profiles_dir)
|
|
111
|
+
profiles_root.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
profile_dir = profiles_root / safe_email_to_dirname(args.email)
|
|
113
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
proxy_dict = None
|
|
116
|
+
if args.proxy_server:
|
|
117
|
+
proxy_dict = {"server": args.proxy_server}
|
|
118
|
+
if args.proxy_user:
|
|
119
|
+
proxy_dict["username"] = args.proxy_user
|
|
120
|
+
if args.proxy_pass:
|
|
121
|
+
proxy_dict["password"] = args.proxy_pass
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
from camoufox.sync_api import Camoufox
|
|
125
|
+
except ImportError:
|
|
126
|
+
sys.stdout.write(json.dumps({"status": "error", "message": "Camoufox tidak terinstall di environment python."}) + "\n")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
kwargs = dict(
|
|
130
|
+
headless=args.headless,
|
|
131
|
+
persistent_context=True, no_viewport=True,
|
|
132
|
+
user_data_dir=str(profile_dir),
|
|
133
|
+
humanize=True,
|
|
134
|
+
geoip=True,
|
|
135
|
+
locale="en-US",
|
|
136
|
+
os=("windows", "macos", "linux"),
|
|
137
|
+
window=(1280, 800),
|
|
138
|
+
firefox_user_prefs={
|
|
139
|
+
"network.trr.mode": 5,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
if proxy_dict:
|
|
143
|
+
kwargs["proxy"] = {k: v for k, v in proxy_dict.items() if v}
|
|
144
|
+
|
|
145
|
+
log_step("Meluncurkan browser...")
|
|
146
|
+
token_data = {}
|
|
147
|
+
try:
|
|
148
|
+
with Camoufox(**kwargs) as browser:
|
|
149
|
+
context = getattr(browser, "context", None) or browser
|
|
150
|
+
page = context.new_page()
|
|
151
|
+
|
|
152
|
+
log_step("Membuka halaman verifikasi Qoder...")
|
|
153
|
+
page.goto(url, wait_until="domcontentloaded", timeout=45000)
|
|
154
|
+
|
|
155
|
+
# Wait for either sign-in or authorization page to render
|
|
156
|
+
login_needed = False
|
|
157
|
+
for _ in range(30):
|
|
158
|
+
cur_url = page.url.lower()
|
|
159
|
+
if "sign-in" in cur_url or page.locator("a:has-text('Sign in with Google')").count() > 0:
|
|
160
|
+
login_needed = True
|
|
161
|
+
break
|
|
162
|
+
# Authorize / Confirm page or success already loaded
|
|
163
|
+
if "success" in cur_url or page.locator("button:has-text('Confirm'), button:has-text('Authorize'), button:has-text('Allow')").count() > 0 or "success" in (page.title() or "").lower():
|
|
164
|
+
break
|
|
165
|
+
time.sleep(0.5)
|
|
166
|
+
|
|
167
|
+
if login_needed:
|
|
168
|
+
google_sel = "a:has-text('Sign in with Google'), a[href*='/sso/login/google']"
|
|
169
|
+
try:
|
|
170
|
+
page.wait_for_selector(google_sel, timeout=10000)
|
|
171
|
+
google_link = page.locator(google_sel).first
|
|
172
|
+
log_step("Mengklik tombol Google Sign-in...")
|
|
173
|
+
google_link.click(force=True)
|
|
174
|
+
time.sleep(5.0)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
log_step(f"Tombol Google Sign-in tidak ditemukan: {e}")
|
|
177
|
+
|
|
178
|
+
# Handle Google login pages
|
|
179
|
+
google_deadline = time.time() + 60.0
|
|
180
|
+
filled_email = False
|
|
181
|
+
filled_pass = False
|
|
182
|
+
clicked_account = False
|
|
183
|
+
|
|
184
|
+
GOOGLE_EMAIL_SELECTORS = ["input[type='email']", "input[name='identifier']", "input#identifierId"]
|
|
185
|
+
GOOGLE_PASSWORD_SELECTORS = ["input[type='password'][name='Passwd']", "input[type='password']", "input[name='password']"]
|
|
186
|
+
|
|
187
|
+
while time.time() < google_deadline:
|
|
188
|
+
cur_url = page.url.lower()
|
|
189
|
+
if "google.com" not in cur_url and "googleusercontent.com" not in cur_url:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
# Disabled speedbump check
|
|
193
|
+
if "speedbump/disabled" in cur_url or "disabled" in cur_url or "disabled" in (page.title() or "").lower():
|
|
194
|
+
raise QoderAutomationError(f"Akun Google ({args.email}) dinonaktifkan oleh Google.")
|
|
195
|
+
|
|
196
|
+
# 1. Chooser page / Select account
|
|
197
|
+
if not clicked_account and ("accountchooser" in cur_url or "Choose an account" in (page.title() or "")):
|
|
198
|
+
clicked = page.evaluate(f"""
|
|
199
|
+
() => {{
|
|
200
|
+
const email = '{args.email}';
|
|
201
|
+
const lis = document.querySelectorAll('li[data-identifier], li.JDAKTe');
|
|
202
|
+
for (const li of lis) {{
|
|
203
|
+
const id = li.getAttribute('data-identifier') || '';
|
|
204
|
+
if (id === email || li.textContent.includes(email)) {{
|
|
205
|
+
li.click(); return true;
|
|
206
|
+
}}
|
|
207
|
+
}}
|
|
208
|
+
const all = document.querySelectorAll('[data-email], [data-identifier]');
|
|
209
|
+
for (const el of all) {{
|
|
210
|
+
const v = el.getAttribute('data-email') || el.getAttribute('data-identifier') || '';
|
|
211
|
+
if (v === email) {{ el.click(); return true; }}
|
|
212
|
+
}}
|
|
213
|
+
return false;
|
|
214
|
+
}}
|
|
215
|
+
""")
|
|
216
|
+
if clicked:
|
|
217
|
+
log_step("JS klik akun di chooser...")
|
|
218
|
+
clicked_account = True
|
|
219
|
+
time.sleep(3.0)
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# 2. Email input
|
|
223
|
+
if not filled_email:
|
|
224
|
+
email_in = page.locator(",".join(GOOGLE_EMAIL_SELECTORS)).first
|
|
225
|
+
if email_in.count() > 0 and email_in.is_visible(timeout=500):
|
|
226
|
+
log_step("Mengisi email Google...")
|
|
227
|
+
email_in.fill(args.email)
|
|
228
|
+
page.keyboard.press("Enter")
|
|
229
|
+
filled_email = True
|
|
230
|
+
time.sleep(3.0)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# 3. Password input
|
|
234
|
+
if filled_email and not filled_pass:
|
|
235
|
+
pass_in = page.locator(",".join(GOOGLE_PASSWORD_SELECTORS)).first
|
|
236
|
+
if pass_in.count() > 0 and pass_in.is_visible(timeout=500):
|
|
237
|
+
log_step("Mengisi password Google...")
|
|
238
|
+
pass_in.fill(args.password)
|
|
239
|
+
page.keyboard.press("Enter")
|
|
240
|
+
filled_pass = True
|
|
241
|
+
time.sleep(5.0)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# 4. Workspace terms agreement
|
|
245
|
+
understand_btn = page.locator("button:has-text('I understand'), button:has-text('I Understand')").first
|
|
246
|
+
if understand_btn.count() > 0 and understand_btn.is_visible(timeout=500):
|
|
247
|
+
log_step("Klik 'I understand' di terms...")
|
|
248
|
+
understand_btn.click(timeout=3000)
|
|
249
|
+
time.sleep(2.0)
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# 5. Consent Continue/Allow buttons
|
|
253
|
+
clicked_btn = False
|
|
254
|
+
for btn_text in ["Continue", "Allow", "Next", "Accept", "Confirm"]:
|
|
255
|
+
try:
|
|
256
|
+
btn = page.locator(f"button:has-text('{btn_text}')").first
|
|
257
|
+
if btn.count() > 0 and btn.is_visible(timeout=300):
|
|
258
|
+
log_step(f"Klik tombol Google: {btn_text}")
|
|
259
|
+
btn.click(timeout=1000, force=True)
|
|
260
|
+
clicked_btn = True
|
|
261
|
+
time.sleep(3.0)
|
|
262
|
+
break
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
if clicked_btn:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
time.sleep(1.0)
|
|
269
|
+
|
|
270
|
+
# Back on Qoder - Wait for authorization to resolve
|
|
271
|
+
time.sleep(5.0)
|
|
272
|
+
|
|
273
|
+
# Check if there is an authorize button to confirm
|
|
274
|
+
confirm_sel = "button:has-text('Confirm'), button:has-text('Authorize'), button:has-text('Allow'), button:has-text('Sign in'), button:has-text('Continue')"
|
|
275
|
+
try:
|
|
276
|
+
confirm_btn = page.locator(confirm_sel).first
|
|
277
|
+
if confirm_btn.count() > 0 and confirm_btn.is_visible(timeout=1000):
|
|
278
|
+
log_step("Mengklik tombol konfirmasi/otorisasi...")
|
|
279
|
+
confirm_btn.click(force=True)
|
|
280
|
+
time.sleep(5.0)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
# Step 3: Poll for the device token
|
|
285
|
+
log_step("Menunggu Qoder menerbitkan token...")
|
|
286
|
+
token_data = poll_device_token(nonce, verifier)
|
|
287
|
+
if not token_data or "token" not in token_data:
|
|
288
|
+
# Take screenshot on warning/error
|
|
289
|
+
try:
|
|
290
|
+
screenshot_path = str(profile_dir / "qoder_not_success.png")
|
|
291
|
+
page.screenshot(path=screenshot_path)
|
|
292
|
+
log_step(f"Screenshot disimpan ke {screenshot_path}")
|
|
293
|
+
html_path = str(profile_dir / "qoder_not_success.html")
|
|
294
|
+
with open(html_path, "w", encoding="utf-8") as f:
|
|
295
|
+
f.write(page.content())
|
|
296
|
+
log_step(f"HTML disimpan ke {html_path}")
|
|
297
|
+
except Exception as ex:
|
|
298
|
+
log_step(f"Gagal mengambil screenshot/HTML: {ex}")
|
|
299
|
+
raise QoderAutomationError("Timeout menunggu device token Qoder.")
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
sys.stdout.write(json.dumps({"status": "error", "message": f"Browser Error: {e}"}) + "\n")
|
|
303
|
+
sys.exit(1)
|
|
304
|
+
|
|
305
|
+
# Step 4: Fetch Profile User Info best effort
|
|
306
|
+
user_info = fetch_user_info(token_data["token"])
|
|
307
|
+
|
|
308
|
+
# Output Success JSON line
|
|
309
|
+
sys.stdout.write(json.dumps({
|
|
310
|
+
"status": "success",
|
|
311
|
+
"api_key": token_data["token"],
|
|
312
|
+
"refresh_token": token_data.get("refresh_token") or "",
|
|
313
|
+
"expires_in": token_data.get("expires_in") or 2592000,
|
|
314
|
+
"user_id": token_data.get("user_id") or user_info.get("user_id") or "",
|
|
315
|
+
"machine_id": machine_id,
|
|
316
|
+
"name": user_info.get("name") or user_info.get("username") or "",
|
|
317
|
+
"email": user_info.get("email") or args.email,
|
|
318
|
+
"organization_id": user_info.get("organization_id") or ""
|
|
319
|
+
}) + "\n")
|
|
320
|
+
sys.stdout.flush()
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Proxy smoke-test using Camoufox — matches leoapi-main approach."""
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
def parse_proxy(proxy_str):
|
|
9
|
+
"""Parse any proxy format into Camoufox proxy dict."""
|
|
10
|
+
if not proxy_str:
|
|
11
|
+
return None
|
|
12
|
+
raw = proxy_str.strip()
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
# Correct URL format: http://user:pass@host:port or socks5://host:port
|
|
17
|
+
url_match = re.match(r'^(socks[45]?|https?|http)://(?:([^:@]+):([^@]+)@)?([^:]+):(\d+)$', raw, re.I)
|
|
18
|
+
if url_match:
|
|
19
|
+
proto, user, password, host, port = url_match.groups()
|
|
20
|
+
proxy = {"server": f"{proto}://{host}:{port}"}
|
|
21
|
+
if user: proxy["username"] = user
|
|
22
|
+
if password: proxy["password"] = password
|
|
23
|
+
return proxy
|
|
24
|
+
|
|
25
|
+
# Malformed: http://host:port:user:pass (incorrectly stored)
|
|
26
|
+
bad_url = re.match(r'^(https?|socks[45]?)://([^:]+):(\d+):([^:]+):(.+)$', raw, re.I)
|
|
27
|
+
if bad_url:
|
|
28
|
+
proto, host, port, user, password = bad_url.groups()
|
|
29
|
+
return {"server": f"http://{host}:{port}", "username": user, "password": password}
|
|
30
|
+
|
|
31
|
+
# Plain: host:port:user:pass
|
|
32
|
+
parts = raw.split(":")
|
|
33
|
+
if len(parts) == 4 and parts[1].isdigit():
|
|
34
|
+
host, port, user, password = parts
|
|
35
|
+
return {"server": f"http://{host}:{port}", "username": user, "password": password}
|
|
36
|
+
|
|
37
|
+
# Plain: host:port
|
|
38
|
+
if len(parts) == 2 and parts[1].isdigit():
|
|
39
|
+
return {"server": f"http://{parts[0]}:{parts[1]}"}
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
parser = argparse.ArgumentParser()
|
|
45
|
+
parser.add_argument("--proxy", required=True)
|
|
46
|
+
parser.add_argument("--headless", action="store_true", default=True)
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
|
|
49
|
+
proxy = parse_proxy(args.proxy)
|
|
50
|
+
if not proxy:
|
|
51
|
+
print(json.dumps({"ok": False, "error": "Invalid proxy format"}))
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from camoufox.sync_api import Camoufox
|
|
56
|
+
except ImportError:
|
|
57
|
+
try:
|
|
58
|
+
from camoufox import Camoufox
|
|
59
|
+
except ImportError:
|
|
60
|
+
print(json.dumps({"ok": False, "error": "Camoufox not installed"}))
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
result = {"ok": False, "server": proxy.get("server")}
|
|
64
|
+
try:
|
|
65
|
+
t0 = time.time()
|
|
66
|
+
kwargs = dict(
|
|
67
|
+
headless=args.headless,
|
|
68
|
+
proxy=proxy,
|
|
69
|
+
humanize=False,
|
|
70
|
+
)
|
|
71
|
+
with Camoufox(**kwargs) as browser:
|
|
72
|
+
context = getattr(browser, "context", None) or browser
|
|
73
|
+
page = context.new_page()
|
|
74
|
+
page.goto("https://api.ipify.org?format=json", wait_until="domcontentloaded", timeout=20000)
|
|
75
|
+
body = page.locator("body").inner_text(timeout=5000)
|
|
76
|
+
info = json.loads(body)
|
|
77
|
+
result["ok"] = True
|
|
78
|
+
result["ip"] = info.get("ip")
|
|
79
|
+
result["latency"] = round((time.time() - t0) * 1000)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
result["error"] = str(e)[:300]
|
|
82
|
+
|
|
83
|
+
print(json.dumps(result))
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|