From ca8a386f822b2afff14ef89b0b6ceed7e704a824 Mon Sep 17 00:00:00 2001 From: Yuriy Yuriev Date: Mon, 18 May 2026 18:35:09 +0700 Subject: [PATCH] fix command ana params --- .claude/settings.local.json | 9 + handlers/commands.py | 439 +++++++++++++++++++++++++++++------- managers/storage.py | 12 + managers/task_manager.py | 64 ++++-- requirements.txt | 1 + 5 files changed, 430 insertions(+), 95 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a6cac9a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(pip install *)", + "Bash(python -c \"import psutil; print\\('psutil ok, version:', psutil.__version__\\)\")", + "Bash(python -c ' *)" + ] + } +} diff --git a/handlers/commands.py b/handlers/commands.py index 3dc1b12..304ff24 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -4,6 +4,8 @@ import asyncio import logging +import os +import random import re from datetime import datetime from typing import Optional, Dict @@ -71,6 +73,7 @@ class BotInterface: 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: @@ -251,6 +254,31 @@ class BotInterface: await interface._show_status(callback.message, edit=True) await callback.answer() + # === Перезапуск === + @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") @@ -405,11 +433,42 @@ class BotInterface: } prompts = { - "delay": "⏱️ Задержка (сек): `10-30` или `15`", - "reading": "📖 Чтение (сек): `10-30` или `20`", - "perlink": "🔗 Посещений на ссылку: `5`", - "minutes": "⏰ Длительность (мин): `30`", - "domains": "🌐 Домены через запятую: `site1.com, site2.ru`\nИли `all` для всех", + "delay": ( + "⏱️ *Пауза между переходами*\n" + "Сколько секунд ждать перед следующим кликом.\n" + "Диапазон `30-60` — каждый раз рандом от 30 до 60.\n" + "Одно число `45` — всегда фиксированно." + ), + "reading": ( + "📖 *Время на странице*\n" + "Сколько секунд браузер будет листать страницу (имитация чтения).\n" + "Диапазон `45-90` или фиксированно `60`." + ), + "perlink": ( + "🔗 *Резервное кол-во кликов*\n" + "Используется только если Twitch GQL недоступен.\n" + "В норме: клики = зрители × CTR.\n" + "Пример: `5`" + ), + "ctr": ( + "📊 *CTR — процент от зрителей*\n" + "Формула: клики = зрители × CTR%.\n" + "Например: 120 зрителей × 1% = 1 клик.\n" + "Диапазон `0.8-1` — случайно в этом диапазоне.\n" + "Одно число `1` — фиксированно." + ), + "minutes": ( + "⏰ *Длительность мониторинга*\n" + "Как долго следить за чатом стримера (в минутах).\n" + "`0` — мониторить бесконечно.\n" + "Пример: `30`" + ), + "domains": ( + "🌐 *Разрешённые домены*\n" + "Кликать только по ссылкам с этих сайтов.\n" + "Несколько через запятую: `twitch.tv, youtube.com`\n" + "`all` — любые ссылки без фильтра." + ), } builder = InlineKeyboardBuilder() @@ -748,11 +807,66 @@ class BotInterface: 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` или фиксированно `3`." + ) + delay_label = ( + f"⏱️ *Пауза между сериями*\n" + f"Сколько секунд ждать после каждой серии кликов.\n" + f"Текущая: {params.min_delay}–{params.max_delay} сек\n" + f"Диапазон `30-60` или фиксированно `45`." + ) + 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` или фиксированно `45`." + ) prompts = { - "delay": f"Задержка (сек): например 30-60 или 45\nТекущая: {params.min_delay}-{params.max_delay}", - "reading": f"Время чтения (сек): например 45-90 или 60\nТекущее: {params.min_reading}-{params.max_reading}", - "percent": f"% зрителей (0 = отключить): например 5\nТекущее: {params.visits_percent}%", - "perlink": f"Кол-во переходов на ссылку: например 3\nТекущее: {params.visits_per_link}", + "delay": delay_label, + "reading": ( + f"📖 *Время на странице*\n" + f"Сколько секунд браузер листает страницу (имитация чтения).\n" + f"Текущее: {params.min_reading}–{params.max_reading} сек\n" + f"Диапазон `45-90` или фиксированно `60`." + ), + "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` или фиксированно `10`." + ), + "ctr": ( + f"📊 *CTR — процент от зрителей*\n" + f"Формула: клики = зрители × CTR%.\n" + f"Текущий: {params.min_ctr}%–{params.max_ctr}%\n" + f"Диапазон `0.8-1` или фиксированно `1`." + ), } prompt = prompts.get(param, f"Введите значение для {param}:") @@ -1187,7 +1301,7 @@ class BotInterface: params = TaskParams( task_type="twitch_irc", channel=channel, - target_username="*", # Все пользователи + target_username="*", visits_per_link=vl, monitor_minutes=mm, min_delay=mind, max_delay=maxd, @@ -1216,7 +1330,7 @@ class BotInterface: f"✅ Мониторинг запущен\n\n" f"📺 Канал: `{channel}`\n" f"👤 Отслеживание: все пользователи\n" - f"🔗 На ссылку: {vl} визитов\n" + f"🔗 На ссылку: {vl} кликов\n" f"⏱️ Мониторинг: {mm} мин\n" f"⏱️ Задержка: {format_range(mind, maxd)}с\n" f"📖 Чтение: {format_range(minr, maxr)}с\n" @@ -1454,22 +1568,65 @@ class BotInterface: 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"📺 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\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="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) # ========================================================================= # УПРАВЛЕНИЕ ЗАДАЧЕЙ @@ -1493,15 +1650,16 @@ class BotInterface: 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") + InlineKeyboardButton(text="⏱️ Пауза (сек)", callback_data=f"tset_{task_id}_delay"), + InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"tset_{task_id}_reading") ) builder.row( - InlineKeyboardButton(text="🔗 На ссылку", callback_data=f"tset_{task_id}_perlink"), - InlineKeyboardButton(text="⏰ Минуты", callback_data=f"tset_{task_id}_minutes") + 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"tset_{task_id}_domains"), InlineKeyboardButton(text="⏭️ Пропустить", callback_data=f"tskip_{task_id}") ) builder.row( @@ -1568,9 +1726,32 @@ class BotInterface: await message.answer(f"✅ Чтение: {format_range(mn, mx)}с") elif param == "perlink": - await self.task_manager.update_task(task_id, visits_per_link=int(value)) - await message.answer(f"✅ На ссылку: {value}") + 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) + await message.answer(f"✅ Серия: {lbl} кликов") + else: + await self.task_manager.update_task(task_id, visits_per_link=int(value)) + await message.answer(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}%" + await message.answer(f"✅ CTR: {lbl}") + elif param == "minutes": await self.task_manager.update_task(task_id, monitor_minutes=int(value)) await message.answer(f"✅ Мониторинг: {value} мин") @@ -1591,6 +1772,27 @@ class BotInterface: await self.task_manager.update_task(task_id, visits_percent=pct) await message.answer(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) + await message.answer(f"✅ Задержка между кликами: {format_range(mn, mx)}с") + + 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}%" + await message.answer(f"✅ CTR: {lbl}") + else: await message.answer(f"❌ Неизвестный параметр: {param}") @@ -1599,32 +1801,57 @@ class BotInterface: 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" - f"⏱ Задержка: {params.min_delay}-{params.max_delay} сек\n" - f"📖 Чтение: {params.min_reading}-{params.max_reading} сек" + 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}–{params.max_delay} сек\n" + f"📖 Время на странице: {params.min_reading}–{params.max_reading} сек" ) else: domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все" - pct = f"{params.visits_percent}% зрителей" if params.visits_percent > 0 else f"{params.visits_per_link} фикс." + pct = ( + f"{params.visits_percent}% от зрителей" + if params.visits_percent > 0 + else f"фиксированно {params.visits_per_link}" + ) + 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} | 🌐 {domains}\n" - f"🔗 {params.links_found} ссылок | ✅ {params.successful_visits} визитов\n" - f"⏱ Задержка: {params.min_delay}-{params.max_delay} сек\n" - f"📖 Чтение: {params.min_reading}-{params.max_reading} сек\n" - f"👥 Переходов: {pct}" + f"📺 Стример: {params.channel}\n" + f"🌐 Домены: {domains}\n\n" + f"🔗 Ссылок найдено: {params.links_found}\n" + f"✅ Успешных кликов: {params.successful_visits}\n\n" + f"🔗 Кликов на ссылку: {pct}\n" + f"📊 CTR: {ctr_str}\n" + f"⏱ Пауза между кликами: {params.min_delay}–{params.max_delay} сек\n" + f"📖 Время на странице: {params.min_reading}–{params.max_reading} сек" ) builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="⏱️ Задержка", callback_data=f"uset_{task_id}_delay"), - InlineKeyboardButton(text="📖 Чтение", callback_data=f"uset_{task_id}_reading"), + InlineKeyboardButton(text="⏱️ Пауза (сек)", callback_data=f"uset_{task_id}_delay"), + InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"), ) - if params.task_type == "twitch_irc": + if params.task_type == "user_visit": builder.row( - InlineKeyboardButton(text="👥 % зрителей", callback_data=f"uset_{task_id}_percent"), - InlineKeyboardButton(text="🔗 На ссылку", callback_data=f"uset_{task_id}_perlink"), + 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}_percent"), + InlineKeyboardButton(text="🔗 Резерв кликов", callback_data=f"uset_{task_id}_perlink"), + InlineKeyboardButton(text="📊 CTR %", callback_data=f"uset_{task_id}_ctr"), ) builder.row(InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}")) builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks")) @@ -1728,48 +1955,71 @@ class BotInterface: async def _run_url_visits(self, task_id: str, params: TaskParams, bot): - """Посещает URL фиксированное число раз с балансом.""" - try: - for i in range(params.max_visits or 1): + """Посещает URL сериями: sleep(межсерийная) → N кликов(+sleep между ними) → повтор.""" + total_done = 0 + max_v = params.max_visits or 1 + + async def _sleep_interruptible(seconds: int): + for _ in range(max(0, seconds)): if params.stopped: - break + return while params.paused and not params.stopped: await asyncio.sleep(2) + await asyncio.sleep(1) + + try: + while total_done < max_v and not params.stopped: + # --- пауза перед серией --- + await _sleep_interruptible(params.get_delay()) if params.stopped: 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, "Баланс исчерпан — задача остановлена") - break + # размер серии выбирается случайно каждый раз + series_size = params.get_series_size() - reading = params.get_reading_time() - try: - result = await self.browser_service.visit_page(params.url, reading) - params.total_visits += 1 - if result.success: - params.successful_visits += 1 - await self.balance_storage.deduct(params.user_id, 1) - except asyncio.CancelledError: - raise - except Exception as e: - logger.error(f"URL visit error: {e}") + # --- серия кликов --- + for click_num in range(series_size): + if params.stopped or total_done >= max_v: + break - if i < (params.max_visits or 1) - 1 and not params.stopped: - await asyncio.sleep(params.get_delay()) + while params.paused and not params.stopped: + await asyncio.sleep(2) + if params.stopped: + 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 + + reading = params.get_reading_time() + try: + result = await self.browser_service.visit_page(params.url, reading) + params.total_visits += 1 + total_done += 1 + if result.success: + params.successful_visits += 1 + await self.balance_storage.deduct(params.user_id, 1) + except asyncio.CancelledError: + raise + except Exception as e: + logger.error(f"URL visit error: {e}") + total_done += 1 + + # задержка между кликами внутри серии (кроме последнего) + if click_num < series_size - 1 and not params.stopped and total_done < 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) - msg = await send_message_safe( + 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}" ) - if msg and params.user_id: - await self._track_msg(params.user_id, msg.message_id) except asyncio.CancelledError: raise finally: @@ -1793,13 +2043,15 @@ class BotInterface: ) return - # Вычисляем количество переходов: по % зрителей или фиксировано - if params.visits_percent > 0: - viewers = await get_viewer_count(params.channel) - visits_count = max(1, int(viewers * params.visits_percent / 100)) - logger.info(f"Viewer-based visits: {viewers} × {params.visits_percent}% = {visits_count}") + # 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)) + logger.info(f"visits: {viewers} viewers × {ctr:.2f}% = {visits_count}") else: - visits_count = params.visits_per_link + visits_count = max(1, round(params.visits_per_link * ctr / 100)) + logger.info(f"visits: fallback {params.visits_per_link} × {ctr:.2f}% = {visits_count}") logger.info(f"🔗 @{username}: {url}") await send_message_safe( @@ -1989,7 +2241,7 @@ class BotInterface: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) sent = await message.answer( - f"🔗 {text[:50]}\n\nШаг 2/2 — введите количество переходов:", + f"🔗 {text[:50]}\n\nШаг 2/3 — введите количество переходов:", reply_markup=builder.as_markup() ) self._menu_msg[user_id] = sent.message_id @@ -2003,12 +2255,7 @@ class BotInterface: await self._send_temp(message, "Введите целое число больше 0") return - del self._user_task_state[user_id] - await self._clean_prev(message) - balance = await self.balance_storage.get_balance(user_id) - url = state["url"] - if balance <= 0: await self._send_temp(message, "Недостаточно переходов") return @@ -2016,11 +2263,47 @@ class BotInterface: 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/3 — сколько кликов в одной серии?\n\n" + "Бот делает N кликов подряд, затем ждёт паузу и начинает следующую серию.\n\n" + "Диапазон `2-5` — каждая серия будет случайной длины от 2 до 5.\n" + "Одно число `3` — всегда ровно 3 клика.", + 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 + + del self._user_task_state[user_id] + await self._clean_prev(message) + + url = state["url"] + count = state["count"] + series_label = f"{mn}–{mx}" if mn != mx else str(mn) + 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=settings.DEFAULT_MIN_DELAY, max_delay=settings.DEFAULT_MAX_DELAY, @@ -2043,7 +2326,9 @@ class BotInterface: await self.storage.save_task(task_id, params) await self._send_temp( message, - f"✅ Задача создана\n🔗 {url[:50]}\n🔢 Переходов: {count}", + f"✅ Задача создана\n🔗 {url[:50]}\n" + f"🔢 Переходов: {count} | Серия: {series_label} кликов\n" + f"⏱ Пауза между сериями: {settings.DEFAULT_MIN_DELAY}–{settings.DEFAULT_MAX_DELAY} сек", delay=6 ) await self._show_user_menu(message, user_id=user_id) diff --git a/managers/storage.py b/managers/storage.py index 6bfa2ac..d0ff51e 100644 --- a/managers/storage.py +++ b/managers/storage.py @@ -46,6 +46,12 @@ class TaskStorage: "chat_id": params.chat_id, "user_id": params.user_id, "visits_percent": params.visits_percent, + "min_click_delay": params.min_click_delay, + "max_click_delay": params.max_click_delay, + "min_series": params.min_series, + "max_series": params.max_series, + "min_ctr": params.min_ctr, + "max_ctr": params.max_ctr, "started_at": params.started_at.isoformat() if params.started_at else None, } @@ -73,6 +79,12 @@ class TaskStorage: chat_id=data.get("chat_id"), user_id=data.get("user_id"), visits_percent=data.get("visits_percent", 0.0), + min_click_delay=data.get("min_click_delay", 5), + max_click_delay=data.get("max_click_delay", 15), + min_series=data.get("min_series", data.get("visits_per_link", 1)), + max_series=data.get("max_series", data.get("visits_per_link", 1)), + min_ctr=data.get("min_ctr", 0.8), + max_ctr=data.get("max_ctr", 1.0), ) if data.get("started_at"): try: diff --git a/managers/task_manager.py b/managers/task_manager.py index 79ed4cf..96642c2 100644 --- a/managers/task_manager.py +++ b/managers/task_manager.py @@ -24,6 +24,8 @@ class TaskParams: max_delay: int = 30 min_reading: int = 5 max_reading: int = 15 + min_click_delay: int = 5 # задержка между кликами внутри серии + max_click_delay: int = 15 # Лимиты max_visits: Optional[int] = None @@ -37,6 +39,10 @@ class TaskParams: allowed_domains: Optional[List[str]] = None visits_percent: float = 0.0 # % от зрителей (0 = использовать visits_per_link) + min_series: int = 1 # для user_visit: мин. кликов в серии + max_series: int = 3 # для user_visit: макс. кликов в серии + min_ctr: float = 0.8 # для twitch_irc: мин. коэф. эффективности клика + max_ctr: float = 1.0 # для twitch_irc: макс. коэф. эффективности клика # Состояние paused: bool = False @@ -82,6 +88,12 @@ class TaskParams: self.force_reading = None return reading return random.randint(self.min_reading, self.max_reading) + + def get_click_delay(self) -> int: + return random.randint(self.min_click_delay, self.max_click_delay) + + def get_series_size(self) -> int: + return random.randint(self.min_series, self.max_series) def get_status_emoji(self) -> str: if self.stopped: @@ -109,34 +121,50 @@ class TaskParams: duration = "" if self.started_at and (self.completed or self.stopped): delta = datetime.now() - self.started_at - duration = f"├ ⏱️ Длилась: {delta.seconds // 60} мин\n" - - # Домены - domains_text = "" - if self.allowed_domains: - domains_text = f"├ 🌐 Домены: {', '.join(self.allowed_domains)}\n" - else: - domains_text = "├ 🌐 Домены: все\n" - + duration = f"├ 🕐 Длилась: {delta.seconds // 60} мин\n" + + domains_text = ( + f"├ 🌐 Домены: {', '.join(self.allowed_domains)}\n" + if self.allowed_domains else + "├ 🌐 Домены: все (без фильтра)\n" + ) + ctr_str = ( + f"{self.min_ctr}%–{self.max_ctr}%" + if self.min_ctr != self.max_ctr + else f"{self.min_ctr}%" + ) + ctr_line = f"├ 📊 CTR: {ctr_str} (зрители × CTR% = кликов)" + monitor = ( + f"{self.monitor_minutes} мин" if self.monitor_minutes > 0 else "бесконечно" + ) + return ( f"📺 **{self.channel}**\n" f"├ Статус: {status}\n" - f"├ 🔗 Ссылок найдено: {self.links_found}\n" - f"├ 📊 Всего визитов: {self.total_visits}\n" - f"├ ✅ Успешно: {self.successful_visits}\n" - f"├ ⏱️ Задержка: {self.min_delay}-{self.max_delay}с\n" - f"├ 📖 Чтение: {self.min_reading}-{self.max_reading}с\n" - f"├ 🔗 На ссылку: {self.visits_per_link} визитов\n" - f"├ ⏰ Мониторинг: {self.monitor_minutes} мин\n" + f"├ 🔗 Ссылок найдено в чате: {self.links_found}\n" + f"├ 📊 Всего кликов: {self.total_visits}\n" + f"├ ✅ Успешных кликов: {self.successful_visits}\n" + f"{ctr_line}\n" + f"├ 🔗 Резерв (без GQL): {self.visits_per_link}\n" + f"├ ⏱️ Пауза между переходами: {self.min_delay}–{self.max_delay} сек\n" + f"├ 📖 Время на странице: {self.min_reading}–{self.max_reading} сек\n" + f"├ ⏰ Длительность мониторинга: {monitor}\n" f"{domains_text}" f"{duration}" ) else: + series_range = ( + f"{self.min_series}" if self.min_series == self.max_series + else f"{self.min_series}–{self.max_series}" + ) return ( f"🌐 `{self.url[:50]}`\n" f"├ Статус: {self.get_status_text()}\n" - f"├ 📊 Визитов: {self.total_visits}\n" - f"├ ✅ Успешно: {self.successful_visits}\n" + f"├ 📊 Выполнено: {self.successful_visits}/{self.max_visits} переходов\n" + f"├ 🔢 Кликов в серии: {series_range}\n" + f"├ ⏳ Задержка между кликами: {self.min_click_delay}–{self.max_click_delay} сек\n" + f"├ ⏱️ Пауза между сериями: {self.min_delay}–{self.max_delay} сек\n" + f"├ 📖 Время на странице: {self.min_reading}–{self.max_reading} сек\n" ) diff --git a/requirements.txt b/requirements.txt index 45fe4c5..cacb201 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ aiofiles>=24.1.0 playwright-captcha>=0.1.0 playwright>=1.49.0 camoufox[geoip]>=0.4.0 +psutil>=5.9.0