Files
Telegram-bot-old/handlers/subscriptions.py

517 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from aiogram import Router, types
from aiogram.enums.parse_mode import ParseMode
from aiogram.filters import Command
import logging
from datetime import datetime
import aiohttp
from instences.config import BASE_URL_FASTAPI
from keyboard.keyboards import (
buy_keyboard,
tarif_Lark_keyboard,
tarif_Lark_pro_keyboard,
tarif_Lark_family_keyboard,
tarif_confirm_keyboard,
subscriptions_card_keyboard,
)
router = Router()
logger = logging.getLogger(__name__)
async def call_api(method: str, endpoint: str, data: dict | None = None):
"""
Унифицированный вызов backend API.
"""
url = f"{BASE_URL_FASTAPI}{endpoint}"
logger.info(f"API {method} {url} data={data}")
try:
async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as resp:
logger.info(f"API response {resp.status} {resp.reason}")
if resp.status in {200, 201}:
return await resp.json()
if resp.status in {400, 404}:
# Возвращаем JSON с detail, чтобы разобрать ошибку
try:
return await resp.json()
except Exception:
return None
logger.error(f"Unexpected status {resp.status}: {await resp.text()}")
return "ERROR"
except Exception as e:
logger.exception(f"API exception {url}: {e}")
return "ERROR"
def escape_markdown_v2(text: str) -> str:
"""
Экранирует спецсимволы для Markdown_V2.
"""
special_chars = r"_*[]()~`>#+-=|{}.!\\"
for ch in special_chars:
text = text.replace(ch, f"\\{ch}")
return text
def _plan_human_title(plan: str) -> str:
"""
Красивое имя тарифа по plan-строке из БД.
"""
if not plan:
return ""
if plan.startswith("Lark_Standart"):
return "Lark Basic"
if plan.startswith("Lark_Pro"):
return "Lark Pro"
if plan.startswith("Lark_Family"):
return "Lark Family"
return plan
async def _fetch_user_and_subs(telegram_id: int):
"""
Бляяяяядь.. убейте
"""
user = await call_api("GET", f"/user/{telegram_id}")
if not isinstance(user, dict) or user.get("id") is None:
return None, []
user_id = user["id"]
subs = await call_api("GET", f"/subscriptions/{user_id}")
if subs == "ERROR" or not isinstance(subs, list):
subs = []
return user, subs
async def _show_subscriptions_view(
msg: types.Message,
telegram_id: int,
index: int = 0,
edit: bool = False,
):
"""
Рендер карточки подписки: навигация + конфиг + продление.
"""
user, subs = await _fetch_user_and_subs(telegram_id)
if not user:
text = "Не удалось получить данные пользователя. Попробуй чуть позже."
if edit:
await msg.edit_text(text)
else:
await msg.answer(text)
return
if not subs:
text = (
"У тебя пока нет активных подписок.\n\n"
"Можешь подключить тариф через меню подписок."
)
if edit:
await msg.edit_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=buy_keyboard(),
)
else:
await msg.answer(
text,
parse_mode=ParseMode.HTML,
reply_markup=buy_keyboard(),
)
return
# Нормализуем индекс
index = max(0, min(index, len(subs) - 1))
sub = subs[index]
plan_title = _plan_human_title(sub.get("plan_name"))
end_raw = sub.get("end_date") # Исправил
date_str = ""
days_left = None
status_emoji = "🟢"
if end_raw:
try:
dt = datetime.fromisoformat(end_raw)
date_str = dt.strftime("%d.%m.%Y")
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
days_left = (dt.date() - now.date()).days
if days_left < 0:
status_emoji = ""
else:
status_emoji = "🟢"
except ValueError:
status_emoji = ""
lines = [
f"{status_emoji} <b>Подписка</b>",
f"Тариф: {plan_title}",
]
if days_left is None:
lines.append(f"Действует до {date_str}")
else:
if days_left >= 0:
lines.append(f"Действует до {date_str} (ещё {days_left} дн.)")
else:
lines.append(f"Истекла {date_str}")
lines.append("")
lines.append(f"{index + 1} из {len(subs)}")
text = "\n".join(lines)
kb = subscriptions_card_keyboard(
sub_id=sub["id"],
index=index,
total=len(subs),
)
if edit:
await msg.edit_text(text, parse_mode=ParseMode.HTML, reply_markup=kb)
else:
await msg.answer(text, parse_mode=ParseMode.HTML, reply_markup=kb)
# ===== Мои подписки / карточка =====
@router.message(Command("subscriptions"))
async def cmd_subscriptions(message: types.Message):
"""
/subscriptions — открыть карточку подписок.
"""
await _show_subscriptions_view(
msg=message,
telegram_id=message.from_user.id,
index=0,
edit=False,
)
@router.callback_query(lambda c: c.data == "go_subscriptions")
async def cb_go_subscriptions(callback: types.CallbackQuery):
"""
Кнопка из других экранов — перейти в 'Мои подписки'.
"""
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=0,
edit=True,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_prev:"))
async def cb_sub_prev(callback: types.CallbackQuery):
"""
Навигация: предыдущая подписка.
"""
try:
index = int(callback.data.split(":")[1])
except (IndexError, ValueError):
index = 0
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=index,
edit=True,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_next:"))
async def cb_sub_next(callback: types.CallbackQuery):
"""
Навигация: следующая подписка.
"""
try:
index = int(callback.data.split(":")[1])
except (IndexError, ValueError):
index = 0
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=index,
edit=True,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_cfg:"))
async def cb_sub_cfg(callback: types.CallbackQuery):
"""
Кнопка 'Конфиг' в карточке подписки.
Тянем URI через /uri?telegram_id=...
"""
result = await call_api(
"GET",
f"/uri?telegram_id={callback.from_user.id}",
)
if result == "ERROR" or not isinstance(result, dict):
await callback.message.answer(
"Не удалось получить конфиг. Попробуй позже."
)
await callback.answer()
return
detail = result.get("detail")
if detail == "SUB_ERROR":
await callback.message.answer(
"У тебя нет активной подписки для генерации конфига."
)
await callback.answer()
return
uri = None
if isinstance(detail, str):
uri = detail.strip()
elif isinstance(detail, list) and detail:
uri = str(detail[0]).strip()
if not uri:
await callback.message.answer(
"Что-то пошло не так при выдаче конфига."
)
await callback.answer()
return
await callback.message.answer(
f"Твой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.HTML,
)
await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_renew:"))
async def cb_sub_renew(callback: types.CallbackQuery):
"""
Кнопка 'Продлить' в карточке подписки.
По факту — повторный /subscription/buy с тем же планом.
"""
sub_id = callback.data.split(":", maxsplit=1)[
1] if ":" in callback.data else None
user, subs = await _fetch_user_and_subs(callback.from_user.id)
if not user or not subs:
await callback.message.answer("Подписка не найдена.")
await callback.answer()
return
target = next((s for s in subs if s.get("id") == sub_id), None)
if not target:
await callback.message.answer("Подписка не найдена.")
await callback.answer()
return
plan_id = target.get("plan_name")
if not plan_id:
await callback.message.answer("Не удалось определить тариф для продления.")
await callback.answer()
return
result = await call_api(
"POST",
"/subscription/buy",
{"telegram_id": str(callback.from_user.id), "plan_id": plan_id},
)
# Ошибки backend (detail)
if result == "ERROR" or not isinstance(result, dict):
await callback.message.answer("Ошибка при продлении. Попробуй позже.")
await callback.answer()
return
detail = result.get("detail")
if detail == "INSUFFICIENT_FUNDS":
await callback.message.answer("Недостаточно средств на балансе.")
await callback.answer()
return
if detail == "TARIFF_NOT_FOUND":
await callback.message.answer("Тариф не найден.")
await callback.answer()
return
if detail == "ACTIVE_SUBSCRIPTION_EXISTS":
await callback.message.answer("У тебя уже есть активная подписка этого типа.")
await callback.answer()
return
# Успех — backend вернёт message с URI
uri = result.get("message")
if isinstance(uri, str) and uri:
escaped_uri = escape_markdown_v2(uri)
await callback.message.answer(
"Подписка успешно продлена.\n"
f"Твой конфиг:\n```{escaped_uri}```",
parse_mode=ParseMode.MARKDOWN_V2,
)
else:
await callback.message.answer("Подписка успешно продлена.")
# Перерисовать карточку (обновлённый список)
await _show_subscriptions_view(
msg=callback.message,
telegram_id=callback.from_user.id,
index=0,
edit=True,
)
await callback.answer()
# ===== Меню покупки подписки (Basic / Pro / Family) =====
@router.callback_query(lambda callback: callback.data == "buy_subscription")
async def buy_subscription_callback_handler(callback: types.CallbackQuery):
"""
Меню выбора тарифа: Basic / Pro / Family + показ баланса.
"""
user_data = await call_api("GET", f"/user/{callback.from_user.id}")
balance = user_data.get("balance", 0) if user_data else "?"
text = (
" <b>Подключить новую подписку</b>\n\n"
"🐣 Lark Basic - 200 ₽ / мес\n"
"🦅 Lark Pro - 400 ₽ / мес\n"
"👨‍👩‍👧 Lark Family - 700 ₽ / мес\n\n"
f"💰 Баланс: {balance}\n\n"
"Выбери тариф:"
)
await callback.message.edit_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=buy_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "subs")
async def subs_callback_handler(callback: types.CallbackQuery):
"""
Кнопка 'Lark Basic' — раскрываем варианты сроков.
"""
await callback.message.edit_text(
"Тариф Lark Basic — выбери срок:",
reply_markup=tarif_Lark_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "subs_pro")
async def subs_pro_callback_handler(callback: types.CallbackQuery):
"""
Кнопка 'Lark Pro'.
"""
await callback.message.edit_text(
"Тариф Lark Pro — выбери срок:",
reply_markup=tarif_Lark_pro_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data == "subs_family")
async def subs_family_callback_handler(callback: types.CallbackQuery):
"""
Кнопка 'Lark Family'.
"""
await callback.message.edit_text(
"Тариф Lark Family — выбери срок:",
reply_markup=tarif_Lark_family_keyboard(),
)
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("Lark:"))
async def lark_tariff_callback_handler(callback: types.CallbackQuery):
"""
Обработчик выбора конкретного тарифа Lark.
callback_data: Lark:<Class>:<months>
"""
try:
_, tariff_class, months_str = callback.data.split(":")
months = int(months_str)
except Exception:
await callback.message.answer("Некорректные данные тарифа.")
await callback.answer()
return
if months == 1:
months_label = "1 месяц"
elif 2 <= months <= 4:
months_label = f"{months} месяца"
else:
months_label = f"{months} месяцев"
text = f"Тариф Lark {tariff_class} на {months_label}. Продолжить покупку?"
keyboard = tarif_confirm_keyboard("Lark", months, tariff_class)
await callback.message.edit_text(text=text, reply_markup=keyboard)
await callback.answer()
@router.callback_query(lambda callback: callback.data.startswith("confirm:"))
async def confirm_callback_handler(callback: types.CallbackQuery):
"""
Подтверждение покупки тарифа.
"""
try:
data = callback.data.split(":", maxsplit=1)[1]
name, classif, amount_str = data.split("_")
plan_id = f"{name}_{classif}_{amount_str}"
except Exception:
await callback.message.edit_text("Некорректные данные тарифа.")
await callback.answer()
return
result = await call_api(
"POST",
"/subscription/buy",
{"telegram_id": str(callback.from_user.id), "plan_id": plan_id},
)
if result == "ERROR" or not isinstance(result, dict):
await callback.message.edit_text("Ошибка при оформлении подписки.")
await callback.answer()
return
detail = result.get("detail")
if detail == "INSUFFICIENT_FUNDS":
await callback.message.edit_text(
"Недостаточно средств. Попробуй пополнить баланс."
)
await callback.answer()
return
if detail == "TARIFF_NOT_FOUND":
await callback.message.edit_text("Тариф не найден.")
await callback.answer()
return
if detail == "ACTIVE_SUBSCRIPTION_EXISTS":
await callback.message.edit_text("У тебя уже есть активная подписка.")
await callback.answer()
return
uri = result.get("message", "")
if uri:
escaped_uri = escape_markdown_v2(uri)
answer_text = f"Подписка успешно оформлена!\n\nТвой конфиг:\n```{escaped_uri}```"
await callback.message.edit_text("Подписка успешно оформлена.")
await callback.message.answer(
answer_text,
parse_mode=ParseMode.MARKDOWN_V2,
)
else:
await callback.message.edit_text("Подписка успешно оформлена.")
await callback.answer()