diff --git a/facebook_marketplace.db b/facebook_marketplace.db index b296e94..2863f8d 100644 Binary files a/facebook_marketplace.db and b/facebook_marketplace.db differ diff --git a/gui/handle/login_fb.py b/gui/handle/login_fb.py index 3686ac7..cf2e40e 100644 --- a/gui/handle/login_fb.py +++ b/gui/handle/login_fb.py @@ -1,43 +1,39 @@ -# gui/handle/login_fb.py import os import sys import cv2 import numpy as np from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QTextEdit, QPushButton -from PyQt5.QtWebEngineWidgets import QWebEngineView -from PyQt5.QtGui import QImage, QPixmap +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile +from PyQt5.QtGui import QImage from services.action_service import ActionService from services.detect_service import DetectService +from services.profile_service import ProfileService from config import TEMPLATE_DIR class LoginFB(QMainWindow): - def __init__(self, account=None, delay=0.1): + def __init__(self, account=None, delay=0.3): super().__init__() self.account = account or {} self.template_dir = os.path.abspath(TEMPLATE_DIR) self.delay = delay + # ✅ Lấy tên profile từ email hoặc username + self.profile_name = self.account.get("email") or self.account.get("username") or "default" + # --- Detect services --- self.detector = DetectService( template_dir=TEMPLATE_DIR, - target_labels=["username", "password", "buttons/login"] + target_labels=["username", "password"] ) - # --- Detect login fail templates --- - self.fail_detector = DetectService( - template_dir=os.path.join(TEMPLATE_DIR, "login_fail") - ) - - # --- UI --- - self.setWindowTitle("FB Auto Vision Login") + # --- UI cơ bản --- + self.setWindowTitle(f"FB Auto Vision Login - {self.profile_name}") self.setFixedSize(480, 680) self.web = QWebEngineView() - self.web.setUrl(QUrl("https://facebook.com")) - self.web.setFixedSize(480, 480) self.status = QLabel("Status: Ready") self.status.setAlignment(Qt.AlignLeft) @@ -57,6 +53,19 @@ class LoginFB(QMainWindow): self.btn_refresh.setFixedHeight(30) self.btn_refresh.clicked.connect(self.refresh_page) + # --- Profile --- + self.profile_service = ProfileService() + profile_path = self.profile_service.get_profile_path(self.profile_name) + profile = self.web.page().profile() + profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) + profile.setPersistentStoragePath(profile_path) + self.log(f"[INFO] Profile applied at: {profile_path}") + + # --- Webview --- + self.web.setUrl(QUrl("https://facebook.com")) + self.web.setFixedSize(480, 480) + + # --- Layout --- layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.web) @@ -68,12 +77,8 @@ class LoginFB(QMainWindow): container.setLayout(layout) self.setCentralWidget(container) + # --- Services --- self.action = ActionService(webview=self.web, delay=self.delay) - self.login_clicked = False - - # Giữ reference popup (nếu cần) – hiện tại không dùng - # self.login_fail_popup = None - self.web.loadFinished.connect(self.on_web_loaded) # ---------------------------------------------------- @@ -95,38 +100,26 @@ class LoginFB(QMainWindow): # ---------------------------------------------------- def on_web_loaded(self, ok=True): - """Called every time page load finished""" if not ok: self.log("[ERROR] Page failed to load") self.status.setText("Status: Page load failed") return + self.log("[INFO] Page loaded") + self.status.setText("Status: Page loaded") + + # ✅ Lưu profile khi load xong + self.profile_service.save_profile(self.profile_name) + self.log(f"[INFO] Profile saved for {self.profile_name}") + + # 🧠 Detect field screen = self.capture_webview() if screen is None: self.status.setText("Status: Unable to capture webview") self.log("Status: Unable to capture webview") return - # Nếu đang sau nhấn login, check login failure - if self.login_clicked: - self.log("[INFO] Page loaded after login click. Detecting login failure...") - fail_regions = self.fail_detector.detect(screen) - - if fail_regions: - self.log(f"[FAIL] Login failed detected via template ({len(fail_regions)} regions):") - for folder_name, filename, top_left, bottom_right, score in fail_regions: - self.log(f" - {filename} @ {top_left}-{bottom_right} (score: {score:.2f})") - - self.status.setText("Status: Login failed") - else: - self.log("[SUCCESS] Login seems successful!") - self.status.setText("Status: Login successful") - - self.login_clicked = False - return - - # Nếu là initial page load - self.log("[INFO] Page loaded. Starting template detection...") + self.log("[INFO] Detecting email/password fields...") regions = self.detector.detect(screen) if not regions: @@ -143,10 +136,17 @@ class LoginFB(QMainWindow): email = self.account.get("email", "") password = self.account.get("password", "") - for folder_name, filename, top_left, bottom_right, score in regions: - self.log(f"[ACTION] {folder_name}: {filename} ({score:.2f})") + # sắp xếp để điền username trước, password sau + ordered = sorted(regions, key=lambda r: ("pass" in r[0].lower(), "user" not in r[0].lower())) + + def do_action(i=0): + if i >= len(ordered): + return + folder_name, filename, top_left, bottom_right, score = ordered[i] label = folder_name.lower() + self.log(f"[ACTION] {folder_name}: {filename} ({score:.2f})") + if ("user" in label or "email" in label) and email: self.log(f"[DO] Filling email: {email}") self.action.write_in_region(top_left, bottom_right, email) @@ -155,10 +155,9 @@ class LoginFB(QMainWindow): self.log(f"[DO] Filling password: {'*' * len(password)}") self.action.write_in_region(top_left, bottom_right, password) - elif "button" in label or "login" in label: - self.log("[DO] Clicking login button...") - self.login_clicked = True - self.action.click_center_of_region(top_left, bottom_right) + QTimer.singleShot(int(self.delay * 1000), lambda: do_action(i + 1)) + + do_action() # ---------------------------------------------------- def refresh_page(self): @@ -169,6 +168,6 @@ class LoginFB(QMainWindow): if __name__ == "__main__": app = QApplication(sys.argv) fake_account = {"email": "test@example.com", "password": "123456"} - win = LoginFB(account=fake_account) + win = LoginFB(account=fake_account, delay=0.5) win.show() sys.exit(app.exec_()) diff --git a/profiles/a@gmail.com/Cookies b/profiles/a@gmail.com/Cookies new file mode 100644 index 0000000..6badfb8 Binary files /dev/null and b/profiles/a@gmail.com/Cookies differ diff --git a/profiles/a@gmail.com/Cookies-journal b/profiles/a@gmail.com/Cookies-journal new file mode 100644 index 0000000..e69de29 diff --git a/profiles/a@gmail.com/Network Persistent State b/profiles/a@gmail.com/Network Persistent State new file mode 100644 index 0000000..9300a8c --- /dev/null +++ b/profiles/a@gmail.com/Network Persistent State @@ -0,0 +1 @@ +{"net":{"network_qualities":{"CAASABiAgICA+P////8B":"4G"}}} \ No newline at end of file diff --git a/profiles/a@gmail.com/TransportSecurity b/profiles/a@gmail.com/TransportSecurity new file mode 100644 index 0000000..89371d2 --- /dev/null +++ b/profiles/a@gmail.com/TransportSecurity @@ -0,0 +1 @@ +{"expect_ct":[],"sts":[{"expiry":1776067079.455842,"host":"TZmujbl93Yt3JI8wZ4X/zjkA0WFNGNW44A+o7h4YyHw=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1760515079.455847},{"expiry":1776067078.271296,"host":"7QzmF0xxCtHTEKYxqWspZY5pl1F0B90+PraFnPulnH8=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1760515078.271298}],"version":2} \ No newline at end of file diff --git a/profiles/a@gmail.com/Visited Links b/profiles/a@gmail.com/Visited Links new file mode 100644 index 0000000..f42e0a7 Binary files /dev/null and b/profiles/a@gmail.com/Visited Links differ diff --git a/profiles/a@gmail.com/user_prefs.json b/profiles/a@gmail.com/user_prefs.json new file mode 100644 index 0000000..95e78e8 --- /dev/null +++ b/profiles/a@gmail.com/user_prefs.json @@ -0,0 +1 @@ +{"qtwebengine":{"media_device_salt_id":"8B4A2B48A507D8F41F8B993414E497B4"},"spellcheck":{"use_spelling_service":false}} \ No newline at end of file diff --git a/profiles/b@gmail.com/Cookies b/profiles/b@gmail.com/Cookies new file mode 100644 index 0000000..1130cfe Binary files /dev/null and b/profiles/b@gmail.com/Cookies differ diff --git a/profiles/b@gmail.com/Cookies-journal b/profiles/b@gmail.com/Cookies-journal new file mode 100644 index 0000000..e69de29 diff --git a/profiles/b@gmail.com/GPUCache/data_0 b/profiles/b@gmail.com/GPUCache/data_0 new file mode 100644 index 0000000..d76fb77 Binary files /dev/null and b/profiles/b@gmail.com/GPUCache/data_0 differ diff --git a/profiles/b@gmail.com/GPUCache/data_1 b/profiles/b@gmail.com/GPUCache/data_1 new file mode 100644 index 0000000..a5e11f3 Binary files /dev/null and b/profiles/b@gmail.com/GPUCache/data_1 differ diff --git a/profiles/b@gmail.com/GPUCache/data_2 b/profiles/b@gmail.com/GPUCache/data_2 new file mode 100644 index 0000000..c7e2eb9 Binary files /dev/null and b/profiles/b@gmail.com/GPUCache/data_2 differ diff --git a/profiles/b@gmail.com/GPUCache/data_3 b/profiles/b@gmail.com/GPUCache/data_3 new file mode 100644 index 0000000..5eec973 Binary files /dev/null and b/profiles/b@gmail.com/GPUCache/data_3 differ diff --git a/profiles/b@gmail.com/GPUCache/index b/profiles/b@gmail.com/GPUCache/index new file mode 100644 index 0000000..c845723 Binary files /dev/null and b/profiles/b@gmail.com/GPUCache/index differ diff --git a/profiles/b@gmail.com/Local Storage/leveldb/CURRENT b/profiles/b@gmail.com/Local Storage/leveldb/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/profiles/b@gmail.com/Local Storage/leveldb/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/profiles/b@gmail.com/Local Storage/leveldb/LOCK b/profiles/b@gmail.com/Local Storage/leveldb/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/profiles/b@gmail.com/Local Storage/leveldb/LOG b/profiles/b@gmail.com/Local Storage/leveldb/LOG new file mode 100644 index 0000000..0d25de8 --- /dev/null +++ b/profiles/b@gmail.com/Local Storage/leveldb/LOG @@ -0,0 +1 @@ +2025/10/15-14:57:28.745 1f603 Reusing MANIFEST /Users/admin/Workspace/python/facebook-tool/profiles/b@gmail.com/Local Storage/leveldb/MANIFEST-000001 diff --git a/profiles/b@gmail.com/Local Storage/leveldb/MANIFEST-000001 b/profiles/b@gmail.com/Local Storage/leveldb/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/profiles/b@gmail.com/Local Storage/leveldb/MANIFEST-000001 differ diff --git a/profiles/b@gmail.com/Network Persistent State b/profiles/b@gmail.com/Network Persistent State new file mode 100644 index 0000000..a5c537e --- /dev/null +++ b/profiles/b@gmail.com/Network Persistent State @@ -0,0 +1 @@ +{"net":{"http_server_properties":{"servers":[{"isolation":[],"server":"https://facebook.com","supports_spdy":true},{"isolation":[],"server":"https://www.facebook.com","supports_spdy":true}],"version":5},"network_qualities":{"CAASABiAgICA+P////8B":"4G"}}} \ No newline at end of file diff --git a/profiles/b@gmail.com/Platform Notifications/CURRENT b/profiles/b@gmail.com/Platform Notifications/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/profiles/b@gmail.com/Platform Notifications/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/profiles/b@gmail.com/Platform Notifications/LOCK b/profiles/b@gmail.com/Platform Notifications/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/profiles/b@gmail.com/Platform Notifications/LOG b/profiles/b@gmail.com/Platform Notifications/LOG new file mode 100644 index 0000000..2d56337 --- /dev/null +++ b/profiles/b@gmail.com/Platform Notifications/LOG @@ -0,0 +1 @@ +2025/10/15-14:57:28.022 12203 Reusing MANIFEST /Users/admin/Workspace/python/facebook-tool/profiles/b@gmail.com/Platform Notifications/MANIFEST-000001 diff --git a/profiles/b@gmail.com/Platform Notifications/MANIFEST-000001 b/profiles/b@gmail.com/Platform Notifications/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/profiles/b@gmail.com/Platform Notifications/MANIFEST-000001 differ diff --git a/profiles/b@gmail.com/Session Storage/CURRENT b/profiles/b@gmail.com/Session Storage/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/profiles/b@gmail.com/Session Storage/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/profiles/b@gmail.com/Session Storage/LOCK b/profiles/b@gmail.com/Session Storage/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/profiles/b@gmail.com/Session Storage/LOG b/profiles/b@gmail.com/Session Storage/LOG new file mode 100644 index 0000000..1ae6cd4 --- /dev/null +++ b/profiles/b@gmail.com/Session Storage/LOG @@ -0,0 +1 @@ +2025/10/15-14:57:28.968 12203 Reusing MANIFEST /Users/admin/Workspace/python/facebook-tool/profiles/b@gmail.com/Session Storage/MANIFEST-000001 diff --git a/profiles/b@gmail.com/Session Storage/MANIFEST-000001 b/profiles/b@gmail.com/Session Storage/MANIFEST-000001 new file mode 100644 index 0000000..18e5cab Binary files /dev/null and b/profiles/b@gmail.com/Session Storage/MANIFEST-000001 differ diff --git a/profiles/b@gmail.com/TransportSecurity b/profiles/b@gmail.com/TransportSecurity new file mode 100644 index 0000000..0b754cf --- /dev/null +++ b/profiles/b@gmail.com/TransportSecurity @@ -0,0 +1 @@ +{"expect_ct":[],"sts":[{"expiry":1776067070.409664,"host":"TZmujbl93Yt3JI8wZ4X/zjkA0WFNGNW44A+o7h4YyHw=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1760515070.40967},{"expiry":1776067049.100545,"host":"7QzmF0xxCtHTEKYxqWspZY5pl1F0B90+PraFnPulnH8=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1760515049.100546}],"version":2} \ No newline at end of file diff --git a/profiles/b@gmail.com/Visited Links b/profiles/b@gmail.com/Visited Links new file mode 100644 index 0000000..7ca27d1 Binary files /dev/null and b/profiles/b@gmail.com/Visited Links differ diff --git a/profiles/b@gmail.com/user_prefs.json b/profiles/b@gmail.com/user_prefs.json new file mode 100644 index 0000000..068b1c3 --- /dev/null +++ b/profiles/b@gmail.com/user_prefs.json @@ -0,0 +1 @@ +{"qtwebengine":{"media_device_salt_id":"B29F9C8B2F1F956169C8D8998DB9DAF6"},"spellcheck":{"dictionary":"","use_spelling_service":false}} \ No newline at end of file diff --git a/services/profile_service.py b/services/profile_service.py index 80a9913..a305da1 100644 --- a/services/profile_service.py +++ b/services/profile_service.py @@ -4,6 +4,7 @@ import re import shutil import logging from typing import Optional, List +from config import PROFILES_DIR # 👈 dùng từ config logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -32,21 +33,30 @@ def _sanitize_name(name: str) -> str: class ProfileService: """ Service để quản lý thư mục profiles. - Mặc định root folder là ./profiles (tương đối với working dir). + Mặc định root folder lấy từ config.PROFILES_DIR. """ + + base_dir = PROFILES_DIR def __init__(self, profiles_root: Optional[str] = None): - self.profiles_root = os.path.abspath(profiles_root or "profiles") + # 👇 Dùng PROFILES_DIR nếu không truyền thủ công + self.profiles_root = os.path.abspath(profiles_root or PROFILES_DIR) os.makedirs(self.profiles_root, exist_ok=True) logger.info("Profiles root: %s", self.profiles_root) def get_profile_dirname(self, name: str) -> str: """Tên folder đã sanitize (chỉ tên folder, không có path)""" return _sanitize_name(name) + + def save_profile(self, key: str): + # ở đây có thể không cần làm gì nhiều vì Qt tự lưu cookie + # nhưng bạn có thể log hoặc thêm custom logic + print(f"[ProfileService] Saved profile for {key}") - def get_profile_path(self, name: str) -> str: - """Trả về path tuyệt đối tới folder profile""" - return os.path.join(self.profiles_root, self.get_profile_dirname(name)) + def get_profile_path(self, key: str) -> str: + path = os.path.join(self.base_dir, key) + os.makedirs(path, exist_ok=True) + return path def exists(self, name: str) -> bool: """Check folder có tồn tại không""" @@ -73,7 +83,6 @@ class ProfileService: if copy_from: copy_from = os.path.abspath(copy_from) if os.path.isdir(copy_from): - # copy nội dung bên trong copy_from vào path for item in os.listdir(copy_from): s = os.path.join(copy_from, item) d = os.path.join(path, item) @@ -136,7 +145,6 @@ class ProfileService: try: profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) except Exception: - # Một vài phiên bản PyQt có thể khác tên hằng, bọc try/except để an toàn pass logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path) return profile diff --git a/templates/password/Screenshot 2025-10-15 at 13.48.55.png b/templates/password/Screenshot 2025-10-15 at 13.48.55.png new file mode 100644 index 0000000..f183ec8 Binary files /dev/null and b/templates/password/Screenshot 2025-10-15 at 13.48.55.png differ diff --git a/templates/username/Screenshot 2025-10-15 at 13.49.25.png b/templates/username/Screenshot 2025-10-15 at 13.49.25.png new file mode 100644 index 0000000..5e77a99 Binary files /dev/null and b/templates/username/Screenshot 2025-10-15 at 13.49.25.png differ