diff --git a/handlers/commands.py b/handlers/commands.py index 852feba..6ea050e 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -483,9 +483,14 @@ class BotInterface: if not await require_admin(callback): return tid = callback.data.replace("tdelete_", "", 1) + params = await interface.task_manager.get_task(tid) + is_visit = params and params.task_type in ("visit", "user_visit") await interface.task_manager.remove_task(tid) await callback.answer("🗑️ Удалена") - await interface._show_streamers_list(callback.message, edit=True) + if is_visit: + await interface._show_tasks_list(callback.message, edit=True) + else: + await interface._show_streamers_list(callback.message, edit=True) @dp.callback_query(F.data.startswith("treset_")) @@ -1690,9 +1695,67 @@ class BotInterface: 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() + """Все задачи — посещения и мониторинг.""" + active = await self.task_manager.get_active_tasks() + completed = await self.task_manager.get_completed_tasks() + + visit_active = {tid: p for tid, p in active.items() if p.task_type in ("visit", "user_visit")} + visit_done = {tid: p for tid, p in completed.items() if p.task_type in ("visit", "user_visit")} + 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"} + builder = InlineKeyboardBuilder() + + if not any([visit_active, visit_done, twitch_active, twitch_done]): + text = "📊 Задачи\n\nЗадач нет." + else: + text = "📊 Задачи\n\n" + + if twitch_active: + text += "📺 Twitch — активные:\n" + for tid, p in list(twitch_active.items())[:10]: + em = p.get_status_emoji() + domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" + text += f"{em} {p.channel}\n 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n" + builder.row(InlineKeyboardButton( + text=f"{em} 📺 {p.channel} (🔗{p.links_found})", + callback_data=f"tdetail_{tid}" + )) + + if twitch_done: + text += "\n📺 Twitch — завершённые:\n" + for tid, p in list(twitch_done.items())[:5]: + em = p.get_status_emoji() + text += f"{em} {p.channel}\n" + builder.row(InlineKeyboardButton( + text=f"{em} 📺 {p.channel} (завершена)", + callback_data=f"tdetail_{tid}" + )) + + if visit_active: + text += "\n🔗 Посещения — активные:\n" + for tid, p in list(visit_active.items())[:10]: + em = p.get_status_emoji() + url_s = (p.url or "")[:35] + uid_tag = f" | uid:{p.user_id}" if p.user_id else "" + text += f"{em} {url_s}{uid_tag}\n ✅{p.successful_visits}/{p.max_visits} | 📊{p.total_visits}\n" + builder.row(InlineKeyboardButton( + text=f"{em} 🔗 {url_s[:28]} ({p.successful_visits}/{p.max_visits})", + callback_data=f"tdetail_{tid}" + )) + + if visit_done: + text += "\n🔗 Посещения — завершённые:\n" + for tid, p in list(visit_done.items())[:5]: + em = p.get_status_emoji() + url_s = (p.url or "")[:35] + uid_tag = f" | uid:{p.user_id}" if p.user_id else "" + text += f"{em} {url_s}{uid_tag}\n" + builder.row(InlineKeyboardButton( + text=f"{em} 🔗 {url_s[:28]} (завершена)", + callback_data=f"tdetail_{tid}" + )) + 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) @@ -1802,7 +1865,8 @@ class BotInterface: InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"tdelete_{task_id}") ) - builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data="menu_streamers")) + back_target = "menu_tasks" if params.task_type in ("visit", "user_visit") else "menu_streamers" + builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data=back_target)) try: await callback.message.edit_text(text, reply_markup=builder.as_markup()) @@ -2230,6 +2294,7 @@ class BotInterface: break # --- Серия кликов --- + loop = asyncio.get_event_loop() for click_num in range(series_size): if params.stopped or params.successful_visits >= max_v: break @@ -2240,6 +2305,11 @@ class BotInterface: await send_message_safe(bot, params.chat_id, "⚠️ Баланс исчерпан — задача остановлена") return + # Фиксируем старт тика до посещения + is_last = (click_num == series_size - 1) + click_delay = 0 if is_last else params.get_click_delay() + tick_start = loop.time() + try: visitor = self.browser_pool or self.browser_service result = await visitor.visit_page(params.url, params.get_reading_time()) @@ -2267,9 +2337,12 @@ class BotInterface: 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()) + # Задержка между кликами строго по таймеру: спим только остаток + if not is_last and params.successful_visits < max_v: + elapsed = loop.time() - tick_start + remaining_wait = int(click_delay - elapsed) + if remaining_wait > 0: + await _sleep_interruptible(remaining_wait) await self.task_manager.complete_task(task_id) remaining = await self.balance_storage.get_balance(params.user_id) diff --git a/managers/task_manager.py b/managers/task_manager.py index 458a03e..057507d 100644 --- a/managers/task_manager.py +++ b/managers/task_manager.py @@ -272,8 +272,12 @@ class TaskManager: return await self.get_tasks_by_type("twitch_irc") async def get_visit_tasks(self) -> Dict[str, TaskParams]: - """Только задачи посещения.""" - return await self.get_tasks_by_type("visit") + """Задачи посещения (admin и user).""" + async with self._lock: + return { + tid: p for tid, p in self._tasks.items() + if p.task_type in ("visit", "user_visit") + } async def get_stats(self) -> dict: """Общая статистика.""" @@ -281,7 +285,7 @@ class TaskManager: for p in self._tasks.values(): if p.task_type == "twitch_irc": twitch += 1 - elif p.task_type == "visit": + elif p.task_type in ("visit", "user_visit"): visit += 1 if p.paused: paused += 1 @@ -298,14 +302,20 @@ class TaskManager: def _get_task_info(self, task_id: str, params: TaskParams) -> str: """Информация о задаче для отображения.""" status = params.get_status_emoji() - + if params.task_type == "twitch_irc": + user_tag = f" | 👤uid:{params.user_id}" if params.user_id else "" return ( f"{status} 📺 **{params.channel}**\n" - f" `{task_id[:12]}...`\n" - f" 👤 @{params.target_username} | " - f"🔗{params.links_found} | " - f"📊{params.total_visits}\n" + f" `{task_id[:12]}...`{user_tag}\n" + f" 🔗{params.links_found} | 📊{params.total_visits}\n" + ) + elif params.task_type == "user_visit": + user_tag = f" | 👤uid:{params.user_id}" if params.user_id else "" + return ( + f"{status} 🔗 `{params.url[:40]}`\n" + f" `{task_id[:12]}...`{user_tag}\n" + f" ✅{params.successful_visits}/{params.max_visits} | 📊{params.total_visits}\n" ) else: return ( diff --git a/services/visit_service.py b/services/visit_service.py index 50dfea3..02e56e6 100644 --- a/services/visit_service.py +++ b/services/visit_service.py @@ -51,59 +51,68 @@ class VisitScheduler: async def run(self) -> dict: """ Run scheduled visits. - + Returns: Statistics dict """ total_reading_time = 0 - + loop = asyncio.get_event_loop() + try: while True: if self.max_visits and self.visit_count >= self.max_visits: logger.info(f"Visit limit reached: {self.max_visits}") break - - # Randomize parameters + + # Randomize parameters once per cycle current_reading = random.randint(self.min_reading, self.max_reading) current_delay = random.randint(self.min_delay, self.max_delay) - + self.visit_count += 1 - + + # Record start time so the next visit fires strictly on the timer + tick_start = loop.time() + logger.info( f"Visit {self.visit_count}/{'∞' if not self.max_visits else self.max_visits}: " - f"reading={current_reading}s" + f"reading={current_reading}s delay={current_delay}s" ) - + # Visit page result = await self.browser_service.visit_page( self.url, current_reading ) - + if result.success: self.successful += 1 total_reading_time += current_reading else: self.failed += 1 - + # Notify about visit completion if self.on_visit_complete: await self.on_visit_complete( self.visit_count, result, current_delay, current_reading ) - + if self.max_visits and self.visit_count >= self.max_visits: break - - # Wait before next visit + + # Wait only the remaining time so that the period between visit + # starts is exactly current_delay (independent of visit duration) + elapsed = loop.time() - tick_start + remaining = current_delay - elapsed + if self.on_progress: await self.on_progress(self.visit_count, current_delay) - - await asyncio.sleep(current_delay) - + + if remaining > 0: + await asyncio.sleep(remaining) + except asyncio.CancelledError: logger.info("Visit scheduler cancelled") raise - + return { "total_visits": self.visit_count, "successful": self.successful,