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

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

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

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

786
main.py
View File

@@ -5,10 +5,11 @@ import time
import os import os
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QWidget, QMessageBox, QPushButton, QVBoxLayout, QWidget, QMessageBox,
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.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
from PySide6.QtWebEngineWidgets import QWebEngineView 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 urllib.parse import urlparse, parse_qs, unquote
from vk_api.exceptions import VkApiError from vk_api.exceptions import VkApiError
from PySide6.QtCore import QStandardPaths from PySide6.QtCore import QStandardPaths
@@ -16,7 +17,6 @@ from PySide6.QtCore import QStandardPaths
# --- Управление токенами и настройками --- # --- Управление токенами и настройками ---
APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation), "AnabasisVKChatManager") APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation), "AnabasisVKChatManager")
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json") TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
# Директория для сохранения данных веб-движка (куки, кэш и т.д.)
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache") WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache")
@@ -205,12 +205,28 @@ class AuthBrowserWindow(QDialog):
super().closeEvent(event) super().closeEvent(event)
class VkChatManager(QMainWindow): class MultiLinkDialog(QDialog):
""" def __init__(self, parent=None):
Главное окно приложения VK Chat Manager. super().__init__(parent)
Позволяет авторизоваться через VK и удалять/добавлять пользователей в чаты. 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): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Anabasis Chat Manager") self.setWindowTitle("Anabasis Chat Manager")
@@ -219,35 +235,39 @@ class VkChatManager(QMainWindow):
self.token = None self.token = None
self.token_expiration_time = None self.token_expiration_time = None
self.chats = [] self.chats = []
self.chat_checkboxes = [] self.office_chat_checkboxes = []
self.retail_chat_checkboxes = []
self.other_chat_checkboxes = []
self.vk_session = None self.vk_session = None
self.vk = 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.init_ui()
self.load_saved_token_on_startup() self.load_saved_token_on_startup()
self.setup_token_timer() self.setup_token_timer()
def init_ui(self): def init_ui(self):
"""Инициализирует пользовательский интерфейс приложения."""
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
layout = QVBoxLayout() layout = QVBoxLayout(central_widget)
layout.setContentsMargins(10, 10, 10, 10) layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5) layout.setSpacing(5)
self.instructions = QTextBrowser() self.instructions = QTextBrowser()
self.instructions.setPlainText( self.instructions.setPlainText(
"Инструкция:\n" "Инструкция:\n"
"1. Нажмите 'Авторизоваться через VK'\n" "1. Авторизуйтесь через VK.\n"
"2. В открывшемся окне войдите в свой аккаунт VK\n" "2. Выберите чаты.\n"
"3. Разрешите доступ приложению\n" "3. Вставьте ссылку на пользователя в поле ниже. ID определится автоматически.\n"
"4. Токен автоматически сохранится на 1 час\n" "4. Для массовых операций, нажмите кнопку 'Список' и вставьте ссылки в окне.\n"
"5. Выберите один или несколько чатов, установив галочки\n" "5. Нажмите 'ИСКЛЮЧИТЬ' или 'ПРИГЛАСИТЬ'."
"6. Введите или вставьте ссылку на страницу пользователя VK (например, vk.com/id123 или vk.com/durov). ID будет получен автоматически.\n"
"7. Нажмите соответствующую кнопку 'ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ' или 'ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ'."
) )
self.instructions.setFixedHeight(180) self.instructions.setFixedHeight(120)
layout.addWidget(self.instructions) layout.addWidget(self.instructions)
layout.addWidget(QLabel("Access Token VK:")) layout.addWidget(QLabel("Access Token VK:"))
@@ -258,547 +278,397 @@ class VkChatManager(QMainWindow):
self.token_timer_label = QLabel("Срок действия токена: Н") self.token_timer_label = QLabel("Срок действия токена: Н")
self.token_timer_label.setAlignment(Qt.AlignRight) self.token_timer_label.setAlignment(Qt.AlignRight)
self.token_timer_label.setStyleSheet("font-weight: bold; color: #555;")
layout.addWidget(self.token_timer_label) layout.addWidget(self.token_timer_label)
self.auth_btn = QPushButton("Авторизоваться через VK") self.auth_btn = QPushButton("Авторизоваться через VK")
self.auth_btn.clicked.connect(self.start_auth) self.auth_btn.clicked.connect(self.start_auth)
layout.addWidget(self.auth_btn) layout.addWidget(self.auth_btn)
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("Выберите чаты:")) layout.addWidget(QLabel("Выберите чаты:"))
select_buttons_layout = QHBoxLayout()
self.chat_checkbox_layout = QVBoxLayout() self.select_all_btn = QPushButton("Выбрать все на вкладке")
self.chat_checkbox_layout.setSpacing(2) self.select_all_btn.clicked.connect(lambda: self.set_all_checkboxes_on_current_tab(True))
self.chat_checkbox_widget = QWidget() self.deselect_all_btn = QPushButton("Снять выбор на вкладке")
self.chat_checkbox_widget.setLayout(self.chat_checkbox_layout) self.deselect_all_btn.clicked.connect(lambda: self.set_all_checkboxes_on_current_tab(False))
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 = QPushButton("Обновить чаты")
self.refresh_chats_btn.clicked.connect(self.load_chats) 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.select_all_btn)
select_buttons_layout.addWidget(self.deselect_all_btn) select_buttons_layout.addWidget(self.deselect_all_btn)
select_buttons_layout.addWidget(self.refresh_chats_btn) select_buttons_layout.addWidget(self.refresh_chats_btn)
layout.addLayout(select_buttons_layout) 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 = QLineEdit()
self.vk_url_input.setPlaceholderText("Например: vk.com/id123 или vk.com/durov") self.vk_url_input.setPlaceholderText("https://vk.com/id1")
self.vk_url_input.textChanged.connect(self.on_vk_url_input_changed) # Подключаем сигнал self.vk_url_input.textChanged.connect(self.on_vk_url_input_changed)
layout.addWidget(self.vk_url_input) link_input_layout.addWidget(self.vk_url_input)
# Кнопки для исключения/приглашения пользователя self.multi_link_btn = QPushButton("Список")
self.remove_user_btn = QPushButton("ИСКЛЮЧИТЬ ПОЛЬЗОВАТЕЛЯ") self.multi_link_btn.setToolTip("Ввести несколько ссылок списком")
self.remove_user_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.multi_link_btn.clicked.connect(self.open_multi_link_dialog)
self.remove_user_btn.setMinimumHeight(60) 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.clicked.connect(self.remove_user)
self.remove_user_btn.setEnabled(False) # Изначально отключена
layout.addWidget(self.remove_user_btn) layout.addWidget(self.remove_user_btn)
self.visible_messages_checkbox = QCheckBox("Показать 250 последних сообщений при добавлении") self.visible_messages_checkbox = QCheckBox("Показать 250 последних сообщений при добавлении")
self.visible_messages_checkbox.setChecked(False)
layout.addWidget(self.visible_messages_checkbox) layout.addWidget(self.visible_messages_checkbox)
self.add_user_btn = QPushButton("ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЕЙ")
self.add_user_btn = QPushButton("ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЯ") self.add_user_btn.setMinimumHeight(50)
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.clicked.connect(self.add_user_to_chat)
self.add_user_btn.setEnabled(False) # Изначально отключена
layout.addWidget(self.add_user_btn) layout.addWidget(self.add_user_btn)
self.status_label = QLabel("Статус: не авторизован") self.status_label = QLabel("Статус: не авторизован")
self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
layout.addStretch(1) layout.addStretch(1)
central_widget.setLayout(layout) 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): def setup_token_timer(self):
"""Настраивает QTimer для обновления отображения обратного отсчета токена."""
self.token_countdown_timer = QTimer(self) self.token_countdown_timer = QTimer(self)
self.token_countdown_timer.timeout.connect(self.update_token_timer_display) self.token_countdown_timer.timeout.connect(self.update_token_timer_display)
self.token_countdown_timer.start(1000) self.token_countdown_timer.start(1000)
def update_token_timer_display(self): def update_token_timer_display(self):
"""
Обновляет QLabel с оставшимся временем до истечения срока действия токена.
"""
if self.token_expiration_time is None: if self.token_expiration_time is None:
self.token_timer_label.setText("Срок действия токена: Н") self.token_timer_label.setText("Срок действия токена: Н")
return return
remaining_seconds = int(self.token_expiration_time - time.time()) remaining_seconds = int(self.token_expiration_time - time.time())
if remaining_seconds <= 0: if remaining_seconds <= 0:
self.token_timer_label.setText("Срок действия токена истек!") self.token_timer_label.setText("Срок действия токена истек!")
if self.token_countdown_timer and self.token_countdown_timer.isActive(): if self.token_countdown_timer.isActive(): self.token_countdown_timer.stop()
self.token_countdown_timer.stop()
self.auth_btn.setEnabled(True)
self.set_ui_state(False) self.set_ui_state(False)
self.status_label.setText("Статус: Срок действия токена истек, пожалуйста, авторизуйтесь заново.") self.status_label.setText("Статус: Срок действия токена истек, авторизуйтесь заново.")
self.token = None self.token, self.token_expiration_time = None, None
self.token_expiration_time = None
self.token_input.clear() self.token_input.clear()
return return
minutes, seconds = divmod(remaining_seconds, 60) minutes, seconds = divmod(remaining_seconds, 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
self.token_timer_label.setText(f"Срок: {hours:02d}ч {minutes:02d}м {seconds:02d}с")
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): def set_ui_state(self, authorized):
"""Устанавливает состояние элементов пользовательского интерфейса в зависимости от статуса авторизации.""" self.auth_btn.setEnabled(not authorized)
self.select_all_btn.setEnabled(authorized) for btn in [self.select_all_btn, self.deselect_all_btn, self.refresh_chats_btn,
self.deselect_all_btn.setEnabled(authorized) self.vk_url_input, self.multi_link_btn,
self.refresh_chats_btn.setEnabled(authorized) self.visible_messages_checkbox]:
self.vk_url_input.setEnabled(authorized) btn.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)
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: if authorized:
self.chat_scroll_area.show() # Когда авторизованы, задаем минимальную высоту, достаточную для ~10-12 чатов
self.chat_tabs.setMinimumHeight(300)
else: 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): def load_saved_token_on_startup(self):
"""Пытается загрузить сохраненный токен при запуске приложения."""
loaded_token, expiration_time = load_token() loaded_token, expiration_time = load_token()
if loaded_token: if loaded_token:
self.token = loaded_token self.handle_auth_token_on_load(loaded_token, expiration_time)
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: else:
self.status_label.setText("Статус: не авторизован (токен не найден или просрочен)")
self.set_ui_state(False) self.set_ui_state(False)
self.update_token_timer_display()
def set_all_checkboxes(self, 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 self.chat_checkboxes: for checkbox in checkbox_lists[current_index]:
checkbox.setChecked(checked) checkbox.setChecked(checked)
def start_auth(self): def start_auth(self):
""" self.status_label.setText("Статус: ожидание авторизации...")
Запускает процесс OAuth авторизации VK в новом окне.
"""
self.status_label.setText("Статус: ожидание авторизации в новом окне...")
auth_window = AuthBrowserWindow(self) 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.start_auth_flow()
auth_window.exec() # Изменено с exec_() на exec() auth_window.exec()
def handle_new_auth_token(self, token, expires_in):
if not token:
self.status_label.setText("Статус: Авторизация не удалась")
self.set_ui_state(False)
return
def handle_auth_token(self, token, expires_in):
"""
Обрабатывает полученный токен авторизации после закрытия окна браузера.
"""
if token:
self.token = token self.token = token
self.token_expiration_time = time.time() + expires_in self.token_expiration_time = time.time() + expires_in
save_token(self.token, expires_in) save_token(self.token, expires_in)
self.token_input.setText(self.token[:50] + "...") self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован") self.status_label.setText("Статус: авторизован")
self.auth_btn.setEnabled(False)
self.set_ui_state(True)
self.update_token_timer_display()
self.vk_session = vk_api.VkApi(token=self.token) self.vk_session = vk_api.VkApi(token=self.token)
self.vk = self.vk_session.get_api() self.vk = self.vk_session.get_api()
self.set_ui_state(True)
self.load_chats() self.load_chats()
else:
QMessageBox.warning(self, "Ошибка", "Не удалось получить токен. Попробуйте еще раз.") def handle_auth_token_on_load(self, token, expiration_time):
self.status_label.setText("Статус: Авторизация не удалась") self.token = token
self.set_ui_state(False) self.token_expiration_time = expiration_time
self.update_token_timer_display()
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): def load_chats(self):
""" self._clear_chat_tabs()
Загружает список чатов пользователя из VK API и заполняет их чекбоксами.
""" # Get the checkbox layouts from each tab
for i in reversed(range(self.chat_checkbox_layout.count())): layouts = [
widget_to_remove = self.chat_checkbox_layout.itemAt(i).widget() self.office_tab.findChild(QWidget).findChild(QVBoxLayout),
if widget_to_remove: self.retail_tab.findChild(QWidget).findChild(QVBoxLayout),
widget_to_remove.setParent(None) self.other_tab.findChild(QWidget).findChild(QVBoxLayout)
widget_to_remove.deleteLater() ]
self.chat_checkboxes.clear()
self.chats.clear()
try: try:
conversations = self.vk.messages.getConversations(count=200)['items'] conversations = self.vk.messages.getConversations(count=200, filter="all")['items']
for conv in conversations: for conv in conversations:
if conv['conversation']['peer']['type'] == 'chat': if conv['conversation']['peer']['type'] == 'chat':
chat_id = conv['conversation']['peer']['local_id'] chat_id = conv['conversation']['peer']['local_id']
title = conv['conversation']['chat_settings']['title'] title = conv['conversation']['chat_settings']['title']
chat_data = {'id': chat_id, 'title': title} self.chats.append({'id': chat_id, 'title': title})
self.chats.append(chat_data)
checkbox = QCheckBox(f"{title} (id: {chat_id})") checkbox = QCheckBox(f"{title} (id: {chat_id})")
checkbox.setChecked(False)
checkbox.setProperty("chat_id", chat_id) checkbox.setProperty("chat_id", chat_id)
self.chat_checkbox_layout.addWidget(checkbox)
self.chat_checkboxes.append(checkbox)
if not self.chats: # Insert checkbox at the top of the layout (before the stretch)
QMessageBox.information(self, "Информация", "У вас нет доступных чатов.") if "AG офис" in title:
self.chat_scroll_area.hide() layouts[0].insertWidget(layouts[0].count() - 1, checkbox)
self.select_all_btn.setEnabled(False) self.office_chat_checkboxes.append(checkbox)
self.deselect_all_btn.setEnabled(False) elif "AG розница" in title:
self.refresh_chats_btn.setEnabled(False) layouts[1].insertWidget(layouts[1].count() - 1, checkbox)
self.retail_chat_checkboxes.append(checkbox)
else: else:
self.chat_scroll_area.show() layouts[2].insertWidget(layouts[2].count() - 1, checkbox)
self.select_all_btn.setEnabled(True) self.other_chat_checkboxes.append(checkbox)
self.deselect_all_btn.setEnabled(True)
self.refresh_chats_btn.setEnabled(True)
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: except VkApiError as e:
error_message = str(e) QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить чаты: {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.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): def get_user_info_by_id(self, user_id):
"""
Получает имя и фамилию пользователя по его ID.
Возвращает строку "Имя Фамилия" или "Неизвестный пользователь".
"""
if not self.vk:
return "Неизвестный пользователь"
try: try:
users = self.vk.users.get(user_ids=user_id) user = self.vk.users.get(user_ids=user_id)[0]
if users and len(users) > 0:
user = users[0]
return f"{user.get('first_name', '')} {user.get('last_name', '')}" return f"{user.get('first_name', '')} {user.get('last_name', '')}"
except Exception:
return f"Пользователь {user_id}"
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
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: else:
return "Неизвестный пользователь" # в 1 выбранный чат (Винительный падеж, ед.ч.)
except VkApiError as e: chat_str = f"{chat_count} выбранный чат"
print(f"Ошибка получения информации о пользователе {user_id}: {e}") elif 2 <= chat_count % 10 <= 4 and (chat_count % 100 < 10 or chat_count % 100 >= 20):
return "Неизвестный пользователь" if action_type == 'remove':
except Exception as e: # из 3 выбранных чатов (Родительный падеж, мн.ч.)
print(f"Неизвестная ошибка при получении информации о пользователе {user_id}: {e}") chat_str = f"{chat_count} выбранных чатов"
return "Неизвестный пользователь" else:
# в 3 выбранных чата (Родительный падеж, ед.ч.)
chat_str = f"{chat_count} выбранных чата"
else:
# из 5 выбранных чатов / в 5 выбранных чатов (Родительный падеж, мн.ч.)
chat_str = f"{chat_count} выбранных чатов"
def resolve_user_id_from_url(self, vk_url_to_resolve): msg = (
""" f"Вы уверены, что хотите {action_verb} следующих пользователей:\n\n"
Разрешает ID пользователя VK из введенной ссылки и сохраняет его в self.user_id_to_process. f"{user_names_str}\n\n"
""" f"{preposition} {chat_str}?"
if not self.vk: )
self.user_id_to_process = None
self.set_ui_state(self.token is not None) confirm_dialog = QMessageBox(self)
return confirm_dialog.setWindowTitle("Подтверждение действия")
confirm_dialog.setText(msg)
vk_url = vk_url_to_resolve.strip() confirm_dialog.setIcon(QMessageBox.Question)
if not vk_url: yes_button = confirm_dialog.addButton("Да", QMessageBox.YesRole)
self.user_id_to_process = None no_button = confirm_dialog.addButton("Нет", QMessageBox.NoRole)
self.set_ui_state(self.token is not None) confirm_dialog.setDefaultButton(no_button) # "Нет" - кнопка по умолчанию
confirm_dialog.exec()
if confirm_dialog.clickedButton() != yes_button:
return return
results = []
for chat in selected_chats:
for user_id, user_info in user_infos.items():
try: try:
parsed_url = urlparse(vk_url) if action_type == "remove":
path_parts = parsed_url.path.split('/') self.vk.messages.removeChatUser(chat_id=chat['id'], member_id=user_id)
results.append(f"'{user_info}' исключен из '{chat['title']}'.")
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: else:
self.user_id_to_process = None params = {'chat_id': chat['id'], 'user_id': user_id}
self.status_label.setText("Статус: Не удалось найти пользователя по этой ссылке.") 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: except VkApiError as e:
self.user_id_to_process = None results.append(f"✗ Ошибка для '{user_info}' в '{chat['title']}': {e}")
error_message = str(e)
if "[5] User authorization failed: access_token was given to another ip address" in error_message: QMessageBox.information(self, "Результаты", "\n".join(results))
self.status_label.setText("Статус: Требуется повторная авторизация (IP изменен).") self.vk_url_input.clear()
QMessageBox.critical(self, "Ошибка авторизации VK", self.user_ids_to_process.clear()
"Ваш IP-адрес изменился, и токен стал недействительным. " self.set_ui_state(self.token is not None)
"Пожалуйста, авторизуйтесь заново.")
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): def remove_user(self):
""" self._execute_user_action("remove")
Исключает пользователя из всех выбранных чатов.
Перед исключением выводит окно подтверждения.
"""
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): def add_user_to_chat(self):
""" self._execute_user_action("add")
Приглашает пользователя во все выбранные чаты.
Перед приглашением выводит окно подтверждения.
"""
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__": if __name__ == "__main__":