feat(auth): pywebview вместо webengine
- добавлен auth_webview.py и режим --auth - build.py обновлён, WebEngine исключён - pywebview добавлен в requirements
This commit is contained in:
78
auth_webview.py
Normal file
78
auth_webview.py
Normal file
@@ -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()
|
||||||
41
build.py
41
build.py
@@ -5,11 +5,28 @@ import sys
|
|||||||
|
|
||||||
# --- Конфигурация ---
|
# --- Конфигурация ---
|
||||||
APP_NAME = "AnabasisManager"
|
APP_NAME = "AnabasisManager"
|
||||||
VERSION = "1.3" # Ваша версия
|
VERSION = "1.5" # Ваша версия
|
||||||
MAIN_SCRIPT = "main.py"
|
MAIN_SCRIPT = "main.py"
|
||||||
ICON_PATH = "icon.ico"
|
ICON_PATH = "icon.ico"
|
||||||
DIST_DIR = os.path.join("dist", APP_NAME)
|
DIST_DIR = os.path.join("dist", APP_NAME)
|
||||||
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия
|
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():
|
def run_build():
|
||||||
@@ -20,12 +37,14 @@ def run_build():
|
|||||||
"--noconfirm",
|
"--noconfirm",
|
||||||
"--onedir",
|
"--onedir",
|
||||||
"--windowed",
|
"--windowed",
|
||||||
|
"--exclude-module", "PySide6.QtWebEngineCore",
|
||||||
|
"--exclude-module", "PySide6.QtWebEngineWidgets",
|
||||||
|
"--exclude-module", "PySide6.QtWebEngineQuick",
|
||||||
f"--name={APP_NAME}",
|
f"--name={APP_NAME}",
|
||||||
f"--icon={ICON_PATH}" if os.path.exists(ICON_PATH) else "",
|
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 "",
|
f"--add-data={ICON_PATH}{os.pathsep}." if os.path.exists(ICON_PATH) else "",
|
||||||
"--collect-all", "PySide6.QtWebEngineCore",
|
f"--add-data=auth_webview.py{os.pathsep}.",
|
||||||
"--collect-all", "PySide6.QtWebEngineWidgets",
|
MAIN_SCRIPT
|
||||||
MAIN_SCRIPT
|
|
||||||
]
|
]
|
||||||
|
|
||||||
command = [arg for arg in command if arg]
|
command = [arg for arg in command if arg]
|
||||||
@@ -46,16 +65,7 @@ def run_cleanup():
|
|||||||
if not os.path.exists(pyside_path):
|
if not os.path.exists(pyside_path):
|
||||||
pyside_path = DIST_DIR
|
pyside_path = DIST_DIR
|
||||||
|
|
||||||
to_remove = [
|
for item in 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"
|
|
||||||
]
|
|
||||||
|
|
||||||
for item in to_remove:
|
|
||||||
path = os.path.join(pyside_path, item)
|
path = os.path.join(pyside_path, item)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
try:
|
try:
|
||||||
@@ -81,6 +91,7 @@ def create_archive():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
ensure_project_root()
|
||||||
# Предварительная очистка
|
# Предварительная очистка
|
||||||
for folder in ["build", "dist"]:
|
for folder in ["build", "dist"]:
|
||||||
if os.path.exists(folder):
|
if os.path.exists(folder):
|
||||||
@@ -91,6 +102,6 @@ if __name__ == "__main__":
|
|||||||
create_archive()
|
create_archive()
|
||||||
|
|
||||||
print("\n" + "=" * 30)
|
print("\n" + "=" * 30)
|
||||||
print(f"ПРОЦЕСС ЗАВЕРШЕН")
|
print("ПРОЦЕСС ЗАВЕРШЕН")
|
||||||
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
|
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
|
||||||
print("=" * 30)
|
print("=" * 30)
|
||||||
196
main.py
196
main.py
@@ -2,9 +2,11 @@ import sys
|
|||||||
import base64
|
import base64
|
||||||
import ctypes
|
import ctypes
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from vk_api import VkApi
|
from vk_api import VkApi
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import auth_webview
|
||||||
import os
|
import os
|
||||||
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
|
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
|
||||||
QPushButton, QVBoxLayout, QWidget, QMessageBox,
|
QPushButton, QVBoxLayout, QWidget, QMessageBox,
|
||||||
@@ -12,8 +14,6 @@ from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
|
|||||||
QSizePolicy, QDialog, QTextEdit, QTabWidget, QDialogButtonBox)
|
QSizePolicy, QDialog, QTextEdit, QTabWidget, QDialogButtonBox)
|
||||||
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
|
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
|
||||||
from PySide6.QtGui import QIcon, QAction
|
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 urllib.parse import urlparse, parse_qs, unquote
|
||||||
from vk_api.exceptions import VkApiError
|
from vk_api.exceptions import VkApiError
|
||||||
from PySide6.QtCore import QStandardPaths
|
from PySide6.QtCore import QStandardPaths
|
||||||
@@ -176,142 +176,6 @@ def load_token():
|
|||||||
print(f"Ошибка загрузки: {e}")
|
print(f"Ошибка загрузки: {e}")
|
||||||
return None, None
|
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):
|
class MultiLinkDialog(QDialog):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -784,10 +648,49 @@ class VkChatManager(QMainWindow):
|
|||||||
|
|
||||||
def start_auth(self):
|
def start_auth(self):
|
||||||
self.status_label.setText("Статус: ожидание авторизации...")
|
self.status_label.setText("Статус: ожидание авторизации...")
|
||||||
auth_window = AuthBrowserWindow(self)
|
auth_url = (
|
||||||
auth_window.token_extracted_signal.connect(self.handle_new_auth_token)
|
"https://oauth.vk.com/authorize?"
|
||||||
auth_window.start_auth_flow()
|
"client_id=2685278&"
|
||||||
auth_window.exec()
|
"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):
|
def handle_new_auth_token(self, token, expires_in):
|
||||||
if not token:
|
if not token:
|
||||||
@@ -1107,6 +1010,15 @@ class VkChatManager(QMainWindow):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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 = QApplication(sys.argv)
|
||||||
app.setStyle("Fusion")
|
app.setStyle("Fusion")
|
||||||
app.setPalette(app.style().standardPalette())
|
app.setPalette(app.style().standardPalette())
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
PySide6~=6.9.1
|
PySide6~=6.10.2
|
||||||
vk-api~=11.9.9
|
vk-api~=11.9.9
|
||||||
|
pywebview
|
||||||
|
|||||||
Reference in New Issue
Block a user