""" Обработчики команд с удобным интерфейсом управления задачами. """ import asyncio import logging import os import random 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.browser_pool import BrowserPool 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, RubleBalanceStorage, PaymentStorage from services.payment_service import HelketPayment from services.twitch_api import get_viewer_count 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, browser_pool: BrowserPool = None, 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.browser_pool = browser_pool 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.rub_storage = RubleBalanceStorage() # рубли self.payment_storage = PaymentStorage() self.heleket = HelketPayment() self._user_input_state: Dict[int, dict] = {} self._payment_state: Dict[int, str] = {} # user_id -> "topup" | "buy_clicks" self._payment_confirm: Dict[int, dict] = {} # user_id -> {clicks, rub} pending confirm self._topup_confirm: Dict[int, int] = {} # user_id -> rub amount pending confirm 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._chat_history: Dict[int, list] = {} self._bot_ref = bot_ref self._waiting_password: Dict[int, str] = {} # user_id -> "register" or "login" self._start_time = datetime.now() 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._clear_chat( message.bot, message.chat.id, user_id, latest_msg_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("С возвращением! Введите пароль:") interface._waiting_password[user_id] = "login" else: sent = await message.answer( "Добро пожаловать!\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("Требуется авторизация") 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("Требуется авторизация") return False if not interface.auth_manager.is_admin(user_id): await event.answer("Нет доступа — войдите как администратор") 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.callback_query(F.data == "reload_proxies") async def cb_reload_proxies(callback: CallbackQuery): if not await require_admin(callback): return count = interface.proxy_manager.reload() await interface._show_status(callback.message, edit=True) await callback.answer(f"✅ Прокси перезагружены: {count} шт.") @dp.callback_query(F.data == "bot_restart_confirm") async def cb_restart_confirm(callback: CallbackQuery): if not await require_admin(callback): return builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="✅ Да, перезапустить", callback_data="bot_restart_do"), InlineKeyboardButton(text="❌ Отмена", callback_data="menu_status"), ) await callback.message.edit_text( "⚠️ *Перезапустить бота?*\n\n" "Все задачи будут сохранены и восстановлены автоматически.", reply_markup=builder.as_markup() ) await callback.answer() @dp.callback_query(F.data == "bot_restart_do") async def cb_restart_do(callback: CallbackQuery): if not await require_admin(callback): return await callback.message.edit_text("🔄 Перезапуск бота...") await callback.answer() asyncio.create_task(interface._do_restart()) # === Стоп всё === @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": ( "⏱️ *Пауза между переходами*\n" "Сколько секунд ждать перед следующим кликом.\n" "Диапазон `30-60` — каждый раз рандом от 30 до 60." ), "reading": ( "📖 *Время на странице*\n" "Сколько секунд браузер будет листать страницу (имитация чтения).\n" "Диапазон `45-90`." ), "perlink": ( "🔗 *Резервное кол-во кликов*\n" "Используется только если Twitch GQL недоступен.\n" "В норме: клики = зрители × CTR.\n" "Пример: `5`" ), "ctr": ( "📊 *CTR — процент от зрителей*\n" "Формула: клики = зрители × CTR%.\n" "Например: 120 зрителей × 1% = 1 клик.\n" "Диапазон `0.8-1` — случайно в этом диапазоне." ), "minutes": ( "⏰ *Длительность мониторинга*\n" "Как долго следить за чатом стримера (в минутах).\n" "`0` — мониторить бесконечно.\n" "Пример: `30`" ), "domains": ( "🌐 *Разрешённые домены*\n" "Кликать только по ссылкам с этих сайтов.\n" "Несколько через запятую: `twitch.tv, youtube.com`\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, user_id=callback.from_user.id ) await callback.answer() @dp.callback_query(F.data == "my_balance") async def cb_my_balance(callback: CallbackQuery): await interface._show_user_menu( callback.message, edit=True, user_id=callback.from_user.id ) await callback.answer() # ── Пополнение рублей (крипта → рубли) ────────────────────────────── _TOPUP_PRESETS = [100, 300, 500, 1000, 3000] @dp.callback_query(F.data == "topup_main") async def cb_topup_main(callback: CallbackQuery): uid = callback.from_user.id existing = await interface.payment_storage.get_by_user(uid) if existing: await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True) await interface._show_invoice(callback, existing) return builder = InlineKeyboardBuilder() for amount in _TOPUP_PRESETS: builder.row(InlineKeyboardButton( text=f"{amount} ₽", callback_data=f"topup_preset_{amount}" )) builder.row(InlineKeyboardButton(text="✏️ Другая сумма", callback_data="topup_custom")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input")) await callback.message.edit_text( "💳 Пополнить криптой\n\nВыберите сумму:", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data.startswith("topup_preset_")) async def cb_topup_preset(callback: CallbackQuery): uid = callback.from_user.id existing = await interface.payment_storage.get_by_user(uid) if existing: await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True) return try: amount = int(callback.data.replace("topup_preset_", "", 1)) except ValueError: await callback.answer("Ошибка") return interface._topup_confirm[uid] = amount builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="✅ Подтвердить и перейти к оплате", callback_data="confirm_topup")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="topup_main")) await callback.message.edit_text( f"💳 Пополнение криптой\n\n" f"Сумма: {amount} ₽\n\n" f"После подтверждения откроется страница оплаты.", reply_markup=builder.as_markup() ) await callback.answer() @dp.callback_query(F.data == "confirm_topup") async def cb_confirm_topup(callback: CallbackQuery): uid = callback.from_user.id amount = interface._topup_confirm.pop(uid, None) if not amount: await callback.answer("Сессия истекла, начните заново", show_alert=True) return existing = await interface.payment_storage.get_by_user(uid) if existing: await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True) await interface._show_invoice(callback, existing) return order_id = f"{uid}_topup_{int(datetime.now().timestamp())}" invoice = await interface.heleket.create_invoice(amount, order_id) if not invoice: await callback.answer("Ошибка создания счёта", show_alert=True) return payment_data = { "user_id": uid, "chat_id": callback.message.chat.id, "payment_id": invoice["payment_id"], "rub": amount, "address": invoice.get("address"), "url": invoice.get("url"), "mock": invoice.get("mock", False), "created_at": datetime.now().isoformat(), } await interface.payment_storage.save(invoice["payment_id"], payment_data) await interface._show_invoice(callback, payment_data) await callback.answer() @dp.callback_query(F.data == "topup_custom") async def cb_topup_custom(callback: CallbackQuery): uid = callback.from_user.id interface._payment_state[uid] = "topup" builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input")) await callback.message.edit_text( f"✏️ Введите сумму пополнения в рублях\n" f"(от {settings.TOPUP_MIN_RUB} до {settings.TOPUP_MAX_RUB} ₽):", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data == "check_pay") async def cb_check_payment(callback: CallbackQuery): uid = callback.from_user.id payment = await interface.payment_storage.get_by_user(uid) if not payment: await callback.answer("❌ Платёж не найден", show_alert=True) await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id) return await callback.answer("⏳ Проверяем...") # Тестовый режим — сразу начисляем без проверки платежа if payment.get("mock"): status = "paid" else: status = await interface.heleket.check_status(payment["payment_id"]) if status == "paid": await interface.payment_storage.delete(payment["payment_id"]) if payment.get("clicks"): # Прямая покупка переходов new_visits = await interface.balance_storage.add_balance(uid, payment["clicks"]) # Удаляем счёт и показываем временное уведомление await callback.message.delete() await interface._send_temp( callback.message, f"✅ Оплата прошла успешно!\n" f"+{payment['clicks']} переходов начислено\n" f"Итого переходов: {new_visits}", delay=8 ) await interface._show_user_menu(callback.message, user_id=uid) else: # Пополнение рублёвого баланса new_rub = await interface.rub_storage.add_balance(uid, payment["rub"]) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🛒 Купить переходы", callback_data="visit_shop")) builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance")) await callback.message.edit_text( f"✅ Баланс пополнен!\n\n" f"+{payment['rub']} ₽\n" f"Баланс: {new_rub} ₽", reply_markup=builder.as_markup() ) elif status == "expired": await interface.payment_storage.delete(payment["payment_id"]) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="💳 Пополнить снова", callback_data="topup_main")) builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance")) await callback.message.edit_text( "Счёт истёк. Создайте новый.", reply_markup=builder.as_markup() ) else: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔄 Проверить снова", callback_data="check_pay")) builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay")) await callback.message.edit_text( "⏳ Платёж ещё не поступил\n\nОбычно перевод занимает 1–5 минут.", reply_markup=builder.as_markup() ) @dp.callback_query(F.data == "cancel_pay") async def cb_cancel_payment(callback: CallbackQuery): uid = callback.from_user.id payment = await interface.payment_storage.get_by_user(uid) if payment: await interface.payment_storage.delete(payment["payment_id"]) await callback.answer("Счёт отменён") await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id) # ── Покупка переходов (рубли → переходы) ──────────────────────────── _CLICK_PRESETS = [50, 100, 200, 500] @dp.callback_query(F.data == "visit_shop") async def cb_visit_shop(callback: CallbackQuery): uid = callback.from_user.id price = settings.CLICK_PRICE_RUB builder = InlineKeyboardBuilder() for n in _CLICK_PRESETS: cost = int(n * price) builder.row(InlineKeyboardButton( text=f"{n} переходов — {cost} ₽", callback_data=f"buy_preset_{n}" )) builder.row(InlineKeyboardButton(text="✏️ Другое количество", callback_data="buy_custom")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input")) await callback.message.edit_text( f"🛒 Купить переходы\n\n" f"Цена: {price:.0f} ₽ за 1 переход", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() async def _show_confirmation(callback: CallbackQuery, clicks: int, cost: int): uid = callback.from_user.id interface._payment_confirm[uid] = {"clicks": clicks, "rub": cost} rub_bal = await interface.rub_storage.get_balance(uid) builder = InlineKeyboardBuilder() if rub_bal >= cost: builder.row(InlineKeyboardButton( text=f"💰 С баланса ({rub_bal} ₽)", callback_data="pay_from_balance" )) builder.row(InlineKeyboardButton(text="💳 Оплатить криптой", callback_data="confirm_buy")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_confirm_buy")) await callback.message.edit_text( f"🛒 Подтвердите покупку\n\n" f"🔢 {clicks} переходов\n" f"💰 Стоимость: {cost} ₽" + (f"\n\n🪙 Ваш баланс: {rub_bal} ₽" if rub_bal > 0 else ""), reply_markup=builder.as_markup() ) await callback.answer() @dp.callback_query(F.data == "pay_from_balance") async def cb_pay_from_balance(callback: CallbackQuery): uid = callback.from_user.id confirm = interface._payment_confirm.pop(uid, None) if not confirm: await callback.answer("Сессия истекла", show_alert=True) return clicks, cost = confirm["clicks"], confirm["rub"] rub_bal = await interface.rub_storage.get_balance(uid) if rub_bal < cost: await callback.answer(f"Недостаточно рублей: нужно {cost} ₽, есть {rub_bal} ₽", show_alert=True) return new_rub = await interface.rub_storage.deduct(uid, cost) new_visits = await interface.balance_storage.add_balance(uid, clicks) await callback.message.delete() await interface._send_temp( callback.message, f"✅ Куплено {clicks} переходов\n" f"Списано: {cost} ₽ | Остаток: {new_rub} ₽\n" f"Переходов: {new_visits}", delay=6 ) await interface._show_user_menu(callback.message, user_id=uid) await callback.answer() @dp.callback_query(F.data.startswith("buy_preset_")) async def cb_buy_preset(callback: CallbackQuery): try: clicks = int(callback.data.replace("buy_preset_", "", 1)) except ValueError: await callback.answer("Ошибка") return cost = int(clicks * settings.CLICK_PRICE_RUB) await _show_confirmation(callback, clicks, cost) @dp.callback_query(F.data == "buy_custom") async def cb_buy_custom(callback: CallbackQuery): uid = callback.from_user.id interface._payment_state[uid] = "buy_clicks" builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input")) await callback.message.edit_text( f"✏️ Введите количество переходов\n\n" f"Цена: {settings.CLICK_PRICE_RUB:.0f} ₽ за 1 переход", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data.startswith("utask_")) async def cb_user_task_detail(callback: CallbackQuery): tid = callback.data.replace("utask_", "", 1) params, uid = await _get_user_task(callback, tid) if not params: return await interface._show_user_task_detail(callback, tid, params) await callback.answer() @dp.callback_query(F.data.startswith("uset_")) async def cb_user_set_param(callback: CallbackQuery): uid = callback.from_user.id if not interface.auth_manager.is_authenticated(uid): await callback.answer("Требуется авторизация", show_alert=True) return # формат: uset_{task_id}_{param} data = callback.data.replace("uset_", "", 1) idx = data.rfind("_") if idx == -1: await callback.answer("Ошибка формата") return task_id = data[:idx] param = data[idx + 1:] params = await interface.task_manager.get_task(task_id) if not params or params.user_id != uid: await callback.answer("Задача не найдена", show_alert=True) return if params.task_type == "user_visit": cur = ( f"{params.min_series}–{params.max_series}" if params.min_series != params.max_series else str(params.min_series) ) perlink_label = ( f"🔢 *Кликов в одном цикле*\n" f"Сколько кликов подряд делать перед паузой.\n" f"Текущее: {cur}\n" f"Диапазон `2-5`." ) _d_mn = params.min_delay // 60 _d_mx = params.max_delay // 60 _d_cur = f"{_d_mn}–{_d_mx}" if _d_mn != _d_mx else str(_d_mn) delay_label = ( f"⏱️ *Пауза между циклами*\n" f"Сколько минут ждать после каждого цикла кликов.\n" f"Текущая: {_d_cur} мин\n" f"Диапазон `2-5`." ) else: perlink_label = ( f"🔗 *Резервное кол-во кликов*\n" f"Используется только если не удаётся получить зрителей.\n" f"В норме клики = зрители × CTR.\n" f"Текущее: {params.visits_per_link}\n" f"Пример: `5`" ) delay_label = ( f"⏱️ *Пауза между переходами*\n" f"Сколько секунд ждать перед следующим кликом.\n" f"Текущая: {params.min_delay}–{params.max_delay} сек\n" f"Диапазон `30-60`." ) prompts = { "delay": delay_label, "reading": ( f"📖 *Время на странице*\n" f"Сколько секунд браузер листает страницу (имитация чтения).\n" f"Текущее: {params.min_reading}–{params.max_reading} сек\n" f"Диапазон `45-90`." ), "percent": ( f"👥 *Кликов = % от зрителей*\n" f"Количество кликов рассчитывается как % от числа зрителей стрима.\n" f"Текущий: {params.visits_percent}%\n" f"`0` — отключить.\n" f"Пример: `5` = 5 кликов на 100 зрителей." ), "perlink": perlink_label, "clickdelay": ( f"⏳ *Задержка между кликами в цикле*\n" f"Сколько секунд ждать между каждым кликом внутри одного цикла.\n" f"Текущая: {params.min_click_delay}–{params.max_click_delay} сек\n" f"Диапазон `5-15`." ), "ctr": ( f"📊 *CTR — процент от зрителей*\n" f"Формула: клики = зрители × CTR%.\n" f"Текущий: {params.min_ctr}%–{params.max_ctr}%\n" f"Диапазон `0.8-1`." ), } prompt = prompts.get(param, f"Введите значение для {param}:") interface._user_input_state[uid] = { "task_id": task_id, "param": param, "menu_msg_id": callback.message.message_id, } builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"utask_{task_id}")) await callback.message.edit_text(prompt, reply_markup=builder.as_markup()) await callback.answer() @dp.callback_query(F.data == "confirm_buy") async def cb_confirm_buy(callback: CallbackQuery): uid = callback.from_user.id confirm = interface._payment_confirm.pop(uid, None) if not confirm: await callback.answer("Сессия истекла, начните заново", show_alert=True) return clicks, rub = confirm["clicks"], confirm["rub"] order_id = f"{uid}_clicks_{int(datetime.now().timestamp())}" invoice = await interface.heleket.create_invoice(rub, order_id) if not invoice: await callback.answer("Ошибка создания счёта", show_alert=True) return payment_data = { "user_id": uid, "chat_id": callback.message.chat.id, "payment_id": invoice["payment_id"], "clicks": clicks, "rub": rub, "address": invoice.get("address"), "url": invoice.get("url"), "mock": invoice.get("mock", False), "created_at": datetime.now().isoformat(), } await interface.payment_storage.save(invoice["payment_id"], payment_data) builder = InlineKeyboardBuilder() if invoice.get("url"): builder.row(InlineKeyboardButton(text="💳 Перейти к оплате", url=invoice["url"])) builder.row(InlineKeyboardButton(text="✅ Подтвердить", callback_data="check_pay")) builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay")) if payment_data["mock"]: body = "Нажмите «Подтвердить» для зачисления средств" elif invoice.get("url"): body = "Нажмите кнопку для перехода на страницу оплаты" else: body = f"Адрес:\n{invoice['address']}" await callback.message.edit_text( f"Счёт создан\n\n" f"🔢 {clicks} переходов\n" f"💰 {rub} ₽\n\n" f"{body}", reply_markup=builder.as_markup() ) await callback.answer() @dp.callback_query(F.data == "cancel_confirm_buy") async def cb_cancel_confirm_buy(callback: CallbackQuery): uid = callback.from_user.id interface._payment_confirm.pop(uid, None) await interface._show_user_menu(callback.message, edit=True, user_id=uid) await callback.answer() @dp.callback_query(F.data == "cancel_pay_input") async def cb_cancel_pay_input(callback: CallbackQuery): uid = callback.from_user.id interface._payment_state.pop(uid, None) await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id) @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 builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="📺 Twitch мониторинг", callback_data="task_type_twitch")) builder.row(InlineKeyboardButton(text="🔗 Посещение по ссылке", callback_data="task_type_url")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) await callback.message.edit_text( f"📝 Новая задача\n\nПереходов: {balance}\n\nВыберите тип:", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data == "task_type_twitch") async def cb_task_type_twitch(callback: CallbackQuery): uid = callback.from_user.id interface._user_task_state[uid] = {"step": "channel"} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) await callback.message.edit_text( "📺 Twitch мониторинг\n\nШаг 1/2 — введите название канала:", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data == "task_type_url") async def cb_task_type_url(callback: CallbackQuery): uid = callback.from_user.id balance = await interface.balance_storage.get_balance(uid) interface._user_task_state[uid] = {"step": "url"} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) await callback.message.edit_text( f"🔗 Посещение по ссылке\n\nПереходов: {balance}\n\nШаг 1/2 — введите URL:", reply_markup=builder.as_markup() ) interface._menu_msg[uid] = callback.message.message_id await callback.answer() @dp.callback_query(F.data == "skip_percent") async def cb_skip_percent(callback: CallbackQuery): uid = callback.from_user.id state = interface._user_task_state.pop(uid, None) if not state or state.get("step") != "percent": await callback.answer() return interface._menu_msg.pop(uid, None) await callback.message.delete() await interface._create_twitch_task( callback.message, uid, state["channel"], state["domains"], 0.0 ) 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, user_id=uid) await callback.answer("👤 Переключились в режим пользователя") @dp.callback_query(F.data == "noop") async def _cb_noop(callback: CallbackQuery): await callback.answer() @dp.callback_query(F.data == "cancel_new_task") async def cb_cancel_new_task(callback: CallbackQuery): uid = callback.from_user.id interface._user_task_state.pop(uid, None) await interface._show_user_menu(callback.message, user_id=uid) 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() @dp.callback_query(F.data == "my_tasks_visits") async def cb_my_tasks_visits(callback: CallbackQuery): await interface._show_user_visits(callback.message, callback.from_user.id, edit=True) await callback.answer() @dp.callback_query(F.data == "my_tasks_monitors") async def cb_my_tasks_monitors(callback: CallbackQuery): await interface._show_user_monitors(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("urestart_")) async def cb_user_restart_task(callback: CallbackQuery): tid = callback.data.replace("urestart_", "", 1) params, uid = await _get_user_task(callback, tid) if not params: return balance = await interface.balance_storage.get_balance(uid) if balance <= 0: await callback.answer("❌ Недостаточно переходов", show_alert=True) return # Сбрасываем статистику и статус await interface.task_manager.update_task( tid, completed=False, stopped=False, paused=False, successful_visits=0, total_visits=0, ) if params.task_type == "user_visit": await interface.background_tasks.cancel_task(tid) success = await interface.background_tasks.start_task( task_id=tid, coro=interface._run_url_visits(tid, params, callback.message.bot), task_type="user_visit", metadata={"type": "user_visit", "url": params.url, "chat_id": params.chat_id}, ) else: from types import SimpleNamespace chat_id = params.chat_id or callback.message.chat.id msg_mock = SimpleNamespace( chat=SimpleNamespace(id=chat_id), bot=callback.message.bot, ) await interface.background_tasks.cancel_task(tid) success = await interface.background_tasks.start_task( task_id=tid, coro=interface._run_twitch(tid, params, msg_mock), task_type="twitch_irc", metadata={"type": "twitch_irc", "channel": params.channel, "chat_id": chat_id}, ) if success: if interface.storage: await interface.storage.save_task(tid, params) await callback.answer("🔄 Задача перезапущена") else: await callback.answer("❌ Ошибка перезапуска", show_alert=True) 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, "Вы не зарегистрированы") 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 user_id in interface._payment_state: await interface._process_payment_input(message) return if not interface.auth_manager.is_authenticated(user_id): await interface._send_temp(message, "Требуется авторизация") return # Ввод параметра задачи (admin И user) if user_id in interface._user_input_state: await interface._process_param_input(message) 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 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 _track_msg(self, user_id: int, msg_id: int) -> None: pass # трекинг заменён sweep-методом async def _clear_chat(self, bot, chat_id: int, user_id: int, latest_msg_id: int = None, depth: int = 200) -> None: """Удаляет последние N сообщений бота перебором ID — трекинг не нужен.""" self._menu_msg.pop(user_id, None) self._menu_top_msg.pop(user_id, None) self._chat_history.pop(user_id, None) if not latest_msg_id: return ids = list(range(max(1, latest_msg_id - depth), latest_msg_id + 1)) # deleteMessages принимает до 100 за раз for i in range(0, len(ids), 100): try: await bot.delete_messages(chat_id, ids[i:i + 100]) except Exception: for mid in ids[i:i + 100]: await self._safe_delete(bot, chat_id, mid) async def _send_temp(self, message: Message, text: str, delay: int = 5) -> None: """Отправляет сообщение и удаляет его через delay секунд.""" sent = await message.answer(text) uid = message.from_user.id if message.from_user else None if uid: await self._track_msg(uid, sent.message_id) 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 await self._track_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 await self._track_msg(uid, sent1.message_id) await self._track_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Задач нет. Отправьте название канала для добавления." 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) def _get_sys_info(self) -> str: try: import psutil proc = psutil.Process(os.getpid()) cpu = psutil.cpu_percent(interval=None) vm = psutil.virtual_memory() proc_mb = proc.memory_info().rss / 1024 / 1024 ram_used = vm.used / 1024 / 1024 / 1024 ram_total = vm.total / 1024 / 1024 / 1024 return ( f"💻 CPU: {cpu:.1f}%\n" f"🖥️ RAM: {ram_used:.1f}/{ram_total:.1f} GB ({vm.percent:.0f}%)\n" f"🤖 Процесс: {proc_mb:.1f} MB\n" ) except Exception: return "" async def _show_status(self, message: Message, edit: bool = False): stats = await self.task_manager.get_stats() proxy_stats = self.proxy_manager.get_stats() bg_count = self.background_tasks.active_count delta = datetime.now() - self._start_time h, rem = divmod(int(delta.total_seconds()), 3600) m, s = divmod(rem, 60) uptime = f"{h}ч {m}м {s}с" if h else f"{m}м {s}с" sys_info = self._get_sys_info() text = ( "📈 *Статус бота*\n\n" f"⏱️ Аптайм: {uptime}\n\n" f"📊 *Задачи:*\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']} доступно\n" f"🔁 Фоновых задач: {bg_count}\n\n" f"{sys_info}" ) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_status")) builder.row(InlineKeyboardButton(text="🔃 Перезагрузить прокси", callback_data="reload_proxies")) builder.row(InlineKeyboardButton(text="🔁 Перезапустить бота", callback_data="bot_restart_confirm")) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _do_restart(self): """Сохраняет задачи и перезапускает процесс.""" import sys try: if self.storage: await self.storage.save_tasks(self.task_manager._tasks) await self.background_tasks.cancel_all() except Exception as e: logger.error(f"Restart cleanup error: {e}") finally: await asyncio.sleep(0.5) os.execv(sys.executable, [sys.executable] + sys.argv) # ========================================================================= # УПРАВЛЕНИЕ ЗАДАЧЕЙ # ========================================================================= 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="📊 CTR %", callback_data=f"tset_{task_id}_ctr"), 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) await self._safe_delete(message.bot, message.chat.id, message.message_id) task_id = state["task_id"] param = state["param"] menu_msg_id = state.get("menu_msg_id") 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 self._send_temp(message, "❌ Пустое значение. Попробуйте ещё раз.") return confirm = "" try: if param == "delay": if "-" in value: mn, mx = parse_range(value) else: mn = mx = int(value) task_obj = await self.task_manager.get_task(task_id) if task_obj and task_obj.task_type == "user_visit": await self.task_manager.update_task(task_id, min_delay=mn * 60, max_delay=mx * 60) confirm = f"✅ Пауза: {format_range(mn, mx)} мин" else: await self.task_manager.update_task(task_id, min_delay=mn, max_delay=mx) confirm = 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) confirm = f"✅ Чтение: {format_range(mn, mx)}с" elif param == "perlink": params_obj = await self.task_manager.get_task(task_id) if params_obj and params_obj.task_type == "user_visit": 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_series=mn, max_series=mx) lbl = f"{mn}–{mx}" if mn != mx else str(mn) confirm = f"✅ Цикл: {lbl} кликов" else: await self.task_manager.update_task(task_id, visits_per_link=int(value)) confirm = f"✅ На ссылку: {value}" elif param == "ctr": if "-" in value: parts_v = value.split("-", 1) mn_c = float(parts_v[0].replace(",", ".")) mx_c = float(parts_v[1].replace(",", ".")) else: mn_c = mx_c = float(value.replace(",", ".")) if not (0 < mn_c <= mx_c <= 100): raise ValueError("CTR должен быть от 0 до 100, например 0.8-1") await self.task_manager.update_task(task_id, min_ctr=mn_c, max_ctr=mx_c) lbl = f"{mn_c}%–{mx_c}%" if mn_c != mx_c else f"{mn_c}%" confirm = f"✅ CTR: {lbl}" elif param == "minutes": await self.task_manager.update_task(task_id, monitor_minutes=int(value)) confirm = f"✅ Мониторинг: {value} мин" elif param == "domains": if value.lower() in ["all", "все"]: await self.task_manager.update_task(task_id, allowed_domains=None) confirm = "✅ Домены: все" else: domains = [d.strip().lower() for d in value.split(",") if d.strip()] await self.task_manager.update_task(task_id, allowed_domains=domains) confirm = f"✅ Домены: {', '.join(domains)}" elif param == "percent": pct = float(value.replace(",", ".")) if pct < 0 or pct > 100: raise ValueError("Значение от 0 до 100") await self.task_manager.update_task(task_id, visits_percent=pct) confirm = f"✅ % зрителей: {pct}%" if pct > 0 else "✅ % зрителей отключён" elif param == "clickdelay": if "-" in value: mn, mx = parse_range(value, default_min=0) else: mn = mx = int(value) await self.task_manager.update_task(task_id, min_click_delay=mn, max_click_delay=mx) confirm = f"✅ Задержка между кликами: {format_range(mn, mx)}с" else: await self._send_temp(message, f"❌ Неизвестный параметр: {param}") return except ValueError as e: await self._send_temp(message, f"❌ Ошибка: {e}") return # Обновляем меню с параметрами (заменяем prompt обратно на настройки задачи) updated = await self.task_manager.get_task(task_id) if updated and menu_msg_id: try: await self._rebuild_user_task_menu( message.bot, message.chat.id, menu_msg_id, task_id, updated, confirm ) return except Exception: pass await self._send_temp(message, confirm, delay=4) async def _rebuild_user_task_menu( self, bot, chat_id: int, msg_id: int, task_id: str, params: TaskParams, confirm: str = "" ): """Редактирует сообщение обратно в настройки задачи после ввода параметра.""" if params.task_type == "user_visit": series_range = ( f"{params.min_series}–{params.max_series}" if params.min_series != params.max_series else str(params.min_series) ) info = ( f"🔗 {params.url[:50]}\n" f"📊 Выполнено: {params.successful_visits} из {params.max_visits} переходов\n\n" f"🔢 Кликов в цикле: {series_range}\n" f"⏳ Задержка между кликами: {params.min_click_delay}–{params.max_click_delay} сек\n" f"⏱ Пауза между циклами: {params.min_delay // 60}–{params.max_delay // 60} мин\n" f"📖 Время на странице: {params.min_reading}–{params.max_reading} сек" ) else: domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все" ctr_str = ( f"{params.min_ctr}%–{params.max_ctr}%" if params.min_ctr != params.max_ctr else f"{params.min_ctr}%" ) info = ( f"📺 Стример: {params.channel}\n" f"🌐 Домены: {domains}\n\n" f"🔗 Ссылок найдено: {params.links_found}\n" f"✅ Кликов: {params.successful_visits}/{params.total_planned_visits}\n" f"⏳ В очереди: {params.pending_visits}\n\n" f"📊 CTR: {ctr_str}\n" f"⏱ Пауза между кликами: {params.min_delay}–{params.max_delay} сек\n" f"📖 Время на странице: {params.min_reading}–{params.max_reading} сек" ) builder = InlineKeyboardBuilder() if params.task_type == "user_visit": builder.row( InlineKeyboardButton(text="⏱️ Пауза (мин)", callback_data=f"uset_{task_id}_delay"), InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"), ) builder.row( InlineKeyboardButton(text="🔢 Размер цикла", callback_data=f"uset_{task_id}_perlink"), InlineKeyboardButton(text="⏳ Между кликами", callback_data=f"uset_{task_id}_clickdelay"), ) else: builder.row( InlineKeyboardButton(text="⏱️ Пауза (сек)", callback_data=f"uset_{task_id}_delay"), InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"), ) builder.row( InlineKeyboardButton(text="📊 % CTR зрителей", callback_data=f"uset_{task_id}_ctr"), InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}"), ) builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks")) prefix = f"{confirm}\n\n" if confirm else "" await bot.edit_message_text( f"{prefix}⚙️ Параметры задачи\n\n{info}", chat_id=chat_id, message_id=msg_id, reply_markup=builder.as_markup() ) async def _show_user_task_detail(self, callback: CallbackQuery, task_id: str, params: TaskParams): if params.task_type == "user_visit": series_range = ( f"{params.min_series}–{params.max_series}" if params.min_series != params.max_series else str(params.min_series) ) info = ( f"🔗 {params.url[:50]}\n" f"📊 Выполнено: {params.successful_visits} из {params.max_visits} переходов\n\n" f"🔢 Кликов в цикле: {series_range}\n" f"⏳ Задержка между кликами: {params.min_click_delay}–{params.max_click_delay} сек\n" f"⏱ Пауза между циклами: {params.min_delay // 60}–{params.max_delay // 60} мин\n" f"📖 Время на странице: {params.min_reading}–{params.max_reading} сек" ) else: domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все" ctr_str = ( f"{params.min_ctr}%–{params.max_ctr}%" if params.min_ctr != params.max_ctr else f"{params.min_ctr}%" ) info = ( f"📺 Стример: {params.channel}\n" f"🌐 Домены: {domains}\n\n" f"🔗 Ссылок найдено: {params.links_found}\n" f"✅ Кликов: {params.successful_visits}/{params.total_planned_visits}\n" f"⏳ В очереди: {params.pending_visits}\n\n" f"📊 CTR: {ctr_str} от зрителей\n" f"⏱ Пауза между кликами: {params.min_delay}–{params.max_delay} сек\n" f"📖 Время на странице: {params.min_reading}–{params.max_reading} сек" ) builder = InlineKeyboardBuilder() if params.task_type == "user_visit": builder.row( InlineKeyboardButton(text="⏱️ Пауза (мин)", callback_data=f"uset_{task_id}_delay"), InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"), ) builder.row( InlineKeyboardButton(text="🔢 Размер цикла", callback_data=f"uset_{task_id}_perlink"), InlineKeyboardButton(text="⏳ Между кликами", callback_data=f"uset_{task_id}_clickdelay"), ) else: builder.row( InlineKeyboardButton(text="⏱️ Пауза (сек)", callback_data=f"uset_{task_id}_delay"), InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"), ) builder.row( InlineKeyboardButton(text="📊 % CTR зрителей", callback_data=f"uset_{task_id}_ctr"), InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}"), ) builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks")) try: await callback.message.edit_text( f"⚙️ Параметры задачи\n\n{info}", reply_markup=builder.as_markup() ) except Exception: await callback.message.answer( f"⚙️ Параметры задачи\n\n{info}", reply_markup=builder.as_markup() ) async def _stream_watcher(self, task_id: str, params: TaskParams, message: Message, check_interval: int = 60) -> None: """Проверяет онлайн-статус стрима, ставит/снимает паузу и сохраняет состояние.""" was_offline = params.stream_offline while not params.stopped: await asyncio.sleep(check_interval) if params.stopped: break viewers = await get_viewer_count(params.channel) if viewers is None: logger.debug(f"[watcher] {params.channel}: GQL failed, skipping") continue if viewers == 0 and not was_offline: was_offline = True params.stream_offline = True # Ставим паузу только если не уже на паузе вручную if not params.paused: params.paused = True params.auto_paused = True await self.task_manager.update_task(task_id, paused=True) if self.storage: await self.storage.save_task(task_id, params) logger.info(f"[watcher] {params.channel} went offline") msg = await send_message_safe( message.bot, params.chat_id, f"⏸ {params.channel} ушёл в оффлайн — мониторинг приостановлен" ) if msg and params.user_id: await self._track_msg(params.user_id, msg.message_id) elif viewers > 0 and was_offline: was_offline = False params.stream_offline = False if params.auto_paused: params.paused = False params.auto_paused = False await self.task_manager.update_task(task_id, paused=False) if self.storage: await self.storage.save_task(task_id, params) logger.info(f"[watcher] {params.channel} is online ({viewers} viewers)") msg = await send_message_safe( message.bot, params.chat_id, f"▶️ {params.channel} снова онлайн ({viewers} зрителей) — мониторинг возобновлён" ) if msg and params.user_id: await self._track_msg(params.user_id, msg.message_id) 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) url_tasks: set = set() async def on_url(url, username): if params.stream_offline: return task = asyncio.create_task(self._process_url(url, username, params, message)) url_tasks.add(task) task.add_done_callback(url_tasks.discard) 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 and not params.stream_offline async def on_drain_fail(): await self.task_manager.pause_task(task_id) await send_message_safe( message.bot, params.chat_id, f"⚠️ IRC #{params.channel}: ошибка соединения (3 попытки) — задача поставлена на паузу" ) logger.info(f"🚀 IRC monitor: {params.channel}") watcher = asyncio.create_task(self._stream_watcher(task_id, params, message)) try: await irc.listen_for_messages( on_url, params.monitor_duration, params.allowed_domains, is_active=is_active, on_drain_fail=on_drain_fail ) 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: watcher.cancel() for t in list(url_tasks): t.cancel() if url_tasks: await asyncio.gather(*url_tasks, return_exceptions=True) await irc.disconnect() if self.storage: await self.storage.save_task(task_id, params) async def _run_url_visits(self, task_id: str, params: TaskParams, bot): """Посещает URL циклами: sleep(межцикловая) → N кликов(+sleep между ними) → повтор.""" max_v = params.max_visits or 1 async def _sleep_interruptible(seconds: int): for _ in range(max(0, seconds)): if params.stopped: return while params.paused and not params.stopped: await asyncio.sleep(2) await asyncio.sleep(1) # Роут: [задержка] → клик1 → [задержка] → клик2 → ... → кликN → повтор # Пауза: текущий клик завершается, серия останавливается на N # Resume < 5 мин: продолжает с N+1 без задержки # Resume > 5 мин: серия этой ссылки отбрасывается, ждём следующий цикл remaining_clicks = 0 series_paused_at: Optional[float] = None SERIES_EXPIRY = 5 * 60 try: while params.successful_visits < max_v and not params.stopped: # Ждём паузу если активна перед новым циклом while params.paused and not params.stopped: await asyncio.sleep(2) if params.stopped: break # Определяем размер серии if remaining_clicks > 0: elapsed = datetime.now().timestamp() - (series_paused_at or 0) if elapsed > SERIES_EXPIRY: # Серия устарела — отбрасываем, ждём следующую задержку logger.info(f"⏰ Series expired ({elapsed:.0f}s > {SERIES_EXPIRY//60}min), skipping remaining {remaining_clicks} clicks") remaining_clicks = 0 series_paused_at = None # Стандартная задержка перед следующей серией await _sleep_interruptible(params.get_delay()) if params.stopped: break series_size = params.get_series_size() else: # Продолжаем с сохранённого места — без задержки series_size = remaining_clicks remaining_clicks = 0 series_paused_at = None logger.info(f"▶️ Resuming: {series_size} clicks left ({elapsed:.0f}s paused)") else: # Новая серия: задержка → рандомный размер delay = params.get_delay() series_size = params.get_series_size() logger.info( f"⏳ Next series: {series_size} clicks " f"(range {params.min_series}-{params.max_series}), " f"delay {delay}s ({delay//60}m {delay%60}s), " f"reading {params.min_reading}-{params.max_reading}s, " f"done {params.successful_visits}/{max_v}" ) await _sleep_interruptible(delay) if params.stopped: break # --- Серия кликов --- for click_num in range(series_size): if params.stopped or params.successful_visits >= max_v: break balance = await self.balance_storage.get_balance(params.user_id) if balance <= 0: await self.task_manager.stop_task(task_id) await send_message_safe(bot, params.chat_id, "⚠️ Баланс исчерпан — задача остановлена") return try: visitor = self.browser_pool or self.browser_service result = await visitor.visit_page(params.url, params.get_reading_time()) params.total_visits += 1 if result.success: params.successful_visits += 1 await self.balance_storage.deduct(params.user_id, 1) logger.info(f"✅ Visit {params.successful_visits}/{max_v}: {params.url[:40]}") else: logger.warning(f"❌ Visit failed ({params.total_visits}): {result.error or 'unknown'}") except asyncio.CancelledError: raise except Exception as e: logger.error(f"URL visit error: {e}") params.total_visits += 1 # Клик завершён — проверяем паузу if params.paused or params.stopped: remaining_clicks = series_size - click_num - 1 if remaining_clicks > 0 and not params.stopped: series_paused_at = datetime.now().timestamp() logger.info(f"⏸️ Paused after click {click_num+1}/{series_size}, {remaining_clicks} saved") break # Задержка между кликами (кроме последнего) if click_num < series_size - 1 and params.successful_visits < max_v: await _sleep_interruptible(params.get_click_delay()) await self.task_manager.complete_task(task_id) remaining = await self.balance_storage.get_balance(params.user_id) await send_message_safe( bot, params.chat_id, f"✅ Задача завершена\n" f"🔗 {params.url[:50]}\n" f"Выполнено: {params.successful_visits}/{params.max_visits}\n" f"Переходов осталось: {remaining}" ) except asyncio.CancelledError: raise finally: 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 # visits = viewers × CTR; fallback на visits_per_link если GQL недоступен ctr = random.uniform(params.min_ctr, params.max_ctr) viewers = await get_viewer_count(params.channel) if viewers and viewers > 0: visits_count = max(1, round(viewers * ctr / 100)) calc_info = f"{viewers} зрителей × {ctr:.2f}% = {visits_count} кликов" else: visits_count = max(1, round(params.visits_per_link * ctr / 100)) calc_info = f"зрители недоступны, фикс {params.visits_per_link} × {ctr:.2f}% = {visits_count} кликов" params.pending_visits += visits_count params.total_planned_visits += visits_count logger.info(f"🔗 @{username} {url} → {visits_count} clicks ({calc_info}), total pending: {params.pending_visits}") await send_message_safe( message.bot, message.chat.id, f"🔗 @{username}: {url[:60]}\n" f"📊 {calc_info}\n" f"⏳ В очереди всего: {params.pending_visits} кликов" ) # Роут: [задержка] → клик1 → [задержка] → клик2 → ... → кликN # Пауза: текущий клик завершается, серия останавливается # Resume < 5 мин: продолжает с сохранённого клика # Resume > 5 мин: серия этой ссылки полностью отбрасывается SERIES_EXPIRY = 5 * 60 remaining = visits_count paused_at: Optional[float] = None first_click = True while remaining > 0 and not params.stopped: # Проверяем не истекла ли серия после паузы if paused_at is not None: elapsed = datetime.now().timestamp() - paused_at if elapsed > SERIES_EXPIRY: logger.info(f"⏰ Серия устарела ({elapsed:.0f}s), ссылка {url[:40]} отброшена") return paused_at = None logger.info(f"▶️ Продолжаем серию: {remaining} кликов осталось") # Задержка перед каждым кликом (включая первый) if not params.stopped and not params.paused: delay = params.get_delay() if first_click: logger.info(f"⏳ Задержка перед первым кликом: {delay}с") await asyncio.sleep(delay) first_click = False if params.stopped: return reading = params.get_reading_time() try: visitor = self.browser_pool or self.browser_service result = await visitor.visit_page(url, reading) params.total_visits += 1 params.pending_visits = max(0, params.pending_visits - 1) if result.success: params.successful_visits += 1 remaining -= 1 if params.user_id: new_balance = await self.balance_storage.deduct(params.user_id, 1) if new_balance <= 0: params.stopped = True params.pending_visits = max(0, params.pending_visits - remaining) await send_message_safe( message.bot, params.chat_id, "⚠️ Баланс исчерпан — задача остановлена." ) return else: remaining -= 1 except Exception as e: logger.error(f"Visit error: {e}") params.pending_visits = max(0, params.pending_visits - 1) remaining -= 1 # Клик завершён — проверяем паузу if params.paused and remaining > 0: paused_at = datetime.now().timestamp() logger.info(f"⏸️ Серия на паузе, {remaining} кликов сохранено (истечёт через {SERIES_EXPIRY//60}мин)") while params.paused and not params.stopped: await asyncio.sleep(2) if params.stopped: return except Exception as e: logger.error(f"Process URL error: {e}") # ========================================================================= # КАБИНЕТ ПОЛЬЗОВАТЕЛЯ # ========================================================================= async def _show_user_menu(self, message: Message, edit: bool = False, user_id: int = None): if user_id is None: user_id = message.from_user.id if message.from_user else None if not user_id: return # Сбрасываем все незавершённые состояния при возврате в меню self._waiting_password.pop(user_id, None) self._user_task_state.pop(user_id, None) self._payment_state.pop(user_id, None) visits = await self.balance_storage.get_balance(user_id) rubles = await self.rub_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 ) pending = await self.payment_storage.get_by_user(user_id) text = ( "👤 Личный кабинет\n\n" f"🪙 Рублей: {rubles} ₽\n" f"🔢 Переходов: {visits}\n" f"📋 Активных задач: {active}" ) if pending: text += "\n\n⏳ Есть незакрытый счёт" builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"), InlineKeyboardButton(text="📋 Мои задачи", callback_data="my_tasks"), ) if pending: builder.row(InlineKeyboardButton(text="⏳ Проверить оплату", callback_data="check_pay")) else: builder.row( InlineKeyboardButton(text="💳 Пополнить криптой", callback_data="topup_main"), InlineKeyboardButton(text="🛒 Купить переходы", callback_data="visit_shop"), ) builder.row(InlineKeyboardButton(text="🔑 Режим администратора", callback_data="enter_admin_mode")) await self._edit_or_send(message, text, builder.as_markup(), edit, user_id=user_id) 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} visits = {tid: p for tid, p in user_tasks.items() if p.task_type == "user_visit"} monitors = {tid: p for tid, p in user_tasks.items() if p.task_type != "user_visit"} v_active = sum(1 for p in visits.values() if not p.completed and not p.stopped) m_active = sum(1 for p in monitors.values() if not p.completed and not p.stopped) 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 if visits: text += f"🔗 Посещения — {v_active} акт. / {len(visits)} всего\n" if monitors: text += f"📺 Мониторинг — {m_active} акт. / {len(monitors)} всего\n" builder = InlineKeyboardBuilder() if for_admin: builder.row(InlineKeyboardButton(text="🔙 К пользователю", callback_data=f"udetail_{user_id}")) else: builder.row( InlineKeyboardButton( text=f"🔗 Посещения ({len(visits)})", callback_data="my_tasks_visits" ), InlineKeyboardButton( text=f"📺 Мониторинг ({len(monitors)})", callback_data="my_tasks_monitors" ), ) 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) def _build_task_buttons(self, builder: InlineKeyboardBuilder, tasks: dict): for tid, p in list(tasks.items())[:8]: if p.task_type == "user_visit": domain = extract_domain(p.url)[:16] if p.url else "?" label = f"🔗 {domain}" else: label = f"📺 {p.channel}" 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( InlineKeyboardButton(text=label, callback_data=f"utask_{tid}"), pause_btn, InlineKeyboardButton(text="⚙️", callback_data=f"utask_{tid}"), InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"), ) else: builder.row( InlineKeyboardButton(text=label, callback_data=f"utask_{tid}"), InlineKeyboardButton(text="🔄", callback_data=f"urestart_{tid}"), InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"), ) builder.row(InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task")) builder.row(InlineKeyboardButton(text="🔙 Мои задачи", callback_data="my_tasks")) async def _show_user_visits(self, message: Message, user_id: int, edit: bool = False): all_tasks = await self.task_manager.get_all_tasks() visits = { tid: p for tid, p in all_tasks.items() if p.user_id == user_id and p.task_type == "user_visit" } if not visits: text = "🔗 Посещения\n\nЗадач нет." else: text = "🔗 Посещения\n\n" for _, p in list(visits.items())[:8]: text += ( f"{p.get_status_emoji()} {p.url[:40]}\n" f" ✅ {p.successful_visits}/{p.max_visits} | 📊 {p.total_visits}\n" ) builder = InlineKeyboardBuilder() self._build_task_buttons(builder, visits) await self._edit_or_send(message, text, builder.as_markup(), edit) async def _show_user_monitors(self, message: Message, user_id: int, edit: bool = False): all_tasks = await self.task_manager.get_all_tasks() monitors = { tid: p for tid, p in all_tasks.items() if p.user_id == user_id and p.task_type != "user_visit" } if not monitors: text = "📺 Мониторинг\n\nЗадач нет." else: text = "📺 Мониторинг\n\n" for _, p in list(monitors.items())[:8]: 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() self._build_task_buttons(builder, monitors) 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"] == "url": if not (text.startswith("http://") or text.startswith("https://")): await self._send_temp(message, "Ссылка должна начинаться с http:// или https://") return 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": "count", "url": text} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) sent = await message.answer( f"🔗 {text[:50]}\n\nШаг 2/4 — введите количество переходов:", reply_markup=builder.as_markup() ) self._menu_msg[user_id] = sent.message_id elif state["step"] == "count": try: count = int(text) if count < 1: raise ValueError except ValueError: await self._send_temp(message, "Введите целое число больше 0") return balance = await self.balance_storage.get_balance(user_id) if balance <= 0: await self._send_temp(message, "Недостаточно переходов") return if count > balance: await self._send_temp(message, f"Недостаточно переходов: нужно {count}, доступно {balance}") return 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] = {**state, "step": "batch", "count": count} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) sent = await message.answer( f"✅ Переходов: {count}\n\n" "Шаг 3/4 — сколько кликов в одном цикле?\n\n" "Бот делает N кликов подряд, затем ждёт паузу и начинает следующий цикл.\n\n" "Диапазон `2-5` — каждый цикл будет случайной длины от 2 до 5.", reply_markup=builder.as_markup() ) self._menu_msg[user_id] = sent.message_id elif state["step"] == "batch": try: if "-" in text: mn, mx = parse_range(text, default_min=1) else: mn = mx = int(text) if mn < 1: raise ValueError except Exception: await self._send_temp(message, "Введите число или диапазон, например: `3` или `2-5`") return 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] = {**state, "step": "delay", "min_series": mn, "max_series": mx} builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) series_label = f"{mn}–{mx}" if mn != mx else str(mn) sent = await message.answer( f"✅ Цикл: {series_label} кликов\n\n" "Шаг 4/4 — таймаут между циклами (в минутах)\n\n" "`2-5` — случайно от 2 до 5 минут.", reply_markup=builder.as_markup() ) self._menu_msg[user_id] = sent.message_id elif state["step"] == "delay": try: if "-" in text: mn_d, mx_d = parse_range(text, default_min=1) else: mn_d = mx_d = int(text) if mn_d < 1: raise ValueError except Exception: await self._send_temp(message, "Введите число или диапазон минут, например: `3` или `2-5`") return del self._user_task_state[user_id] await self._clean_prev(message) url = state["url"] count = state["count"] mn = state["min_series"] mx = state["max_series"] series_label = f"{mn}–{mx}" if mn != mx else str(mn) delay_label = f"{mn_d}–{mx_d}" if mn_d != mx_d else str(mn_d) task_id = f"u{user_id}_{extract_domain(url)[:12]}_{datetime.now().strftime('%H%M%S')}" params = TaskParams( url=url, task_type="user_visit", max_visits=count, min_series=mn, max_series=mx, user_id=user_id, min_delay=mn_d * 60, max_delay=mx_d * 60, 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_url_visits(task_id, params, message.bot), task_type="user_visit", metadata={"type": "user_visit", "url": url, "chat_id": message.chat.id}, ) if success: if self.storage: await self.storage.save_task(task_id, params) await self._send_temp( message, f"✅ Задача создана\n🔗 {url[:50]}\n" f"🔢 Переходов: {count} | Цикл: {series_label} кликов\n" f"⏱ Пауза между циклами: {delay_label} мин", delay=6 ) await self._show_user_menu(message, user_id=user_id) else: await self._send_temp(message, "Ошибка запуска задачи") 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 # Новая задача — идём к шагу ввода процента зрителей 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": "percent", "channel": state["channel"], "domains": domains, } builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⏭️ Без процента", callback_data="skip_percent")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) sent = await message.answer( f"📺 Канал: {state['channel']}\n" f"🌐 Домены: {', '.join(domains)}\n\n" "Шаг 3/3 — введите % от зрителей для расчёта переходов на ссылку:\n" "(например: 5 = 5% от текущих зрителей)\n" "Или нажмите «Без процента» для фиксированного значения", reply_markup=builder.as_markup() ) self._menu_msg[user_id] = sent.message_id elif state["step"] == "percent": try: percent = float(text.replace(",", ".")) if percent <= 0 or percent > 100: raise ValueError except ValueError: await self._send_temp(message, "Введите число от 0.1 до 100") return await self._clean_prev(message) del self._user_task_state[user_id] await self._create_twitch_task(message, user_id, state["channel"], state["domains"], percent) return # ========================================================================= # ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN) # ========================================================================= # ========================================================================= # КАБИНЕТ / МАГАЗИН # ========================================================================= async def _process_payment_input(self, message: Message): user_id = message.from_user.id action = self._payment_state.pop(user_id) await self._safe_delete(message.bot, message.chat.id, message.message_id) text = message.text.strip() if message.text else "" prompt_id = self._menu_msg.pop(user_id, None) async def _edit_prompt(new_text: str, markup=None): if prompt_id: try: await message.bot.edit_message_text( chat_id=message.chat.id, message_id=prompt_id, text=new_text, reply_markup=markup ) return except Exception: pass await message.answer(new_text, reply_markup=markup) try: value = int(text) except (ValueError, TypeError): await self._send_temp(message, "❌ Введите целое число") self._payment_state[user_id] = action if prompt_id: self._menu_msg[user_id] = prompt_id return if action == "topup": if value < settings.TOPUP_MIN_RUB: await self._send_temp(message, f"❌ Минимум {settings.TOPUP_MIN_RUB} ₽") self._payment_state[user_id] = "topup" if prompt_id: self._menu_msg[user_id] = prompt_id return if value > settings.TOPUP_MAX_RUB: await self._send_temp(message, f"❌ Максимум {settings.TOPUP_MAX_RUB} ₽") self._payment_state[user_id] = "topup" if prompt_id: self._menu_msg[user_id] = prompt_id return order_id = f"{user_id}_topup_{int(datetime.now().timestamp())}" invoice = await self.heleket.create_invoice(value, order_id) if not invoice: await self._send_temp(message, "❌ Ошибка создания счёта. Попробуйте позже.") return payment_data = { "user_id": user_id, "payment_id": invoice["payment_id"], "rub": value, "address": invoice.get("address"), "url": invoice.get("url"), "mock": invoice.get("mock", False), "created_at": datetime.now().isoformat(), } await self.payment_storage.save(invoice["payment_id"], payment_data) builder = InlineKeyboardBuilder() if invoice.get("url"): builder.row(InlineKeyboardButton(text="💳 Оплатить через Heleket", url=invoice["url"])) builder.row(InlineKeyboardButton(text="✅ Подтвердить", callback_data="check_pay")) builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay")) if payment_data["mock"]: body = "Нажмите «Подтвердить» для зачисления средств" elif invoice.get("url"): body = "Нажмите кнопку ниже для оплаты" else: body = f"Адрес:\n`{invoice['address']}`" await _edit_prompt( f"💳 Счёт на оплату\n\n" f"💰 Сумма: {value} ₽\n\n" f"{body}\n\n" f"⏱ Счёт действителен 60 минут", builder.as_markup() ) elif action == "buy_clicks": if value < settings.CLICKS_MIN: await self._send_temp(message, f"Минимум {settings.CLICKS_MIN} переходов") self._payment_state[user_id] = "buy_clicks" if prompt_id: self._menu_msg[user_id] = prompt_id return cost = int(value * settings.CLICK_PRICE_RUB) # Сохраняем для шага подтверждения self._payment_confirm[user_id] = {"clicks": value, "rub": cost} builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="✅ Подтвердить", callback_data="confirm_buy"), InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_confirm_buy"), ) await _edit_prompt( f"🛒 Подтвердите покупку\n\n" f"🔢 {value} переходов\n" f"💰 Стоимость: {cost} ₽", builder.as_markup() ) async def _show_invoice(self, callback: CallbackQuery, payment: dict): """Отображает счёт на оплату.""" builder = InlineKeyboardBuilder() if payment.get("url"): builder.row(InlineKeyboardButton(text="💳 Оплатить через Heleket", url=payment["url"])) builder.row(InlineKeyboardButton(text="✅ Подтвердить", callback_data="check_pay")) builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay")) if payment.get("mock"): body = "Нажмите «Подтвердить» для зачисления средств" elif payment.get("url"): body = f"Нажмите кнопку ниже для оплаты через Heleket" else: body = f"Адрес для оплаты:\n`{payment['address']}`" await callback.message.edit_text( f"💳 Счёт на оплату\n\n" f"💰 Сумма: {payment['rub']} ₽\n\n" f"{body}\n\n" f"⏱ Счёт действителен 60 минут", reply_markup=builder.as_markup() ) # ========================================================================= # ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN) # ========================================================================= async def _create_twitch_task( self, message: Message, user_id: int, channel: str, domains: list, visits_percent: float ): """Создаёт twitch_irc задачу и запускает её.""" balance = await self.balance_storage.get_balance(user_id) if balance <= 0: await self._send_temp(message, "Недостаточно переходов") return # Проверяем статус стрима до создания задачи (None = ошибка запроса → считаем онлайн) viewers = await get_viewer_count(channel) is_offline = (viewers == 0) # None → не 0 → False → онлайн 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, visits_percent=visits_percent, min_ctr=visits_percent if visits_percent > 0 else 0.8, max_ctr=visits_percent if visits_percent > 0 else 1.0, 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, stream_offline=is_offline, ) 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) percent_info = f"\n📊 {visits_percent}% от зрителей" if visits_percent > 0 else "" status = f"⏸ Стрим оффлайн — задача на паузе" if is_offline else f"▶️ Онлайн: {viewers} зрителей" await self._send_temp( message, f"✅ Задача создана\n" f"📺 {channel} | 🌐 {', '.join(domains)}\n" f"{status}{percent_info}", delay=6 ) await self._show_user_menu(message, user_id=user_id) else: await self._send_temp(message, "Ошибка запуска задачи") 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())