11 Commits

Author SHA1 Message Date
e97d346682 feat: добавлена функция назначения администраторов чатов
- Добавлено верхнее меню "Инструменты".
- Реализован метод set_user_admin с вызовом API messages.setMemberRole.
- Добавлена конвертация локального chat_id в peer_id (2000000000+id) для корректной работы метода.
- Добавлены диалоги подтверждения и отчет о результатах выполнения.
2026-01-22 12:55:49 +03:00
3ed5bba9af feat: автоматизация сборки и поддержка бессрочных токенов
- Исправлена ошибка ImportError: QtWebEngineCore путем перехода на PyInstaller.
- Добавлен скрипт build.py для автоматической сборки, очистки DLL и создания ZIP-архива.
- Реализована поддержка бессрочного доступа (offline_access) для VK Access Token.
- Обновлен README.md: добавлены разделы для разработчиков и описание структуры данных.
- Оптимизирован размер билда за счет удаления неиспользуемых библиотек Qt и папок локализации.
2026-01-13 02:38:26 +03:00
1eab8651f2 Исправление логина в чатах, добавлены новые списки чатов для Склада и Кофеен
Signed-off-by: benya <benya@daemonlord.ru>
2025-12-25 15:59:03 +03:00
Alex
ea188ffc13 edit: changed .gitignore 2025-07-24 23:46:13 +03:00
Alex
6aa50b03da feat(build): Добавлена кросс-платформенная поддержка в setup.py
Модифицирован скрипт сборки на основе cx_Freeze для обеспечения совместимости с основными операционными системами (Windows, macOS, Linux). Ранее скрипт был настроен преимущественно для Windows.

Ключевые изменения:
- **Динамическое имя файла:** Исполняемый файл получает расширение `.exe` только при сборке на Windows.
- **Разделение сборок:** Для каждой целевой ОС создается своя папка (например, `build_linux`), что позволяет хранить сборки для разных систем одновременно.
- **Платформо-зависимые опции:** Учтены особенности сборки для каждой ОС, включая `base="Win32GUI"` для Windows и `base=None` для Linux.
2025-07-24 23:43:27 +03:00
Alex
86aa2ddc1a Merge remote-tracking branch 'origin/master' 2025-07-24 23:04:51 +03:00
Alex
32e30f5484 feat: Добавлены массовые операции и разделение чатов на вкладки
- Добавлена кнопка "Список" для одновременной обработки нескольких пользователей.
- Реализовано разделение чатов на вкладки "Офис", "Розница" и "Прочие".

fix: Улучшен интерфейс и исправлены ошибки

- Область списка чатов теперь корректно увеличивается после авторизации.
- В окне подтверждения отображаются имена всех пользователей.
- Исправлены грамматика и склонения в диалоговых окнах.
- Кнопки в диалогах заменены на русскоязычные ("Да"/"Нет", "ОК"/"Отмена").
2025-07-24 23:01:59 +03:00
benya
194d696430 Обновить README.md 2025-06-27 23:49:33 +00:00
Alex
0dbd71c036 Добавлены README и LICENSE 2025-06-28 02:48:28 +03:00
Alex
134aa88f22 Добавлены параметры для сборки программы в исполняемый файл 2025-06-28 02:15:41 +03:00
Alex
0c270a6cb1 - Улучшена верстка UI, устранено перекрытие элементов.
- UI был упрощён
- Добавлено автоматическое получение ID пользователя из VK-ссылки.
- Улучшена обработка ошибок VK API при смене IP-адреса.
- Добавлено отображение имени пользователя в диалогах подтверждения.
- Удалены неиспользуемые импорты и обновлены вызовы методов Qt.
- Добавлено сохранение данных браузера (включая куки) через QWebEngineProfile для поддержания сессии в WebEngine.
2025-06-28 02:03:50 +03:00
9 changed files with 214 additions and 1400 deletions

View File

@@ -1,35 +0,0 @@
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

@@ -1,133 +0,0 @@
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: Generate SHA256 checksum
if: env.CONTINUE == 'true'
shell: powershell
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$archiveName = "AnabasisManager-$version.zip"
$archivePath = "dist/$archiveName"
$checksumPath = "dist/$archiveName.sha256"
$hash = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower()
"$hash $archiveName" | Set-Content -Path $checksumPath -Encoding UTF8
Write-Host "Checksum created: $checksumPath"
- 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
dist/AnabasisManager-${{ steps.extract_version.outputs.version }}.zip.sha256

7
.gitignore vendored
View File

@@ -4,10 +4,3 @@
/build_linux/ /build_linux/
/build_win32/ /build_win32/
/build_darwin/ /build_darwin/
.idea/
__pycache__/
*.py[cod]
tests/__pycache__/
build/
dist/
AnabasisManager.spec

View File

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

View File

@@ -1,78 +0,0 @@
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()

View File

@@ -2,32 +2,14 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
from app_version import APP_VERSION
# --- Конфигурация --- # --- Конфигурация ---
APP_NAME = "AnabasisManager" APP_NAME = "AnabasisManager"
VERSION = APP_VERSION # Единая версия приложения VERSION = "1.3" # Ваша версия
MAIN_SCRIPT = "main.py" MAIN_SCRIPT = "main.py"
ICON_PATH = "icon.ico" ICON_PATH = "icon.ico"
DIST_DIR = os.path.join("dist", APP_NAME) DIST_DIR = os.path.join("dist", APP_NAME)
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия 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(): def run_build():
@@ -38,14 +20,12 @@ def run_build():
"--noconfirm", "--noconfirm",
"--onedir", "--onedir",
"--windowed", "--windowed",
"--exclude-module", "PySide6.QtWebEngineCore",
"--exclude-module", "PySide6.QtWebEngineWidgets",
"--exclude-module", "PySide6.QtWebEngineQuick",
f"--name={APP_NAME}", f"--name={APP_NAME}",
f"--icon={ICON_PATH}" if os.path.exists(ICON_PATH) else "", 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={ICON_PATH}{os.pathsep}." if os.path.exists(ICON_PATH) else "",
f"--add-data=auth_webview.py{os.pathsep}.", "--collect-all", "PySide6.QtWebEngineCore",
MAIN_SCRIPT "--collect-all", "PySide6.QtWebEngineWidgets",
MAIN_SCRIPT
] ]
command = [arg for arg in command if arg] command = [arg for arg in command if arg]
@@ -66,7 +46,16 @@ def run_cleanup():
if not os.path.exists(pyside_path): if not os.path.exists(pyside_path):
pyside_path = DIST_DIR pyside_path = DIST_DIR
for item in REMOVE_LIST: 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) path = os.path.join(pyside_path, item)
if os.path.exists(path): if os.path.exists(path):
try: try:
@@ -92,7 +81,6 @@ def create_archive():
if __name__ == "__main__": if __name__ == "__main__":
ensure_project_root()
# Предварительная очистка # Предварительная очистка
for folder in ["build", "dist"]: for folder in ["build", "dist"]:
if os.path.exists(folder): if os.path.exists(folder):
@@ -103,6 +91,6 @@ if __name__ == "__main__":
create_archive() create_archive()
print("\n" + "=" * 30) print("\n" + "=" * 30)
print("ПРОЦЕСС ЗАВЕРШЕН") print(f"ПРОЦЕСС ЗАВЕРШЕН")
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip") print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
print("=" * 30) print("=" * 30)

1253
main.py

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,56 +0,0 @@
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, checksum_url=\"\", download_name=\"\"):", self.source)
self.assertIn("def _verify_update_checksum(self, zip_path, checksum_url, download_name):", self.source)
self.assertIn("def _build_update_script(self, app_dir, source_dir, exe_name, target_pid):", self.source)
self.assertIn("set TARGET_PID=", self.source)
self.assertIn("set BACKUP_DIR=", self.source)
self.assertIn(":rollback", self.source)
if __name__ == "__main__":
unittest.main()