Add VK callback auth support and admin demotion
This commit is contained in:
156
main.py
156
main.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user