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

130
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** или выше.
### Ручная установка 1. **Клонируйте репозиторий:**
Для запуска приложения вам потребуется Python 3 и библиотеки `PySide6` и `vk_api`. ```bash
git clone [https://github.com/your-username/AnabasisVKChatManager.git](https://github.com/your-username/AnabasisVKChatManager.git)
cd AnabasisVKChatManager
```
1. **Клонируйте репозиторий** 2. **Настройте виртуальное окружение:**
2. **Создайте и активируйте виртуальное окружение (рекомендуется):**
```bash ```bash
python -m venv venv python -m venv venv
# Для Windows: # Для Windows:
@@ -45,50 +50,55 @@
pip install PySide6 vk_api pip install PySide6 vk_api
``` ```
## Использование 4. **Запустите приложение:**
```bash
python main.py
```
1. **Запустите приложение:** ---
* *Готовый билд:*
* Запустите **AnabasisHRChatManager.exe**
* *Ручная установка:*
* ```bash
python main.py
```
2. **Авторизация:**
* Нажмите кнопку "Авторизоваться через VK".
* В открывшемся окне браузера войдите в свой аккаунт ВКонтакте.
* Разрешите доступ приложению, если потребуется.
* После успешной авторизации окно закроется, и токен доступа будет сохранен.
3. **Выбор чатов:** ## 🕹 Инструкция по использованию
* После авторизации приложение автоматически загрузит список ваших чатов.
* Отметьте галочками те чаты, с которыми хотите работать. Используйте кнопки "Выбрать все" / "Снять выбор со всех" для удобства.
* Кнопка "Обновить чаты" позволяет перезагрузить список чатов.
4. **Управление пользователями:** 1. **Вход:** Нажмите кнопку «Авторизоваться через VK». Введите данные в открывшемся окне браузера.
* В поле "Введите или вставьте ссылку на страницу VK" вставьте ссылку на страницу пользователя ВКонтакте (например, `vk.com/id123` или `vk.com/durov`). Приложение автоматически определит ID пользователя. 2. **Выбор целей:** Отметьте галочками чаты, в которых нужно произвести изменения.
* Нажмите кнопку "ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ" для удаления пользователя из выбранных чатов. 3. **Данные пользователя:** Вставьте ссылку на профиль VK человека, которого нужно добавить или удалить.
* Нажмите кнопку "ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ" для добавления пользователя в выбранные чаты. 4. **Действие:** Нажмите кнопку нужной операции. Следите за прогрессом в окне системных сообщений.
* Опция "Показать 250 последних сообщений при добавлении" позволяет управлять видимостью истории сообщений для нового участника (Примечание: VK API может игнорировать этот параметр для `messages.addChatUser`).
## Структура данных и конфигурация ---
Приложение хранит данные в директории, специфичной для данных приложения, что соответствует рекомендациям операционных систем. ## 📂 Техническая информация
* **`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`) ```bash
* macOS: `~/Library/Application Support` python build.py
* Linux: `~/.local/share` ```
## Известные проблемы / Ограничения Скрипт автоматически:
* Параметр `visible_messages_count` для `messages.addChatUser` может быть проигнорирован VK API согласно официальной документации. Приложение уведомит вас об этом при попытке использования. Собирает .exe через PyInstaller с использованием --collect-all для модулей WebEngine.
* При смене IP-адреса, токен авторизации VK может стать недействительным, потребуется повторная авторизация. Приложение автоматически предложит её.
## Лицензия Удаляет лишние библиотеки (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 sys
import vk_api from vk_api import VkApi
import json import json
import time import time
import os import os
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout, QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout,
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
from PySide6.QtWebEngineWidgets import QWebEngineView from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile
from urllib.parse import urlparse, parse_qs, unquote 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") TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache") 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) 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 = { data = {
"token": token, "token": token,
"expiration_time": expiration_time "expiration_time": expiration_time
} }
try: try:
with open(TOKEN_FILE, "w") as f: with open(TOKEN_FILE, "w") as f:
json.dump(data, 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: except IOError as e:
print(f"Ошибка сохранения токена: {e}") print(f"Ошибка сохранения токена: {e}")
return None
def load_token(): def load_token():
""" """Загружает токен и проверяет его валидность."""
Загружает VK access токен из JSON файла, если он еще действителен.
Возвращает (токен, время_истечения_unix) или (None, None).
"""
try: try:
if not os.path.exists(TOKEN_FILE): if not os.path.exists(TOKEN_FILE):
print(f"Файл токена не найден по пути {TOKEN_FILE}.")
return None, None return None, None
with open(TOKEN_FILE, "r") as f: with open(TOKEN_FILE, "r") as f:
data = json.load(f) data = json.load(f)
token = data.get("token") token = data.get("token")
expiration_time = data.get("expiration_time") expiration_time = data.get("expiration_time")
if token and expiration_time and expiration_time > time.time(): # ИСПРАВЛЕНИЕ: токен валиден, если время истечения 0 ИЛИ оно больше текущего
print( if token and (expiration_time == 0 or expiration_time > time.time()):
f"Токен загружен из {TOKEN_FILE}. Действителен до {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}")
return token, expiration_time return token, expiration_time
else: else:
print("Токен просрочен или недействителен.")
if os.path.exists(TOKEN_FILE): if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE) os.remove(TOKEN_FILE)
return None, None return None, None
except (IOError, json.JSONDecodeError) as e: except Exception as e:
print(f"Ошибка загрузки токена: {e}") print(f"Ошибка загрузки: {e}")
return None, None return None, None
class WebEnginePage(QWebEnginePage): class WebEnginePage(QWebEnginePage):
""" """
Класс для обработки навигационных запросов в QWebEngineView, Класс для обработки навигационных запросов в QWebEngineView,
@@ -443,15 +453,24 @@ class VkChatManager(QMainWindow):
if self.token_expiration_time is None: if self.token_expiration_time is None:
self.token_timer_label.setText("Срок действия токена: Н") self.token_timer_label.setText("Срок действия токена: Н")
return return
# ИСПРАВЛЕНИЕ: обрабатываем бессрочный токен
if self.token_expiration_time == 0:
self.token_timer_label.setText("Срок действия: Бессрочно")
return
remaining_seconds = int(self.token_expiration_time - time.time()) remaining_seconds = int(self.token_expiration_time - time.time())
if remaining_seconds <= 0: if remaining_seconds <= 0:
self.token_timer_label.setText("Срок действия токена истек!") self.token_timer_label.setText("Срок действия истек!")
if self.token_countdown_timer.isActive(): self.token_countdown_timer.stop() if self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
self.set_ui_state(False) self.set_ui_state(False)
self.status_label.setText("Статус: Срок действия токена истек, авторизуйтесь заново.") self.status_label.setText("Статус: Срок действия истек, авторизуйтесь заново.")
self.token, self.token_expiration_time = None, None self.token, self.token_expiration_time = None, None
self.token_input.clear() self.token_input.clear()
return return
minutes, seconds = divmod(remaining_seconds, 60) minutes, seconds = divmod(remaining_seconds, 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
self.token_timer_label.setText(f"Срок: {hours:02d}ч {minutes:02d}м {seconds:02d}с") self.token_timer_label.setText(f"Срок: {hours:02d}ч {minutes:02d}м {seconds:02d}с")
@@ -507,12 +526,12 @@ class VkChatManager(QMainWindow):
return return
self.token = token self.token = token
self.token_expiration_time = time.time() + expires_in # Сохраняем и получаем корректный expiration_time (0 или будущее время)
save_token(self.token, expires_in) self.token_expiration_time = save_token(self.token, expires_in)
self.token_input.setText(self.token[:50] + "...") self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован") 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.vk = self.vk_session.get_api()
self.set_ui_state(True) self.set_ui_state(True)
self.load_chats() self.load_chats()
@@ -523,7 +542,7 @@ class VkChatManager(QMainWindow):
self.token_input.setText(self.token[:50] + "...") self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован (токен загружен)") 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.vk = self.vk_session.get_api()
self.set_ui_state(True) self.set_ui_state(True)
self.load_chats() self.load_chats()
@@ -692,6 +711,11 @@ if __name__ == "__main__":
app.setStyle("Fusion") app.setStyle("Fusion")
app.setPalette(app.style().standardPalette()) 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 = VkChatManager()
window.show() window.show()
sys.exit(app.exec()) 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
)