""" Обработчики команд с удобным интерфейсом управления задачами. """ import asyncio import logging import re from datetime import datetime from typing import Optional, Dict from urllib.parse import urlparse from aiogram import Dispatcher, F, Bot from aiogram.filters import Command from aiogram.types import ( Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove ) from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from config.settings import settings from managers.background_tasks import BackgroundTaskManager from managers.proxy_manager import ProxyManager from managers.task_manager import TaskManager, TaskParams from services.browser_service import BrowserService from services.visit_service import VisitScheduler from utils.helpers import parse_range, format_range, extract_domain from utils.telegram import send_message_safe, send_visit_result from managers.storage import TaskStorage, ChatStorage, BalanceStorage from auth.manager import AuthManager from auth.storage import AuthStorage from auth import ADMIN_PASSWORD logger = logging.getLogger(__name__) class BotInterface: """Интерфейс бота с управлением задачами.""" def __init__( self, background_tasks: BackgroundTaskManager, proxy_manager: ProxyManager, browser_service: BrowserService, storage: TaskStorage = None, chat_storage: ChatStorage = None, bot_ref=None, ): self.background_tasks = background_tasks self.proxy_manager = proxy_manager self.browser_service = browser_service self.task_manager = TaskManager() self.storage = storage self.chat_storage = chat_storage self.auth_manager = AuthManager() self.auth_storage = AuthStorage() self.balance_storage = BalanceStorage() self._user_input_state: Dict[int, dict] = {} self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем self._admin_balance_state: Dict[int, int] = {} # admin_id -> target_user_id self._menu_msg: Dict[int, int] = {} # user_id -> inline keyboard message_id self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id self._bot_ref = bot_ref self._waiting_password: Dict[int, str] = {} # user_id -> "register" or "login" async def _update_commands(self): if self._bot_ref: await self._bot_ref._update_bot_commands() def register(self, dp: Dispatcher): """Регистрация всех обработчиков.""" interface = self # --- АВТОРИЗАЦИЯ --- @dp.message(Command("start")) async def cmd_start(message: Message): """Обработка /start.""" user_id = message.from_user.id await interface._safe_delete(message.bot, message.chat.id, message.message_id) if interface.auth_manager.is_authenticated(user_id) and interface.auth_manager.is_admin(user_id): await interface._show_main_menu(message) return if interface.auth_manager.is_authenticated(user_id): await interface._show_user_menu(message) return exists = await interface.auth_storage.user_exists(user_id) if exists: sent = await message.answer("👋 С возвращением!\n\nВведите пароль для входа:") interface._waiting_password[user_id] = "login" else: sent = await message.answer( "👋 Добро пожаловать!\n\n" "Вы здесь впервые. Придумайте пароль для регистрации\n" "_(минимум 4 символа):_" ) interface._waiting_password[user_id] = "register" interface._menu_msg[user_id] = sent.message_id @dp.message(Command("register")) async def cmd_register(message: Message): """Регистрация: /register <пароль>""" await interface._safe_delete(message.bot, message.chat.id, message.message_id) parts = message.text.split(maxsplit=1) if len(parts) < 2 or len(parts[1]) < 4: await interface._send_temp(message, "❌ Использование: /register <пароль от 4 символов>") return password = parts[1] user_id = message.from_user.id exists = await interface.auth_storage.user_exists(user_id) if exists: await interface._send_temp(message, "⚠️ Вы уже зарегистрированы. Используйте /login <пароль>") return password_hash, salt = interface.auth_manager._hash_password(password) await interface.auth_storage.register(user_id, password_hash, salt) is_admin = (password == ADMIN_PASSWORD) interface.auth_manager.login(user_id, is_admin=is_admin) if is_admin: await interface._update_commands() await interface._show_main_menu(message) else: await interface._show_user_menu(message) @dp.message(Command("login")) async def cmd_login(message: Message): """Вход: /login <пароль>""" await interface._safe_delete(message.bot, message.chat.id, message.message_id) parts = message.text.split(maxsplit=1) if len(parts) < 2: await interface._send_temp(message, "❌ Использование: /login <пароль>") return password = parts[1] user_id = message.from_user.id user_data = await interface.auth_storage.get_user(user_id) if not user_data: await interface._send_temp(message, "❌ Вы не зарегистрированы.\nИспользуйте /register <пароль>") return if interface.auth_manager.is_locked_out(user_id): await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.") return if password == ADMIN_PASSWORD: interface.auth_manager.login(user_id, is_admin=True) await interface._update_commands() await interface._show_main_menu(message) elif interface.auth_manager.verify_password( password, user_data["password_hash"], user_data["salt"] ): interface.auth_manager.login(user_id, is_admin=False) await interface._show_user_menu(message) else: interface.auth_manager.record_failed_attempt(user_id) await interface._send_temp(message, "❌ Неверный пароль") @dp.message(Command("logout")) async def cmd_logout(message: Message): """Выйти из аккаунта.""" await interface._safe_delete(message.bot, message.chat.id, message.message_id) interface.auth_manager.logout(message.from_user.id) await interface._update_commands() await interface._send_temp(message, "🔑 Вы вышли из системы.", delay=3) # Декоратор: доступ только для авторизованных async def require_auth(event): user_id = event.from_user.id if not interface.auth_manager.is_authenticated(user_id): await event.answer( "🔒 Требуется авторизация!\n\n" "Напишите администратору для получения доступа." ) return False return True # Декоратор: доступ только для админов async def require_admin(event): user_id = event.from_user.id if not interface.auth_manager.is_authenticated(user_id): await event.answer( "🔒 Требуется авторизация!\n\n" "/register <пароль> — зарегистрироваться\n" "/login <пароль> — войти" ) return False if not interface.auth_manager.is_admin(user_id): await event.answer( "⛔ **Нет доступа!**\n\n" "Эта функция доступна только администраторам.\n" "Войдите как администратор." ) return False return True # === Главное меню === @dp.callback_query(F.data == "menu_main") async def cb_main_menu(callback: CallbackQuery): if not await require_admin(callback): return await interface._show_main_menu(callback.message, user_id=callback.from_user.id) await callback.answer() # === Пользователи (admin reply-button) === @dp.message(F.text == "👥 Пользователи") async def btn_users(message: Message): if not await require_admin(message): return await interface._show_users_list(message) # === Стримеры === @dp.message(F.text == "📺 Стримеры") @dp.message(Command("streamers")) async def btn_streamers(message: Message): if not await require_admin(message): return await interface._show_streamers_list(message) @dp.callback_query(F.data == "menu_streamers") async def cb_streamers(callback: CallbackQuery): if not await require_admin(callback): return await interface._show_streamers_list(callback.message, edit=True) await callback.answer() # === Задачи === @dp.message(F.text == "📊 Задачи") @dp.message(Command("tasks")) async def btn_tasks(message: Message): if not await require_admin(message): return await interface._show_tasks_list(message) @dp.callback_query(F.data == "menu_tasks") async def cb_tasks(callback: CallbackQuery): if not await require_admin(callback): return await interface._show_tasks_list(callback.message, edit=True) await callback.answer() # === Статус === @dp.message(F.text == "📈 Статус") async def btn_status(message: Message): if not await require_admin(message): return await interface._show_status(message) @dp.callback_query(F.data == "menu_status") async def cb_status(callback: CallbackQuery): if not await require_admin(callback): return await interface._show_status(callback.message, edit=True) await callback.answer() # === Стоп всё === @dp.message(F.text == "🛑 Остановить всё") @dp.callback_query(F.data == "stop_all") async def handle_stop_all(event): if not await require_admin(event): return count = await interface.background_tasks.cancel_all() for task_id in list(interface.task_manager._tasks.keys()): await interface.task_manager.remove_task(task_id) text = f"🛑 Остановлено задач: {count}" kb = InlineKeyboardBuilder() kb.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) if isinstance(event, CallbackQuery): await event.message.edit_text(text, reply_markup=kb.as_markup()) await event.answer() else: await event.answer(text, reply_markup=kb.as_markup()) # === Быстрый старт === @dp.message(F.text == "🚀 Быстрый старт") async def btn_quick(message: Message): if not await require_admin(message): return await message.answer( "🚀 **БЫСТРЫЙ СТАРТ**\n\n" "**Twitch мониторинг:**\n" "`<канал> [посещений] [минут] [задержка] [чтение] [домены]`\n\n" "**Пример:** `streamer user123 5 30 10-30 10-30`\n\n" "**Посещение URL:**\n" "` [задержка] [повторов] [чтение]`\n\n" "**Пример:** `https://example.com 10-30 100 15-30`" ) # === Управление задачей === @dp.callback_query(F.data.startswith("tdetail_")) async def cb_task_detail(callback: CallbackQuery): if not await require_admin(callback): return task_id = callback.data.replace("tdetail_", "", 1) await interface._show_task_detail(callback, task_id) await callback.answer() @dp.callback_query(F.data.startswith("tpause_")) async def cb_task_pause(callback: CallbackQuery): if not await require_admin(callback): return task_id = callback.data.replace("tpause_", "", 1) await interface.task_manager.pause_task(task_id) await callback.answer("⏸️ Пауза") await interface._show_task_detail(callback, task_id) @dp.callback_query(F.data.startswith("tresume_")) async def cb_task_resume(callback: CallbackQuery): if not await require_admin(callback): return task_id = callback.data.replace("tresume_", "", 1) await interface.task_manager.resume_task(task_id) await callback.answer("▶️ Возобновлено") await interface._show_task_detail(callback, task_id) @dp.callback_query(F.data.startswith("tstop_")) async def tstop(callback: CallbackQuery): if not await require_admin(callback): return tid = callback.data.replace("tstop_", "", 1) await interface.task_manager.stop_task(tid) await interface.background_tasks.cancel_task(tid) await callback.answer("🛑 Остановлена") await interface._show_task_detail(callback, tid) @dp.callback_query(F.data.startswith("tskip_")) async def cb_task_skip(callback: CallbackQuery): if not await require_admin(callback): return task_id = callback.data.replace("tskip_", "", 1) await interface.task_manager.update_task(task_id, skip_next=True) await callback.answer("⏭️ Пропущено") @dp.callback_query(F.data.startswith("trestart_")) async def cb_task_restart(callback: CallbackQuery): if not await require_admin(callback): return data = callback.data # Проверяем, что это именно перезапуск задачи (trestart_task_) if data.startswith("trestart_task_"): tid = data.replace("trestart_task_", "", 1) logger.info(f"Перезапуск задачи: {tid}") params = await interface.task_manager.get_task(tid) if not params: await callback.answer("❌ Не найдена") return await interface.task_manager.update_task( tid, completed=False, stopped=False, paused=False, current_visit=0, total_visits=0, successful_visits=0, links_found=0, started_at=datetime.now() ) ok = await interface.background_tasks.start_task( task_id=tid, coro=interface._run_twitch(tid, params, callback.message), task_type="twitch_irc", metadata={'type': 'twitch_irc', 'channel': params.channel, 'chat_id': params.chat_id} ) if ok: await callback.answer("🔄 Перезапущено!") else: await callback.answer("❌ Ошибка запуска") await interface._show_task_detail(callback, tid) else: # Обычный сброс статистики (trestart_ без task) task_id = data.replace("trestart_", "", 1) await interface.task_manager.restart_task(task_id) await callback.answer("🔄 Статистика сброшена") await interface._show_task_detail(callback, task_id) @dp.callback_query(F.data.startswith("tset_")) async def cb_task_set(callback: CallbackQuery): if not await require_admin(callback): return data = callback.data logger.info(f"SET callback: {data}") # Формат: tset_TASKID_PARAM # Убираем префикс data = data.replace("tset_", "", 1) # Ищем последнее подчеркивание для разделения task_id и param idx = data.rfind("_") if idx == -1: await callback.answer("❌ Неверный формат") return task_id = data[:idx] param = data[idx + 1:] logger.info(f"Parsed: task_id={task_id[:20]}, param={param}") # Сохраняем состояние ожидания ввода interface._user_input_state[callback.from_user.id] = { "task_id": task_id, "param": param } prompts = { "delay": "⏱️ Задержка (сек): `10-30` или `15`", "reading": "📖 Чтение (сек): `10-30` или `20`", "perlink": "🔗 Посещений на ссылку: `5`", "minutes": "⏰ Длительность (мин): `30`", "domains": "🌐 Домены через запятую: `site1.com, site2.ru`\nИли `all` для всех", } builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"tdetail_{task_id}")) prompt = prompts.get(param, f"Введите значение для {param}:") sent = await callback.message.answer(prompt, reply_markup=builder.as_markup()) interface._user_input_state[callback.from_user.id]["prompt_msg_id"] = sent.message_id await callback.answer(f"Ожидаю ввод: {param}") @dp.callback_query(F.data.startswith("tdelete_")) async def tdelete(callback: CallbackQuery): if not await require_admin(callback): return tid = callback.data.replace("tdelete_", "", 1) await interface.task_manager.remove_task(tid) await callback.answer("🗑️ Удалена") await interface._show_streamers_list(callback.message, edit=True) @dp.callback_query(F.data.startswith("treset_")) async def treset(callback: CallbackQuery): if not await require_admin(callback): return tid = callback.data.replace("treset_", "", 1) await interface.task_manager.update_task( tid, current_visit=0, total_visits=0, successful_visits=0, links_found=0 ) await callback.answer("🔄 Статистика сброшена") await interface._show_task_detail(callback, tid) # === Кабинет пользователя === @dp.callback_query(F.data == "user_menu") async def cb_user_menu(callback: CallbackQuery): await interface._show_user_menu(callback.message, edit=True) await callback.answer() @dp.callback_query(F.data == "my_balance") async def cb_my_balance(callback: CallbackQuery): uid = callback.from_user.id balance = await interface.balance_storage.get_balance(uid) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_menu")) await callback.message.edit_text( f"💰 **Ваш баланс**\n\n**{balance}** переходов", reply_markup=builder.as_markup() ) await callback.answer() @dp.callback_query(F.data == "new_task") async def cb_new_task(callback: CallbackQuery): uid = callback.from_user.id balance = await interface.balance_storage.get_balance(uid) if balance <= 0: await callback.answer("❌ Недостаточно баланса", show_alert=True) return all_tasks = await interface.task_manager.get_all_tasks() active_count = sum( 1 for p in all_tasks.values() if p.user_id == uid and not p.completed and not p.stopped ) if active_count >= 3: await callback.answer("❌ Максимум 3 активные задачи", show_alert=True) return interface._user_task_state[uid] = {"step": "channel"} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) await callback.message.edit_text( f"📝 **НОВАЯ ЗАДАЧА**\n\n" f"💰 Баланс: {balance} переходов\n\n" "Шаг 1/2: Введите название вашего Twitch канала:", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data == "enter_admin_mode") async def cb_enter_admin_mode(callback: CallbackQuery): uid = callback.from_user.id if not interface.auth_manager.is_authenticated(uid): await callback.answer("❌ Требуется авторизация", show_alert=True) return interface._user_task_state.pop(uid, None) interface._waiting_password[uid] = "admin_upgrade" builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="user_menu")) await callback.message.edit_text( "🔑 Введите пароль администратора:", reply_markup=builder.as_markup() ) await callback.answer() @dp.callback_query(F.data == "switch_to_user") async def cb_switch_to_user(callback: CallbackQuery): uid = callback.from_user.id if not interface.auth_manager.is_authenticated(uid): await callback.answer("❌ Требуется авторизация", show_alert=True) return interface.auth_manager.login(uid, is_admin=False) await interface._show_user_menu(callback.message) await callback.answer("👤 Переключились в режим пользователя") @dp.callback_query(F.data == "cancel_new_task") async def cb_cancel_new_task(callback: CallbackQuery): interface._user_task_state.pop(callback.from_user.id, None) await interface._show_user_menu(callback.message) await callback.answer("Отменено") @dp.callback_query(F.data == "my_tasks") async def cb_my_tasks(callback: CallbackQuery): await interface._show_user_tasks(callback.message, callback.from_user.id, edit=True) await callback.answer() async def _get_user_task(callback: CallbackQuery, tid: str): uid = callback.from_user.id if not interface.auth_manager.is_authenticated(uid): await callback.answer("❌ Требуется авторизация", show_alert=True) return None, uid params = await interface.task_manager.get_task(tid) if not params or params.user_id != uid: await callback.answer("❌ Задача не найдена", show_alert=True) return None, uid return params, uid @dp.callback_query(F.data.startswith("ustop_")) async def cb_user_stop_task(callback: CallbackQuery): tid = callback.data.replace("ustop_", "", 1) params, uid = await _get_user_task(callback, tid) if not params: return await interface.background_tasks.cancel_task(tid) await interface.task_manager.remove_task(tid) if interface.storage: await interface.storage.delete_task(tid) await callback.answer("🗑️ Задача удалена") await interface._show_user_tasks(callback.message, uid, edit=True) @dp.callback_query(F.data.startswith("upause_")) async def cb_user_pause_task(callback: CallbackQuery): tid = callback.data.replace("upause_", "", 1) params, uid = await _get_user_task(callback, tid) if not params: return await interface.task_manager.pause_task(tid) await callback.answer("⏸️ Пауза") await interface._show_user_tasks(callback.message, uid, edit=True) @dp.callback_query(F.data.startswith("uresume_")) async def cb_user_resume_task(callback: CallbackQuery): tid = callback.data.replace("uresume_", "", 1) params, uid = await _get_user_task(callback, tid) if not params: return await interface.task_manager.resume_task(tid) await callback.answer("▶️ Возобновлено") await interface._show_user_tasks(callback.message, uid, edit=True) @dp.callback_query(F.data.startswith("uedit_")) async def cb_user_edit_task(callback: CallbackQuery): tid = callback.data.replace("uedit_", "", 1) params, uid = await _get_user_task(callback, tid) if not params: return interface._user_task_state[uid] = {"step": "edit_domains", "task_id": tid} current = ", ".join(params.allowed_domains) if params.allowed_domains else "все" builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="my_tasks")) await callback.message.edit_text( f"🌐 **Изменить домены**\n\n" f"Текущие: `{current}`\n\n" "Введите новые домены через запятую:", reply_markup=builder.as_markup() ) await callback.answer() # === Панель пользователей (admin) === @dp.callback_query(F.data == "menu_users") async def cb_menu_users(callback: CallbackQuery): if not await require_admin(callback): return await interface._show_users_list(callback.message, edit=True) await callback.answer() @dp.callback_query(F.data.startswith("udetail_")) async def cb_user_detail(callback: CallbackQuery): if not await require_admin(callback): return target_uid = int(callback.data.replace("udetail_", "", 1)) await interface._show_user_detail(callback, target_uid) await callback.answer() @dp.callback_query(F.data.startswith("ubal_")) async def cb_user_balance(callback: CallbackQuery): if not await require_admin(callback): return parts = callback.data.split("_") # формат: ubal_{amount}_{user_id} или ubal_custom_{user_id} if parts[1] == "custom": target_uid = int(parts[2]) interface._admin_balance_state[callback.from_user.id] = target_uid builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}")) await callback.message.answer("Введите сумму для пополнения:", reply_markup=builder.as_markup()) await callback.answer() else: amount = int(parts[1]) target_uid = int(parts[2]) new_balance = await interface.balance_storage.add_balance(target_uid, amount) await callback.answer(f"✅ +{amount} переходов. Баланс: {new_balance}") await interface._show_user_detail(callback, target_uid) @dp.callback_query(F.data.startswith("utasks_")) async def cb_user_tasks_admin(callback: CallbackQuery): if not await require_admin(callback): return target_uid = int(callback.data.replace("utasks_", "", 1)) await interface._show_user_tasks(callback.message, target_uid, edit=True, for_admin=True) await callback.answer() # Единый обработчик всех текстовых сообщений — регистрируется последним, # чтобы не перекрывать обработчики кнопок с фильтрами F.text @dp.message() async def handle_message(message: Message): user_id = message.from_user.id text = message.text.strip() if message.text else "" # Ввод пароля (неавторизованный пользователь ждёт пароль) if user_id in interface._waiting_password: if not text: return # Пароль удаляем немедленно await interface._safe_delete(message.bot, message.chat.id, message.message_id) mode = interface._waiting_password.pop(user_id) if mode == "register": if len(text) < 4: sent = await message.answer("❌ Пароль слишком короткий (минимум 4 символа). Попробуйте ещё раз:") interface._menu_msg[user_id] = sent.message_id interface._waiting_password[user_id] = "register" return password_hash, salt = interface.auth_manager._hash_password(text) await interface.auth_storage.register(user_id, password_hash, salt) is_admin = (text == ADMIN_PASSWORD) interface.auth_manager.login(user_id, is_admin=is_admin) if is_admin: await interface._update_commands() await interface._show_main_menu(message) else: await interface._show_user_menu(message) elif mode == "login": user_data = await interface.auth_storage.get_user(user_id) if not user_data: await interface._send_temp(message, "❌ Вы не зарегистрированы.\nНапишите администратору.") return if interface.auth_manager.is_locked_out(user_id): await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.") return if text == ADMIN_PASSWORD: interface.auth_manager.login(user_id, is_admin=True) await interface._update_commands() await interface._show_main_menu(message) elif interface.auth_manager.verify_password( text, user_data["password_hash"], user_data["salt"] ): interface.auth_manager.login(user_id, is_admin=False) await interface._show_user_menu(message) else: interface.auth_manager.record_failed_attempt(user_id) sent = await message.answer("❌ Неверный пароль. Попробуйте ещё раз:") interface._menu_msg[user_id] = sent.message_id interface._waiting_password[user_id] = "login" elif mode == "admin_upgrade": if text == ADMIN_PASSWORD: interface.auth_manager.login(user_id, is_admin=True) await interface._update_commands() await interface._show_main_menu(message) else: sent = await message.answer("❌ Неверный пароль администратора. Попробуйте ещё раз:") interface._menu_msg[user_id] = sent.message_id interface._waiting_password[user_id] = "admin_upgrade" return # Ввод шагов создания задачи (пользователь) if user_id in interface._user_task_state: await interface._process_user_task_input(message) return if not interface.auth_manager.is_authenticated(user_id): await interface._send_temp( message, "🔒 Требуется авторизация!\n\n/register <пароль> — зарегистрироваться\n/login <пароль> — войти" ) return if not interface.auth_manager.is_admin(user_id): await interface._show_user_menu(message) return # Ввод суммы баланса (admin) if user_id in interface._admin_balance_state: target_uid = interface._admin_balance_state.pop(user_id) await interface._safe_delete(message.bot, message.chat.id, message.message_id) try: amount = int(text) if amount <= 0: raise ValueError new_balance = await interface.balance_storage.add_balance(target_uid, amount) await interface._send_temp(message, f"✅ Пополнено на {amount}. Баланс: {new_balance}") except (ValueError, TypeError): await interface._send_temp(message, "❌ Введите целое положительное число") return logger.info(f"Handle text: '{text[:50]}' from {user_id}, param_state={user_id in interface._user_input_state}") if user_id in interface._user_input_state: await interface._process_param_input(message) return await interface._safe_delete(message.bot, message.chat.id, message.message_id) if text.startswith('http://') or text.startswith('https://'): await interface._add_visit(message) return parts = text.split() if parts and not text.startswith('/') and re.match(r'^[a-zA-Z0-9_]+$', parts[0]): await interface._add_streamer(message, parts) return logger.info(f"Unhandled message: {text[:50]}") # ========================================================================= # ДОБАВЛЕНИЕ ЗАДАЧ # ========================================================================= async def _add_streamer(self, message: Message, parts: list): channel = parts[0].lower() # Проверяем, что второй параметр - число (посещений), а не username vl = settings.DEFAULT_VISITS_PER_LINK mm = settings.DEFAULT_MONITOR_MINUTES mind = settings.DEFAULT_MIN_DELAY maxd = settings.DEFAULT_MAX_DELAY minr = settings.DEFAULT_MIN_READING maxr = settings.DEFAULT_MAX_READING domains = None try: if len(parts) > 1: vl = int(parts[1]) if len(parts) > 2: mm = int(parts[2]) if len(parts) > 3 and '-' in parts[3]: mind, maxd = parse_range(parts[3]) if len(parts) > 4 and '-' in parts[4]: minr, maxr = parse_range(parts[4], default_min=1) if len(parts) > 5: domains = [d.strip() for d in parts[5].split(',')] except ValueError as e: await message.answer(f"❌ {e}") return tid = f"twitch_{channel}_{datetime.now().strftime('%H%M%S')}" params = TaskParams( task_type="twitch_irc", channel=channel, target_username="*", # Все пользователи visits_per_link=vl, monitor_minutes=mm, min_delay=mind, max_delay=maxd, min_reading=minr, max_reading=maxr, allowed_domains=domains, started_at=datetime.now(), chat_id=message.chat.id ) await self.task_manager.add_task(tid, params) success = await self.background_tasks.start_task( task_id=tid, coro=self._run_twitch(tid, params, message), task_type="twitch_irc", metadata={'type': 'twitch_irc', 'channel': channel, 'chat_id': message.chat.id} ) if success: if self.storage: await self.storage.save_task(tid, params) if self.chat_storage: await self.chat_storage.add_streamer(message.chat.id, channel) domains_str = ", ".join(domains) if domains else "все" text = ( f"✅ **МОНИТОРИНГ ЗАПУЩЕН**\n\n" f"📺 Канал: `{channel}`\n" f"👤 Отслеживание: все пользователи\n" f"🔗 На ссылку: {vl} визитов\n" f"⏱️ Мониторинг: {mm} мин\n" f"⏱️ Задержка: {format_range(mind, maxd)}с\n" f"📖 Чтение: {format_range(minr, maxr)}с\n" f"🌐 Домены: {domains_str}\n" ) text += f"\n🆔 `{tid[:12]}...`\n📺 /streamers — управление" await message.answer(text) async def _add_visit(self, message: Message): """Добавление URL для посещения.""" parts = message.text.strip().split() url = parts[0] min_delay = settings.DEFAULT_MIN_DELAY max_delay = settings.DEFAULT_MAX_DELAY min_reading = settings.DEFAULT_MIN_READING max_reading = settings.DEFAULT_MAX_READING max_visits = None try: if len(parts) > 1 and '-' in parts[1]: min_delay, max_delay = parse_range(parts[1]) elif len(parts) > 1: min_delay = max_delay = int(parts[1]) if len(parts) > 2: if '-' in parts[2]: min_reading, max_reading = parse_range(parts[2], default_min=1) else: max_visits = int(parts[2]) if len(parts) > 3 and '-' in parts[3]: min_reading, max_reading = parse_range(parts[3], default_min=1) except ValueError as e: await message.answer(f"❌ Ошибка: {e}") return domain = extract_domain(url)[:20] task_id = f"visit_{domain}_{datetime.now().strftime('%H%M%S')}" params = TaskParams( url=url, task_type="visit", min_delay=min_delay, max_delay=max_delay, min_reading=min_reading, max_reading=max_reading, max_visits=max_visits, started_at=datetime.now(), chat_id=message.chat.id ) await self.task_manager.add_task(task_id, params) scheduler = VisitScheduler( browser_service=self.browser_service, url=url, min_delay=min_delay, max_delay=max_delay, min_reading=min_reading, max_reading=max_reading, max_visits=max_visits, ) async def on_complete(visit_num, result, delay, reading): await send_visit_result(message.bot, message.chat.id, visit_num, result, delay, reading, scheduler.successful, scheduler.failed) scheduler.on_visit_complete = on_complete success = await self.background_tasks.start_task( task_id=task_id, coro=scheduler.run(), task_type="visit", metadata={'type': 'visit', 'url': url, 'chat_id': message.chat.id} ) if success: await message.answer( f"✅ **ПОСЕЩЕНИЕ ЗАПУЩЕНО**\n\n" f"🌐 `{url}`\n" f"⏱️ Задержка: {format_range(min_delay, max_delay)}с\n" f"📖 Чтение: {format_range(min_reading, max_reading)}с\n" f"🔢 Повторов: {'∞' if max_visits is None else max_visits}\n" f"🆔 `{task_id}`" ) # ========================================================================= # ОТОБРАЖЕНИЕ # ========================================================================= async def _safe_delete(self, bot, chat_id: int, msg_id: int) -> None: try: await bot.delete_message(chat_id, msg_id) except Exception: pass async def _send_temp(self, message: Message, text: str, delay: int = 5) -> None: """Отправляет сообщение и удаляет его через delay секунд.""" sent = await message.answer(text) async def _delete(): await asyncio.sleep(delay) await self._safe_delete(message.bot, message.chat.id, sent.message_id) asyncio.create_task(_delete()) async def _clean_prev(self, message: Message, user_id: int = None) -> None: """Удаляет триггер-сообщение и все предыдущие меню пользователя.""" if user_id is None: user_id = message.from_user.id if message.from_user else None if not user_id: return if user_id in self._menu_top_msg: await self._safe_delete(message.bot, message.chat.id, self._menu_top_msg.pop(user_id)) if user_id in self._menu_msg: await self._safe_delete(message.bot, message.chat.id, self._menu_msg.pop(user_id)) await self._safe_delete(message.bot, message.chat.id, message.message_id) async def _edit_or_send(self, message: Message, text: str, markup, edit: bool, user_id: int = None) -> None: if edit: try: await message.edit_text(text, reply_markup=markup) return except Exception as e: logger.debug(f"Edit message skipped: {e}") await self._clean_prev(message, user_id=user_id) sent = await message.answer(text, reply_markup=markup) uid = user_id or (message.from_user.id if message.from_user else None) if uid: self._menu_msg[uid] = sent.message_id async def _show_main_menu(self, message: Message, user_id: int = None): """Главное меню.""" stats = await self.task_manager.get_stats() text = ( "👋 **ГЛАВНОЕ МЕНЮ**\n\n" f"📺 Twitch: **{stats['twitch']}**\n" f"🌐 Визитов: **{stats['visit']}**\n" f"⏸️ Пауза: **{stats['paused']}**\n\n" "Выберите раздел:" ) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=f"📺 Стримеры ({stats['twitch']})", callback_data="menu_streamers")) builder.row(InlineKeyboardButton(text=f"📊 Задачи ({stats['total']})", callback_data="menu_tasks")) builder.row(InlineKeyboardButton(text="📈 Статус", callback_data="menu_status")) builder.row(InlineKeyboardButton(text="👥 Пользователи", callback_data="menu_users")) builder.row(InlineKeyboardButton(text="🛑 Остановить всё", callback_data="stop_all")) builder.row(InlineKeyboardButton(text="👤 Режим пользователя", callback_data="switch_to_user")) reply = ReplyKeyboardBuilder() reply.row(KeyboardButton(text="📺 Стримеры"), KeyboardButton(text="📊 Задачи")) reply.row(KeyboardButton(text="👥 Пользователи"), KeyboardButton(text="📈 Статус")) reply.row(KeyboardButton(text="🚀 Быстрый старт"), KeyboardButton(text="🛑 Остановить всё")) uid = user_id or (message.from_user.id if message.from_user else None) await self._clean_prev(message, user_id=uid) sent1 = await message.answer(text, reply_markup=reply.as_markup(resize_keyboard=True)) sent2 = await message.answer("💡 Кнопки управления:", reply_markup=builder.as_markup()) if uid: self._menu_top_msg[uid] = sent1.message_id self._menu_msg[uid] = sent2.message_id async def _show_streamers_list(self, message: Message, edit: bool = False): active = await self.task_manager.get_active_tasks() completed = await self.task_manager.get_completed_tasks() twitch_active = {tid: p for tid, p in active.items() if p.task_type == "twitch_irc"} twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"} if not twitch_active and not twitch_done: text = "📺 **СТРИМЕРЫ**\n\nНет задач.\nОтправьте `<канал>` для добавления." builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) else: text = "📺 **СТРИМЕРЫ**\n\n" builder = InlineKeyboardBuilder() if twitch_active: text += "**🔄 Активные:**\n" for tid, p in list(twitch_active.items())[:10]: emoji = p.get_status_emoji() domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" text += ( f"{emoji} **{p.channel}**\n" f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n" ) builder.row(InlineKeyboardButton( text=f"{emoji} {p.channel} (🔗{p.links_found})", callback_data=f"tdetail_{tid}" )) if twitch_done: text += "\n**📁 Завершенные:**\n" for tid, p in list(twitch_done.items())[:5]: emoji = p.get_status_emoji() domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" text += ( f"{emoji} **{p.channel}**\n" f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n" ) builder.row(InlineKeyboardButton( text=f"{emoji} {p.channel} (завершена)", callback_data=f"tdetail_{tid}" )) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _show_tasks_list(self, message: Message, edit: bool = False): """Все задачи.""" text = await self.task_manager.format_task_list() builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_tasks")) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _show_status(self, message: Message, edit: bool = False): """Статус.""" stats = await self.task_manager.get_stats() proxy_stats = self.proxy_manager.get_stats() text = ( "📈 **СТАТУС**\n\n" f"📺 Twitch: **{stats['twitch']}**\n" f"🌐 Визиты: **{stats['visit']}**\n" f"⏸️ Пауза: **{stats['paused']}**\n" f"🔄 Активно: **{stats['active']}**\n\n" f"🔌 Прокси: {proxy_stats['total']} всего, {proxy_stats['available']} доступно" ) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_status")) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) await self._edit_or_send(message, text, builder.as_markup(), edit) # ========================================================================= # УПРАВЛЕНИЕ ЗАДАЧЕЙ # ========================================================================= async def _show_task_detail(self, callback: CallbackQuery, task_id: str): params = await self.task_manager.get_task(task_id) if not params: await callback.answer("❌ Задача не найдена") return text = f"📋 **УПРАВЛЕНИЕ**\n\n{params.get_stats_text()}" builder = InlineKeyboardBuilder() # Для активных задач if not params.completed and not params.stopped: if params.paused: builder.row(InlineKeyboardButton(text="▶️ Возобновить", callback_data=f"tresume_{task_id}")) else: builder.row(InlineKeyboardButton(text="⏸️ Пауза", callback_data=f"tpause_{task_id}")) builder.row( InlineKeyboardButton(text="⏱️ Задержка", callback_data=f"tset_{task_id}_delay"), InlineKeyboardButton(text="📖 Чтение", callback_data=f"tset_{task_id}_reading") ) builder.row( InlineKeyboardButton(text="🔗 На ссылку", callback_data=f"tset_{task_id}_perlink"), InlineKeyboardButton(text="⏰ Минуты", callback_data=f"tset_{task_id}_minutes") ) builder.row( InlineKeyboardButton(text="🌐 Домены", callback_data=f"tset_{task_id}_domains"), InlineKeyboardButton(text="⏭️ Пропустить", callback_data=f"tskip_{task_id}") ) builder.row( InlineKeyboardButton(text="🔄 Сброс", callback_data=f"treset_{task_id}"), InlineKeyboardButton(text="🛑 Стоп", callback_data=f"tstop_{task_id}") ) else: # Для завершенных/остановленных builder.row( InlineKeyboardButton(text="🔄 Перезапустить", callback_data=f"trestart_task_{task_id}"), ) builder.row( InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"tdelete_{task_id}") ) builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data="menu_streamers")) try: await callback.message.edit_text(text, reply_markup=builder.as_markup()) except Exception as e: if "not modified" not in str(e): logger.error(f"Edit error: {e}") async def _process_param_input(self, message: Message): """Обработка ввода параметров.""" user_id = message.from_user.id state = self._user_input_state.pop(user_id) # Удаляем prompt и ответ пользователя prompt_id = state.get("prompt_msg_id") if prompt_id: await self._safe_delete(message.bot, message.chat.id, prompt_id) await self._safe_delete(message.bot, message.chat.id, message.message_id) task_id = state["task_id"] param = state["param"] value = message.text.strip() if message.text else "" value = value.replace("'", "").replace('"', "").replace("`", "").strip() logger.info(f"Process param: {param} = {value}") if not value: await message.answer("❌ Пустое значение. Попробуйте ещё раз.") return try: if param == "delay": if "-" in value: mn, mx = parse_range(value) else: mn = mx = int(value) await self.task_manager.update_task(task_id, min_delay=mn, max_delay=mx) await message.answer(f"✅ Задержка: {format_range(mn, mx)}с") elif param == "reading": if "-" in value: mn, mx = parse_range(value, default_min=1) else: mn = mx = int(value) await self.task_manager.update_task(task_id, min_reading=mn, max_reading=mx) await message.answer(f"✅ Чтение: {format_range(mn, mx)}с") elif param == "perlink": await self.task_manager.update_task(task_id, visits_per_link=int(value)) await message.answer(f"✅ На ссылку: {value}") elif param == "minutes": await self.task_manager.update_task(task_id, monitor_minutes=int(value)) await message.answer(f"✅ Мониторинг: {value} мин") elif param == "domains": if value.lower() in ["all", "все"]: await self.task_manager.update_task(task_id, allowed_domains=None) await message.answer("✅ Домены: все (фильтр отключен)") else: domains = [d.strip().lower() for d in value.split(",") if d.strip()] await self.task_manager.update_task(task_id, allowed_domains=domains) await message.answer(f"✅ Домены: {', '.join(domains)}") else: await message.answer(f"❌ Неизвестный параметр: {param}") except ValueError as e: await message.answer(f"❌ Ошибка: {e}") async def _run_twitch(self, task_id: str, params: TaskParams, message: Message): from services.irc_service import TwitchIRCClient irc = TwitchIRCClient(params.channel, params.target_username) async def on_url(url, username): """Запускаем обработку ссылки в отдельной задаче.""" task = asyncio.create_task(self._process_url(url, username, params, message)) task.add_done_callback( lambda t: logger.error(f"URL processing failed: {t.exception()}") if not t.cancelled() and t.exception() else None ) def is_active(): return not params.paused and not params.stopped logger.info(f"🚀 IRC monitor: {params.channel}") try: await irc.listen_for_messages(on_url, params.monitor_duration, params.allowed_domains, is_active=is_active) # Только для задач с лимитом времени (admin), не для бесконечных (user) if params.monitor_minutes > 0: await self.task_manager.complete_task(task_id) except asyncio.CancelledError: raise except Exception as e: logger.error(f"IRC error [{params.channel}]: {e}") # Не помечаем как завершённую — задача будет восстановлена при рестарте finally: await irc.disconnect() if self.storage: await self.storage.save_task(task_id, params) async def _process_url(self, url: str, username: str, params: TaskParams, message: Message): """Обрабатывает найденную ссылку (выполняется параллельно).""" try: params.links_found += 1 # Проверяем баланс для пользовательских задач if params.user_id: balance = await self.balance_storage.get_balance(params.user_id) if balance <= 0: params.stopped = True await send_message_safe( message.bot, params.chat_id, "⚠️ Баланс исчерпан — задача остановлена.\n" "Обратитесь к администратору для пополнения." ) return logger.info(f"🔗 @{username}: {url}") await send_message_safe( message.bot, message.chat.id, f"🔗 @{username}: `{url[:60]}`" ) for i in range(params.visits_per_link): while params.paused: await asyncio.sleep(1) if params.stopped: break reading = params.get_reading_time() try: result = await self.browser_service.visit_page_from_twitch( url=url, channel=params.channel, reading_time=reading ) params.total_visits += 1 if result.success: params.successful_visits += 1 if params.user_id: new_balance = await self.balance_storage.deduct(params.user_id, 1) if new_balance <= 0: params.stopped = True await send_message_safe( message.bot, params.chat_id, "⚠️ Баланс исчерпан — задача остановлена.\n" "Обратитесь к администратору для пополнения." ) break except Exception as e: logger.error(f"Visit error: {e}") if i < params.visits_per_link - 1: await asyncio.sleep(params.get_delay()) except Exception as e: logger.error(f"Process URL error: {e}") # ========================================================================= # КАБИНЕТ ПОЛЬЗОВАТЕЛЯ # ========================================================================= async def _show_user_menu(self, message: Message, edit: bool = False): user_id = message.from_user.id balance = await self.balance_storage.get_balance(user_id) all_tasks = await self.task_manager.get_all_tasks() active = sum( 1 for p in all_tasks.values() if p.user_id == user_id and not p.completed and not p.stopped ) text = ( "👤 **ЛИЧНЫЙ КАБИНЕТ**\n\n" f"💰 Баланс: **{balance}** переходов\n" f"📋 Активных задач: **{active}**\n\n" "Выберите действие:" ) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"), InlineKeyboardButton(text="📋 Мои задачи", callback_data="my_tasks"), ) builder.row(InlineKeyboardButton(text="💰 Баланс", callback_data="my_balance")) builder.row(InlineKeyboardButton(text="🔑 Режим администратора", callback_data="enter_admin_mode")) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _show_user_tasks( self, message: Message, user_id: int, edit: bool = False, for_admin: bool = False ): all_tasks = await self.task_manager.get_all_tasks() user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == user_id} if not user_tasks: text = f"📋 **ЗАДАЧИ** {'пользователя `' + str(user_id) + '`' if for_admin else ''}\n\nЗадач нет." else: header = f"📋 **ЗАДАЧИ** `{user_id}`\n\n" if for_admin else "📋 **МОИ ЗАДАЧИ**\n\n" text = header for tid, p in list(user_tasks.items())[:10]: domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" text += ( f"{p.get_status_emoji()} 📺 `{p.channel}` — 🌐 {domains}\n" f" 🔗 {p.links_found} | ✅ {p.successful_visits} | 📊 {p.total_visits}\n" ) builder = InlineKeyboardBuilder() if for_admin: builder.row(InlineKeyboardButton(text="🔙 К пользователю", callback_data=f"udetail_{user_id}")) else: for tid, p in list(user_tasks.items())[:8]: if not p.completed and not p.stopped: pause_btn = ( InlineKeyboardButton(text="▶️", callback_data=f"uresume_{tid}") if p.paused else InlineKeyboardButton(text="⏸️", callback_data=f"upause_{tid}") ) builder.row( pause_btn, InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{tid}"), InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"), ) else: builder.row(InlineKeyboardButton( text=f"🗑️ {p.channel} (завершена)", callback_data=f"ustop_{tid}" )) builder.row(InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_menu")) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _process_user_task_input(self, message: Message): user_id = message.from_user.id state = self._user_task_state[user_id] text = message.text.strip() if message.text else "" if not text: return await self._safe_delete(message.bot, message.chat.id, message.message_id) if state["step"] == "channel": if not re.match(r'^[a-zA-Z0-9_]+$', text): await message.answer("❌ Неверное название канала (только буквы, цифры, _). Попробуйте ещё раз:") return channel = text.lower() # Проверка на дубликат all_tasks = await self.task_manager.get_all_tasks() duplicate = any( p.user_id == user_id and p.channel == channel and not p.completed and not p.stopped for p in all_tasks.values() ) if duplicate: await self._send_temp( message, f"❌ У вас уже есть активная задача для канала `{channel}`.\nВведите другой канал:" ) return # Удаляем промпт шага 1 перед отправкой шага 2 if user_id in self._menu_msg: await self._safe_delete(message.bot, message.chat.id, self._menu_msg.pop(user_id)) self._user_task_state[user_id] = {"step": "domains", "channel": channel} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) sent = await message.answer( f"📺 Канал: `{channel}`\n\n" "Шаг 2/2: Введите домены через запятую:\n" "_Пример: `twitch.tv, youtube.com`_", reply_markup=builder.as_markup() ) self._menu_msg[user_id] = sent.message_id elif state["step"] in ("domains", "edit_domains"): domains = [d.strip().lower() for d in text.split(",") if d.strip()] if not domains: await self._send_temp(message, "❌ Введите хотя бы один домен. Пример: `twitch.tv, example.com`") return invalid = [d for d in domains if not re.match(r'^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', d)] if invalid: await self._send_temp(message, f"❌ Неверный формат: `{', '.join(invalid)}`\nПример: `twitch.tv, example.com`") return del self._user_task_state[user_id] # Редактирование доменов существующей задачи if state["step"] == "edit_domains": task_id = state["task_id"] await self._clean_prev(message) await self.task_manager.update_task(task_id, allowed_domains=domains) if self.storage: params = await self.task_manager.get_task(task_id) if params: await self.storage.save_task(task_id, params) await self._send_temp(message, f"✅ Домены обновлены: `{', '.join(domains)}`", delay=4) await self._show_user_tasks(message, user_id) return # Удаляем промпт шага 2 перед любым ответом await self._clean_prev(message) # Создание новой задачи balance = await self.balance_storage.get_balance(user_id) if balance <= 0: await self._send_temp(message, "❌ Недостаточно баланса. Обратитесь к администратору.") return channel = state["channel"] task_id = f"u{user_id}_{channel}_{datetime.now().strftime('%H%M%S')}" params = TaskParams( task_type="twitch_irc", channel=channel, target_username="*", allowed_domains=domains, user_id=user_id, visits_per_link=settings.DEFAULT_VISITS_PER_LINK, monitor_minutes=0, min_delay=settings.DEFAULT_MIN_DELAY, max_delay=settings.DEFAULT_MAX_DELAY, min_reading=settings.DEFAULT_MIN_READING, max_reading=settings.DEFAULT_MAX_READING, started_at=datetime.now(), chat_id=message.chat.id, ) await self.task_manager.add_task(task_id, params) success = await self.background_tasks.start_task( task_id=task_id, coro=self._run_twitch(task_id, params, message), task_type="twitch_irc", metadata={"type": "twitch_irc", "channel": channel, "chat_id": message.chat.id}, ) if success: if self.storage: await self.storage.save_task(task_id, params) await self._send_temp( message, f"✅ Мониторинг `{channel}` запущен!\n🌐 {', '.join(domains)}\n💰 Баланс: {balance}", delay=6 ) await self._show_user_menu(message) else: await self._send_temp(message, "❌ Ошибка запуска задачи") # ========================================================================= # ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN) # ========================================================================= async def _show_users_list(self, message: Message, edit: bool = False): balances = await self.balance_storage.get_all() all_tasks = await self.task_manager.get_all_tasks() text = "👥 **ПОЛЬЗОВАТЕЛИ**\n\n" builder = InlineKeyboardBuilder() if not balances: text += "Нет пользователей с балансом." else: for uid_str, balance in list(balances.items())[:15]: uid = int(uid_str) active = sum( 1 for p in all_tasks.values() if p.user_id == uid and not p.completed and not p.stopped ) text += f"👤 `{uid}` — 💰 {balance} | 📋 {active} задач\n" builder.row(InlineKeyboardButton( text=f"👤 {uid} (💰{balance})", callback_data=f"udetail_{uid}", )) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _show_user_detail(self, callback: CallbackQuery, target_user_id: int): balance = await self.balance_storage.get_balance(target_user_id) all_tasks = await self.task_manager.get_all_tasks() user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == target_user_id} active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped) text = ( f"👤 **Пользователь** `{target_user_id}`\n\n" f"💰 Баланс: **{balance}** переходов\n" f"📋 Задач: {len(user_tasks)} (активных: {active})\n\n" "Пополнить баланс:" ) builder = InlineKeyboardBuilder() for amount in [10, 50, 100, 500]: builder.button(text=f"+{amount}", callback_data=f"ubal_{amount}_{target_user_id}") builder.adjust(4) builder.row(InlineKeyboardButton( text="✏️ Другая сумма", callback_data=f"ubal_custom_{target_user_id}", )) if user_tasks: builder.row(InlineKeyboardButton( text=f"📋 Задачи ({len(user_tasks)})", callback_data=f"utasks_{target_user_id}", )) builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data="menu_users")) try: await callback.message.edit_text(text, reply_markup=builder.as_markup()) except Exception: await callback.message.answer(text, reply_markup=builder.as_markup())