add balance
This commit is contained in:
+6
-6
@@ -26,12 +26,12 @@ class Settings(BaseSettings):
|
|||||||
VIEWPORT_HEIGHT: int = 768
|
VIEWPORT_HEIGHT: int = 768
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_MIN_DELAY: int = 10
|
DEFAULT_MIN_DELAY: int = 30
|
||||||
DEFAULT_MAX_DELAY: int = 30
|
DEFAULT_MAX_DELAY: int = 60
|
||||||
DEFAULT_MIN_READING: int = 10
|
DEFAULT_MIN_READING: int = 45
|
||||||
DEFAULT_MAX_READING: int = 30
|
DEFAULT_MAX_READING: int = 90
|
||||||
DEFAULT_MONITOR_MINUTES: int = 10
|
DEFAULT_VISITS_PER_LINK: int = 2
|
||||||
DEFAULT_VISITS_PER_LINK: int = 5
|
DEFAULT_MONITOR_MINUTES: int = 60
|
||||||
MTPROTO_SECRET: str = "00000000000000000000000000000000"
|
MTPROTO_SECRET: str = "00000000000000000000000000000000"
|
||||||
# Timing
|
# Timing
|
||||||
MAX_REDIRECT_WAIT: int = 30
|
MAX_REDIRECT_WAIT: int = 30
|
||||||
|
|||||||
+614
-125
@@ -26,7 +26,7 @@ from services.browser_service import BrowserService
|
|||||||
from services.visit_service import VisitScheduler
|
from services.visit_service import VisitScheduler
|
||||||
from utils.helpers import parse_range, format_range, extract_domain
|
from utils.helpers import parse_range, format_range, extract_domain
|
||||||
from utils.telegram import send_message_safe, send_visit_result
|
from utils.telegram import send_message_safe, send_visit_result
|
||||||
from managers.storage import TaskStorage, ChatStorage
|
from managers.storage import TaskStorage, ChatStorage, BalanceStorage
|
||||||
from auth.manager import AuthManager
|
from auth.manager import AuthManager
|
||||||
from auth.storage import AuthStorage
|
from auth.storage import AuthStorage
|
||||||
from auth import ADMIN_PASSWORD
|
from auth import ADMIN_PASSWORD
|
||||||
@@ -54,7 +54,12 @@ class BotInterface:
|
|||||||
self.chat_storage = chat_storage
|
self.chat_storage = chat_storage
|
||||||
self.auth_manager = AuthManager()
|
self.auth_manager = AuthManager()
|
||||||
self.auth_storage = AuthStorage()
|
self.auth_storage = AuthStorage()
|
||||||
|
self.balance_storage = BalanceStorage()
|
||||||
self._user_input_state: Dict[int, dict] = {}
|
self._user_input_state: Dict[int, dict] = {}
|
||||||
|
self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем
|
||||||
|
self._admin_balance_state: Dict[int, int] = {} # admin_id -> target_user_id
|
||||||
|
self._menu_msg: Dict[int, int] = {} # user_id -> inline keyboard message_id
|
||||||
|
self._menu_top_msg: Dict[int, int] = {} # user_id -> reply keyboard message_id
|
||||||
self._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"
|
||||||
|
|
||||||
@@ -73,116 +78,90 @@ class BotInterface:
|
|||||||
async def cmd_start(message: Message):
|
async def cmd_start(message: Message):
|
||||||
"""Обработка /start."""
|
"""Обработка /start."""
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
|
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
|
|
||||||
if interface.auth_manager.is_authenticated(user_id) and interface.auth_manager.is_admin(user_id):
|
if interface.auth_manager.is_authenticated(user_id) and interface.auth_manager.is_admin(user_id):
|
||||||
await interface._show_main_menu(message)
|
await interface._show_main_menu(message)
|
||||||
return
|
return
|
||||||
|
|
||||||
if interface.auth_manager.is_authenticated(user_id):
|
if interface.auth_manager.is_authenticated(user_id):
|
||||||
await message.answer(
|
await interface._show_user_menu(message)
|
||||||
"👋 Привет!\n\n"
|
|
||||||
"У вас нет доступа к функционалу бота.\n"
|
|
||||||
"Обратитесь к администратору.",
|
|
||||||
reply_markup=ReplyKeyboardRemove()
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
exists = await interface.auth_storage.user_exists(user_id)
|
exists = await interface.auth_storage.user_exists(user_id)
|
||||||
if exists:
|
if exists:
|
||||||
await message.answer("👋 С возвращением!\n\nВведите пароль для входа:")
|
sent = await message.answer("👋 С возвращением!\n\nВведите пароль для входа:")
|
||||||
interface._waiting_password[user_id] = "login"
|
interface._waiting_password[user_id] = "login"
|
||||||
else:
|
else:
|
||||||
await message.answer(
|
sent = await message.answer(
|
||||||
"👋 Добро пожаловать!\n\n"
|
"👋 Добро пожаловать!\n\n"
|
||||||
"Вы здесь впервые. Придумайте пароль для регистрации\n"
|
"Вы здесь впервые. Придумайте пароль для регистрации\n"
|
||||||
"_(минимум 4 символа):_"
|
"_(минимум 4 символа):_"
|
||||||
)
|
)
|
||||||
interface._waiting_password[user_id] = "register"
|
interface._waiting_password[user_id] = "register"
|
||||||
|
interface._menu_msg[user_id] = sent.message_id
|
||||||
|
|
||||||
@dp.message(Command("register"))
|
@dp.message(Command("register"))
|
||||||
async def cmd_register(message: Message):
|
async def cmd_register(message: Message):
|
||||||
"""Регистрация: /register <пароль>"""
|
"""Регистрация: /register <пароль>"""
|
||||||
|
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
parts = message.text.split(maxsplit=1)
|
parts = message.text.split(maxsplit=1)
|
||||||
if len(parts) < 2 or len(parts[1]) < 4:
|
if len(parts) < 2 or len(parts[1]) < 4:
|
||||||
await message.answer(
|
await interface._send_temp(message, "❌ Использование: /register <пароль от 4 символов>")
|
||||||
"❌ Использование: /register <пароль от 4 символов>"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
password = parts[1]
|
password = parts[1]
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
exists = await interface.auth_storage.user_exists(user_id)
|
exists = await interface.auth_storage.user_exists(user_id)
|
||||||
if exists:
|
if exists:
|
||||||
await message.answer(
|
await interface._send_temp(message, "⚠️ Вы уже зарегистрированы. Используйте /login <пароль>")
|
||||||
"⚠️ Вы уже зарегистрированы. Используйте /login <пароль>"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
password_hash, salt = interface.auth_manager._hash_password(password)
|
password_hash, salt = interface.auth_manager._hash_password(password)
|
||||||
await interface.auth_storage.register(user_id, password_hash, salt)
|
await interface.auth_storage.register(user_id, password_hash, salt)
|
||||||
is_admin = (password == ADMIN_PASSWORD)
|
is_admin = (password == ADMIN_PASSWORD)
|
||||||
interface.auth_manager.login(user_id, is_admin=is_admin)
|
interface.auth_manager.login(user_id, is_admin=is_admin)
|
||||||
if is_admin:
|
if is_admin:
|
||||||
await message.answer(
|
|
||||||
"✅ Регистрация прошла успешно!\n"
|
|
||||||
"🔓 Вы вошли как **АДМИНИСТРАТОР**.\n"
|
|
||||||
"Весь функционал бота доступен."
|
|
||||||
)
|
|
||||||
await interface._update_commands()
|
await interface._update_commands()
|
||||||
|
await interface._show_main_menu(message)
|
||||||
else:
|
else:
|
||||||
await message.answer(
|
await interface._show_user_menu(message)
|
||||||
"✅ Регистрация прошла успешно!\n"
|
|
||||||
"👤 Вы вошли как **ПОЛЬЗОВАТЕЛЬ**.\n"
|
|
||||||
"Функционал ограничен."
|
|
||||||
)
|
|
||||||
|
|
||||||
@dp.message(Command("login"))
|
@dp.message(Command("login"))
|
||||||
async def cmd_login(message: Message):
|
async def cmd_login(message: Message):
|
||||||
"""Вход: /login <пароль>"""
|
"""Вход: /login <пароль>"""
|
||||||
|
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
parts = message.text.split(maxsplit=1)
|
parts = message.text.split(maxsplit=1)
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
await message.answer("❌ Использование: /login <пароль>")
|
await interface._send_temp(message, "❌ Использование: /login <пароль>")
|
||||||
return
|
return
|
||||||
password = parts[1]
|
password = parts[1]
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
user_data = await interface.auth_storage.get_user(user_id)
|
user_data = await interface.auth_storage.get_user(user_id)
|
||||||
if not user_data:
|
if not user_data:
|
||||||
await message.answer(
|
await interface._send_temp(message, "❌ Вы не зарегистрированы.\nИспользуйте /register <пароль>")
|
||||||
"❌ Вы не зарегистрированы.\n"
|
|
||||||
"Используйте /register <пароль>"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
locked = interface.auth_manager.is_locked_out(user_id)
|
if interface.auth_manager.is_locked_out(user_id):
|
||||||
if locked:
|
await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.")
|
||||||
await message.answer("🔒 Слишком много попыток. Попробуйте позже.")
|
|
||||||
return
|
return
|
||||||
if interface.auth_manager.verify_password(
|
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"]
|
password, user_data["password_hash"], user_data["salt"]
|
||||||
):
|
):
|
||||||
is_admin = (password == ADMIN_PASSWORD)
|
interface.auth_manager.login(user_id, is_admin=False)
|
||||||
interface.auth_manager.login(user_id, is_admin=is_admin)
|
await interface._show_user_menu(message)
|
||||||
if is_admin:
|
|
||||||
await message.answer(
|
|
||||||
"✅ Вы вошли в систему!\n"
|
|
||||||
"🔓 Вы вошли как **АДМИНИСТРАТОР**."
|
|
||||||
)
|
|
||||||
await interface._update_commands()
|
|
||||||
else:
|
|
||||||
await message.answer(
|
|
||||||
"✅ Вы вошли в систему!\n"
|
|
||||||
"👤 Вы вошли как **ПОЛЬЗОВАТЕЛЬ**."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
interface.auth_manager.record_failed_attempt(user_id)
|
interface.auth_manager.record_failed_attempt(user_id)
|
||||||
await message.answer("❌ Неверный пароль")
|
await interface._send_temp(message, "❌ Неверный пароль")
|
||||||
|
|
||||||
@dp.message(Command("logout"))
|
@dp.message(Command("logout"))
|
||||||
async def cmd_logout(message: Message):
|
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)
|
interface.auth_manager.logout(message.from_user.id)
|
||||||
await message.answer(
|
|
||||||
"🔑 Вы вышли из системы.",
|
|
||||||
reply_markup=ReplyKeyboardRemove()
|
|
||||||
)
|
|
||||||
await interface._update_commands()
|
await interface._update_commands()
|
||||||
|
await interface._send_temp(message, "🔑 Вы вышли из системы.", delay=3)
|
||||||
|
|
||||||
# Декоратор: доступ только для авторизованных
|
# Декоратор: доступ только для авторизованных
|
||||||
async def require_auth(event):
|
async def require_auth(event):
|
||||||
@@ -219,9 +198,16 @@ class BotInterface:
|
|||||||
async def cb_main_menu(callback: CallbackQuery):
|
async def cb_main_menu(callback: CallbackQuery):
|
||||||
if not await require_admin(callback):
|
if not await require_admin(callback):
|
||||||
return
|
return
|
||||||
await interface._show_main_menu(callback.message)
|
await interface._show_main_menu(callback.message, user_id=callback.from_user.id)
|
||||||
await callback.answer()
|
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(F.text == "📺 Стримеры")
|
||||||
@dp.message(Command("streamers"))
|
@dp.message(Command("streamers"))
|
||||||
@@ -394,7 +380,6 @@ class BotInterface:
|
|||||||
async def cb_task_set(callback: CallbackQuery):
|
async def cb_task_set(callback: CallbackQuery):
|
||||||
if not await require_admin(callback):
|
if not await require_admin(callback):
|
||||||
return
|
return
|
||||||
"""Установка параметра задачи."""
|
|
||||||
data = callback.data
|
data = callback.data
|
||||||
|
|
||||||
logger.info(f"SET callback: {data}")
|
logger.info(f"SET callback: {data}")
|
||||||
@@ -432,7 +417,8 @@ class BotInterface:
|
|||||||
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"tdetail_{task_id}"))
|
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"tdetail_{task_id}"))
|
||||||
|
|
||||||
prompt = prompts.get(param, f"Введите значение для {param}:")
|
prompt = prompts.get(param, f"Введите значение для {param}:")
|
||||||
await callback.message.answer(prompt, reply_markup=builder.as_markup())
|
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}")
|
await callback.answer(f"Ожидаю ввод: {param}")
|
||||||
|
|
||||||
@dp.callback_query(F.data.startswith("tdelete_"))
|
@dp.callback_query(F.data.startswith("tdelete_"))
|
||||||
@@ -449,7 +435,6 @@ class BotInterface:
|
|||||||
async def treset(callback: CallbackQuery):
|
async def treset(callback: CallbackQuery):
|
||||||
if not await require_admin(callback):
|
if not await require_admin(callback):
|
||||||
return
|
return
|
||||||
"""Сброс статистики активной задачи."""
|
|
||||||
tid = callback.data.replace("treset_", "", 1)
|
tid = callback.data.replace("treset_", "", 1)
|
||||||
await interface.task_manager.update_task(
|
await interface.task_manager.update_task(
|
||||||
tid,
|
tid,
|
||||||
@@ -461,6 +446,194 @@ class BotInterface:
|
|||||||
await callback.answer("🔄 Статистика сброшена")
|
await callback.answer("🔄 Статистика сброшена")
|
||||||
await interface._show_task_detail(callback, tid)
|
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)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == "my_balance")
|
||||||
|
async def cb_my_balance(callback: CallbackQuery):
|
||||||
|
uid = callback.from_user.id
|
||||||
|
balance = await interface.balance_storage.get_balance(uid)
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_menu"))
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"💰 **Ваш баланс**\n\n**{balance}** переходов",
|
||||||
|
reply_markup=builder.as_markup()
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@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
|
||||||
|
interface._user_task_state[uid] = {"step": "channel"}
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"📝 **НОВАЯ ЗАДАЧА**\n\n"
|
||||||
|
f"💰 Баланс: {balance} переходов\n\n"
|
||||||
|
"Шаг 1/2: Введите название вашего Twitch канала:",
|
||||||
|
reply_markup=builder.as_markup()
|
||||||
|
)
|
||||||
|
interface._menu_msg[uid] = callback.message.message_id
|
||||||
|
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)
|
||||||
|
await callback.answer("👤 Переключились в режим пользователя")
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == "cancel_new_task")
|
||||||
|
async def cb_cancel_new_task(callback: CallbackQuery):
|
||||||
|
interface._user_task_state.pop(callback.from_user.id, None)
|
||||||
|
await interface._show_user_menu(callback.message)
|
||||||
|
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()
|
||||||
|
|
||||||
|
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("upause_"))
|
||||||
|
async def cb_user_pause_task(callback: CallbackQuery):
|
||||||
|
tid = callback.data.replace("upause_", "", 1)
|
||||||
|
params, uid = await _get_user_task(callback, tid)
|
||||||
|
if not params:
|
||||||
|
return
|
||||||
|
await interface.task_manager.pause_task(tid)
|
||||||
|
await callback.answer("⏸️ Пауза")
|
||||||
|
await interface._show_user_tasks(callback.message, uid, edit=True)
|
||||||
|
|
||||||
|
@dp.callback_query(F.data.startswith("uresume_"))
|
||||||
|
async def cb_user_resume_task(callback: CallbackQuery):
|
||||||
|
tid = callback.data.replace("uresume_", "", 1)
|
||||||
|
params, uid = await _get_user_task(callback, tid)
|
||||||
|
if not params:
|
||||||
|
return
|
||||||
|
await interface.task_manager.resume_task(tid)
|
||||||
|
await callback.answer("▶️ Возобновлено")
|
||||||
|
await interface._show_user_tasks(callback.message, uid, edit=True)
|
||||||
|
|
||||||
|
@dp.callback_query(F.data.startswith("uedit_"))
|
||||||
|
async def cb_user_edit_task(callback: CallbackQuery):
|
||||||
|
tid = callback.data.replace("uedit_", "", 1)
|
||||||
|
params, uid = await _get_user_task(callback, tid)
|
||||||
|
if not params:
|
||||||
|
return
|
||||||
|
interface._user_task_state[uid] = {"step": "edit_domains", "task_id": tid}
|
||||||
|
current = ", ".join(params.allowed_domains) if params.allowed_domains else "все"
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="my_tasks"))
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"🌐 **Изменить домены**\n\n"
|
||||||
|
f"Текущие: `{current}`\n\n"
|
||||||
|
"Введите новые домены через запятую:",
|
||||||
|
reply_markup=builder.as_markup()
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
# === Панель пользователей (admin) ===
|
||||||
|
@dp.callback_query(F.data == "menu_users")
|
||||||
|
async def cb_menu_users(callback: CallbackQuery):
|
||||||
|
if not await require_admin(callback):
|
||||||
|
return
|
||||||
|
await interface._show_users_list(callback.message, edit=True)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(F.data.startswith("udetail_"))
|
||||||
|
async def cb_user_detail(callback: CallbackQuery):
|
||||||
|
if not await require_admin(callback):
|
||||||
|
return
|
||||||
|
target_uid = int(callback.data.replace("udetail_", "", 1))
|
||||||
|
await interface._show_user_detail(callback, target_uid)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(F.data.startswith("ubal_"))
|
||||||
|
async def cb_user_balance(callback: CallbackQuery):
|
||||||
|
if not await require_admin(callback):
|
||||||
|
return
|
||||||
|
parts = callback.data.split("_")
|
||||||
|
# формат: ubal_{amount}_{user_id} или ubal_custom_{user_id}
|
||||||
|
if parts[1] == "custom":
|
||||||
|
target_uid = int(parts[2])
|
||||||
|
interface._admin_balance_state[callback.from_user.id] = target_uid
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data=f"udetail_{target_uid}"))
|
||||||
|
await callback.message.answer("Введите сумму для пополнения:", reply_markup=builder.as_markup())
|
||||||
|
await callback.answer()
|
||||||
|
else:
|
||||||
|
amount = int(parts[1])
|
||||||
|
target_uid = int(parts[2])
|
||||||
|
new_balance = await interface.balance_storage.add_balance(target_uid, amount)
|
||||||
|
await callback.answer(f"✅ +{amount} переходов. Баланс: {new_balance}")
|
||||||
|
await interface._show_user_detail(callback, target_uid)
|
||||||
|
|
||||||
|
@dp.callback_query(F.data.startswith("utasks_"))
|
||||||
|
async def cb_user_tasks_admin(callback: CallbackQuery):
|
||||||
|
if not await require_admin(callback):
|
||||||
|
return
|
||||||
|
target_uid = int(callback.data.replace("utasks_", "", 1))
|
||||||
|
await interface._show_user_tasks(callback.message, target_uid, edit=True, for_admin=True)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
# Единый обработчик всех текстовых сообщений — регистрируется последним,
|
# Единый обработчик всех текстовых сообщений — регистрируется последним,
|
||||||
# чтобы не перекрывать обработчики кнопок с фильтрами F.text
|
# чтобы не перекрывать обработчики кнопок с фильтрами F.text
|
||||||
@dp.message()
|
@dp.message()
|
||||||
@@ -472,11 +645,14 @@ class BotInterface:
|
|||||||
if user_id in interface._waiting_password:
|
if user_id in interface._waiting_password:
|
||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
|
# Пароль удаляем немедленно
|
||||||
|
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
mode = interface._waiting_password.pop(user_id)
|
mode = interface._waiting_password.pop(user_id)
|
||||||
|
|
||||||
if mode == "register":
|
if mode == "register":
|
||||||
if len(text) < 4:
|
if len(text) < 4:
|
||||||
await message.answer("❌ Пароль слишком короткий (минимум 4 символа). Попробуйте ещё раз:")
|
sent = await message.answer("❌ Пароль слишком короткий (минимум 4 символа). Попробуйте ещё раз:")
|
||||||
|
interface._menu_msg[user_id] = sent.message_id
|
||||||
interface._waiting_password[user_id] = "register"
|
interface._waiting_password[user_id] = "register"
|
||||||
return
|
return
|
||||||
password_hash, salt = interface.auth_manager._hash_password(text)
|
password_hash, salt = interface.auth_manager._hash_password(text)
|
||||||
@@ -487,57 +663,70 @@ class BotInterface:
|
|||||||
await interface._update_commands()
|
await interface._update_commands()
|
||||||
await interface._show_main_menu(message)
|
await interface._show_main_menu(message)
|
||||||
else:
|
else:
|
||||||
await message.answer(
|
await interface._show_user_menu(message)
|
||||||
"✅ Регистрация прошла успешно!\n"
|
|
||||||
"👤 Вы вошли как **ПОЛЬЗОВАТЕЛЬ**.\n\n"
|
|
||||||
"Обратитесь к администратору для получения доступа."
|
|
||||||
)
|
|
||||||
|
|
||||||
elif mode == "login":
|
elif mode == "login":
|
||||||
user_data = await interface.auth_storage.get_user(user_id)
|
user_data = await interface.auth_storage.get_user(user_id)
|
||||||
if not user_data:
|
if not user_data:
|
||||||
await message.answer(
|
await interface._send_temp(message, "❌ Вы не зарегистрированы.\nНапишите администратору.")
|
||||||
"❌ Вы не зарегистрированы.\n"
|
|
||||||
"Напишите администратору для получения доступа."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
if interface.auth_manager.is_locked_out(user_id):
|
if interface.auth_manager.is_locked_out(user_id):
|
||||||
await message.answer("🔒 Слишком много попыток. Попробуйте позже.")
|
await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.")
|
||||||
return
|
return
|
||||||
if interface.auth_manager.verify_password(
|
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"]
|
text, user_data["password_hash"], user_data["salt"]
|
||||||
):
|
):
|
||||||
is_admin = (text == ADMIN_PASSWORD)
|
interface.auth_manager.login(user_id, is_admin=False)
|
||||||
interface.auth_manager.login(user_id, is_admin=is_admin)
|
await interface._show_user_menu(message)
|
||||||
if is_admin:
|
|
||||||
await interface._update_commands()
|
|
||||||
await interface._show_main_menu(message)
|
|
||||||
else:
|
|
||||||
await message.answer(
|
|
||||||
"👋 Привет!\n\n"
|
|
||||||
"У вас нет доступа к функционалу бота.\n"
|
|
||||||
"Обратитесь к администратору.",
|
|
||||||
reply_markup=ReplyKeyboardRemove()
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
interface.auth_manager.record_failed_attempt(user_id)
|
interface.auth_manager.record_failed_attempt(user_id)
|
||||||
await message.answer("❌ Неверный пароль. Попробуйте ещё раз:")
|
sent = await message.answer("❌ Неверный пароль. Попробуйте ещё раз:")
|
||||||
|
interface._menu_msg[user_id] = sent.message_id
|
||||||
interface._waiting_password[user_id] = "login"
|
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
|
return
|
||||||
|
|
||||||
if not interface.auth_manager.is_authenticated(user_id):
|
if not interface.auth_manager.is_authenticated(user_id):
|
||||||
await message.answer(
|
await interface._send_temp(
|
||||||
"🔒 Требуется авторизация!\n\n"
|
message,
|
||||||
"/register <пароль> — зарегистрироваться\n"
|
"🔒 Требуется авторизация!\n\n/register <пароль> — зарегистрироваться\n/login <пароль> — войти"
|
||||||
"/login <пароль> — войти"
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not interface.auth_manager.is_admin(user_id):
|
if not interface.auth_manager.is_admin(user_id):
|
||||||
await message.answer(
|
await interface._show_user_menu(message)
|
||||||
"⛔ **Нет доступа!**\n\n"
|
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
|
return
|
||||||
|
|
||||||
logger.info(f"Handle text: '{text[:50]}' from {user_id}, param_state={user_id in interface._user_input_state}")
|
logger.info(f"Handle text: '{text[:50]}' from {user_id}, param_state={user_id in interface._user_input_state}")
|
||||||
@@ -546,6 +735,8 @@ class BotInterface:
|
|||||||
await interface._process_param_input(message)
|
await interface._process_param_input(message)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await interface._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
|
|
||||||
if text.startswith('http://') or text.startswith('https://'):
|
if text.startswith('http://') or text.startswith('https://'):
|
||||||
await interface._add_visit(message)
|
await interface._add_visit(message)
|
||||||
return
|
return
|
||||||
@@ -699,7 +890,46 @@ class BotInterface:
|
|||||||
# ОТОБРАЖЕНИЕ
|
# ОТОБРАЖЕНИЕ
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
async def _show_main_menu(self, message: Message):
|
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 _send_temp(self, message: Message, text: str, delay: int = 5) -> None:
|
||||||
|
"""Отправляет сообщение и удаляет его через delay секунд."""
|
||||||
|
sent = await message.answer(text)
|
||||||
|
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
|
||||||
|
|
||||||
|
async def _show_main_menu(self, message: Message, user_id: int = None):
|
||||||
"""Главное меню."""
|
"""Главное меню."""
|
||||||
stats = await self.task_manager.get_stats()
|
stats = await self.task_manager.get_stats()
|
||||||
|
|
||||||
@@ -715,15 +945,22 @@ class BotInterface:
|
|||||||
builder.row(InlineKeyboardButton(text=f"📺 Стримеры ({stats['twitch']})", callback_data="menu_streamers"))
|
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=f"📊 Задачи ({stats['total']})", callback_data="menu_tasks"))
|
||||||
builder.row(InlineKeyboardButton(text="📈 Статус", callback_data="menu_status"))
|
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="stop_all"))
|
||||||
|
builder.row(InlineKeyboardButton(text="👤 Режим пользователя", callback_data="switch_to_user"))
|
||||||
|
|
||||||
reply = ReplyKeyboardBuilder()
|
reply = ReplyKeyboardBuilder()
|
||||||
reply.row(KeyboardButton(text="📺 Стримеры"), KeyboardButton(text="📊 Задачи"))
|
reply.row(KeyboardButton(text="📺 Стримеры"), KeyboardButton(text="📊 Задачи"))
|
||||||
reply.row(KeyboardButton(text="📈 Статус"), KeyboardButton(text="🚀 Быстрый старт"))
|
reply.row(KeyboardButton(text="👥 Пользователи"), KeyboardButton(text="📈 Статус"))
|
||||||
reply.row(KeyboardButton(text="🛑 Остановить всё"), KeyboardButton(text="📋 Главное меню"))
|
reply.row(KeyboardButton(text="🚀 Быстрый старт"), KeyboardButton(text="🛑 Остановить всё"))
|
||||||
|
|
||||||
await message.answer(text, reply_markup=reply.as_markup(resize_keyboard=True))
|
uid = user_id or (message.from_user.id if message.from_user else None)
|
||||||
await message.answer("💡 Кнопки управления:", reply_markup=builder.as_markup())
|
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
|
||||||
|
|
||||||
async def _show_streamers_list(self, message: Message, edit: bool = False):
|
async def _show_streamers_list(self, message: Message, edit: bool = False):
|
||||||
active = await self.task_manager.get_active_tasks()
|
active = await self.task_manager.get_active_tasks()
|
||||||
@@ -772,13 +1009,7 @@ class BotInterface:
|
|||||||
|
|
||||||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||||
|
|
||||||
if edit:
|
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||||||
try:
|
|
||||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Edit message skipped: {e}")
|
|
||||||
else:
|
|
||||||
await message.answer(text, reply_markup=builder.as_markup())
|
|
||||||
|
|
||||||
async def _show_tasks_list(self, message: Message, edit: bool = False):
|
async def _show_tasks_list(self, message: Message, edit: bool = False):
|
||||||
"""Все задачи."""
|
"""Все задачи."""
|
||||||
@@ -786,20 +1017,12 @@ class BotInterface:
|
|||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_tasks"))
|
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_tasks"))
|
||||||
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)
|
||||||
if edit:
|
|
||||||
try:
|
|
||||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Edit message skipped: {e}")
|
|
||||||
else:
|
|
||||||
await message.answer(text, reply_markup=builder.as_markup())
|
|
||||||
|
|
||||||
async def _show_status(self, message: Message, edit: bool = False):
|
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()
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"📈 **СТАТУС**\n\n"
|
"📈 **СТАТУС**\n\n"
|
||||||
f"📺 Twitch: **{stats['twitch']}**\n"
|
f"📺 Twitch: **{stats['twitch']}**\n"
|
||||||
@@ -808,18 +1031,10 @@ class BotInterface:
|
|||||||
f"🔄 Активно: **{stats['active']}**\n\n"
|
f"🔄 Активно: **{stats['active']}**\n\n"
|
||||||
f"🔌 Прокси: {proxy_stats['total']} всего, {proxy_stats['available']} доступно"
|
f"🔌 Прокси: {proxy_stats['total']} всего, {proxy_stats['available']} доступно"
|
||||||
)
|
)
|
||||||
|
|
||||||
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="menu_main"))
|
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||||
|
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||||||
if edit:
|
|
||||||
try:
|
|
||||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Edit message skipped: {e}")
|
|
||||||
else:
|
|
||||||
await message.answer(text, reply_markup=builder.as_markup())
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# УПРАВЛЕНИЕ ЗАДАЧЕЙ
|
# УПРАВЛЕНИЕ ЗАДАЧЕЙ
|
||||||
@@ -882,10 +1097,15 @@ class BotInterface:
|
|||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
state = self._user_input_state.pop(user_id)
|
state = self._user_input_state.pop(user_id)
|
||||||
|
|
||||||
|
# Удаляем prompt и ответ пользователя
|
||||||
|
prompt_id = state.get("prompt_msg_id")
|
||||||
|
if prompt_id:
|
||||||
|
await self._safe_delete(message.bot, message.chat.id, prompt_id)
|
||||||
|
await self._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||||
|
|
||||||
task_id = state["task_id"]
|
task_id = state["task_id"]
|
||||||
param = state["param"]
|
param = state["param"]
|
||||||
|
|
||||||
# Получаем текст и очищаем от кавычек
|
|
||||||
value = message.text.strip() if message.text else ""
|
value = message.text.strip() if message.text else ""
|
||||||
value = value.replace("'", "").replace('"', "").replace("`", "").strip()
|
value = value.replace("'", "").replace('"', "").replace("`", "").strip()
|
||||||
|
|
||||||
@@ -953,9 +1173,14 @@ class BotInterface:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await irc.listen_for_messages(on_url, params.monitor_duration, params.allowed_domains, is_active=is_active)
|
await irc.listen_for_messages(on_url, params.monitor_duration, params.allowed_domains, is_active=is_active)
|
||||||
await self.task_manager.complete_task(task_id)
|
# Только для задач с лимитом времени (admin), не для бесконечных (user)
|
||||||
|
if params.monitor_minutes > 0:
|
||||||
|
await self.task_manager.complete_task(task_id)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"IRC error [{params.channel}]: {e}")
|
||||||
|
# Не помечаем как завершённую — задача будет восстановлена при рестарте
|
||||||
finally:
|
finally:
|
||||||
await irc.disconnect()
|
await irc.disconnect()
|
||||||
if self.storage:
|
if self.storage:
|
||||||
@@ -968,11 +1193,19 @@ class BotInterface:
|
|||||||
try:
|
try:
|
||||||
params.links_found += 1
|
params.links_found += 1
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
# Проверяем баланс для пользовательских задач
|
||||||
domain = urlparse(url).netloc
|
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
|
||||||
|
|
||||||
logger.info(f"🔗 @{username}: {url}")
|
logger.info(f"🔗 @{username}: {url}")
|
||||||
|
|
||||||
await send_message_safe(
|
await send_message_safe(
|
||||||
message.bot, message.chat.id,
|
message.bot, message.chat.id,
|
||||||
f"🔗 @{username}: `{url[:60]}`"
|
f"🔗 @{username}: `{url[:60]}`"
|
||||||
@@ -981,9 +1214,10 @@ class BotInterface:
|
|||||||
for i in range(params.visits_per_link):
|
for i in range(params.visits_per_link):
|
||||||
while params.paused:
|
while params.paused:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
if params.stopped:
|
||||||
|
break
|
||||||
|
|
||||||
reading = params.get_reading_time()
|
reading = params.get_reading_time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await self.browser_service.visit_page_from_twitch(
|
result = await self.browser_service.visit_page_from_twitch(
|
||||||
url=url,
|
url=url,
|
||||||
@@ -993,6 +1227,16 @@ class BotInterface:
|
|||||||
params.total_visits += 1
|
params.total_visits += 1
|
||||||
if result.success:
|
if result.success:
|
||||||
params.successful_visits += 1
|
params.successful_visits += 1
|
||||||
|
if params.user_id:
|
||||||
|
new_balance = await self.balance_storage.deduct(params.user_id, 1)
|
||||||
|
if new_balance <= 0:
|
||||||
|
params.stopped = True
|
||||||
|
await send_message_safe(
|
||||||
|
message.bot, params.chat_id,
|
||||||
|
"⚠️ Баланс исчерпан — задача остановлена.\n"
|
||||||
|
"Обратитесь к администратору для пополнения."
|
||||||
|
)
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Visit error: {e}")
|
logger.error(f"Visit error: {e}")
|
||||||
|
|
||||||
@@ -1002,5 +1246,250 @@ class BotInterface:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Process URL error: {e}")
|
logger.error(f"Process URL error: {e}")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# КАБИНЕТ ПОЛЬЗОВАТЕЛЯ
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
async def _show_user_menu(self, message: Message, edit: bool = False):
|
||||||
|
user_id = message.from_user.id
|
||||||
|
balance = await self.balance_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
|
||||||
|
)
|
||||||
|
text = (
|
||||||
|
"👤 **ЛИЧНЫЙ КАБИНЕТ**\n\n"
|
||||||
|
f"💰 Баланс: **{balance}** переходов\n"
|
||||||
|
f"📋 Активных задач: **{active}**\n\n"
|
||||||
|
"Выберите действие:"
|
||||||
|
)
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"),
|
||||||
|
InlineKeyboardButton(text="📋 Мои задачи", callback_data="my_tasks"),
|
||||||
|
)
|
||||||
|
builder.row(InlineKeyboardButton(text="💰 Баланс", callback_data="my_balance"))
|
||||||
|
builder.row(InlineKeyboardButton(text="🔑 Режим администратора", callback_data="enter_admin_mode"))
|
||||||
|
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
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
|
||||||
|
for tid, p in list(user_tasks.items())[:10]:
|
||||||
|
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()
|
||||||
|
if for_admin:
|
||||||
|
builder.row(InlineKeyboardButton(text="🔙 К пользователю", callback_data=f"udetail_{user_id}"))
|
||||||
|
else:
|
||||||
|
for tid, p in list(user_tasks.items())[:8]:
|
||||||
|
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(
|
||||||
|
pause_btn,
|
||||||
|
InlineKeyboardButton(text="🌐 Домены", callback_data=f"uedit_{tid}"),
|
||||||
|
InlineKeyboardButton(text="🗑️", callback_data=f"ustop_{tid}"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
builder.row(InlineKeyboardButton(
|
||||||
|
text=f"🗑️ {p.channel} (завершена)",
|
||||||
|
callback_data=f"ustop_{tid}"
|
||||||
|
))
|
||||||
|
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)
|
||||||
|
|
||||||
|
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"] 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
|
||||||
|
|
||||||
|
# Удаляем промпт шага 2 перед любым ответом
|
||||||
|
await self._clean_prev(message)
|
||||||
|
|
||||||
|
# Создание новой задачи
|
||||||
|
balance = await self.balance_storage.get_balance(user_id)
|
||||||
|
if balance <= 0:
|
||||||
|
await self._send_temp(message, "❌ Недостаточно баланса. Обратитесь к администратору.")
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = state["channel"]
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
await self._send_temp(
|
||||||
|
message,
|
||||||
|
f"✅ Мониторинг `{channel}` запущен!\n🌐 {', '.join(domains)}\n💰 Баланс: {balance}",
|
||||||
|
delay=6
|
||||||
|
)
|
||||||
|
await self._show_user_menu(message)
|
||||||
|
else:
|
||||||
|
await self._send_temp(message, "❌ Ошибка запуска задачи")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN)
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
async def _show_users_list(self, message: Message, edit: bool = False):
|
||||||
|
balances = await self.balance_storage.get_all()
|
||||||
|
all_tasks = await self.task_manager.get_all_tasks()
|
||||||
|
|
||||||
|
text = "👥 **ПОЛЬЗОВАТЕЛИ**\n\n"
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
if not balances:
|
||||||
|
text += "Нет пользователей с балансом."
|
||||||
|
else:
|
||||||
|
for uid_str, balance in list(balances.items())[:15]:
|
||||||
|
uid = int(uid_str)
|
||||||
|
active = sum(
|
||||||
|
1 for p in all_tasks.values()
|
||||||
|
if p.user_id == uid and not p.completed and not p.stopped
|
||||||
|
)
|
||||||
|
text += f"👤 `{uid}` — 💰 {balance} | 📋 {active} задач\n"
|
||||||
|
builder.row(InlineKeyboardButton(
|
||||||
|
text=f"👤 {uid} (💰{balance})",
|
||||||
|
callback_data=f"udetail_{uid}",
|
||||||
|
))
|
||||||
|
|
||||||
|
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||||
|
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||||||
|
|
||||||
|
async def _show_user_detail(self, callback: CallbackQuery, target_user_id: int):
|
||||||
|
balance = await self.balance_storage.get_balance(target_user_id)
|
||||||
|
all_tasks = await self.task_manager.get_all_tasks()
|
||||||
|
user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == target_user_id}
|
||||||
|
active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped)
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"👤 **Пользователь** `{target_user_id}`\n\n"
|
||||||
|
f"💰 Баланс: **{balance}** переходов\n"
|
||||||
|
f"📋 Задач: {len(user_tasks)} (активных: {active})\n\n"
|
||||||
|
"Пополнить баланс:"
|
||||||
|
)
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
for amount in [10, 50, 100, 500]:
|
||||||
|
builder.button(text=f"+{amount}", callback_data=f"ubal_{amount}_{target_user_id}")
|
||||||
|
builder.adjust(4)
|
||||||
|
builder.row(InlineKeyboardButton(
|
||||||
|
text="✏️ Другая сумма",
|
||||||
|
callback_data=f"ubal_custom_{target_user_id}",
|
||||||
|
))
|
||||||
|
if user_tasks:
|
||||||
|
builder.row(InlineKeyboardButton(
|
||||||
|
text=f"📋 Задачи ({len(user_tasks)})",
|
||||||
|
callback_data=f"utasks_{target_user_id}",
|
||||||
|
))
|
||||||
|
builder.row(InlineKeyboardButton(text="🔙 К списку", callback_data="menu_users"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
except Exception:
|
||||||
|
await callback.message.answer(text, reply_markup=builder.as_markup())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,8 +95,15 @@ class BotApplication:
|
|||||||
|
|
||||||
restored = 0
|
restored = 0
|
||||||
for task_id, params in saved_tasks.items():
|
for task_id, params in saved_tasks.items():
|
||||||
if params.completed or params.stopped:
|
if params.stopped:
|
||||||
continue
|
continue
|
||||||
|
# Задачи с лимитом времени, завершившиеся нормально — не восстанавливаем
|
||||||
|
if params.completed and params.monitor_minutes > 0:
|
||||||
|
continue
|
||||||
|
# Сбрасываем некорректный completed (баг IRC) для бесконечных задач
|
||||||
|
if params.completed:
|
||||||
|
params.completed = False
|
||||||
|
params.paused = False
|
||||||
|
|
||||||
await self.interface.task_manager.add_task(task_id, params)
|
await self.interface.task_manager.add_task(task_id, params)
|
||||||
|
|
||||||
|
|||||||
@@ -116,21 +116,16 @@ class BackgroundTaskManager:
|
|||||||
Returns:
|
Returns:
|
||||||
Number of tasks cancelled
|
Number of tasks cancelled
|
||||||
"""
|
"""
|
||||||
cancelled = 0
|
|
||||||
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
for task in list(self._tasks.values()):
|
running = [t for t in self._tasks.values() if not t.done()]
|
||||||
if not task.done():
|
for task in running:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
try:
|
if running:
|
||||||
await task
|
await asyncio.gather(*running, return_exceptions=True)
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
cancelled += 1
|
|
||||||
|
|
||||||
self._tasks.clear()
|
self._tasks.clear()
|
||||||
self._task_info.clear()
|
self._task_info.clear()
|
||||||
|
|
||||||
|
cancelled = len(running)
|
||||||
logger.info(f"All tasks cancelled: {cancelled}")
|
logger.info(f"All tasks cancelled: {cancelled}")
|
||||||
return cancelled
|
return cancelled
|
||||||
|
|
||||||
|
|||||||
+55
-2
@@ -44,6 +44,7 @@ class TaskStorage:
|
|||||||
"successful_visits": params.successful_visits,
|
"successful_visits": params.successful_visits,
|
||||||
"links_found": params.links_found,
|
"links_found": params.links_found,
|
||||||
"chat_id": params.chat_id,
|
"chat_id": params.chat_id,
|
||||||
|
"user_id": params.user_id,
|
||||||
"started_at": params.started_at.isoformat() if params.started_at else None,
|
"started_at": params.started_at.isoformat() if params.started_at else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ class TaskStorage:
|
|||||||
successful_visits=data.get("successful_visits", 0),
|
successful_visits=data.get("successful_visits", 0),
|
||||||
links_found=data.get("links_found", 0),
|
links_found=data.get("links_found", 0),
|
||||||
chat_id=data.get("chat_id"),
|
chat_id=data.get("chat_id"),
|
||||||
|
user_id=data.get("user_id"),
|
||||||
)
|
)
|
||||||
if data.get("started_at"):
|
if data.get("started_at"):
|
||||||
try:
|
try:
|
||||||
@@ -84,8 +86,12 @@ class TaskStorage:
|
|||||||
def _load_sync(self) -> dict:
|
def _load_sync(self) -> dict:
|
||||||
if not self.file_path.exists():
|
if not self.file_path.exists():
|
||||||
return {}
|
return {}
|
||||||
with open(self.file_path, "r", encoding="utf-8") as f:
|
try:
|
||||||
return json.load(f)
|
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError) as e:
|
||||||
|
logger.warning(f"Failed to load tasks: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
async def save_tasks(self, tasks: Dict[str, TaskParams]) -> None:
|
async def save_tasks(self, tasks: Dict[str, TaskParams]) -> None:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
@@ -165,3 +171,50 @@ class ChatStorage:
|
|||||||
if key in data and channel in data[key]:
|
if key in data and channel in data[key]:
|
||||||
data[key].remove(channel)
|
data[key].remove(channel)
|
||||||
await asyncio.to_thread(self._save_sync, data)
|
await asyncio.to_thread(self._save_sync, data)
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceStorage:
|
||||||
|
"""Баланс переходов пользователей."""
|
||||||
|
|
||||||
|
def __init__(self, file_path: str = "data/balances.json"):
|
||||||
|
self.file_path = Path(file_path)
|
||||||
|
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
|
def _load_sync(self) -> dict:
|
||||||
|
if not self.file_path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError) as e:
|
||||||
|
logger.warning(f"Failed to load balances: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_sync(self, data: dict) -> None:
|
||||||
|
with open(self.file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
async def get_balance(self, user_id: int) -> int:
|
||||||
|
async with self._lock:
|
||||||
|
data = await asyncio.to_thread(self._load_sync)
|
||||||
|
return data.get(str(user_id), 0)
|
||||||
|
|
||||||
|
async def add_balance(self, user_id: int, amount: int) -> int:
|
||||||
|
async with self._lock:
|
||||||
|
data = await asyncio.to_thread(self._load_sync)
|
||||||
|
data[str(user_id)] = data.get(str(user_id), 0) + amount
|
||||||
|
await asyncio.to_thread(self._save_sync, data)
|
||||||
|
return data[str(user_id)]
|
||||||
|
|
||||||
|
async def deduct(self, user_id: int, amount: int = 1) -> int:
|
||||||
|
async with self._lock:
|
||||||
|
data = await asyncio.to_thread(self._load_sync)
|
||||||
|
new_bal = max(0, data.get(str(user_id), 0) - amount)
|
||||||
|
data[str(user_id)] = new_bal
|
||||||
|
await asyncio.to_thread(self._save_sync, data)
|
||||||
|
return new_bal
|
||||||
|
|
||||||
|
async def get_all(self) -> dict:
|
||||||
|
async with self._lock:
|
||||||
|
return await asyncio.to_thread(self._load_sync)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class TaskParams:
|
|||||||
successful_visits: int = 0
|
successful_visits: int = 0
|
||||||
links_found: int = 0
|
links_found: int = 0
|
||||||
chat_id: Optional[int] = None
|
chat_id: Optional[int] = None
|
||||||
|
user_id: Optional[int] = None # Telegram user_id, если задача создана пользователем
|
||||||
|
|
||||||
completed: bool = False # Задача завершена
|
completed: bool = False # Задача завершена
|
||||||
stopped: bool = False # Задача остановлена пользователем
|
stopped: bool = False # Задача остановлена пользователем
|
||||||
@@ -241,15 +242,22 @@ class TaskManager:
|
|||||||
|
|
||||||
async def get_stats(self) -> dict:
|
async def get_stats(self) -> dict:
|
||||||
"""Общая статистика."""
|
"""Общая статистика."""
|
||||||
twitch_tasks = await self.get_twitch_tasks()
|
twitch = visit = paused = active = 0
|
||||||
visit_tasks = await self.get_visit_tasks()
|
for p in self._tasks.values():
|
||||||
|
if p.task_type == "twitch_irc":
|
||||||
|
twitch += 1
|
||||||
|
elif p.task_type == "visit":
|
||||||
|
visit += 1
|
||||||
|
if p.paused:
|
||||||
|
paused += 1
|
||||||
|
else:
|
||||||
|
active += 1
|
||||||
return {
|
return {
|
||||||
"total": len(self._tasks),
|
"total": len(self._tasks),
|
||||||
"twitch": len(twitch_tasks),
|
"twitch": twitch,
|
||||||
"visit": len(visit_tasks),
|
"visit": visit,
|
||||||
"paused": sum(1 for p in self._tasks.values() if p.paused),
|
"paused": paused,
|
||||||
"active": sum(1 for p in self._tasks.values() if not p.paused),
|
"active": active,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_task_info(self, task_id: str, params: TaskParams) -> str:
|
def _get_task_info(self, task_id: str, params: TaskParams) -> str:
|
||||||
|
|||||||
+18
-6
@@ -41,9 +41,12 @@ class TwitchIRCClient:
|
|||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
ssl_context = ssl.create_default_context()
|
ssl_context = ssl.create_default_context()
|
||||||
self._reader, self._writer = await asyncio.open_connection(
|
self._reader, self._writer = await asyncio.wait_for(
|
||||||
settings.IRC_SERVER, settings.IRC_PORT,
|
asyncio.open_connection(
|
||||||
ssl=ssl_context
|
settings.IRC_SERVER, settings.IRC_PORT,
|
||||||
|
ssl=ssl_context
|
||||||
|
),
|
||||||
|
timeout=15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._send(f"PASS {self.irc_oauth}")
|
await self._send(f"PASS {self.irc_oauth}")
|
||||||
@@ -173,13 +176,21 @@ class TwitchIRCClient:
|
|||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
last_message_time = datetime.now()
|
last_message_time = datetime.now()
|
||||||
|
|
||||||
await self.connect()
|
if not await self.connect():
|
||||||
|
raise ConnectionError(f"IRC connect failed for #{self.channel}")
|
||||||
|
|
||||||
while (datetime.now() - start_time).seconds < duration:
|
while duration == 0 or (datetime.now() - start_time).seconds < duration:
|
||||||
# === ПРОВЕРКА АКТИВНОСТИ ===
|
# === ПРОВЕРКА АКТИВНОСТИ ===
|
||||||
if is_active and not is_active():
|
if is_active and not is_active():
|
||||||
logger.info("⏸️ Task paused/stopped, disconnecting...")
|
logger.info("⏸️ Task paused/stopped, disconnecting...")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Нет соединения — ждём перед повтором
|
||||||
|
if not self._connected:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await self.connect()
|
||||||
|
continue
|
||||||
|
|
||||||
# === ПЕРЕПОДКЛЮЧЕНИЕ КАЖДЫЕ 30 СЕКУНД ===
|
# === ПЕРЕПОДКЛЮЧЕНИЕ КАЖДЫЕ 30 СЕКУНД ===
|
||||||
if (datetime.now() - last_message_time).seconds > 30:
|
if (datetime.now() - last_message_time).seconds > 30:
|
||||||
logger.info("🔄 Reconnecting (30s)...")
|
logger.info("🔄 Reconnecting (30s)...")
|
||||||
@@ -227,6 +238,7 @@ class TwitchIRCClient:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not line:
|
if not line:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith('PONG'):
|
if line.startswith('PONG'):
|
||||||
|
|||||||
Reference in New Issue
Block a user