From aad6e8c5af002a62ac2097e78fa8e72ae9df2ab2 Mon Sep 17 00:00:00 2001 From: benya Date: Wed, 4 Feb 2026 00:02:32 +0300 Subject: [PATCH] =?UTF-8?q?feat(auth):=20pywebview=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20webengine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен auth_webview.py и режим --auth - build.py обновлён, WebEngine исключён - pywebview добавлен в requirements --- auth_webview.py | 78 +++++++++++++++++++ build.py | 43 +++++++---- main.py | 196 +++++++++++++---------------------------------- requirements.txt | 3 +- 4 files changed, 161 insertions(+), 159 deletions(-) create mode 100644 auth_webview.py diff --git a/auth_webview.py b/auth_webview.py new file mode 100644 index 0000000..c43eeac --- /dev/null +++ b/auth_webview.py @@ -0,0 +1,78 @@ +import os +import sys +import time +import json +import threading +from urllib.parse import urlparse, parse_qs, unquote + +import webview + + +def extract_token(url_string): + token = None + expires_in = 3600 + parsed = urlparse(url_string) + + if parsed.fragment: + params = parse_qs(parsed.fragment) + else: + params = parse_qs(parsed.query) + + if 'access_token' in params: + token = params['access_token'][0] + if 'expires_in' in params: + try: + expires_in = int(params['expires_in'][0]) + except ValueError: + pass + + if not token: + start_marker = "access_token%253D" + end_marker = "%25" + + start_index = url_string.find(start_marker) + if start_index != -1: + token_start_index = start_index + len(start_marker) + remaining_url = url_string[token_start_index:] + end_index = remaining_url.find(end_marker) + + if end_index != -1: + raw_token = remaining_url[:end_index] + else: + amp_index = remaining_url.find('&') + if amp_index != -1: + raw_token = remaining_url[:amp_index] + else: + raw_token = remaining_url + token = unquote(raw_token) + + return token, expires_in + + +def main_auth(auth_url, output_path): + def poll_url(): + try: + url = window.get_current_url() + except Exception: + url = None + if url: + token, expires_in = extract_token(url) + if token: + data = {"token": token, "expires_in": expires_in} + with open(output_path, "w", encoding="utf-8") as f: + json.dump(data, f) + window.destroy() + return + threading.Timer(0.5, poll_url).start() + + def on_loaded(): + threading.Timer(0.5, poll_url).start() + + window = webview.create_window("VK Авторизация", auth_url) + window.events.loaded += on_loaded + storage_path = os.path.join(os.path.dirname(output_path), "webview_profile") + webview.start(private_mode=False, storage_path=storage_path) + + +if __name__ == "__main__": + main() diff --git a/build.py b/build.py index 6eb5be3..2f4a9ba 100644 --- a/build.py +++ b/build.py @@ -5,11 +5,28 @@ import sys # --- Конфигурация --- APP_NAME = "AnabasisManager" -VERSION = "1.3" # Ваша версия +VERSION = "1.5" # Ваша версия MAIN_SCRIPT = "main.py" ICON_PATH = "icon.ico" DIST_DIR = os.path.join("dist", APP_NAME) ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия +SAFE_CLEAN_ROOT_FILES = {"main.py", "requirements.txt", "build.py"} +REMOVE_LIST = [ + "Qt6Pdf.dll", "Qt6PdfQuick.dll", "Qt6PdfWidgets.dll", + "Qt6VirtualKeyboard.dll", "Qt6Positioning.dll", + "Qt6PrintSupport.dll", "Qt6Svg.dll", "Qt6Sql.dll", + "Qt6Charts.dll", "Qt6Multimedia.dll", "Qt63DCore.dll", + "translations", + "Qt6QuickTemplates2.dll" +] + + +def ensure_project_root(): + missing = [name for name in SAFE_CLEAN_ROOT_FILES if not os.path.exists(name)] + if missing: + print("[ERROR] Скрипт нужно запускать из корня проекта.") + print(f"[ERROR] Не найдены: {', '.join(missing)}") + sys.exit(1) def run_build(): @@ -20,12 +37,14 @@ def run_build(): "--noconfirm", "--onedir", "--windowed", + "--exclude-module", "PySide6.QtWebEngineCore", + "--exclude-module", "PySide6.QtWebEngineWidgets", + "--exclude-module", "PySide6.QtWebEngineQuick", f"--name={APP_NAME}", f"--icon={ICON_PATH}" if os.path.exists(ICON_PATH) else "", f"--add-data={ICON_PATH}{os.pathsep}." if os.path.exists(ICON_PATH) else "", - "--collect-all", "PySide6.QtWebEngineCore", - "--collect-all", "PySide6.QtWebEngineWidgets", - MAIN_SCRIPT + f"--add-data=auth_webview.py{os.pathsep}.", + MAIN_SCRIPT ] command = [arg for arg in command if arg] @@ -46,16 +65,7 @@ def run_cleanup(): if not os.path.exists(pyside_path): pyside_path = DIST_DIR - to_remove = [ - "Qt6Pdf.dll", "Qt6PdfQuick.dll", "Qt6PdfWidgets.dll", - "Qt6VirtualKeyboard.dll", "Qt6Positioning.dll", - "Qt6PrintSupport.dll", "Qt6Svg.dll", "Qt6Sql.dll", - "Qt6Charts.dll", "Qt6Multimedia.dll", "Qt63DCore.dll", - "translations", - "Qt6QuickTemplates2.dll" - ] - - for item in to_remove: + for item in REMOVE_LIST: path = os.path.join(pyside_path, item) if os.path.exists(path): try: @@ -81,6 +91,7 @@ def create_archive(): if __name__ == "__main__": + ensure_project_root() # Предварительная очистка for folder in ["build", "dist"]: if os.path.exists(folder): @@ -91,6 +102,6 @@ if __name__ == "__main__": create_archive() print("\n" + "=" * 30) - print(f"ПРОЦЕСС ЗАВЕРШЕН") + print("ПРОЦЕСС ЗАВЕРШЕН") print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip") - print("=" * 30) \ No newline at end of file + print("=" * 30) diff --git a/main.py b/main.py index d5d6397..bc27b42 100644 --- a/main.py +++ b/main.py @@ -2,9 +2,11 @@ import sys import base64 import ctypes import shutil +import subprocess from vk_api import VkApi import json import time +import auth_webview import os from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget, QMessageBox, @@ -12,8 +14,6 @@ from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, QSizePolicy, QDialog, QTextEdit, QTabWidget, QDialogButtonBox) from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer from PySide6.QtGui import QIcon, QAction -from PySide6.QtWebEngineWidgets import QWebEngineView -from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile from urllib.parse import urlparse, parse_qs, unquote from vk_api.exceptions import VkApiError from PySide6.QtCore import QStandardPaths @@ -176,142 +176,6 @@ def load_token(): print(f"Ошибка загрузки: {e}") return None, None -class WebEnginePage(QWebEnginePage): - """ - Класс для обработки навигационных запросов в QWebEngineView, - специально для извлечения токена авторизации VK. - """ - - # Добавлен параметр profile в конструктор, чтобы использовать пользовательский профиль - def __init__(self, profile=None, parent=None, browser_window_instance=None): - super().__init__(profile, parent) # Передаем profile в базовый класс - self.parent_browser_window = browser_window_instance - self.token_extracted = False - - def acceptNavigationRequest(self, url, _type, isMainFrame): - """ - Переопределенный метод для перехвата URL-адреса, содержащего токен доступа. - """ - url_string = url.toString() - if "access_token" in url_string and not self.token_extracted: - self.token_extracted = True - if self.parent_browser_window: - self.parent_browser_window.process_auth_url(url_string) - return False - return super().acceptNavigationRequest(url, _type, isMainFrame) - - -class AuthBrowserWindow(QDialog): - """ - Отдельное окно-диалог для проведения OAuth авторизации VK. - """ - token_extracted_signal = Signal(str, int) - - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("Авторизация VK") - self.setGeometry(350, 350, 800, 600) - - layout = QVBoxLayout(self) - self.browser = QWebEngineView() - - # Настройка QWebEngineProfile для сохранения куки и других данных - os.makedirs(WEB_ENGINE_CACHE_DIR, exist_ok=True) - - # ИСПРАВЛЕНО: Сделать profile атрибутом экземпляра и дать ему self как родителя - self.profile = QWebEngineProfile("AnabasisVKWebProfile", self) - self.profile.setPersistentCookiesPolicy(QWebEngineProfile.AllowPersistentCookies) - self.profile.setPersistentStoragePath(WEB_ENGINE_CACHE_DIR) - - # Создаем страницу с настроенным профилем - self.browser.page = WebEnginePage(profile=self.profile, parent=self.browser, browser_window_instance=self) - self.browser.setPage(self.browser.page) - layout.addWidget(self.browser) - - self.status_label = QLabel("Ожидание авторизации...") - self.status_label.setAlignment(Qt.AlignCenter) - layout.addWidget(self.status_label) - - def start_auth_flow(self): - """Запускает OAuth авторизацию VK в этом окне.""" - auth_url = ( - "https://oauth.vk.com/authorize?" - "client_id=2685278&" - "display=page&" - "redirect_uri=https://oauth.vk.com/blank.html&" - "scope=1073737727&" - "response_type=token&" - "v=5.131" - ) - self.browser.setUrl(QUrl(auth_url)) - self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...") - - def process_auth_url(self, url_string): - """ - Извлекает токен доступа из URL перенаправления и испускает сигнал. - """ - token = None - expires_in = 3600 - parsed = urlparse(url_string) - - if parsed.fragment: - params = parse_qs(parsed.fragment) - else: - params = parse_qs(parsed.query) - - if 'access_token' in params: - token = params['access_token'][0] - if 'expires_in' in params: - try: - expires_in = int(params['expires_in'][0]) - except ValueError: - pass - - if not token: - start_marker = "access_token%253D" - end_marker = "%25" - - start_index = url_string.find(start_marker) - if start_index != -1: - token_start_index = start_index + len(start_marker) - remaining_url = url_string[token_start_index:] - end_index = remaining_url.find(end_marker) - - if end_index != -1: - raw_token = remaining_url[:end_index] - else: - amp_index = remaining_url.find('&') - if amp_index != -1: - raw_token = remaining_url[:amp_index] - else: - raw_token = remaining_url - token = unquote(raw_token) - - if token: - self.token_extracted_signal.emit(token, expires_in) - self.accept() - else: - QMessageBox.warning(self, "Ошибка Авторизации", - "Не удалось получить токен. Проверьте URL или попробуйте еще раз.") - self.reject() - - def closeEvent(self, event): - """ - Переопределенный метод для обработки закрытия окна. - Выполняет очистку ресурсов QWebEngine, чтобы избежать предупреждения - "Release of profile requested but WebEnginePage still not deleted. Expect troubles!". - """ - current_page = self.browser.page() - if current_page: - # Снимаем страницу с QWebEngineView, чтобы View перестала ей владеть - self.browser.setPage(None) - # Планируем удаление объекта страницы - current_page.deleteLater() - - # Вызываем метод базового класса для корректного закрытия диалога - super().closeEvent(event) - - class MultiLinkDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) @@ -784,10 +648,49 @@ class VkChatManager(QMainWindow): def start_auth(self): self.status_label.setText("Статус: ожидание авторизации...") - auth_window = AuthBrowserWindow(self) - auth_window.token_extracted_signal.connect(self.handle_new_auth_token) - auth_window.start_auth_flow() - auth_window.exec() + auth_url = ( + "https://oauth.vk.com/authorize?" + "client_id=2685278&" + "display=page&" + "redirect_uri=https://oauth.vk.com/blank.html&" + "scope=1073737727&" + "response_type=token&" + "v=5.131" + ) + output_path = os.path.join(APP_DATA_DIR, "auth_result.json") + try: + if os.path.exists(output_path): + os.remove(output_path) + except Exception: + pass + + cmd = [sys.executable, "--auth", auth_url, output_path] + try: + subprocess.check_call(cmd) + except Exception as e: + self.status_label.setText(f"Статус: ошибка запуска авторизации: {e}") + return + + if not os.path.exists(output_path): + self.status_label.setText("Статус: авторизация не удалась") + return + + try: + with open(output_path, "r", encoding="utf-8") as f: + data = json.load(f) + token = data.get("token") + expires_in = data.get("expires_in", 0) + except Exception: + token = None + expires_in = 0 + + try: + if os.path.exists(output_path): + os.remove(output_path) + except Exception: + pass + + self.handle_new_auth_token(token, expires_in) def handle_new_auth_token(self, token, expires_in): if not token: @@ -1107,6 +1010,15 @@ class VkChatManager(QMainWindow): if __name__ == "__main__": + if "--auth" in sys.argv: + try: + idx = sys.argv.index("--auth") + auth_url = sys.argv[idx + 1] + output_path = sys.argv[idx + 2] + except Exception: + sys.exit(1) + auth_webview.main_auth(auth_url, output_path) + sys.exit(0) app = QApplication(sys.argv) app.setStyle("Fusion") app.setPalette(app.style().standardPalette()) diff --git a/requirements.txt b/requirements.txt index 147c614..00c2c52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -PySide6~=6.9.1 +PySide6~=6.10.2 vk-api~=11.9.9 +pywebview