diff --git a/README.md b/README.md index fda165a..89e1569 100644 --- a/README.md +++ b/README.md @@ -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 ``` -## Использование +4. **Запустите приложение:** + ```bash + python main.py + ``` -1. **Запустите приложение:** - * *Готовый билд:* - * Запустите **AnabasisHRChatManager.exe** - * *Ручная установка:* - * ```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. \ No newline at end of file + Создает готовый ZIP-архив с актуальной версией в названии. + +Хранение данных + +Приложение использует системные папки AppData для изоляции пользовательских данных: + + Windows: %APPDATA%/AnabasisVKChatManager + + macOS: ~/Library/Application Support/AnabasisVKChatManager + +В этих папках хранятся token.json (доступ к API) и web_engine_cache/ (сессия браузера). + +--- + +## 📜 Лицензия + +--- +Проект распространяется под лицензией MIT. + +Сэкономьте часы ручного труда с Anabasis VK Chat Manager. \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..6eb5be3 --- /dev/null +++ b/build.py @@ -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) \ No newline at end of file diff --git a/main.py b/main.py index c826343..ab87f19 100644 --- a/main.py +++ b/main.py @@ -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()) \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index d9da717..0000000 --- a/setup.py +++ /dev/null @@ -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 -) \ No newline at end of file