From 7e2ac63523660a9579913f67597ac2438972a7d6 Mon Sep 17 00:00:00 2001 From: benya Date: Mon, 23 Jun 2025 00:35:12 +0300 Subject: [PATCH] Initial Commit --- .gitignore | 3 + main.py | 753 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 3 files changed, 758 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f89590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.venv/ +/.venv1/ +/.venv3/ diff --git a/main.py b/main.py new file mode 100644 index 0000000..f658164 --- /dev/null +++ b/main.py @@ -0,0 +1,753 @@ +import sys +import vk_api +import json +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 добавлен +from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtWebEngineCore import QWebEnginePage +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") + + +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: + # Обрабатываем ошибки ввода/вывода или ошибки декодирования 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 в базовый класс + # Сохраняем ссылку на окно браузера для вызова его методов + self.parent_browser_window = browser_window_instance + # Флаг, чтобы избежать многократного извлечения токена при перенаправлении + self.token_extracted = False + + def acceptNavigationRequest(self, url, _type, isMainFrame): + """ + Переопределенный метод для перехвата 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) + + +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() + # Изменен вызов конструктора WebEnginePage + self.browser.page = WebEnginePage(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&" # Официальный client_id VK для Android + "display=page&" + "redirect_uri=https://oauth.vk.com/blank.html&" + "scope=1073737727&" # Широкий scope для работы с сообщениями и чатами + "response_type=token&" + "v=5.131" # Версия API VK + ) + self.browser.setUrl(QUrl(auth_url)) + self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...") + + def process_auth_url(self, url_string): + """ + Извлекает токен доступа из URL перенаправления и испускает сигнал. + """ + token = None + expires_in = 3600 # По умолчанию 1 час, если не найден в URL + parsed = urlparse(url_string) + + # 1. Попытка стандартного парсинга URL-параметров + 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 # Используем значение по умолчанию, если преобразование не удалось + + # 2. Если стандартный парсинг не дал результат, пробуем строковый поиск по маркерам + 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() # Закрываем диалог с результатом QDialog.Accepted + else: + QMessageBox.warning(self, "Ошибка Авторизации", + "Не удалось получить токен. Проверьте URL или попробуйте еще раз.") + self.reject() # Закрываем диалог с результатом QDialog.Rejected + + +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 # Храним время истечения токена в Unix-таймстамп + self.chats = [] + self.chat_checkboxes = [] + self.vk_session = None + self.vk = None + + 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. Введите ID пользователя для удаления/добавления (можно получить по ссылке) и нажмите соответствующую кнопку" + ) + self.instructions.setFixedHeight(150) # Увеличена высота для инструкций + 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) + + # Секция получения ID по ссылке + layout.addWidget(QLabel("Получить ID по ссылке VK:")) + self.vk_url_input = QLineEdit() + self.vk_url_input.setPlaceholderText("Введите ссылку на страницу VK (vk.com/id123 или vk.com/durov)") + 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) + + # Поле для 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) # По умолчанию не отмечен + 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.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) + # Таймер срабатывает каждую 1000 миллисекунд (1 секунду) + # Он запускается, когда токен загружен/получен + 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("Срок действия токена истек!") + self.token_countdown_timer.stop() # Останавливаем таймер + # При необходимости, повторно включаем кнопку авторизации и отключаем другие функции + self.auth_btn.setEnabled(True) + self.set_ui_state(False) # Новый метод для последовательной установки состояния UI + self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.") + self.token = None # Очищаем токен + self.token_expiration_time = None + 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) + self.resolve_id_btn.setEnabled(authorized) + self.user_remove_input.setEnabled(authorized) + self.remove_btn.setEnabled(authorized) + self.user_add_input.setEnabled(authorized) + self.visible_messages_checkbox.setEnabled(authorized) + self.add_btn.setEnabled(authorized) + + 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) # Устанавливаем состояние UI + 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() # Загружаем чаты + else: + self.status_label.setText("Статус: не авторизован (токен не найден или просрочен)") + self.set_ui_state(False) # Устанавливаем состояние UI + 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) + # Подключаем сигнал из AuthBrowserWindow к нашему обработчику, включая expires_in + auth_window.token_extracted_signal.connect(self.handle_auth_token) + auth_window.start_auth_flow() + auth_window.exec_() # Открываем окно как модальный диалог + + def handle_auth_token(self, token, expires_in): + """ + Обрабатывает полученный токен авторизации после закрытия окна браузера. + """ + 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] + "...") # Отображаем часть токена + self.status_label.setText("Статус: авторизован") + self.auth_btn.setEnabled(False) # Отключаем кнопку авторизации + self.set_ui_state(True) # Устанавливаем состояние UI + 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() # Загружаем чаты + else: + QMessageBox.warning(self, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.") + self.status_label.setText("Статус: Авторизация не удалась") + self.set_ui_state(False) # Устанавливаем состояние UI + 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: + # Получаем до 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.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: + 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): + """ + Разрешает ID пользователя VK из введенной ссылки и заполняет поля ID. + """ + if not self.vk: + QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!") + return + + vk_url = self.vk_url_input.text().strip() + if not vk_url: + QMessageBox.warning(self, "Ошибка", "Введите ссылку на страницу VK!") + 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 заканчивается на /) + screen_name = path_parts[-2] + + if not screen_name: + QMessageBox.warning(self, "Ошибка", + "Не удалось извлечь имя страницы (screen_name) из ссылки. Проверьте формат ссылки.") + 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} успешно получен и заполнен в поля.") + elif resolved_object and 'type' in resolved_object and resolved_object['type'] != 'user': + QMessageBox.warning(self, "Ошибка", + f"Ссылка ведет на {resolved_object['type']}, а не на страницу пользователя. Пожалуйста, введите ссылку на страницу пользователя.") + else: + QMessageBox.warning(self, "Ошибка", + "Не удалось найти пользователя по этой ссылке. Проверьте корректность ссылки.") + + except VkApiError as e: + QMessageBox.critical(self, "Ошибка VK API", f"Ошибка при получении ID пользователя: {e}\n" + "Убедитесь, что ссылка корректна и у приложения есть необходимые права.") + except Exception as e: + QMessageBox.critical(self, "Неизвестная ошибка", f"Произошла непредвиденная ошибка: {e}") + + 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 пользователя (только цифры) для удаления!") + return + + user_id = int(user_id_str) + + selected_chat_ids = [] + selected_chat_titles = [] # Список для названий выбранных чатов + # Собираем ID и названия всех выбранных чатов + 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"Вы точно хотите удалить пользователя с 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_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}). Доступ запрещен (нет прав админа или пользователь - создатель чата).") + 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 пользователя.") + elif "[900] Cannot remove yourself" in error_message: + results.append( + f"✗ Ошибка: Не удалось удалить пользователя {user_id} из чата '{chat_title}' (ID: {chat_id}). Невозможно удалить самого себя.") + else: + results.append( + f"✗ Ошибка: Не удалось удалить пользователя {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.user_remove_input.clear() # Очищаем поле ввода пользователя + else: + 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 пользователя (только цифры) для добавления!") + return + + user_id = int(user_id_str) + + visible_messages_count = None + if self.visible_messages_checkbox.isChecked(): + visible_messages_count = 250 # Если галочка отмечена, устанавливаем значение 250 + + selected_chat_ids = [] + selected_chat_titles = [] # Список для названий выбранных чатов + # Собираем ID и названия всех выбранных чатов + 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"Вы точно хотите добавить пользователя с 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: + # Создаем словарь параметров для 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}).") + 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}). Нет доступа к чату или недостаточно прав.") + elif "[935] User has been invited to this chat" in error_message: + results.append( + f"✗ Ошибка: Пользователь {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 пользователя.") + else: + results.append( + f"✗ Ошибка: Не удалось добавить пользователя {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.user_add_input.clear() # Очищаем поле ввода пользователя + self.visible_messages_checkbox.setChecked(False) # Сбрасываем чекбокс + else: + 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_()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..147c614 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PySide6~=6.9.1 +vk-api~=11.9.9