- Улучшена верстка UI, устранено перекрытие элементов.

- UI был упрощён
- Добавлено автоматическое получение ID пользователя из VK-ссылки.
- Улучшена обработка ошибок VK API при смене IP-адреса.
- Добавлено отображение имени пользователя в диалогах подтверждения.
- Удалены неиспользуемые импорты и обновлены вызовы методов Qt.
- Добавлено сохранение данных браузера (включая куки) через QWebEngineProfile для поддержания сессии в WebEngine.
This commit is contained in:
Alex
2025-06-28 02:03:50 +03:00
parent 7e2ac63523
commit 0c270a6cb1

524
main.py
View File

@@ -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())