Files
Click/handlers/commands.py
T
Yuriy Yuriev 328f510151 add register
2026-05-14 18:23:50 +07:00

1007 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Обработчики команд с удобным интерфейсом управления задачами.
"""
import asyncio
import logging
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.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
from auth import ADMIN_PASSWORD
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,
bot_ref=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] = {}
self._bot_ref = bot_ref
self._waiting_password: Dict[int, str] = {} # user_id -> "register" or "login"
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
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 message.answer(
"👋 Привет!\n\n"
"У вас нет доступа к функционалу бота.\n"
"Обратитесь к администратору.",
reply_markup=ReplyKeyboardRemove()
)
return
exists = await interface.auth_storage.user_exists(user_id)
if exists:
await message.answer("👋 С возвращением!\n\nВведите пароль для входа:")
interface._waiting_password[user_id] = "login"
else:
await message.answer(
"👋 Добро пожаловать!\n\n"
"Вы здесь впервые. Придумайте пароль для регистрации\n"
"_(минимум 4 символа):_"
)
interface._waiting_password[user_id] = "register"
@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)
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 message.answer(
"✅ Регистрация прошла успешно!\n"
"🔓 Вы вошли как **АДМИНИСТРАТОР**.\n"
"Весь функционал бота доступен."
)
await interface._update_commands()
else:
await message.answer(
"✅ Регистрация прошла успешно!\n"
"👤 Вы вошли как **ПОЛЬЗОВАТЕЛЬ**.\n"
"Функционал ограничен."
)
@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"]
):
is_admin = (password == ADMIN_PASSWORD)
interface.auth_manager.login(user_id, is_admin=is_admin)
if is_admin:
await message.answer(
"✅ Вы вошли в систему!\n"
"🔓 Вы вошли как **АДМИНИСТРАТОР**."
)
await interface._update_commands()
else:
await message.answer(
"✅ Вы вошли в систему!\n"
"👤 Вы вошли как **ПОЛЬЗОВАТЕЛЬ**."
)
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(
"🔑 Вы вышли из системы.",
reply_markup=ReplyKeyboardRemove()
)
await interface._update_commands()
# Декоратор: доступ только для авторизованных
async def require_auth(event):
user_id = event.from_user.id
if not interface.auth_manager.is_authenticated(user_id):
await event.answer(
"🔒 Требуется авторизация!\n\n"
"Напишите администратору для получения доступа."
)
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(
"🔒 Требуется авторизация!\n\n"
"/register <пароль> — зарегистрироваться\n"
"/login <пароль> — войти"
)
return False
if not interface.auth_manager.is_admin(user_id):
await event.answer(
"⛔ **Нет доступа!**\n\n"
"Эта функция доступна только администраторам.\n"
"Войдите как администратор."
)
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)
await callback.answer()
# === Стримеры ===
@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.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"
"`<канал> <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_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": "⏱️ Задержка (сек): `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.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)
# Единый обработчик всех текстовых сообщений — регистрируется последним,
# чтобы не перекрывать обработчики кнопок с фильтрами 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
mode = interface._waiting_password.pop(user_id)
if mode == "register":
if len(text) < 4:
await message.answer("❌ Пароль слишком короткий (минимум 4 символа). Попробуйте ещё раз:")
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 message.answer(
"✅ Регистрация прошла успешно!\n"
"👤 Вы вошли как **ПОЛЬЗОВАТЕЛЬ**.\n\n"
"Обратитесь к администратору для получения доступа."
)
elif mode == "login":
user_data = await interface.auth_storage.get_user(user_id)
if not user_data:
await message.answer(
"❌ Вы не зарегистрированы.\n"
"Напишите администратору для получения доступа."
)
return
if interface.auth_manager.is_locked_out(user_id):
await message.answer("🔒 Слишком много попыток. Попробуйте позже.")
return
if interface.auth_manager.verify_password(
text, user_data["password_hash"], user_data["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 message.answer(
"👋 Привет!\n\n"
"У вас нет доступа к функционалу бота.\n"
"Обратитесь к администратору.",
reply_markup=ReplyKeyboardRemove()
)
else:
interface.auth_manager.record_failed_attempt(user_id)
await message.answer("❌ Неверный пароль. Попробуйте ещё раз:")
interface._waiting_password[user_id] = "login"
return
if not interface.auth_manager.is_authenticated(user_id):
await message.answer(
"🔒 Требуется авторизация!\n\n"
"/register <пароль> — зарегистрироваться\n"
"/login <пароль> — войти"
)
return
if not interface.auth_manager.is_admin(user_id):
await message.answer(
"⛔ **Нет доступа!**\n\n"
"Эта функция доступна только администраторам."
)
return
logger.info(f"Handle text: '{text[:50]}' from {user_id}, param_state={user_id in interface._user_input_state}")
if user_id in interface._user_input_state:
await interface._process_param_input(message)
return
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 _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(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.cancelled() 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}")