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

126
README.md
View File

@@ -1,37 +1,42 @@
# Anabasis VK Chat Manager
# 🚀 Anabasis VK Chat Manager
## Описание проекта
**Anabasis VK Chat Manager** — специализированное десктопное приложение для HR-менеджеров и администраторов сообществ, предназначенное для автоматизации управления участниками в чатах ВКонтакте. Избавьтесь от рутины и управляйте всеми беседами из одного удобного интерфейса.
**Anabasis VK Chat Manager** — это десктопное приложение на Python с графическим интерфейсом, разработанное для упрощения управления пользователями в чатах ВКонтакте. Оно позволяет авторизоваться через VK OAuth, просматривать список своих чатов, а также исключать или приглашать пользователей в выбранные чаты.
---
Приложение спроектировано для минимизации ручных операций и повышения удобства управления групповыми беседами VK.
## ✨ Основные возможности
## Возможности
* **🔐 Безопасная авторизация:** Вход через официальный VK OAuth во встроенном защищенном браузере.
* **💾 Умное сохранение сессий:** Поддержка Persistent Cookies — не нужно вводить пароль при каждом запуске.
* **⏳ Таймер токена:** Наглядное отображение времени действия сессии прямо в интерфейсе.
* **📊 Массовые операции:**
* Моментальная загрузка всех доступных чатов пользователя.
* Групповой выбор чатов («Выбрать все» / «Снять выбор»).
* Быстрое обновление списка бесед.
* **👤 Интеллектуальный поиск ID:** Автоматическое распознавание ID пользователя из ссылок любого формата (например, `vk.com/id123`, `vk.com/durov` или просто `durov`).
* **🛠 Управление в один клик:** Кнопки для мгновенного исключения или приглашения пользователя во все выбранные чаты одновременно.
* **🛡 Стабильность:** Улучшенная обработка ошибок VK API и автоматическая реакция на смену IP-адреса.
* **Авторизация через VK OAuth:** Безопасный процесс входа через официальный VK API.
* **Сохранение сессии:** Поддержка сохранения куки-файлов браузера для длительной авторизации в `QWebEngineView`.
* **Управление токенами:** Автоматическое сохранение и загрузка VK Access Token для удобства использования.
* **Список чатов:** Загрузка и отображение списка доступных чатов пользователя.
* **Выбор чатов:** Возможность выбора одного или нескольких чатов для выполнения операций.
* **Автоматическое определение ID пользователя:** Получение ID пользователя VK из различных форматов ссылок (например, `vk.com/id123`, `vk.com/durov`).
* **Исключение пользователей:** Удаление пользователя из выбранных чатов.
* **Приглашение пользователей:** Добавление пользователя в выбранные чаты.
* **Визуальный таймер токена:** Отображение оставшегося времени действия Access Token.
* **Информативные сообщения:** Детальные статусы операций и сообщения об ошибках.
* **Обработка ошибок:** Улучшенная обработка ошибок VK API, включая смену IP-адреса.
---
## Установка
## 📦 Установка и запуск
### Готовый билд
### Вариант 1: Готовый билд (Windows)
1. Перейдите в раздел **Releases** на GitHub.
2. Скачайте архив формата `AnabasisManager-1.x.zip`.
3. Распакуйте архив в удобную папку.
4. Запустите файл **AnabasisManager.exe**.
Скачайте последнюю доступную версию из релизов и распакуйте архив
### Вариант 2: Запуск из исходного кода
Вам потребуется **Python 3.10** или выше.
### Ручная установка
Для запуска приложения вам потребуется Python 3 и библиотеки `PySide6` и `vk_api`.
1. **Клонируйте репозиторий:**
```bash
git clone [https://github.com/your-username/AnabasisVKChatManager.git](https://github.com/your-username/AnabasisVKChatManager.git)
cd AnabasisVKChatManager
```
1. **Клонируйте репозиторий**
2. **Создайте и активируйте виртуальное окружение (рекомендуется):**
2. **Настройте виртуальное окружение:**
```bash
python -m venv venv
# Для Windows:
@@ -45,50 +50,55 @@
pip install PySide6 vk_api
```
## Использование
1. **Запустите приложение:**
* *Готовый билд:*
* Запустите **AnabasisHRChatManager.exe**
* *Ручная установка:*
* ```bash
4. **Запустите приложение:**
```bash
python main.py
```
2. **Авторизация:**
* Нажмите кнопку "Авторизоваться через VK".
* В открывшемся окне браузера войдите в свой аккаунт ВКонтакте.
* Разрешите доступ приложению, если потребуется.
* После успешной авторизации окно закроется, и токен доступа будет сохранен.
---
3. **Выбор чатов:**
* После авторизации приложение автоматически загрузит список ваших чатов.
* Отметьте галочками те чаты, с которыми хотите работать. Используйте кнопки "Выбрать все" / "Снять выбор со всех" для удобства.
* Кнопка "Обновить чаты" позволяет перезагрузить список чатов.
## 🕹 Инструкция по использованию
4. **Управление пользователями:**
* В поле "Введите или вставьте ссылку на страницу VK" вставьте ссылку на страницу пользователя ВКонтакте (например, `vk.com/id123` или `vk.com/durov`). Приложение автоматически определит ID пользователя.
* Нажмите кнопку "ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ" для удаления пользователя из выбранных чатов.
* Нажмите кнопку "ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ" для добавления пользователя в выбранные чаты.
* Опция "Показать 250 последних сообщений при добавлении" позволяет управлять видимостью истории сообщений для нового участника (Примечание: VK API может игнорировать этот параметр для `messages.addChatUser`).
1. **Вход:** Нажмите кнопку «Авторизоваться через VK». Введите данные в открывшемся окне браузера.
2. **Выбор целей:** Отметьте галочками чаты, в которых нужно произвести изменения.
3. **Данные пользователя:** Вставьте ссылку на профиль VK человека, которого нужно добавить или удалить.
4. **Действие:** Нажмите кнопку нужной операции. Следите за прогрессом в окне системных сообщений.
## Структура данных и конфигурация
---
Приложение хранит данные в директории, специфичной для данных приложения, что соответствует рекомендациям операционных систем.
## 📂 Техническая информация
* **`token.json`**: Файл для сохранения VK Access Token. Находится в `[AppDataLocation]/AnabasisVKChatManager/token.json`.
* **`web_engine_cache/`**: Директория для хранения куки-файлов и кэша `QWebEngineProfile`, обеспечивающая сохранение сессии внутри встроенного браузера. Находится в `[AppDataLocation]/AnabasisVKChatManager/web_engine_cache/`.
### Сборка проекта (для разработчиков)
Проект использует кастомный скрипт автоматизации `build.py`, который оптимизирует зависимости `PySide6` и корректно упаковывает `QtWebEngineCore`.
`[AppDataLocation]` соответствует:
* Windows: `%APPDATA%` (например, `C:\Users\YourUser\AppData\Roaming`)
* macOS: `~/Library/Application Support`
* Linux: `~/.local/share`
**Команда для сборки:**
```bash
python build.py
```
## Известные проблемы / Ограничения
Скрипт автоматически:
* Параметр `visible_messages_count` для `messages.addChatUser` может быть проигнорирован VK API согласно официальной документации. Приложение уведомит вас об этом при попытке использования.
* При смене IP-адреса, токен авторизации VK может стать недействительным, потребуется повторная авторизация. Приложение автоматически предложит её.
Собирает .exe через PyInstaller с использованием --collect-all для модулей WebEngine.
## Лицензия
Удаляет лишние библиотеки (PDF, Multimedia, Designer) и папки переводов, сокращая размер сборки на ~100 МБ.
Этот проект распространяется под лицензией MIT.
Создает готовый ZIP-архив с актуальной версией в названии.
Хранение данных
Приложение использует системные папки AppData для изоляции пользовательских данных:
Windows: %APPDATA%/AnabasisVKChatManager
macOS: ~/Library/Application Support/AnabasisVKChatManager
В этих папках хранятся token.json (доступ к API) и web_engine_cache/ (сессия браузера).
---
## 📜 Лицензия
---
Проект распространяется под лицензией MIT.
Сэкономьте часы ручного труда с Anabasis VK Chat Manager.

96
build.py Normal file
View File

@@ -0,0 +1,96 @@
import os
import shutil
import subprocess
import sys
# --- Конфигурация ---
APP_NAME = "AnabasisManager"
VERSION = "1.3" # Ваша версия
MAIN_SCRIPT = "main.py"
ICON_PATH = "icon.ico"
DIST_DIR = os.path.join("dist", APP_NAME)
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия
def run_build():
print(f"--- 1. Запуск PyInstaller для {APP_NAME} v{VERSION} ---")
command = [
"pyinstaller",
"--noconfirm",
"--onedir",
"--windowed",
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
]
command = [arg for arg in command if arg]
try:
subprocess.check_call(command)
print("\n[OK] Сборка PyInstaller завершена.")
except subprocess.CalledProcessError as e:
print(f"\n[ERROR] Ошибка при сборке: {e}")
sys.exit(1)
def run_cleanup():
print(f"\n--- 2. Оптимизация папки {APP_NAME} ---")
# Пытаемся найти папку PySide6 внутри сборки
pyside_path = os.path.join(DIST_DIR, "PySide6")
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:
path = os.path.join(pyside_path, item)
if os.path.exists(path):
try:
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
print(f"Удалено: {item}")
except Exception as e:
print(f"Пропуск {item}: {e}")
def create_archive():
print(f"\n--- 3. Создание архива {ARCHIVE_NAME}.zip ---")
try:
# Создаем zip-архив из папки DIST_DIR
# base_name - имя файла без расширения, format - 'zip', root_dir - что упаковываем
shutil.make_archive(os.path.join("dist", ARCHIVE_NAME), 'zip', DIST_DIR)
print(f"[OK] Архив создан: dist/{ARCHIVE_NAME}.zip")
except Exception as e:
print(f"[ERROR] Не удалось создать архив: {e}")
if __name__ == "__main__":
# Предварительная очистка
for folder in ["build", "dist"]:
if os.path.exists(folder):
shutil.rmtree(folder)
run_build()
run_cleanup()
create_archive()
print("\n" + "=" * 30)
print(f"ПРОЦЕСС ЗАВЕРШЕН")
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
print("=" * 30)

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())

View File

@@ -1,79 +0,0 @@
# setup.py
import sys
import os
from cx_Freeze import setup, Executable
# --- Основные настройки ---
# Имя вашего основного скрипта
main_script = "main.py" # Замените на имя вашего основного Python-файла
# Имя вашего приложения (исполняемого файла без расширения)
exe_name = "AnabasisHRChatManager"
# --- Платформо-зависимые настройки ---
# Определяем базовый тип приложения и имя конечного файла
base = None
target_name = exe_name
icon_path = "icon.ico" # Путь к иконке по умолчанию
if sys.platform == "win32":
# Для графических приложений на Windows (консоль не будет открываться)
base = "Win32GUI"
# Добавляем расширение .exe для Windows
target_name = f"{exe_name}.exe"
elif sys.platform == "darwin": # macOS
base = "MacOSX"
# Иконки для macOS имеют формат .icns
# icon_path = "icon.icns"
elif sys.platform.startswith("linux"): # Linux
# Для Linux обычно не требуется специальный 'base'
# Иконки могут быть в формате .png или .xpm
# icon_path = "icon.png"
pass # Оставляем base = None
# --- Опции сборки ---
# Общие опции сборки для всех платформ
build_exe_options = {
# 'packages' - список пакетов для обязательного включения.
"packages": ["os", "sys", "requests", "json", "webbrowser"],
# 'excludes' - список пакетов для исключения.
"excludes": ["tkinter", "unittest", "PyQt5.QtWebEngineWidgets"],
# 'include_files' - список дополнительных файлов или папок.
# Формат: [('источник', 'назначение_в_сборке')]
"include_files": [], # Например: ["resources/", "config.ini"]
# 'build_exe' - папка для выходных файлов
"build_exe": f"build_{sys.platform}", # Создаём отдельную папку для каждой ОС
}
# Опции, специфичные для Windows
if sys.platform == "win32":
build_exe_options["include_msvcr"] = True # Включаем C++ Runtime Library
# --- Определение исполняемого файла ---
executables = [
Executable(
script=main_script,
base=base,
target_name=target_name, # Имя конечного файла
icon=icon_path # Путь к файлу иконки
)
]
# --- Настройка метаданных и запуск сборки ---
setup(
name=exe_name,
version="1.2",
description="Управление чатами для HR-менеджеров",
options={
"build_exe": build_exe_options
},
executables=executables
)