add heleket

This commit is contained in:
Yuriy Yuriev
2026-05-15 00:12:14 +07:00
parent ae5f7967f2
commit 7d28ba794b
4 changed files with 508 additions and 89 deletions
+8
View File
@@ -10,6 +10,14 @@ class Settings(BaseSettings):
BOT_TOKEN: str = "" BOT_TOKEN: str = ""
TELEGRAM_PROXY: str = "" # socks5://user:pass@host:port TELEGRAM_PROXY: str = "" # socks5://user:pass@host:port
ADMIN_PASSWORD: str = "" ADMIN_PASSWORD: str = ""
# Heleket payment
HELEKET_API_KEY: str = ""
HELEKET_SHOP_ID: str = ""
CLICK_PRICE_RUB: float = 2.0 # Цена одного перехода в рублях
TOPUP_MIN_RUB: int = 50 # Минимальное пополнение ₽
TOPUP_MAX_RUB: int = 50000 # Максимальное пополнение ₽
CLICKS_MIN: int = 10 # Минимальная покупка переходов
# Telethon/MTProto settings # Telethon/MTProto settings
API_ID: int = 12345 API_ID: int = 12345
+340 -83
View File
@@ -26,7 +26,8 @@ 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, BalanceStorage from managers.storage import TaskStorage, ChatStorage, BalanceStorage, RubleBalanceStorage, PaymentStorage
from services.payment_service import HelketPayment
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,8 +55,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.balance_storage = BalanceStorage() # переходы
self.rub_storage = RubleBalanceStorage() # рубли
self.payment_storage = PaymentStorage()
self.heleket = HelketPayment()
self._user_input_state: Dict[int, dict] = {} self._user_input_state: Dict[int, dict] = {}
self._payment_state: Dict[int, str] = {} # user_id -> "topup" | "buy_clicks"
self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем self._user_task_state: Dict[int, dict] = {} # шаги создания задачи пользователем
self._admin_balance_state: Dict[int, int] = {} # admin_id -> target_user_id 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_msg: Dict[int, int] = {} # user_id -> inline keyboard message_id
@@ -90,13 +95,11 @@ class BotInterface:
exists = await interface.auth_storage.user_exists(user_id) exists = await interface.auth_storage.user_exists(user_id)
if exists: if exists:
sent = await message.answer("👋 С возвращением!\n\nВведите пароль для входа:") sent = await message.answer("С возвращением! Введите пароль:")
interface._waiting_password[user_id] = "login" interface._waiting_password[user_id] = "login"
else: else:
sent = await message.answer( sent = await message.answer(
"👋 Добро пожаловать!\n\n" "Добро пожаловать!\n\nПридумайте пароль для входа (мин. 4 символа):"
"Вы здесь впервые. Придумайте пароль для регистрации\n"
"_(минимум 4 символа):_"
) )
interface._waiting_password[user_id] = "register" interface._waiting_password[user_id] = "register"
interface._menu_msg[user_id] = sent.message_id interface._menu_msg[user_id] = sent.message_id
@@ -167,10 +170,7 @@ class BotInterface:
async def require_auth(event): async def require_auth(event):
user_id = event.from_user.id user_id = event.from_user.id
if not interface.auth_manager.is_authenticated(user_id): if not interface.auth_manager.is_authenticated(user_id):
await event.answer( await event.answer("Требуется авторизация")
"🔒 Требуется авторизация!\n\n"
"Напишите администратору для получения доступа."
)
return False return False
return True return True
@@ -178,18 +178,10 @@ class BotInterface:
async def require_admin(event): async def require_admin(event):
user_id = event.from_user.id user_id = event.from_user.id
if not interface.auth_manager.is_authenticated(user_id): if not interface.auth_manager.is_authenticated(user_id):
await event.answer( await event.answer("Требуется авторизация")
"🔒 Требуется авторизация!\n\n"
"/register <пароль> — зарегистрироваться\n"
"/login <пароль> — войти"
)
return False return False
if not interface.auth_manager.is_admin(user_id): if not interface.auth_manager.is_admin(user_id):
await event.answer( await event.answer("Нет доступа — войдите как администратор")
"⛔ **Нет доступа!**\n\n"
"Эта функция доступна только администраторам.\n"
"Войдите как администратор."
)
return False return False
return True return True
@@ -278,13 +270,13 @@ class BotInterface:
if not await require_admin(message): if not await require_admin(message):
return return
await message.answer( await message.answer(
"🚀 **БЫСТРЫЙ СТАРТ**\n\n" "🚀 БЫСТРЫЙ СТАРТ\n\n"
"**Twitch мониторинг:**\n" "Twitch мониторинг:\n"
"`<канал> <username> [посещений] [минут] [задержка] [чтение] [домены]`\n\n" "`<канал> <username> [посещений] [минут] [задержка] [чтение] [домены]`\n\n"
"**Пример:** `streamer user123 5 30 10-30 10-30`\n\n" "Пример: `streamer user123 5 30 10-30 10-30`\n\n"
"**Посещение URL:**\n" "Посещение URL:\n"
"`<url> [задержка] [повторов] [чтение]`\n\n" "`<url> [задержка] [повторов] [чтение]`\n\n"
"**Пример:** `https://example.com 10-30 100 15-30`" "Пример: `https://example.com 10-30 100 15-30`"
) )
# === Управление задачей === # === Управление задачей ===
@@ -449,21 +441,117 @@ class BotInterface:
# === Кабинет пользователя === # === Кабинет пользователя ===
@dp.callback_query(F.data == "user_menu") @dp.callback_query(F.data == "user_menu")
async def cb_user_menu(callback: CallbackQuery): async def cb_user_menu(callback: CallbackQuery):
await interface._show_user_menu(callback.message, edit=True) await interface._show_user_menu(
callback.message, edit=True, user_id=callback.from_user.id
)
await callback.answer() await callback.answer()
@dp.callback_query(F.data == "my_balance") @dp.callback_query(F.data == "my_balance")
async def cb_my_balance(callback: CallbackQuery): async def cb_my_balance(callback: CallbackQuery):
uid = callback.from_user.id await interface._show_user_menu(
balance = await interface.balance_storage.get_balance(uid) callback.message, edit=True, user_id=callback.from_user.id
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() await callback.answer()
# ── Пополнение рублей (крипта → рубли) ──────────────────────────────
@dp.callback_query(F.data == "topup_main")
async def cb_topup_main(callback: CallbackQuery):
uid = callback.from_user.id
existing = await interface.payment_storage.get_by_user(uid)
if existing:
await callback.answer("⚠️ Уже есть незакрытый счёт", show_alert=True)
await interface._show_invoice(callback, existing)
return
interface._payment_state[uid] = "topup"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input"))
await callback.message.edit_text(
f"💳 ПОПОЛНЕНИЕ БАЛАНСА\n\n"
f"Введите сумму пополнения в рублях:\n"
f"(от {settings.TOPUP_MIN_RUB} до {settings.TOPUP_MAX_RUB} ₽)",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data == "check_pay")
async def cb_check_payment(callback: CallbackQuery):
uid = callback.from_user.id
payment = await interface.payment_storage.get_by_user(uid)
if not payment:
await callback.answer("❌ Платёж не найден", show_alert=True)
await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id)
return
await callback.answer("⏳ Проверяем...")
status = await interface.heleket.check_status(payment["payment_id"])
if status == "paid":
await interface.payment_storage.delete(payment["payment_id"])
new_rub = await interface.rub_storage.add_balance(uid, payment["rub"])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🛒 Купить переходы", callback_data="visit_shop"))
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
await callback.message.edit_text(
f"✅ Оплата подтверждена!\n\n"
f"Начислено: +{payment['rub']}\n"
f"Баланс: {new_rub}",
reply_markup=builder.as_markup()
)
elif status == "expired":
await interface.payment_storage.delete(payment["payment_id"])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить снова", callback_data="topup_main"))
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
await callback.message.edit_text(
"Счёт истёк. Создайте новый.",
reply_markup=builder.as_markup()
)
else:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔄 Проверить снова", callback_data="check_pay"))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
await callback.message.edit_text(
"⏳ Платёж ещё не поступил\n\nОбычно перевод занимает 1–5 минут.",
reply_markup=builder.as_markup()
)
@dp.callback_query(F.data == "cancel_pay")
async def cb_cancel_payment(callback: CallbackQuery):
uid = callback.from_user.id
payment = await interface.payment_storage.get_by_user(uid)
if payment:
await interface.payment_storage.delete(payment["payment_id"])
await callback.answer("Счёт отменён")
await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id)
# ── Покупка переходов (рубли → переходы) ────────────────────────────
@dp.callback_query(F.data == "visit_shop")
async def cb_visit_shop(callback: CallbackQuery):
uid = callback.from_user.id
rub_bal = await interface.rub_storage.get_balance(uid)
price = settings.CLICK_PRICE_RUB
interface._payment_state[uid] = "buy_clicks"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_pay_input"))
await callback.message.edit_text(
f"🛒 Купить переходы\n\n"
f"Баланс: {rub_bal}\n"
f"Цена: {price:.0f} ₽ за 1 переход (мин. {settings.CLICKS_MIN})\n\n"
f"Введите количество:",
reply_markup=builder.as_markup()
)
interface._menu_msg[uid] = callback.message.message_id
await callback.answer()
@dp.callback_query(F.data == "cancel_pay_input")
async def cb_cancel_pay_input(callback: CallbackQuery):
uid = callback.from_user.id
interface._payment_state.pop(uid, None)
await interface._show_user_menu(callback.message, edit=True, user_id=callback.from_user.id)
@dp.callback_query(F.data == "new_task") @dp.callback_query(F.data == "new_task")
async def cb_new_task(callback: CallbackQuery): async def cb_new_task(callback: CallbackQuery):
uid = callback.from_user.id uid = callback.from_user.id
@@ -483,9 +571,9 @@ class BotInterface:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_new_task"))
await callback.message.edit_text( await callback.message.edit_text(
f"📝 **НОВАЯ ЗАДАЧА**\n\n" f"📝 Новая задача\n\n"
f"💰 Баланс: {balance} переходов\n\n" f"Переходов: {balance}\n\n"
"Шаг 1/2: Введите название вашего Twitch канала:", "Шаг 1/2 — введите название Twitch канала:",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
interface._menu_msg[uid] = callback.message.message_id interface._menu_msg[uid] = callback.message.message_id
@@ -503,6 +591,7 @@ class BotInterface:
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="user_menu")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="user_menu"))
await callback.message.edit_text( await callback.message.edit_text(
"🔑 Введите пароль администратора:", "🔑 Введите пароль администратора:",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
await callback.answer() await callback.answer()
@@ -514,13 +603,14 @@ class BotInterface:
await callback.answer("❌ Требуется авторизация", show_alert=True) await callback.answer("❌ Требуется авторизация", show_alert=True)
return return
interface.auth_manager.login(uid, is_admin=False) interface.auth_manager.login(uid, is_admin=False)
await interface._show_user_menu(callback.message) await interface._show_user_menu(callback.message, user_id=uid)
await callback.answer("👤 Переключились в режим пользователя") await callback.answer("👤 Переключились в режим пользователя")
@dp.callback_query(F.data == "cancel_new_task") @dp.callback_query(F.data == "cancel_new_task")
async def cb_cancel_new_task(callback: CallbackQuery): async def cb_cancel_new_task(callback: CallbackQuery):
interface._user_task_state.pop(callback.from_user.id, None) uid = callback.from_user.id
await interface._show_user_menu(callback.message) interface._user_task_state.pop(uid, None)
await interface._show_user_menu(callback.message, user_id=uid)
await callback.answer("Отменено") await callback.answer("Отменено")
@dp.callback_query(F.data == "my_tasks") @dp.callback_query(F.data == "my_tasks")
@@ -583,8 +673,8 @@ class BotInterface:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="my_tasks")) builder.row(InlineKeyboardButton(text="❌ Отмена", callback_data="my_tasks"))
await callback.message.edit_text( await callback.message.edit_text(
f"🌐 **Изменить домены**\n\n" f"🌐 Изменить домены\n\n"
f"Текущие: `{current}`\n\n" f"Текущие: {current}\n\n"
"Введите новые домены через запятую:", "Введите новые домены через запятую:",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
@@ -651,7 +741,7 @@ class BotInterface:
if mode == "register": if mode == "register":
if len(text) < 4: if len(text) < 4:
sent = await message.answer("Пароль слишком короткий (минимум 4 символа). Попробуйте ещё раз:") sent = await message.answer("Пароль слишком короткий, мин. 4 символа:")
interface._menu_msg[user_id] = sent.message_id interface._menu_msg[user_id] = sent.message_id
interface._waiting_password[user_id] = "register" interface._waiting_password[user_id] = "register"
return return
@@ -668,10 +758,10 @@ class BotInterface:
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 interface._send_temp(message, "Вы не зарегистрированы.\nНапишите администратору.") await interface._send_temp(message, "Вы не зарегистрированы")
return return
if interface.auth_manager.is_locked_out(user_id): if interface.auth_manager.is_locked_out(user_id):
await interface._send_temp(message, "🔒 Слишком много попыток. Попробуйте позже.") await interface._send_temp(message, "Слишком много попыток, попробуйте позже")
return return
if text == ADMIN_PASSWORD: if text == ADMIN_PASSWORD:
interface.auth_manager.login(user_id, is_admin=True) interface.auth_manager.login(user_id, is_admin=True)
@@ -684,7 +774,7 @@ class BotInterface:
await interface._show_user_menu(message) await interface._show_user_menu(message)
else: else:
interface.auth_manager.record_failed_attempt(user_id) interface.auth_manager.record_failed_attempt(user_id)
sent = await message.answer("Неверный пароль. Попробуйте ещё раз:") sent = await message.answer("Неверный пароль, попробуйте ещё раз:")
interface._menu_msg[user_id] = sent.message_id interface._menu_msg[user_id] = sent.message_id
interface._waiting_password[user_id] = "login" interface._waiting_password[user_id] = "login"
@@ -694,7 +784,7 @@ class BotInterface:
await interface._update_commands() await interface._update_commands()
await interface._show_main_menu(message) await interface._show_main_menu(message)
else: else:
sent = await message.answer("Неверный пароль администратора. Попробуйте ещё раз:") sent = await message.answer("Неверный пароль администратора:")
interface._menu_msg[user_id] = sent.message_id interface._menu_msg[user_id] = sent.message_id
interface._waiting_password[user_id] = "admin_upgrade" interface._waiting_password[user_id] = "admin_upgrade"
return return
@@ -704,11 +794,13 @@ class BotInterface:
await interface._process_user_task_input(message) await interface._process_user_task_input(message)
return return
# Ввод суммы пополнения или покупки переходов
if user_id in interface._payment_state:
await interface._process_payment_input(message)
return
if not interface.auth_manager.is_authenticated(user_id): if not interface.auth_manager.is_authenticated(user_id):
await interface._send_temp( await interface._send_temp(message, "Требуется авторизация")
message,
"🔒 Требуется авторизация!\n\n/register <пароль> — зарегистрироваться\n/login <пароль> — войти"
)
return return
if not interface.auth_manager.is_admin(user_id): if not interface.auth_manager.is_admin(user_id):
@@ -806,7 +898,7 @@ class BotInterface:
await self.chat_storage.add_streamer(message.chat.id, channel) await self.chat_storage.add_streamer(message.chat.id, channel)
domains_str = ", ".join(domains) if domains else "все" domains_str = ", ".join(domains) if domains else "все"
text = ( text = (
f"**МОНИТОРИНГ ЗАПУЩЕН**\n\n" f"Мониторинг запущен\n\n"
f"📺 Канал: `{channel}`\n" f"📺 Канал: `{channel}`\n"
f"👤 Отслеживание: все пользователи\n" f"👤 Отслеживание: все пользователи\n"
f"🔗 На ссылку: {vl} визитов\n" f"🔗 На ссылку: {vl} визитов\n"
@@ -878,7 +970,7 @@ class BotInterface:
if success: if success:
await message.answer( await message.answer(
f"**ПОСЕЩЕНИЕ ЗАПУЩЕНО**\n\n" f"Посещение запущено\n\n"
f"🌐 `{url}`\n" f"🌐 `{url}`\n"
f"⏱️ Задержка: {format_range(min_delay, max_delay)}с\n" f"⏱️ Задержка: {format_range(min_delay, max_delay)}с\n"
f"📖 Чтение: {format_range(min_reading, max_reading)}с\n" f"📖 Чтение: {format_range(min_reading, max_reading)}с\n"
@@ -934,10 +1026,10 @@ class BotInterface:
stats = await self.task_manager.get_stats() stats = await self.task_manager.get_stats()
text = ( text = (
"👋 **ГЛАВНОЕ МЕНЮ**\n\n" "👋 Главное меню\n\n"
f"📺 Twitch: **{stats['twitch']}**\n" f"📺 Twitch: {stats['twitch']}\n"
f"🌐 Визитов: **{stats['visit']}**\n" f"🌐 Визитов: {stats['visit']}\n"
f"⏸️ Пауза: **{stats['paused']}**\n\n" f"⏸️ Пауза: {stats['paused']}\n\n"
"Выберите раздел:" "Выберите раздел:"
) )
@@ -970,21 +1062,21 @@ class BotInterface:
twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"} twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"}
if not twitch_active and not twitch_done: if not twitch_active and not twitch_done:
text = "📺 **СТРИМЕРЫ**\n\nНет задач.\nОтправьте `<канал>` для добавления." text = "📺 Стримеры\n\nЗадач нет. Отправьте название канала для добавления."
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main")) builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
else: else:
text = "📺 **СТРИМЕРЫ**\n\n" text = "📺 Стримеры\n\n"
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if twitch_active: if twitch_active:
text += "**🔄 Активные:**\n" text += "🔄 Активные:\n"
for tid, p in list(twitch_active.items())[:10]: for tid, p in list(twitch_active.items())[:10]:
emoji = p.get_status_emoji() emoji = p.get_status_emoji()
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
text += ( text += (
f"{emoji} **{p.channel}**\n" f"{emoji} {p.channel}\n"
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n" f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
) )
builder.row(InlineKeyboardButton( builder.row(InlineKeyboardButton(
@@ -993,13 +1085,13 @@ class BotInterface:
)) ))
if twitch_done: if twitch_done:
text += "\n**📁 Завершенные:**\n" text += "\n📁 Завершенные:\n"
for tid, p in list(twitch_done.items())[:5]: for tid, p in list(twitch_done.items())[:5]:
emoji = p.get_status_emoji() emoji = p.get_status_emoji()
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
text += ( text += (
f"{emoji} **{p.channel}**\n" f"{emoji} {p.channel}\n"
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n" f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
) )
builder.row(InlineKeyboardButton( builder.row(InlineKeyboardButton(
@@ -1024,11 +1116,11 @@ class BotInterface:
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"
f"🌐 Визиты: **{stats['visit']}**\n" f"🌐 Визиты: {stats['visit']}\n"
f"⏸️ Пауза: **{stats['paused']}**\n" f"⏸️ Пауза: {stats['paused']}\n"
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()
@@ -1046,7 +1138,7 @@ class BotInterface:
await callback.answer("❌ Задача не найдена") await callback.answer("❌ Задача не найдена")
return return
text = f"📋 **УПРАВЛЕНИЕ**\n\n{params.get_stats_text()}" text = f"📋 Управление\n\n{params.get_stats_text()}"
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
@@ -1250,31 +1342,48 @@ class BotInterface:
# КАБИНЕТ ПОЛЬЗОВАТЕЛЯ # КАБИНЕТ ПОЛЬЗОВАТЕЛЯ
# ========================================================================= # =========================================================================
async def _show_user_menu(self, message: Message, edit: bool = False): async def _show_user_menu(self, message: Message, edit: bool = False, user_id: int = None):
user_id = message.from_user.id if user_id is None:
user_id = message.from_user.id if message.from_user else None
if not user_id:
return
# Сбрасываем все незавершённые состояния при возврате в меню # Сбрасываем все незавершённые состояния при возврате в меню
self._waiting_password.pop(user_id, None) self._waiting_password.pop(user_id, None)
self._user_task_state.pop(user_id, None) self._user_task_state.pop(user_id, None)
balance = await self.balance_storage.get_balance(user_id) self._payment_state.pop(user_id, None)
visits = await self.balance_storage.get_balance(user_id)
rubles = await self.rub_storage.get_balance(user_id)
all_tasks = await self.task_manager.get_all_tasks() all_tasks = await self.task_manager.get_all_tasks()
active = sum( active = sum(
1 for p in all_tasks.values() 1 for p in all_tasks.values()
if p.user_id == user_id and not p.completed and not p.stopped if p.user_id == user_id and not p.completed and not p.stopped
) )
pending = await self.payment_storage.get_by_user(user_id)
text = ( text = (
"👤 **ЛИЧНЫЙ КАБИНЕТ**\n\n" "👤 Личный кабинет\n\n"
f"💰 Баланс: **{balance}** переходов\n" f"🪙 Рублей: {rubles}\n"
f"📋 Активных задач: **{active}**\n\n" f"🔢 Переходов: {visits}\n"
"Выберите действие:" f"📋 Активных задач: {active}"
) )
if pending:
text += "\n\n⏳ Есть незакрытый счёт"
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row( builder.row(
InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"), InlineKeyboardButton(text="📝 Новая задача", callback_data="new_task"),
InlineKeyboardButton(text="📋 Мои задачи", callback_data="my_tasks"), InlineKeyboardButton(text="📋 Мои задачи", callback_data="my_tasks"),
) )
builder.row(InlineKeyboardButton(text="💰 Баланс", callback_data="my_balance")) if pending:
builder.row(InlineKeyboardButton(text="⏳ Проверить оплату", callback_data="check_pay"))
else:
builder.row(
InlineKeyboardButton(text="💳 Пополнить", callback_data="topup_main"),
InlineKeyboardButton(text="🛒 Купить переходы", callback_data="visit_shop"),
)
builder.row(InlineKeyboardButton(text="🔑 Режим администратора", callback_data="enter_admin_mode")) builder.row(InlineKeyboardButton(text="🔑 Режим администратора", callback_data="enter_admin_mode"))
await self._edit_or_send(message, text, builder.as_markup(), edit) await self._edit_or_send(message, text, builder.as_markup(), edit, user_id=user_id)
async def _show_user_tasks( async def _show_user_tasks(
self, message: Message, user_id: int, self, message: Message, user_id: int,
@@ -1284,9 +1393,9 @@ class BotInterface:
user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == user_id} user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == user_id}
if not user_tasks: if not user_tasks:
text = f"📋 **ЗАДАЧИ** {'пользователя `' + str(user_id) + '`' if for_admin else ''}\n\nЗадач нет." text = f"📋 Задачи {str(user_id) if for_admin else ''}\n\nЗадач нет."
else: else:
header = f"📋 **ЗАДАЧИ** `{user_id}`\n\n" if for_admin else "📋 **МОИ ЗАДАЧИ**\n\n" header = f"📋 Задачи пользователя {user_id}\n\n" if for_admin else "📋 Мои задачи\n\n"
text = header text = header
for tid, p in list(user_tasks.items())[:10]: for tid, p in list(user_tasks.items())[:10]:
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все" domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
@@ -1355,7 +1464,7 @@ class BotInterface:
sent = await message.answer( sent = await message.answer(
f"📺 Канал: `{channel}`\n\n" f"📺 Канал: `{channel}`\n\n"
"Шаг 2/2: Введите домены через запятую:\n" "Шаг 2/2: Введите домены через запятую:\n"
"_Пример: `twitch.tv, youtube.com`_", "Пример: twitch.tv, youtube.com",
reply_markup=builder.as_markup() reply_markup=builder.as_markup()
) )
self._menu_msg[user_id] = sent.message_id self._menu_msg[user_id] = sent.message_id
@@ -1437,11 +1546,159 @@ class BotInterface:
# ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN) # ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN)
# ========================================================================= # =========================================================================
# =========================================================================
# КАБИНЕТ / МАГАЗИН
# =========================================================================
async def _process_payment_input(self, message: Message):
user_id = message.from_user.id
action = self._payment_state.pop(user_id)
await self._safe_delete(message.bot, message.chat.id, message.message_id)
text = message.text.strip() if message.text else ""
prompt_id = self._menu_msg.pop(user_id, None)
async def _edit_prompt(new_text: str, markup=None):
if prompt_id:
try:
await message.bot.edit_message_text(
chat_id=message.chat.id, message_id=prompt_id,
text=new_text, reply_markup=markup
)
return
except Exception:
pass
await message.answer(new_text, reply_markup=markup)
try:
value = int(text)
except (ValueError, TypeError):
await self._send_temp(message, "❌ Введите целое число")
self._payment_state[user_id] = action
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
if action == "topup":
if value < settings.TOPUP_MIN_RUB:
await self._send_temp(message, f"❌ Минимум {settings.TOPUP_MIN_RUB}")
self._payment_state[user_id] = "topup"
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
if value > settings.TOPUP_MAX_RUB:
await self._send_temp(message, f"❌ Максимум {settings.TOPUP_MAX_RUB}")
self._payment_state[user_id] = "topup"
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
order_id = f"{user_id}_topup_{int(datetime.now().timestamp())}"
invoice = await self.heleket.create_invoice(value, order_id)
if not invoice:
await self._send_temp(message, "❌ Ошибка создания счёта. Попробуйте позже.")
return
payment_data = {
"user_id": user_id,
"payment_id": invoice["payment_id"],
"rub": value,
"address": invoice.get("address"),
"url": invoice.get("url"),
"mock": invoice.get("mock", False),
"created_at": datetime.now().isoformat(),
}
await self.payment_storage.save(invoice["payment_id"], payment_data)
builder = InlineKeyboardBuilder()
if invoice.get("url"):
builder.row(InlineKeyboardButton(text="💳 Оплатить через Heleket", url=invoice["url"]))
builder.row(InlineKeyboardButton(text="✅ Я оплатил", callback_data="check_pay"))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
if payment_data["mock"]:
body = "⚠️ Тестовый режим — добавьте HELEKET_API_KEY в .env"
elif invoice.get("url"):
body = "Нажмите кнопку ниже для оплаты"
else:
body = f"Адрес:\n`{invoice['address']}`"
await _edit_prompt(
f"💳 Счёт на оплату\n\n"
f"💰 Сумма: {value}\n\n"
f"{body}\n\n"
f"⏱ Счёт действителен 60 минут",
builder.as_markup()
)
elif action == "buy_clicks":
if value < settings.CLICKS_MIN:
await self._send_temp(message, f"❌ Минимум {settings.CLICKS_MIN} переходов")
self._payment_state[user_id] = "buy_clicks"
if prompt_id:
self._menu_msg[user_id] = prompt_id
return
cost = int(value * settings.CLICK_PRICE_RUB)
rub_bal = await self.rub_storage.get_balance(user_id)
if rub_bal < cost:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить рубли", callback_data="topup_main"))
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
await _edit_prompt(
f"Недостаточно рублей\n\nНужно: {cost} ₽, у вас: {rub_bal}",
builder.as_markup()
)
return
new_rub = await self.rub_storage.deduct(user_id, cost)
new_visits = await self.balance_storage.add_balance(user_id, value)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🛒 Купить ещё", callback_data="visit_shop"))
builder.row(InlineKeyboardButton(text="🔙 В кабинет", callback_data="my_balance"))
await _edit_prompt(
f"✅ Переходы начислены\n\n"
f"+{value} переходов за {cost}\n"
f"Остаток: {new_rub} ₽ | Переходов: {new_visits}",
builder.as_markup()
)
async def _show_invoice(self, callback: CallbackQuery, payment: dict):
"""Отображает счёт на оплату."""
builder = InlineKeyboardBuilder()
if payment.get("url"):
builder.row(InlineKeyboardButton(text="💳 Оплатить через Heleket", url=payment["url"]))
builder.row(InlineKeyboardButton(text="✅ Я оплатил", callback_data="check_pay"))
builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_pay"))
if payment.get("mock"):
body = "⚠️ Тестовый режим — добавьте HELEKET_API_KEY и HELEKET_SHOP_ID в .env"
elif payment.get("url"):
body = f"Нажмите кнопку ниже для оплаты через Heleket"
else:
body = f"Адрес для оплаты:\n`{payment['address']}`"
await callback.message.edit_text(
f"💳 Счёт на оплату\n\n"
f"💰 Сумма: {payment['rub']}\n\n"
f"{body}\n\n"
f"⏱ Счёт действителен 60 минут",
reply_markup=builder.as_markup()
)
# =========================================================================
# ПАНЕЛЬ ПОЛЬЗОВАТЕЛЕЙ (ADMIN)
# =========================================================================
async def _show_users_list(self, message: Message, edit: bool = False): async def _show_users_list(self, message: Message, edit: bool = False):
balances = await self.balance_storage.get_all() balances = await self.balance_storage.get_all()
all_tasks = await self.task_manager.get_all_tasks() all_tasks = await self.task_manager.get_all_tasks()
text = "👥 **ПОЛЬЗОВАТЕЛИ**\n\n" text = "👥 Пользователи\n\n"
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if not balances: if not balances:
@@ -1469,8 +1726,8 @@ class BotInterface:
active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped) active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped)
text = ( text = (
f"👤 **Пользователь** `{target_user_id}`\n\n" f"👤 Пользователь {target_user_id}\n\n"
f"💰 Баланс: **{balance}** переходов\n" f"🔢 Переходов: {balance}\n"
f"📋 Задач: {len(user_tasks)} (активных: {active})\n\n" f"📋 Задач: {len(user_tasks)} (активных: {active})\n\n"
"Пополнить баланс:" "Пополнить баланс:"
) )
+70 -6
View File
@@ -7,7 +7,7 @@ import json
import logging import logging
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict, Optional
from managers.task_manager import TaskParams from managers.task_manager import TaskParams
@@ -173,10 +173,10 @@ class ChatStorage:
await asyncio.to_thread(self._save_sync, data) await asyncio.to_thread(self._save_sync, data)
class BalanceStorage: class _IntBalanceStorage:
"""Баланс переходов пользователей.""" """Базовое хранилище числового баланса пользователей."""
def __init__(self, file_path: str = "data/balances.json"): def __init__(self, file_path: str):
self.file_path = Path(file_path) self.file_path = Path(file_path)
self.file_path.parent.mkdir(parents=True, exist_ok=True) self.file_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
@@ -188,7 +188,7 @@ class BalanceStorage:
with open(self.file_path, "r", encoding="utf-8") as f: with open(self.file_path, "r", encoding="utf-8") as f:
return json.load(f) return json.load(f)
except (json.JSONDecodeError, IOError) as e: except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Failed to load balances: {e}") logger.warning(f"Failed to load {self.file_path}: {e}")
return {} return {}
def _save_sync(self, data: dict) -> None: def _save_sync(self, data: dict) -> None:
@@ -207,7 +207,7 @@ class BalanceStorage:
await asyncio.to_thread(self._save_sync, data) await asyncio.to_thread(self._save_sync, data)
return data[str(user_id)] return data[str(user_id)]
async def deduct(self, user_id: int, amount: int = 1) -> int: async def deduct(self, user_id: int, amount: int) -> int:
async with self._lock: async with self._lock:
data = await asyncio.to_thread(self._load_sync) data = await asyncio.to_thread(self._load_sync)
new_bal = max(0, data.get(str(user_id), 0) - amount) new_bal = max(0, data.get(str(user_id), 0) - amount)
@@ -218,3 +218,67 @@ class BalanceStorage:
async def get_all(self) -> dict: async def get_all(self) -> dict:
async with self._lock: async with self._lock:
return await asyncio.to_thread(self._load_sync) return await asyncio.to_thread(self._load_sync)
class BalanceStorage(_IntBalanceStorage):
"""Баланс переходов пользователей."""
def __init__(self, file_path: str = "data/balances.json"):
super().__init__(file_path)
class RubleBalanceStorage(_IntBalanceStorage):
"""Рублёвый баланс пользователей."""
def __init__(self, file_path: str = "data/rub_balances.json"):
super().__init__(file_path)
class PaymentStorage:
"""Хранилище ожидающих платежей."""
def __init__(self, file_path: str = "data/payments.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):
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 save(self, payment_id: str, data: dict) -> None:
async with self._lock:
payments = await asyncio.to_thread(self._load_sync)
payments[payment_id] = data
await asyncio.to_thread(self._save_sync, payments)
async def get(self, payment_id: str) -> Optional[dict]:
async with self._lock:
payments = await asyncio.to_thread(self._load_sync)
return payments.get(payment_id)
async def delete(self, payment_id: str) -> None:
async with self._lock:
payments = await asyncio.to_thread(self._load_sync)
payments.pop(payment_id, None)
await asyncio.to_thread(self._save_sync, payments)
async def get_by_user(self, user_id: int) -> Optional[dict]:
async with self._lock:
payments = await asyncio.to_thread(self._load_sync)
for pid, data in payments.items():
if data.get("user_id") == user_id:
return {**data, "payment_id": pid}
return None
+90
View File
@@ -0,0 +1,90 @@
"""Heleket payment integration."""
import logging
from typing import Optional
import aiohttp
from config.settings import settings
logger = logging.getLogger(__name__)
class HelketPayment:
"""Heleket payment gateway. Принимает сумму в рублях напрямую."""
BASE_URL = "https://api.heleket.com/v1"
def __init__(self):
self.api_key = settings.HELEKET_API_KEY
self.shop_id = settings.HELEKET_SHOP_ID
self._mock = not (self.api_key and self.shop_id)
async def create_invoice(self, amount_rub: int, order_id: str) -> Optional[dict]:
"""
Создаёт счёт на сумму в рублях.
Возвращает dict: payment_id, address, url. None при ошибке.
"""
if self._mock:
return {
"payment_id": f"mock_{order_id}",
"address": None,
"url": None,
"mock": True,
}
try:
async with aiohttp.ClientSession() as session:
payload = {
"shop_id": self.shop_id,
"amount": str(amount_rub),
"currency": "RUB",
"order_id": order_id,
"url_return": "",
"url_callback": "",
}
headers = {"merchant": self.api_key, "Content-Type": "application/json"}
async with session.post(
f"{self.BASE_URL}/payment",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
data = await resp.json()
if data.get("state") == 0:
r = data.get("result", {})
return {
"payment_id": r.get("uuid"),
"address": r.get("address"),
"url": r.get("url"),
"mock": False,
}
logger.error(f"Heleket create error: {data}")
return None
except Exception as e:
logger.error(f"Heleket request error: {e}")
return None
async def check_status(self, payment_id: str) -> str:
"""Возвращает: 'paid' | 'pending' | 'expired' | 'error'"""
if payment_id.startswith("mock_"):
return "pending"
try:
async with aiohttp.ClientSession() as session:
headers = {"merchant": self.api_key}
async with session.get(
f"{self.BASE_URL}/payment/info/{payment_id}",
headers=headers,
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
data = await resp.json()
if data.get("state") == 0:
status = data.get("result", {}).get("status", "")
if status == "paid":
return "paid"
if status in ("cancel", "expired"):
return "expired"
return "pending"
return "error"
except Exception as e:
logger.error(f"Heleket check error: {e}")
return "error"