925 lines
41 KiB
Python
925 lines
41 KiB
Python
"""
|
||
Обработчики команд с удобным интерфейсом управления задачами.
|
||
"""
|
||
|
||
import asyncio
|
||
import logging
|
||
from datetime import datetime
|
||
from typing import Optional, Dict
|
||
|
||
from aiogram import Dispatcher, F, Bot
|
||
from aiogram.filters import Command
|
||
from aiogram.types import (
|
||
Message, CallbackQuery, InlineKeyboardMarkup,
|
||
InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton
|
||
)
|
||
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
|
||
|
||
from config.settings import settings
|
||
from managers.background_tasks import BackgroundTaskManager
|
||
from managers.proxy_manager import ProxyManager
|
||
from managers.task_manager import TaskManager, TaskParams
|
||
from services.browser_service import BrowserService
|
||
from services.visit_service import VisitScheduler
|
||
from utils.helpers import parse_range, format_range, extract_domain
|
||
from utils.telegram import send_message_safe, send_visit_result
|
||
from managers.storage import TaskStorage, ChatStorage
|
||
from auth.manager import AuthManager
|
||
from auth.storage import AuthStorage
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class BotInterface:
|
||
"""Интерфейс бота с управлением задачами."""
|
||
|
||
def __init__(
|
||
self,
|
||
background_tasks: BackgroundTaskManager,
|
||
proxy_manager: ProxyManager,
|
||
browser_service: BrowserService,
|
||
storage: TaskStorage = None,
|
||
chat_storage: ChatStorage = None,
|
||
):
|
||
self.background_tasks = background_tasks
|
||
self.proxy_manager = proxy_manager
|
||
self.browser_service = browser_service
|
||
self.task_manager = TaskManager()
|
||
self.storage = storage
|
||
self.chat_storage = chat_storage
|
||
self.auth_manager = AuthManager()
|
||
self.auth_storage = AuthStorage()
|
||
self._user_input_state: Dict[int, dict] = {}
|
||
|
||
def register(self, dp: Dispatcher):
|
||
"""Регистрация всех обработчиков."""
|
||
|
||
# Сохраняем ссылку на self для использования в обработчиках
|
||
interface = self
|
||
|
||
# --- АВТОРИЗАЦИЯ ---
|
||
|
||
@dp.message(Command("register"))
|
||
async def cmd_register(message: Message):
|
||
"""Регистрация: /register <пароль>"""
|
||
parts = message.text.split(maxsplit=1)
|
||
if len(parts) < 2 or len(parts[1]) < 4:
|
||
await message.answer(
|
||
"❌ Использование: /register <пароль от 4 символов>"
|
||
)
|
||
return
|
||
password = parts[1]
|
||
user_id = message.from_user.id
|
||
exists = await interface.auth_storage.user_exists(user_id)
|
||
if exists:
|
||
await message.answer(
|
||
"⚠️ Вы уже зарегистрированы. Используйте /login <пароль>"
|
||
)
|
||
return
|
||
password_hash, salt = interface.auth_manager._hash_password(password)
|
||
success = await interface.auth_storage.register(
|
||
user_id, password_hash, salt
|
||
)
|
||
if success:
|
||
interface.auth_manager.login(user_id)
|
||
await message.answer(
|
||
"✅ Регистрация прошла успешно!\n"
|
||
"Теперь вы можете использовать бота."
|
||
)
|
||
else:
|
||
await message.answer("❌ Ошибка регистрации")
|
||
|
||
@dp.message(Command("login"))
|
||
async def cmd_login(message: Message):
|
||
"""Вход: /login <пароль>"""
|
||
parts = message.text.split(maxsplit=1)
|
||
if len(parts) < 2:
|
||
await message.answer("❌ Использование: /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 message.answer(
|
||
"❌ Вы не зарегистрированы.\n"
|
||
"Используйте /register <пароль>"
|
||
)
|
||
return
|
||
locked = interface.auth_manager.is_locked_out(user_id)
|
||
if locked:
|
||
await message.answer("🔒 Слишком много попыток. Попробуйте позже.")
|
||
return
|
||
if interface.auth_manager.verify_password(
|
||
password, user_data["password_hash"], user_data["salt"]
|
||
):
|
||
interface.auth_manager.login(user_id)
|
||
await message.answer(
|
||
"✅ Вы вошли в систему!\n"
|
||
"Используйте /start для главного меню."
|
||
)
|
||
else:
|
||
interface.auth_manager.record_failed_attempt(user_id)
|
||
await message.answer("❌ Неверный пароль")
|
||
|
||
@dp.message(Command("logout"))
|
||
async def cmd_logout(message: Message):
|
||
"""Выйти из аккаунта."""
|
||
interface.auth_manager.logout(message.from_user.id)
|
||
await message.answer("🔑 Вы вышли из системы.")
|
||
|
||
# Декоратор: доступ только для авторизованных
|
||
async def require_auth(handler_fn, event):
|
||
user_id = event.from_user.id
|
||
if not interface.auth_manager.is_authenticated(user_id):
|
||
await event.answer(
|
||
"🔒 Требуется авторизация!\n\n"
|
||
"/register <пароль> — зарегистрироваться\n"
|
||
"/login <пароль> — войти"
|
||
)
|
||
return False
|
||
return True
|
||
|
||
# === Главное меню ===
|
||
@dp.message(Command("start"))
|
||
@dp.message(F.text == "📋 Главное меню")
|
||
async def cmd_start(message: Message):
|
||
if not await require_auth(message):
|
||
return
|
||
await interface._show_main_menu(message)
|
||
|
||
@dp.callback_query(F.data == "menu_main")
|
||
async def cb_main_menu(callback: CallbackQuery):
|
||
if not await require_auth(callback):
|
||
return
|
||
await interface._show_main_menu(callback.message)
|
||
await callback.answer()
|
||
|
||
# === Стримеры ===
|
||
@dp.message(F.text == "📺 Стримеры")
|
||
@dp.message(Command("streamers"))
|
||
async def btn_streamers(message: Message):
|
||
if not await require_auth(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_auth(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_auth(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_auth(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_auth(message):
|
||
return
|
||
await interface._show_status(message)
|
||
|
||
@dp.callback_query(F.data == "menu_status")
|
||
async def cb_status(callback: CallbackQuery):
|
||
if not await require_auth(callback):
|
||
return
|
||
await interface._show_status(callback.message, edit=True)
|
||
await callback.answer()
|
||
|
||
# === Стоп всё ===
|
||
@dp.message(F.text == "🛑 Остановить всё")
|
||
@dp.callback_query(F.data == "stop_all")
|
||
async def handle_stop_all(event):
|
||
if not await require_auth(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_auth(message):
|
||
return
|
||
await message.answer(
|
||
"🚀 **БЫСТРЫЙ СТАРТ**\n\n"
|
||
"**Twitch мониторинг:**\n"
|
||
"`<канал> <username> [посещений] [минут] [задержка] [чтение] [домены]`\n\n"
|
||
"**Пример:** `streamer user123 5 30 10-30 10-30`\n\n"
|
||
"**Посещение URL:**\n"
|
||
"`<url> [задержка] [повторов] [чтение]`\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_auth(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_auth(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_auth(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_auth(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_auth(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):
|
||
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_auth(callback):
|
||
return
|
||
"""Установка параметра задачи."""
|
||
data = callback.data
|
||
|
||
logger.info(f"SET callback: {data}")
|
||
|
||
# Формат: tset_TASKID_PARAM
|
||
# Убираем префикс
|
||
data = data.replace("tset_", "", 1)
|
||
|
||
# Ищем последнее подчеркивание для разделения task_id и param
|
||
idx = data.rfind("_")
|
||
if idx == -1:
|
||
await callback.answer("❌ Неверный формат")
|
||
return
|
||
|
||
task_id = data[:idx]
|
||
param = data[idx + 1:]
|
||
|
||
logger.info(f"Parsed: task_id={task_id[:20]}, param={param}")
|
||
|
||
# Сохраняем состояние ожидания ввода
|
||
interface._user_input_state[callback.from_user.id] = {
|
||
"task_id": task_id,
|
||
"param": param
|
||
}
|
||
|
||
prompts = {
|
||
"delay": "⏱️ Задержка (сек): `10-30` или `15`",
|
||
"reading": "📖 Чтение (сек): `10-30` или `20`",
|
||
"perlink": "🔗 Посещений на ссылку: `5`",
|
||
"minutes": "⏰ Длительность (мин): `30`",
|
||
"domains": "🌐 Домены через запятую: `site1.com, site2.ru`\nИли `all` для всех",
|
||
}
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"tdetail_{task_id}"))
|
||
|
||
prompt = prompts.get(param, f"Введите значение для {param}:")
|
||
await callback.message.answer(prompt, reply_markup=builder.as_markup())
|
||
await callback.answer(f"Ожидаю ввод: {param}")
|
||
|
||
# === Ввод параметров ===
|
||
@dp.message()
|
||
async def handle_all_text(message: Message):
|
||
"""Обрабатывает ВСЕ текстовые сообщения."""
|
||
text = message.text.strip() if message.text else ""
|
||
user_id = message.from_user.id
|
||
|
||
if not interface.auth_manager.is_authenticated(user_id):
|
||
await message.answer(
|
||
"🔒 Требуется авторизация!\n\n"
|
||
"/register <пароль> — зарегистрироваться\n"
|
||
"/login <пароль> — войти"
|
||
)
|
||
return
|
||
|
||
logger.info(f"Handle text: '{text[:50]}' from user {user_id}, waiting: {user_id in interface._user_input_state}")
|
||
|
||
# 1. Проверяем - ожидаем ввод параметра
|
||
if user_id in interface._user_input_state:
|
||
await interface._process_param_input(message)
|
||
return
|
||
|
||
# 2. Проверяем - это URL
|
||
if text.startswith('http://') or text.startswith('https://'):
|
||
parts = text.split()
|
||
await interface._add_visit(message)
|
||
return
|
||
|
||
# 3. Проверяем - это команда добавления стримера с параметрами
|
||
# Формат: channel [visits] [minutes] [delay] [reading] [domains]
|
||
parts = text.split()
|
||
if len(parts) >= 1 and not text.startswith('/'):
|
||
# Проверяем что первый параметр похож на канал (буквы/цифры/_)
|
||
import re
|
||
if re.match(r'^[a-zA-Z0-9_]+$', parts[0]):
|
||
await interface._add_streamer(message, parts)
|
||
return
|
||
|
||
# 4. Если ничего не подошло
|
||
logger.info(f"Unhandled message: {text[:50]}")
|
||
|
||
@dp.callback_query(F.data.startswith("tdelete_"))
|
||
async def tdelete(callback: CallbackQuery):
|
||
if not await require_auth(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_auth(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)
|
||
|
||
|
||
# =========================================================================
|
||
# ДОБАВЛЕНИЕ ЗАДАЧ
|
||
# =========================================================================
|
||
|
||
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 _show_main_menu(self, message: Message):
|
||
"""Главное меню."""
|
||
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="stop_all"))
|
||
|
||
reply = ReplyKeyboardBuilder()
|
||
reply.row(KeyboardButton(text="📺 Стримеры"), KeyboardButton(text="📊 Задачи"))
|
||
reply.row(KeyboardButton(text="📈 Статус"), KeyboardButton(text="🚀 Быстрый старт"))
|
||
reply.row(KeyboardButton(text="🛑 Остановить всё"), KeyboardButton(text="📋 Главное меню"))
|
||
|
||
await message.answer(text, reply_markup=reply.as_markup(resize_keyboard=True))
|
||
await message.answer("💡 Кнопки управления:", reply_markup=builder.as_markup())
|
||
|
||
async def _show_streamers_list(self, message: Message, edit: bool = False):
|
||
active = await self.task_manager.get_active_tasks()
|
||
completed = await self.task_manager.get_completed_tasks()
|
||
|
||
twitch_active = {tid: p for tid, p in active.items() if p.task_type == "twitch_irc"}
|
||
twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"}
|
||
|
||
if not twitch_active and not twitch_done:
|
||
text = "📺 **СТРИМЕРЫ**\n\nНет задач.\nОтправьте `<канал>` для добавления."
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||
else:
|
||
text = "📺 **СТРИМЕРЫ**\n\n"
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
# Активные
|
||
if twitch_active:
|
||
text += "**🔄 Активные:**\n"
|
||
for tid, p in list(twitch_active.items())[:10]:
|
||
emoji = p.get_status_emoji()
|
||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||
|
||
text += (
|
||
f"{emoji} **{p.channel}**\n"
|
||
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||
)
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"{emoji} {p.channel} (🔗{p.links_found})",
|
||
callback_data=f"tdetail_{tid}"
|
||
))
|
||
|
||
# Завершенные
|
||
if twitch_done:
|
||
text += "\n**📁 Завершенные:**\n"
|
||
for tid, p in list(twitch_done.items())[:5]:
|
||
emoji = p.get_status_emoji()
|
||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||
|
||
text += (
|
||
f"{emoji} **{p.channel}**\n"
|
||
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||
)
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"{emoji} {p.channel} (завершена)",
|
||
callback_data=f"tdetail_{tid}"
|
||
))
|
||
|
||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||
|
||
if edit:
|
||
try:
|
||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||
except Exception as e:
|
||
logger.debug(f"Edit message skipped: {e}")
|
||
else:
|
||
await message.answer(text, reply_markup=builder.as_markup())
|
||
|
||
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"))
|
||
|
||
if edit:
|
||
try:
|
||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||
except Exception as e:
|
||
logger.debug(f"Edit message skipped: {e}")
|
||
else:
|
||
await message.answer(text, reply_markup=builder.as_markup())
|
||
|
||
async def _show_status(self, message: Message, edit: bool = False):
|
||
"""Статус."""
|
||
stats = await self.task_manager.get_stats()
|
||
proxy_stats = self.proxy_manager.get_stats()
|
||
|
||
text = (
|
||
"📈 **СТАТУС**\n\n"
|
||
f"📺 Twitch: **{stats['twitch']}**\n"
|
||
f"🌐 Визиты: **{stats['visit']}**\n"
|
||
f"⏸️ Пауза: **{stats['paused']}**\n"
|
||
f"🔄 Активно: **{stats['active']}**\n\n"
|
||
f"🔌 Прокси: {proxy_stats['total']} всего, {proxy_stats['available']} доступно"
|
||
)
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_status"))
|
||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||
|
||
if edit:
|
||
try:
|
||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||
except Exception as e:
|
||
logger.debug(f"Edit message skipped: {e}")
|
||
else:
|
||
await message.answer(text, reply_markup=builder.as_markup())
|
||
|
||
# =========================================================================
|
||
# УПРАВЛЕНИЕ ЗАДАЧЕЙ
|
||
# =========================================================================
|
||
|
||
async def _show_task_detail(self, callback: CallbackQuery, task_id: str):
|
||
params = await self.task_manager.get_task(task_id)
|
||
if not params:
|
||
await callback.answer("❌ Задача не найдена")
|
||
return
|
||
|
||
text = f"📋 **УПРАВЛЕНИЕ**\n\n{params.get_stats_text()}"
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
# Для активных задач
|
||
if not params.completed and not params.stopped:
|
||
if params.paused:
|
||
builder.row(InlineKeyboardButton(text="▶️ Возобновить", callback_data=f"tresume_{task_id}"))
|
||
else:
|
||
builder.row(InlineKeyboardButton(text="⏸️ Пауза", callback_data=f"tpause_{task_id}"))
|
||
|
||
builder.row(
|
||
InlineKeyboardButton(text="⏱️ Задержка", callback_data=f"tset_{task_id}_delay"),
|
||
InlineKeyboardButton(text="📖 Чтение", callback_data=f"tset_{task_id}_reading")
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(text="🔗 На ссылку", callback_data=f"tset_{task_id}_perlink"),
|
||
InlineKeyboardButton(text="⏰ Минуты", callback_data=f"tset_{task_id}_minutes")
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(text="🌐 Домены", callback_data=f"tset_{task_id}_domains"),
|
||
InlineKeyboardButton(text="⏭️ Пропустить", callback_data=f"tskip_{task_id}")
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(text="🔄 Сброс", callback_data=f"treset_{task_id}"),
|
||
InlineKeyboardButton(text="🛑 Стоп", callback_data=f"tstop_{task_id}")
|
||
)
|
||
else:
|
||
# Для завершенных/остановленных
|
||
builder.row(
|
||
InlineKeyboardButton(text="🔄 Перезапустить", callback_data=f"trestart_task_{task_id}"),
|
||
)
|
||
builder.row(
|
||
InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"tdelete_{task_id}")
|
||
)
|
||
|
||
builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data="menu_streamers"))
|
||
|
||
try:
|
||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||
except Exception as e:
|
||
if "not modified" not in str(e):
|
||
logger.error(f"Edit error: {e}")
|
||
|
||
|
||
|
||
async def _process_param_input(self, message: Message):
|
||
"""Обработка ввода параметров."""
|
||
user_id = message.from_user.id
|
||
state = self._user_input_state.pop(user_id)
|
||
|
||
task_id = state["task_id"]
|
||
param = state["param"]
|
||
|
||
# Получаем текст и очищаем от кавычек
|
||
value = message.text.strip() if message.text else ""
|
||
value = value.replace("'", "").replace('"', "").replace("`", "").strip()
|
||
|
||
logger.info(f"Process param: {param} = {value}")
|
||
|
||
if not value:
|
||
await message.answer("❌ Пустое значение. Попробуйте ещё раз.")
|
||
return
|
||
|
||
try:
|
||
if param == "delay":
|
||
if "-" in value:
|
||
mn, mx = parse_range(value)
|
||
else:
|
||
mn = mx = int(value)
|
||
await self.task_manager.update_task(task_id, min_delay=mn, max_delay=mx)
|
||
await message.answer(f"✅ Задержка: {format_range(mn, mx)}с")
|
||
|
||
elif param == "reading":
|
||
if "-" in value:
|
||
mn, mx = parse_range(value, default_min=1)
|
||
else:
|
||
mn = mx = int(value)
|
||
await self.task_manager.update_task(task_id, min_reading=mn, max_reading=mx)
|
||
await message.answer(f"✅ Чтение: {format_range(mn, mx)}с")
|
||
|
||
elif param == "perlink":
|
||
await self.task_manager.update_task(task_id, visits_per_link=int(value))
|
||
await message.answer(f"✅ На ссылку: {value}")
|
||
|
||
elif param == "minutes":
|
||
await self.task_manager.update_task(task_id, monitor_minutes=int(value))
|
||
await message.answer(f"✅ Мониторинг: {value} мин")
|
||
|
||
elif param == "domains":
|
||
if value.lower() in ["all", "все"]:
|
||
await self.task_manager.update_task(task_id, allowed_domains=None)
|
||
await message.answer("✅ Домены: все (фильтр отключен)")
|
||
else:
|
||
domains = [d.strip().lower() for d in value.split(",") if d.strip()]
|
||
await self.task_manager.update_task(task_id, allowed_domains=domains)
|
||
await message.answer(f"✅ Домены: {', '.join(domains)}")
|
||
else:
|
||
await message.answer(f"❌ Неизвестный параметр: {param}")
|
||
|
||
except ValueError as e:
|
||
await message.answer(f"❌ Ошибка: {e}")
|
||
|
||
async def _run_twitch_monitor(self, task_id: str, params: TaskParams, message: Message):
|
||
"""Запуск Twitch мониторинга."""
|
||
from services.irc_service import TwitchIRCClient
|
||
|
||
irc = TwitchIRCClient(params.channel, params.target_username)
|
||
|
||
async def on_url(url: str, username: str):
|
||
params.links_found += 1
|
||
try:
|
||
from urllib.parse import urlparse
|
||
domain = urlparse(url).netloc
|
||
await send_message_safe(
|
||
message.bot, message.chat.id,
|
||
f"🔗 @{username}: `{url[:60]}`\n🌐 Домен: `{domain}`"
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f"Failed to send URL with domain: {e}")
|
||
await send_message_safe(message.bot, message.chat.id, f"🔗 @{username}: `{url[:60]}`")
|
||
|
||
for i in range(params.visits_per_link):
|
||
while params.paused:
|
||
await asyncio.sleep(1)
|
||
|
||
reading = params.get_reading_time()
|
||
result = await self.browser_service.visit_page(url, reading)
|
||
|
||
params.total_visits += 1
|
||
if result.success:
|
||
params.successful_visits += 1
|
||
|
||
if i < params.visits_per_link - 1:
|
||
await asyncio.sleep(params.get_delay())
|
||
|
||
try:
|
||
await irc.listen_for_messages(on_url, params.monitor_duration, params.allowed_domains)
|
||
except asyncio.CancelledError:
|
||
raise
|
||
finally:
|
||
await irc.disconnect()
|
||
await self.task_manager.remove_task(task_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)
|
||
|
||
async def on_url(url, username):
|
||
"""Запускаем обработку ссылки в отдельной задаче."""
|
||
task = asyncio.create_task(self._process_url(url, username, params, message))
|
||
task.add_done_callback(
|
||
lambda t: logger.error(f"URL processing failed: {t.exception()}") if not t.done() and t.exception() else None
|
||
)
|
||
|
||
def is_active():
|
||
return not params.paused and not params.stopped
|
||
|
||
logger.info(f"🚀 IRC monitor: {params.channel}")
|
||
|
||
try:
|
||
await irc.listen_for_messages(on_url, params.monitor_duration, params.allowed_domains, is_active=is_active)
|
||
await self.task_manager.complete_task(task_id)
|
||
except asyncio.CancelledError:
|
||
raise
|
||
finally:
|
||
await irc.disconnect()
|
||
if self.storage:
|
||
await self.storage.save_task(task_id, params)
|
||
|
||
|
||
|
||
async def _process_url(self, url: str, username: str, params: TaskParams, message: Message):
|
||
"""Обрабатывает найденную ссылку (выполняется параллельно)."""
|
||
try:
|
||
params.links_found += 1
|
||
|
||
from urllib.parse import urlparse
|
||
domain = urlparse(url).netloc
|
||
|
||
logger.info(f"🔗 @{username}: {url}")
|
||
|
||
await send_message_safe(
|
||
message.bot, message.chat.id,
|
||
f"🔗 @{username}: `{url[:60]}`"
|
||
)
|
||
|
||
for i in range(params.visits_per_link):
|
||
while params.paused:
|
||
await asyncio.sleep(1)
|
||
|
||
reading = params.get_reading_time()
|
||
|
||
try:
|
||
result = await self.browser_service.visit_page_from_twitch(
|
||
url=url,
|
||
channel=params.channel,
|
||
reading_time=reading
|
||
)
|
||
params.total_visits += 1
|
||
if result.success:
|
||
params.successful_visits += 1
|
||
except Exception as e:
|
||
logger.error(f"Visit error: {e}")
|
||
|
||
if i < params.visits_per_link - 1:
|
||
await asyncio.sleep(params.get_delay())
|
||
|
||
except Exception as e:
|
||
logger.error(f"Process URL error: {e}")
|
||
|
||
|
||
|