add heleket
This commit is contained in:
@@ -11,6 +11,14 @@ class Settings(BaseSettings):
|
|||||||
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
|
||||||
API_HASH: str = "abc123"
|
API_HASH: str = "abc123"
|
||||||
|
|||||||
+340
-83
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user