forked from okky-x0f/qoder-creator
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
2347 lines (2054 loc) · 107 KB
/
Copy pathmain.py
File metadata and controls
2347 lines (2054 loc) · 107 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Qoder Automation - FULL AUTOMATION WITH UI INTERACTION
Auto-claim Pro Trial + 300 Credits
Version: 5.0.0
Mendukung: macOS, Windows, Linux
Dengan otomatisasi klik tombol Sign In
Menggunakan Playwright dengan stealth
"""
import asyncio
import json
import re
import time
import random
import os
import subprocess
import sys
import shutil
import platform
import errno
from typing import Optional, Dict, List, Any
from datetime import datetime, timezone
from pathlib import Path
# ================= ANSI COLORS =================
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
CYAN = '\033[96m'
WHITE = '\033[97m'
RESET = '\033[0m'
BOLD = '\033[1m'
MAGENTA = '\033[95m'
def print_color(text, color=Colors.RESET):
print(f"{color}{text}{Colors.RESET}")
def banner():
print(f"""
{Colors.CYAN}╔══════════════════════════════════════════════════════════╗
║ Qoder Automation - FULL AUTOMATION ║
║ Auto-claim Pro Trial + 300 Credits ║
║ Version: 5.0.0 ║
║ Support: macOS | Windows | Linux ║
║ With Stealth + Playwright ║
╚══════════════════════════════════════════════════════════════╝{Colors.RESET}
""")
# ================= PLATFORM DETECTION =================
SYSTEM = platform.system()
IS_MAC = SYSTEM == "Darwin"
IS_WINDOWS = SYSTEM == "Windows"
IS_LINUX = SYSTEM == "Linux"
# ================= KONFIGURASI PER PLATFORM =================
def get_platform_config(platform_type: str = None):
"""
Mendapatkan konfigurasi untuk platform tertentu
"""
if platform_type is None:
platform_type = SYSTEM
configs = {
"Darwin": {
"name": "macOS",
"app_path": "/Applications/Qoder.app",
"binary_name": "Electron",
"data_dir": Path.home() / "Library" / "Application Support" / "Qoder",
"user_dir": Path.home() / ".qoder",
"cache_dir": Path.home() / "Library" / "Caches" / "Qoder",
"preferences": Path.home() / "Library" / "Preferences" / "com.qoder.Qoder.plist",
"saved_state": Path.home() / "Library" / "Saved Application State" / "com.qoder.Qoder.savedState",
"launch_cmd": "open",
"launch_args": ["-a", "/Applications/Qoder.app"]
},
"Windows": {
"name": "Windows",
"app_path_system": "C:/Program Files/Qoder/Qoder.exe",
"app_path_user": os.path.expandvars("%LOCALAPPDATA%/Qoder/Qoder.exe"),
"binary_name": "Qoder.exe",
"data_dir": Path(os.path.expandvars("%APPDATA%/Qoder")),
"user_dir": Path.home() / ".qoder",
"cache_dir": Path(os.path.expandvars("%LOCALAPPDATA%/Qoder/Cache")),
"preferences": Path(os.path.expandvars("%APPDATA%/Qoder/Preferences")),
"saved_state": None,
"launch_cmd": "start",
"launch_args": ["", ""]
},
"Linux": {
"name": "Linux",
"app_path": "/usr/bin/qoder",
"app_path_local": "/usr/local/bin/qoder",
"binary_name": "qoder",
"data_dir": Path.home() / ".config" / "Qoder",
"user_dir": Path.home() / ".qoder",
"cache_dir": Path.home() / ".cache" / "Qoder",
"preferences": Path.home() / ".config" / "Qoder" / "preferences",
"saved_state": None,
"launch_cmd": "qoder",
"launch_args": []
}
}
return configs.get(platform_type, configs.get("Darwin"))
# ================= FILE PATHS =================
QODER_APP_PATH = None
QODER_DATA_DIR = None
QODER_USER_DIR = None
QODER_CACHE_DIR = None
QODER_PREFERENCES = None
QODER_SAVED_STATE = None
QODER_BINARY = None
LAUNCH_CMD = None
LAUNCH_ARGS = []
PLATFORM_NAME = ""
SELECTED_PLATFORM = SYSTEM
def init_platform(platform_type: str):
"""Inisialisasi path berdasarkan platform yang dipilih"""
global QODER_APP_PATH, QODER_DATA_DIR, QODER_USER_DIR, QODER_BINARY
global LAUNCH_CMD, LAUNCH_ARGS, PLATFORM_NAME, QODER_CACHE_DIR
global QODER_PREFERENCES, QODER_SAVED_STATE
config = get_platform_config(platform_type)
PLATFORM_NAME = config.get("name", "Unknown")
if platform_type == "Darwin":
QODER_APP_PATH = config.get("app_path")
QODER_BINARY = Path(QODER_APP_PATH) / "Contents" / "MacOS" / config.get("binary_name")
QODER_DATA_DIR = config.get("data_dir")
QODER_USER_DIR = config.get("user_dir")
QODER_CACHE_DIR = config.get("cache_dir")
QODER_PREFERENCES = config.get("preferences")
QODER_SAVED_STATE = config.get("saved_state")
LAUNCH_CMD = config.get("launch_cmd")
LAUNCH_ARGS = config.get("launch_args", [])
elif platform_type == "Windows":
system_path = config.get("app_path_system")
user_path = config.get("app_path_user")
if os.path.exists(system_path):
QODER_APP_PATH = system_path
elif os.path.exists(user_path):
QODER_APP_PATH = user_path
else:
QODER_APP_PATH = None
QODER_BINARY = QODER_APP_PATH if QODER_APP_PATH else None
QODER_DATA_DIR = config.get("data_dir")
QODER_USER_DIR = config.get("user_dir")
QODER_CACHE_DIR = config.get("cache_dir")
QODER_PREFERENCES = config.get("preferences")
QODER_SAVED_STATE = config.get("saved_state")
LAUNCH_CMD = config.get("launch_cmd")
LAUNCH_ARGS = config.get("launch_args", [])
elif platform_type == "Linux":
if os.path.exists(config.get("app_path")):
QODER_APP_PATH = config.get("app_path")
elif os.path.exists(config.get("app_path_local")):
QODER_APP_PATH = config.get("app_path_local")
else:
QODER_APP_PATH = None
QODER_BINARY = QODER_APP_PATH
QODER_DATA_DIR = config.get("data_dir")
QODER_USER_DIR = config.get("user_dir")
QODER_CACHE_DIR = config.get("cache_dir")
QODER_PREFERENCES = config.get("preferences")
QODER_SAVED_STATE = config.get("saved_state")
LAUNCH_CMD = config.get("launch_cmd")
LAUNCH_ARGS = config.get("launch_args", [])
print_color(f" [*] Platform: {PLATFORM_NAME}", Colors.CYAN)
if QODER_BINARY:
print_color(f" [*] Binary path: {QODER_BINARY}", Colors.CYAN)
else:
print_color(f" [!] Qoder binary not found!", Colors.RED)
# ================= FILE PATHS =================
SUCCESS_FILE = "qoder_sukses.txt"
AKUN_FILE = "qoder_akun.txt"
API_KEY_FILE = "qoder_api_keys.txt"
FAILED_FILE = "qoder_failed.txt"
LOG_FILE = "qoder_log.txt"
# ================= UTILITY FUNCTIONS =================
def setup_logging():
"""Setup logging configuration"""
if not os.path.exists(LOG_FILE):
with open(LOG_FILE, 'w') as f:
f.write(f"# Qoder Automation Log - {datetime.now(timezone.utc).isoformat()}\n")
def write_log(message: str, level: str = "INFO"):
"""Write log message to file"""
timestamp = datetime.now(timezone.utc).isoformat()
with open(LOG_FILE, 'a') as f:
f.write(f"[{timestamp}] [{level}] {message}\n")
def load_accounts():
"""Load accounts from file"""
try:
with open(AKUN_FILE) as f:
return [{
"email": l.split("|")[0].strip(),
"password": l.split("|")[1].strip()
} for l in f if "|" in l and len(l.split("|")) >= 2]
except FileNotFoundError:
write_log(f"File {AKUN_FILE} tidak ditemukan", "ERROR")
return []
def save_success(email, data):
"""Save successful login"""
with open(SUCCESS_FILE, "a") as f:
f.write(f"{email}|{json.dumps(data)}|{datetime.now(timezone.utc).isoformat()}\n")
write_log(f"Success: {email} - Credits: {data.get('credits', 0)}", "SUCCESS")
def save_failed(email, error_msg):
"""Save failed login attempt"""
with open(FAILED_FILE, "a") as f:
f.write(f"{email}|{error_msg}|{datetime.now(timezone.utc).isoformat()}\n")
write_log(f"Failed: {email} - {error_msg}", "ERROR")
def remove_account(email):
"""Remove account from list after successful processing"""
accs = load_accounts()
with open(AKUN_FILE, "w") as f:
for a in accs:
if a["email"] != email:
f.write(f"{a['email']}|{a['password']}\n")
def load_processed_emails():
"""Load emails that have been processed successfully"""
try:
with open(SUCCESS_FILE, 'r') as f:
return [line.split('|')[0] for line in f if '|' in line]
except FileNotFoundError:
return []
# ================= PLAYWRIGHT HELPER =================
def ensure_playwright_browsers():
"""Pastikan browser Playwright dan Google Chrome terinstall"""
try:
import playwright
from playwright._impl._api_structures import BrowserType
# Cek apakah Google Chrome terinstall (PRIORITAS)
chrome_paths = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'C:/Program Files/Google/Chrome/Application/chrome.exe',
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
]
chrome_installed = False
for cp in chrome_paths:
if os.path.exists(cp):
chrome_installed = True
print_color(f" ✅ Google Chrome found at: {cp}", Colors.GREEN)
break
if not chrome_installed:
print_color(" ⚠️ Google Chrome TIDAK ditemukan!", Colors.YELLOW)
print_color(" ℹ️ Google Chrome diperlukan untuk login Google yang aman", Colors.YELLOW)
print_color(" ℹ️ Download: https://www.google.com/chrome/", Colors.CYAN)
# Cek apakah chromium sudah terinstall (sebagai fallback)
cache_dir = Path.home() / "Library" / "Caches" / "ms-playwright"
chromium_installed = False
if cache_dir.exists():
for item in cache_dir.iterdir():
if "chromium" in str(item).lower() and item.is_dir():
chromium_installed = True
break
if not chromium_installed:
print_color(" [*] Playwright Chromium belum terinstall. Menginstall sebagai fallback...", Colors.YELLOW)
print_color(" [*] Ini mungkin memakan waktu beberapa menit...", Colors.YELLOW)
# Install chromium dengan output
process = subprocess.Popen(
[sys.executable, "-m", "playwright", "install", "chromium"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
# Tampilkan progress
for line in process.stdout:
print(f" {line.strip()}")
process.wait()
if process.returncode == 0:
print_color(" ✅ Playwright Chromium berhasil diinstall!", Colors.GREEN)
write_log("Playwright Chromium installed", "INFO")
else:
print_color(f" ❌ Gagal install Chromium. Silakan install manual:", Colors.RED)
print_color(f" {sys.executable} -m playwright install chromium", Colors.YELLOW)
write_log(f"Failed to install Chromium", "ERROR")
else:
print_color(" ✅ Playwright Chromium sudah terinstall (fallback)", Colors.GREEN)
return chrome_installed or chromium_installed
except ImportError:
print_color(" [!] Playwright tidak terinstall. Menginstall...", Colors.YELLOW)
process = subprocess.Popen(
[sys.executable, "-m", "pip", "install", "playwright"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
for line in process.stdout:
print(f" {line.strip()}")
process.wait()
if process.returncode == 0:
print_color(" ✅ Playwright berhasil diinstall!", Colors.GREEN)
# Install browser
print_color(" [*] Menginstall browser Chromium...", Colors.YELLOW)
process = subprocess.Popen(
[sys.executable, "-m", "playwright", "install", "chromium"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
for line in process.stdout:
print(f" {line.strip()}")
process.wait()
if process.returncode == 0:
print_color(" ✅ Browser berhasil diinstall!", Colors.GREEN)
return True
else:
print_color(f" ❌ Gagal install browser.", Colors.RED)
return False
else:
print_color(f" ❌ Gagal install playwright.", Colors.RED)
return False
# ================= QODER PATCHER =================
class QoderPatcher:
"""Handle Qoder app patching for multiple platforms"""
def __init__(self, platform_type: str = None):
if platform_type is None:
platform_type = SYSTEM
self.platform = platform_type
self.config = get_platform_config(platform_type)
self.mac_address = None
self.machine_id = None
self.ms_deviceid = None
self.umid = None
self.is_patched = False
def generate_fake_mac(self):
"""Generate fake MAC address (for macOS)"""
mac = ':'.join(['{:02x}'.format(random.randint(0x00, 0xff)) for _ in range(6)])
self.mac_address = mac
write_log(f"Generated fake MAC: {mac}", "INFO")
return mac
def generate_machine_id(self):
"""Generate fake machine ID"""
import hashlib
machine_id = hashlib.md5(str(random.randint(1000000, 9999999)).encode()).hexdigest()[:32]
self.machine_id = machine_id
write_log(f"Generated machine ID: {machine_id}", "INFO")
return machine_id
def generate_ms_deviceid(self):
"""Generate fake Microsoft device ID"""
import uuid
device_id = str(uuid.uuid4())
self.ms_deviceid = device_id
write_log(f"Generated MS device ID: {device_id}", "INFO")
return device_id
def generate_umid(self):
"""Generate fake UMID"""
import uuid
umid = str(uuid.uuid4())
self.umid = umid
write_log(f"Generated UMID: {umid}", "INFO")
return umid
def generate_pkce(self):
"""Generate PKCE code_verifier and code_challenge (S256) for device auth"""
import hashlib
import base64
# Generate code_verifier: 32 random bytes, base64url encoded
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('ascii')
# Generate code_challenge: SHA256 of code_verifier, base64url encoded
digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
self.code_verifier = code_verifier
self.code_challenge = code_challenge
write_log(f"Generated PKCE challenge (S256)", "INFO")
return code_verifier, code_challenge
def generate_nonce(self):
"""Generate random nonce for OAuth device auth"""
import uuid
nonce = uuid.uuid4().hex
self.nonce = nonce
write_log(f"Generated nonce: {nonce[:8]}...", "INFO")
return nonce
def patch_qoder_data(self):
"""Patch Qoder data directory with fake identifiers"""
global QODER_DATA_DIR, QODER_USER_DIR
print_color(" [*] Patching Qoder data...", Colors.YELLOW)
write_log("Starting Qoder data patch", "INFO")
try:
# Generate fake identifiers
if self.platform == "Darwin":
self.generate_fake_mac()
self.generate_machine_id()
self.generate_ms_deviceid()
self.generate_umid()
else:
self.generate_machine_id()
self.generate_ms_deviceid()
# Clear and recreate state files
if QODER_DATA_DIR and QODER_DATA_DIR.exists():
# Backup existing data (skip socket files)
backup_dir = QODER_DATA_DIR.parent / "Qoder_backup"
if not backup_dir.exists():
# Copy only regular files, skip sockets
for item in QODER_DATA_DIR.rglob('*'):
if item.is_file() and not item.is_socket():
try:
relative_path = item.relative_to(QODER_DATA_DIR)
backup_path = backup_dir / relative_path
backup_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, backup_path)
except Exception as e:
write_log(f"Skip backup for {item}: {e}", "WARNING")
write_log(f"Backup created at {backup_dir}", "INFO")
print(f" [*] Backup created (skipped socket files)")
# Clear state files
state_files = [
QODER_DATA_DIR / "state.vscdb",
QODER_DATA_DIR / "machineid",
QODER_DATA_DIR / "ms_deviceid",
QODER_DATA_DIR / "serviceMachineId"
]
for state_file in state_files:
if state_file.exists() and state_file.is_file():
state_file.unlink()
print(f" [*] Cleared {state_file.name}")
# Create Qoder data directory if it doesn't exist
if QODER_DATA_DIR:
QODER_DATA_DIR.mkdir(parents=True, exist_ok=True)
# Create new state files with fake data
if QODER_DATA_DIR:
with open(QODER_DATA_DIR / "machineid", 'w') as f:
f.write(self.machine_id)
with open(QODER_DATA_DIR / "ms_deviceid", 'w') as f:
f.write(self.ms_deviceid)
with open(QODER_DATA_DIR / "serviceMachineId", 'w') as f:
f.write(self.machine_id)
# Create/update user data
if QODER_USER_DIR:
QODER_USER_DIR.mkdir(parents=True, exist_ok=True)
# Save patch info
patch_info = {
"platform": self.platform,
"machine_id": self.machine_id,
"ms_deviceid": self.ms_deviceid,
"patched_at": datetime.now(timezone.utc).isoformat()
}
if self.platform == "Darwin":
patch_info["mac_address"] = self.mac_address
patch_info["umid"] = self.umid
if QODER_DATA_DIR:
with open(QODER_DATA_DIR / "patch_info.json", 'w') as f:
json.dump(patch_info, f, indent=2)
self.is_patched = True
write_log("Patch completed successfully", "SUCCESS")
print_color(" ✅ Patch + Reset Complete!", Colors.GREEN)
print_color(f" Platform: {self.platform}", Colors.CYAN)
print_color(f" machineId: {self.machine_id}", Colors.CYAN)
print_color(f" ms_deviceid: {self.ms_deviceid}", Colors.CYAN)
if self.platform == "Darwin":
print_color(f" Fake MAC: {self.mac_address}", Colors.CYAN)
print_color(f" UMID: {self.umid}", Colors.CYAN)
return True
except Exception as e:
write_log(f"Patch failed: {e}", "ERROR")
print_color(f" [!] Patch failed: {e}", Colors.RED)
import traceback
traceback.print_exc()
return False
# ================= QODER CLIENT AUTOMATION =================
class QoderClientAutomation:
"""Automate Qoder Client for multiple platforms with UI interaction"""
def __init__(self, headless: bool = False, platform_type: str = None, timeout: int = 120000):
self.headless = headless
self.timeout = timeout
self.process = None
self.email = None
self.password = None
self.credits = 0
self.platform = platform_type or SYSTEM
self.patcher = QoderPatcher(self.platform)
self.binary_path = self.get_qoder_binary_path()
write_log(f"Initialized QoderClientAutomation (platform={self.platform}, headless={headless})", "INFO")
def check_qoder_installed(self) -> bool:
"""Check if Qoder Client is installed"""
global QODER_BINARY, QODER_APP_PATH
if self.platform == "Darwin":
if os.path.exists(QODER_APP_PATH):
print_color(f" ✅ Qoder Client found at {QODER_APP_PATH}", Colors.GREEN)
write_log(f"Qoder Client found at {QODER_APP_PATH}", "INFO")
return True
else:
print_color(f" ❌ Qoder Client not found at {QODER_APP_PATH}", Colors.RED)
print_color(" Please download from https://qoder.com/download", Colors.YELLOW)
return False
elif self.platform == "Windows":
if QODER_BINARY and os.path.exists(QODER_BINARY):
print_color(f" ✅ Qoder Client found at {QODER_BINARY}", Colors.GREEN)
write_log(f"Qoder Client found at {QODER_BINARY}", "INFO")
return True
else:
print_color(" ❌ Qoder Client not found", Colors.RED)
print_color(" Please download from https://qoder.com/download", Colors.YELLOW)
return False
elif self.platform == "Linux":
if QODER_BINARY and os.path.exists(QODER_BINARY):
print_color(f" ✅ Qoder Client found at {QODER_BINARY}", Colors.GREEN)
write_log(f"Qoder Client found at {QODER_BINARY}", "INFO")
return True
else:
print_color(" ❌ Qoder Client not found", Colors.RED)
print_color(" Please download from https://qoder.com/download", Colors.YELLOW)
return False
return False
def get_qoder_version(self) -> Optional[str]:
"""Get Qoder Client version"""
if self.platform == "Darwin":
try:
cmd = ["defaults", "read", f"{QODER_APP_PATH}/Contents/Info.plist", "CFBundleShortVersionString"]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
version = result.stdout.strip()
print_color(f" [*] Qoder Version: {version}", Colors.CYAN)
write_log(f"Qoder version: {version}", "INFO")
return version
except Exception as e:
write_log(f"Error checking version: {e}", "WARNING")
elif self.platform == "Windows" or self.platform == "Linux":
if QODER_BINARY and os.path.exists(QODER_BINARY):
try:
result = subprocess.run([QODER_BINARY, "--version"], capture_output=True, text=True)
if result.returncode == 0:
version = result.stdout.strip()
print_color(f" [*] Qoder Version: {version}", Colors.CYAN)
write_log(f"Qoder version: {version}", "INFO")
return version
except:
pass
return None
def get_qoder_binary_path(self) -> Optional[str]:
"""Get Qoder binary path"""
global QODER_BINARY
return QODER_BINARY
def get_qoder_pid(self) -> Optional[int]:
"""Get Qoder process ID"""
try:
if self.platform == "Darwin":
result = subprocess.run(["pgrep", "-f", "Qoder"], capture_output=True, text=True)
if result.returncode == 0 and result.stdout:
return int(result.stdout.strip().split('\n')[0])
elif self.platform == "Windows":
result = subprocess.run(["tasklist", "/FI", "IMAGENAME eq Qoder.exe"], capture_output=True, text=True)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if "Qoder.exe" in line:
parts = line.split()
if len(parts) >= 2:
return int(parts[1])
elif self.platform == "Linux":
result = subprocess.run(["pgrep", "-f", "qoder"], capture_output=True, text=True)
if result.returncode == 0 and result.stdout:
return int(result.stdout.strip().split('\n')[0])
except Exception:
pass
return None
def kill_qoder_process(self):
"""Kill Qoder process"""
try:
if self.platform == "Darwin":
subprocess.run(["pkill", "-f", "Qoder"], capture_output=True)
elif self.platform == "Windows":
subprocess.run(["taskkill", "/F", "/IM", "Qoder.exe"], capture_output=True)
elif self.platform == "Linux":
subprocess.run(["pkill", "-f", "qoder"], capture_output=True)
write_log("Killed Qoder process", "INFO")
print(" [*] Killed existing Qoder process")
time.sleep(2)
except Exception as e:
write_log(f"Error killing Qoder: {e}", "WARNING")
def launch_qoder(self) -> bool:
"""Launch Qoder Client"""
if not self.check_qoder_installed():
return False
# Kill existing process
self.kill_qoder_process()
# Apply patch before launch
self.patcher.patch_qoder_data()
# Detect actual binary
binary_path = self.binary_path
if not binary_path:
print_color(" ❌ Could not find Qoder binary", Colors.RED)
write_log("Qoder binary not found", "ERROR")
return False
if not os.path.exists(binary_path):
print_color(f" ❌ Binary not found at: {binary_path}", Colors.RED)
write_log(f"Binary not found: {binary_path}", "ERROR")
return False
try:
print_color(f" [*] Launching Qoder Client from: {binary_path}", Colors.YELLOW)
write_log(f"Launching Qoder Client: {binary_path}", "INFO")
# Launch Qoder with appropriate command
if self.platform == "Darwin":
cmd = [
binary_path,
"--disable-extensions",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage"
]
elif self.platform == "Windows":
cmd = [binary_path]
else: # Linux
cmd = [
binary_path,
"--disable-extensions",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage"
]
self.process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
time.sleep(8) # Wait for app to launch
# Check if process is running
if self.process.poll() is None:
print_color(" ✅ Qoder Client launched successfully!", Colors.GREEN)
write_log("Qoder Client launched", "SUCCESS")
return True
else:
print_color(" ❌ Qoder Client failed to launch", Colors.RED)
return False
except Exception as e:
write_log(f"Error launching Qoder: {e}", "ERROR")
print_color(f" [!] Error launching Qoder: {e}", Colors.RED)
import traceback
traceback.print_exc()
return False
# ================= UI AUTOMATION METHODS =================
async def click_signin_with_accessibility(self) -> bool:
"""Klik Sign In menggunakan macOS Accessibility API"""
if not IS_MAC:
print_color(" [!] Accessibility API only available on macOS", Colors.YELLOW)
return False
applescript = '''
tell application "System Events"
tell process "Qoder"
set frontmost to true
delay 2
-- Cari tombol Sign In
set signInButton to null
set allButtons to every button of window 1
repeat with btn in allButtons
set btnTitle to title of btn
if btnTitle contains "Sign" or btnTitle contains "sign" then
set signInButton to btn
exit repeat
end if
end repeat
if signInButton is not null then
click signInButton
return "found_by_title"
end if
-- Fallback: cari berdasarkan deskripsi
set allUIElements to every UI element of window 1
repeat with elem in allUIElements
set elemDescription to description of elem
if elemDescription contains "Sign" or elemDescription contains "sign" then
click elem
return "found_by_description"
end if
end repeat
-- Fallback: cari berdasarkan posisi (pojok kanan atas)
tell window 1
set windowSize to size
set windowWidth to item 1 of windowSize
set windowHeight to item 2 of windowSize
set signInX to windowWidth - 80
set signInY to 25
click at {signInX, signInY}
return "clicked_at_position"
end tell
end tell
end tell
'''
try:
result = subprocess.run(["osascript", "-e", applescript], capture_output=True, text=True, timeout=15)
output = result.stdout.strip()
if "found" in output or "clicked" in output:
print_color(f" ✅ Sign In clicked! ({output})", Colors.GREEN)
write_log(f"Sign In clicked via Accessibility: {output}", "INFO")
return True
else:
print_color(f" ⚠️ Sign In not found: {output}", Colors.YELLOW)
return False
except subprocess.TimeoutExpired:
print_color(" ⚠️ Accessibility timeout", Colors.YELLOW)
return False
except Exception as e:
write_log(f"Accessibility error: {e}", "ERROR")
print_color(f" [!] Accessibility error: {e}", Colors.RED)
return False
async def click_signin_with_coordinates(self) -> bool:
"""Klik Sign In menggunakan koordinat presisi di macOS"""
if not IS_MAC:
return False
applescript = '''
tell application "System Events"
tell process "Qoder"
set frontmost to true
delay 1
tell window 1
-- Dapatkan posisi window
set windowPosition to position
set windowSize to size
set windowX to item 1 of windowPosition
set windowY to item 2 of windowPosition
set windowWidth to item 1 of windowSize
set windowHeight to item 2 of windowSize
-- Koordinat tombol Sign In (pojok kanan atas)
-- Biasanya di sekitar (windowWidth - 80, 25)
set signInX to windowX + windowWidth - 80
set signInY to windowY + 25
click at {signInX, signInY}
return "clicked_at_position"
end tell
end tell
end tell
'''
try:
result = subprocess.run(["osascript", "-e", applescript], capture_output=True, text=True, timeout=10)
if "clicked" in result.stdout:
print_color(" ✅ Sign In clicked using coordinates!", Colors.GREEN)
return True
else:
print_color(" ⚠️ Could not click using coordinates", Colors.YELLOW)
return False
except Exception as e:
write_log(f"Coordinate click error: {e}", "ERROR")
return False
async def click_signin_pyautogui(self) -> bool:
"""Klik Sign In menggunakan pyautogui dengan image recognition"""
try:
import pyautogui
pyautogui.FAILSAFE = True
# Cari Qoder window
try:
qoder_windows = pyautogui.getWindowsWithTitle("Qoder")
if qoder_windows:
window = qoder_windows[0]
window.activate()
time.sleep(1)
# Cari tombol Sign In dengan image recognition
try:
sign_in_location = pyautogui.locateOnScreen('sign_in_button.png', confidence=0.7)
if sign_in_location:
center_x = sign_in_location.left + sign_in_location.width // 2
center_y = sign_in_location.top + sign_in_location.height // 2
pyautogui.click(center_x, center_y)
print_color(" ✅ Sign In clicked using image recognition!", Colors.GREEN)
return True
except:
pass
# Fallback: klik berdasarkan posisi relatif window
window_x, window_y = window.topleft
window_width, window_height = window.size
click_x = window_x + window_width - 80
click_y = window_y + 25
pyautogui.click(click_x, click_y)
print_color(" ✅ Sign In clicked using relative position!", Colors.GREEN)
return True
else:
print_color(" ⚠️ Qoder window not found", Colors.YELLOW)
return False
except:
# Jika getWindowsWithTitle tidak tersedia
pyautogui.click(1500, 30) # Default position untuk Mac
print_color(" ✅ Sign In clicked using default position!", Colors.GREEN)
return True
except ImportError:
print_color(" ⚠️ pyautogui not installed. Install with: pip install pyautogui pillow", Colors.YELLOW)
return False
except Exception as e:
write_log(f"pyautogui error: {e}", "ERROR")
return False
async def click_signin_button(self) -> bool:
"""Klik Sign In button dengan multiple methods"""
print_color(" [*] Clicking Sign In button...", Colors.CYAN)
# Try methods in order of reliability
methods = [
("Accessibility API", self.click_signin_with_accessibility),
("Coordinates", self.click_signin_with_coordinates),
]
# Add pyautogui if installed
try:
import pyautogui
methods.append(("PyAutoGUI", self.click_signin_pyautogui))
except ImportError:
pass
for method_name, method in methods:
print(f" [*] Trying: {method_name}...")
success = await method()
if success:
write_log(f"Sign In clicked using {method_name}", "INFO")
await asyncio.sleep(2)
return True
print_color(" ⚠️ All automation methods failed", Colors.YELLOW)
print_color(" ℹ️ Please click 'Sign In' manually in Qoder Desktop", Colors.CYAN)
return False
# ================= DEVICE AUTH URL BUILDER =================
def build_device_auth_url(self) -> str:
"""
Build the Qoder device authorization URL with PKCE parameters.
Qoder Desktop uses a PKCE-based device authorization flow:
https://qoder.com/device/selectAccounts?nonce=...&challenge=...&challenge_method=S256&redirect_uri=qoder://aicoding.aicoding-agent/login-success&machine_id=...
This constructs the URL ourselves instead of trying to capture it
from the system browser (which is unreliable).
"""
# Generate PKCE values
code_verifier, code_challenge = self.patcher.generate_pkce()
nonce = self.patcher.generate_nonce()
# Get the machine_id that was written to Qoder data
machine_id = self.patcher.machine_id
if not machine_id:
self.patcher.generate_machine_id()
machine_id = self.patcher.machine_id
# Build the device auth URL
params = {
'nonce': nonce,
'challenge': code_challenge,
'challenge_method': 'S256',
'redirect_uri': 'qoder://aicoding.aicoding-agent/login-success',
'machine_id': machine_id
}
query = '&'.join(f'{k}={v}' for k, v in params.items())
url = f'https://qoder.com/device/selectAccounts?{query}'
write_log(f"Built device auth URL (machine_id={machine_id[:8]}...)", "INFO")
print_color(f" [*] Generated device auth URL with machine_id={machine_id[:16]}...", Colors.CYAN)
return url
def scan_chrome_for_qoder_url(self) -> Optional[str]:
"""
Scan ALL Chrome tabs (not just the active one) for the Qoder device auth URL.
This is more reliable than checking only the active tab of the front window,
since Qoder Desktop opens the URL in a new tab that may not be focused.
"""
if self.platform != "Darwin":
return None
applescript = '''
tell application "Google Chrome"
if it is running then
set qoderURL to ""
repeat with w in windows
repeat with t in tabs of w
try
set tabURL to URL of t
if tabURL contains "qoder.com/device/selectAccounts" then
set qoderURL to tabURL
exit repeat
end if
end try
end repeat
if qoderURL is not "" then exit repeat
end repeat
return qoderURL
end if
end tell
'''
try:
result = subprocess.run(
["osascript", "-e", applescript],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0 and result.stdout.strip():
url = result.stdout.strip()
write_log(f"Scanned Chrome tabs and found Qoder URL", "INFO")
display_url = url[:120] + "..." if len(url) > 120 else url