@@ -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 :
interface . auth_manager . login ( user_id , is_admin = False )
await interface . _show_user_menu ( message )
else :
interface . auth_manager . record_failed_attempt ( user_id )
sent = await message . answer ( " ❌ Неверный пароль. Попробуйте ещё раз: " )
interface . _menu_msg [ user_id ] = sent . message_id
interface . _waiting_password [ user_id ] = " login "
elif mode == " admin_upgrade " :
if text == ADMIN_PASSWORD :
interface . auth_manager . login ( user_id , is_admin = True )
await interface . _update_commands ( )
await interface . _show_main_menu ( message )
else :
await message . answer (
" 👋 Привет! \n \n "
" У вас нет доступа к функционалу бота. \n "
" Обратитесь к администратору. " ,
reply_markup = ReplyKeyboardRemove ( )
)
else :
interface . auth_manager . record_failed_attempt ( user_id )
await message . answer ( " ❌ Неверный пароль. Попробуйте ещё раз: " )
interface . _waiting_password [ user_id ] = " login "
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 )
# =========================================================================
# УПРАВЛЕНИЕ ЗАДАЧЕЙ
@@ -882,10 +1097,15 @@ 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 )
# Только для задач с лимитом времени (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 :
@@ -968,11 +1193,19 @@ 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 ] } ` "
@@ -981,9 +1214,10 @@ class BotInterface:
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,6 +1227,16 @@ 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 } " )
@@ -1002,5 +1246,250 @@ class BotInterface:
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 ( ) )