Files
Click/handlers/commands.py
T
2026-05-22 18:29:29 +07:00

3079 lines
150 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 os
import random
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.browser_pool import BrowserPool
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, BalanceStorage, RubleBalanceStorage, PaymentStorage
from services.payment_service import HelketPayment
from services.twitch_api import get_viewer_count
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,
browser_pool: BrowserPool = None,
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.browser_pool = browser_pool
self.task_manager = TaskManager()
self.storage = storage
self.chat_storage = chat_storage
self.auth_manager = AuthManager()
self.auth_storage = AuthStorage()
self.balance_storage = BalanceStorage() # переходы
self.rub_storage = RubleBalanceStorage() # рубли
self.payment_storage = PaymentStorage()
self.heleket = HelketPayment()
self._user_input_state: Dict[int, dict] = {}
self._payment_state: Dict[int, str] = {} # user_id -> "topup" | "buy_clicks"
self._payment_confirm: Dict[int, dict] = {} # user_id -> {clicks, rub} pending confirm
self._topup_confirm: Dict[int, int] = {} # user_id -> rub amount pending confirm
self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем
self._admin_balance_state: Dict[int, int] = {} # admin_id -> target_user_id
self._menu_msg: Dict[int, int] = {} # user_id -> inline keyboard message_id
self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id
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:
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
await interface._clear_chat(
message.bot, message.chat.id, user_id,
latest_msg_id=message.message_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 interface._show_user_menu(message)
return
exists = await interface.auth_storage.user_exists(user_id)
if exists:
sent = await message.answer("С возвращением! Введите пароль:")
interface._waiting_password[user_id] = "login"
else:
sent = await message.answer(
"Добро пожаловать!\n\nПридумайте пароль для входа (мин. 4 символа):"
)
interface._waiting_password[user_id] = "register"
interface._menu_msg[user_id] = sent.message_id
@dp.message(Command("register"))
async def cmd_register(message: Message):
"""Регистрация: /register <пароль>"""
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
parts = message.text.split(maxsplit=1)
if len(parts) < 2 or len(parts[1]) < 4:
await interface._send_temp(message, "❌ Использование: /register <пароль от 4 символов>")
return
password = parts[1]
user_id = message.from_user.id
exists = await interface.auth_storage.user_exists(user_id)
if exists:
await interface._send_temp(message, "⚠️ Вы уже зарегистрированы. Используйте /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 interface._update_commands()
await interface._show_main_menu(message)
else:
await interface._show_user_menu(message)
@dp.message(Command("login"))
async def cmd_login(message: Message):
"""Вход: /login <пароль>"""
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
await interface._send_temp(message, "❌ Использование: /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 interface._send_temp(message, "❌ Вы не зарегистрированы.\nИспользуйте /register <пароль>")
return
if interface.auth_manager.is_locked_out(user_id):
await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.")
return
if password == ADMIN_PASSWORD:
interface.auth_manager.login(user_id, is_admin=True)
await interface._update_commands()
await interface._show_main_menu(message)
elif interface.auth_manager.verify_password(
password, user_data["password_hash"], user_data["salt"]
):
interface.auth_manager.login(user_id, is_admin=False)
await interface._show_user_menu(message)
else:
interface.auth_manager.record_failed_attempt(user_id)
await interface._send_temp(message, "❌ Неверный пароль")
@dp.message(Command("logout"))
async def cmd_logout(message: Message):
"""Выйти из аккаунта."""
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
interface.auth_manager.logout(message.from_user.id)
await interface._update_commands()
await interface._send_temp(message, "🔑 Вы вышли из системы.", delay=3)
# Декоратор: доступ только для авторизованных
async def require_auth(event):
user_id = event.from_user.id
if not interface.auth_manager.is_authenticated(user_id):
await event.answer("Требуется авторизация")
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("Требуется авторизация")
return False
if not interface.auth_manager.is_admin(user_id):
await event.answer("Нет доступа — войдите как администратор")
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, user_id=callback.from_user.id)
await callback.answer()
# === Пользователи (admin reply-button) ===
@dp.message(F.text == "👥 Пользователи")
async def btn_users(message: Message):
if not await require_admin(message):
return
await interface._show_users_list(message)
# === Стримеры ===
@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.callback_query(F.data == "reload_proxies")
async def cb_reload_proxies(callback: CallbackQuery):
if not await require_admin(callback):
return
count = interface.proxy_manager.reload()
await interface._show_status(callback.message, edit=True)
await callback.answer(f"✅ Прокси перезагружены: {count} шт.")
@dp.callback_query(F.data == "check_proxies")
async def cb_check_proxies(callback: CallbackQuery):
if not await require_admin(callback):
return
total = interface.proxy_manager.count
if total == 0:
await callback.answer("❌ Нет загруженных прокси", show_alert=True)
return
await callback.message.edit_text(
f"⏳ Проверка {total} прокси через туннель...\n\nЭто займёт несколько секунд."
)
await callback.answer()
asyncio.create_task(interface._check_all_proxies(callback.message))
@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")
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": (
"⏱️ *Пауза между переходами*\n"
"Сколько секунд ждать перед следующим кликом.\n"
"Диапазон `30-60` — каждый раз рандом от 30 до 60."
),
"reading": (
"📖 *Время на странице*\n"
"Сколько секунд браузер будет листать страницу (имитация чтения).\n"
"Диапазон `45-90`."
),
"perlink": (
"🔗 *Резервное кол-во кликов*\n"
"Используется только если Twitch GQL недоступен.\n"
"В норме: клики = зрители × CTR.\n"
"Пример: `5`"
),
"ctr": (
"📊 *CTR — процент от зрителей*\n"
"Формула: клики = зрители × CTR%.\n"
"Например: 120 зрителей × 1% = 1 клик.\n"
"Диапазон `0.8-1` — случайно в этом диапазоне."
),
"minutes": (
"⏰ *Длительность мониторинга*\n"
"Как долго следить за чатом стримера (в минутах).\n"
"`0` — мониторить бесконечно.\n"
"Пример: `30`"
),
"domains": (
"🌐 *Разрешённые домены*\n"
"Кликать только по ссылкам с этих сайтов.\n"
"Несколько через запятую: `twitch.tv, youtube.com`\n"
"`all` — любые ссылки без фильтра."
),
}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"tdetail_{task_id}"))
prompt = prompts.get(param, f"Введите значение для {param}:")
sent = await callback.message.answer(prompt, reply_markup=builder.as_markup())
interface._user_input_state[callback.from_user.id]["prompt_msg_id"] = sent.message_id
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)
# === Кабинет пользователя ===
@dp.callback_query(F.data == "user_menu")
async def cb_user_menu(callback: CallbackQuery):
await interface._show_user_menu(
callback.message, edit=True, user_id=callback.from_user.id
)
await callback.answer()
@dp.callback_query(F.data == "my_balance")
async def cb_my_balance(callback: CallbackQuery):
await interface._show_user_menu(
callback.message, edit=True, user_id=callback.from_user.id
)
await callback.answer()
# ── Пополнение рублей (крипта → рубли) ──────────────────────────────
_TOPUP_PRESETS = [100, 300, 500, 1000, 3000]
@dp.callback_query(F.data == "topup_main")
async def cb_topup_main(callback: CallbackQuery):
uid = callback.from_user.id
existing = await interface.payment_storage.get_by_user(uid)
if existing:
await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True)
await interface._show_invoice(callback, existing)
return
builder = InlineKeyboardBuilder()
for amount in _TOPUP_PRESETS:
builder.row(InlineKeyboardButton(
text=f"{amount}",
callback_data=f"topup_preset_{amount}"
))
builder.row(InlineKeyboardButton(text="✏️ Другая сумма", callback_data="topup_custom"))
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input"))
await callback.message.edit_text(
"💳 Пополнить криптой\n\nВыберите сумму:",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data.startswith("topup_preset_"))
async def cb_topup_preset(callback: CallbackQuery):
uid = callback.from_user.id
existing = await interface.payment_storage.get_by_user(uid)
if existing:
await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True)
return
try:
amount = int(callback.data.replace("topup_preset_", "", 1))
except ValueError:
await callback.answer("Ошибка")
return
interface._topup_confirm[uid] = amount
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="✅ Подтвердить и перейти к оплате", callback_data="confirm_topup"))
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="topup_main"))
await callback.message.edit_text(
f"💳 Пополнение криптой\n\n"
f"Сумма: {amount}\n\n"
f"После подтверждения откроется страница оплаты.",
reply_markup=builder.as_markup()
)
await callback.answer()
@dp.callback_query(F.data == "confirm_topup")
async def cb_confirm_topup(callback: CallbackQuery):
uid = callback.from_user.id
amount = interface._topup_confirm.pop(uid, None)
if not amount:
await callback.answer("Сессия истекла, начните заново", show_alert=True)
return
existing = await interface.payment_storage.get_by_user(uid)
if existing:
await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True)
await interface._show_invoice(callback, existing)
return
order_id = f"{uid}_topup_{int(datetime.now().timestamp())}"
invoice = await interface.heleket.create_invoice(amount, order_id)
if not invoice:
await callback.answer("Ошибка создания счёта", show_alert=True)
return
payment_data = {
"user_id": uid,
"chat_id": callback.message.chat.id,
"payment_id": invoice["payment_id"],
"rub": amount,
"address": invoice.get("address"),
"url": invoice.get("url"),
"created_at": datetime.now().isoformat(),
}
await interface.payment_storage.save(invoice["payment_id"], payment_data)
await interface._show_invoice(callback, payment_data)
await callback.answer()
@dp.callback_query(F.data == "topup_custom")
async def cb_topup_custom(callback: CallbackQuery):
uid = callback.from_user.id
interface._payment_state[uid] = "topup"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input"))
await callback.message.edit_text(
f"✏️ Введите сумму пополнения в рублях\n"
f"(от {settings.TOPUP_MIN_RUB} до {settings.TOPUP_MAX_RUB} ₽):",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data == "check_pay")
async def cb_check_payment(callback: CallbackQuery):
uid = callback.from_user.id
payment = await interface.payment_storage.get_by_user(uid)
if not payment:
await callback.answer("❌ Платёж не найден", show_alert=True)
await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id)
return
await callback.answer("⏳ Проверяем...")
status = await interface.heleket.check_status(payment["payment_id"])
if status == "paid":
await interface.payment_storage.delete(payment["payment_id"])
if payment.get("clicks"):
# Прямая покупка переходов
new_visits = await interface.balance_storage.add_balance(uid, payment["clicks"])
# Удаляем счёт и показываем временное уведомление
await callback.message.delete()
await interface._send_temp(
callback.message,
f"✅ Оплата прошла успешно!\n"
f"+{payment['clicks']} переходов начислено\n"
f"Итого переходов: {new_visits}",
delay=8
)
await interface._show_user_menu(callback.message, user_id=uid)
else:
# Пополнение рублёвого баланса
new_rub = await interface.rub_storage.add_balance(uid, payment["rub"])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🛒 Купить переходы", callback_data="visit_shop"))
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
await callback.message.edit_text(
f"✅ Баланс пополнен!\n\n"
f"+{payment['rub']}\n"
f"Баланс: {new_rub}",
reply_markup=builder.as_markup()
)
elif status == "expired":
await interface.payment_storage.delete(payment["payment_id"])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить снова", callback_data="topup_main"))
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
await callback.message.edit_text(
"Счёт истёк. Создайте новый.",
reply_markup=builder.as_markup()
)
else:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔄 Проверить снова", callback_data="check_pay"))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
await callback.message.edit_text(
"⏳ Платёж ещё не поступил\n\nОбычно перевод занимает 1–5 минут.",
reply_markup=builder.as_markup()
)
@dp.callback_query(F.data == "cancel_pay")
async def cb_cancel_payment(callback: CallbackQuery):
uid = callback.from_user.id
payment = await interface.payment_storage.get_by_user(uid)
if payment:
await interface.payment_storage.delete(payment["payment_id"])
await callback.answer("Счёт отменён")
await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id)
# ── Покупка переходов (рубли → переходы) ────────────────────────────
_CLICK_PRESETS = [50, 100, 200, 500]
@dp.callback_query(F.data == "visit_shop")
async def cb_visit_shop(callback: CallbackQuery):
uid = callback.from_user.id
price = settings.CLICK_PRICE_RUB
builder = InlineKeyboardBuilder()
for n in _CLICK_PRESETS:
cost = int(n * price)
builder.row(InlineKeyboardButton(
text=f"{n} переходов — {cost}",
callback_data=f"buy_preset_{n}"
))
builder.row(InlineKeyboardButton(text="✏️ Другое количество", callback_data="buy_custom"))
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input"))
await callback.message.edit_text(
f"🛒 Купить переходы\n\n"
f"Цена: {price:.0f} ₽ за 1 переход",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
async def _show_confirmation(callback: CallbackQuery, clicks: int, cost: int):
uid = callback.from_user.id
interface._payment_confirm[uid] = {"clicks": clicks, "rub": cost}
rub_bal = await interface.rub_storage.get_balance(uid)
builder = InlineKeyboardBuilder()
if rub_bal >= cost:
builder.row(InlineKeyboardButton(
text=f"💰 С баланса ({rub_bal} ₽)",
callback_data="pay_from_balance"
))
builder.row(InlineKeyboardButton(text="💳 Оплатить криптой", callback_data="confirm_buy"))
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_confirm_buy"))
await callback.message.edit_text(
f"🛒 Подтвердите покупку\n\n"
f"🔢 {clicks} переходов\n"
f"💰 Стоимость: {cost}"
+ (f"\n\n🪙 Ваш баланс: {rub_bal}" if rub_bal > 0 else ""),
reply_markup=builder.as_markup()
)
await callback.answer()
@dp.callback_query(F.data == "pay_from_balance")
async def cb_pay_from_balance(callback: CallbackQuery):
uid = callback.from_user.id
confirm = interface._payment_confirm.pop(uid, None)
if not confirm:
await callback.answer("Сессия истекла", show_alert=True)
return
clicks, cost = confirm["clicks"], confirm["rub"]
rub_bal = await interface.rub_storage.get_balance(uid)
if rub_bal < cost:
await callback.answer(f"Недостаточно рублей: нужно {cost} ₽, есть {rub_bal}", show_alert=True)
return
new_rub = await interface.rub_storage.deduct(uid, cost)
new_visits = await interface.balance_storage.add_balance(uid, clicks)
await callback.message.delete()
await interface._send_temp(
callback.message,
f"✅ Куплено {clicks} переходов\n"
f"Списано: {cost} ₽ | Остаток: {new_rub}\n"
f"Переходов: {new_visits}",
delay=6
)
await interface._show_user_menu(callback.message, user_id=uid)
await callback.answer()
@dp.callback_query(F.data.startswith("buy_preset_"))
async def cb_buy_preset(callback: CallbackQuery):
try:
clicks = int(callback.data.replace("buy_preset_", "", 1))
except ValueError:
await callback.answer("Ошибка")
return
cost = int(clicks * settings.CLICK_PRICE_RUB)
await _show_confirmation(callback, clicks, cost)
@dp.callback_query(F.data == "buy_custom")
async def cb_buy_custom(callback: CallbackQuery):
uid = callback.from_user.id
interface._payment_state[uid] = "buy_clicks"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input"))
await callback.message.edit_text(
f"✏️ Введите количество переходов\n\n"
f"Цена: {settings.CLICK_PRICE_RUB:.0f} ₽ за 1 переход",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data.startswith("utask_"))
async def cb_user_task_detail(callback: CallbackQuery):
tid = callback.data.replace("utask_", "", 1)
params, uid = await _get_user_task(callback, tid)
if not params:
return
await interface._show_user_task_detail(callback, tid, params)
await callback.answer()
@dp.callback_query(F.data.startswith("uset_"))
async def cb_user_set_param(callback: CallbackQuery):
uid = callback.from_user.id
if not interface.auth_manager.is_authenticated(uid):
await callback.answer("Требуется авторизация", show_alert=True)
return
# формат: uset_{task_id}_{param}
data = callback.data.replace("uset_", "", 1)
idx = data.rfind("_")
if idx == -1:
await callback.answer("Ошибка формата")
return
task_id = data[:idx]
param = data[idx + 1:]
params = await interface.task_manager.get_task(task_id)
if not params or params.user_id != uid:
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`."
)
_d_mn = params.min_delay // 60
_d_mx = params.max_delay // 60
_d_cur = f"{_d_mn}{_d_mx}" if _d_mn != _d_mx else str(_d_mn)
delay_label = (
f"⏱️ *Пауза между циклами*\n"
f"Сколько минут ждать после каждого цикла кликов.\n"
f"Текущая: {_d_cur} мин\n"
f"Диапазон `2-5`."
)
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`."
)
prompts = {
"delay": delay_label,
"reading": (
f"📖 *Время на странице*\n"
f"Сколько секунд браузер листает страницу (имитация чтения).\n"
f"Текущее: {params.min_reading}{params.max_reading} сек\n"
f"Диапазон `45-90`."
),
"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`."
),
"ctr": (
f"📊 *CTR — процент от зрителей*\n"
f"Формула: клики = зрители × CTR%.\n"
f"Текущий: {params.min_ctr}%{params.max_ctr}%\n"
f"Диапазон `0.8-1`."
),
}
prompt = prompts.get(param, f"Введите значение для {param}:")
interface._user_input_state[uid] = {
"task_id": task_id,
"param": param,
"menu_msg_id": callback.message.message_id,
}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"utask_{task_id}"))
await callback.message.edit_text(prompt, reply_markup=builder.as_markup())
await callback.answer()
@dp.callback_query(F.data == "confirm_buy")
async def cb_confirm_buy(callback: CallbackQuery):
uid = callback.from_user.id
confirm = interface._payment_confirm.pop(uid, None)
if not confirm:
await callback.answer("Сессия истекла, начните заново", show_alert=True)
return
clicks, rub = confirm["clicks"], confirm["rub"]
order_id = f"{uid}_clicks_{int(datetime.now().timestamp())}"
invoice = await interface.heleket.create_invoice(rub, order_id)
if not invoice:
await callback.answer("Ошибка создания счёта", show_alert=True)
return
payment_data = {
"user_id": uid,
"chat_id": callback.message.chat.id,
"payment_id": invoice["payment_id"],
"clicks": clicks,
"rub": rub,
"address": invoice.get("address"),
"url": invoice.get("url"),
"created_at": datetime.now().isoformat(),
}
await interface.payment_storage.save(invoice["payment_id"], payment_data)
builder = InlineKeyboardBuilder()
if invoice.get("url"):
builder.row(InlineKeyboardButton(text="💳 Перейти к оплате", url=invoice["url"]))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
body = "После оплаты баланс пополнится автоматически"
await callback.message.edit_text(
f"Счёт создан\n\n"
f"🔢 {clicks} переходов\n"
f"💰 {rub}\n\n"
f"{body}",
reply_markup=builder.as_markup()
)
await callback.answer()
@dp.callback_query(F.data == "cancel_confirm_buy")
async def cb_cancel_confirm_buy(callback: CallbackQuery):
uid = callback.from_user.id
interface._payment_confirm.pop(uid, None)
await interface._show_user_menu(callback.message, edit=True, user_id=uid)
await callback.answer()
@dp.callback_query(F.data == "cancel_pay_input")
async def cb_cancel_pay_input(callback: CallbackQuery):
uid = callback.from_user.id
interface._payment_state.pop(uid, None)
await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id)
@dp.callback_query(F.data == "new_task")
async def cb_new_task(callback: CallbackQuery):
uid = callback.from_user.id
balance = await interface.balance_storage.get_balance(uid)
if balance <= 0:
await callback.answer("❌ Недостаточно баланса", show_alert=True)
return
all_tasks = await interface.task_manager.get_all_tasks()
active_count = sum(
1 for p in all_tasks.values()
if p.user_id == uid and not p.completed and not p.stopped
)
if active_count >= 3:
await callback.answer("❌ Максимум 3 активные задачи", show_alert=True)
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📺 Twitch мониторинг", callback_data="task_type_twitch"))
builder.row(InlineKeyboardButton(text="🔗 Посещение по ссылке", callback_data="task_type_url"))
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
await callback.message.edit_text(
f"📝 Новая задача\n\nПереходов: {balance}\n\nВыберите тип:",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data == "task_type_twitch")
async def cb_task_type_twitch(callback: CallbackQuery):
uid = callback.from_user.id
interface._user_task_state[uid] = {"step": "channel"}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
await callback.message.edit_text(
"📺 Twitch мониторинг\n\nШаг 1/2 — введите название канала:",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data == "task_type_url")
async def cb_task_type_url(callback: CallbackQuery):
uid = callback.from_user.id
balance = await interface.balance_storage.get_balance(uid)
interface._user_task_state[uid] = {"step": "url"}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
await callback.message.edit_text(
f"🔗 Посещение по ссылке\n\nПереходов: {balance}\n\nШаг 1/2 — введите URL:",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data == "skip_percent")
async def cb_skip_percent(callback: CallbackQuery):
uid = callback.from_user.id
state = interface._user_task_state.pop(uid, None)
if not state or state.get("step") != "percent":
await callback.answer()
return
interface._menu_msg.pop(uid, None)
await callback.message.delete()
await interface._create_twitch_task(
callback.message, uid, state["channel"], state["domains"], 0.0
)
await callback.answer()
@dp.callback_query(F.data == "enter_admin_mode")
async def cb_enter_admin_mode(callback: CallbackQuery):
uid = callback.from_user.id
if not interface.auth_manager.is_authenticated(uid):
await callback.answer("❌ Требуется авторизация", show_alert=True)
return
interface._user_task_state.pop(uid, None)
interface._waiting_password[uid] = "admin_upgrade"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="user_menu"))
await callback.message.edit_text(
"🔑 Введите пароль администратора:",
reply_markup=builder.as_markup()
)
await callback.answer()
@dp.callback_query(F.data == "switch_to_user")
async def cb_switch_to_user(callback: CallbackQuery):
uid = callback.from_user.id
if not interface.auth_manager.is_authenticated(uid):
await callback.answer("❌ Требуется авторизация", show_alert=True)
return
interface.auth_manager.login(uid, is_admin=False)
await interface._show_user_menu(callback.message, user_id=uid)
await callback.answer("👤 Переключились в режим пользователя")
@dp.callback_query(F.data == "noop")
async def _cb_noop(callback: CallbackQuery):
await callback.answer()
@dp.callback_query(F.data == "cancel_new_task")
async def cb_cancel_new_task(callback: CallbackQuery):
uid = callback.from_user.id
interface._user_task_state.pop(uid, None)
await interface._show_user_menu(callback.message, user_id=uid)
await callback.answer("Отменено")
@dp.callback_query(F.data == "my_tasks")
async def cb_my_tasks(callback: CallbackQuery):
await interface._show_user_tasks(callback.message, callback.from_user.id, edit=True)
await callback.answer()
@dp.callback_query(F.data == "my_tasks_visits")
async def cb_my_tasks_visits(callback: CallbackQuery):
await interface._show_user_visits(callback.message, callback.from_user.id, edit=True)
await callback.answer()
@dp.callback_query(F.data == "my_tasks_monitors")
async def cb_my_tasks_monitors(callback: CallbackQuery):
await interface._show_user_monitors(callback.message, callback.from_user.id, edit=True)
await callback.answer()
async def _get_user_task(callback: CallbackQuery, tid: str):
uid = callback.from_user.id
if not interface.auth_manager.is_authenticated(uid):
await callback.answer("❌ Требуется авторизация", show_alert=True)
return None, uid
params = await interface.task_manager.get_task(tid)
if not params or params.user_id != uid:
await callback.answer("❌ Задача не найдена", show_alert=True)
return None, uid
return params, uid
@dp.callback_query(F.data.startswith("ustop_"))
async def cb_user_stop_task(callback: CallbackQuery):
tid = callback.data.replace("ustop_", "", 1)
params, uid = await _get_user_task(callback, tid)
if not params:
return
await interface.background_tasks.cancel_task(tid)
await interface.task_manager.remove_task(tid)
if interface.storage:
await interface.storage.delete_task(tid)
await callback.answer("🗑️ Задача удалена")
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
chat_id = params.chat_id or callback.message.chat.id
msg_mock = SimpleNamespace(
chat=SimpleNamespace(id=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": 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_"))
async def cb_user_pause_task(callback: CallbackQuery):
tid = callback.data.replace("upause_", "", 1)
params, uid = await _get_user_task(callback, tid)
if not params:
return
await interface.task_manager.pause_task(tid)
await callback.answer("⏸️ Пауза")
await interface._show_user_tasks(callback.message, uid, edit=True)
@dp.callback_query(F.data.startswith("uresume_"))
async def cb_user_resume_task(callback: CallbackQuery):
tid = callback.data.replace("uresume_", "", 1)
params, uid = await _get_user_task(callback, tid)
if not params:
return
await interface.task_manager.resume_task(tid)
await callback.answer("▶️ Возобновлено")
await interface._show_user_tasks(callback.message, uid, edit=True)
@dp.callback_query(F.data.startswith("uedit_"))
async def cb_user_edit_task(callback: CallbackQuery):
tid = callback.data.replace("uedit_", "", 1)
params, uid = await _get_user_task(callback, tid)
if not params:
return
interface._user_task_state[uid] = {"step": "edit_domains", "task_id": tid}
current = ", ".join(params.allowed_domains) if params.allowed_domains else "все"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="my_tasks"))
await callback.message.edit_text(
f"🌐 Изменить домены\n\n"
f"Текущие: {current}\n\n"
"Введите новые домены через запятую:",
reply_markup=builder.as_markup()
)
await callback.answer()
# === Панель пользователей (admin) ===
@dp.callback_query(F.data == "menu_users")
async def cb_menu_users(callback: CallbackQuery):
if not await require_admin(callback):
return
await interface._show_users_list(callback.message, edit=True)
await callback.answer()
@dp.callback_query(F.data.startswith("udetail_"))
async def cb_user_detail(callback: CallbackQuery):
if not await require_admin(callback):
return
target_uid = int(callback.data.replace("udetail_", "", 1))
await interface._show_user_detail(callback, target_uid)
await callback.answer()
@dp.callback_query(F.data.startswith("ubal_"))
async def cb_user_balance(callback: CallbackQuery):
if not await require_admin(callback):
return
parts = callback.data.split("_")
# формат: ubal_{amount}_{user_id} или ubal_custom_{user_id}
if parts[1] == "custom":
target_uid = int(parts[2])
interface._admin_balance_state[callback.from_user.id] = target_uid
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}"))
await callback.message.answer("Введите сумму для пополнения:", reply_markup=builder.as_markup())
await callback.answer()
else:
amount = int(parts[1])
target_uid = int(parts[2])
new_balance = await interface.balance_storage.add_balance(target_uid, amount)
await callback.answer(f"✅ +{amount} переходов. Баланс: {new_balance}")
await interface._show_user_detail(callback, target_uid)
@dp.callback_query(F.data.startswith("utasks_"))
async def cb_user_tasks_admin(callback: CallbackQuery):
if not await require_admin(callback):
return
target_uid = int(callback.data.replace("utasks_", "", 1))
await interface._show_user_tasks(callback.message, target_uid, edit=True, for_admin=True)
await callback.answer()
# Единый обработчик всех текстовых сообщений — регистрируется последним,
# чтобы не перекрывать обработчики кнопок с фильтрами 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
# Пароль удаляем немедленно
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
mode = interface._waiting_password.pop(user_id)
if mode == "register":
if len(text) < 4:
sent = await message.answer("Пароль слишком короткий, мин. 4 символа:")
interface._menu_msg[user_id] = sent.message_id
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 interface._show_user_menu(message)
elif mode == "login":
user_data = await interface.auth_storage.get_user(user_id)
if not user_data:
await interface._send_temp(message, "Вы не зарегистрированы")
return
if interface.auth_manager.is_locked_out(user_id):
await interface._send_temp(message, "Слишком много попыток, попробуйте позже")
return
if text == ADMIN_PASSWORD:
interface.auth_manager.login(user_id, is_admin=True)
await interface._update_commands()
await interface._show_main_menu(message)
elif interface.auth_manager.verify_password(
text, user_data["password_hash"], user_data["salt"]
):
interface.auth_manager.login(user_id, is_admin=False)
await interface._show_user_menu(message)
else:
interface.auth_manager.record_failed_attempt(user_id)
sent = await message.answer("Неверный пароль, попробуйте ещё раз:")
interface._menu_msg[user_id] = sent.message_id
interface._waiting_password[user_id] = "login"
elif mode == "admin_upgrade":
if text == ADMIN_PASSWORD:
interface.auth_manager.login(user_id, is_admin=True)
await interface._update_commands()
await interface._show_main_menu(message)
else:
sent = await message.answer("Неверный пароль администратора:")
interface._menu_msg[user_id] = sent.message_id
interface._waiting_password[user_id] = "admin_upgrade"
return
# Ввод шагов создания задачи (пользователь)
if user_id in interface._user_task_state:
await interface._process_user_task_input(message)
return
# Ввод суммы пополнения или покупки переходов
if user_id in interface._payment_state:
await interface._process_payment_input(message)
return
if not interface.auth_manager.is_authenticated(user_id):
await interface._send_temp(message, "Требуется авторизация")
return
# Ввод параметра задачи (admin И user)
if user_id in interface._user_input_state:
await interface._process_param_input(message)
return
if not interface.auth_manager.is_admin(user_id):
await interface._show_user_menu(message)
return
# Ввод суммы баланса (admin)
if user_id in interface._admin_balance_state:
target_uid = interface._admin_balance_state.pop(user_id)
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
try:
amount = int(text)
if amount <= 0:
raise ValueError
new_balance = await interface.balance_storage.add_balance(target_uid, amount)
await interface._send_temp(message, f"✅ Пополнено на {amount}. Баланс: {new_balance}")
except (ValueError, TypeError):
await interface._send_temp(message, "❌ Введите целое положительное число")
return
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
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 _safe_delete(self, bot, chat_id: int, msg_id: int) -> None:
try:
await bot.delete_message(chat_id, msg_id)
except Exception:
pass
async def _track_msg(self, user_id: int, msg_id: int) -> None:
pass # трекинг заменён sweep-методом
async def _clear_chat(self, bot, chat_id: int, user_id: int,
latest_msg_id: int = None, depth: int = 200) -> None:
"""Удаляет последние N сообщений бота перебором ID — трекинг не нужен."""
self._menu_msg.pop(user_id, None)
self._menu_top_msg.pop(user_id, None)
self._chat_history.pop(user_id, None)
if not latest_msg_id:
return
ids = list(range(max(1, latest_msg_id - depth), latest_msg_id + 1))
# deleteMessages принимает до 100 за раз
for i in range(0, len(ids), 100):
try:
await bot.delete_messages(chat_id, ids[i:i + 100])
except Exception:
for mid in ids[i:i + 100]:
await self._safe_delete(bot, chat_id, mid)
async def _send_temp(self, message: Message, text: str, delay: int = 5) -> None:
"""Отправляет сообщение и удаляет его через delay секунд."""
sent = await message.answer(text)
uid = message.from_user.id if message.from_user else None
if uid:
await self._track_msg(uid, sent.message_id)
async def _delete():
await asyncio.sleep(delay)
await self._safe_delete(message.bot, message.chat.id, sent.message_id)
asyncio.create_task(_delete())
async def _clean_prev(self, message: Message, user_id: int = None) -> None:
"""Удаляет триггер-сообщение и все предыдущие меню пользователя."""
if user_id is None:
user_id = message.from_user.id if message.from_user else None
if not user_id:
return
if user_id in self._menu_top_msg:
await self._safe_delete(message.bot, message.chat.id, self._menu_top_msg.pop(user_id))
if user_id in self._menu_msg:
await self._safe_delete(message.bot, message.chat.id, self._menu_msg.pop(user_id))
await self._safe_delete(message.bot, message.chat.id, message.message_id)
async def _edit_or_send(self, message: Message, text: str, markup, edit: bool, user_id: int = None) -> None:
if edit:
try:
await message.edit_text(text, reply_markup=markup)
return
except Exception as e:
logger.debug(f"Edit message skipped: {e}")
await self._clean_prev(message, user_id=user_id)
sent = await message.answer(text, reply_markup=markup)
uid = user_id or (message.from_user.id if message.from_user else None)
if uid:
self._menu_msg[uid] = sent.message_id
await self._track_msg(uid, sent.message_id)
async def _show_main_menu(self, message: Message, user_id: int = None):
"""Главное меню."""
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="menu_users"))
builder.row(InlineKeyboardButton(text="🛑 Остановить всё", callback_data="stop_all"))
builder.row(InlineKeyboardButton(text="👤 Режим пользователя", callback_data="switch_to_user"))
reply = ReplyKeyboardBuilder()
reply.row(KeyboardButton(text="📺 Стримеры"), KeyboardButton(text="📊 Задачи"))
reply.row(KeyboardButton(text="👥 Пользователи"), KeyboardButton(text="📈 Статус"))
reply.row(KeyboardButton(text="🚀 Быстрый старт"), KeyboardButton(text="🛑 Остановить всё"))
uid = user_id or (message.from_user.id if message.from_user else None)
await self._clean_prev(message, user_id=uid)
sent1 = await message.answer(text, reply_markup=reply.as_markup(resize_keyboard=True))
sent2 = await message.answer("💡 Кнопки управления:", reply_markup=builder.as_markup())
if uid:
self._menu_top_msg[uid] = sent1.message_id
self._menu_msg[uid] = sent2.message_id
await self._track_msg(uid, sent1.message_id)
await self._track_msg(uid, sent2.message_id)
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Задач нет. Отправьте название канала для добавления."
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"))
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()
builder = InlineKeyboardBuilder()
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)
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"⏱️ Аптайм: {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="reload_proxies"))
builder.row(InlineKeyboardButton(text="🔍 Проверить прокси", callback_data="check_proxies"))
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)
async def _check_all_proxies(self, message) -> None:
"""Проверяет все загруженные прокси через туннель и редактирует сообщение с результатами."""
from services.socks5_to_http_proxy import Socks5ToHttpProxy
from managers.proxy_manager import ProxyType
proxies = list(self.proxy_manager._proxies)
if not proxies:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 К статусу", callback_data="menu_status"))
try:
await message.edit_text("❌ Нет загруженных прокси", reply_markup=builder.as_markup())
except Exception:
pass
return
sem = asyncio.Semaphore(5)
results: dict[str, bool] = {}
async def _check_one(proxy):
async with sem:
try:
if proxy.proxy_type == ProxyType.SOCKS5:
tunnel = Socks5ToHttpProxy(
socks5_host=proxy.ip,
socks5_port=int(proxy.port),
username=proxy.login,
password=proxy.password,
)
await tunnel.start()
try:
ok = await tunnel.check_connection(timeout=8.0)
finally:
await tunnel.stop()
else:
from aiohttp import ClientSession, ClientTimeout
proxy_url = proxy.server
if proxy.login and proxy.password:
from urllib.parse import urlparse
parsed = urlparse(proxy_url)
proxy_url = f"{parsed.scheme}://{proxy.login}:{proxy.password}@{parsed.netloc}"
async with ClientSession(timeout=ClientTimeout(total=8)) as s:
async with s.get(
"https://www.google.com/generate_204",
proxy=proxy_url,
allow_redirects=False,
) as resp:
ok = resp.status in (200, 204)
except Exception:
ok = False
results[proxy.id] = ok
await asyncio.gather(*[_check_one(p) for p in proxies], return_exceptions=True)
await self.proxy_manager.apply_check_results(results)
ok_ids = [pid for pid, ok in results.items() if ok]
fail_ids = [pid for pid, ok in results.items() if not ok]
lines = [
f"🔍 Проверка прокси завершена\n",
f"✅ Рабочих: {len(ok_ids)} / {len(proxies)}",
f"❌ Нерабочих: {len(fail_ids)}\n",
]
if ok_ids:
lines.append("✅ Работают:")
for pid in ok_ids[:15]:
lines.append(f"{pid}")
if len(ok_ids) > 15:
lines.append(f" ...ещё {len(ok_ids) - 15}")
if fail_ids:
lines.append("\n❌ Не работают:")
for pid in fail_ids[:15]:
lines.append(f"{pid}")
if len(fail_ids) > 15:
lines.append(f" ...ещё {len(fail_ids) - 15}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 К статусу", callback_data="menu_status"))
try:
await message.edit_text("\n".join(lines), reply_markup=builder.as_markup())
except Exception as e:
logger.error(f"Proxy check result edit failed: {e}")
# =========================================================================
# УПРАВЛЕНИЕ ЗАДАЧЕЙ
# =========================================================================
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="📊 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"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)
await self._safe_delete(message.bot, message.chat.id, message.message_id)
task_id = state["task_id"]
param = state["param"]
menu_msg_id = state.get("menu_msg_id")
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 self._send_temp(message, "❌ Пустое значение. Попробуйте ещё раз.")
return
confirm = ""
try:
if param == "delay":
if "-" in value:
mn, mx = parse_range(value)
else:
mn = mx = int(value)
task_obj = await self.task_manager.get_task(task_id)
if task_obj and task_obj.task_type == "user_visit":
await self.task_manager.update_task(task_id, min_delay=mn * 60, max_delay=mx * 60)
confirm = f"✅ Пауза: {format_range(mn, mx)} мин"
else:
await self.task_manager.update_task(task_id, min_delay=mn, max_delay=mx)
confirm = 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)
confirm = f"✅ Чтение: {format_range(mn, mx)}с"
elif param == "perlink":
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)
confirm = f"✅ Цикл: {lbl} кликов"
else:
await self.task_manager.update_task(task_id, visits_per_link=int(value))
confirm = 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}%"
confirm = f"✅ CTR: {lbl}"
elif param == "minutes":
await self.task_manager.update_task(task_id, monitor_minutes=int(value))
confirm = f"✅ Мониторинг: {value} мин"
elif param == "domains":
if value.lower() in ["all", "все"]:
await self.task_manager.update_task(task_id, allowed_domains=None)
confirm = "✅ Домены: все"
else:
domains = [d.strip().lower() for d in value.split(",") if d.strip()]
await self.task_manager.update_task(task_id, allowed_domains=domains)
confirm = f"✅ Домены: {', '.join(domains)}"
elif param == "percent":
pct = float(value.replace(",", "."))
if pct < 0 or pct > 100:
raise ValueError("Значение от 0 до 100")
await self.task_manager.update_task(task_id, visits_percent=pct)
confirm = 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)
confirm = f"✅ Задержка между кликами: {format_range(mn, mx)}с"
else:
await self._send_temp(message, f"❌ Неизвестный параметр: {param}")
return
except ValueError as e:
await self._send_temp(message, f"❌ Ошибка: {e}")
return
# Обновляем меню с параметрами (заменяем prompt обратно на настройки задачи)
updated = await self.task_manager.get_task(task_id)
if updated and menu_msg_id:
try:
await self._rebuild_user_task_menu(
message.bot, message.chat.id, menu_msg_id, task_id, updated, confirm
)
return
except Exception:
pass
await self._send_temp(message, confirm, delay=4)
async def _rebuild_user_task_menu(
self, bot, chat_id: int, msg_id: int,
task_id: str, params: TaskParams, confirm: str = ""
):
"""Редактирует сообщение обратно в настройки задачи после ввода параметра."""
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\n"
f"🔢 Кликов в цикле: {series_range}\n"
f"⏳ Задержка между кликами: {params.min_click_delay}{params.max_click_delay} сек\n"
f"⏱ Пауза между циклами: {params.min_delay // 60}{params.max_delay // 60} мин\n"
f"📖 Время на странице: {params.min_reading}{params.max_reading} сек"
)
else:
domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все"
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}\n"
f"🌐 Домены: {domains}\n\n"
f"🔗 Ссылок найдено: {params.links_found}\n"
f"✅ Кликов: {params.successful_visits}/{params.total_planned_visits}\n"
f"В очереди: {params.pending_visits}\n\n"
f"📊 CTR: {ctr_str}\n"
f"⏱ Пауза между кликами: {params.min_delay}{params.max_delay} сек\n"
f"📖 Время на странице: {params.min_reading}{params.max_reading} сек"
)
builder = InlineKeyboardBuilder()
if params.task_type == "user_visit":
builder.row(
InlineKeyboardButton(text="⏱️ Пауза (мин)", callback_data=f"uset_{task_id}_delay"),
InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"),
)
builder.row(
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}_delay"),
InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"),
)
builder.row(
InlineKeyboardButton(text="📊 % CTR зрителей", callback_data=f"uset_{task_id}_ctr"),
InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}"),
)
builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks"))
prefix = f"{confirm}\n\n" if confirm else ""
await bot.edit_message_text(
f"{prefix}⚙️ Параметры задачи\n\n{info}",
chat_id=chat_id,
message_id=msg_id,
reply_markup=builder.as_markup()
)
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\n"
f"🔢 Кликов в цикле: {series_range}\n"
f"⏳ Задержка между кликами: {params.min_click_delay}{params.max_click_delay} сек\n"
f"⏱ Пауза между циклами: {params.min_delay // 60}{params.max_delay // 60} мин\n"
f"📖 Время на странице: {params.min_reading}{params.max_reading} сек"
)
else:
domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все"
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}\n"
f"🌐 Домены: {domains}\n\n"
f"🔗 Ссылок найдено: {params.links_found}\n"
f"✅ Кликов: {params.successful_visits}/{params.total_planned_visits}\n"
f"В очереди: {params.pending_visits}\n\n"
f"📊 CTR: {ctr_str} от зрителей\n"
f"⏱ Пауза между кликами: {params.min_delay}{params.max_delay} сек\n"
f"📖 Время на странице: {params.min_reading}{params.max_reading} сек"
)
builder = InlineKeyboardBuilder()
if params.task_type == "user_visit":
builder.row(
InlineKeyboardButton(text="⏱️ Пауза (мин)", callback_data=f"uset_{task_id}_delay"),
InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"),
)
builder.row(
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}_delay"),
InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"),
)
builder.row(
InlineKeyboardButton(text="📊 % CTR зрителей", callback_data=f"uset_{task_id}_ctr"),
InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}"),
)
builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks"))
try:
await callback.message.edit_text(
f"⚙️ Параметры задачи\n\n{info}",
reply_markup=builder.as_markup()
)
except Exception:
await callback.message.answer(
f"⚙️ Параметры задачи\n\n{info}",
reply_markup=builder.as_markup()
)
async def _stream_watcher(self, task_id: str, params: TaskParams, message: Message,
check_interval: int = 60) -> None:
"""Проверяет онлайн-статус стрима, ставит/снимает паузу и сохраняет состояние."""
was_offline = params.stream_offline
while not params.stopped:
await asyncio.sleep(check_interval)
if params.stopped:
break
viewers = await get_viewer_count(params.channel)
if viewers is None:
logger.debug(f"[watcher] {params.channel}: GQL failed, skipping")
continue
if viewers == 0 and not was_offline:
was_offline = True
params.stream_offline = True
# Ставим паузу только если не уже на паузе вручную
if not params.paused:
params.paused = True
params.auto_paused = True
await self.task_manager.update_task(task_id, paused=True)
if self.storage:
await self.storage.save_task(task_id, params)
logger.info(f"[watcher] {params.channel} went offline")
msg = await send_message_safe(
message.bot, params.chat_id,
f"{params.channel} ушёл в оффлайн — мониторинг приостановлен"
)
if msg and params.user_id:
await self._track_msg(params.user_id, msg.message_id)
elif viewers > 0 and was_offline:
was_offline = False
params.stream_offline = False
if params.auto_paused:
params.paused = False
params.auto_paused = False
await self.task_manager.update_task(task_id, paused=False)
if self.storage:
await self.storage.save_task(task_id, params)
logger.info(f"[watcher] {params.channel} is online ({viewers} viewers)")
msg = await send_message_safe(
message.bot, params.chat_id,
f"▶️ {params.channel} снова онлайн ({viewers} зрителей) — мониторинг возобновлён"
)
if msg and params.user_id:
await self._track_msg(params.user_id, msg.message_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)
url_tasks: set = set()
async def on_url(url, username):
if params.stream_offline:
return
task = asyncio.create_task(self._process_url(url, username, params, message))
url_tasks.add(task)
task.add_done_callback(url_tasks.discard)
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 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}")
watcher = asyncio.create_task(self._stream_watcher(task_id, params, message))
try:
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:
await self.task_manager.complete_task(task_id)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"IRC error [{params.channel}]: {e}")
finally:
watcher.cancel()
for t in list(url_tasks):
t.cancel()
if url_tasks:
await asyncio.gather(*url_tasks, return_exceptions=True)
await irc.disconnect()
if self.storage:
await self.storage.save_task(task_id, params)
async def _run_url_visits(self, task_id: str, params: TaskParams, bot):
"""Посещает URL циклами: sleep(межцикловая) → N кликов(+sleep между ними) → повтор."""
max_v = params.max_visits or 1
async def _sleep_interruptible(seconds: int):
for _ in range(max(0, seconds)):
if params.stopped:
return
while params.paused and not params.stopped:
await asyncio.sleep(2)
await asyncio.sleep(1)
# Роут: [задержка] → клик1 → [задержка] → клик2 → ... → кликN → повтор
# Пауза: текущий клик завершается, серия останавливается на N
# Resume < 5 мин: продолжает с N+1 без задержки
# Resume > 5 мин: серия этой ссылки отбрасывается, ждём следующий цикл
remaining_clicks = 0
series_paused_at: Optional[float] = None
SERIES_EXPIRY = 5 * 60
try:
while params.successful_visits < max_v and not params.stopped:
# Ждём паузу если активна перед новым циклом
while params.paused and not params.stopped:
await asyncio.sleep(2)
if params.stopped:
break
# Определяем размер серии
if remaining_clicks > 0:
elapsed = datetime.now().timestamp() - (series_paused_at or 0)
if elapsed > SERIES_EXPIRY:
# Серия устарела — отбрасываем, ждём следующую задержку
logger.info(f"⏰ Series expired ({elapsed:.0f}s > {SERIES_EXPIRY//60}min), skipping remaining {remaining_clicks} clicks")
remaining_clicks = 0
series_paused_at = None
# Стандартная задержка перед следующей серией
await _sleep_interruptible(params.get_delay())
if params.stopped:
break
series_size = params.get_series_size()
else:
# Продолжаем с сохранённого места — без задержки
series_size = remaining_clicks
remaining_clicks = 0
series_paused_at = None
logger.info(f"▶️ Resuming: {series_size} clicks left ({elapsed:.0f}s paused)")
else:
# Новая серия: задержка → рандомный размер
delay = params.get_delay()
series_size = params.get_series_size()
logger.info(
f"⏳ Next series: {series_size} clicks "
f"(range {params.min_series}-{params.max_series}), "
f"delay {delay}s ({delay//60}m {delay%60}s), "
f"reading {params.min_reading}-{params.max_reading}s, "
f"done {params.successful_visits}/{max_v}"
)
await _sleep_interruptible(delay)
if params.stopped:
break
# --- Серия кликов ---
for click_num in range(series_size):
if params.stopped or params.successful_visits >= max_v:
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
try:
visitor = self.browser_pool or self.browser_service
result = await visitor.visit_page(params.url, params.get_reading_time())
params.total_visits += 1
if result.success:
params.successful_visits += 1
await self.balance_storage.deduct(params.user_id, 1)
logger.info(f"✅ Visit {params.successful_visits}/{max_v}: {params.url[:40]}")
else:
logger.warning(f"❌ Visit failed ({params.total_visits}): {result.error or 'unknown'}")
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"URL visit error: {e}")
params.total_visits += 1
# Клик завершён — проверяем паузу
if params.paused or params.stopped:
remaining_clicks = series_size - click_num - 1
if remaining_clicks > 0 and not params.stopped:
series_paused_at = datetime.now().timestamp()
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())
await self.task_manager.complete_task(task_id)
remaining = await self.balance_storage.get_balance(params.user_id)
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}"
)
except asyncio.CancelledError:
raise
finally:
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
# Проверяем баланс для пользовательских задач
if params.user_id:
balance = await self.balance_storage.get_balance(params.user_id)
if balance <= 0:
params.stopped = True
await send_message_safe(
message.bot, params.chat_id,
"⚠️ Баланс исчерпан — задача остановлена.\n"
"Обратитесь к администратору для пополнения."
)
return
# 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))
calc_info = f"{viewers} зрителей × {ctr:.2f}% = {visits_count} кликов"
else:
visits_count = max(1, round(params.visits_per_link * ctr / 100))
calc_info = f"зрители недоступны, фикс {params.visits_per_link} × {ctr:.2f}% = {visits_count} кликов"
params.pending_visits += visits_count
params.total_planned_visits += visits_count
logger.info(f"🔗 @{username} {url}{visits_count} clicks ({calc_info}), total pending: {params.pending_visits}")
await send_message_safe(
message.bot, message.chat.id,
f"🔗 @{username}: {url[:60]}\n"
f"📊 {calc_info}\n"
f"В очереди всего: {params.pending_visits} кликов"
)
# Роут: [задержка] → клик1 → [задержка] → клик2 → ... → кликN
# Пауза: текущий клик завершается, серия останавливается
# Resume < 5 мин: продолжает с сохранённого клика
# Resume > 5 мин: серия этой ссылки полностью отбрасывается
SERIES_EXPIRY = 5 * 60
remaining = visits_count
paused_at: Optional[float] = None
first_click = True
while remaining > 0 and not params.stopped:
# Проверяем не истекла ли серия после паузы
if paused_at is not None:
elapsed = datetime.now().timestamp() - paused_at
if elapsed > SERIES_EXPIRY:
logger.info(f"⏰ Серия устарела ({elapsed:.0f}s), ссылка {url[:40]} отброшена")
return
paused_at = None
logger.info(f"▶️ Продолжаем серию: {remaining} кликов осталось")
# Задержка перед каждым кликом (включая первый)
if not params.stopped and not params.paused:
delay = params.get_delay()
if first_click:
logger.info(f"⏳ Задержка перед первым кликом: {delay}с")
await asyncio.sleep(delay)
first_click = False
if params.stopped:
return
reading = params.get_reading_time()
try:
visitor = self.browser_pool or self.browser_service
result = await visitor.visit_page(url, reading)
params.total_visits += 1
params.pending_visits = max(0, params.pending_visits - 1)
if result.success:
params.successful_visits += 1
remaining -= 1
if params.user_id:
new_balance = await self.balance_storage.deduct(params.user_id, 1)
if new_balance <= 0:
params.stopped = True
params.pending_visits = max(0, params.pending_visits - remaining)
await send_message_safe(
message.bot, params.chat_id,
"⚠️ Баланс исчерпан — задача остановлена."
)
return
else:
remaining -= 1
except Exception as e:
logger.error(f"Visit error: {e}")
params.pending_visits = max(0, params.pending_visits - 1)
remaining -= 1
# Клик завершён — проверяем паузу
if params.paused and remaining > 0:
paused_at = datetime.now().timestamp()
logger.info(f"⏸️ Серия на паузе, {remaining} кликов сохранено (истечёт через {SERIES_EXPIRY//60}мин)")
while params.paused and not params.stopped:
await asyncio.sleep(2)
if params.stopped:
return
except Exception as e:
logger.error(f"Process URL error: {e}")
# =========================================================================
# КАБИНЕТ ПОЛЬЗОВАТЕЛЯ
# =========================================================================
async def _show_user_menu(self, message: Message, edit: bool = False, user_id: int = None):
if user_id is None:
user_id = message.from_user.id if message.from_user else None
if not user_id:
return
# Сбрасываем все незавершённые состояния при возврате в меню
self._waiting_password.pop(user_id, None)
self._user_task_state.pop(user_id, None)
self._payment_state.pop(user_id, None)
visits = await self.balance_storage.get_balance(user_id)
rubles = await self.rub_storage.get_balance(user_id)
all_tasks = await self.task_manager.get_all_tasks()
active = sum(
1 for p in all_tasks.values()
if p.user_id == user_id and not p.completed and not p.stopped
)
pending = await self.payment_storage.get_by_user(user_id)
text = (
"👤 Личный кабинет\n\n"
f"🪙 Рублей: {rubles}\n"
f"🔢 Переходов: {visits}\n"
f"📋 Активных задач: {active}"
)
if pending:
text += "\n\n⏳ Есть незакрытый счёт"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"),
InlineKeyboardButton(text="📋 Мои задачи", callback_data="my_tasks"),
)
if pending:
pending_url = pending.get("url")
if pending_url:
builder.row(InlineKeyboardButton(text="💳 Открыть счёт", url=pending_url))
builder.row(InlineKeyboardButton(text="❌ Отменить счёт", callback_data="cancel_pay"))
else:
builder.row(
InlineKeyboardButton(text="💳 Пополнить криптой", callback_data="topup_main"),
InlineKeyboardButton(text="🛒 Купить переходы", callback_data="visit_shop"),
)
builder.row(InlineKeyboardButton(text="🔑 Режим администратора", callback_data="enter_admin_mode"))
await self._edit_or_send(message, text, builder.as_markup(), edit, user_id=user_id)
async def _show_user_tasks(
self, message: Message, user_id: int,
edit: bool = False, for_admin: bool = False
):
all_tasks = await self.task_manager.get_all_tasks()
user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == user_id}
visits = {tid: p for tid, p in user_tasks.items() if p.task_type == "user_visit"}
monitors = {tid: p for tid, p in user_tasks.items() if p.task_type != "user_visit"}
v_active = sum(1 for p in visits.values() if not p.completed and not p.stopped)
m_active = sum(1 for p in monitors.values() if not p.completed and not p.stopped)
if not user_tasks:
text = f"📋 Задачи {str(user_id) if for_admin else ''}\n\nЗадач нет."
else:
header = f"📋 Задачи пользователя {user_id}\n\n" if for_admin else "📋 Мои задачи\n\n"
text = header
if visits:
text += f"🔗 Посещения — {v_active} акт. / {len(visits)} всего\n"
if monitors:
text += f"📺 Мониторинг — {m_active} акт. / {len(monitors)} всего\n"
builder = InlineKeyboardBuilder()
if for_admin:
builder.row(InlineKeyboardButton(text="🔙 К пользователю", callback_data=f"udetail_{user_id}"))
else:
builder.row(
InlineKeyboardButton(
text=f"🔗 Посещения ({len(visits)})",
callback_data="my_tasks_visits"
),
InlineKeyboardButton(
text=f"📺 Мониторинг ({len(monitors)})",
callback_data="my_tasks_monitors"
),
)
builder.row(InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_menu"))
await self._edit_or_send(message, text, builder.as_markup(), edit)
def _build_task_buttons(self, builder: InlineKeyboardBuilder, tasks: dict):
for tid, p in list(tasks.items())[:8]:
if p.task_type == "user_visit":
domain = extract_domain(p.url)[:16] if p.url else "?"
label = f"🔗 {domain}"
else:
label = f"📺 {p.channel}"
if not p.completed and not p.stopped:
pause_btn = (
InlineKeyboardButton(text="▶️", callback_data=f"uresume_{tid}")
if p.paused else
InlineKeyboardButton(text="⏸️", callback_data=f"upause_{tid}")
)
builder.row(
InlineKeyboardButton(text=label, callback_data=f"utask_{tid}"),
pause_btn,
InlineKeyboardButton(text="⚙️", callback_data=f"utask_{tid}"),
InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"),
)
else:
builder.row(
InlineKeyboardButton(text=label, callback_data=f"utask_{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="my_tasks"))
async def _show_user_visits(self, message: Message, user_id: int, edit: bool = False):
all_tasks = await self.task_manager.get_all_tasks()
visits = {
tid: p for tid, p in all_tasks.items()
if p.user_id == user_id and p.task_type == "user_visit"
}
if not visits:
text = "🔗 Посещения\n\nЗадач нет."
else:
text = "🔗 Посещения\n\n"
for _, p in list(visits.items())[:8]:
text += (
f"{p.get_status_emoji()} {p.url[:40]}\n"
f"{p.successful_visits}/{p.max_visits} | 📊 {p.total_visits}\n"
)
builder = InlineKeyboardBuilder()
self._build_task_buttons(builder, visits)
await self._edit_or_send(message, text, builder.as_markup(), edit)
async def _show_user_monitors(self, message: Message, user_id: int, edit: bool = False):
all_tasks = await self.task_manager.get_all_tasks()
monitors = {
tid: p for tid, p in all_tasks.items()
if p.user_id == user_id and p.task_type != "user_visit"
}
if not monitors:
text = "📺 Мониторинг\n\nЗадач нет."
else:
text = "📺 Мониторинг\n\n"
for _, p in list(monitors.items())[:8]:
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
text += (
f"{p.get_status_emoji()} {p.channel}{domains}\n"
f" 🔗 {p.links_found} | ✅ {p.successful_visits} | 📊 {p.total_visits}\n"
)
builder = InlineKeyboardBuilder()
self._build_task_buttons(builder, monitors)
await self._edit_or_send(message, text, builder.as_markup(), edit)
async def _process_user_task_input(self, message: Message):
user_id = message.from_user.id
state = self._user_task_state[user_id]
text = message.text.strip() if message.text else ""
if not text:
return
await self._safe_delete(message.bot, message.chat.id, message.message_id)
if state["step"] == "channel":
if not re.match(r'^[a-zA-Z0-9_]+$', text):
await message.answer("❌ Неверное название канала (только буквы, цифры, _). Попробуйте ещё раз:")
return
channel = text.lower()
# Проверка на дубликат
all_tasks = await self.task_manager.get_all_tasks()
duplicate = any(
p.user_id == user_id and p.channel == channel and not p.completed and not p.stopped
for p in all_tasks.values()
)
if duplicate:
await self._send_temp(
message,
f"❌ У вас уже есть активная задача для канала `{channel}`.\nВведите другой канал:"
)
return
# Удаляем промпт шага 1 перед отправкой шага 2
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] = {"step": "domains", "channel": channel}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
sent = await message.answer(
f"📺 Канал: `{channel}`\n\n"
"Шаг 2/2: Введите домены через запятую:\n"
"Пример: twitch.tv, youtube.com",
reply_markup=builder.as_markup()
)
self._menu_msg[user_id] = sent.message_id
elif state["step"] == "url":
if not (text.startswith("http://") or text.startswith("https://")):
await self._send_temp(message, "Ссылка должна начинаться с http:// или https://")
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] = {"step": "count", "url": text}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
sent = await message.answer(
f"🔗 {text[:50]}\n\nШаг 2/4 — введите количество переходов:",
reply_markup=builder.as_markup()
)
self._menu_msg[user_id] = sent.message_id
elif state["step"] == "count":
try:
count = int(text)
if count < 1:
raise ValueError
except ValueError:
await self._send_temp(message, "Введите целое число больше 0")
return
balance = await self.balance_storage.get_balance(user_id)
if balance <= 0:
await self._send_temp(message, "Недостаточно переходов")
return
if count > balance:
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/4 — сколько кликов в одном цикле?\n\n"
"Бот делает N кликов подряд, затем ждёт паузу и начинает следующий цикл.\n\n"
"Диапазон `2-5` — каждый цикл будет случайной длины от 2 до 5.",
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
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": "delay", "min_series": mn, "max_series": mx}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
series_label = f"{mn}{mx}" if mn != mx else str(mn)
sent = await message.answer(
f"✅ Цикл: {series_label} кликов\n\n"
"Шаг 4/4 — таймаут между циклами (в минутах)\n\n"
"`2-5` — случайно от 2 до 5 минут.",
reply_markup=builder.as_markup()
)
self._menu_msg[user_id] = sent.message_id
elif state["step"] == "delay":
try:
if "-" in text:
mn_d, mx_d = parse_range(text, default_min=1)
else:
mn_d = mx_d = int(text)
if mn_d < 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"]
mn = state["min_series"]
mx = state["max_series"]
series_label = f"{mn}{mx}" if mn != mx else str(mn)
delay_label = f"{mn_d}{mx_d}" if mn_d != mx_d else str(mn_d)
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=mn_d * 60,
max_delay=mx_d * 60,
min_reading=settings.DEFAULT_MIN_READING,
max_reading=settings.DEFAULT_MAX_READING,
started_at=datetime.now(),
chat_id=message.chat.id,
)
await self.task_manager.add_task(task_id, params)
success = await self.background_tasks.start_task(
task_id=task_id,
coro=self._run_url_visits(task_id, params, message.bot),
task_type="user_visit",
metadata={"type": "user_visit", "url": url, "chat_id": message.chat.id},
)
if success:
if self.storage:
await self.storage.save_task(task_id, params)
await self._send_temp(
message,
f"✅ Задача создана\n🔗 {url[:50]}\n"
f"🔢 Переходов: {count} | Цикл: {series_label} кликов\n"
f"⏱ Пауза между циклами: {delay_label} мин",
delay=6
)
await self._show_user_menu(message, user_id=user_id)
else:
await self._send_temp(message, "Ошибка запуска задачи")
elif state["step"] in ("domains", "edit_domains"):
domains = [d.strip().lower() for d in text.split(",") if d.strip()]
if not domains:
await self._send_temp(message, "❌ Введите хотя бы один домен. Пример: `twitch.tv, example.com`")
return
invalid = [d for d in domains if not re.match(r'^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', d)]
if invalid:
await self._send_temp(message, f"❌ Неверный формат: `{', '.join(invalid)}`\nПример: `twitch.tv, example.com`")
return
del self._user_task_state[user_id]
# Редактирование доменов существующей задачи
if state["step"] == "edit_domains":
task_id = state["task_id"]
await self._clean_prev(message)
await self.task_manager.update_task(task_id, allowed_domains=domains)
if self.storage:
params = await self.task_manager.get_task(task_id)
if params:
await self.storage.save_task(task_id, params)
await self._send_temp(message, f"✅ Домены обновлены: {', '.join(domains)}", delay=4)
await self._show_user_tasks(message, user_id)
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] = {
"step": "percent",
"channel": state["channel"],
"domains": domains,
}
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⏭️ Без процента", callback_data="skip_percent"))
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
sent = await message.answer(
f"📺 Канал: {state['channel']}\n"
f"🌐 Домены: {', '.join(domains)}\n\n"
"Шаг 3/3 — введите % от зрителей для расчёта переходов на ссылку:\n"
"(например: 5 = 5% от текущих зрителей)\n"
"Или нажмите «Без процента» для фиксированного значения",
reply_markup=builder.as_markup()
)
self._menu_msg[user_id] = sent.message_id
elif state["step"] == "percent":
try:
percent = float(text.replace(",", "."))
if percent <= 0 or percent > 100:
raise ValueError
except ValueError:
await self._send_temp(message, "Введите число от 0.1 до 100")
return
await self._clean_prev(message)
del self._user_task_state[user_id]
await self._create_twitch_task(message, user_id, state["channel"], state["domains"], percent)
return
# =========================================================================
# ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN)
# =========================================================================
# =========================================================================
# КАБИНЕТ / МАГАЗИН
# =========================================================================
async def _process_payment_input(self, message: Message):
user_id = message.from_user.id
action = self._payment_state.pop(user_id)
await self._safe_delete(message.bot, message.chat.id, message.message_id)
text = message.text.strip() if message.text else ""
prompt_id = self._menu_msg.pop(user_id, None)
async def _edit_prompt(new_text: str, markup=None):
if prompt_id:
try:
await message.bot.edit_message_text(
chat_id=message.chat.id, message_id=prompt_id,
text=new_text, reply_markup=markup
)
return
except Exception:
pass
await message.answer(new_text, reply_markup=markup)
try:
value = int(text)
except (ValueError, TypeError):
await self._send_temp(message, "❌ Введите целое число")
self._payment_state[user_id] = action
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
if action == "topup":
if value < settings.TOPUP_MIN_RUB:
await self._send_temp(message, f"❌ Минимум {settings.TOPUP_MIN_RUB}")
self._payment_state[user_id] = "topup"
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
if value > settings.TOPUP_MAX_RUB:
await self._send_temp(message, f"❌ Максимум {settings.TOPUP_MAX_RUB}")
self._payment_state[user_id] = "topup"
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
order_id = f"{user_id}_topup_{int(datetime.now().timestamp())}"
invoice = await self.heleket.create_invoice(value, order_id)
if not invoice:
await self._send_temp(message, "❌ Ошибка создания счёта. Попробуйте позже.")
return
payment_data = {
"user_id": user_id,
"payment_id": invoice["payment_id"],
"rub": value,
"address": invoice.get("address"),
"url": invoice.get("url"),
"mock": invoice.get("mock", False),
"created_at": datetime.now().isoformat(),
}
await self.payment_storage.save(invoice["payment_id"], payment_data)
builder = InlineKeyboardBuilder()
if invoice.get("url"):
builder.row(InlineKeyboardButton(text="💳 Оплатить через Heleket", url=invoice["url"]))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
await _edit_prompt(
f"💳 Счёт на оплату\n\n"
f"💰 Сумма: {value}\n\n"
f"Нажмите кнопку для перехода на страницу оплаты.\n"
f"После оплаты баланс пополнится автоматически.\n\n"
f"⏱ Счёт действителен 60 минут",
builder.as_markup()
)
elif action == "buy_clicks":
if value < settings.CLICKS_MIN:
await self._send_temp(message, f"Минимум {settings.CLICKS_MIN} переходов")
self._payment_state[user_id] = "buy_clicks"
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
cost = int(value * settings.CLICK_PRICE_RUB)
# Сохраняем для шага подтверждения
self._payment_confirm[user_id] = {"clicks": value, "rub": cost}
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="✅ Подтвердить", callback_data="confirm_buy"),
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_confirm_buy"),
)
await _edit_prompt(
f"🛒 Подтвердите покупку\n\n"
f"🔢 {value} переходов\n"
f"💰 Стоимость: {cost}",
builder.as_markup()
)
async def _show_invoice(self, callback: CallbackQuery, payment: dict):
"""Отображает счёт на оплату."""
builder = InlineKeyboardBuilder()
if payment.get("url"):
builder.row(InlineKeyboardButton(text="💳 Оплатить через Heleket", url=payment["url"]))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
body = (
f"Нажмите кнопку для перехода на страницу оплаты.\n"
f"После оплаты баланс пополнится автоматически."
)
await callback.message.edit_text(
f"💳 Счёт на оплату\n\n"
f"💰 Сумма: {payment['rub']}\n\n"
f"{body}\n\n"
f"⏱ Счёт действителен 60 минут",
reply_markup=builder.as_markup()
)
# =========================================================================
# ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN)
# =========================================================================
async def _create_twitch_task(
self, message: Message, user_id: int,
channel: str, domains: list, visits_percent: float
):
"""Создаёт twitch_irc задачу и запускает её."""
balance = await self.balance_storage.get_balance(user_id)
if balance <= 0:
await self._send_temp(message, "Недостаточно переходов")
return
# Проверяем статус стрима до создания задачи (None = ошибка запроса → считаем онлайн)
viewers = await get_viewer_count(channel)
is_offline = (viewers == 0) # None → не 0 → False → онлайн
task_id = f"u{user_id}_{channel}_{datetime.now().strftime('%H%M%S')}"
params = TaskParams(
task_type="twitch_irc",
channel=channel,
target_username="*",
allowed_domains=domains,
user_id=user_id,
visits_per_link=settings.DEFAULT_VISITS_PER_LINK,
visits_percent=visits_percent,
min_ctr=visits_percent if visits_percent > 0 else 0.8,
max_ctr=visits_percent if visits_percent > 0 else 1.0,
monitor_minutes=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,
started_at=datetime.now(),
chat_id=message.chat.id,
stream_offline=is_offline,
)
await self.task_manager.add_task(task_id, params)
success = await self.background_tasks.start_task(
task_id=task_id,
coro=self._run_twitch(task_id, 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(task_id, params)
percent_info = f"\n📊 {visits_percent}% от зрителей" if visits_percent > 0 else ""
status = f"⏸ Стрим оффлайн — задача на паузе" if is_offline else f"▶️ Онлайн: {viewers} зрителей"
await self._send_temp(
message,
f"✅ Задача создана\n"
f"📺 {channel} | 🌐 {', '.join(domains)}\n"
f"{status}{percent_info}",
delay=6
)
await self._show_user_menu(message, user_id=user_id)
else:
await self._send_temp(message, "Ошибка запуска задачи")
async def _show_users_list(self, message: Message, edit: bool = False):
balances = await self.balance_storage.get_all()
all_tasks = await self.task_manager.get_all_tasks()
text = "👥 Пользователи\n\n"
builder = InlineKeyboardBuilder()
if not balances:
text += "Нет пользователей с балансом."
else:
for uid_str, balance in list(balances.items())[:15]:
uid = int(uid_str)
active = sum(
1 for p in all_tasks.values()
if p.user_id == uid and not p.completed and not p.stopped
)
text += f"👤 `{uid}` — 💰 {balance} | 📋 {active} задач\n"
builder.row(InlineKeyboardButton(
text=f"👤 {uid} (💰{balance})",
callback_data=f"udetail_{uid}",
))
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
await self._edit_or_send(message, text, builder.as_markup(), edit)
async def _show_user_detail(self, callback: CallbackQuery, target_user_id: int):
balance = await self.balance_storage.get_balance(target_user_id)
all_tasks = await self.task_manager.get_all_tasks()
user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == target_user_id}
active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped)
text = (
f"👤 Пользователь {target_user_id}\n\n"
f"🔢 Переходов: {balance}\n"
f"📋 Задач: {len(user_tasks)} (активных: {active})\n\n"
"Пополнить баланс:"
)
builder = InlineKeyboardBuilder()
for amount in [10, 50, 100, 500]:
builder.button(text=f"+{amount}", callback_data=f"ubal_{amount}_{target_user_id}")
builder.adjust(4)
builder.row(InlineKeyboardButton(
text="✏️ Другая сумма",
callback_data=f"ubal_custom_{target_user_id}",
))
if user_tasks:
builder.row(InlineKeyboardButton(
text=f"📋 Задачи ({len(user_tasks)})",
callback_data=f"utasks_{target_user_id}",
))
builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data="menu_users"))
try:
await callback.message.edit_text(text, reply_markup=builder.as_markup())
except Exception:
await callback.message.answer(text, reply_markup=builder.as_markup())