Files
AnabasisChatRemove/main.py
benya 3ed5bba9af feat: автоматизация сборки и поддержка бессрочных токенов
- Исправлена ошибка ImportError: QtWebEngineCore путем перехода на PyInstaller.
- Добавлен скрипт build.py для автоматической сборки, очистки DLL и создания ZIP-архива.
- Реализована поддержка бессрочного доступа (offline_access) для VK Access Token.
- Обновлен README.md: добавлены разделы для разработчиков и описание структуры данных.
- Оптимизирован размер билда за счет удаления неиспользуемых библиотек Qt и папок локализации.
2026-01-13 02:38:26 +03:00

721 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
from vk_api import VkApi
import json
import time
import os
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QWidget, QMessageBox,
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout,
QSizePolicy, QDialog, QTextEdit, QTabWidget, QDialogButtonBox)
from PySide6.QtCore import Qt, QUrl, QDateTime, Signal, QTimer
from PySide6.QtGui import QIcon
from PySide6.QtWebEngineWidgets import QWebEngineView
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
# --- Управление токенами и настройками ---
APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation), "AnabasisVKChatManager")
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache")
def get_resource_path(relative_path):
""" Получает абсолютный путь к ресурсу, работает для скрипта и для .exe """
if hasattr(sys, '_MEIPASS'): # Для PyInstaller (на всякий случай)
return os.path.join(sys._MEIPASS, relative_path)
# Для cx_Freeze и обычного запуска
return os.path.join(os.path.abspath("."), relative_path)
def save_token(token, expires_in=0):
"""Сохраняет токен. Если expires_in=0, токен считается бессрочным."""
try:
expires_in = int(expires_in)
except (ValueError, TypeError):
expires_in = 0
os.makedirs(APP_DATA_DIR, exist_ok=True)
# ИСПРАВЛЕНИЕ: если expires_in равно 0, то и время истечения ставим 0
expiration_time = (time.time() + expires_in) if expires_in > 0 else 0
data = {
"token": token,
"expiration_time": expiration_time
}
try:
with open(TOKEN_FILE, "w") as f:
json.dump(data, f)
status = "Бессрочно" if expiration_time == 0 else QDateTime.fromSecsSinceEpoch(int(expiration_time)).toString()
print(f"Токен сохранен. Срок действия: {status}")
return expiration_time
except IOError as e:
print(f"Ошибка сохранения токена: {e}")
return None
def load_token():
"""Загружает токен и проверяет его валидность."""
try:
if not os.path.exists(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")
# ИСПРАВЛЕНИЕ: токен валиден, если время истечения 0 ИЛИ оно больше текущего
if token and (expiration_time == 0 or expiration_time > time.time()):
return token, expiration_time
else:
if os.path.exists(TOKEN_FILE):
os.remove(TOKEN_FILE)
return None, None
except Exception as e:
print(f"Ошибка загрузки: {e}")
return None, None
class WebEnginePage(QWebEnginePage):
"""
Класс для обработки навигационных запросов в QWebEngineView,
специально для извлечения токена авторизации VK.
"""
# Добавлен параметр profile в конструктор, чтобы использовать пользовательский профиль
def __init__(self, profile=None, parent=None, browser_window_instance=None):
super().__init__(profile, parent) # Передаем profile в базовый класс
self.parent_browser_window = browser_window_instance
self.token_extracted = False
def acceptNavigationRequest(self, url, _type, isMainFrame):
"""
Переопределенный метод для перехвата URL-адреса, содержащего токен доступа.
"""
url_string = url.toString()
if "access_token" in url_string and not self.token_extracted:
self.token_extracted = True
if self.parent_browser_window:
self.parent_browser_window.process_auth_url(url_string)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
class AuthBrowserWindow(QDialog):
"""
Отдельное окно-диалог для проведения OAuth авторизации VK.
"""
token_extracted_signal = Signal(str, int)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Авторизация VK")
self.setGeometry(350, 350, 800, 600)
layout = QVBoxLayout(self)
self.browser = QWebEngineView()
# Настройка QWebEngineProfile для сохранения куки и других данных
os.makedirs(WEB_ENGINE_CACHE_DIR, exist_ok=True)
# ИСПРАВЛЕНО: Сделать profile атрибутом экземпляра и дать ему self как родителя
self.profile = QWebEngineProfile("AnabasisVKWebProfile", self)
self.profile.setPersistentCookiesPolicy(QWebEngineProfile.AllowPersistentCookies)
self.profile.setPersistentStoragePath(WEB_ENGINE_CACHE_DIR)
# Создаем страницу с настроенным профилем
self.browser.page = WebEnginePage(profile=self.profile, parent=self.browser, browser_window_instance=self)
self.browser.setPage(self.browser.page)
layout.addWidget(self.browser)
self.status_label = QLabel("Ожидание авторизации...")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
def start_auth_flow(self):
"""Запускает OAuth авторизацию VK в этом окне."""
auth_url = (
"https://oauth.vk.com/authorize?"
"client_id=2685278&"
"display=page&"
"redirect_uri=https://oauth.vk.com/blank.html&"
"scope=1073737727&"
"response_type=token&"
"v=5.131"
)
self.browser.setUrl(QUrl(auth_url))
self.status_label.setText("Пожалуйста, войдите в VK и разрешите доступ...")
def process_auth_url(self, url_string):
"""
Извлекает токен доступа из URL перенаправления и испускает сигнал.
"""
token = None
expires_in = 3600
parsed = urlparse(url_string)
if parsed.fragment:
params = parse_qs(parsed.fragment)
else:
params = parse_qs(parsed.query)
if 'access_token' in params:
token = params['access_token'][0]
if 'expires_in' in params:
try:
expires_in = int(params['expires_in'][0])
except ValueError:
pass
if not token:
start_marker = "access_token%253D"
end_marker = "%25"
start_index = url_string.find(start_marker)
if start_index != -1:
token_start_index = start_index + len(start_marker)
remaining_url = url_string[token_start_index:]
end_index = remaining_url.find(end_marker)
if end_index != -1:
raw_token = remaining_url[:end_index]
else:
amp_index = remaining_url.find('&')
if amp_index != -1:
raw_token = remaining_url[:amp_index]
else:
raw_token = remaining_url
token = unquote(raw_token)
if token:
self.token_extracted_signal.emit(token, expires_in)
self.accept()
else:
QMessageBox.warning(self, "Ошибка Авторизации",
"Не удалось получить токен. Проверьте URL или попробуйте еще раз.")
self.reject()
def closeEvent(self, event):
"""
Переопределенный метод для обработки закрытия окна.
Выполняет очистку ресурсов QWebEngine, чтобы избежать предупреждения
"Release of profile requested but WebEnginePage still not deleted. Expect troubles!".
"""
current_page = self.browser.page()
if current_page:
# Снимаем страницу с QWebEngineView, чтобы View перестала ей владеть
self.browser.setPage(None)
# Планируем удаление объекта страницы
current_page.deleteLater()
# Вызываем метод базового класса для корректного закрытия диалога
super().closeEvent(event)
class 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")
self.setGeometry(300, 300, 600, 800)
self.token = None
self.token_expiration_time = None
self.chats = []
self.office_chat_checkboxes = []
self.retail_chat_checkboxes = []
self.warehouse_chat_checkboxes = []
self.coffee_chat_checkboxes = []
self.other_chat_checkboxes = []
self.vk_session = None
self.vk = None
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(central_widget)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
self.instructions = QTextBrowser()
self.instructions.setPlainText(
"Инструкция:\n"
"1. Авторизуйтесь через VK.\n"
"2. Выберите чаты.\n"
"3. Вставьте ссылку на пользователя в поле ниже. ID определится автоматически.\n"
"4. Для массовых операций, нажмите кнопку 'Список' и вставьте ссылки в окне.\n"
"5. Нажмите 'ИСКЛЮЧИТЬ' или 'ПРИГЛАСИТЬ'."
)
self.instructions.setFixedHeight(120)
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)
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.warehouse_tab = self.create_chat_tab()
self.chat_tabs.addTab(self.warehouse_tab, "AG Склад")
self.coffee_tab = self.create_chat_tab()
self.chat_tabs.addTab(self.coffee_tab, "AG Кофейни")
self.other_tab = self.create_chat_tab()
self.chat_tabs.addTab(self.other_tab, "Прочие")
layout.addWidget(QLabel("Выберите чаты:"))
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)
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_tabs)
layout.addWidget(QLabel("Ссылка на страницу VK (ID определится автоматически):"))
link_input_layout = QHBoxLayout()
self.vk_url_input = QLineEdit()
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.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)
layout.addWidget(self.remove_user_btn)
self.visible_messages_checkbox = QCheckBox("Показать 250 последних сообщений при добавлении")
layout.addWidget(self.visible_messages_checkbox)
self.add_user_btn = QPushButton("ПРИГЛАСИТЬ ПОЛЬЗОВАТЕЛЕЙ")
self.add_user_btn.setMinimumHeight(50)
self.add_user_btn.clicked.connect(self.add_user_to_chat)
layout.addWidget(self.add_user_btn)
self.status_label = QLabel("Статус: не авторизован")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
layout.addStretch(1)
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):
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):
if self.token_expiration_time is None:
self.token_timer_label.setText("Срок действия токена: Н")
return
# ИСПРАВЛЕНИЕ: обрабатываем бессрочный токен
if self.token_expiration_time == 0:
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.isActive():
self.token_countdown_timer.stop()
self.set_ui_state(False)
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)
self.token_timer_label.setText(f"Срок: {hours:02d}ч {minutes:02d}м {seconds:02d}с")
def set_ui_state(self, authorized):
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:
# Когда авторизованы, задаем минимальную высоту, достаточную для ~10-12 чатов
self.chat_tabs.setMinimumHeight(300)
else:
# Когда не авторизованы, сбрасываем минимальную высоту
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.handle_auth_token_on_load(loaded_token, expiration_time)
else:
self.set_ui_state(False)
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.retail_warehouse_checkboxes, self.retail_coffee_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):
self.status_label.setText("Статус: ожидание авторизации...")
auth_window = AuthBrowserWindow(self)
auth_window.token_extracted_signal.connect(self.handle_new_auth_token)
auth_window.start_auth_flow()
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
self.token = token
# Сохраняем и получаем корректный expiration_time (0 или будущее время)
self.token_expiration_time = save_token(self.token, expires_in)
self.token_input.setText(self.token[:50] + "...")
self.status_label.setText("Статус: авторизован")
self.vk_session = 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 = 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.warehouse_chat_checkboxes, self.coffee_chat_checkboxes, self.other_chat_checkboxes]:
for checkbox in chk_list:
checkbox.setParent(None)
checkbox.deleteLater()
chk_list.clear()
def load_chats(self):
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.warehouse_tab.findChild(QWidget).findChild(QVBoxLayout),
self.coffee_tab.findChild(QWidget).findChild(QVBoxLayout),
self.other_tab.findChild(QWidget).findChild(QVBoxLayout)
]
try:
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']
self.chats.append({'id': chat_id, 'title': title})
checkbox = QCheckBox(f"{title} (id: {chat_id})")
checkbox.setProperty("chat_id", chat_id)
# 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)
elif "AG склад" in title:
layouts[2].insertWidget(layouts[2].count() - 1, checkbox)
self.warehouse_chat_checkboxes.append(checkbox)
elif "AG кофейни" in title:
layouts[3].insertWidget(layouts[3].count() - 1, checkbox)
self.coffee_chat_checkboxes.append(checkbox)
else:
layouts[4].insertWidget(layouts[4].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"AG Склад ({len(self.warehouse_chat_checkboxes)})")
self.chat_tabs.setTabText(3, f"AG Кофейни ({len(self.coffee_chat_checkboxes)})")
self.chat_tabs.setTabText(4, f"Прочие ({len(self.other_chat_checkboxes)})")
except VkApiError as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить чаты: {e}")
self.set_ui_state(False)
def get_user_info_by_id(self, user_id):
try:
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 _get_selected_chats(self):
selected = []
for chk in self.office_chat_checkboxes + self.retail_chat_checkboxes + self.warehouse_chat_checkboxes + self.coffee_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:
# в 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
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}")
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):
self._execute_user_action("remove")
def add_user_to_chat(self):
self._execute_user_action("add")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
app.setPalette(app.style().standardPalette())
# Установка иконки для ВСЕХ окон приложения
icon_path = get_resource_path("icon.ico")
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
window = VkChatManager()
window.show()
sys.exit(app.exec())