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