Initial Commit

This commit is contained in:
benya
2025-06-23 00:35:12 +03:00
commit 7e2ac63523
3 changed files with 758 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/.venv/
/.venv1/
/.venv3/

753
main.py Normal file
View File

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

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
PySide6~=6.9.1
vk-api~=11.9.9