fix command ana params

This commit is contained in:
Yuriy Yuriev
2026-05-18 18:35:09 +07:00
parent 2f163aaa3e
commit ca8a386f82
5 changed files with 430 additions and 95 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(pip install *)",
"Bash(python -c \"import psutil; print\\('psutil ok, version:', psutil.__version__\\)\")",
"Bash(python -c ' *)"
]
}
}
+362 -77
View File
@@ -4,6 +4,8 @@
import asyncio import asyncio
import logging import logging
import os
import random
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional, Dict from typing import Optional, Dict
@@ -71,6 +73,7 @@ class BotInterface:
self._chat_history: Dict[int, list] = {} self._chat_history: Dict[int, list] = {}
self._bot_ref = bot_ref self._bot_ref = bot_ref
self._waiting_password: Dict[int, str] = {} # user_id -> "register" or "login" self._waiting_password: Dict[int, str] = {} # user_id -> "register" or "login"
self._start_time = datetime.now()
async def _update_commands(self): async def _update_commands(self):
if self._bot_ref: if self._bot_ref:
@@ -251,6 +254,31 @@ class BotInterface:
await interface._show_status(callback.message, edit=True) await interface._show_status(callback.message, edit=True)
await callback.answer() 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.message(F.text == "🛑 Остановить всё")
@dp.callback_query(F.data == "stop_all") @dp.callback_query(F.data == "stop_all")
@@ -405,11 +433,42 @@ class BotInterface:
} }
prompts = { prompts = {
"delay": "⏱️ Задержка (сек): `10-30` или `15`", "delay": (
"reading": "📖 Чтение (сек): `10-30` или `20`", "⏱️ *Пауза между переходами*\n"
"perlink": "🔗 Посещений на ссылку: `5`", "Сколько секунд ждать перед следующим кликом.\n"
"minutes": "⏰ Длительность (мин): `30`", "Диапазон `30-60` — каждый раз рандом от 30 до 60.\n"
"domains": "🌐 Домены через запятую: `site1.com, site2.ru`\nИли `all` для всех", "Одно число `45` — всегда фиксированно."
),
"reading": (
"📖 *Время на странице*\n"
"Сколько секунд браузер будет листать страницу (имитация чтения).\n"
"Диапазон `45-90` или фиксированно `60`."
),
"perlink": (
"🔗 *Резервное кол-во кликов*\n"
"Используется только если Twitch GQL недоступен.\n"
"В норме: клики = зрители × CTR.\n"
"Пример: `5`"
),
"ctr": (
"📊 *CTR — процент от зрителей*\n"
"Формула: клики = зрители × CTR%.\n"
"Например: 120 зрителей × 1% = 1 клик.\n"
"Диапазон `0.8-1` — случайно в этом диапазоне.\n"
"Одно число `1` — фиксированно."
),
"minutes": (
"⏰ *Длительность мониторинга*\n"
"Как долго следить за чатом стримера (в минутах).\n"
"`0` — мониторить бесконечно.\n"
"Пример: `30`"
),
"domains": (
"🌐 *Разрешённые домены*\n"
"Кликать только по ссылкам с этих сайтов.\n"
"Несколько через запятую: `twitch.tv, youtube.com`\n"
"`all` — любые ссылки без фильтра."
),
} }
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
@@ -748,11 +807,66 @@ class BotInterface:
await callback.answer("Задача не найдена", show_alert=True) await callback.answer("Задача не найдена", show_alert=True)
return 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` или фиксированно `3`."
)
delay_label = (
f"⏱️ *Пауза между сериями*\n"
f"Сколько секунд ждать после каждой серии кликов.\n"
f"Текущая: {params.min_delay}{params.max_delay} сек\n"
f"Диапазон `30-60` или фиксированно `45`."
)
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` или фиксированно `45`."
)
prompts = { prompts = {
"delay": f"Задержка (сек): например 30-60 или 45\nТекущая: {params.min_delay}-{params.max_delay}", "delay": delay_label,
"reading": f"Время чтения (сек): например 45-90 или 60\nТекущее: {params.min_reading}-{params.max_reading}", "reading": (
"percent": f"% зрителей (0 = отключить): например 5\nТекущее: {params.visits_percent}%", f"📖 *Время на странице*\n"
"perlink": f"Кол-во переходов на ссылку: например 3\nТекущее: {params.visits_per_link}", f"Сколько секунд браузер листает страницу (имитация чтения).\n"
f"Текущее: {params.min_reading}{params.max_reading} сек\n"
f"Диапазон `45-90` или фиксированно `60`."
),
"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` или фиксированно `10`."
),
"ctr": (
f"📊 *CTR — процент от зрителей*\n"
f"Формула: клики = зрители × CTR%.\n"
f"Текущий: {params.min_ctr}%{params.max_ctr}%\n"
f"Диапазон `0.8-1` или фиксированно `1`."
),
} }
prompt = prompts.get(param, f"Введите значение для {param}:") prompt = prompts.get(param, f"Введите значение для {param}:")
@@ -1187,7 +1301,7 @@ class BotInterface:
params = TaskParams( params = TaskParams(
task_type="twitch_irc", task_type="twitch_irc",
channel=channel, channel=channel,
target_username="*", # Все пользователи target_username="*",
visits_per_link=vl, visits_per_link=vl,
monitor_minutes=mm, monitor_minutes=mm,
min_delay=mind, max_delay=maxd, min_delay=mind, max_delay=maxd,
@@ -1216,7 +1330,7 @@ class BotInterface:
f"✅ Мониторинг запущен\n\n" f"✅ Мониторинг запущен\n\n"
f"📺 Канал: `{channel}`\n" f"📺 Канал: `{channel}`\n"
f"👤 Отслеживание: все пользователи\n" f"👤 Отслеживание: все пользователи\n"
f"🔗 На ссылку: {vl} визитов\n" f"🔗 На ссылку: {vl} кликов\n"
f"⏱️ Мониторинг: {mm} мин\n" f"⏱️ Мониторинг: {mm} мин\n"
f"⏱️ Задержка: {format_range(mind, maxd)}с\n" f"⏱️ Задержка: {format_range(mind, maxd)}с\n"
f"📖 Чтение: {format_range(minr, maxr)}с\n" f"📖 Чтение: {format_range(minr, maxr)}с\n"
@@ -1454,23 +1568,66 @@ class BotInterface:
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
await self._edit_or_send(message, text, builder.as_markup(), edit) 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): async def _show_status(self, message: Message, edit: bool = False):
"""Статус."""
stats = await self.task_manager.get_stats() stats = await self.task_manager.get_stats()
proxy_stats = self.proxy_manager.get_stats() proxy_stats = self.proxy_manager.get_stats()
bg_count = self.background_tasks.active_count
delta = datetime.now() - self._start_time
h, rem = divmod(int(delta.total_seconds()), 3600)
m, s = divmod(rem, 60)
uptime = f"{h}ч {m}м {s}с" if h else f"{m}м {s}с"
sys_info = self._get_sys_info()
text = ( text = (
"📈 Статус\n\n" "📈 *Статус бота*\n\n"
f"📺 Twitch: {stats['twitch']}\n" f"⏱️ Аптайм: {uptime}\n\n"
f"🌐 Визиты: {stats['visit']}\n" f"📊 *Задачи:*\n"
f"⏸️ Пауза: {stats['paused']}\n" f"├ 📺 Twitch: {stats['twitch']}\n"
f"🔄 Активно: {stats['active']}\n\n" f"├ 🌐 Визиты: {stats['visit']}\n"
f"🔌 Прокси: {proxy_stats['total']} всего, {proxy_stats['available']} доступно" f"├ ⏸️ Пауза: {stats['paused']}\n"
f"└ 🔄 Активно: {stats['active']}\n\n"
f"🔌 Прокси: {proxy_stats['total']} / {proxy_stats['available']} доступно\n"
f"🔁 Фоновых задач: {bg_count}\n\n"
f"{sys_info}"
) )
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_status")) 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")) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
await self._edit_or_send(message, text, builder.as_markup(), edit) 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)
# ========================================================================= # =========================================================================
# УПРАВЛЕНИЕ ЗАДАЧЕЙ # УПРАВЛЕНИЕ ЗАДАЧЕЙ
# ========================================================================= # =========================================================================
@@ -1493,15 +1650,16 @@ class BotInterface:
builder.row(InlineKeyboardButton(text="⏸️ Пауза", callback_data=f"tpause_{task_id}")) builder.row(InlineKeyboardButton(text="⏸️ Пауза", callback_data=f"tpause_{task_id}"))
builder.row( builder.row(
InlineKeyboardButton(text="⏱️ Задержка", callback_data=f"tset_{task_id}_delay"), InlineKeyboardButton(text="⏱️ Пауза (сек)", callback_data=f"tset_{task_id}_delay"),
InlineKeyboardButton(text="📖 Чтение", callback_data=f"tset_{task_id}_reading") InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"tset_{task_id}_reading")
) )
builder.row( builder.row(
InlineKeyboardButton(text="🔗 На ссылку", callback_data=f"tset_{task_id}_perlink"), InlineKeyboardButton(text="🔗 Резерв кликов", callback_data=f"tset_{task_id}_perlink"),
InlineKeyboardButton(text="⏰ Минуты", callback_data=f"tset_{task_id}_minutes") InlineKeyboardButton(text="📊 CTR %", callback_data=f"tset_{task_id}_ctr"),
InlineKeyboardButton(text="⏰ Длительность", callback_data=f"tset_{task_id}_minutes")
) )
builder.row( builder.row(
InlineKeyboardButton(text="🌐 Домены", callback_data=f"tset_{task_id}_domains"), InlineKeyboardButton(text="🌐 Фильтр доменов", callback_data=f"tset_{task_id}_domains"),
InlineKeyboardButton(text="⏭️ Пропустить", callback_data=f"tskip_{task_id}") InlineKeyboardButton(text="⏭️ Пропустить", callback_data=f"tskip_{task_id}")
) )
builder.row( builder.row(
@@ -1568,8 +1726,31 @@ class BotInterface:
await message.answer(f"✅ Чтение: {format_range(mn, mx)}с") await message.answer(f"✅ Чтение: {format_range(mn, mx)}с")
elif param == "perlink": elif param == "perlink":
await self.task_manager.update_task(task_id, visits_per_link=int(value)) params_obj = await self.task_manager.get_task(task_id)
await message.answer(f"✅ На ссылку: {value}") 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)
await message.answer(f"✅ Серия: {lbl} кликов")
else:
await self.task_manager.update_task(task_id, visits_per_link=int(value))
await message.answer(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}%"
await message.answer(f"✅ CTR: {lbl}")
elif param == "minutes": elif param == "minutes":
await self.task_manager.update_task(task_id, monitor_minutes=int(value)) await self.task_manager.update_task(task_id, monitor_minutes=int(value))
@@ -1591,6 +1772,27 @@ class BotInterface:
await self.task_manager.update_task(task_id, visits_percent=pct) await self.task_manager.update_task(task_id, visits_percent=pct)
await message.answer(f"✅ % зрителей: {pct}%" if pct > 0 else "% зрителей отключён") await message.answer(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)
await message.answer(f"✅ Задержка между кликами: {format_range(mn, mx)}с")
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}%"
await message.answer(f"✅ CTR: {lbl}")
else: else:
await message.answer(f"❌ Неизвестный параметр: {param}") await message.answer(f"❌ Неизвестный параметр: {param}")
@@ -1599,32 +1801,57 @@ class BotInterface:
async def _show_user_task_detail(self, callback: CallbackQuery, task_id: str, params: TaskParams): async def _show_user_task_detail(self, callback: CallbackQuery, task_id: str, params: TaskParams):
if params.task_type == "user_visit": 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 = ( info = (
f"🔗 {params.url[:50]}\n" f"🔗 {params.url[:50]}\n"
f"Прогресс: {params.successful_visits}/{params.max_visits}\n" f"📊 Выполнено: {params.successful_visits} из {params.max_visits} переходов\n\n"
f"⏱ Задержка: {params.min_delay}-{params.max_delay} сек\n" f"🔢 Кликов в серии: {series_range}\n"
f"📖 Чтение: {params.min_reading}-{params.max_reading} сек" f"⏳ Задержка между кликами: {params.min_click_delay}{params.max_click_delay} сек\n"
f"⏱ Пауза между сериями: {params.min_delay}{params.max_delay} сек\n"
f"📖 Время на странице: {params.min_reading}{params.max_reading} сек"
) )
else: else:
domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все" domains = ", ".join(params.allowed_domains) if params.allowed_domains else "все"
pct = f"{params.visits_percent}% зрителей" if params.visits_percent > 0 else f"{params.visits_per_link} фикс." pct = (
f"{params.visits_percent}% от зрителей"
if params.visits_percent > 0
else f"фиксированно {params.visits_per_link}"
)
ctr_str = (
f"{params.min_ctr}%{params.max_ctr}%"
if params.min_ctr != params.max_ctr
else f"{params.min_ctr}%"
)
info = ( info = (
f"📺 {params.channel} | 🌐 {domains}\n" f"📺 Стример: {params.channel}\n"
f"🔗 {params.links_found} ссылок | ✅ {params.successful_visits} визитов\n" f"🌐 Домены: {domains}\n\n"
f"⏱ Задержка: {params.min_delay}-{params.max_delay} сек\n" f"🔗 Ссылок найдено: {params.links_found}\n"
f"📖 Чтение: {params.min_reading}-{params.max_reading} сек\n" f"✅ Успешных кликов: {params.successful_visits}\n\n"
f"👥 Переходов: {pct}" f"🔗 Кликов на ссылку: {pct}\n"
f"📊 CTR: {ctr_str}\n"
f"⏱ Пауза между кликами: {params.min_delay}{params.max_delay} сек\n"
f"📖 Время на странице: {params.min_reading}{params.max_reading} сек"
) )
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row( builder.row(
InlineKeyboardButton(text="⏱️ Задержка", callback_data=f"uset_{task_id}_delay"), InlineKeyboardButton(text="⏱️ Пауза (сек)", callback_data=f"uset_{task_id}_delay"),
InlineKeyboardButton(text="📖 Чтение", callback_data=f"uset_{task_id}_reading"), InlineKeyboardButton(text="📖 Время на стр.", callback_data=f"uset_{task_id}_reading"),
) )
if params.task_type == "twitch_irc": if params.task_type == "user_visit":
builder.row( builder.row(
InlineKeyboardButton(text="👥 % зрителей", callback_data=f"uset_{task_id}_percent"), InlineKeyboardButton(text="🔢 Размер серии", callback_data=f"uset_{task_id}_perlink"),
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}_percent"),
InlineKeyboardButton(text="🔗 Резерв кликов", callback_data=f"uset_{task_id}_perlink"),
InlineKeyboardButton(text="📊 CTR %", callback_data=f"uset_{task_id}_ctr"),
) )
builder.row(InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}")) builder.row(InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{task_id}"))
builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks")) builder.row(InlineKeyboardButton(text="🔙 К задачам", callback_data="my_tasks"))
@@ -1728,48 +1955,71 @@ class BotInterface:
async def _run_url_visits(self, task_id: str, params: TaskParams, bot): async def _run_url_visits(self, task_id: str, params: TaskParams, bot):
"""Посещает URL фиксированное число раз с балансом.""" """Посещает URL сериями: sleep(межсерийная) → N кликов(+sleep между ними) → повтор."""
try: total_done = 0
for i in range(params.max_visits or 1): max_v = params.max_visits or 1
async def _sleep_interruptible(seconds: int):
for _ in range(max(0, seconds)):
if params.stopped: if params.stopped:
break return
while params.paused and not params.stopped: while params.paused and not params.stopped:
await asyncio.sleep(2) await asyncio.sleep(2)
await asyncio.sleep(1)
try:
while total_done < max_v and not params.stopped:
# --- пауза перед серией ---
await _sleep_interruptible(params.get_delay())
if params.stopped: if params.stopped:
break break
balance = await self.balance_storage.get_balance(params.user_id) # размер серии выбирается случайно каждый раз
if balance <= 0: series_size = params.get_series_size()
await self.task_manager.stop_task(task_id)
await send_message_safe(bot, params.chat_id, "Баланс исчерпан — задача остановлена")
break
reading = params.get_reading_time() # --- серия кликов ---
try: for click_num in range(series_size):
result = await self.browser_service.visit_page(params.url, reading) if params.stopped or total_done >= max_v:
params.total_visits += 1 break
if result.success:
params.successful_visits += 1
await self.balance_storage.deduct(params.user_id, 1)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"URL visit error: {e}")
if i < (params.max_visits or 1) - 1 and not params.stopped: while params.paused and not params.stopped:
await asyncio.sleep(params.get_delay()) await asyncio.sleep(2)
if params.stopped:
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
reading = params.get_reading_time()
try:
result = await self.browser_service.visit_page(params.url, reading)
params.total_visits += 1
total_done += 1
if result.success:
params.successful_visits += 1
await self.balance_storage.deduct(params.user_id, 1)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"URL visit error: {e}")
total_done += 1
# задержка между кликами внутри серии (кроме последнего)
if click_num < series_size - 1 and not params.stopped and total_done < max_v:
await _sleep_interruptible(params.get_click_delay())
await self.task_manager.complete_task(task_id) await self.task_manager.complete_task(task_id)
remaining = await self.balance_storage.get_balance(params.user_id) remaining = await self.balance_storage.get_balance(params.user_id)
msg = await send_message_safe( await send_message_safe(
bot, params.chat_id, bot, params.chat_id,
f"✅ Задача завершена\n" f"✅ Задача завершена\n"
f"🔗 {params.url[:50]}\n" f"🔗 {params.url[:50]}\n"
f"Выполнено: {params.successful_visits}/{params.max_visits}\n" f"Выполнено: {params.successful_visits}/{params.max_visits}\n"
f"Переходов осталось: {remaining}" f"Переходов осталось: {remaining}"
) )
if msg and params.user_id:
await self._track_msg(params.user_id, msg.message_id)
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
finally: finally:
@@ -1793,13 +2043,15 @@ class BotInterface:
) )
return return
# Вычисляем количество переходов: по % зрителей или фиксировано # visits = viewers × CTR; fallback на visits_per_link если GQL недоступен
if params.visits_percent > 0: ctr = random.uniform(params.min_ctr, params.max_ctr)
viewers = await get_viewer_count(params.channel) viewers = await get_viewer_count(params.channel)
visits_count = max(1, int(viewers * params.visits_percent / 100)) if viewers and viewers > 0:
logger.info(f"Viewer-based visits: {viewers} × {params.visits_percent}% = {visits_count}") visits_count = max(1, round(viewers * ctr / 100))
logger.info(f"visits: {viewers} viewers × {ctr:.2f}% = {visits_count}")
else: else:
visits_count = params.visits_per_link visits_count = max(1, round(params.visits_per_link * ctr / 100))
logger.info(f"visits: fallback {params.visits_per_link} × {ctr:.2f}% = {visits_count}")
logger.info(f"🔗 @{username}: {url}") logger.info(f"🔗 @{username}: {url}")
await send_message_safe( await send_message_safe(
@@ -1989,7 +2241,7 @@ class BotInterface:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
sent = await message.answer( sent = await message.answer(
f"🔗 {text[:50]}\n\nШаг 2/2 — введите количество переходов:", f"🔗 {text[:50]}\n\nШаг 2/3 — введите количество переходов:",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
self._menu_msg[user_id] = sent.message_id self._menu_msg[user_id] = sent.message_id
@@ -2003,12 +2255,7 @@ class BotInterface:
await self._send_temp(message, "Введите целое число больше 0") await self._send_temp(message, "Введите целое число больше 0")
return return
del self._user_task_state[user_id]
await self._clean_prev(message)
balance = await self.balance_storage.get_balance(user_id) balance = await self.balance_storage.get_balance(user_id)
url = state["url"]
if balance <= 0: if balance <= 0:
await self._send_temp(message, "Недостаточно переходов") await self._send_temp(message, "Недостаточно переходов")
return return
@@ -2016,11 +2263,47 @@ class BotInterface:
await self._send_temp(message, f"Недостаточно переходов: нужно {count}, доступно {balance}") await self._send_temp(message, f"Недостаточно переходов: нужно {count}, доступно {balance}")
return 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/3 — сколько кликов в одной серии?\n\n"
"Бот делает N кликов подряд, затем ждёт паузу и начинает следующую серию.\n\n"
"Диапазон `2-5` — каждая серия будет случайной длины от 2 до 5.\n"
"Одно число `3` — всегда ровно 3 клика.",
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
del self._user_task_state[user_id]
await self._clean_prev(message)
url = state["url"]
count = state["count"]
series_label = f"{mn}{mx}" if mn != mx else str(mn)
task_id = f"u{user_id}_{extract_domain(url)[:12]}_{datetime.now().strftime('%H%M%S')}" task_id = f"u{user_id}_{extract_domain(url)[:12]}_{datetime.now().strftime('%H%M%S')}"
params = TaskParams( params = TaskParams(
url=url, url=url,
task_type="user_visit", task_type="user_visit",
max_visits=count, max_visits=count,
min_series=mn,
max_series=mx,
user_id=user_id, user_id=user_id,
min_delay=settings.DEFAULT_MIN_DELAY, min_delay=settings.DEFAULT_MIN_DELAY,
max_delay=settings.DEFAULT_MAX_DELAY, max_delay=settings.DEFAULT_MAX_DELAY,
@@ -2043,7 +2326,9 @@ class BotInterface:
await self.storage.save_task(task_id, params) await self.storage.save_task(task_id, params)
await self._send_temp( await self._send_temp(
message, message,
f"✅ Задача создана\n🔗 {url[:50]}\n🔢 Переходов: {count}", f"✅ Задача создана\n🔗 {url[:50]}\n"
f"🔢 Переходов: {count} | Серия: {series_label} кликов\n"
f"⏱ Пауза между сериями: {settings.DEFAULT_MIN_DELAY}{settings.DEFAULT_MAX_DELAY} сек",
delay=6 delay=6
) )
await self._show_user_menu(message, user_id=user_id) await self._show_user_menu(message, user_id=user_id)
+12
View File
@@ -46,6 +46,12 @@ class TaskStorage:
"chat_id": params.chat_id, "chat_id": params.chat_id,
"user_id": params.user_id, "user_id": params.user_id,
"visits_percent": params.visits_percent, "visits_percent": params.visits_percent,
"min_click_delay": params.min_click_delay,
"max_click_delay": params.max_click_delay,
"min_series": params.min_series,
"max_series": params.max_series,
"min_ctr": params.min_ctr,
"max_ctr": params.max_ctr,
"started_at": params.started_at.isoformat() if params.started_at else None, "started_at": params.started_at.isoformat() if params.started_at else None,
} }
@@ -73,6 +79,12 @@ class TaskStorage:
chat_id=data.get("chat_id"), chat_id=data.get("chat_id"),
user_id=data.get("user_id"), user_id=data.get("user_id"),
visits_percent=data.get("visits_percent", 0.0), visits_percent=data.get("visits_percent", 0.0),
min_click_delay=data.get("min_click_delay", 5),
max_click_delay=data.get("max_click_delay", 15),
min_series=data.get("min_series", data.get("visits_per_link", 1)),
max_series=data.get("max_series", data.get("visits_per_link", 1)),
min_ctr=data.get("min_ctr", 0.8),
max_ctr=data.get("max_ctr", 1.0),
) )
if data.get("started_at"): if data.get("started_at"):
try: try:
+44 -16
View File
@@ -24,6 +24,8 @@ class TaskParams:
max_delay: int = 30 max_delay: int = 30
min_reading: int = 5 min_reading: int = 5
max_reading: int = 15 max_reading: int = 15
min_click_delay: int = 5 # задержка между кликами внутри серии
max_click_delay: int = 15
# Лимиты # Лимиты
max_visits: Optional[int] = None max_visits: Optional[int] = None
@@ -37,6 +39,10 @@ class TaskParams:
allowed_domains: Optional[List[str]] = None allowed_domains: Optional[List[str]] = None
visits_percent: float = 0.0 # % от зрителей (0 = использовать visits_per_link) visits_percent: float = 0.0 # % от зрителей (0 = использовать visits_per_link)
min_series: int = 1 # для user_visit: мин. кликов в серии
max_series: int = 3 # для user_visit: макс. кликов в серии
min_ctr: float = 0.8 # для twitch_irc: мин. коэф. эффективности клика
max_ctr: float = 1.0 # для twitch_irc: макс. коэф. эффективности клика
# Состояние # Состояние
paused: bool = False paused: bool = False
@@ -83,6 +89,12 @@ class TaskParams:
return reading return reading
return random.randint(self.min_reading, self.max_reading) return random.randint(self.min_reading, self.max_reading)
def get_click_delay(self) -> int:
return random.randint(self.min_click_delay, self.max_click_delay)
def get_series_size(self) -> int:
return random.randint(self.min_series, self.max_series)
def get_status_emoji(self) -> str: def get_status_emoji(self) -> str:
if self.stopped: if self.stopped:
return "🛑" return "🛑"
@@ -109,34 +121,50 @@ class TaskParams:
duration = "" duration = ""
if self.started_at and (self.completed or self.stopped): if self.started_at and (self.completed or self.stopped):
delta = datetime.now() - self.started_at delta = datetime.now() - self.started_at
duration = f"⏱️ Длилась: {delta.seconds // 60} мин\n" duration = f"🕐 Длилась: {delta.seconds // 60} мин\n"
# Домены domains_text = (
domains_text = "" f"├ 🌐 Домены: {', '.join(self.allowed_domains)}\n"
if self.allowed_domains: if self.allowed_domains else
domains_text = f"├ 🌐 Домены: {', '.join(self.allowed_domains)}\n" "├ 🌐 Домены: все (без фильтра)\n"
else: )
domains_text = "├ 🌐 Домены: все\n" ctr_str = (
f"{self.min_ctr}%{self.max_ctr}%"
if self.min_ctr != self.max_ctr
else f"{self.min_ctr}%"
)
ctr_line = f"├ 📊 CTR: {ctr_str} (зрители × CTR% = кликов)"
monitor = (
f"{self.monitor_minutes} мин" if self.monitor_minutes > 0 else "бесконечно"
)
return ( return (
f"📺 **{self.channel}**\n" f"📺 **{self.channel}**\n"
f"├ Статус: {status}\n" f"├ Статус: {status}\n"
f"├ 🔗 Ссылок найдено: {self.links_found}\n" f"├ 🔗 Ссылок найдено в чате: {self.links_found}\n"
f"├ 📊 Всего визитов: {self.total_visits}\n" f"├ 📊 Всего кликов: {self.total_visits}\n"
f"├ ✅ Успешно: {self.successful_visits}\n" f"├ ✅ Успешных кликов: {self.successful_visits}\n"
f"├ ⏱️ Задержка: {self.min_delay}-{self.max_delay}с\n" f"{ctr_line}\n"
f"📖 Чтение: {self.min_reading}-{self.max_reading}с\n" f"🔗 Резерв (без GQL): {self.visits_per_link}\n"
f"🔗 На ссылку: {self.visits_per_link} визитов\n" f"⏱️ Пауза между переходами: {self.min_delay}{self.max_delay} сек\n"
f"⏰ Мониторинг: {self.monitor_minutes} мин\n" f"📖 Время на странице: {self.min_reading}{self.max_reading} сек\n"
f"├ ⏰ Длительность мониторинга: {monitor}\n"
f"{domains_text}" f"{domains_text}"
f"{duration}" f"{duration}"
) )
else: else:
series_range = (
f"{self.min_series}" if self.min_series == self.max_series
else f"{self.min_series}{self.max_series}"
)
return ( return (
f"🌐 `{self.url[:50]}`\n" f"🌐 `{self.url[:50]}`\n"
f"├ Статус: {self.get_status_text()}\n" f"├ Статус: {self.get_status_text()}\n"
f"├ 📊 Визитов: {self.total_visits}\n" f"├ 📊 Выполнено: {self.successful_visits}/{self.max_visits} переходов\n"
f"✅ Успешно: {self.successful_visits}\n" f"🔢 Кликов в серии: {series_range}\n"
f"├ ⏳ Задержка между кликами: {self.min_click_delay}{self.max_click_delay} сек\n"
f"├ ⏱️ Пауза между сериями: {self.min_delay}{self.max_delay} сек\n"
f"├ 📖 Время на странице: {self.min_reading}{self.max_reading} сек\n"
) )
+1
View File
@@ -8,3 +8,4 @@ aiofiles>=24.1.0
playwright-captcha>=0.1.0 playwright-captcha>=0.1.0
playwright>=1.49.0 playwright>=1.49.0
camoufox[geoip]>=0.4.0 camoufox[geoip]>=0.4.0
psutil>=5.9.0