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