Files
AnabasisChatRemove/main.py
Alex 0c270a6cb1 - Улучшена верстка UI, устранено перекрытие элементов.
- UI был упрощён
- Добавлено автоматическое получение ID пользователя из VK-ссылки.
- Улучшена обработка ошибок VK API при смене IP-адреса.
- Добавлено отображение имени пользователя в диалогах подтверждения.
- Удалены неиспользуемые импорты и обновлены вызовы методов Qt.
- Добавлено сохранение данных браузера (включая куки) через QWebEngineProfile для поддержания сессии в WebEngine.
2025-06-28 02:03:50 +03:00

811 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import vk_api
import json
import time
import os
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QWidget, QMessageBox,
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout, QSizePolicy, QDialog)
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile # Добавлен QWebEngineProfile
from urllib.parse import urlparse, parse_qs, unquote
from vk_api.exceptions import VkApiError
from PySide6.QtCore import QStandardPaths
# --- Управление токенами и настройками ---
APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation), "AnabasisVKChatManager")
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
# Директория для сохранения данных веб-движка (куки, кэш и т.д.)
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache")
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
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()}")
except IOError as e:
print(f"Ошибка сохранения токена: {e}")
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()}")
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}")
return None, None
class WebEnginePage(QWebEnginePage):
"""
Класс для обработки навигационных запросов в QWebEngineView,
специально для извлечения токена авторизации VK.
"""
# Добавлен параметр profile в конструктор, чтобы использовать пользовательский профиль
def __init__(self, profile=None, parent=None, browser_window_instance=None):
super().__init__(profile, parent) # Передаем profile в базовый класс
self.parent_browser_window = browser_window_instance
self.token_extracted = False
def acceptNavigationRequest(self, url, _type, isMainFrame):
"""
Переопределенный метод для перехвата URL-адреса, содержащего токен доступа.
"""
url_string = url.toString()
if "access_token" in url_string and not self.token_extracted:
self.token_extracted = True
if self.parent_browser_window:
self.parent_browser_window.process_auth_url(url_string)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
class AuthBrowserWindow(QDialog):
"""
Отдельное окно-диалог для проведения OAuth авторизации VK.
"""
token_extracted_signal = Signal(str, int)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Авторизация VK")
self.setGeometry(350, 350, 800, 600)
layout = QVBoxLayout(self)
self.browser = QWebEngineView()
# Настройка QWebEngineProfile для сохранения куки и других данных
os.makedirs(WEB_ENGINE_CACHE_DIR, exist_ok=True)
# ИСПРАВЛЕНО: Сделать profile атрибутом экземпляра и дать ему self как родителя
self.profile = QWebEngineProfile("AnabasisVKWebProfile", self)
self.profile.setPersistentCookiesPolicy(QWebEngineProfile.AllowPersistentCookies)
self.profile.setPersistentStoragePath(WEB_ENGINE_CACHE_DIR)
# Создаем страницу с настроенным профилем
self.browser.page = WebEnginePage(profile=self.profile, parent=self.browser, browser_window_instance=self)
self.browser.setPage(self.browser.page)
layout.addWidget(self.browser)
self.status_label = QLabel("Ожидание авторизации...")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
def start_auth_flow(self):
"""Запускает OAuth авторизацию VK в этом окне."""
auth_url = (
"https://oauth.vk.com/authorize?"
"client_id=6287487&"
"display=page&"
"redirect_uri=https://oauth.vk.com/blank.html&"
"scope=1073737727&"
"response_type=token&"
"v=5.131"
)
self.browser.setUrl(QUrl(auth_url))
self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...")
def process_auth_url(self, url_string):
"""
Извлекает токен доступа из URL перенаправления и испускает сигнал.
"""
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)
if token:
self.token_extracted_signal.emit(token, expires_in)
self.accept()
else:
QMessageBox.warning(self, "Ошибка Авторизации",
"Не удалось получить токен. Проверьте URL или попробуйте еще раз.")
self.reject()
def closeEvent(self, event):
"""
Переопределенный метод для обработки закрытия окна.
Выполняет очистку ресурсов QWebEngine, чтобы избежать предупреждения
"Release of profile requested but WebEnginePage still not deleted. Expect troubles!".
"""
current_page = self.browser.page()
if current_page:
# Снимаем страницу с QWebEngineView, чтобы View перестала ей владеть
self.browser.setPage(None)
# Планируем удаление объекта страницы
current_page.deleteLater()
# Вызываем метод базового класса для корректного закрытия диалога
super().closeEvent(event)
class VkChatManager(QMainWindow):
"""
Главное окно приложения VK Chat Manager.
Позволяет авторизоваться через VK и удалять/добавлять пользователей в чаты.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Anabasis Chat Manager")
self.setGeometry(300, 300, 600, 800)
self.token = None
self.token_expiration_time = None
self.chats = []
self.chat_checkboxes = []
self.vk_session = None
self.vk = None
self.user_id_to_process = None # Новое поле для хранения ID пользователя из ссылки
self.init_ui()
self.load_saved_token_on_startup()
self.setup_token_timer()
def init_ui(self):
"""Инициализирует пользовательский интерфейс приложения."""
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
self.instructions = QTextBrowser()
self.instructions.setPlainText(
"Инструкция:\n"
"1. Нажмите 'Авторизоваться через VK'\n"
"2. В открывшемся окне войдите в свой аккаунт VK\n"
"3. Разрешите доступ приложению\n"
"4. Токен автоматически сохранится на 1 час\n"
"5. Выберите один или несколько чатов, установив галочки\n"
"6. Введите или вставьте ссылку на страницу пользователя VK (например, vk.com/id123 или vk.com/durov). ID будет получен автоматически.\n"
"7. Нажмите соответствующую кнопку 'ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ' или 'ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ'."
)
self.instructions.setFixedHeight(180)
layout.addWidget(self.instructions)
layout.addWidget(QLabel("Access Token VK:"))
self.token_input = QLineEdit()
self.token_input.setPlaceholderText("Токен появится здесь после авторизации...")
self.token_input.setReadOnly(True)
layout.addWidget(self.token_input)
self.token_timer_label = QLabel("Срок действия токена: Н")
self.token_timer_label.setAlignment(Qt.AlignRight)
self.token_timer_label.setStyleSheet("font-weight: bold; color: #555;")
layout.addWidget(self.token_timer_label)
self.auth_btn = QPushButton("Авторизоваться через VK")
self.auth_btn.clicked.connect(self.start_auth)
layout.addWidget(self.auth_btn)
layout.addWidget(QLabel("Выберите чаты:"))
self.chat_checkbox_layout = QVBoxLayout()
self.chat_checkbox_layout.setSpacing(2)
self.chat_checkbox_widget = QWidget()
self.chat_checkbox_widget.setLayout(self.chat_checkbox_layout)
self.chat_scroll_area = QScrollArea()
self.chat_scroll_area.setWidgetResizable(True)
self.chat_scroll_area.setWidget(self.chat_checkbox_widget)
self.chat_scroll_area.setFixedHeight(240)
self.chat_scroll_area.hide()
self.select_all_btn = QPushButton("Выбрать все")
self.select_all_btn.clicked.connect(lambda: self.set_all_checkboxes(True))
self.select_all_btn.setEnabled(False)
self.deselect_all_btn = QPushButton("Снять выбор со всех")
self.deselect_all_btn.clicked.connect(lambda: self.set_all_checkboxes(False))
self.deselect_all_btn.setEnabled(False)
self.refresh_chats_btn = QPushButton("Обновить чаты")
self.refresh_chats_btn.clicked.connect(self.load_chats)
self.refresh_chats_btn.setEnabled(False)
select_buttons_layout = QHBoxLayout()
select_buttons_layout.addWidget(self.select_all_btn)
select_buttons_layout.addWidget(self.deselect_all_btn)
select_buttons_layout.addWidget(self.refresh_chats_btn)
layout.addLayout(select_buttons_layout)
layout.addWidget(self.chat_scroll_area)
layout.addWidget(QLabel("Введите или вставьте ссылку на страницу VK (ID будет получен автоматически):"))
self.vk_url_input = QLineEdit()
self.vk_url_input.setPlaceholderText("Например: vk.com/id123 или vk.com/durov")
self.vk_url_input.textChanged.connect(self.on_vk_url_input_changed) # Подключаем сигнал
layout.addWidget(self.vk_url_input)
# Кнопки для исключения/приглашения пользователя
self.remove_user_btn = QPushButton("ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ")
self.remove_user_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.remove_user_btn.setMinimumHeight(60)
self.remove_user_btn.clicked.connect(self.remove_user)
self.remove_user_btn.setEnabled(False) # Изначально отключена
layout.addWidget(self.remove_user_btn)
self.visible_messages_checkbox = QCheckBox("Показать 250 последних сообщений при добавлении")
self.visible_messages_checkbox.setChecked(False)
layout.addWidget(self.visible_messages_checkbox)
self.add_user_btn = QPushButton("ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ")
self.add_user_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.add_user_btn.setMinimumHeight(60)
self.add_user_btn.clicked.connect(self.add_user_to_chat)
self.add_user_btn.setEnabled(False) # Изначально отключена
layout.addWidget(self.add_user_btn)
self.status_label = QLabel("Статус: не авторизован")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
layout.addStretch(1)
central_widget.setLayout(layout)
def setup_token_timer(self):
"""Настраивает QTimer для обновления отображения обратного отсчета токена."""
self.token_countdown_timer = QTimer(self)
self.token_countdown_timer.timeout.connect(self.update_token_timer_display)
self.token_countdown_timer.start(1000)
def update_token_timer_display(self):
"""
Обновляет QLabel с оставшимся временем до истечения срока действия токена.
"""
if self.token_expiration_time is None:
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 and self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
self.auth_btn.setEnabled(True)
self.set_ui_state(False)
self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.")
self.token = None
self.token_expiration_time = None
self.token_input.clear()
return
minutes, seconds = divmod(remaining_seconds, 60)
hours, minutes = divmod(minutes, 60)
time_str = ""
if hours > 0:
time_str += f"{hours:02d}ч "
if minutes > 0 or hours > 0:
time_str += f"{minutes:02d}м "
time_str += f"{seconds:02d}с"
self.token_timer_label.setText(f"Срок действия токена: {time_str}")
def set_ui_state(self, authorized):
"""Устанавливает состояние элементов пользовательского интерфейса в зависимости от статуса авторизации."""
self.select_all_btn.setEnabled(authorized)
self.deselect_all_btn.setEnabled(authorized)
self.refresh_chats_btn.setEnabled(authorized)
self.vk_url_input.setEnabled(authorized)
# Состояние кнопок теперь зависит от наличия user_id_to_process
self.remove_user_btn.setEnabled(authorized and self.user_id_to_process is not None)
self.visible_messages_checkbox.setEnabled(authorized)
self.add_user_btn.setEnabled(authorized and self.user_id_to_process is not None)
if authorized:
self.chat_scroll_area.show()
else:
self.chat_scroll_area.hide()
def load_saved_token_on_startup(self):
"""Пытается загрузить сохраненный токен при запуске приложения."""
loaded_token, expiration_time = load_token()
if loaded_token:
self.token = loaded_token
self.token_expiration_time = expiration_time
self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован (токен загружен из файла)")
self.auth_btn.setEnabled(False)
self.set_ui_state(True)
self.update_token_timer_display()
self.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api()
self.load_chats()
else:
self.status_label.setText("Статус: не авторизован (токен не найден или просрочен)")
self.set_ui_state(False)
self.update_token_timer_display()
def set_all_checkboxes(self, checked):
"""
Устанавливает состояние (выбран/не выбран) для всех чекбоксов чатов.
"""
for checkbox in self.chat_checkboxes:
checkbox.setChecked(checked)
def start_auth(self):
"""
Запускает процесс OAuth авторизации VK в новом окне.
"""
self.status_label.setText("Статус: ожидание авторизации в новом окне...")
auth_window = AuthBrowserWindow(self)
auth_window.token_extracted_signal.connect(self.handle_auth_token)
auth_window.start_auth_flow()
auth_window.exec() # Изменено с exec_() на exec()
def handle_auth_token(self, token, expires_in):
"""
Обрабатывает полученный токен авторизации после закрытия окна браузера.
"""
if token:
self.token = token
self.token_expiration_time = time.time() + expires_in
save_token(self.token, expires_in)
self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован")
self.auth_btn.setEnabled(False)
self.set_ui_state(True)
self.update_token_timer_display()
self.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api()
self.load_chats()
else:
QMessageBox.warning(self, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.")
self.status_label.setText("Статус: Авторизация не удалась")
self.set_ui_state(False)
self.update_token_timer_display()
def load_chats(self):
"""
Загружает список чатов пользователя из VK API и заполняет их чекбоксами.
"""
for i in reversed(range(self.chat_checkbox_layout.count())):
widget_to_remove = self.chat_checkbox_layout.itemAt(i).widget()
if widget_to_remove:
widget_to_remove.setParent(None)
widget_to_remove.deleteLater()
self.chat_checkboxes.clear()
self.chats.clear()
try:
conversations = self.vk.messages.getConversations(count=200)['items']
for conv in conversations:
if conv['conversation']['peer']['type'] == 'chat':
chat_id = conv['conversation']['peer']['local_id']
title = conv['conversation']['chat_settings']['title']
chat_data = {'id': chat_id, 'title': title}
self.chats.append(chat_data)
checkbox = QCheckBox(f"{title} (id: {chat_id})")
checkbox.setChecked(False)
checkbox.setProperty("chat_id", chat_id)
self.chat_checkbox_layout.addWidget(checkbox)
self.chat_checkboxes.append(checkbox)
if not self.chats:
QMessageBox.information(self, "Информация", "У вас нет доступных чатов.")
self.chat_scroll_area.hide()
self.select_all_btn.setEnabled(False)
self.deselect_all_btn.setEnabled(False)
self.refresh_chats_btn.setEnabled(False)
else:
self.chat_scroll_area.show()
self.select_all_btn.setEnabled(True)
self.deselect_all_btn.setEnabled(True)
self.refresh_chats_btn.setEnabled(True)
except VkApiError as e:
error_message = str(e)
if "[5] User authorization failed: access_token was given to another ip address" in error_message:
QMessageBox.critical(self, "Ошибка авторизации VK",
"Ваш IP-адрес изменился, и токен стал недействительным. "
"Пожалуйста, авторизуйтесь заново.")
self.token = None
self.token_expiration_time = None
self.token_input.clear()
self.auth_btn.setEnabled(True)
self.set_ui_state(False)
self.status_label.setText("Статус: требуется повторная авторизация (IP изменен)")
if self.token_countdown_timer and self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE)
else:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить чаты: {e}\n"
"Убедитесь, что у приложения есть необходимые права доступа.")
self.chat_scroll_area.hide()
self.select_all_btn.setEnabled(False)
self.deselect_all_btn.setEnabled(False)
self.refresh_chats_btn.setEnabled(False)
def on_vk_url_input_changed(self, text):
"""
Слот, вызываемый при изменении текста в поле vk_url_input.
Автоматически пытается получить ID пользователя и сохраняет его в self.user_id_to_process.
"""
if not self.vk or not text:
self.user_id_to_process = None
self.set_ui_state(self.token is not None)
return
self.resolve_user_id_from_url(text)
def get_user_info_by_id(self, user_id):
"""
Получает имя и фамилию пользователя по его ID.
Возвращает строку "Имя Фамилия" или "Неизвестный пользователь".
"""
if not self.vk:
return "Неизвестный пользователь"
try:
users = self.vk.users.get(user_ids=user_id)
if users and len(users) > 0:
user = users[0]
return f"{user.get('first_name', '')} {user.get('last_name', '')}"
else:
return "Неизвестный пользователь"
except VkApiError as e:
print(f"Ошибка получения информации о пользователе {user_id}: {e}")
return "Неизвестный пользователь"
except Exception as e:
print(f"Неизвестная ошибка при получении информации о пользователе {user_id}: {e}")
return "Неизвестный пользователь"
def resolve_user_id_from_url(self, vk_url_to_resolve):
"""
Разрешает ID пользователя VK из введенной ссылки и сохраняет его в self.user_id_to_process.
"""
if not self.vk:
self.user_id_to_process = None
self.set_ui_state(self.token is not None)
return
vk_url = vk_url_to_resolve.strip()
if not vk_url:
self.user_id_to_process = None
self.set_ui_state(self.token is not None)
return
try:
parsed_url = urlparse(vk_url)
path_parts = parsed_url.path.split('/')
screen_name = ''
if path_parts:
screen_name = path_parts[-1]
if not screen_name and len(path_parts) > 1:
screen_name = path_parts[-2]
if not screen_name:
self.user_id_to_process = None
self.status_label.setText("Статус: Не удалось извлечь имя страницы из ссылки.")
self.set_ui_state(self.token is not None)
return
resolved_object = self.vk.utils.resolveScreenName(screen_name=screen_name)
if resolved_object and 'object_id' in resolved_object and resolved_object['type'] == 'user':
self.user_id_to_process = resolved_object['object_id']
self.status_label.setText(f"Статус: ID пользователя {self.user_id_to_process} успешно получен.")
elif resolved_object and 'type' in resolved_object and resolved_object['type'] != 'user':
self.user_id_to_process = None
self.status_label.setText(f"Статус: Ссылка ведет на {resolved_object['type']}, а не на пользователя.")
else:
self.user_id_to_process = None
self.status_label.setText("Статус: Не удалось найти пользователя по этой ссылке.")
except VkApiError as e:
self.user_id_to_process = None
error_message = str(e)
if "[5] User authorization failed: access_token was given to another ip address" in error_message:
self.status_label.setText("Статус: Требуется повторная авторизация (IP изменен).")
QMessageBox.critical(self, "Ошибка авторизации VK",
"Ваш IP-адрес изменился, и токен стал недействительным. "
"Пожалуйста, авторизуйтесь заново.")
self.token = None
self.token_expiration_time = None
self.token_input.clear()
self.auth_btn.setEnabled(True)
self.set_ui_state(False)
if self.token_countdown_timer and self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE)
else:
self.status_label.setText(f"Статус: Ошибка VK API при получении ID: {e}")
except Exception as e:
self.user_id_to_process = None
self.status_label.setText(f"Статус: Неизвестная ошибка при получении ID: {e}")
finally:
self.set_ui_state(self.token is not None) # Обновляем состояние кнопок после попытки разрешения ID
def remove_user(self):
"""
Исключает пользователя из всех выбранных чатов.
Перед исключением выводит окно подтверждения.
"""
if not self.token:
QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!")
return
if self.user_id_to_process is None:
QMessageBox.warning(self, "Ошибка",
"ID пользователя не получен. Пожалуйста, введите корректную ссылку и убедитесь, что ID определился.")
return
user_id = self.user_id_to_process
user_info = self.get_user_info_by_id(user_id)
selected_chat_ids = []
selected_chat_titles = []
for checkbox in self.chat_checkboxes:
if checkbox.isChecked():
chat_id = checkbox.property("chat_id")
chat_title = next((chat['title'] for chat in self.chats if chat['id'] == chat_id),
f"Чат с ID: {chat_id}")
selected_chat_ids.append(chat_id)
selected_chat_titles.append(chat_title)
if not selected_chat_ids:
QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для исключения пользователя!")
return
confirmation_message = (
f"Вы точно хотите исключить пользователя '{user_info}' (ID: {user_id}) из следующих чатов?\n\n"
"Выбранные чаты:\n" + "\n".join([f"- {title}" for title in selected_chat_titles]) +
"\n\nЭто действие необратимо."
)
reply = QMessageBox.question(self, "Подтверждение исключения",
confirmation_message,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
results = []
for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles):
try:
self.vk.messages.removeChatUser(chat_id=chat_id, user_id=user_id)
results.append(
f"✓ Пользователь '{user_info}' (ID: {user_id}) успешно исключен из чата '{chat_title}' (ID: {chat_id}).")
except VkApiError as e:
error_message = str(e)
if "[15] Access denied: user cannot be removed from this chat" in error_message:
results.append(
f"✗ Ошибка: Не удалось исключить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}). Доступ запрещен (нет прав админа или пользователь - создатель чата).")
elif "[100] One of the parameters specified was missing or invalid: user_id is invalid" in error_message:
results.append(
f"✗ Ошибка: Не удалось исключить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}). Некорректный ID пользователя.")
elif "[900] Cannot remove yourself" in error_message:
results.append(
f"✗ Ошибка: Не удалось исключить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}). Невозможно исключить самого себя.")
elif "[5] User authorization failed: access_token was given to another ip address" in error_message:
results.append(
f"✗ Ошибка: Не удалось исключить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}). "
"Токен недействителен из-за смены IP-адреса. Требуется повторная авторизация.")
self.token = None
self.token_expiration_time = None
self.token_input.clear()
self.auth_btn.setEnabled(True)
self.set_ui_state(False)
self.status_label.setText("Статус: требуется повторная авторизация (IP изменен)")
if self.token_countdown_timer and self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE)
break
else:
results.append(
f"✗ Ошибка: Не удалось исключить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}): {e}")
except Exception as e:
results.append(f"✗ Неизвестная ошибка при исключении из чата '{chat_title}' (ID: {chat_id}): {e}")
QMessageBox.information(self, "Результаты исключения", "\n".join(results))
self.vk_url_input.clear()
self.user_id_to_process = None
self.set_ui_state(self.token is not None)
else:
QMessageBox.information(self, "Отмена", "Операция исключения отменена.")
def add_user_to_chat(self):
"""
Приглашает пользователя во все выбранные чаты.
Перед приглашением выводит окно подтверждения.
"""
if not self.token:
QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!")
return
if self.user_id_to_process is None:
QMessageBox.warning(self, "Ошибка",
"ID пользователя не получен. Пожалуйста, введите корректную ссылку и убедитесь, что ID определился.")
return
user_id = self.user_id_to_process
user_info = self.get_user_info_by_id(user_id)
visible_messages_count = None
if self.visible_messages_checkbox.isChecked():
visible_messages_count = 250
selected_chat_ids = []
selected_chat_titles = []
for checkbox in self.chat_checkboxes:
if checkbox.isChecked():
chat_id = checkbox.property("chat_id")
chat_title = next((chat['title'] for chat in self.chats if chat['id'] == chat_id),
f"Чат с ID: {chat_id}")
selected_chat_ids.append(chat_id)
selected_chat_titles.append(chat_title)
if not selected_chat_ids:
QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для приглашения пользователя!")
return
confirmation_message = (
f"Вы точно хотите пригласить пользователя '{user_info}' (ID: {user_id}) в следующие чаты?\n"
f"(Параметр 'Показать 250 последних сообщений' установлен: {'ДА' if visible_messages_count is not None else 'НЕТ'})\n\n"
"Выбранные чаты:\n" + "\n".join([f"- {title}" for title in selected_chat_titles]) +
"\n\nЭто действие может привести к отправке уведомлений пользователю."
)
reply = QMessageBox.question(self, "Подтверждение приглашения",
confirmation_message,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
results = []
for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles):
try:
params = {
'chat_id': chat_id,
'user_id': user_id
}
if visible_messages_count is not None:
params['visible_messages_count'] = visible_messages_count
results.append(
f"Внимание: Параметр 'visible_messages_count' (значение: {visible_messages_count}) передан в messages.addChatUser для чата '{chat_title}' (ID: {chat_id}). Согласно официальной документации VK API, этот параметр не поддерживается для данного метода и может быть проигнорирован или привести к ошибке.")
self.vk.messages.addChatUser(**params)
results.append(
f"✓ Пользователь '{user_info}' (ID: {user_id}) успешно приглашен в чат '{chat_title}' (ID: {chat_id}).")
except VkApiError as e:
error_message = str(e)
if "[917] You don't have access to this chat" in error_message:
results.append(
f"✗ Ошибка: Не удалось пригласить пользователя '{user_info}' (ID: {user_id}) в чат '{chat_title}' (ID: {chat_id}). Нет доступа к чату или недостаточно прав.")
elif "[935] User has been invited to this chat" in error_message:
results.append(
f"✗ Ошибка: Пользователь '{user_info}' (ID: {user_id}) уже приглашен в чат '{chat_title}' (ID: {chat_id}) или уже был добавлен.")
elif "[100] One of the parameters specified was missing or invalid: user_id is invalid" in error_message:
results.append(
f"✗ Ошибка: Не удалось пригласить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}). Некорректный ID пользователя.")
elif "[5] User authorization failed: access_token was given to another ip address" in error_message:
results.append(
f"✗ Ошибка: Не удалось пригласить пользователя '{user_info}' (ID: {user_id}) в чат '{chat_title}' (ID: {chat_id}). "
"Токен недействителен из-за смены IP-адреса. Требуется повторная авторизация.")
self.token = None
self.token_expiration_time = None
self.token_input.clear()
self.auth_btn.setEnabled(True)
self.set_ui_state(False)
self.status_label.setText("Статус: требуется повторная авторизация (IP изменен)")
if self.token_countdown_timer and self.token_countdown_timer.isActive():
self.token_countdown_timer.stop()
if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE)
break
else:
results.append(
f"✗ Ошибка: Не удалось пригласить пользователя '{user_info}' (ID: {user_id}) в чат '{chat_title}' (ID: {chat_id}): {e}")
except Exception as e:
results.append(f"✗ Неизвестная ошибка при приглашении в чат '{chat_title}' (ID: {chat_id}): {e}")
QMessageBox.information(self, "Результаты приглашения", "\n".join(results))
self.visible_messages_checkbox.setChecked(False)
self.vk_url_input.clear()
self.user_id_to_process = None
self.set_ui_state(self.token is not None)
else:
QMessageBox.information(self, "Отмена", "Операция приглашения отменена.")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
app.setPalette(app.style().standardPalette())
window = VkChatManager()
window.show()
sys.exit(app.exec())