This commit is contained in:
Yuriy Yuriev
2026-05-18 21:19:05 +07:00
parent 88bd8bfefa
commit 0102461e78
4 changed files with 173 additions and 75 deletions
+84 -26
View File
@@ -436,13 +436,12 @@ class BotInterface:
"delay": ( "delay": (
"⏱️ *Пауза между переходами*\n" "⏱️ *Пауза между переходами*\n"
"Сколько секунд ждать перед следующим кликом.\n" "Сколько секунд ждать перед следующим кликом.\n"
"Диапазон `30-60` — каждый раз рандом от 30 до 60.\n" "Диапазон `30-60` — каждый раз рандом от 30 до 60."
"Одно число `45` — всегда фиксированно."
), ),
"reading": ( "reading": (
"📖 *Время на странице*\n" "📖 *Время на странице*\n"
"Сколько секунд браузер будет листать страницу (имитация чтения).\n" "Сколько секунд браузер будет листать страницу (имитация чтения).\n"
"Диапазон `45-90` или фиксированно `60`." "Диапазон `45-90`."
), ),
"perlink": ( "perlink": (
"🔗 *Резервное кол-во кликов*\n" "🔗 *Резервное кол-во кликов*\n"
@@ -454,8 +453,7 @@ class BotInterface:
"📊 *CTR — процент от зрителей*\n" "📊 *CTR — процент от зрителей*\n"
"Формула: клики = зрители × CTR%.\n" "Формула: клики = зрители × CTR%.\n"
"Например: 120 зрителей × 1% = 1 клик.\n" "Например: 120 зрителей × 1% = 1 клик.\n"
"Диапазон `0.8-1` — случайно в этом диапазоне.\n" "Диапазон `0.8-1` — случайно в этом диапазоне."
"Одно число `1` — фиксированно."
), ),
"minutes": ( "minutes": (
"⏰ *Длительность мониторинга*\n" "⏰ *Длительность мониторинга*\n"
@@ -817,7 +815,7 @@ class BotInterface:
f"🔢 *Кликов в одном цикле*\n" f"🔢 *Кликов в одном цикле*\n"
f"Сколько кликов подряд делать перед паузой.\n" f"Сколько кликов подряд делать перед паузой.\n"
f"Текущее: {cur}\n" f"Текущее: {cur}\n"
f"Диапазон `2-5` или фиксированно `3`." f"Диапазон `2-5`."
) )
_d_mn = params.min_delay // 60 _d_mn = params.min_delay // 60
_d_mx = params.max_delay // 60 _d_mx = params.max_delay // 60
@@ -826,7 +824,7 @@ class BotInterface:
f"⏱️ *Пауза между циклами*\n" f"⏱️ *Пауза между циклами*\n"
f"Сколько минут ждать после каждого цикла кликов.\n" f"Сколько минут ждать после каждого цикла кликов.\n"
f"Текущая: {_d_cur} мин\n" f"Текущая: {_d_cur} мин\n"
f"Диапазон `2-5` или фиксированно `3`." f"Диапазон `2-5`."
) )
else: else:
perlink_label = ( perlink_label = (
@@ -840,7 +838,7 @@ class BotInterface:
f"⏱️ *Пауза между переходами*\n" f"⏱️ *Пауза между переходами*\n"
f"Сколько секунд ждать перед следующим кликом.\n" f"Сколько секунд ждать перед следующим кликом.\n"
f"Текущая: {params.min_delay}{params.max_delay} сек\n" f"Текущая: {params.min_delay}{params.max_delay} сек\n"
f"Диапазон `30-60` или фиксированно `45`." f"Диапазон `30-60`."
) )
prompts = { prompts = {
"delay": delay_label, "delay": delay_label,
@@ -848,13 +846,13 @@ class BotInterface:
f"📖 *Время на странице*\n" f"📖 *Время на странице*\n"
f"Сколько секунд браузер листает страницу (имитация чтения).\n" f"Сколько секунд браузер листает страницу (имитация чтения).\n"
f"Текущее: {params.min_reading}{params.max_reading} сек\n" f"Текущее: {params.min_reading}{params.max_reading} сек\n"
f"Диапазон `45-90` или фиксированно `60`." f"Диапазон `45-90`."
), ),
"percent": ( "percent": (
f"👥 *Кликов = % от зрителей*\n" f"👥 *Кликов = % от зрителей*\n"
f"Количество кликов рассчитывается как % от числа зрителей стрима.\n" f"Количество кликов рассчитывается как % от числа зрителей стрима.\n"
f"Текущий: {params.visits_percent}%\n" f"Текущий: {params.visits_percent}%\n"
f"`0` — отключить, использовать фиксированное кол-во.\n" f"`0` — отключить.\n"
f"Пример: `5` = 5 кликов на 100 зрителей." f"Пример: `5` = 5 кликов на 100 зрителей."
), ),
"perlink": perlink_label, "perlink": perlink_label,
@@ -862,13 +860,13 @@ class BotInterface:
f"⏳ *Задержка между кликами в цикле*\n" f"⏳ *Задержка между кликами в цикле*\n"
f"Сколько секунд ждать между каждым кликом внутри одного цикла.\n" f"Сколько секунд ждать между каждым кликом внутри одного цикла.\n"
f"Текущая: {params.min_click_delay}{params.max_click_delay} сек\n" f"Текущая: {params.min_click_delay}{params.max_click_delay} сек\n"
f"Диапазон `5-15` или фиксированно `10`." f"Диапазон `5-15`."
), ),
"ctr": ( "ctr": (
f"📊 *CTR — процент от зрителей*\n" f"📊 *CTR — процент от зрителей*\n"
f"Формула: клики = зрители × CTR%.\n" f"Формула: клики = зрители × CTR%.\n"
f"Текущий: {params.min_ctr}%{params.max_ctr}%\n" f"Текущий: {params.min_ctr}%{params.max_ctr}%\n"
f"Диапазон `0.8-1` или фиксированно `1`." f"Диапазон `0.8-1`."
), ),
} }
prompt = prompts.get(param, f"Введите значение для {param}:") prompt = prompts.get(param, f"Введите значение для {param}:")
@@ -1089,6 +1087,59 @@ class BotInterface:
await callback.answer("🗑️ Задача удалена") await callback.answer("🗑️ Задача удалена")
await interface._show_user_tasks(callback.message, uid, edit=True) 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
msg_mock = SimpleNamespace(
chat=SimpleNamespace(id=params.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": params.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_")) @dp.callback_query(F.data.startswith("upause_"))
async def cb_user_pause_task(callback: CallbackQuery): async def cb_user_pause_task(callback: CallbackQuery):
tid = callback.data.replace("upause_", "", 1) tid = callback.data.replace("upause_", "", 1)
@@ -2022,12 +2073,22 @@ class BotInterface:
def is_active(): def is_active():
return not params.paused and not params.stopped and not params.stream_offline 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}") logger.info(f"🚀 IRC monitor: {params.channel}")
watcher = asyncio.create_task(self._stream_watcher(task_id, params, message)) watcher = asyncio.create_task(self._stream_watcher(task_id, params, message))
try: try:
await irc.listen_for_messages(on_url, params.monitor_duration, params.allowed_domains, is_active=is_active) 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: if params.monitor_minutes > 0:
await self.task_manager.complete_task(task_id) await self.task_manager.complete_task(task_id)
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -2044,7 +2105,6 @@ class BotInterface:
async def _run_url_visits(self, task_id: str, params: TaskParams, bot): async def _run_url_visits(self, task_id: str, params: TaskParams, bot):
"""Посещает URL циклами: sleep(межцикловая) → N кликов(+sleep между ними) → повтор.""" """Посещает URL циклами: sleep(межцикловая) → N кликов(+sleep между ними) → повтор."""
total_done = 0
max_v = params.max_visits or 1 max_v = params.max_visits or 1
async def _sleep_interruptible(seconds: int): async def _sleep_interruptible(seconds: int):
@@ -2056,7 +2116,7 @@ class BotInterface:
await asyncio.sleep(1) await asyncio.sleep(1)
try: try:
while total_done < max_v and not params.stopped: while params.successful_visits < max_v and not params.stopped:
# --- пауза перед циклом --- # --- пауза перед циклом ---
await _sleep_interruptible(params.get_delay()) await _sleep_interruptible(params.get_delay())
if params.stopped: if params.stopped:
@@ -2067,7 +2127,7 @@ class BotInterface:
# --- цикл кликов --- # --- цикл кликов ---
for click_num in range(series_size): for click_num in range(series_size):
if params.stopped or total_done >= max_v: if params.stopped or params.successful_visits >= max_v:
break break
while params.paused and not params.stopped: while params.paused and not params.stopped:
@@ -2085,7 +2145,6 @@ class BotInterface:
try: try:
result = await self.browser_service.visit_page(params.url, reading) result = await self.browser_service.visit_page(params.url, reading)
params.total_visits += 1 params.total_visits += 1
total_done += 1
if result.success: if result.success:
params.successful_visits += 1 params.successful_visits += 1
await self.balance_storage.deduct(params.user_id, 1) await self.balance_storage.deduct(params.user_id, 1)
@@ -2093,10 +2152,10 @@ class BotInterface:
raise raise
except Exception as e: except Exception as e:
logger.error(f"URL visit error: {e}") logger.error(f"URL visit error: {e}")
total_done += 1 params.total_visits += 1
# задержка между кликами внутри цикла (кроме последнего) # задержка между кликами внутри цикла (кроме последнего)
if click_num < series_size - 1 and not params.stopped and total_done < max_v: if click_num < series_size - 1 and not params.stopped and params.successful_visits < max_v:
await _sleep_interruptible(params.get_click_delay()) await _sleep_interruptible(params.get_click_delay())
await self.task_manager.complete_task(task_id) await self.task_manager.complete_task(task_id)
@@ -2290,10 +2349,11 @@ class BotInterface:
InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"), InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"),
) )
else: else:
builder.row(InlineKeyboardButton( builder.row(
text=f"🗑️ {label} (завершена)", InlineKeyboardButton(text=label, callback_data=f"utask_{tid}"),
callback_data=f"ustop_{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="new_task"))
builder.row(InlineKeyboardButton(text="🔙 Мои задачи", callback_data="my_tasks")) builder.row(InlineKeyboardButton(text="🔙 Мои задачи", callback_data="my_tasks"))
@@ -2417,8 +2477,7 @@ class BotInterface:
f"✅ Переходов: {count}\n\n" f"✅ Переходов: {count}\n\n"
"Шаг 3/4 — сколько кликов в одном цикле?\n\n" "Шаг 3/4 — сколько кликов в одном цикле?\n\n"
"Бот делает N кликов подряд, затем ждёт паузу и начинает следующий цикл.\n\n" "Бот делает N кликов подряд, затем ждёт паузу и начинает следующий цикл.\n\n"
"Диапазон `2-5` — каждый цикл будет случайной длины от 2 до 5.\n" "Диапазон `2-5` — каждый цикл будет случайной длины от 2 до 5.",
"Одно число `3` — всегда ровно 3 клика.",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
self._menu_msg[user_id] = sent.message_id self._menu_msg[user_id] = sent.message_id
@@ -2444,7 +2503,6 @@ class BotInterface:
sent = await message.answer( sent = await message.answer(
f"✅ Цикл: {series_label} кликов\n\n" f"✅ Цикл: {series_label} кликов\n\n"
"Шаг 4/4 — таймаут между циклами (в минутах)\n\n" "Шаг 4/4 — таймаут между циклами (в минутах)\n\n"
"`3` — ровно 3 минуты.\n"
"`2-5` — случайно от 2 до 5 минут.", "`2-5` — случайно от 2 до 5 минут.",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
+4 -7
View File
@@ -97,16 +97,13 @@ class BotApplication:
notify_users: dict = {} # user_id -> chat_id notify_users: dict = {} # user_id -> chat_id
for task_id, params in saved_tasks.items(): for task_id, params in saved_tasks.items():
if params.stopped: # Завершённые и остановленные — загружаем в память но не запускаем
if params.completed or params.stopped:
await self.interface.task_manager.add_task(task_id, params)
continue continue
if params.completed and params.monitor_minutes > 0:
continue
if params.completed:
params.completed = False
# Все восстановленные задачи ставим на паузу # Активные задачи ставим на паузу и перезапускаем
params.paused = True params.paused = True
await self.interface.task_manager.add_task(task_id, params) await self.interface.task_manager.add_task(task_id, params)
if params.task_type == "twitch_irc" and params.chat_id: if params.task_type == "twitch_irc" and params.chat_id:
+33 -11
View File
@@ -135,27 +135,41 @@ class BrowserService:
) -> VisitResult: ) -> VisitResult:
"""Выполняет просмотр страницы.""" """Выполняет просмотр страницы."""
page = await browser.new_page() page = await browser.new_page()
initial_url = url
final_url = url
redirect = False
reading_completed = False
try: try:
await page.set_viewport_size({ await page.set_viewport_size({
"width": settings.VIEWPORT_WIDTH, "width": settings.VIEWPORT_WIDTH,
"height": settings.VIEWPORT_HEIGHT "height": settings.VIEWPORT_HEIGHT
}) })
goto_task = asyncio.create_task( goto_task = asyncio.create_task(
page.goto(url, wait_until="commit", timeout=30000) page.goto(url, wait_until="commit", timeout=30000)
) )
await self._move_mouse(page, goto_task) await self._move_mouse(page, goto_task)
await goto_task await goto_task
initial_url = page.url initial_url = page.url
final_url, redirect = await self._wait_redirect(page, initial_url)
try:
final_url, redirect = await self._wait_redirect(page, initial_url)
except Exception as e:
logger.warning(f"Redirect wait error (ignored): {e}")
final_url = page.url
await self._simulate_reading(page, reading_time) await self._simulate_reading(page, reading_time)
reading_completed = True
screenshot_path = None screenshot_path = None
if settings.SCREENSHOTS_DIR: if settings.SCREENSHOTS_DIR:
screenshot_path = await self._take_screenshot(page, final_url) try:
screenshot_path = await self._take_screenshot(page, final_url)
except Exception:
pass
return VisitResult( return VisitResult(
url=url, url=url,
initial_url=initial_url, initial_url=initial_url,
@@ -166,12 +180,20 @@ class BrowserService:
screenshot_path=screenshot_path, screenshot_path=screenshot_path,
success=True success=True
) )
except asyncio.CancelledError:
raise
except Exception as e: except Exception as e:
return VisitResult(url=url, reading_time=reading_time, return VisitResult(
success=False, error=str(e)) url=url,
initial_url=initial_url,
final_url=final_url,
redirect_occurred=redirect,
proxy_info=proxy_info,
reading_time=reading_time,
success=reading_completed,
error=str(e) if not reading_completed else None
)
finally: finally:
if page.url == url:
logger.error()
await page.close() await page.close()
async def _move_mouse(self, page, task): async def _move_mouse(self, page, task):
+52 -31
View File
@@ -165,17 +165,19 @@ class TwitchIRCClient:
return False return False
async def listen_for_messages( async def listen_for_messages(
self, self,
on_url_found: Callable[[str, str], Awaitable[None]], on_url_found: Callable[[str, str], Awaitable[None]],
duration: int, duration: int,
allowed_domains: Optional[List[str]] = None, allowed_domains: Optional[List[str]] = None,
is_active: Callable[[], bool] = None, # Функция проверки активности is_active: Callable[[], bool] = None,
) -> dict: on_drain_fail: Callable[[], Awaitable[None]] = None,
) -> dict:
stats = {"links_found": 0, "messages": 0, "reconnects": 0} stats = {"links_found": 0, "messages": 0, "reconnects": 0}
start_time = datetime.now() start_time = datetime.now()
last_message_time = datetime.now() last_message_time = datetime.now()
drain_errors = 0
if not await self.connect(): if not await self.connect():
raise ConnectionError(f"IRC connect failed for #{self.channel}") raise ConnectionError(f"IRC connect failed for #{self.channel}")
@@ -194,28 +196,47 @@ class TwitchIRCClient:
# === ПЕРЕПОДКЛЮЧЕНИЕ КАЖДЫЕ 30 СЕКУНД === # === ПЕРЕПОДКЛЮЧЕНИЕ КАЖДЫЕ 30 СЕКУНД ===
if (datetime.now() - last_message_time).seconds > 30: if (datetime.now() - last_message_time).seconds > 30:
logger.info("🔄 Reconnecting (30s)...") logger.info("🔄 Reconnecting (30s)...")
# Сначала читаем всё что осталось # Сначала читаем всё что осталось (с retry)
try: drain_ok = False
while True: for attempt in range(3):
line = await asyncio.wait_for(self._reader.readline(), timeout=1.0) try:
if not line: while True:
break line = await asyncio.wait_for(self._reader.readline(), timeout=1.0)
decoded = line.decode('utf-8', errors='ignore').strip() if not line:
if 'PRIVMSG' in decoded: break
msg = self._parse_message(decoded) decoded = line.decode('utf-8', errors='ignore').strip()
if msg: if 'PRIVMSG' in decoded:
stats["messages"] += 1 msg = self._parse_message(decoded)
for url in self._extract_urls(msg['message']): if msg:
if self._is_domain_allowed(url, allowed_domains): stats["messages"] += 1
stats["links_found"] += 1 for url in self._extract_urls(msg['message']):
try: if self._is_domain_allowed(url, allowed_domains):
await on_url_found(url, msg['display_name']) stats["links_found"] += 1
except Exception as e: try:
logger.warning(f"URL callback error: {e}") await on_url_found(url, msg['display_name'])
except Exception as e: except Exception as e:
logger.warning(f"Error draining IRC data: {e}") logger.warning(f"URL callback error: {e}")
drain_ok = True
break
except asyncio.TimeoutError:
drain_ok = True
break
except Exception as e:
logger.warning(f"Error draining IRC data (attempt {attempt + 1}/3): {e}")
await asyncio.sleep(2)
if not drain_ok:
drain_errors += 1
logger.error(f"IRC drain failed 3 times, total failures: {drain_errors}")
if drain_errors >= 3:
logger.error("IRC drain failed 3 consecutive times — pausing task")
if on_drain_fail:
await on_drain_fail()
return stats
else:
drain_errors = 0
# Переподключаемся # Переподключаемся
try: try:
await self.disconnect() await self.disconnect()
@@ -223,7 +244,7 @@ class TwitchIRCClient:
logger.warning(f"Disconnect error: {e}") logger.warning(f"Disconnect error: {e}")
await asyncio.sleep(1) await asyncio.sleep(1)
await self.connect() await self.connect()
stats["reconnects"] += 1 stats["reconnects"] += 1
last_message_time = datetime.now() last_message_time = datetime.now()
continue continue