feat: Добавлены массовые операции и разделение чатов на вкладки

- Добавлена кнопка "Список" для одновременной обработки нескольких пользователей.
- Реализовано разделение чатов на вкладки "Офис", "Розница" и "Прочие".

fix: Улучшен интерфейс и исправлены ошибки

- Область списка чатов теперь корректно увеличивается после авторизации.
- В окне подтверждения отображаются имена всех пользователей.
- Исправлены грамматика и склонения в диалоговых окнах.
- Кнопки в диалогах заменены на русскоязычные ("Да"/"Нет", "ОК"/"Отмена").
This commit is contained in:
Alex
2025-07-24 23:01:59 +03:00
parent 0dbd71c036
commit 32e30f5484

808
main.py
View File

@@ -5,10 +5,11 @@ import time
import os
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QWidget, QMessageBox,
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout, QSizePolicy, QDialog)
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout,
QSizePolicy, QDialog, QTextEdit, QTabWidget, QDialogButtonBox)
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile # Добавлен QWebEngineProfile
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineProfile
from urllib.parse import urlparse, parse_qs, unquote
from vk_api.exceptions import VkApiError
from PySide6.QtCore import QStandardPaths
@@ -16,7 +17,6 @@ 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")
@@ -205,12 +205,28 @@ class AuthBrowserWindow(QDialog):
super().closeEvent(event)
class VkChatManager(QMainWindow):
"""
Главное окно приложения VK Chat Manager.
Позволяет авторизоваться через VK и удалять/добавлять пользователей в чаты.
"""
class MultiLinkDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Ввод нескольких ссылок")
self.setMinimumSize(400, 300)
layout = QVBoxLayout(self)
label = QLabel("Вставьте ссылки на страницы VK, каждая с новой строки:")
layout.addWidget(label)
self.links_text_edit = QTextEdit()
layout.addWidget(self.links_text_edit)
button_box = QDialogButtonBox()
button_box.addButton("ОК", QDialogButtonBox.AcceptRole)
button_box.addButton("Отмена", QDialogButtonBox.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
def get_links(self):
return [line.strip() for line in self.links_text_edit.toPlainText().strip().split('\n') if line.strip()]
class VkChatManager(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Anabasis Chat Manager")
@@ -219,35 +235,39 @@ class VkChatManager(QMainWindow):
self.token = None
self.token_expiration_time = None
self.chats = []
self.chat_checkboxes = []
self.office_chat_checkboxes = []
self.retail_chat_checkboxes = []
self.other_chat_checkboxes = []
self.vk_session = None
self.vk = None
self.user_id_to_process = None # Новое поле для хранения ID пользователя из ссылки
self.user_ids_to_process = []
self.resolve_timer = QTimer(self)
self.resolve_timer.setSingleShot(True)
self.resolve_timer.setInterval(750)
self.resolve_timer.timeout.connect(self.resolve_single_user_id_from_input)
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 = QVBoxLayout(central_widget)
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. Нажмите соответствующую кнопку 'ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ' или 'ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ'."
"1. Авторизуйтесь через VK.\n"
"2. Выберите чаты.\n"
"3. Вставьте ссылку на пользователя в поле ниже. ID определится автоматически.\n"
"4. Для массовых операций, нажмите кнопку 'Список' и вставьте ссылки в окне.\n"
"5. Нажмите 'ИСКЛЮЧИТЬ' или 'ПРИГЛАСИТЬ'."
)
self.instructions.setFixedHeight(180)
self.instructions.setFixedHeight(120)
layout.addWidget(self.instructions)
layout.addWidget(QLabel("Access Token VK:"))
@@ -258,547 +278,397 @@ class VkChatManager(QMainWindow):
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)
self.chat_tabs = QTabWidget()
self.chat_tabs.hide()
self.office_tab = self.create_chat_tab()
self.chat_tabs.addTab(self.office_tab, "AG Офис")
self.retail_tab = self.create_chat_tab()
self.chat_tabs.addTab(self.retail_tab, "AG Розница")
self.other_tab = self.create_chat_tab()
self.chat_tabs.addTab(self.other_tab, "Прочие")
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)
select_buttons_layout = QHBoxLayout()
self.select_all_btn = QPushButton("Выбрать все на вкладке")
self.select_all_btn.clicked.connect(lambda: self.set_all_checkboxes_on_current_tab(True))
self.deselect_all_btn = QPushButton("Снять выбор на вкладке")
self.deselect_all_btn.clicked.connect(lambda: self.set_all_checkboxes_on_current_tab(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(self.chat_tabs)
layout.addWidget(QLabel("Введите или вставьте ссылку на страницу VK (ID будет получен автоматически):"))
layout.addWidget(QLabel("Ссылка на страницу VK (ID определится автоматически):"))
link_input_layout = QHBoxLayout()
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.vk_url_input.setPlaceholderText("https://vk.com/id1")
self.vk_url_input.textChanged.connect(self.on_vk_url_input_changed)
link_input_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.multi_link_btn = QPushButton("Список")
self.multi_link_btn.setToolTip("Ввести несколько ссылок списком")
self.multi_link_btn.clicked.connect(self.open_multi_link_dialog)
link_input_layout.addWidget(self.multi_link_btn)
layout.addLayout(link_input_layout)
self.remove_user_btn = QPushButton("ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЕЙ")
self.remove_user_btn.setMinimumHeight(50)
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 = QPushButton("ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЕЙ")
self.add_user_btn.setMinimumHeight(50)
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)
self.set_ui_state(False)
def on_vk_url_input_changed(self, text):
if self.vk_url_input.hasFocus():
self.resolve_timer.start()
def open_multi_link_dialog(self):
dialog = MultiLinkDialog(self)
if dialog.exec():
links = dialog.get_links()
if links:
self.vk_url_input.clear()
self._process_links_list(links)
else:
QMessageBox.information(self, "Информация", "Список ссылок пуст.")
def resolve_single_user_id_from_input(self):
url = self.vk_url_input.text().strip()
if not url:
self.user_ids_to_process.clear()
self.status_label.setText("Статус: Введите ссылку или откройте список.")
self.set_ui_state(self.token is not None)
return
self._process_links_list([url])
def _process_links_list(self, links_list):
if not self.vk:
QMessageBox.warning(self, "Ошибка", "Сначала авторизуйтесь.")
return
self.user_ids_to_process.clear()
resolved_ids = []
failed_links = []
self.status_label.setText("Статус: Определяю ID...")
QApplication.processEvents()
for link in links_list:
try:
path = urlparse(link).path
screen_name = path.split('/')[-1] if path else ''
if not screen_name and len(path.split('/')) > 1:
screen_name = path.split('/')[-2]
if not screen_name:
failed_links.append(link)
continue
resolved_object = self.vk.utils.resolveScreenName(screen_name=screen_name)
if resolved_object and resolved_object.get('type') == 'user':
resolved_ids.append(resolved_object['object_id'])
else:
failed_links.append(link)
except Exception:
failed_links.append(link)
self.user_ids_to_process = resolved_ids
status_message = f"Статус: Готово к работе с {len(resolved_ids)} пользовател(ем/ями)."
if len(links_list) > 1:
self.vk_url_input.setText(f"Загружено {len(resolved_ids)}/{len(links_list)} из списка")
if failed_links:
QMessageBox.warning(self, "Ошибка получения ID",
f"Не удалось получить ID для следующих ссылок:\n" + "\n".join(failed_links))
self.status_label.setText(status_message)
self.set_ui_state(self.token is not None)
def create_chat_tab(self):
# This implementation correctly creates a scrollable area for chat lists.
tab_content_widget = QWidget()
tab_layout = QVBoxLayout(tab_content_widget)
tab_layout.setContentsMargins(0, 0, 0, 0)
tab_layout.setSpacing(0)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
tab_layout.addWidget(scroll_area)
checkbox_container_widget = QWidget()
checkbox_layout = QVBoxLayout(checkbox_container_widget)
checkbox_layout.setContentsMargins(5, 5, 5, 5)
checkbox_layout.setSpacing(2)
checkbox_layout.addStretch()
scroll_area.setWidget(checkbox_container_widget)
return tab_content_widget
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)
if self.token_countdown_timer.isActive(): self.token_countdown_timer.stop()
self.set_ui_state(False)
self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.")
self.token = None
self.token_expiration_time = None
self.status_label.setText("Статус: Срок действия токена истек, авторизуйтесь заново.")
self.token, self.token_expiration_time = None, 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}")
self.token_timer_label.setText(f"Срок: {hours:02d}ч {minutes:02d}м {seconds:02d}с")
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)
self.auth_btn.setEnabled(not authorized)
for btn in [self.select_all_btn, self.deselect_all_btn, self.refresh_chats_btn,
self.vk_url_input, self.multi_link_btn,
self.visible_messages_checkbox]:
btn.setEnabled(authorized)
has_ids = authorized and bool(self.user_ids_to_process)
self.remove_user_btn.setEnabled(has_ids)
self.add_user_btn.setEnabled(has_ids)
self.chat_tabs.setVisible(authorized)
if authorized:
self.chat_scroll_area.show()
# Когда авторизованы, задаем минимальную высоту, достаточную для ~10-12 чатов
self.chat_tabs.setMinimumHeight(300)
else:
self.chat_scroll_area.hide()
# Когда не авторизованы, сбрасываем минимальную высоту
self.chat_tabs.setMinimumHeight(0)
if not authorized:
self.user_ids_to_process.clear()
self.vk_url_input.clear()
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()
self.handle_auth_token_on_load(loaded_token, expiration_time)
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 set_all_checkboxes_on_current_tab(self, checked):
current_index = self.chat_tabs.currentIndex()
checkbox_lists = [self.office_chat_checkboxes, self.retail_chat_checkboxes, self.other_chat_checkboxes]
if 0 <= current_index < len(checkbox_lists):
for checkbox in checkbox_lists[current_index]:
checkbox.setChecked(checked)
def start_auth(self):
"""
Запускает процесс OAuth авторизации VK в новом окне.
"""
self.status_label.setText("Статус: ожидание авторизации в новом окне...")
self.status_label.setText("Статус: ожидание авторизации...")
auth_window = AuthBrowserWindow(self)
auth_window.token_extracted_signal.connect(self.handle_auth_token)
auth_window.token_extracted_signal.connect(self.handle_new_auth_token)
auth_window.start_auth_flow()
auth_window.exec() # Изменено с exec_() на exec()
auth_window.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, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.")
def handle_new_auth_token(self, token, expires_in):
if not token:
self.status_label.setText("Статус: Авторизация не удалась")
self.set_ui_state(False)
self.update_token_timer_display()
return
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.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api()
self.set_ui_state(True)
self.load_chats()
def handle_auth_token_on_load(self, token, expiration_time):
self.token = token
self.token_expiration_time = expiration_time
self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован (токен загружен)")
self.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api()
self.set_ui_state(True)
self.load_chats()
def _clear_chat_tabs(self):
self.chats.clear()
for chk_list in [self.office_chat_checkboxes, self.retail_chat_checkboxes, self.other_chat_checkboxes]:
for checkbox in chk_list:
checkbox.setParent(None)
checkbox.deleteLater()
chk_list.clear()
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()
self._clear_chat_tabs()
# Get the checkbox layouts from each tab
layouts = [
self.office_tab.findChild(QWidget).findChild(QVBoxLayout),
self.retail_tab.findChild(QWidget).findChild(QVBoxLayout),
self.other_tab.findChild(QWidget).findChild(QVBoxLayout)
]
try:
conversations = self.vk.messages.getConversations(count=200)['items']
conversations = self.vk.messages.getConversations(count=200, filter="all")['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)
self.chats.append({'id': chat_id, 'title': title})
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)
# Insert checkbox at the top of the layout (before the stretch)
if "AG офис" in title:
layouts[0].insertWidget(layouts[0].count() - 1, checkbox)
self.office_chat_checkboxes.append(checkbox)
elif "AG розница" in title:
layouts[1].insertWidget(layouts[1].count() - 1, checkbox)
self.retail_chat_checkboxes.append(checkbox)
else:
layouts[2].insertWidget(layouts[2].count() - 1, checkbox)
self.other_chat_checkboxes.append(checkbox)
self.chat_tabs.setTabText(0, f"AG Офис ({len(self.office_chat_checkboxes)})")
self.chat_tabs.setTabText(1, f"AG Розница ({len(self.retail_chat_checkboxes)})")
self.chat_tabs.setTabText(2, f"Прочие ({len(self.other_chat_checkboxes)})")
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)
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить чаты: {e}")
self.set_ui_state(False)
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 "Неизвестный пользователь"
user = self.vk.users.get(user_ids=user_id)[0]
return f"{user.get('first_name', '')} {user.get('last_name', '')}"
except Exception:
return f"Пользователь {user_id}"
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)
def _get_selected_chats(self):
selected = []
for chk in self.office_chat_checkboxes + self.retail_chat_checkboxes + self.other_chat_checkboxes:
if chk.isChecked():
chat_id = chk.property("chat_id")
title = next((c['title'] for c in self.chats if c['id'] == chat_id), "")
selected.append({'id': chat_id, 'title': title})
return selected
def _execute_user_action(self, action_type):
if not self.user_ids_to_process:
QMessageBox.warning(self, "Ошибка", f"Нет ID пользователей для операции.")
return
selected_chats = self._get_selected_chats()
if not selected_chats:
QMessageBox.warning(self, "Ошибка", "Выберите хотя бы один чат.")
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)
user_infos = {uid: self.get_user_info_by_id(uid) for uid in self.user_ids_to_process}
action_verb = "исключить" if action_type == "remove" else "пригласить"
preposition = "из" if action_type == "remove" else "в"
user_names_list = list(user_infos.values())
user_names_str = "\n".join([f"{name}" for name in user_names_list])
chat_count = len(selected_chats)
chat_str = ""
# Финальная логика склонения с учетом падежа (для "из" и "в")
if chat_count % 10 == 1 and chat_count % 100 != 11:
if action_type == 'remove':
# из 1 выбранного чата (Родительный падеж, ед.ч.)
chat_str = f"{chat_count} выбранного чата"
else:
# в 1 выбранный чат (Винительный падеж, ед.ч.)
chat_str = f"{chat_count} выбранный чат"
elif 2 <= chat_count % 10 <= 4 and (chat_count % 100 < 10 or chat_count % 100 >= 20):
if action_type == 'remove':
# из 3 выбранных чатов (Родительный падеж, мн.ч.)
chat_str = f"{chat_count} выбранных чатов"
else:
# в 3 выбранных чата (Родительный падеж, ед.ч.)
chat_str = f"{chat_count} выбранных чата"
else:
# из 5 выбранных чатов / в 5 выбранных чатов (Родительный падеж, мн.ч.)
chat_str = f"{chat_count} выбранных чатов"
msg = (
f"Вы уверены, что хотите {action_verb} следующих пользователей:\n\n"
f"{user_names_str}\n\n"
f"{preposition} {chat_str}?"
)
confirm_dialog = QMessageBox(self)
confirm_dialog.setWindowTitle("Подтверждение действия")
confirm_dialog.setText(msg)
confirm_dialog.setIcon(QMessageBox.Question)
yes_button = confirm_dialog.addButton("Да", QMessageBox.YesRole)
no_button = confirm_dialog.addButton("Нет", QMessageBox.NoRole)
confirm_dialog.setDefaultButton(no_button) # "Нет" - кнопка по умолчанию
confirm_dialog.exec()
if confirm_dialog.clickedButton() != yes_button:
return
try:
parsed_url = urlparse(vk_url)
path_parts = parsed_url.path.split('/')
results = []
for chat in selected_chats:
for user_id, user_info in user_infos.items():
try:
if action_type == "remove":
self.vk.messages.removeChatUser(chat_id=chat['id'], member_id=user_id)
results.append(f"'{user_info}' исключен из '{chat['title']}'.")
else:
params = {'chat_id': chat['id'], 'user_id': user_id}
if self.visible_messages_checkbox.isChecked():
params['visible_messages_count'] = 250
self.vk.messages.addChatUser(**params)
results.append(f"'{user_info}' приглашен в '{chat['title']}'.")
except VkApiError as e:
results.append(f"✗ Ошибка для '{user_info}' в '{chat['title']}': {e}")
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
QMessageBox.information(self, "Результаты", "\n".join(results))
self.vk_url_input.clear()
self.user_ids_to_process.clear()
self.set_ui_state(self.token is not None)
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, "Отмена", "Операция исключения отменена.")
self._execute_user_action("remove")
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, "Отмена", "Операция приглашения отменена.")
self._execute_user_action("add")
if __name__ == "__main__":