10 Commits

6 changed files with 388 additions and 177 deletions

View File

@@ -1,7 +1,12 @@
# Профиль. последнее изменение 24.11.2025 # Профиль. последнее изменение 24.11.2025
from aiogram import Router, types from aiogram import Router, types, F
from aiogram.types import CallbackQuery from aiogram.types import (
CallbackQuery,
LabeledPrice,
Message,
PreCheckoutQuery,
)
import logging import logging
from datetime import datetime from datetime import datetime
from aiogram.enums.parse_mode import ParseMode from aiogram.enums.parse_mode import ParseMode
@@ -88,11 +93,12 @@ async def profile_callback_handler(callback: CallbackQuery):
sub_status = "⚫ Нет активных" sub_status = "⚫ Нет активных"
if sub_data: if sub_data:
expiry_date = sub_data.get("expiry_date") end_date_str = sub_data.get("end_date") # Исправил
if expiry_date: if end_date_str:
try: try:
is_expired = datetime.fromisoformat( is_expired = (
expiry_date) < datetime.now() datetime.fromisoformat(end_date_str) < datetime.now()
)
except ValueError: except ValueError:
is_expired = True is_expired = True
else: else:
@@ -233,15 +239,52 @@ async def popup_confirm_callback_handler(callback: CallbackQuery):
await callback.answer() await callback.answer()
# ===== Telegram Stars =====
@router.callback_query(lambda callback: callback.data.startswith("method_stars_")) @router.callback_query(lambda callback: callback.data.startswith("method_stars_"))
async def method_stars_handler(callback: CallbackQuery): async def method_stars_handler(callback: CallbackQuery):
""" """
Заглушка: оплата через Telegram Stars. Оплата через Telegram Stars.
Формируем invoice прямо из бота, без отдельного биллинга.
""" """
amount = callback.data.split("_")[-1] try:
await callback.message.edit_text( amount_str = callback.data.split("_")[-1]
f"⭐ Оплата через Telegram Stars на {amount} ₽ пока в разработке.\n\n" amount_rub = int(float(amount_str))
"Позже сюда подвяжем реальный платёж.", except Exception:
await callback.message.answer("Некорректная сумма для оплаты.")
await callback.answer()
return
# Внутренний payload, чтобы при успешной оплате понять, что это пополнение баланса
payload = f"stars_topup:{callback.from_user.id}:{amount_rub}"
# 1 ₽ == 1 Star, если нужна другая конвертация — меняй тут
stars_amount = amount_rub
prices = [
LabeledPrice(
label=f"Пополнение баланса на {amount_rub}",
amount=stars_amount,
)
]
try:
await callback.message.answer_invoice(
title="Пополнение баланса Lark VPN",
description=(
f"Пополнение баланса на {amount_rub} ₽ через Telegram Stars.\n\n"
"После успешной оплаты баланс будет зачислен автоматически."
),
payload=payload,
provider_token="", # для Stars провайдер пустой
currency="XTR",
prices=prices,
)
await callback.answer()
except Exception as e:
logger.exception(f"Ошибка при отправке invoice Telegram Stars: {e}")
await callback.message.answer(
"Не удалось создать счёт в Telegram Stars. Попробуй позже или выбери другой способ оплаты."
) )
await callback.answer() await callback.answer()
@@ -383,3 +426,87 @@ async def guide_callback_handler(callback: CallbackQuery):
reply_markup=guide_keyboard(), reply_markup=guide_keyboard(),
) )
await callback.answer() await callback.answer()
# ===== Служебные хендлеры для платежей Telegram Stars =====
@router.pre_checkout_query()
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery):
"""
Обязательный шаг для Telegram Payments:
подтверждаем pre_checkout_query, иначе платёж не пройдёт.
"""
try:
await pre_checkout_query.bot.answer_pre_checkout_query(
pre_checkout_query.id,
ok=True,
)
except Exception as e:
logger.exception(f"Ошибка при answer_pre_checkout_query: {e}")
async def _process_stars_topup(message: Message):
"""
Логика зачисления средств после успешной оплаты Stars.
"""
sp = message.successful_payment
if not sp:
return
payload = sp.invoice_payload or ""
parts = payload.split(":")
if len(parts) != 3 or parts[0] != "stars_topup":
logger.info(
"successful_payment не относится к пополнению баланса Stars.")
return
_, telegram_id_str, amount_str = parts
try:
amount_rub = int(amount_str)
except ValueError:
# На всякий случай fallback к total_amount
amount_rub = sp.total_amount
data = {
"telegram_id": telegram_id_str,
"amount": amount_rub,
"currency": sp.currency,
"provider": "telegram_stars",
"telegram_payment_charge_id": sp.telegram_payment_charge_id,
}
logger.info(
f"Обработка успешного платежа Telegram Stars: "
f"user={telegram_id_str}, amount={amount_rub}, currency={sp.currency}"
)
result = await call_api("POST", "/user/deposit", data)
if result == "ERROR" or result is None:
await message.answer(
"⭐ Оплата через Telegram Stars прошла, но не удалось автоматически обновить баланс.\n"
"Если баланс не изменился — напиши, пожалуйста, в поддержку и укажи время платежа."
)
return
await message.answer(
f"⭐ Оплата через Telegram Stars успешно проведена.\n"
f"На твой баланс зачислено {amount_rub} ₽."
)
@router.message(F.successful_payment)
async def successful_payment_handler(message: Message):
"""
Глобальный хендлер успешных платежей.
Сейчас используем только для пополнения баланса через Stars.
"""
try:
await _process_stars_topup(message)
except Exception as e:
logger.exception(f"Ошибка при обработке successful_payment: {e}")
await message.answer(
"Оплата прошла, но произошла ошибка при обработке. "
"Если баланс не изменился — напиши, пожалуйста, в поддержку."
)

View File

@@ -3,6 +3,8 @@ from aiogram.filters import Command
from aiogram.enums.parse_mode import ParseMode from aiogram.enums.parse_mode import ParseMode
import logging import logging
from .start import call_api
router = Router() router = Router()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -13,22 +15,27 @@ async def _build_referral_text(bot, user_id: int) -> str:
link = f"https://t.me/{bot_username}?start=ref_{user_id}" link = f"https://t.me/{bot_username}?start=ref_{user_id}"
invited_count = ""
try:
data = await call_api("GET", f"/user/{user_id}/referrals")
if isinstance(data, dict):
invited_count = str(data.get("invited_count", 0))
except Exception as e:
logger.exception(
f"Ошибка при получении количества рефералов для {user_id}: {e}"
)
text = ( text = (
"👥 <b>Реферальная программа</b>\n\n" "👥 <b>Реферальная программа</b>\n\n"
"Зови друзей в Lark VPN и получай бонусы на баланс.\n\n" "Зови друзей в Lark VPN и получай бонусы на баланс.\n\n"
f"🔗 Твоя ссылка:\n<code>{link}</code>\n\n" f"🔗 Твоя ссылка:\n<code>{link}</code>\n\n"
"👤 Приглашено: \n" f"👤 Приглашено: {invited_count}\n\n"
"💰 Начислено бонусов: — ₽\n\n"
"Бонусы падают автоматически, когда приглашённые пополняют баланс."
) )
return text return text
@router.message(Command("referrals")) @router.message(Command("referrals"))
async def referrals_command(message: types.Message): async def referrals_command(message: types.Message):
"""
Команда /referrals — показывает текст реферальной программы.
"""
logger.info(f"Получена команда /referrals от {message.from_user.id}") logger.info(f"Получена команда /referrals от {message.from_user.id}")
try: try:
text = await _build_referral_text(message.bot, message.from_user.id) text = await _build_referral_text(message.bot, message.from_user.id)
@@ -40,9 +47,6 @@ async def referrals_command(message: types.Message):
@router.callback_query(lambda callback: callback.data == "referral") @router.callback_query(lambda callback: callback.data == "referral")
async def referrals_callback(callback: types.CallbackQuery): async def referrals_callback(callback: types.CallbackQuery):
"""
Кнопка «Реферальная программа» в главном меню.
"""
try: try:
text = await _build_referral_text( text = await _build_referral_text(
callback.message.bot, callback.message.bot,

View File

@@ -1,114 +1,162 @@
from aiogram import Router, types from aiogram import Router, types
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
import logging from aiogram.filters import Command
from instences.config import BASE_URL_FASTAPI
import aiohttp
from keyboard.keyboards import main_keyboard
from aiogram.enums.parse_mode import ParseMode from aiogram.enums.parse_mode import ParseMode
from .referrals import _build_referral_text import logging
import aiohttp
from instences.config import BASE_URL_FASTAPI
from keyboard.keyboards import main_keyboard
router = Router() router = Router()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def call_api(method, endpoint, data=None): async def call_api(method: str, endpoint: str, data: dict | None = None):
""" """
Выполняет HTTP-запрос к FastAPI. Мини-обёртка для запросов к backend API.
:param method: HTTP метод (GET, POST, и т.д.)
:param endpoint: конечная точка API
:param data: тело запроса (если необходимо)
:return: JSON-ответ или "ERROR" при неуспехе
""" """
url = f"{BASE_URL_FASTAPI}{endpoint}" url = f"{BASE_URL_FASTAPI}{endpoint}"
logger.info(f"Инициализация запроса: {method} {url} с данными {data}") logger.info(f"API {method} {url} data={data}")
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as response: async with session.request(method, url, json=data) as resp:
logger.info( logger.info(f"API response {resp.status} {resp.reason}")
f"Получен ответ от {url}: статус {response.status}" if resp.status in {200, 201}:
) return await resp.json()
if resp.status in {400, 404}:
if response.status in {200, 201}: try:
result = await response.json() return await resp.json()
logger.debug(f"Ответ JSON: {result}") except Exception:
return result
if response.status == 404:
logger.debug(f"Код {response.status}, возвращаю ничего")
return None return None
logger.error( logger.error(f"Unexpected status {resp.status}: {await resp.text()}")
f"Ошибка в запросе: статус {response.status}, причина {response.reason}"
)
return "ERROR" return "ERROR"
except Exception as e: except Exception as e:
logger.exception(f"Исключение при выполнении запроса к {url}: {e}") logger.exception(f"API exception {url}: {e}")
return "ERROR" return "ERROR"
def _welcome_text(username: str | None) -> str: def _welcome_text(username: str | None) -> str:
""" """
Текст приветствия в /start и в главном меню. Текст приветствия в /start и в главном меню.
Имя пока не используем — оставляем сигнатуру на будущее.
""" """
return "🥚 Lark Security\n\nВыберите действие из меню ниже." return "🥚 Lark Security\n\nВыберите действие из меню ниже."
def _parse_referrer_id(message: Message) -> int | None:
"""
Достаём ref_<telegram_id> из /start.
Примеры:
/start
/start ref_123456789
"""
text = message.text or ""
parts = text.split(maxsplit=1)
if len(parts) < 2:
return None
arg = parts[1].strip()
if not arg.startswith("ref_"):
return None
raw_id = arg[4:]
if not raw_id.isdigit():
return None
try:
return int(raw_id)
except ValueError:
return None
@router.message(Command("start")) @router.message(Command("start"))
async def start_command(message: Message): async def start_command(message: Message):
""" """
Обработчик команды /start. /start c обработкой реферального параметра.
Визуал и текст — обновлены, логика работы с API не тронута.
Логика:
- проверяем, есть ли пользователь в БД по telegram_id;
- если нет — создаём /user/create;
- если есть корректный ref_ и пользователь новый — вызываем
/user/{referrer_id}/add_referral с invited_id = telegram_id.
""" """
user_id = message.from_user.id
username = message.from_user.username
referrer_id = _parse_referrer_id(message)
logger.info( logger.info(
f"Получена команда /start от пользователя: " f"[start] Команда /start от {user_id} (@{username}), "
f"{message.from_user.id} ({message.from_user.username})" f"text={message.text!r}, referrer_id={referrer_id}"
) )
try: try:
user_data = await call_api("GET", f"/user/{message.from_user.id}") existing = await call_api("GET", f"/user/{user_id}")
if not user_data: user_exists = isinstance(
logger.debug( existing, dict) and existing.get("id") is not None
"Пользователь не найден в базе, создаем новую запись." if not user_exists:
) logger.debug(f"[start] Пользователь {user_id} не найден, создаю.")
await call_api( create_result = await call_api(
"POST", "POST",
"/user/create", "/user/create",
{"telegram_id": message.from_user.id}, {"telegram_id": user_id},
)
if create_result == "ERROR":
logger.error(
f"[start] Не удалось создать пользователя {user_id} в БД"
) )
logger.debug("Отправка приветственного сообщения пользователю.") if referrer_id is not None:
if referrer_id == user_id:
logger.info(
f"[start] Пользователь {user_id} попытался зайти "
f"по своей реферальной ссылке."
)
await message.answer( await message.answer(
_welcome_text(message.from_user.username), "Нельзя переходить по своей же реферальной ссылке."
)
elif user_exists:
logger.info(
f"[start] Пользователь {user_id} уже есть в БД, "
f"реферальная ссылка {referrer_id} не сработает."
)
await message.answer(
"Вы уже зарегистрированы в боте, "
"реферальная ссылка не сработает."
)
else:
payload = {
"invited_id": user_id,
}
logger.info(
f"[start] Фиксирую реферала в бекенде: "
f"referrer_id={referrer_id}, payload={payload}"
)
result = await call_api(
"POST",
f"/user/{referrer_id}/add_referral",
payload,
)
if result == "ERROR":
logger.error(
f"[start] Ошибка при фиксации реферала через "
f"/user/{referrer_id}/add_referral: referrer={referrer_id}, "
f"invited={user_id}"
)
await message.answer("Вы вошли по реферальной ссылке.")
await message.answer(
_welcome_text(username),
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
logger.info("Приветственное сообщение отправлено.") logger.info(f"[start] Главное меню отправлено пользователю {user_id}.")
except Exception as e: except Exception as e:
logger.exception( logger.exception(
f"Ошибка при обработке команды /start для пользователя " f"[start] Ошибка при обработке /start для пользователя {user_id}: {e}"
f"{message.from_user.id}: {e}"
)
await message.answer("Произошла ошибка. Попробуйте позже.")
@router.message(Command("referrals"))
async def referrals_menu_command(message: Message):
"""
Команда /referrals из бокового меню Telegram.
Показывает текст реферальной программы.
"""
logger.info(
f"Получена команда /referrals от пользователя: "
f"{message.from_user.id} ({message.from_user.username})"
)
try:
text = await _build_referral_text(message.bot, message.from_user.id)
await message.answer(text, parse_mode=ParseMode.HTML)
logger.info("Реферальная программа отправлена пользователю.")
except Exception as e:
logger.exception(
f"Ошибка при обработке команды /referrals для пользователя "
f"{message.from_user.id}: {e}"
) )
await message.answer("Произошла ошибка. Попробуйте позже.") await message.answer("Произошла ошибка. Попробуйте позже.")
@@ -116,24 +164,27 @@ async def referrals_menu_command(message: Message):
@router.callback_query(lambda callback: callback.data == "base") @router.callback_query(lambda callback: callback.data == "base")
async def start_callback_handler(callback: CallbackQuery): async def start_callback_handler(callback: CallbackQuery):
""" """
Обработчик callback_query с data="base". Callback с data="base" — возврат в главное меню.
Возвращает пользователя в главное меню.
""" """
try: try:
user_data = await call_api("GET", f"/user/{callback.from_user.id}") user_id = callback.from_user.id
if not user_data: username = callback.from_user.username
logger.info(f"[start] callback 'base' от {user_id} (@{username})")
user_data = await call_api("GET", f"/user/{user_id}")
if user_data in (None, "ERROR"):
await call_api( await call_api(
"POST", "POST",
"/user/create", "/user/create",
{"telegram_id": callback.from_user.id}, {"telegram_id": user_id},
) )
await callback.message.edit_text( await callback.message.edit_text(
_welcome_text(callback.from_user.username), _welcome_text(username),
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
except Exception as e: except Exception as e:
logger.exception(f"Ошибка при обработке callback с data='base': {e}") logger.exception(f"[start] Ошибка при обработке callback 'base': {e}")
await callback.message.answer("Произошла ошибка. Попробуйте позже.") await callback.message.answer("Произошла ошибка. Попробуйте позже.")
finally: finally:
await callback.answer() await callback.answer()

View File

@@ -71,19 +71,15 @@ def _plan_human_title(plan: str) -> str:
async def _fetch_user_and_subs(telegram_id: int): async def _fetch_user_and_subs(telegram_id: int):
"""
Берём пользователя и список его подписок с backend.
"""
user = await call_api("GET", f"/user/{telegram_id}") user = await call_api("GET", f"/user/{telegram_id}")
if not user or not isinstance(user, dict): if not isinstance(user, dict):
return None, []
user_id = user.get("id") or user.get("telegram_id") or user.get("user_id")
if not user_id:
return None, [] return None, []
user_id = user["id"]
subs = await call_api("GET", f"/subscriptions/{user_id}") subs = await call_api("GET", f"/subscriptions/{user_id}")
if subs == "ERROR" or not isinstance(subs, list): if subs == "ERROR" or not isinstance(subs, list):
subs = [] subs = []
return user, subs return user, subs
@@ -129,15 +125,15 @@ async def _show_subscriptions_view(
index = max(0, min(index, len(subs) - 1)) index = max(0, min(index, len(subs) - 1))
sub = subs[index] sub = subs[index]
plan_title = _plan_human_title(sub.get("plan")) plan_title = _plan_human_title(sub.get("plan_name"))
expiry_raw = sub.get("expiry_date") end_raw = sub.get("end_date") # Исправил
date_str = "" date_str = ""
days_left = None days_left = None
status_emoji = "🟢" status_emoji = "🟢"
if expiry_raw: if end_raw:
try: try:
dt = datetime.fromisoformat(expiry_raw) dt = datetime.fromisoformat(end_raw)
date_str = dt.strftime("%d.%m.%Y") date_str = dt.strftime("%d.%m.%Y")
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now() now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
days_left = (dt.date() - now.date()).days days_left = (dt.date() - now.date()).days
@@ -272,29 +268,28 @@ async def cb_sub_cfg(callback: types.CallbackQuery):
await callback.answer() await callback.answer()
return return
if not isinstance(detail, str) or not detail: 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.message.answer(
"Что-то пошло не так при выдаче конфига." "Что-то пошло не так при выдаче конфига."
) )
await callback.answer() await callback.answer()
return return
escaped = escape_markdown_v2(detail)
text = f"Твой конфиг:\n```{escaped}```"
await callback.message.answer( await callback.message.answer(
text, f"Твой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.MARKDOWN_V2, parse_mode=ParseMode.HTML,
) )
await callback.answer() await callback.answer()
@router.callback_query(lambda c: c.data.startswith("sub_renew:")) @router.callback_query(lambda c: c.data.startswith("sub_renew:"))
async def cb_sub_renew(callback: types.CallbackQuery): async def cb_sub_renew(callback: types.CallbackQuery):
"""
Кнопка 'Продлить' в карточке подписки.
По факту — повторный /subscription/buy с тем же планом.
"""
sub_id = callback.data.split(":", maxsplit=1)[ sub_id = callback.data.split(":", maxsplit=1)[
1] if ":" in callback.data else None 1] if ":" in callback.data else None
user, subs = await _fetch_user_and_subs(callback.from_user.id) user, subs = await _fetch_user_and_subs(callback.from_user.id)
@@ -310,8 +305,8 @@ async def cb_sub_renew(callback: types.CallbackQuery):
await callback.answer() await callback.answer()
return return
plan_id = target.get("plan") plan_name = target.get("plan_name")
if not plan_id: if not plan_name:
await callback.message.answer("Не удалось определить тариф для продления.") await callback.message.answer("Не удалось определить тариф для продления.")
await callback.answer() await callback.answer()
return return
@@ -319,10 +314,9 @@ async def cb_sub_renew(callback: types.CallbackQuery):
result = await call_api( result = await call_api(
"POST", "POST",
"/subscription/buy", "/subscription/buy",
{"telegram_id": str(callback.from_user.id), "plan_id": plan_id}, {"telegram_id": str(callback.from_user.id), "plan_name": plan_name},
) )
# Ошибки backend (detail)
if result == "ERROR" or not isinstance(result, dict): if result == "ERROR" or not isinstance(result, dict):
await callback.message.answer("Ошибка при продлении. Попробуй позже.") await callback.message.answer("Ошибка при продлении. Попробуй позже.")
await callback.answer() await callback.answer()
@@ -342,19 +336,16 @@ async def cb_sub_renew(callback: types.CallbackQuery):
await callback.answer() await callback.answer()
return return
# Успех — backend вернёт message с URI
uri = result.get("message") uri = result.get("message")
if isinstance(uri, str) and uri: if isinstance(uri, str) and uri:
escaped_uri = escape_markdown_v2(uri)
await callback.message.answer( await callback.message.answer(
"Подписка успешно продлена.\n" "Подписка успешно продлена.\n"
f"Твой конфиг:\n```{escaped_uri}```", f"Твой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.MARKDOWN_V2, parse_mode=ParseMode.HTML,
) )
else: else:
await callback.message.answer("Подписка успешно продлена.") await callback.message.answer("Подписка успешно продлена.")
# Перерисовать карточку (обновлённый список)
await _show_subscriptions_view( await _show_subscriptions_view(
msg=callback.message, msg=callback.message,
telegram_id=callback.from_user.id, telegram_id=callback.from_user.id,
@@ -363,9 +354,9 @@ async def cb_sub_renew(callback: types.CallbackQuery):
) )
await callback.answer() await callback.answer()
# ===== Меню покупки подписки (Basic / Pro / Family) ===== # ===== Меню покупки подписки (Basic / Pro / Family) =====
@router.callback_query(lambda callback: callback.data == "buy_subscription") @router.callback_query(lambda callback: callback.data == "buy_subscription")
async def buy_subscription_callback_handler(callback: types.CallbackQuery): async def buy_subscription_callback_handler(callback: types.CallbackQuery):
""" """
@@ -457,13 +448,10 @@ async def lark_tariff_callback_handler(callback: types.CallbackQuery):
@router.callback_query(lambda callback: callback.data.startswith("confirm:")) @router.callback_query(lambda callback: callback.data.startswith("confirm:"))
async def confirm_callback_handler(callback: types.CallbackQuery): async def confirm_callback_handler(callback: types.CallbackQuery):
"""
Подтверждение покупки тарифа.
"""
try: try:
data = callback.data.split(":", maxsplit=1)[1] data = callback.data.split(":", maxsplit=1)[1]
name, classif, amount_str = data.split("_") name, classif, amount_str = data.split("_")
plan_id = f"{name}_{classif}_{amount_str}" plan_name = f"{name}_{classif}_{amount_str}"
except Exception: except Exception:
await callback.message.edit_text("Некорректные данные тарифа.") await callback.message.edit_text("Некорректные данные тарифа.")
await callback.answer() await callback.answer()
@@ -472,7 +460,7 @@ async def confirm_callback_handler(callback: types.CallbackQuery):
result = await call_api( result = await call_api(
"POST", "POST",
"/subscription/buy", "/subscription/buy",
{"telegram_id": str(callback.from_user.id), "plan_id": plan_id}, {"telegram_id": str(callback.from_user.id), "plan_name": plan_name},
) )
if result == "ERROR" or not isinstance(result, dict): if result == "ERROR" or not isinstance(result, dict):
@@ -499,12 +487,10 @@ async def confirm_callback_handler(callback: types.CallbackQuery):
uri = result.get("message", "") uri = result.get("message", "")
if uri: if uri:
escaped_uri = escape_markdown_v2(uri)
answer_text = f"Подписка успешно оформлена!\n\nТвой конфиг:\n```{escaped_uri}```"
await callback.message.edit_text("Подписка успешно оформлена.") await callback.message.edit_text("Подписка успешно оформлена.")
await callback.message.answer( await callback.message.answer(
answer_text, f"Подписка успешно оформлена!\n\nТвой конфиг:\n<code>{uri}</code>",
parse_mode=ParseMode.MARKDOWN_V2, parse_mode=ParseMode.HTML,
) )
else: else:
await callback.message.edit_text("Подписка успешно оформлена.") await callback.message.edit_text("Подписка успешно оформлена.")

View File

@@ -1,20 +1,26 @@
from aiogram import Router, types from aiogram import Router, types
from aiogram.filters import Command from aiogram.filters import Command
import logging import logging
from datetime import datetime
from instences.config import BASE_URL_FASTAPI from instences.config import BASE_URL_FASTAPI
import aiohttp import aiohttp
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from keyboard.keyboards import faq_keyboard, sup_keyboard, ticket_list_keyboard, ticket_keyboard from keyboard.keyboards import (
faq_keyboard,
sup_keyboard,
ticket_list_keyboard,
ticket_keyboard,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = Router() router = Router()
class TicketState(StatesGroup): class TicketState(StatesGroup):
subject = State() subject = State()
message = State() message = State()
async def call_api(method, endpoint, data=None): async def call_api(method, endpoint, data=None):
""" """
Выполняет HTTP-запрос к FastAPI. Выполняет HTTP-запрос к FastAPI.
@@ -25,7 +31,8 @@ async def call_api(method, endpoint, data=None):
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.request(method, url, json=data) as response: async with session.request(method, url, json=data) as response:
logger.info(f"Получен ответ от {url}: статус {response.status}") logger.info(
f"Получен ответ от {url}: статус {response.status}")
if response.status in {200, 201}: if response.status in {200, 201}:
result = await response.json() result = await response.json()
@@ -34,12 +41,15 @@ async def call_api(method, endpoint, data=None):
if response.status == 404: if response.status == 404:
logger.debug(f"Код {response.status}, возвращаю ничего") logger.debug(f"Код {response.status}, возвращаю ничего")
return None return None
logger.error(f"Ошибка в запросе: статус {response.status}, причина {response.reason}") logger.error(
f"Ошибка в запросе: статус {response.status}, причина {response.reason}"
)
return "ERROR" return "ERROR"
except Exception as e: except Exception as e:
logger.exception(f"Исключение при выполнении запроса к {url}: {e}") logger.exception(f"Исключение при выполнении запроса к {url}: {e}")
return "ERROR" return "ERROR"
@router.callback_query(lambda callback: callback.data == "faq") @router.callback_query(lambda callback: callback.data == "faq")
async def faq_callback_handler(callback: types.CallbackQuery): async def faq_callback_handler(callback: types.CallbackQuery):
""" """
@@ -47,46 +57,64 @@ async def faq_callback_handler(callback: types.CallbackQuery):
""" """
await callback.message.edit_text( await callback.message.edit_text(
"FAQ YOU", "FAQ YOU",
reply_markup=faq_keyboard() reply_markup=faq_keyboard(),
) )
await callback.answer()
@router.message(Command("support")) @router.message(Command("support"))
async def supp(message: types.Message): async def supp(message: types.Message):
""" """
Меню сапп системы Меню сапп системы (через команду).
""" """
await message.answer( await message.answer(
"Добро пожаловать в саппорт систему!", "Добро пожаловать в саппорт систему!",
reply_markup=sup_keyboard() reply_markup=sup_keyboard(),
) )
@router.callback_query(lambda callback: callback.data == "main_sup") @router.callback_query(lambda callback: callback.data == "main_sup")
async def supp_callback(callback: types.CallbackQuery): async def supp_callback(callback: types.CallbackQuery):
""" """
Меню сапп системы (callback версия) Меню сапп системы (callback-версия).
""" """
await callback.message.answer( await callback.message.edit_text(
"Добро пожаловать в саппорт систему!", "Добро пожаловать в саппорт систему!",
reply_markup=sup_keyboard() reply_markup=sup_keyboard(),
) )
await callback.answer()
@router.callback_query(lambda callback: callback.data == "my_tickets") @router.callback_query(lambda callback: callback.data == "my_tickets")
async def list_tickets_callback(callback: types.CallbackQuery): async def list_tickets_callback(callback: types.CallbackQuery):
user_id = callback.from_user.id user_id = callback.from_user.id
user_data = await call_api("GET", f"/user/{user_id}") user_data = await call_api("GET", f"/user/{user_id}")
if not user_data: if not user_data:
user_data = await call_api("POST", f"/user/create", {"telegram_id": f"{user_id}"}) user_data = await call_api(
"POST",
"/user/create",
{"telegram_id": f"{user_id}"},
)
tickets = await call_api("GET", f"/support/tickets?user_id={user_data['id']}") tickets = await call_api(
"GET",
f"/support/tickets?user_id={user_data['id']}",
)
if tickets == "ERROR" or not tickets: if tickets == "ERROR" or not tickets:
await callback.message.edit_text("У вас нет тикетов.", reply_markup=sup_keyboard()) await callback.message.edit_text(
"У вас нет тикетов.",
reply_markup=sup_keyboard(),
)
await callback.answer()
return return
await callback.message.edit_text( await callback.message.edit_text(
"Ваши тикеты:", "Ваши тикеты:",
reply_markup=ticket_list_keyboard(tickets) reply_markup=ticket_list_keyboard(tickets),
) )
await callback.answer()
@router.callback_query(lambda callback: callback.data == "make_ticket") @router.callback_query(lambda callback: callback.data == "make_ticket")
async def start_ticket_creation(callback: types.CallbackQuery, state: FSMContext): async def start_ticket_creation(callback: types.CallbackQuery, state: FSMContext):
@@ -95,9 +123,11 @@ async def start_ticket_creation(callback: types.CallbackQuery, state: FSMContext
""" """
await callback.message.answer( await callback.message.answer(
"Введите тему тикета (или нажмите 'Отмена', чтобы выйти):", "Введите тему тикета (или нажмите 'Отмена', чтобы выйти):",
reply_markup=ticket_keyboard() reply_markup=ticket_keyboard(),
) )
await state.set_state(TicketState.subject) await state.set_state(TicketState.subject)
await callback.answer()
@router.message() @router.message()
async def handle_ticket_input(message: types.Message, state: FSMContext): async def handle_ticket_input(message: types.Message, state: FSMContext):
@@ -107,7 +137,10 @@ async def handle_ticket_input(message: types.Message, state: FSMContext):
current_state = await state.get_state() current_state = await state.get_state()
if current_state == TicketState.subject: if current_state == TicketState.subject:
await state.update_data(subject=message.text) await state.update_data(subject=message.text)
await message.answer("Введите описание проблемы:", reply_markup=ticket_keyboard()) await message.answer(
"Введите описание проблемы:",
reply_markup=ticket_keyboard(),
)
await state.set_state(TicketState.message) await state.set_state(TicketState.message)
elif current_state == TicketState.message: elif current_state == TicketState.message:
@@ -117,14 +150,23 @@ async def handle_ticket_input(message: types.Message, state: FSMContext):
await create_ticket(message, subject, message_text, state) await create_ticket(message, subject, message_text, state)
await state.clear() await state.clear()
async def create_ticket(message: types.Message, subject: str, message_text: str, state: FSMContext):
async def create_ticket(
message: types.Message,
subject: str,
message_text: str,
state: FSMContext,
):
""" """
Отправляет запрос на создание тикета через FastAPI. Отправляет запрос на создание тикета через FastAPI.
""" """
user_id = message.from_user.id user_id = message.from_user.id
try: try:
logger.info(f"Создание тикета для пользователя {user_id}: Тема - {subject}, Сообщение - {message_text}") logger.info(
f"Создание тикета для пользователя {user_id}: "
f"Тема - {subject}, Сообщение - {message_text}"
)
user_data = await call_api("GET", f"/user/{user_id}") user_data = await call_api("GET", f"/user/{user_id}")
if not user_data: if not user_data:
@@ -134,12 +176,12 @@ async def create_ticket(message: types.Message, subject: str, message_text: str,
ticket_data = await call_api( ticket_data = await call_api(
"POST", "POST",
f"/support/tickets?user_id={user_data['id']}", f"/support/tickets?user_id={user_data['id']}",
data={"subject": subject, "message": message_text} data={"subject": subject, "message": message_text},
) )
if ticket_data != "ERROR": if ticket_data != "ERROR":
await message.answer( await message.answer(
f"✅ Тикет успешно создан!\n" "✅ Тикет успешно создан!\n"
f"📌 Тема: {ticket_data['subject']}\n" f"📌 Тема: {ticket_data['subject']}\n"
f"📊 Статус: {ticket_data['status']}\n" f"📊 Статус: {ticket_data['status']}\n"
f"📅 Дата создания: {ticket_data['created_at']}" f"📅 Дата создания: {ticket_data['created_at']}"
@@ -148,13 +190,20 @@ async def create_ticket(message: types.Message, subject: str, message_text: str,
await message.answer("❌ Ошибка создания тикета. Попробуйте позже.") await message.answer("❌ Ошибка создания тикета. Попробуйте позже.")
except Exception as e: except Exception as e:
logger.exception(f"Ошибка при создании тикета для пользователя {user_id}: {e}") logger.exception(
f"Ошибка при создании тикета для пользователя {user_id}: {e}"
)
await message.answer("❌ Произошла ошибка при создании тикета.") await message.answer("❌ Произошла ошибка при создании тикета.")
@router.callback_query(lambda callback: callback.data == "cancel_ticket") @router.callback_query(lambda callback: callback.data == "cancel_ticket")
async def cancel_ticket_creation(callback: types.CallbackQuery, state: FSMContext): async def cancel_ticket_creation(callback: types.CallbackQuery, state: FSMContext):
""" """
Отмена создания тикета. Отмена создания тикета.
""" """
await state.clear() await state.clear()
await callback.message.answer("Создание тикета отменено.", reply_markup=types.ReplyKeyboardRemove()) await callback.message.answer(
"Создание тикета отменено.",
reply_markup=types.ReplyKeyboardRemove(),
)
await callback.answer()

View File

@@ -13,7 +13,6 @@ def main_keyboard():
callback_data="profile", callback_data="profile",
) )
) )
# ------
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="❔ FAQ ❔", text="❔ FAQ ❔",
@@ -102,7 +101,7 @@ def popup_keyboard():
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="🔙 Назад", text="🔙 Назад",
callback_data="profile", # назад в профиль callback_data="profile",
) )
) )
return builder.as_markup() return builder.as_markup()
@@ -121,7 +120,7 @@ def payment_methods_keyboard(amount: int):
) )
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="💵 YooKassa", text="💵 СБП",
callback_data=f"method_ykassa_{amount}", callback_data=f"method_ykassa_{amount}",
) )
) )
@@ -180,7 +179,7 @@ def ticket_keyboard():
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="🔙 Отмена", text="🔙 Отмена",
callback_data="cancel", callback_data="cancel_ticket", # БЫЛО "cancel"
) )
) )
return builder.as_markup() return builder.as_markup()
@@ -317,10 +316,11 @@ def tarif_Lark_family_keyboard():
def subscriptions_card_keyboard(sub_id: str, index: int, total: int): def subscriptions_card_keyboard(sub_id: str, index: int, total: int):
""" """
Карточка подписки: Карточка подписки:
навигация, конфиг, продление, новая, назад. навигация, конфиг, назад в главное меню.
""" """
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
# Навигация по подпискам
nav = [] nav = []
if index > 0: if index > 0:
nav.append( nav.append(
@@ -339,28 +339,22 @@ def subscriptions_card_keyboard(sub_id: str, index: int, total: int):
if nav: if nav:
builder.row(*nav) builder.row(*nav)
# Конфиг
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="🔑 Конфиг", text="🔑 Конфиг",
callback_data=f"sub_cfg:{sub_id}", callback_data=f"sub_cfg:{sub_id}",
),
InlineKeyboardButton(
text="🔁 Продлить",
callback_data=f"sub_renew:{sub_id}",
),
)
builder.row(
InlineKeyboardButton(
text=" Новая",
callback_data="buy_subscription",
) )
) )
# Назад в главное меню
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="🔙 Назад", text="🔙 Назад",
callback_data="profile", callback_data="base", # было "profile" изменил
) )
) )
return builder.as_markup() return builder.as_markup()