add balance

This commit is contained in:
Yuriy Yuriev
2026-05-14 21:04:28 +07:00
parent bdf3fef7ec
commit fe9ada0558
7 changed files with 734 additions and 170 deletions
+6 -6
View File
@@ -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
View File
@@ -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())
+8 -1
View File
@@ -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)
+6 -11
View File
@@ -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
View File
@@ -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)
+15 -7
View File
@@ -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
View File
@@ -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'):