Add VK callback auth support and admin demotion
Some checks are pending
Desktop Release / release (push) Waiting to run
Desktop CI / tests (push) Successful in 1m51s

This commit is contained in:
Денисов Александр Андреевич
2026-06-05 19:01:52 +03:00
parent 5a3e4c188e
commit 0a82ad7e3e
10 changed files with 520 additions and 42 deletions

156
main.py
View File

@@ -3,6 +3,7 @@ import os
import shutil
import sys
import time
from urllib.parse import urlencode
from PySide6.QtCore import QProcess
from PySide6.QtCore import QStandardPaths
@@ -38,18 +39,24 @@ APP_DATA_DIR = os.path.join(
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
SETTINGS_FILE = os.path.join(APP_DATA_DIR, "settings.json")
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache")
AUTH_WEBVIEW_PROFILE_DIR = os.path.join(APP_DATA_DIR, "webview_profile")
CACHE_CLEANUP_MARKER = os.path.join(APP_DATA_DIR, "pending_cache_cleanup")
LOG_FILE = os.path.join(APP_DATA_DIR, "app.log")
LOG_MAX_BYTES = 1024 * 1024 # 1 MB
LOG_BACKUP_FILE = os.path.join(APP_DATA_DIR, "app.log.1")
AUTH_RELOGIN_BACKOFF_SECONDS = 5.0
VK_APP_ID = "54454043"
VK_AUTH_SCOPE = "1073737727"
VK_API_VERSION = "5.131"
VK_AUTH_REDIRECT_URI = os.getenv("ANABASIS_VK_REDIRECT_URI", "https://vk.daemonlord.ru/vk/callback").strip() or "https://vk.daemonlord.ru/vk/callback"
# Legacy owner/repo format for GitHub-only fallback.
UPDATE_REPOSITORY = ""
# Full repository URL is preferred (supports GitHub/Gitea).
UPDATE_REPOSITORY_URL = "https://git.daemonlord.ru/benya/AnabasisChatRemove"
UPDATE_CHANNEL_DEFAULT = "stable"
UPDATE_REQUEST_TIMEOUT = 8
AUTH_ERROR_CONTEXTS = ("load_chats", "execute_user_action", "set_user_admin")
AUTH_ERROR_CONTEXTS = ("load_chats", "execute_user_action", "set_user_admin", "unset_user_admin")
VK_AUTH_URL_OVERRIDE = ""
def get_resource_path(relative_path):
@@ -80,8 +87,14 @@ class BulkActionWorker(QObject):
def _is_auth_error(exc):
return VkService.is_auth_error(exc, str(exc).lower())
def _role_context(self):
return "unset_user_admin" if self.action_type == "unadmin" else "set_user_admin"
def _role_action_name(self):
return "разжалования администраторов" if self.action_type == "unadmin" else "назначения администраторов"
def _emit_progress(self, processed):
label = "admin" if self.action_type == "admin" else ("remove" if self.action_type == "remove" else "add")
label = self.action_type if self.action_type in ("remove", "add", "admin", "unadmin") else "add"
self.progress.emit(processed, self.total, label)
def run(self):
@@ -90,7 +103,7 @@ class BulkActionWorker(QObject):
try:
for chat in self.selected_chats:
peer_id = None
if self.action_type == "admin":
if self.action_type in ("admin", "unadmin", "remove"):
try:
peer_id = 2000000000 + int(chat["id"])
except (ValueError, TypeError):
@@ -103,6 +116,19 @@ class BulkActionWorker(QObject):
for user_id, user_info in self.user_infos.items():
try:
if self.action_type == "remove":
try:
self.vk_call_with_retry(
self.vk.messages.setMemberRole,
peer_id=peer_id,
member_id=user_id,
role="member",
)
results.append(f"'{user_info}' разжалован перед исключением из '{chat['title']}'.")
except VkApiError as demote_exc:
if self._is_auth_error(demote_exc):
self.auth_error.emit("unset_user_admin", demote_exc, "разжалования администратора перед исключением")
return
results.append(f"Не удалось разжаловать '{user_info}' в '{chat['title']}': {demote_exc}")
self.vk_call_with_retry(self.vk.messages.removeChatUser, chat_id=chat["id"], member_id=user_id)
results.append(f"'{user_info}' исключен из '{chat['title']}'.")
elif self.action_type == "add":
@@ -119,12 +145,20 @@ class BulkActionWorker(QObject):
role="admin",
)
results.append(f"'{user_info}' назначен админом в '{chat['title']}'.")
elif self.action_type == "unadmin":
self.vk_call_with_retry(
self.vk.messages.setMemberRole,
peer_id=peer_id,
member_id=user_id,
role="member",
)
results.append(f"'{user_info}' разжалован в '{chat['title']}'.")
else:
raise RuntimeError(f"Unknown action: {self.action_type}")
except VkApiError as exc:
if self._is_auth_error(exc):
context = "set_user_admin" if self.action_type == "admin" else "execute_user_action"
action_name = "назначения администраторов" if self.action_type == "admin" else "выполнения операций с пользователями"
context = self._role_context() if self.action_type in ("admin", "unadmin") else "execute_user_action"
action_name = self._role_action_name() if self.action_type in ("admin", "unadmin") else "выполнения операций с пользователями"
self.auth_error.emit(context, exc, action_name)
return
results.append(f"✗ Ошибка для '{user_info}' в '{chat['title']}': {exc}")
@@ -327,6 +361,12 @@ class VkChatManager(QMainWindow):
tools_menu.addAction(make_admin_action)
self.make_admin_action = make_admin_action
unset_admin_action = QAction("Разжаловать администратора", self)
unset_admin_action.setStatusTip("Снять права администратора с выбранных пользователей в выбранных чатах")
unset_admin_action.triggered.connect(self.unset_user_admin)
tools_menu.addAction(unset_admin_action)
self.unset_admin_action = unset_admin_action
check_updates_action = QAction("Проверить обновления", self)
check_updates_action.setStatusTip("Проверить наличие новой версии приложения")
check_updates_action.triggered.connect(self.check_for_updates)
@@ -629,6 +669,8 @@ class VkChatManager(QMainWindow):
self._clear_chat_tabs()
if hasattr(self, "make_admin_action"):
self.make_admin_action.setEnabled(authorized and (not self._auth_ui_busy))
if hasattr(self, "unset_admin_action"):
self.unset_admin_action.setEnabled(authorized and (not self._auth_ui_busy))
if hasattr(self, "logout_action"):
self.logout_action.setEnabled(not self._auth_ui_busy)
@@ -639,6 +681,8 @@ class VkChatManager(QMainWindow):
self.logout_action.setEnabled(not in_progress)
if hasattr(self, "make_admin_action"):
self.make_admin_action.setEnabled(not in_progress and self.token is not None)
if hasattr(self, "unset_admin_action"):
self.unset_admin_action.setEnabled(not in_progress and self.token is not None)
self.auth_btn.setEnabled((self.token is None) and (not in_progress))
def _set_busy(self, busy, status_text=None):
@@ -702,7 +746,12 @@ class VkChatManager(QMainWindow):
clear_inputs_on_success=True,
):
total = max(1, len(selected_chats) * len(user_infos))
self._bulk_action_context = "set_user_admin" if action_type == "admin" else "execute_user_action"
if action_type == "admin":
self._bulk_action_context = "set_user_admin"
elif action_type == "unadmin":
self._bulk_action_context = "unset_user_admin"
else:
self._bulk_action_context = "execute_user_action"
self._bulk_action_success_message_title = success_message_title
self._bulk_clear_inputs_on_success = clear_inputs_on_success
self._log_event(
@@ -737,7 +786,12 @@ class VkChatManager(QMainWindow):
thread.start()
def _on_bulk_action_progress(self, processed, total, label):
label_text = {"remove": "исключение", "add": "приглашение", "admin": "назначение админов"}.get(label, label)
label_text = {
"remove": "исключение",
"add": "приглашение",
"admin": "назначение админов",
"unadmin": "разжалование админов",
}.get(label, label)
self._update_operation_progress(processed, total, label_text)
self.status_label.setText(f"Статус: выполняется {label_text} ({processed}/{max(1, total)})...")
@@ -855,18 +909,23 @@ class VkChatManager(QMainWindow):
print(f"Ошибка отложенной очистки кэша: {e}")
def _try_remove_web_cache(self):
if not os.path.exists(WEB_ENGINE_CACHE_DIR):
cache_dirs = [WEB_ENGINE_CACHE_DIR, AUTH_WEBVIEW_PROFILE_DIR]
existing_dirs = [path for path in cache_dirs if os.path.exists(path)]
if not existing_dirs:
return
attempts = 5
last_error = None
for _ in range(attempts):
try:
shutil.rmtree(WEB_ENGINE_CACHE_DIR)
last_error = None
for path in existing_dirs:
for _ in range(attempts):
try:
shutil.rmtree(path)
last_error = None
break
except Exception as e:
last_error = e
time.sleep(0.2)
if last_error:
break
except Exception as e:
last_error = e
time.sleep(0.2)
if last_error:
os.makedirs(APP_DATA_DIR, exist_ok=True)
with open(CACHE_CLEANUP_MARKER, "w") as f:
@@ -890,6 +949,19 @@ class VkChatManager(QMainWindow):
def _build_auth_command(self, auth_url, output_path):
return self.vk_service.build_auth_command(auth_url, output_path, entry_script_path=os.path.abspath(__file__))
def _build_vk_auth_url(self, display="mobile"):
if VK_AUTH_URL_OVERRIDE:
return VK_AUTH_URL_OVERRIDE
params = {
"client_id": VK_APP_ID,
"display": display,
"redirect_uri": VK_AUTH_REDIRECT_URI,
"scope": VK_AUTH_SCOPE,
"response_type": "token",
"v": VK_API_VERSION,
}
return f"https://oauth.vk.com/authorize?{urlencode(params)}"
def _on_auth_process_error(self, process_error):
self._auth_process_error_text = f"Ошибка запуска авторизации: {process_error}"
# For failed starts Qt may not emit finished(), so release UI here.
@@ -965,15 +1037,7 @@ class VkChatManager(QMainWindow):
status_text = "Статус: ожидание авторизации..."
self.status_label.setText(status_text)
auth_url = (
"https://oauth.vk.com/authorize?"
"client_id=2685278&"
"display=page&"
"redirect_uri=https://oauth.vk.com/blank.html&"
"scope=1073737727&"
"response_type=token&"
"v=5.131"
)
auth_url = self._build_vk_auth_url(display="mobile")
output_path = os.path.join(APP_DATA_DIR, "auth_result.json")
try:
if os.path.exists(output_path):
@@ -1269,6 +1333,50 @@ class VkChatManager(QMainWindow):
)
return
def unset_user_admin(self):
"""Снимает права администратора в выбранных чатах."""
if not self.user_ids_to_process:
QMessageBox.warning(self, "Ошибка", "Нет ID пользователей для операции.")
return
selected_chats = self._get_selected_chats()
if not selected_chats:
QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат.")
return
user_infos = {uid: self.get_user_info_by_id(uid) for uid in self.user_ids_to_process}
user_names_str = "\n".join([f"{name}" for name in user_infos.values()])
msg = (
f"Вы уверены, что хотите разжаловать следующих администраторов:\n\n"
f"{user_names_str}\n\n"
f"в {len(selected_chats)} выбранных чатах?"
)
confirm_dialog = QMessageBox(self)
confirm_dialog.setWindowTitle("Подтверждение прав")
confirm_dialog.setText(msg)
confirm_dialog.setIcon(QMessageBox.Icon.Question)
yes_button = confirm_dialog.addButton("Да", QMessageBox.ButtonRole.YesRole)
no_button = confirm_dialog.addButton("Нет", QMessageBox.ButtonRole.NoRole)
confirm_dialog.setDefaultButton(no_button)
confirm_dialog.exec()
if confirm_dialog.clickedButton() != yes_button:
return
self._start_bulk_action_worker(
action_type="unadmin",
selected_chats=selected_chats,
user_infos=user_infos,
action_label="разжалование админов",
action_button=None,
success_message_title="Результаты разжалования",
clear_inputs_on_success=True,
)
return
# Refactor overrides: keep logic in service modules and thin UI orchestration here.
def _process_links_list(self, links_list):
if not self.vk: