3222 lines
158 KiB
Python
3222 lines
158 KiB
Python
"""
|
||
Обработчики команд с удобным интерфейсом управления задачами.
|
||
"""
|
||
|
||
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.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, fmt_price
|
||
from utils.telegram import send_message_safe, send_visit_result
|
||
from managers.storage import TaskStorage, ChatStorage, BalanceStorage, RubleBalanceStorage, PaymentStorage, PaymentHistoryStorage
|
||
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,
|
||
browser_service: BrowserService,
|
||
browser_pool: BrowserPool = None,
|
||
storage: TaskStorage = None,
|
||
chat_storage: ChatStorage = None,
|
||
bot_ref=None,
|
||
):
|
||
self.background_tasks = background_tasks
|
||
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.payment_history = PaymentHistoryStorage()
|
||
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._admin_rub_state: Dict[int, int] = {} # admin_id -> target_user_id
|
||
self._admin_msg_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 == "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)
|
||
params = await interface.task_manager.get_task(tid)
|
||
is_visit = params and params.task_type in ("visit", "user_visit")
|
||
await interface.task_manager.remove_task(tid)
|
||
await callback.answer("🗑️ Удалена")
|
||
if is_visit:
|
||
await interface._show_tasks_list(callback.message, edit=True)
|
||
else:
|
||
await interface._show_streamers_list(callback.message, edit=True)
|
||
|
||
|
||
@dp.callback_query(F.data.startswith("treset_"))
|
||
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()
|
||
|
||
@dp.callback_query(F.data == "payment_history")
|
||
async def cb_payment_history(callback: CallbackQuery):
|
||
uid = callback.from_user.id
|
||
history = await interface.payment_history.get_user_history(uid)
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
|
||
|
||
if not history:
|
||
text = "🧾 История платежей\n\nПлатежей пока нет."
|
||
else:
|
||
lines = ["🧾 История платежей\n"]
|
||
for r in history[:10]:
|
||
dt = r.get("at", "")[:16].replace("T", " ")
|
||
if r.get("type") == "clicks":
|
||
lines.append(f"🖱 {r['clicks']} переходов — {r['rub']} ₽ ({dt})")
|
||
else:
|
||
lines.append(f"💳 Пополнение {r['rub']} ₽ ({dt})")
|
||
text = "\n".join(lines)
|
||
|
||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||
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,
|
||
"msg_id": callback.message.message_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=f"🛒 Купить переходы ({fmt_price(settings.CLICK_PRICE_RUB)} ₽/шт)", 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"Цена: {fmt_price(price)} ₽ за 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} переходов × {fmt_price(settings.CLICK_PRICE_RUB)} ₽ = {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"Цена: {fmt_price(settings.CLICK_PRICE_RUB)} ₽ за 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,
|
||
"msg_id": callback.message.message_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} переходов × {fmt_price(settings.CLICK_PRICE_RUB)} ₽ = {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_twitch_ctr")
|
||
async def cb_skip_twitch_ctr(callback: CallbackQuery):
|
||
uid = callback.from_user.id
|
||
state = interface._user_task_state.pop(uid, None)
|
||
if not state or state.get("step") != "twitch_ctr":
|
||
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"],
|
||
min_ctr=0.8, max_ctr=1.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("umsg_"))
|
||
async def cb_admin_msg(callback: CallbackQuery):
|
||
if not await require_admin(callback):
|
||
return
|
||
target_uid = int(callback.data.replace("umsg_", "", 1))
|
||
interface._admin_msg_state[callback.from_user.id] = target_uid
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}"))
|
||
await callback.message.edit_text(
|
||
f"✉️ Сообщение пользователю {target_uid}\n\nВведите текст:",
|
||
reply_markup=builder.as_markup()
|
||
)
|
||
await callback.answer()
|
||
|
||
@dp.callback_query(F.data == "dismiss_msg")
|
||
async def cb_dismiss_msg(callback: CallbackQuery):
|
||
try:
|
||
await callback.message.delete()
|
||
except Exception:
|
||
pass
|
||
await callback.answer()
|
||
|
||
@dp.callback_query(F.data.startswith("ubal_menu_"))
|
||
async def cb_user_balance_menu(callback: CallbackQuery):
|
||
if not await require_admin(callback):
|
||
return
|
||
target_uid = int(callback.data.replace("ubal_menu_", "", 1))
|
||
builder = InlineKeyboardBuilder()
|
||
for amount in [10, 50, 100, 500]:
|
||
builder.button(text=f"+{amount}", callback_data=f"ubal_{amount}_{target_uid}")
|
||
builder.adjust(4)
|
||
builder.row(InlineKeyboardButton(text="✏️ Другая сумма", callback_data=f"ubal_custom_{target_uid}"))
|
||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"udetail_{target_uid}"))
|
||
clicks = await interface.balance_storage.get_balance(target_uid)
|
||
await callback.message.edit_text(
|
||
f"👤 {target_uid} — переходов: {clicks}\n\nДобавить переходов:",
|
||
reply_markup=builder.as_markup()
|
||
)
|
||
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("urub_menu_"))
|
||
async def cb_user_rub_menu(callback: CallbackQuery):
|
||
if not await require_admin(callback):
|
||
return
|
||
target_uid = int(callback.data.replace("urub_menu_", "", 1))
|
||
builder = InlineKeyboardBuilder()
|
||
for amount in [100, 500, 1000, 3000]:
|
||
builder.button(text=f"+{amount}₽", callback_data=f"urub_{amount}_{target_uid}")
|
||
builder.adjust(4)
|
||
builder.row(InlineKeyboardButton(text="✏️ Другая сумма", callback_data=f"urub_custom_{target_uid}"))
|
||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"udetail_{target_uid}"))
|
||
rub = await interface.rub_storage.get_balance(target_uid)
|
||
await callback.message.edit_text(
|
||
f"👤 {target_uid} — рублей: {rub} ₽\n\nДобавить рублей:",
|
||
reply_markup=builder.as_markup()
|
||
)
|
||
await callback.answer()
|
||
|
||
@dp.callback_query(F.data.startswith("urub_"))
|
||
async def cb_user_rub(callback: CallbackQuery):
|
||
if not await require_admin(callback):
|
||
return
|
||
parts = callback.data.split("_")
|
||
# формат: urub_{amount}_{user_id} или urub_custom_{user_id}
|
||
if parts[1] == "custom":
|
||
target_uid = int(parts[2])
|
||
interface._admin_rub_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.rub_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._admin_msg_state:
|
||
if not text:
|
||
return
|
||
target_uid = interface._admin_msg_state.pop(user_id)
|
||
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="✅ Понял", callback_data="dismiss_msg"))
|
||
try:
|
||
await message.bot.send_message(
|
||
target_uid,
|
||
f"📢 Сообщение от администратора:\n\n{text}",
|
||
reply_markup=builder.as_markup()
|
||
)
|
||
await message.answer(f"✅ Сообщение отправлено пользователю {target_uid}")
|
||
except Exception as e:
|
||
await message.answer(f"❌ Не удалось отправить: {e}")
|
||
return
|
||
|
||
# Ввод пароля (неавторизованный пользователь ждёт пароль)
|
||
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
|
||
|
||
# Ввод рублёвого пополнения (admin)
|
||
if user_id in interface._admin_rub_state:
|
||
target_uid = interface._admin_rub_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.rub_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):
|
||
"""Все задачи — посещения и мониторинг."""
|
||
active = await self.task_manager.get_active_tasks()
|
||
completed = await self.task_manager.get_completed_tasks()
|
||
|
||
visit_active = {tid: p for tid, p in active.items() if p.task_type in ("visit", "user_visit")}
|
||
visit_done = {tid: p for tid, p in completed.items() if p.task_type in ("visit", "user_visit")}
|
||
twitch_active = {tid: p for tid, p in active.items() if p.task_type == "twitch_irc"}
|
||
twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"}
|
||
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
if not any([visit_active, visit_done, twitch_active, twitch_done]):
|
||
text = "📊 Задачи\n\nЗадач нет."
|
||
else:
|
||
text = "📊 Задачи\n\n"
|
||
|
||
if twitch_active:
|
||
text += "📺 Twitch — активные:\n"
|
||
for tid, p in list(twitch_active.items())[:10]:
|
||
em = p.get_status_emoji()
|
||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||
text += f"{em} {p.channel}\n 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"{em} 📺 {p.channel} (🔗{p.links_found})",
|
||
callback_data=f"tdetail_{tid}"
|
||
))
|
||
|
||
if twitch_done:
|
||
text += "\n📺 Twitch — завершённые:\n"
|
||
for tid, p in list(twitch_done.items())[:5]:
|
||
em = p.get_status_emoji()
|
||
text += f"{em} {p.channel}\n"
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"{em} 📺 {p.channel} (завершена)",
|
||
callback_data=f"tdetail_{tid}"
|
||
))
|
||
|
||
if visit_active:
|
||
text += "\n🔗 Посещения — активные:\n"
|
||
for tid, p in list(visit_active.items())[:10]:
|
||
em = p.get_status_emoji()
|
||
url_s = (p.url or "")[:35]
|
||
uid_tag = f" | uid:{p.user_id}" if p.user_id else ""
|
||
text += f"{em} {url_s}{uid_tag}\n ✅{p.successful_visits}/{p.max_visits} | 📊{p.total_visits}\n"
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"{em} 🔗 {url_s[:28]} ({p.successful_visits}/{p.max_visits})",
|
||
callback_data=f"tdetail_{tid}"
|
||
))
|
||
|
||
if visit_done:
|
||
text += "\n🔗 Посещения — завершённые:\n"
|
||
for tid, p in list(visit_done.items())[:5]:
|
||
em = p.get_status_emoji()
|
||
url_s = (p.url or "")[:35]
|
||
uid_tag = f" | uid:{p.user_id}" if p.user_id else ""
|
||
text += f"{em} {url_s}{uid_tag}\n"
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"{em} 🔗 {url_s[:28]} (завершена)",
|
||
callback_data=f"tdetail_{tid}"
|
||
))
|
||
|
||
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_tasks"))
|
||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||
|
||
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()
|
||
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"🔁 Фоновых задач: {bg_count}\n\n"
|
||
f"{sys_info}"
|
||
)
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_status"))
|
||
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 _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}")
|
||
)
|
||
|
||
back_target = "menu_tasks" if params.task_type in ("visit", "user_visit") else "menu_streamers"
|
||
builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data=back_target))
|
||
|
||
try:
|
||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||
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.gql_chat_service import TwitchGQLChatClient
|
||
|
||
irc = TwitchGQLChatClient(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, task_id, 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"⚠️ GQL #{params.channel}: ошибка соединения (3 попытки) — задача поставлена на паузу"
|
||
)
|
||
|
||
logger.info(f"🚀 GQL chat 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 = params.remaining_clicks # восстанавливаем из сохранённого состояния
|
||
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
|
||
|
||
# --- Серия кликов ---
|
||
loop = asyncio.get_event_loop()
|
||
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
|
||
|
||
# Фиксируем старт тика до посещения
|
||
is_last = (click_num == series_size - 1)
|
||
click_delay = 0 if is_last else params.get_click_delay()
|
||
tick_start = loop.time()
|
||
|
||
try:
|
||
visitor = self.browser_pool or self.browser_service
|
||
result = await visitor.visit_page(params.url, params.get_reading_time())
|
||
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]}")
|
||
if self.storage:
|
||
await self.storage.save_task(task_id, params)
|
||
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
|
||
params.remaining_clicks = remaining_clicks
|
||
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 not is_last and params.successful_visits < max_v:
|
||
elapsed = loop.time() - tick_start
|
||
remaining_wait = int(click_delay - elapsed)
|
||
if remaining_wait > 0:
|
||
await _sleep_interruptible(remaining_wait)
|
||
|
||
await self.task_manager.complete_task(task_id)
|
||
remaining = await self.balance_storage.get_balance(params.user_id)
|
||
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, task_id: 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}")
|
||
notif_msg = 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
|
||
successful_this_link = 0
|
||
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.error == "PROXY_UNAVAILABLE":
|
||
params.paused = True
|
||
params.auto_paused = True
|
||
await self.task_manager.update_task(task_id, paused=True)
|
||
await send_message_safe(
|
||
message.bot, params.chat_id,
|
||
"⚠️ Прокси недоступен — задача поставлена на паузу"
|
||
)
|
||
return
|
||
if result.success:
|
||
params.successful_visits += 1
|
||
successful_this_link += 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
|
||
|
||
# Серия завершена — удаляем уведомление
|
||
if notif_msg:
|
||
try:
|
||
await message.bot.delete_message(
|
||
chat_id=message.chat.id,
|
||
message_id=notif_msg.message_id,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
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=f"🛒 Купить переходы ({fmt_price(settings.CLICK_PRICE_RUB)} ₽/шт)", callback_data="visit_shop"),
|
||
)
|
||
builder.row(InlineKeyboardButton(text="🧾 История платежей", callback_data="payment_history"))
|
||
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"
|
||
"Введите минимальное и максимальное время ожидания между циклами кликов\n"
|
||
"Пример `1-100` — случайно от 1 до 100 минут.",
|
||
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
|
||
|
||
# Новая задача — идём к шагу ввода CTR
|
||
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": "twitch_ctr",
|
||
"channel": state["channel"],
|
||
"domains": domains,
|
||
}
|
||
builder = InlineKeyboardBuilder()
|
||
builder.row(InlineKeyboardButton(text="⏭️ По умолчанию", callback_data="skip_twitch_ctr"))
|
||
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
|
||
sent = await message.answer(
|
||
f"📺 Канал: {state['channel']}\n"
|
||
f"🌐 Домены: {', '.join(domains)}\n\n"
|
||
"Шаг 3/3 — CTR\n\n"
|
||
"Введите минимальный и максимальный % от зрителей для расчёта переходов на ссылку\n"
|
||
"(например: `0.5-1.5` % от текущих зрителей)",
|
||
reply_markup=builder.as_markup()
|
||
)
|
||
self._menu_msg[user_id] = sent.message_id
|
||
|
||
elif state["step"] == "twitch_ctr":
|
||
try:
|
||
parts = text.replace(",", ".").split("-")
|
||
if len(parts) == 2:
|
||
min_ctr, max_ctr = float(parts[0].strip()), float(parts[1].strip())
|
||
elif len(parts) == 1:
|
||
min_ctr = max_ctr = float(parts[0].strip())
|
||
else:
|
||
raise ValueError
|
||
if min_ctr <= 0 or max_ctr > 100 or min_ctr > max_ctr:
|
||
raise ValueError
|
||
except (ValueError, Exception):
|
||
await self._send_temp(message, "❌ Введите диапазон `0.5-1.5` или одно число `1`")
|
||
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"],
|
||
min_ctr=min_ctr, max_ctr=max_ctr
|
||
)
|
||
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}
|
||
rub_bal = await self.rub_storage.get_balance(user_id)
|
||
|
||
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 _edit_prompt(
|
||
f"🛒 Подтвердите покупку\n\n"
|
||
f"🔢 {value} переходов × {fmt_price(settings.CLICK_PRICE_RUB)} ₽ = {cost} ₽"
|
||
+ (f"\n\n🪙 Ваш баланс: {rub_bal} ₽" if rub_bal > 0 else ""),
|
||
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,
|
||
min_ctr: float = 0.8, max_ctr: float = 1.0,
|
||
):
|
||
"""Создаёт twitch_irc задачу и запускает её."""
|
||
balance = await self.balance_storage.get_balance(user_id)
|
||
if balance <= 0:
|
||
await self._send_temp(message, "Недостаточно переходов")
|
||
return
|
||
|
||
viewers = await get_viewer_count(channel)
|
||
is_offline = (viewers == 0)
|
||
|
||
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,
|
||
min_ctr=min_ctr,
|
||
max_ctr=max_ctr,
|
||
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)
|
||
ctr_str = f"{min_ctr}%" if min_ctr == max_ctr else f"{min_ctr}–{max_ctr}%"
|
||
status = f"⏸ Стрим оффлайн — задача на паузе" if is_offline else f"▶️ Онлайн: {viewers} зрителей"
|
||
await self._send_temp(
|
||
message,
|
||
f"✅ Задача создана\n"
|
||
f"📺 {channel} | 🌐 {', '.join(domains)}\n"
|
||
f"📊 CTR: {ctr_str} от зрителей\n"
|
||
f"{status}",
|
||
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):
|
||
all_user_ids, all_tasks, all_clicks, all_rub = await asyncio.gather(
|
||
self.auth_storage.get_all_user_ids(),
|
||
self.task_manager.get_all_tasks(),
|
||
self.balance_storage.get_all(),
|
||
self.rub_storage.get_all(),
|
||
)
|
||
|
||
text = f"👥 Пользователи ({len(all_user_ids)})\n\n"
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
if not all_user_ids:
|
||
text += "Нет зарегистрированных пользователей."
|
||
else:
|
||
for uid in all_user_ids[:20]:
|
||
clicks = all_clicks.get(str(uid), 0)
|
||
rub = all_rub.get(str(uid), 0)
|
||
active = sum(
|
||
1 for p in all_tasks.values()
|
||
if p.user_id == uid and not p.completed and not p.stopped
|
||
)
|
||
bal_str = f"🖱{clicks}"
|
||
if rub:
|
||
bal_str += f" 🪙{rub}₽"
|
||
if active:
|
||
bal_str += f" 📋{active}"
|
||
text += f"👤 `{uid}` — {bal_str}\n"
|
||
builder.row(InlineKeyboardButton(
|
||
text=f"👤 {uid} {bal_str}",
|
||
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):
|
||
clicks = await self.balance_storage.get_balance(target_user_id)
|
||
rub = await self.rub_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"🖱 Переходов: {clicks}\n"
|
||
f"🪙 Рублей: {rub} ₽\n"
|
||
f"📋 Задач: {len(user_tasks)} (активных: {active})\n"
|
||
)
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
builder.row(InlineKeyboardButton(text="➕ Переходы", callback_data=f"ubal_menu_{target_user_id}"))
|
||
builder.row(InlineKeyboardButton(text="➕ Рубли", callback_data=f"urub_menu_{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=f"umsg_{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())
|
||
|
||
|
||
|