From 0c270a6cb141a11056087c68a197640cbaf7a7cf Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 28 Jun 2025 02:03:50 +0300 Subject: [PATCH] =?UTF-8?q?-=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B5=D1=80=D1=81=D1=82=D0=BA=D0=B0=20UI,=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20=D1=8D?= =?UTF-8?q?=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2.=20-=20UI=20?= =?UTF-8?q?=D0=B1=D1=8B=D0=BB=20=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91=D0=BD?= =?UTF-8?q?=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE?= =?UTF-8?q?=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20ID=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=B8=D0=B7=20VK-=D1=81?= =?UTF-8?q?=D1=81=D1=8B=D0=BB=D0=BA=D0=B8.=20-=20=D0=A3=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20VK?= =?UTF-8?q?=20API=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD=D0=B5=20?= =?UTF-8?q?IP-=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0.=20-=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=B2=20=D0=B4=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B0=D1=85=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F.=20-=20=D0=A3?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D1=8B=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE?= =?UTF-8?q?=D0=B2=20Qt.=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=B0=20(=D0=B2=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D1=8F=20=D0=BA=D1=83=D0=BA=D0=B8)=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20QWebEngineProfile=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B2=20WebEngine.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 524 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 291 insertions(+), 233 deletions(-) diff --git a/main.py b/main.py index f658164..c913c46 100644 --- a/main.py +++ b/main.py @@ -5,19 +5,19 @@ import time import os from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget, QMessageBox, - QComboBox, QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout, QSizePolicy, QDialog) -from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer # QTimer добавлен + 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 +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 # Для кроссплатформенного получения пути к данным приложения +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): @@ -25,10 +25,7 @@ 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, @@ -37,11 +34,9 @@ def save_token(token, expires_in=3600): 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}") @@ -51,7 +46,6 @@ def load_token(): Возвращает (токен, время_истечения_unix) или (None, None). """ try: - # Проверяем, существует ли файл токена перед попыткой чтения if not os.path.exists(TOKEN_FILE): print(f"Файл токена не найден по пути {TOKEN_FILE}.") return None, None @@ -61,37 +55,30 @@ def load_token(): 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: - # Обрабатываем ошибки ввода/вывода или ошибки декодирования JSON print(f"Ошибка загрузки токена: {e}") return None, None -# --- WebEnginePage для VK OAuth --- class WebEnginePage(QWebEnginePage): """ Класс для обработки навигационных запросов в QWebEngineView, специально для извлечения токена авторизации VK. """ - # Изменена сигнатура: parent для QWebEnginePage и browser_window_instance для логической связи - def __init__(self, parent=None, browser_window_instance=None): - super().__init__(parent) # Передаем графический parent в базовый класс - # Сохраняем ссылку на окно браузера для вызова его методов + # Добавлен параметр 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): @@ -99,15 +86,11 @@ class WebEnginePage(QWebEnginePage): Переопределенный метод для перехвата URL-адреса, содержащего токен доступа. """ url_string = url.toString() - # Проверяем, содержит ли URL 'access_token' и не был ли токен уже извлечен 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) @@ -115,22 +98,29 @@ 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) # Размеры окна авторизации + self.setGeometry(350, 350, 800, 600) layout = QVBoxLayout(self) self.browser = QWebEngineView() - # Изменен вызов конструктора WebEnginePage - self.browser.page = WebEnginePage(parent=self.browser, browser_window_instance=self) + + # Настройка 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) @@ -139,12 +129,12 @@ class AuthBrowserWindow(QDialog): """Запускает OAuth авторизацию VK в этом окне.""" auth_url = ( "https://oauth.vk.com/authorize?" - "client_id=6287487&" # Официальный client_id VK для Android + "client_id=6287487&" "display=page&" "redirect_uri=https://oauth.vk.com/blank.html&" - "scope=1073737727&" # Широкий scope для работы с сообщениями и чатами + "scope=1073737727&" "response_type=token&" - "v=5.131" # Версия API VK + "v=5.131" ) self.browser.setUrl(QUrl(auth_url)) self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...") @@ -154,10 +144,9 @@ class AuthBrowserWindow(QDialog): Извлекает токен доступа из URL перенаправления и испускает сигнал. """ token = None - expires_in = 3600 # По умолчанию 1 час, если не найден в URL + expires_in = 3600 parsed = urlparse(url_string) - # 1. Попытка стандартного парсинга URL-параметров if parsed.fragment: params = parse_qs(parsed.fragment) else: @@ -169,9 +158,8 @@ class AuthBrowserWindow(QDialog): try: expires_in = int(params['expires_in'][0]) except ValueError: - pass # Используем значение по умолчанию, если преобразование не удалось + pass - # 2. Если стандартный парсинг не дал результат, пробуем строковый поиск по маркерам if not token: start_marker = "access_token%253D" end_marker = "%25" @@ -190,16 +178,31 @@ class AuthBrowserWindow(QDialog): 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() # Закрываем диалог с результатом QDialog.Accepted + self.token_extracted_signal.emit(token, expires_in) + self.accept() else: QMessageBox.warning(self, "Ошибка Авторизации", "Не удалось получить токен. Проверьте URL или попробуйте еще раз.") - self.reject() # Закрываем диалог с результатом QDialog.Rejected + 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): @@ -214,25 +217,25 @@ class VkChatManager(QMainWindow): self.setGeometry(300, 300, 600, 800) self.token = None - self.token_expiration_time = None # Храним время истечения токена в Unix-таймстамп + 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() # Настройка таймера + 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) # Уменьшаем стандартный отступ между элементами + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) - # Инструкции для пользователя self.instructions = QTextBrowser() self.instructions.setPlainText( "Инструкция:\n" @@ -241,34 +244,31 @@ class VkChatManager(QMainWindow): "3. Разрешите доступ приложению\n" "4. Токен автоматически сохранится на 1 час\n" "5. Выберите один или несколько чатов, установив галочки\n" - "6. Введите ID пользователя для удаления/добавления (можно получить по ссылке) и нажмите соответствующую кнопку" + "6. Введите или вставьте ссылку на страницу пользователя VK (например, vk.com/id123 или vk.com/durov). ID будет получен автоматически.\n" + "7. Нажмите соответствующую кнопку 'ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ' или 'ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ'." ) - self.instructions.setFixedHeight(150) # Увеличена высота для инструкций + 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;") # Сделать немного более заметным + 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_layout.setSpacing(2) self.chat_checkbox_widget = QWidget() self.chat_checkbox_widget.setLayout(self.chat_checkbox_layout) @@ -297,52 +297,36 @@ class VkChatManager(QMainWindow): layout.addLayout(select_buttons_layout) layout.addWidget(self.chat_scroll_area) - # Секция получения ID по ссылке - layout.addWidget(QLabel("Получить ID по ссылке VK:")) + layout.addWidget(QLabel("Введите или вставьте ссылку на страницу VK (ID будет получен автоматически):")) self.vk_url_input = QLineEdit() - self.vk_url_input.setPlaceholderText("Введите ссылку на страницу VK (vk.com/id123 или vk.com/durov)") + 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.resolve_id_btn = QPushButton("Получить ID") - self.resolve_id_btn.clicked.connect(self.resolve_user_id_from_url) - self.resolve_id_btn.setEnabled(False) - layout.addWidget(self.resolve_id_btn) + # Кнопки для исключения/приглашения пользователя + 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) - # Поле для ID пользователя (удаление) - layout.addWidget(QLabel("ID пользователя для УДАЛЕНИЯ:")) - self.user_remove_input = QLineEdit() - self.user_remove_input.setPlaceholderText("Введите ID пользователя...") - layout.addWidget(self.user_remove_input) - - # Кнопка удаления - self.remove_btn = QPushButton("Удалить из выбранных чатов") - self.remove_btn.setEnabled(False) - self.remove_btn.clicked.connect(self.remove_user) - layout.addWidget(self.remove_btn) - - # Поле для ID пользователя (добавление) - layout.addWidget(QLabel("ID пользователя для ДОБАВЛЕНИЯ:")) - self.user_add_input = QLineEdit() - self.user_add_input.setPlaceholderText("Введите ID пользователя...") - layout.addWidget(self.user_add_input) - - # Чекбокс для visible_messages_count self.visible_messages_checkbox = QCheckBox("Показать 250 последних сообщений при добавлении") - self.visible_messages_checkbox.setChecked(False) # По умолчанию не отмечен + self.visible_messages_checkbox.setChecked(False) layout.addWidget(self.visible_messages_checkbox) - # Кнопка добавления - self.add_btn = QPushButton("Добавить в выбранные чаты") - self.add_btn.setEnabled(False) - self.add_btn.clicked.connect(self.add_user_to_chat) - layout.addWidget(self.add_btn) + 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) # Добавляем растяжение внизу + layout.addStretch(1) central_widget.setLayout(layout) @@ -350,8 +334,6 @@ class VkChatManager(QMainWindow): """Настраивает QTimer для обновления отображения обратного отсчета токена.""" self.token_countdown_timer = QTimer(self) self.token_countdown_timer.timeout.connect(self.update_token_timer_display) - # Таймер срабатывает каждую 1000 миллисекунд (1 секунду) - # Он запускается, когда токен загружен/получен self.token_countdown_timer.start(1000) def update_token_timer_display(self): @@ -366,23 +348,23 @@ class VkChatManager(QMainWindow): if remaining_seconds <= 0: self.token_timer_label.setText("Срок действия токена истек!") - self.token_countdown_timer.stop() # Останавливаем таймер - # При необходимости, повторно включаем кнопку авторизации и отключаем другие функции + 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) # Новый метод для последовательной установки состояния UI + self.set_ui_state(False) self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.") - self.token = None # Очищаем токен + 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: # Показываем минуты, если показаны часы или если минуты присутствуют + if minutes > 0 or hours > 0: time_str += f"{minutes:02d}м " time_str += f"{seconds:02d}с" @@ -394,12 +376,10 @@ class VkChatManager(QMainWindow): self.deselect_all_btn.setEnabled(authorized) self.refresh_chats_btn.setEnabled(authorized) self.vk_url_input.setEnabled(authorized) - self.resolve_id_btn.setEnabled(authorized) - self.user_remove_input.setEnabled(authorized) - self.remove_btn.setEnabled(authorized) - self.user_add_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_btn.setEnabled(authorized) + self.add_user_btn.setEnabled(authorized and self.user_id_to_process is not None) if authorized: self.chat_scroll_area.show() @@ -412,20 +392,19 @@ class VkChatManager(QMainWindow): if loaded_token: self.token = loaded_token self.token_expiration_time = expiration_time - self.token_input.setText(self.token[:50] + "...") # Показываем часть токена + self.token_input.setText(self.token[:50] + "...") self.status_label.setText("Статус: авторизован (токен загружен из файла)") - self.auth_btn.setEnabled(False) # Отключаем кнопку авторизации - self.set_ui_state(True) # Устанавливаем состояние UI - self.update_token_timer_display() # Начальное обновление отображения + self.auth_btn.setEnabled(False) + self.set_ui_state(True) + self.update_token_timer_display() - # Инициализируем VK API с загруженным токеном self.vk_session = vk_api.VkApi(token=self.token) self.vk = self.vk_session.get_api() - self.load_chats() # Загружаем чаты + self.load_chats() else: self.status_label.setText("Статус: не авторизован (токен не найден или просрочен)") - self.set_ui_state(False) # Устанавливаем состояние UI - self.update_token_timer_display() # Обновляем отображение на Н/Д + self.set_ui_state(False) + self.update_token_timer_display() def set_all_checkboxes(self, checked): """ @@ -440,10 +419,9 @@ class VkChatManager(QMainWindow): """ self.status_label.setText("Статус: ожидание авторизации в новом окне...") auth_window = AuthBrowserWindow(self) - # Подключаем сигнал из AuthBrowserWindow к нашему обработчику, включая expires_in auth_window.token_extracted_signal.connect(self.handle_auth_token) auth_window.start_auth_flow() - auth_window.exec_() # Открываем окно как модальный диалог + auth_window.exec() # Изменено с exec_() на exec() def handle_auth_token(self, token, expires_in): """ @@ -451,150 +429,207 @@ class VkChatManager(QMainWindow): """ if token: self.token = token - # Вычисляем время истечения на основе текущего времени + expires_in self.token_expiration_time = time.time() + expires_in - save_token(self.token, expires_in) # Сохраняем токен - self.token_input.setText(self.token[:50] + "...") # Отображаем часть токена + 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) # Устанавливаем состояние UI - self.update_token_timer_display() # Начальное обновление отображения + self.auth_btn.setEnabled(False) + self.set_ui_state(True) + self.update_token_timer_display() - # Инициализируем VK API с полученным токеном self.vk_session = vk_api.VkApi(token=self.token) self.vk = self.vk_session.get_api() - self.load_chats() # Загружаем чаты + self.load_chats() else: QMessageBox.warning(self, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.") self.status_label.setText("Статус: Авторизация не удалась") - self.set_ui_state(False) # Устанавливаем состояние UI - self.update_token_timer_display() # Обновляем отображение на Н/Д - + 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() # Очищаем список данных о чатах + widget_to_remove.setParent(None) + widget_to_remove.deleteLater() + self.chat_checkboxes.clear() + self.chats.clear() try: - # Получаем до 200 последних бесед, включая чаты conversations = self.vk.messages.getConversations(count=200)['items'] for conv in conversations: - # Фильтруем только чаты (peer type 'chat') 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) # По умолчанию не выбран - # Сохраняем chat_id в свойстве чекбокса для удобства + checkbox.setChecked(False) checkbox.setProperty("chat_id", chat_id) - self.chat_checkbox_layout.addWidget(checkbox) # Добавляем чекбокс в макет - self.chat_checkboxes.append(checkbox) # Добавляем ссылку на чекбокс в список + 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.chat_scroll_area.hide() + self.select_all_btn.setEnabled(False) self.deselect_all_btn.setEnabled(False) - self.refresh_chats_btn.setEnabled(False) # Отключаем кнопку обновления + self.refresh_chats_btn.setEnabled(False) else: - self.chat_scroll_area.show() # Показываем область с чатами - self.select_all_btn.setEnabled(True) # Включаем кнопки выбора + self.chat_scroll_area.show() + self.select_all_btn.setEnabled(True) self.deselect_all_btn.setEnabled(True) - self.refresh_chats_btn.setEnabled(True) # Включаем кнопку обновления + self.refresh_chats_btn.setEnabled(True) except VkApiError as e: - 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) # Отключаем кнопку обновления + 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 resolve_user_id_from_url(self): + def on_vk_url_input_changed(self, text): """ - Разрешает ID пользователя VK из введенной ссылки и заполняет поля ID. + Слот, вызываемый при изменении текста в поле vk_url_input. + Автоматически пытается получить ID пользователя и сохраняет его в self.user_id_to_process. """ - if not self.vk: - QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!") + if not self.vk or not text: + self.user_id_to_process = None + self.set_ui_state(self.token is not None) return - vk_url = self.vk_url_input.text().strip() + 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: - QMessageBox.warning(self, "Ошибка", "Введите ссылку на страницу VK!") + self.user_id_to_process = None + self.set_ui_state(self.token is not None) return try: - # Парсим URL, чтобы извлечь screen_name parsed_url = urlparse(vk_url) path_parts = parsed_url.path.split('/') - # screen_name обычно последний элемент в пути - # Обработка случая с завершающим слешем или пустым путем screen_name = '' if path_parts: screen_name = path_parts[-1] - if not screen_name and len(path_parts) > 1: # если последний пустой (URL заканчивается на /) + if not screen_name and len(path_parts) > 1: screen_name = path_parts[-2] if not screen_name: - QMessageBox.warning(self, "Ошибка", - "Не удалось извлечь имя страницы (screen_name) из ссылки. Проверьте формат ссылки.") + self.user_id_to_process = None + self.status_label.setText("Статус: Не удалось извлечь имя страницы из ссылки.") + self.set_ui_state(self.token is not None) return - # Используем messages.resolveScreenName для получения ID пользователя resolved_object = self.vk.utils.resolveScreenName(screen_name=screen_name) if resolved_object and 'object_id' in resolved_object and resolved_object['type'] == 'user': - user_id = resolved_object['object_id'] - self.user_remove_input.setText(str(user_id)) - self.user_add_input.setText(str(user_id)) - QMessageBox.information(self, "Успех", f"ID пользователя: {user_id} успешно получен и заполнен в поля.") + 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': - QMessageBox.warning(self, "Ошибка", - f"Ссылка ведет на {resolved_object['type']}, а не на страницу пользователя. Пожалуйста, введите ссылку на страницу пользователя.") + self.user_id_to_process = None + self.status_label.setText(f"Статус: Ссылка ведет на {resolved_object['type']}, а не на пользователя.") else: - QMessageBox.warning(self, "Ошибка", - "Не удалось найти пользователя по этой ссылке. Проверьте корректность ссылки.") + self.user_id_to_process = None + self.status_label.setText("Статус: Не удалось найти пользователя по этой ссылке.") except VkApiError as e: - QMessageBox.critical(self, "Ошибка VK API", f"Ошибка при получении ID пользователя: {e}\n" - "Убедитесь, что ссылка корректна и у приложения есть необходимые права.") + 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: - QMessageBox.critical(self, "Неизвестная ошибка", f"Произошла непредвиденная ошибка: {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 - user_id_str = self.user_remove_input.text().strip() # Используем user_remove_input - if not user_id_str or not user_id_str.isdigit(): - QMessageBox.warning(self, "Ошибка", "Введите корректный ID пользователя (только цифры) для удаления!") + if self.user_id_to_process is None: + QMessageBox.warning(self, "Ошибка", + "ID пользователя не получен. Пожалуйста, введите корректную ссылку и убедитесь, что ID определился.") return - user_id = int(user_id_str) + user_id = self.user_id_to_process + user_info = self.get_user_info_by_id(user_id) selected_chat_ids = [] - selected_chat_titles = [] # Список для названий выбранных чатов - # Собираем ID и названия всех выбранных чатов + selected_chat_titles = [] for checkbox in self.chat_checkboxes: if checkbox.isChecked(): chat_id = checkbox.property("chat_id") @@ -604,74 +639,88 @@ class VkChatManager(QMainWindow): selected_chat_titles.append(chat_title) if not selected_chat_ids: - QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для удаления пользователя!") + QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для исключения пользователя!") return - # Создаем текст подтверждения со списком чатов confirmation_message = ( - f"Вы точно хотите удалить пользователя с ID: {user_id} из следующих чатов?\n\n" - "**Выбранные чаты:**\n" + "\n".join([f"- {title}" for title in selected_chat_titles]) + + 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, "Подтверждение удаления", + reply = QMessageBox.question(self, "Подтверждение исключения", confirmation_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: - results = [] # Список для сбора результатов по каждому чату + 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_id} успешно удален из чата '{chat_title}' (ID: {chat_id}).") + results.append( + f"✓ Пользователь '{user_info}' (ID: {user_id}) успешно исключен из чата '{chat_title}' (ID: {chat_id}).") except VkApiError as e: error_message = str(e) - # Более детальная обработка известных ошибок VK API if "[15] Access denied: user cannot be removed from this chat" in error_message: results.append( - f"✗ Ошибка: Не удалось удалить пользователя {user_id} из чата '{chat_title}' (ID: {chat_id}). Доступ запрещен (нет прав админа или пользователь - создатель чата).") + 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_id} из чата '{chat_title}' (ID: {chat_id}). Некорректный ID пользователя.") + f"✗ Ошибка: Не удалось исключить пользователя '{user_info}' (ID: {user_id}) из чата '{chat_title}' (ID: {chat_id}). Некорректный ID пользователя.") elif "[900] Cannot remove yourself" in error_message: results.append( - f"✗ Ошибка: Не удалось удалить пользователя {user_id} из чата '{chat_title}' (ID: {chat_id}). Невозможно удалить самого себя.") + 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_id} из чата '{chat_title}' (ID: {chat_id}): {e}") + 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}") + results.append(f"✗ Неизвестная ошибка при исключении из чата '{chat_title}' (ID: {chat_id}): {e}") - # Отображаем сводное сообщение со всеми результатами - QMessageBox.information(self, "Результаты удаления", "\n".join(results)) - self.user_remove_input.clear() # Очищаем поле ввода пользователя + 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, "Отмена", "Операция удаления отменена.") + QMessageBox.information(self, "Отмена", "Операция исключения отменена.") def add_user_to_chat(self): """ - Добавляет пользователя во все выбранные чаты. - Перед добавлением выводит окно подтверждения. + Приглашает пользователя во все выбранные чаты. + Перед приглашением выводит окно подтверждения. """ if not self.token: QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!") return - user_id_str = self.user_add_input.text().strip() # Используем user_add_input - if not user_id_str or not user_id_str.isdigit(): - QMessageBox.warning(self, "Ошибка", "Введите корректный ID пользователя (только цифры) для добавления!") + if self.user_id_to_process is None: + QMessageBox.warning(self, "Ошибка", + "ID пользователя не получен. Пожалуйста, введите корректную ссылку и убедитесь, что ID определился.") return - user_id = int(user_id_str) + 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 # Если галочка отмечена, устанавливаем значение 250 + visible_messages_count = 250 selected_chat_ids = [] - selected_chat_titles = [] # Список для названий выбранных чатов - # Собираем ID и названия всех выбранных чатов + selected_chat_titles = [] for checkbox in self.chat_checkboxes: if checkbox.isChecked(): chat_id = checkbox.property("chat_id") @@ -681,73 +730,82 @@ class VkChatManager(QMainWindow): selected_chat_titles.append(chat_title) if not selected_chat_ids: - QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для добавления пользователя!") + QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для приглашения пользователя!") return - # Создаем текст подтверждения со списком чатов confirmation_message = ( - f"Вы точно хотите добавить пользователя с ID: {user_id} в следующие чаты?\n" + 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".join([f"- {title}" for title in selected_chat_titles]) + "\n\nЭто действие может привести к отправке уведомлений пользователю." ) - # Выводим окно подтверждения - reply = QMessageBox.question(self, "Подтверждение добавления", + reply = QMessageBox.question(self, "Подтверждение приглашения", confirmation_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: - results = [] # Список для сбора результатов по каждому чату + results = [] for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles): try: - # Создаем словарь параметров для addChatUser params = { 'chat_id': chat_id, 'user_id': user_id } if visible_messages_count is not None: - # Добавляем visible_messages_count, как запрошено, несмотря на документацию 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_id} успешно добавлен в чат '{chat_title}' (ID: {chat_id}).") + 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) - # Детальная обработка известных ошибок VK API для добавления if "[917] You don't have access to this chat" in error_message: results.append( - f"✗ Ошибка: Не удалось добавить пользователя {user_id} в чат '{chat_title}' (ID: {chat_id}). Нет доступа к чату или недостаточно прав.") + 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_id} уже добавлен в чат '{chat_title}' (ID: {chat_id}) или уже был приглашен.") + 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_id} из чата '{chat_title}' (ID: {chat_id}). Некорректный ID пользователя.") + 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_id} в чат '{chat_title}' (ID: {chat_id}): {e}") + 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}") + results.append(f"✗ Неизвестная ошибка при приглашении в чат '{chat_title}' (ID: {chat_id}): {e}") - # Отображаем сводное сообщение со всеми результатами - QMessageBox.information(self, "Результаты добавления", "\n".join(results)) - self.user_add_input.clear() # Очищаем поле ввода пользователя - self.visible_messages_checkbox.setChecked(False) # Сбрасываем чекбокс + 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, "Отмена", "Операция добавления отменена.") + QMessageBox.information(self, "Отмена", "Операция приглашения отменена.") if __name__ == "__main__": app = QApplication(sys.argv) - # Устанавливаем стиль приложения на "Fusion". app.setStyle("Fusion") - - # Применяем стандартную палитру, предоставленную текущим стилем. app.setPalette(app.style().standardPalette()) window = VkChatManager() window.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) \ No newline at end of file