23 Commits

Author SHA1 Message Date
2eb4c52b81 ci(release): set release title to Anabasis Manager <version>
All checks were successful
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Successful in 2m16s
2026-02-15 17:36:02 +03:00
3d73a504d2 ci(release): use v-prefixed semantic tags 2026-02-15 17:35:37 +03:00
1524271be7 ci(release): write outputs/env as utf8 no bom for powershell
All checks were successful
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Successful in 2m5s
2026-02-15 17:31:37 +03:00
561cf43e09 ci(release): handle missing tag exit code in powershell step
All checks were successful
Desktop CI / tests (push) Successful in 14s
Desktop Release / release (push) Successful in 24s
2026-02-15 17:30:22 +03:00
e8930f7550 ci(release): switch windows release steps from bash to powershell
Some checks failed
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Failing after 1m14s
2026-02-15 17:28:29 +03:00
c8da0f9191 ci(release): use preinstalled Python on self-hosted windows runner
Some checks failed
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Failing after 14s
2026-02-15 17:20:42 +03:00
37ce500fd2 ci(release): remove node bootstrap step, require system node
Some checks failed
Desktop CI / tests (push) Successful in 12s
Desktop Release / release (push) Failing after 25s
2026-02-15 17:19:24 +03:00
098a84e5bd ci(release): restore powershell node bootstrap
Some checks failed
Desktop CI / tests (push) Successful in 12s
Desktop Release / release (push) Failing after 2m42s
2026-02-15 16:42:08 +03:00
5aa17c1a84 ci(release): avoid powershell policy by using cmd node bootstrap
All checks were successful
Desktop CI / tests (push) Successful in 13s
2026-02-15 16:38:40 +03:00
dde14f3714 ci(release): bootstrap node before js-based actions
Some checks failed
Desktop CI / tests (push) Successful in 12s
Desktop Release / release (push) Failing after 5s
2026-02-15 16:37:42 +03:00
fa5d4c6993 ci(release): use windows runner label
Some checks failed
Desktop CI / tests (push) Successful in 20s
Desktop Release / release (push) Failing after 1m21s
2026-02-15 16:35:21 +03:00
f9e0225243 ci: add Gitea CI and desktop release workflow
Some checks failed
Desktop CI / tests (push) Successful in 1m29s
Desktop Release / release (push) Has been cancelled
2026-02-15 15:27:21 +03:00
c42b23bea5 chore(release): bump version to 1.6.1 2026-02-15 15:25:21 +03:00
b52cdea425 feat(update): stage 1 auto-update (one-click) 2026-02-15 15:24:45 +03:00
b7fad78a71 refactor(release): bump to 1.6.0 and unify version source 2026-02-15 15:17:30 +03:00
e590a6cde0 release: 1.5.1 fixes, relogin and updater 2026-02-15 15:13:13 +03:00
aad6e8c5af feat(auth): pywebview вместо webengine
- добавлен auth_webview.py и режим --auth

- build.py обновлён, WebEngine исключён

- pywebview добавлен в requirements
2026-02-04 00:02:32 +03:00
4629037890 feat(app): улучшения UX, логирование и безопасность 2026-02-03 22:26:41 +03:00
ad24cb6fca feat(chat): добавлена функция назначения администраторов чатов
- Добавлено верхнее меню "Инструменты".
- Реализован метод set_user_admin с вызовом API messages.setMemberRole.
- Добавлена конвертация локального chat_id в peer_id (2000000000+id) для корректной работы метода.
- Добавлены диалоги подтверждения и отчет о результатах выполнения.
2026-02-01 05:33:08 +03:00
9b263dd85f feat(build): автоматизация сборки и поддержка бессрочных токенов
- Исправлена ошибка ImportError: QtWebEngineCore путем перехода на PyInstaller.
- Добавлен скрипт build.py для автоматической сборки, очистки DLL и создания ZIP-архива.
- Реализована поддержка бессрочного доступа (offline_access) для VK Access Token.
- Обновлен README.md: добавлены разделы для разработчиков и описание структуры данных.
- Оптимизирован размер билда за счет удаления неиспользуемых библиотек Qt и папок локализации.
2026-02-01 05:32:51 +03:00
d7eaec4ba4 fix(chat): исправление логина и загрузки списков чатов
Signed-off-by: benya <benya@daemonlord.ru>
2026-02-01 05:32:40 +03:00
Alex
30fc78e89b feat(build): Добавлена кросс-платформенная поддержка в setup.py
Модифицирован скрипт сборки на основе cx_Freeze для обеспечения совместимости с основными операционными системами (Windows, macOS, Linux). Ранее скрипт был настроен преимущественно для Windows.

Ключевые изменения:
- **Динамическое имя файла:** Исполняемый файл получает расширение `.exe` только при сборке на Windows.
- **Разделение сборок:** Для каждой целевой ОС создается своя папка (например, `build_linux`), что позволяет хранить сборки для разных систем одновременно.
- **Платформо-зависимые опции:** Учтены особенности сборки для каждой ОС, включая `base="Win32GUI"` для Windows и `base=None` для Linux.

edit: changed .gitignore
2026-02-01 05:32:32 +03:00
Alex
6225fb15d4 feat(ui): массовые операции, вкладки и улучшение UX 2026-02-01 05:31:56 +03:00
12 changed files with 1914 additions and 568 deletions

35
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,35 @@
name: Desktop CI
on:
push:
branches:
- master
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.daemonlord.ru/actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: https://git.daemonlord.ru/actions/setup-python@v5
with:
python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Validate syntax
run: |
python -m py_compile app_version.py main.py build.py tests/test_auth_relogin_smoke.py
- name: Run tests
run: |
python -m unittest tests/test_auth_relogin_smoke.py

View File

@@ -0,0 +1,120 @@
name: Desktop Release
on:
push:
branches:
- master
jobs:
release:
runs-on: windows
steps:
- name: Checkout
uses: https://git.daemonlord.ru/actions/checkout@v4
with:
fetch-depth: 0
tags: true
- name: Ensure Python 3.13
shell: powershell
run: |
if (Get-Command python -ErrorAction SilentlyContinue) {
python --version
} elseif (Get-Command py -ErrorAction SilentlyContinue) {
$pyExe = py -3.13 -c "import sys; print(sys.executable)"
if (-not $pyExe) {
throw "Python 3.13 launcher is available, but interpreter was not found."
}
Split-Path $pyExe | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
python --version
} else {
throw "Python is not installed on runner. Install Python 3.13 and restart runner service."
}
- name: Install dependencies
shell: powershell
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt pyinstaller
- name: Extract app version
id: extract_version
shell: powershell
run: |
$version = (python -c "from app_version import APP_VERSION; print(APP_VERSION)").Trim()
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "version=$version`n", $utf8NoBom)
Write-Host "Detected version: $version"
- name: Stop if version already released
id: stop
shell: powershell
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$tag = "v$version"
git show-ref --tags --quiet --verify "refs/tags/$tag"
$tagExists = ($LASTEXITCODE -eq 0)
$global:LASTEXITCODE = 0
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
if ($tagExists) {
Write-Host "Version $tag already released, stopping job."
[System.IO.File]::AppendAllText($env:GITHUB_ENV, "CONTINUE=false`n", $utf8NoBom)
} else {
Write-Host "Version $tag not released yet, continuing workflow..."
[System.IO.File]::AppendAllText($env:GITHUB_ENV, "CONTINUE=true`n", $utf8NoBom)
}
exit 0
- name: Run tests
if: env.CONTINUE == 'true'
shell: powershell
run: |
python -m py_compile app_version.py main.py build.py tests/test_auth_relogin_smoke.py
python -m unittest tests/test_auth_relogin_smoke.py
- name: Build release zip
if: env.CONTINUE == 'true'
shell: powershell
run: |
python build.py
- name: Ensure archive exists
if: env.CONTINUE == 'true'
shell: powershell
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$archivePath = "dist/AnabasisManager-$version.zip"
if (-not (Test-Path $archivePath)) {
throw "Archive not found: $archivePath"
}
- name: Configure git identity
if: env.CONTINUE == 'true'
shell: powershell
run: |
git config user.name "gitea-actions"
git config user.email "gitea-actions@daemonlord.ru"
- name: Create git tag
if: env.CONTINUE == 'true'
shell: powershell
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$tag = "v$version"
git tag "$tag"
git push origin "$tag"
- name: Create Gitea Release
if: env.CONTINUE == 'true'
uses: https://git.daemonlord.ru/actions/gitea-release-action@v1
with:
server_url: https://git.daemonlord.ru
repository: ${{ gitea.repository }}
token: ${{ secrets.API_TOKEN }}
tag_name: v${{ steps.extract_version.outputs.version }}
name: Anabasis Manager ${{ steps.extract_version.outputs.version }}
body: |
Desktop release v${{ steps.extract_version.outputs.version }}
files: |
dist/AnabasisManager-${{ steps.extract_version.outputs.version }}.zip

14
.gitignore vendored
View File

@@ -1,3 +1,13 @@
/.venv/
/.venv1/
/.venv3/
/setup.py
/build_cx/
/build_linux/
/build_win32/
/build_darwin/
.idea/
__pycache__/
*.py[cod]
tests/__pycache__/
build/
dist/
AnabasisManager.spec

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2025 Aleksandr Denisov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

104
README.md Normal file
View File

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

1
app_version.py Normal file
View File

@@ -0,0 +1 @@
APP_VERSION = "1.6.1"

78
auth_webview.py Normal file
View 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()

108
build.py Normal file
View File

@@ -0,0 +1,108 @@
import os
import shutil
import subprocess
import sys
from app_version import APP_VERSION
# --- Конфигурация ---
APP_NAME = "AnabasisManager"
VERSION = APP_VERSION # Единая версия приложения
MAIN_SCRIPT = "main.py"
ICON_PATH = "icon.ico"
DIST_DIR = os.path.join("dist", APP_NAME)
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():
print(f"--- 1. Запуск PyInstaller для {APP_NAME} v{VERSION} ---")
command = [
"pyinstaller",
"--noconfirm",
"--onedir",
"--windowed",
"--exclude-module", "PySide6.QtWebEngineCore",
"--exclude-module", "PySide6.QtWebEngineWidgets",
"--exclude-module", "PySide6.QtWebEngineQuick",
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 "",
f"--add-data=auth_webview.py{os.pathsep}.",
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
for item in REMOVE_LIST:
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__":
ensure_project_root()
# Предварительная очистка
for folder in ["build", "dist"]:
if os.path.exists(folder):
shutil.rmtree(folder)
run_build()
run_cleanup()
create_archive()
print("\n" + "=" * 30)
print("ПРОЦЕСС ЗАВЕРШЕН")
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
print("=" * 30)

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

1942
main.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,3 @@
PySide6~=6.9.1
PySide6~=6.10.2
vk-api~=11.9.9
pywebview

View File

@@ -0,0 +1,51 @@
import unittest
from pathlib import Path
class AuthReloginSmokeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.source = Path("main.py").read_text(encoding="utf-8")
def test_auth_command_builder_handles_frozen_and_source(self):
self.assertIn("def _build_auth_command(self, auth_url, output_path):", self.source)
self.assertIn('return sys.executable, ["--auth", auth_url, output_path]', self.source)
self.assertIn('return sys.executable, [os.path.abspath(__file__), "--auth", auth_url, output_path]', self.source)
def test_auth_runs_via_qprocess(self):
self.assertIn("process = QProcess(self)", self.source)
self.assertIn("process.start(program, args)", self.source)
self.assertIn("def _on_auth_process_finished(self, exit_code, _exit_status):", self.source)
self.assertIn("if self.auth_process and self.auth_process.state() == QProcess.NotRunning:", self.source)
def test_force_relogin_has_backoff_and_event_log(self):
self.assertIn("AUTH_RELOGIN_BACKOFF_SECONDS = 5.0", self.source)
self.assertIn("if self._auth_relogin_in_progress:", self.source)
self.assertIn("force_relogin_backoff", self.source)
self.assertIn("force_relogin", self.source)
def test_auth_error_paths_trigger_force_relogin(self):
self.assertIn("def _handle_vk_api_error(self, context, exc, action_name=None, ui_message_prefix=None, disable_ui=False):", self.source)
self.assertIn("self._force_relogin(exc, action_name or context)", self.source)
self.assertIn('"load_chats",', self.source)
self.assertIn('"execute_user_action",', self.source)
self.assertIn('"set_user_admin",', self.source)
def test_tab_checkbox_lists_use_existing_attributes(self):
self.assertIn("self.warehouse_chat_checkboxes", self.source)
self.assertIn("self.coffee_chat_checkboxes", self.source)
self.assertNotIn("self.retail_warehouse_checkboxes", self.source)
self.assertNotIn("self.retail_coffee_checkboxes", self.source)
def test_update_check_actions_exist(self):
self.assertIn("from app_version import APP_VERSION", self.source)
self.assertIn("UPDATE_REPOSITORY = ", self.source)
self.assertIn('QAction("Проверить обновления", self)', self.source)
self.assertIn("def check_for_updates(self, silent_no_updates=False):", self.source)
self.assertIn("class UpdateChecker(QObject):", self.source)
self.assertIn('message_box.addButton("Обновить сейчас", QMessageBox.AcceptRole)', self.source)
self.assertIn("def _start_auto_update(self, download_url, latest_version):", self.source)
if __name__ == "__main__":
unittest.main()