feat: автоматизация сборки и поддержка бессрочных токенов

- Исправлена ошибка ImportError: QtWebEngineCore путем перехода на PyInstaller.
- Добавлен скрипт build.py для автоматической сборки, очистки DLL и создания ZIP-архива.
- Реализована поддержка бессрочного доступа (offline_access) для VK Access Token.
- Обновлен README.md: добавлены разделы для разработчиков и описание структуры данных.
- Оптимизирован размер билда за счет удаления неиспользуемых библиотек Qt и папок локализации.
This commit is contained in:
2026-01-13 02:38:26 +03:00
parent 1eab8651f2
commit 3ed5bba9af
4 changed files with 218 additions and 167 deletions

80
main.py
View File

@@ -1,5 +1,5 @@
import sys
import vk_api
from vk_api import VkApi
import json
import time
import os
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout,
QSizePolicy, QDialog, QTextEdit, QTabWidget, QDialogButtonBox)
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
from PySide6.QtGui import QIcon
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile
from urllib.parse import urlparse, parse_qs, unquote
@@ -19,56 +20,65 @@ APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDa
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache")
def get_resource_path(relative_path):
""" Получает абсолютный путь к ресурсу, работает для скрипта и для .exe """
if hasattr(sys, '_MEIPASS'): # Для PyInstaller (на всякий случай)
return os.path.join(sys._MEIPASS, relative_path)
# Для cx_Freeze и обычного запуска
return os.path.join(os.path.abspath("."), relative_path)
def save_token(token, expires_in=0):
"""Сохраняет токен. Если expires_in=0, токен считается бессрочным."""
try:
expires_in = int(expires_in)
except (ValueError, TypeError):
expires_in = 0
def save_token(token, expires_in=3600):
"""
Сохраняет VK access токен и его время истечения в JSON файл.
По умолчанию токен действителен 1 час (3600 секунд).
"""
os.makedirs(APP_DATA_DIR, exist_ok=True)
expiration_time = time.time() + expires_in
# ИСПРАВЛЕНИЕ: если expires_in равно 0, то и время истечения ставим 0
expiration_time = (time.time() + expires_in) if expires_in > 0 else 0
data = {
"token": token,
"expiration_time": expiration_time
}
try:
with open(TOKEN_FILE, "w") as f:
json.dump(data, f)
print(
f"Токен сохранен в {TOKEN_FILE}. Срок действия истекает {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}")
status = "Бессрочно" if expiration_time == 0 else QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()
print(f"Токен сохранен. Срок действия: {status}")
return expiration_time
except IOError as e:
print(f"Ошибка сохранения токена: {e}")
return None
def load_token():
"""
Загружает VK access токен из JSON файла, если он еще действителен.
Возвращает (токен, время_истечения_unix) или (None, None).
"""
"""Загружает токен и проверяет его валидность."""
try:
if not os.path.exists(TOKEN_FILE):
print(f"Файл токена не найден по пути {TOKEN_FILE}.")
return None, None
with open(TOKEN_FILE, "r") as f:
data = json.load(f)
token = data.get("token")
expiration_time = data.get("expiration_time")
if token and expiration_time and expiration_time > time.time():
print(
f"Токен загружен из {TOKEN_FILE}. Действителен до {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}")
# ИСПРАВЛЕНИЕ: токен валиден, если время истечения 0 ИЛИ оно больше текущего
if token and (expiration_time == 0 or expiration_time > time.time()):
return token, expiration_time
else:
print("Токен просрочен или недействителен.")
if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE)
return None, None
except (IOError, json.JSONDecodeError) as e:
print(f"Ошибка загрузки токена: {e}")
except Exception as e:
print(f"Ошибка загрузки: {e}")
return None, None
class WebEnginePage(QWebEnginePage):
"""
Класс для обработки навигационных запросов в QWebEngineView,
@@ -443,15 +453,24 @@ class VkChatManager(QMainWindow):
if self.token_expiration_time is None:
self.token_timer_label.setText("Срок действия токена: Н")
return
# ИСПРАВЛЕНИЕ: обрабатываем бессрочный токен
if self.token_expiration_time == 0:
self.token_timer_label.setText("Срок действия: Бессрочно")
return
remaining_seconds = int(self.token_expiration_time - time.time())
if remaining_seconds <= 0:
self.token_timer_label.setText("Срок действия токена истек!")
if self.token_countdown_timer.isActive(): self.token_countdown_timer.stop()
self.token_timer_label.setText("Срок действия истек!")
if self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
self.set_ui_state(False)
self.status_label.setText("Статус: Срок действия токена истек, авторизуйтесь заново.")
self.status_label.setText("Статус: Срок действия истек, авторизуйтесь заново.")
self.token, self.token_expiration_time = None, None
self.token_input.clear()
return
minutes, seconds = divmod(remaining_seconds, 60)
hours, minutes = divmod(minutes, 60)
self.token_timer_label.setText(f"Срок: {hours:02d}ч {minutes:02d}м {seconds:02d}с")
@@ -507,12 +526,12 @@ class VkChatManager(QMainWindow):
return
self.token = token
self.token_expiration_time = time.time() + expires_in
save_token(self.token, expires_in)
# Сохраняем и получаем корректный expiration_time (0 или будущее время)
self.token_expiration_time = save_token(self.token, expires_in)
self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован")
self.vk_session = vk_api.VkApi(token=self.token)
self.vk_session = VkApi(token=self.token)
self.vk = self.vk_session.get_api()
self.set_ui_state(True)
self.load_chats()
@@ -523,7 +542,7 @@ class VkChatManager(QMainWindow):
self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован (токен загружен)")
self.vk_session = vk_api.VkApi(token=self.token)
self.vk_session = VkApi(token=self.token)
self.vk = self.vk_session.get_api()
self.set_ui_state(True)
self.load_chats()
@@ -692,6 +711,11 @@ if __name__ == "__main__":
app.setStyle("Fusion")
app.setPalette(app.style().standardPalette())
# Установка иконки для ВСЕХ окон приложения
icon_path = get_resource_path("icon.ico")
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
window = VkChatManager()
window.show()
sys.exit(app.exec())