- Улучшена верстка 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 import os
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QWidget, QMessageBox, QPushButton, QVBoxLayout, QWidget, QMessageBox,
QComboBox, QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout, QSizePolicy, QDialog) QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout, QSizePolicy, QDialog)
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer # QTimer добавлен from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
from PySide6.QtWebEngineWidgets import QWebEngineView 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 urllib.parse import urlparse, parse_qs, unquote
from vk_api.exceptions import VkApiError 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") APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation), "AnabasisVKChatManager")
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json") 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): def save_token(token, expires_in=3600):
@@ -25,10 +25,7 @@ def save_token(token, expires_in=3600):
Сохраняет VK access токен и его время истечения в JSON файл. Сохраняет VK access токен и его время истечения в JSON файл.
По умолчанию токен действителен 1 час (3600 секунд). По умолчанию токен действителен 1 час (3600 секунд).
""" """
# Убедимся, что директория для сохранения токена существует
os.makedirs(APP_DATA_DIR, exist_ok=True) os.makedirs(APP_DATA_DIR, exist_ok=True)
# Вычисляем время истечения токена: текущее время + заданный срок действия
expiration_time = time.time() + expires_in expiration_time = time.time() + expires_in
data = { data = {
"token": token, "token": token,
@@ -37,11 +34,9 @@ def save_token(token, expires_in=3600):
try: try:
with open(TOKEN_FILE, "w") as f: with open(TOKEN_FILE, "w") as f:
json.dump(data, f) json.dump(data, f)
# Выводим информацию о сохранении токена и времени его истечения
print( print(
f"Токен сохранен в {TOKEN_FILE}. Срок действия истекает {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}") f"Токен сохранен в {TOKEN_FILE}. Срок действия истекает {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}")
except IOError as e: except IOError as e:
# Обрабатываем ошибки ввода/вывода при сохранении файла
print(f"Ошибка сохранения токена: {e}") print(f"Ошибка сохранения токена: {e}")
@@ -51,7 +46,6 @@ def load_token():
Возвращает (токен, время_истечения_unix) или (None, None). Возвращает (токен, время_истечения_unix) или (None, None).
""" """
try: try:
# Проверяем, существует ли файл токена перед попыткой чтения
if not os.path.exists(TOKEN_FILE): if not os.path.exists(TOKEN_FILE):
print(f"Файл токена не найден по пути {TOKEN_FILE}.") print(f"Файл токена не найден по пути {TOKEN_FILE}.")
return None, None return None, None
@@ -61,37 +55,30 @@ def load_token():
token = data.get("token") token = data.get("token")
expiration_time = data.get("expiration_time") expiration_time = data.get("expiration_time")
# Проверяем, существует ли токен, время истечения и не просрочен ли он
if token and expiration_time and expiration_time > time.time(): if token and expiration_time and expiration_time > time.time():
# Выводим информацию о загруженном токене и его действительности
print( print(
f"Токен загружен из {TOKEN_FILE}. Действителен до {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}") f"Токен загружен из {TOKEN_FILE}. Действителен до {QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()}")
return token, expiration_time return token, expiration_time
else: else:
print("Токен просрочен или недействителен.") print("Токен просрочен или недействителен.")
# Если токен просрочен или недействителен, удаляем файл токена
if os.path.exists(TOKEN_FILE): if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE) os.remove(TOKEN_FILE)
return None, None return None, None
except (IOError, json.JSONDecodeError) as e: except (IOError, json.JSONDecodeError) as e:
# Обрабатываем ошибки ввода/вывода или ошибки декодирования JSON
print(f"Ошибка загрузки токена: {e}") print(f"Ошибка загрузки токена: {e}")
return None, None return None, None
# --- WebEnginePage для VK OAuth ---
class WebEnginePage(QWebEnginePage): class WebEnginePage(QWebEnginePage):
""" """
Класс для обработки навигационных запросов в QWebEngineView, Класс для обработки навигационных запросов в QWebEngineView,
специально для извлечения токена авторизации VK. специально для извлечения токена авторизации VK.
""" """
# Изменена сигнатура: parent для QWebEnginePage и browser_window_instance для логической связи # Добавлен параметр profile в конструктор, чтобы использовать пользовательский профиль
def __init__(self, parent=None, browser_window_instance=None): def __init__(self, profile=None, parent=None, browser_window_instance=None):
super().__init__(parent) # Передаем графический parent в базовый класс super().__init__(profile, parent) # Передаем profile в базовый класс
# Сохраняем ссылку на окно браузера для вызова его методов
self.parent_browser_window = browser_window_instance self.parent_browser_window = browser_window_instance
# Флаг, чтобы избежать многократного извлечения токена при перенаправлении
self.token_extracted = False self.token_extracted = False
def acceptNavigationRequest(self, url, _type, isMainFrame): def acceptNavigationRequest(self, url, _type, isMainFrame):
@@ -99,15 +86,11 @@ class WebEnginePage(QWebEnginePage):
Переопределенный метод для перехвата URL-адреса, содержащего токен доступа. Переопределенный метод для перехвата URL-адреса, содержащего токен доступа.
""" """
url_string = url.toString() url_string = url.toString()
# Проверяем, содержит ли URL 'access_token' и не был ли токен уже извлечен
if "access_token" in url_string and not self.token_extracted: if "access_token" in url_string and not self.token_extracted:
self.token_extracted = True self.token_extracted = True
# Если ссылка на родительское окно браузера существует, вызываем его метод обработки токена
if self.parent_browser_window: if self.parent_browser_window:
self.parent_browser_window.process_auth_url(url_string) self.parent_browser_window.process_auth_url(url_string)
# Предотвращаем дальнейшую навигацию браузера после извлечения токена
return False return False
# Для всех остальных запросов разрешаем навигацию по умолчанию
return super().acceptNavigationRequest(url, _type, isMainFrame) return super().acceptNavigationRequest(url, _type, isMainFrame)
@@ -115,22 +98,29 @@ class AuthBrowserWindow(QDialog):
""" """
Отдельное окно-диалог для проведения OAuth авторизации VK. Отдельное окно-диалог для проведения OAuth авторизации VK.
""" """
# Сигнал, который будет испускать токен доступа и его срок действия (в секундах)
token_extracted_signal = Signal(str, int) token_extracted_signal = Signal(str, int)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Авторизация VK") self.setWindowTitle("Авторизация VK")
self.setGeometry(350, 350, 800, 600) # Размеры окна авторизации self.setGeometry(350, 350, 800, 600)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.browser = QWebEngineView() 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) self.browser.setPage(self.browser.page)
layout.addWidget(self.browser) layout.addWidget(self.browser)
# Статусная строка в окне авторизации
self.status_label = QLabel("Ожидание авторизации...") self.status_label = QLabel("Ожидание авторизации...")
self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
@@ -139,12 +129,12 @@ class AuthBrowserWindow(QDialog):
"""Запускает OAuth авторизацию VK в этом окне.""" """Запускает OAuth авторизацию VK в этом окне."""
auth_url = ( auth_url = (
"https://oauth.vk.com/authorize?" "https://oauth.vk.com/authorize?"
"client_id=6287487&" # Официальный client_id VK для Android "client_id=6287487&"
"display=page&" "display=page&"
"redirect_uri=https://oauth.vk.com/blank.html&" "redirect_uri=https://oauth.vk.com/blank.html&"
"scope=1073737727&" # Широкий scope для работы с сообщениями и чатами "scope=1073737727&"
"response_type=token&" "response_type=token&"
"v=5.131" # Версия API VK "v=5.131"
) )
self.browser.setUrl(QUrl(auth_url)) self.browser.setUrl(QUrl(auth_url))
self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...") self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...")
@@ -154,10 +144,9 @@ class AuthBrowserWindow(QDialog):
Извлекает токен доступа из URL перенаправления и испускает сигнал. Извлекает токен доступа из URL перенаправления и испускает сигнал.
""" """
token = None token = None
expires_in = 3600 # По умолчанию 1 час, если не найден в URL expires_in = 3600
parsed = urlparse(url_string) parsed = urlparse(url_string)
# 1. Попытка стандартного парсинга URL-параметров
if parsed.fragment: if parsed.fragment:
params = parse_qs(parsed.fragment) params = parse_qs(parsed.fragment)
else: else:
@@ -169,9 +158,8 @@ class AuthBrowserWindow(QDialog):
try: try:
expires_in = int(params['expires_in'][0]) expires_in = int(params['expires_in'][0])
except ValueError: except ValueError:
pass # Используем значение по умолчанию, если преобразование не удалось pass
# 2. Если стандартный парсинг не дал результат, пробуем строковый поиск по маркерам
if not token: if not token:
start_marker = "access_token%253D" start_marker = "access_token%253D"
end_marker = "%25" end_marker = "%25"
@@ -190,16 +178,31 @@ class AuthBrowserWindow(QDialog):
raw_token = remaining_url[:amp_index] raw_token = remaining_url[:amp_index]
else: else:
raw_token = remaining_url raw_token = remaining_url
token = unquote(raw_token) token = unquote(raw_token)
if token: if token:
self.token_extracted_signal.emit(token, expires_in) # Испускаем сигнал с токеном и сроком действия self.token_extracted_signal.emit(token, expires_in)
self.accept() # Закрываем диалог с результатом QDialog.Accepted self.accept()
else: else:
QMessageBox.warning(self, "Ошибка Авторизации", QMessageBox.warning(self, "Ошибка Авторизации",
"Не удалось получить токен. Проверьте URL или попробуйте еще раз.") "Не удалось получить токен. Проверьте 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): class VkChatManager(QMainWindow):
@@ -214,25 +217,25 @@ class VkChatManager(QMainWindow):
self.setGeometry(300, 300, 600, 800) self.setGeometry(300, 300, 600, 800)
self.token = None self.token = None
self.token_expiration_time = None # Храним время истечения токена в Unix-таймстамп self.token_expiration_time = None
self.chats = [] self.chats = []
self.chat_checkboxes = [] self.chat_checkboxes = []
self.vk_session = None self.vk_session = None
self.vk = None self.vk = None
self.user_id_to_process = None # Новое поле для хранения ID пользователя из ссылки
self.init_ui() self.init_ui()
self.load_saved_token_on_startup() self.load_saved_token_on_startup()
self.setup_token_timer() # Настройка таймера self.setup_token_timer()
def init_ui(self): def init_ui(self):
"""Инициализирует пользовательский интерфейс приложения.""" """Инициализирует пользовательский интерфейс приложения."""
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10) # Уменьшаем общие отступы от краев окна layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5) # Уменьшаем стандартный отступ между элементами layout.setSpacing(5)
# Инструкции для пользователя
self.instructions = QTextBrowser() self.instructions = QTextBrowser()
self.instructions.setPlainText( self.instructions.setPlainText(
"Инструкция:\n" "Инструкция:\n"
@@ -241,34 +244,31 @@ class VkChatManager(QMainWindow):
"3. Разрешите доступ приложению\n" "3. Разрешите доступ приложению\n"
"4. Токен автоматически сохранится на 1 час\n" "4. Токен автоматически сохранится на 1 час\n"
"5. Выберите один или несколько чатов, установив галочки\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(self.instructions)
# Поле для токена
layout.addWidget(QLabel("Access Token VK:")) layout.addWidget(QLabel("Access Token VK:"))
self.token_input = QLineEdit() self.token_input = QLineEdit()
self.token_input.setPlaceholderText("Токен появится здесь после авторизации...") self.token_input.setPlaceholderText("Токен появится здесь после авторизации...")
self.token_input.setReadOnly(True) self.token_input.setReadOnly(True)
layout.addWidget(self.token_input) layout.addWidget(self.token_input)
# Метка таймера срока действия токена
self.token_timer_label = QLabel("Срок действия токена: Н") self.token_timer_label = QLabel("Срок действия токена: Н")
self.token_timer_label.setAlignment(Qt.AlignRight) # Выравнивание по правому краю self.token_timer_label.setAlignment(Qt.AlignRight)
self.token_timer_label.setStyleSheet("font-weight: bold; color: #555;") # Сделать немного более заметным self.token_timer_label.setStyleSheet("font-weight: bold; color: #555;")
layout.addWidget(self.token_timer_label) layout.addWidget(self.token_timer_label)
# Кнопка авторизации
self.auth_btn = QPushButton("Авторизоваться через VK") self.auth_btn = QPushButton("Авторизоваться через VK")
self.auth_btn.clicked.connect(self.start_auth) self.auth_btn.clicked.connect(self.start_auth)
layout.addWidget(self.auth_btn) layout.addWidget(self.auth_btn)
# Секция выбора чатов
layout.addWidget(QLabel("Выберите чаты:")) layout.addWidget(QLabel("Выберите чаты:"))
self.chat_checkbox_layout = QVBoxLayout() 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 = QWidget()
self.chat_checkbox_widget.setLayout(self.chat_checkbox_layout) self.chat_checkbox_widget.setLayout(self.chat_checkbox_layout)
@@ -297,52 +297,36 @@ class VkChatManager(QMainWindow):
layout.addLayout(select_buttons_layout) layout.addLayout(select_buttons_layout)
layout.addWidget(self.chat_scroll_area) layout.addWidget(self.chat_scroll_area)
# Секция получения ID по ссылке layout.addWidget(QLabel("Введите или вставьте ссылку на страницу VK (ID будет получен автоматически):"))
layout.addWidget(QLabel("Получить ID по ссылке VK:"))
self.vk_url_input = QLineEdit() 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) 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.remove_user_btn = QPushButton("ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ")
self.resolve_id_btn.setEnabled(False) self.remove_user_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout.addWidget(self.resolve_id_btn) 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 = QCheckBox("Показать 250 последних сообщений при добавлении")
self.visible_messages_checkbox.setChecked(False) # По умолчанию не отмечен self.visible_messages_checkbox.setChecked(False)
layout.addWidget(self.visible_messages_checkbox) layout.addWidget(self.visible_messages_checkbox)
# Кнопка добавления self.add_user_btn = QPushButton("ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ")
self.add_btn = QPushButton("Добавить в выбранные чаты") self.add_user_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.add_btn.setEnabled(False) self.add_user_btn.setMinimumHeight(60)
self.add_btn.clicked.connect(self.add_user_to_chat) self.add_user_btn.clicked.connect(self.add_user_to_chat)
layout.addWidget(self.add_btn) self.add_user_btn.setEnabled(False) # Изначально отключена
layout.addWidget(self.add_user_btn)
# Статус приложения
self.status_label = QLabel("Статус: не авторизован") self.status_label = QLabel("Статус: не авторизован")
self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
layout.addStretch(1) # Добавляем растяжение внизу layout.addStretch(1)
central_widget.setLayout(layout) central_widget.setLayout(layout)
@@ -350,8 +334,6 @@ class VkChatManager(QMainWindow):
"""Настраивает QTimer для обновления отображения обратного отсчета токена.""" """Настраивает QTimer для обновления отображения обратного отсчета токена."""
self.token_countdown_timer = QTimer(self) self.token_countdown_timer = QTimer(self)
self.token_countdown_timer.timeout.connect(self.update_token_timer_display) self.token_countdown_timer.timeout.connect(self.update_token_timer_display)
# Таймер срабатывает каждую 1000 миллисекунд (1 секунду)
# Он запускается, когда токен загружен/получен
self.token_countdown_timer.start(1000) self.token_countdown_timer.start(1000)
def update_token_timer_display(self): def update_token_timer_display(self):
@@ -366,23 +348,23 @@ class VkChatManager(QMainWindow):
if remaining_seconds <= 0: if remaining_seconds <= 0:
self.token_timer_label.setText("Срок действия токена истек!") 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.auth_btn.setEnabled(True)
self.set_ui_state(False) # Новый метод для последовательной установки состояния UI self.set_ui_state(False)
self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.") self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.")
self.token = None # Очищаем токен self.token = None
self.token_expiration_time = None self.token_expiration_time = None
self.token_input.clear()
return return
minutes, seconds = divmod(remaining_seconds, 60) minutes, seconds = divmod(remaining_seconds, 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
# Форматируем отображение времени
time_str = "" time_str = ""
if hours > 0: if hours > 0:
time_str += f"{hours:02d}ч " 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"{minutes:02d}м "
time_str += f"{seconds:02d}с" time_str += f"{seconds:02d}с"
@@ -394,12 +376,10 @@ class VkChatManager(QMainWindow):
self.deselect_all_btn.setEnabled(authorized) self.deselect_all_btn.setEnabled(authorized)
self.refresh_chats_btn.setEnabled(authorized) self.refresh_chats_btn.setEnabled(authorized)
self.vk_url_input.setEnabled(authorized) self.vk_url_input.setEnabled(authorized)
self.resolve_id_btn.setEnabled(authorized) # Состояние кнопок теперь зависит от наличия user_id_to_process
self.user_remove_input.setEnabled(authorized) self.remove_user_btn.setEnabled(authorized and self.user_id_to_process is not None)
self.remove_btn.setEnabled(authorized)
self.user_add_input.setEnabled(authorized)
self.visible_messages_checkbox.setEnabled(authorized) 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: if authorized:
self.chat_scroll_area.show() self.chat_scroll_area.show()
@@ -412,20 +392,19 @@ class VkChatManager(QMainWindow):
if loaded_token: if loaded_token:
self.token = loaded_token self.token = loaded_token
self.token_expiration_time = expiration_time 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.status_label.setText("Статус: авторизован (токен загружен из файла)")
self.auth_btn.setEnabled(False) # Отключаем кнопку авторизации self.auth_btn.setEnabled(False)
self.set_ui_state(True) # Устанавливаем состояние UI self.set_ui_state(True)
self.update_token_timer_display() # Начальное обновление отображения self.update_token_timer_display()
# Инициализируем VK API с загруженным токеном
self.vk_session = vk_api.VkApi(token=self.token) self.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api() self.vk = self.vk_session.get_api()
self.load_chats() # Загружаем чаты self.load_chats()
else: else:
self.status_label.setText("Статус: не авторизован (токен не найден или просрочен)") self.status_label.setText("Статус: не авторизован (токен не найден или просрочен)")
self.set_ui_state(False) # Устанавливаем состояние UI self.set_ui_state(False)
self.update_token_timer_display() # Обновляем отображение на Н self.update_token_timer_display()
def set_all_checkboxes(self, checked): def set_all_checkboxes(self, checked):
""" """
@@ -440,10 +419,9 @@ class VkChatManager(QMainWindow):
""" """
self.status_label.setText("Статус: ожидание авторизации в новом окне...") self.status_label.setText("Статус: ожидание авторизации в новом окне...")
auth_window = AuthBrowserWindow(self) auth_window = AuthBrowserWindow(self)
# Подключаем сигнал из AuthBrowserWindow к нашему обработчику, включая expires_in
auth_window.token_extracted_signal.connect(self.handle_auth_token) auth_window.token_extracted_signal.connect(self.handle_auth_token)
auth_window.start_auth_flow() auth_window.start_auth_flow()
auth_window.exec_() # Открываем окно как модальный диалог auth_window.exec() # Изменено с exec_() на exec()
def handle_auth_token(self, token, expires_in): def handle_auth_token(self, token, expires_in):
""" """
@@ -451,150 +429,207 @@ class VkChatManager(QMainWindow):
""" """
if token: if token:
self.token = token self.token = token
# Вычисляем время истечения на основе текущего времени + expires_in
self.token_expiration_time = time.time() + expires_in self.token_expiration_time = time.time() + expires_in
save_token(self.token, expires_in) # Сохраняем токен save_token(self.token, expires_in)
self.token_input.setText(self.token[:50] + "...") # Отображаем часть токена self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован") self.status_label.setText("Статус: авторизован")
self.auth_btn.setEnabled(False) # Отключаем кнопку авторизации self.auth_btn.setEnabled(False)
self.set_ui_state(True) # Устанавливаем состояние UI self.set_ui_state(True)
self.update_token_timer_display() # Начальное обновление отображения self.update_token_timer_display()
# Инициализируем VK API с полученным токеном
self.vk_session = vk_api.VkApi(token=self.token) self.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api() self.vk = self.vk_session.get_api()
self.load_chats() # Загружаем чаты self.load_chats()
else: else:
QMessageBox.warning(self, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.") QMessageBox.warning(self, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.")
self.status_label.setText("Статус: Авторизация не удалась") self.status_label.setText("Статус: Авторизация не удалась")
self.set_ui_state(False) # Устанавливаем состояние UI self.set_ui_state(False)
self.update_token_timer_display() # Обновляем отображение на Н self.update_token_timer_display()
def load_chats(self): def load_chats(self):
""" """
Загружает список чатов пользователя из VK API и заполняет их чекбоксами. Загружает список чатов пользователя из VK API и заполняет их чекбоксами.
""" """
# Очищаем существующие чекбоксы из макета
for i in reversed(range(self.chat_checkbox_layout.count())): for i in reversed(range(self.chat_checkbox_layout.count())):
widget_to_remove = self.chat_checkbox_layout.itemAt(i).widget() widget_to_remove = self.chat_checkbox_layout.itemAt(i).widget()
if widget_to_remove: if widget_to_remove:
widget_to_remove.setParent(None) # Отвязываем от родителя widget_to_remove.setParent(None)
widget_to_remove.deleteLater() # Помечаем на удаление widget_to_remove.deleteLater()
self.chat_checkboxes.clear() # Очищаем список ссылок на чекбоксы self.chat_checkboxes.clear()
self.chats.clear() # Очищаем список данных о чатах self.chats.clear()
try: try:
# Получаем до 200 последних бесед, включая чаты
conversations = self.vk.messages.getConversations(count=200)['items'] conversations = self.vk.messages.getConversations(count=200)['items']
for conv in conversations: for conv in conversations:
# Фильтруем только чаты (peer type 'chat')
if conv['conversation']['peer']['type'] == 'chat': if conv['conversation']['peer']['type'] == 'chat':
chat_id = conv['conversation']['peer']['local_id'] chat_id = conv['conversation']['peer']['local_id']
title = conv['conversation']['chat_settings']['title'] title = conv['conversation']['chat_settings']['title']
chat_data = {'id': chat_id, 'title': title} chat_data = {'id': chat_id, 'title': title}
self.chats.append(chat_data) self.chats.append(chat_data)
# Создаем новый чекбокс для каждого чата
checkbox = QCheckBox(f"{title} (id: {chat_id})") checkbox = QCheckBox(f"{title} (id: {chat_id})")
checkbox.setChecked(False) # По умолчанию не выбран checkbox.setChecked(False)
# Сохраняем chat_id в свойстве чекбокса для удобства
checkbox.setProperty("chat_id", chat_id) checkbox.setProperty("chat_id", chat_id)
self.chat_checkbox_layout.addWidget(checkbox) # Добавляем чекбокс в макет self.chat_checkbox_layout.addWidget(checkbox)
self.chat_checkboxes.append(checkbox) # Добавляем ссылку на чекбокс в список self.chat_checkboxes.append(checkbox)
if not self.chats: if not self.chats:
QMessageBox.information(self, "Информация", "У вас нет доступных чатов.") QMessageBox.information(self, "Информация", "У вас нет доступных чатов.")
self.chat_scroll_area.hide() # Скрываем область, если чатов нет self.chat_scroll_area.hide()
self.select_all_btn.setEnabled(False) # Отключаем кнопки выбора self.select_all_btn.setEnabled(False)
self.deselect_all_btn.setEnabled(False) self.deselect_all_btn.setEnabled(False)
self.refresh_chats_btn.setEnabled(False) # Отключаем кнопку обновления self.refresh_chats_btn.setEnabled(False)
else: else:
self.chat_scroll_area.show() # Показываем область с чатами self.chat_scroll_area.show()
self.select_all_btn.setEnabled(True) # Включаем кнопки выбора self.select_all_btn.setEnabled(True)
self.deselect_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: except VkApiError as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить чаты: {e}\n" error_message = str(e)
"Убедитесь, что у приложения есть необходимые права доступа.") if "[5] User authorization failed: access_token was given to another ip address" in error_message:
self.chat_scroll_area.hide() # Скрываем область при ошибке QMessageBox.critical(self, "Ошибка авторизации VK",
self.select_all_btn.setEnabled(False) # Отключаем кнопки выбора "Ваш IP-адрес изменился, и токен стал недействительным. "
self.deselect_all_btn.setEnabled(False) "Пожалуйста, авторизуйтесь заново.")
self.refresh_chats_btn.setEnabled(False) # Отключаем кнопку обновления 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: if not self.vk or not text:
QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!") self.user_id_to_process = None
self.set_ui_state(self.token is not None)
return 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: if not vk_url:
QMessageBox.warning(self, "Ошибка", "Введите ссылку на страницу VK!") self.user_id_to_process = None
self.set_ui_state(self.token is not None)
return return
try: try:
# Парсим URL, чтобы извлечь screen_name
parsed_url = urlparse(vk_url) parsed_url = urlparse(vk_url)
path_parts = parsed_url.path.split('/') path_parts = parsed_url.path.split('/')
# screen_name обычно последний элемент в пути
# Обработка случая с завершающим слешем или пустым путем
screen_name = '' screen_name = ''
if path_parts: if path_parts:
screen_name = path_parts[-1] 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] screen_name = path_parts[-2]
if not screen_name: if not screen_name:
QMessageBox.warning(self, "Ошибка", self.user_id_to_process = None
"Не удалось извлечь имя страницы (screen_name) из ссылки. Проверьте формат ссылки.") self.status_label.setText("Статус: Не удалось извлечь имя страницы из ссылки.")
self.set_ui_state(self.token is not None)
return return
# Используем messages.resolveScreenName для получения ID пользователя
resolved_object = self.vk.utils.resolveScreenName(screen_name=screen_name) resolved_object = self.vk.utils.resolveScreenName(screen_name=screen_name)
if resolved_object and 'object_id' in resolved_object and resolved_object['type'] == 'user': if resolved_object and 'object_id' in resolved_object and resolved_object['type'] == 'user':
user_id = resolved_object['object_id'] self.user_id_to_process = resolved_object['object_id']
self.user_remove_input.setText(str(user_id)) self.status_label.setText(f"Статус: ID пользователя {self.user_id_to_process} успешно получен.")
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': elif resolved_object and 'type' in resolved_object and resolved_object['type'] != 'user':
QMessageBox.warning(self, "Ошибка", self.user_id_to_process = None
f"Ссылка ведет на {resolved_object['type']}, а не на страницу пользователя. Пожалуйста, введите ссылку на страницу пользователя.") self.status_label.setText(f"Статус: Ссылка ведет на {resolved_object['type']}, а не на пользователя.")
else: else:
QMessageBox.warning(self, "Ошибка", self.user_id_to_process = None
"Не удалось найти пользователя по этой ссылке. Проверьте корректность ссылки.") self.status_label.setText("Статус: Не удалось найти пользователя по этой ссылке.")
except VkApiError as e: 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: 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): def remove_user(self):
""" """
Удаляет пользователя из всех выбранных чатов. Исключает пользователя из всех выбранных чатов.
Перед удалением выводит окно подтверждения. Перед исключением выводит окно подтверждения.
""" """
if not self.token: if not self.token:
QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!") QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!")
return return
user_id_str = self.user_remove_input.text().strip() # Используем user_remove_input if self.user_id_to_process is None:
if not user_id_str or not user_id_str.isdigit(): QMessageBox.warning(self, "Ошибка",
QMessageBox.warning(self, "Ошибка", "Введите корректный ID пользователя (только цифры) для удаления!") "ID пользователя не получен. Пожалуйста, введите корректную ссылку и убедитесь, что ID определился.")
return 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_ids = []
selected_chat_titles = [] # Список для названий выбранных чатов selected_chat_titles = []
# Собираем ID и названия всех выбранных чатов
for checkbox in self.chat_checkboxes: for checkbox in self.chat_checkboxes:
if checkbox.isChecked(): if checkbox.isChecked():
chat_id = checkbox.property("chat_id") chat_id = checkbox.property("chat_id")
@@ -604,74 +639,88 @@ class VkChatManager(QMainWindow):
selected_chat_titles.append(chat_title) selected_chat_titles.append(chat_title)
if not selected_chat_ids: if not selected_chat_ids:
QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для удаления пользователя!") QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для исключения пользователя!")
return return
# Создаем текст подтверждения со списком чатов
confirmation_message = ( confirmation_message = (
f"Вы точно хотите удалить пользователя с ID: {user_id} из следующих чатов?\n\n" f"Вы точно хотите исключить пользователя '{user_info}' (ID: {user_id}) из следующих чатов?\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Это действие необратимо." "\n\nЭто действие необратимо."
) )
# Выводим окно подтверждения reply = QMessageBox.question(self, "Подтверждение исключения",
reply = QMessageBox.question(self, "Подтверждение удаления",
confirmation_message, confirmation_message,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
results = [] # Список для сбора результатов по каждому чату results = []
for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles): for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles):
try: try:
self.vk.messages.removeChatUser(chat_id=chat_id, user_id=user_id) 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: except VkApiError as e:
error_message = str(e) error_message = str(e)
# Более детальная обработка известных ошибок VK API
if "[15] Access denied: user cannot be removed from this chat" in error_message: if "[15] Access denied: user cannot be removed from this chat" in error_message:
results.append( 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: elif "[100] One of the parameters specified was missing or invalid: user_id is invalid" in error_message:
results.append( 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: elif "[900] Cannot remove yourself" in error_message:
results.append( 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: else:
results.append( 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: 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))
QMessageBox.information(self, "Результаты удаления", "\n".join(results)) self.vk_url_input.clear()
self.user_remove_input.clear() # Очищаем поле ввода пользователя self.user_id_to_process = None
self.set_ui_state(self.token is not None)
else: else:
QMessageBox.information(self, "Отмена", "Операция удаления отменена.") QMessageBox.information(self, "Отмена", "Операция исключения отменена.")
def add_user_to_chat(self): def add_user_to_chat(self):
""" """
Добавляет пользователя во все выбранные чаты. Приглашает пользователя во все выбранные чаты.
Перед добавлением выводит окно подтверждения. Перед приглашением выводит окно подтверждения.
""" """
if not self.token: if not self.token:
QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!") QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь!")
return return
user_id_str = self.user_add_input.text().strip() # Используем user_add_input if self.user_id_to_process is None:
if not user_id_str or not user_id_str.isdigit(): QMessageBox.warning(self, "Ошибка",
QMessageBox.warning(self, "Ошибка", "Введите корректный ID пользователя (только цифры) для добавления!") "ID пользователя не получен. Пожалуйста, введите корректную ссылку и убедитесь, что ID определился.")
return 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 visible_messages_count = None
if self.visible_messages_checkbox.isChecked(): if self.visible_messages_checkbox.isChecked():
visible_messages_count = 250 # Если галочка отмечена, устанавливаем значение 250 visible_messages_count = 250
selected_chat_ids = [] selected_chat_ids = []
selected_chat_titles = [] # Список для названий выбранных чатов selected_chat_titles = []
# Собираем ID и названия всех выбранных чатов
for checkbox in self.chat_checkboxes: for checkbox in self.chat_checkboxes:
if checkbox.isChecked(): if checkbox.isChecked():
chat_id = checkbox.property("chat_id") chat_id = checkbox.property("chat_id")
@@ -681,73 +730,82 @@ class VkChatManager(QMainWindow):
selected_chat_titles.append(chat_title) selected_chat_titles.append(chat_title)
if not selected_chat_ids: if not selected_chat_ids:
QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для добавления пользователя!") QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат для приглашения пользователя!")
return return
# Создаем текст подтверждения со списком чатов
confirmation_message = ( 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" 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Это действие может привести к отправке уведомлений пользователю." "\n\nЭто действие может привести к отправке уведомлений пользователю."
) )
# Выводим окно подтверждения reply = QMessageBox.question(self, "Подтверждение приглашения",
reply = QMessageBox.question(self, "Подтверждение добавления",
confirmation_message, confirmation_message,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
results = [] # Список для сбора результатов по каждому чату results = []
for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles): for chat_id, chat_title in zip(selected_chat_ids, selected_chat_titles):
try: try:
# Создаем словарь параметров для addChatUser
params = { params = {
'chat_id': chat_id, 'chat_id': chat_id,
'user_id': user_id 'user_id': user_id
} }
if visible_messages_count is not None: if visible_messages_count is not None:
# Добавляем visible_messages_count, как запрошено, несмотря на документацию
params['visible_messages_count'] = visible_messages_count params['visible_messages_count'] = visible_messages_count
results.append( results.append(
f"Внимание: Параметр 'visible_messages_count' (значение: {visible_messages_count}) передан в messages.addChatUser для чата '{chat_title}' (ID: {chat_id}). Согласно официальной документации VK API, этот параметр не поддерживается для данного метода и может быть проигнорирован или привести к ошибке.") f"Внимание: Параметр 'visible_messages_count' (значение: {visible_messages_count}) передан в messages.addChatUser для чата '{chat_title}' (ID: {chat_id}). Согласно официальной документации VK API, этот параметр не поддерживается для данного метода и может быть проигнорирован или привести к ошибке.")
self.vk.messages.addChatUser(**params) # Передаем параметры как именованные аргументы self.vk.messages.addChatUser(**params)
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: except VkApiError as e:
error_message = str(e) error_message = str(e)
# Детальная обработка известных ошибок VK API для добавления
if "[917] You don't have access to this chat" in error_message: if "[917] You don't have access to this chat" in error_message:
results.append( 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: elif "[935] User has been invited to this chat" in error_message:
results.append( 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: elif "[100] One of the parameters specified was missing or invalid: user_id is invalid" in error_message:
results.append( 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: else:
results.append( 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: 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))
QMessageBox.information(self, "Результаты добавления", "\n".join(results)) self.visible_messages_checkbox.setChecked(False)
self.user_add_input.clear() # Очищаем поле ввода пользователя self.vk_url_input.clear()
self.visible_messages_checkbox.setChecked(False) # Сбрасываем чекбокс self.user_id_to_process = None
self.set_ui_state(self.token is not None)
else: else:
QMessageBox.information(self, "Отмена", "Операция добавления отменена.") QMessageBox.information(self, "Отмена", "Операция приглашения отменена.")
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Устанавливаем стиль приложения на "Fusion".
app.setStyle("Fusion") app.setStyle("Fusion")
# Применяем стандартную палитру, предоставленную текущим стилем.
app.setPalette(app.style().standardPalette()) app.setPalette(app.style().standardPalette())
window = VkChatManager() window = VkChatManager()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec())