feat: enhance subscription management with discount calculations and update support messages

This commit is contained in:
2026-04-22 17:41:44 +07:00
parent 97c187bafb
commit e3e204d7af
7 changed files with 95 additions and 328 deletions

View File

@@ -1,9 +1,9 @@
import logging import logging
import math
from aiogram import F, Router from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings from config import settings
from keyboards.client import ( from keyboards.client import (
@@ -14,22 +14,20 @@ from keyboards.client import (
) )
from misc.utils import ( from misc.utils import (
calculate_price, calculate_price,
convert_duration_to_int,
convert_int_to_duration, convert_int_to_duration,
get_discount,
) )
from schemas.di import DependenciesDTO
from schemas.subscriptions import SubscriptionPlan from schemas.subscriptions import SubscriptionPlan
from services.user_service import UserServiceError
from states.buy import SubscriptionStorage from states.buy import SubscriptionStorage
from texts import ( from texts import (
CALLBACK_FALLBACK, CALLBACK_FALLBACK,
CHECKOUT_PAYMENT_CHOICE, CHECKOUT_PAYMENT_CHOICE,
CONTEXT_REINITIATED, CONTEXT_REINITIATED,
GENERAL_PROCESSING,
NOTHING_PLACEHOLDER, NOTHING_PLACEHOLDER,
STANDARD_FALLBACK, STANDARD_FALLBACK,
SUBSCRIPTION_DEVICE_SELECTOR, SUBSCRIPTION_DEVICE_SELECTOR,
SUBSCRIPTION_DURATION_SELECTOR, SUBSCRIPTION_DURATION_SELECTOR,
SUPPORT_MSG_SENT,
) )
router = Router() router = Router()
@@ -103,8 +101,12 @@ async def select_duration_init(cb: CallbackQuery, state: FSMContext):
return return
try: try:
discount = get_discount(convert_duration_to_int(ctx.duration))
await cb.message.edit_text( await cb.message.edit_text(
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)), SUBSCRIPTION_DURATION_SELECTOR.format(
price=math.ceil(calculate_price(ctx) * (1 - discount / 100)),
discount=math.floor(discount),
),
reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"), reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"),
) )
except Exception as exc: except Exception as exc:
@@ -132,8 +134,12 @@ async def select_duration(cb: CallbackQuery, state: FSMContext):
duration = int(cb_data[1]) if cb_data[1].isdigit() else 1 duration = int(cb_data[1]) if cb_data[1].isdigit() else 1
ctx.duration = convert_int_to_duration(duration) ctx.duration = convert_int_to_duration(duration)
try: try:
discount = get_discount(convert_duration_to_int(ctx.duration))
await cb.message.edit_text( await cb.message.edit_text(
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)), SUBSCRIPTION_DURATION_SELECTOR.format(
price=math.ceil(calculate_price(ctx) * (1 - discount / 100)),
discount=math.floor(discount),
),
reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"), reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"),
) )
except Exception as exc: except Exception as exc:
@@ -164,28 +170,3 @@ async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext):
await state.set_state(SubscriptionStorage.checkout) await state.set_state(SubscriptionStorage.checkout)
await state.set_data({"ctx": ctx}) await state.set_data({"ctx": ctx})
@router.callback_query(F.data == "checkout")
async def checkout_support(
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
data = await state.get_data()
await state.clear()
ctx: SubscriptionPlan | None = data.get("ctx")
if not ctx:
await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu)
return
await cb.message.edit_text(GENERAL_PROCESSING)
try:
await deps.user_service.create_subscription_ticket(session, cb.from_user, ctx, cb.bot)
await cb.message.edit_text(SUPPORT_MSG_SENT)
except UserServiceError:
await cb.message.edit_text(STANDARD_FALLBACK)
raise
except Exception:
await cb.message.edit_text(STANDARD_FALLBACK)
logger.exception("Exception occured while trying to forward a msg to support")
raise

View File

@@ -4,9 +4,10 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from keyboards.client import main_menu, return_to_menu from keyboards.client import close_kb, main_menu, return_to_menu
from misc.utils import build_main_menu_text, format_subscription_link from misc.utils import build_main_menu_text, format_subscription_link
from schemas.di import DependenciesDTO from schemas.di import DependenciesDTO
from services.rw import get_user_by_telegram_id
from texts import FAQ_TEXT, GENERAL_PROCESSING from texts import FAQ_TEXT, GENERAL_PROCESSING
router = Router() router = Router()
@@ -32,7 +33,7 @@ async def fetch_referal(
session, user_id=msg.from_user.id, referal=referal, rw_sdk=deps.rw_sdk session, user_id=msg.from_user.id, referal=referal, rw_sdk=deps.rw_sdk
) )
await msg.reply( await msg.answer(
build_main_menu_text(format_subscription_link(user.subscription_link)), build_main_menu_text(format_subscription_link(user.subscription_link)),
reply_markup=main_menu, reply_markup=main_menu,
) )
@@ -49,7 +50,7 @@ async def standard_start(
user = await deps.user_service.add_user(session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk) user = await deps.user_service.add_user(session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk)
await msg.reply( await msg.answer(
build_main_menu_text(format_subscription_link(user.subscription_link)), build_main_menu_text(format_subscription_link(user.subscription_link)),
reply_markup=main_menu, reply_markup=main_menu,
) )
@@ -88,3 +89,19 @@ async def faq(cb: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
await cb.message.edit_text(FAQ_TEXT, reply_markup=return_to_menu) await cb.message.edit_text(FAQ_TEXT, reply_markup=return_to_menu)
@router.callback_query(F.data == "sub_info")
async def sub_info(
cb: CallbackQuery, state: FSMContext, deps: DependenciesDTO, session: AsyncSession
):
await state.clear()
await cb.message.edit_text(GENERAL_PROCESSING, reply_markup=close_kb)
rw_user = await get_user_by_telegram_id(deps.rw_sdk, cb.from_user.id)
user = await deps.user_service.update_user_link(session, cb.from_user.id, deps.rw_sdk)
await cb.message.edit_text(
deps.user_service.format_subscription_message(rw_user, link=user.subscription_link),
reply_markup=return_to_menu,
)

View File

@@ -4,14 +4,9 @@ from aiogram import F, Router
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from keyboards.client import return_to_menu from keyboards.client import return_to_menu
from schemas.di import DependenciesDTO from texts import SUPPORT_INIT
from services.user_service import UserServiceError
from states.support import SupportStorage
from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT
router = Router() router = Router()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,7 +17,6 @@ async def support_init(cb: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
await cb.message.edit_text(text=SUPPORT_INIT, reply_markup=return_to_menu) await cb.message.edit_text(text=SUPPORT_INIT, reply_markup=return_to_menu)
await state.set_state(SupportStorage.prompt)
@router.message(Command("support")) @router.message(Command("support"))
@@ -31,60 +25,3 @@ async def support_init_cmd(msg: Message, state: FSMContext):
await msg.delete() await msg.delete()
await msg.answer(text=SUPPORT_INIT, reply_markup=return_to_menu) await msg.answer(text=SUPPORT_INIT, reply_markup=return_to_menu)
await state.set_state(SupportStorage.prompt)
@router.message(SupportStorage.prompt)
async def received_message(
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
notification_msg = await msg.reply(GENERAL_PROCESSING)
try:
await deps.user_service.support_forward_message(session, msg, deps.rw_sdk)
except UserServiceError:
await msg.reply(STANDARD_FALLBACK)
raise
except Exception:
await msg.reply(STANDARD_FALLBACK)
logger.exception("Exception occured while trying to forward a msg to support")
raise
await notification_msg.edit_text(SUPPORT_MSG_SENT, reply_markup=return_to_menu)
await state.set_state(SupportStorage.prompt)
@router.message(
F.chat.id == settings.admin_group_id,
F.message_thread_id.is_not(None),
F.from_user.is_bot == False, # noqa: E712
~Command("close"),
~Command("refpay"),
)
async def handle_admin_message(
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
print(str(msg))
await deps.user_service.support_answer(session, msg)
@router.message(F.chat.id == settings.admin_group_id, Command("close"))
async def close_ticket(
msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
await deps.user_service.close_ticket(session, msg)
@router.callback_query(F.message.chat.id == settings.admin_group_id, F.data == "close")
async def close_ticket_cb(
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
await state.clear()
await deps.user_service.close_ticket(session, cb.message)

View File

@@ -7,13 +7,14 @@ from schemas.subscriptions import SubscriptionDuration
main_menu = InlineKeyboardBuilder( main_menu = InlineKeyboardBuilder(
[ [
[InlineKeyboardButton(text="📊 Подписка", callback_data="sub_info")],
[ [
InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"), InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"),
InlineKeyboardButton(text="FAQ", callback_data="faq"), InlineKeyboardButton(text="⁉️ FAQ", callback_data="faq"),
], ],
[ [
InlineKeyboardButton(text="Наш Канал", url="https://goo.gle"), InlineKeyboardButton(text="📢 Наш Канал", url="https://t.me/maleniavpn"),
InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"), InlineKeyboardButton(text="🎧 Тех. Поддержка", url="https://t.me/maleniasupportbot"),
], ],
[InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")], [InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")],
[InlineKeyboardButton(text="👤 Реферальная система", callback_data="referal")], [InlineKeyboardButton(text="👤 Реферальная система", callback_data="referal")],

View File

@@ -1,3 +1,4 @@
import math
import urllib.parse import urllib.parse
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from random import choice from random import choice
@@ -179,8 +180,15 @@ def calculate_price(subscription: SubscriptionPlan):
) * convert_duration_to_int(subscription.duration) ) * convert_duration_to_int(subscription.duration)
def get_discount(months) -> float:
if months <= 0:
return 0
calculated = 5 * (math.log2(months / 3) + 1)
return max(0, calculated)
def format_subscription_link(link: str | None = None): def format_subscription_link(link: str | None = None):
return f"<code>{link or '...'}</code>" return f"<code>{link or texts.NOTHING_PLACEHOLDER}</code>"
def format_share_deep_link(link: str): def format_share_deep_link(link: str):

View File

@@ -1,26 +1,20 @@
import logging import logging
from aiogram import Bot from aiogram.types import Message
from aiogram.types import Message, ReactionTypeEmoji, User as TelegramUser
from remnawave import RemnawaveSDK from remnawave import RemnawaveSDK
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from db.models.tickets import TicketStatus from db.models.tickets import TicketStatus
from db.models.users import User from db.models.users import User
from keyboards.admins import ticket_actions
from misc import utils from misc import utils
from repositories import UserRepository from repositories import UserRepository
from repositories.tickets import TicketRepository from repositories.tickets import TicketRepository
from schemas.subscriptions import SubscriptionPlan
from schemas.tickets import NewTicketDTO
from schemas.users import NewUserDTO, TicketCreator from schemas.users import NewUserDTO, TicketCreator
from services.rw import RWUserInfo, get_user_by_telegram_id from services.rw import RWUserInfo, get_user_by_telegram_id
from texts import ( from texts import (
ADMIN_STANDARD_FALLBACK, NO_SUBSCRIPTION,
ADMIN_TICKET_WITH_RW, NOTHING_PLACEHOLDER,
ADMIN_TICKET_WITHOUT_RW, SUBSCRIPTION_INFORMATION,
SUPPORT_SIGNATURE,
TICKET_STATUS_EMOJIS, TICKET_STATUS_EMOJIS,
) )
@@ -51,49 +45,29 @@ class UserService:
) )
@staticmethod @staticmethod
async def format_admin_support_message( def format_subscription_message(
ticket_number: int,
user: TelegramUser,
rw_user: RWUserInfo | None, rw_user: RWUserInfo | None,
balance: int, link: str | None = None,
referal_id: int | None = None,
): ):
username_display = utils.format_username(user.username)
referal_display = utils.format_referal_id(referal_id)
if rw_user is None: if rw_user is None:
# Пользователь не найден в Remnawave # Пользователь не найден в Remnawave
return ADMIN_TICKET_WITHOUT_RW.format( return NO_SUBSCRIPTION
ticket_number=ticket_number,
full_name=user.full_name,
username_display=username_display,
telegram_id=user.id,
referal_id=referal_display,
balance=balance,
)
status_icon, status_text = utils.format_status(rw_user.status) status_icon, status_text = utils.format_status(rw_user.status)
expire_date = rw_user.expire_at.strftime("%d.%m.%Y") expire_date = rw_user.expire_at.strftime("%d.%m.%Y")
days_left = utils.format_days_left(rw_user.expire_at) days_left = utils.format_days_left(rw_user.expire_at)
used_traffic = utils.format_traffic(rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes) used_traffic = utils.format_traffic(rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes)
hwid_limit = utils.format_hwid_limit(rw_user.hwid_device_limit) hwid_limit = utils.format_hwid_limit(rw_user.hwid_device_limit)
squads = utils.format_squads(rw_user.active_squads) link = link or NOTHING_PLACEHOLDER
return ADMIN_TICKET_WITH_RW.format( return SUBSCRIPTION_INFORMATION.format(
ticket_number=ticket_number,
full_name=user.full_name,
username_display=username_display,
telegram_id=user.id,
balance=balance,
status_icon=status_icon, status_icon=status_icon,
status=status_text, status=status_text,
expire_date=expire_date, expire_date=expire_date,
days_left=days_left, days_left=days_left,
used_traffic=used_traffic, used_traffic=used_traffic,
hwid_limit=hwid_limit, hwid_limit=hwid_limit,
squads=squads, link=link,
remnawave_uuid=rw_user.uuid,
referal_id=referal_display,
) )
async def add_user( async def add_user(
@@ -114,154 +88,6 @@ class UserService:
model = NewUserDTO(id=user_id, referal=referal, link=link) model = NewUserDTO(id=user_id, referal=referal, link=link)
return await self.user_repository.create_user(session, model) return await self.user_repository.create_user(session, model)
async def create_subscription_ticket(
self,
session: AsyncSession,
user: TelegramUser,
subscription: SubscriptionPlan,
bot: Bot,
):
ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id)
db_user = await self.user_repository.get_user_by_id(session, user.id)
if ticket:
try:
await bot.send_message(
chat_id=settings.admin_group_id,
message_thread_id=ticket.thread_id,
text=utils.format_support_subscription(
subscription=subscription,
full_name=user.full_name,
user_id=user.id,
username=user.username,
balance=db_user.balance,
referal_id=db_user.referal_id,
),
reply_markup=ticket_actions,
)
return
except Exception:
await self.ticket_repository.update_ticket_status(
session, ticket, TicketStatus.CLOSED
)
raise
creator = TicketCreator(id=user.id, username=user.username)
ticket_dto = NewTicketDTO(
creator_id=user.id,
)
ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto)
thread = await bot.create_forum_topic(
settings.admin_group_id,
self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING),
)
ticket = await self.ticket_repository.update_ticket_thread_id(
session, ticket, thread.message_thread_id
)
db_user = await self.user_repository.get_user_by_id(session, ticket.creator_id)
await bot.send_message(
chat_id=settings.admin_group_id,
message_thread_id=ticket.thread_id,
text=utils.format_support_subscription(
subscription=subscription,
full_name=user.full_name,
user_id=user.id,
username=user.username,
balance=db_user.balance,
referal_id=db_user.referal_id,
),
reply_markup=ticket_actions,
)
async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK):
user = await self.user_repository.get_user_by_id(session, msg.from_user.id)
if not user:
raise UserServiceError("User %d is not found.", msg.from_user.id)
ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id)
if ticket:
try:
await msg.copy_to(
chat_id=settings.admin_group_id, message_thread_id=ticket.thread_id
)
return
except Exception:
await self.ticket_repository.update_ticket_status(
session, ticket, TicketStatus.CLOSED
)
raise
rw_user = await get_user_by_telegram_id(rw, user.id)
creator = TicketCreator(id=user.id, username=msg.from_user.username)
ticket_dto = NewTicketDTO(
creator_id=user.id,
)
ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto)
thread = await msg.bot.create_forum_topic(
settings.admin_group_id,
self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING),
)
ticket = await self.ticket_repository.update_ticket_thread_id(
session, ticket, thread.message_thread_id
)
await msg.bot.send_message(
settings.admin_group_id,
message_thread_id=thread.message_thread_id,
text=await self.format_admin_support_message(
ticket_number=ticket.id,
user=msg.from_user,
rw_user=rw_user,
balance=user.balance,
referal_id=user.referal_id,
),
reply_markup=ticket_actions,
)
await msg.copy_to(settings.admin_group_id, message_thread_id=thread.message_thread_id)
async def support_answer(self, session: AsyncSession, msg: Message):
thread_id = msg.message_thread_id
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
if ticket.status == TicketStatus.PENDING:
await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.OPEN)
await msg.bot.edit_forum_topic(
settings.admin_group_id,
ticket.thread_id,
name=self.format_ticket_name(TicketCreator(ticket.creator_id), ticket_id=ticket.id),
)
try:
await msg.bot.send_message(ticket.creator_id, SUPPORT_SIGNATURE)
await msg.copy_to(ticket.creator_id)
await msg.react([ReactionTypeEmoji(emoji="")])
except Exception:
await msg.reply(ADMIN_STANDARD_FALLBACK)
async def close_ticket(self, session: AsyncSession, msg: Message):
thread_id = msg.message_thread_id
if not thread_id:
return
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
await msg.bot.edit_forum_topic(
settings.admin_group_id,
ticket.thread_id,
name=self.format_ticket_name(
TicketCreator(ticket.creator_id),
ticket_id=ticket.id,
status=TicketStatus.CLOSED,
),
)
await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.CLOSED)
await msg.bot.close_forum_topic(settings.admin_group_id, message_thread_id=thread_id)
async def update_user_link( async def update_user_link(
self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK
) -> User: ) -> User:
@@ -278,16 +104,6 @@ class UserService:
return user return user
async def fetch_user_by_thread(self, session: AsyncSession, thread_id: int) -> User | None:
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
return await self.user_repository.get_user_by_id(session, ticket.creator_id)
async def fetch_referal_by_thread(self, session: AsyncSession, thread_id: int) -> User | None:
ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id)
return await self.user_repository.get_referal_by_user_id(session, ticket.creator_id)
async def send_promotion_to_users(self, session: AsyncSession, msg: Message) -> list[User]: async def send_promotion_to_users(self, session: AsyncSession, msg: Message) -> list[User]:
users = await self.user_repository.get_users(session) users = await self.user_repository.get_users(session)

View File

@@ -23,6 +23,21 @@ MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒")
MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧") MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧")
# ============================================================
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
# ============================================================
_SUPPORT_USER_DESCRIPTION = (
"👤 <b>Профиль:</b> {full_name}\n"
"🔗 <b>Alias:</b> {username_display}\n"
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
"❤️ <b>Referal UID:</b> <code>{referal_id}</code>\n\n"
"💰 <b>Баланс:</b> <code>{balance}</code>"
)
_DIVIDER = "────────────────────────────\n\n"
# ============================================================ # ============================================================
# МАППИНГИ # МАППИНГИ
# ============================================================ # ============================================================
@@ -46,7 +61,7 @@ STATUS_ICON_LIMITED = "🟠"
STATUS_ICON_UNKNOWN = "" STATUS_ICON_UNKNOWN = ""
# — Тексты статусов # — Тексты статусов
STATUS_ACTIVE = "работает" STATUS_ACTIVE = "активна"
STATUS_DISABLED = "отключена" STATUS_DISABLED = "отключена"
STATUS_EXPIRED = "истёк" STATUS_EXPIRED = "истёк"
STATUS_LIMITED = "трафик ограничен" STATUS_LIMITED = "трафик ограничен"
@@ -109,7 +124,9 @@ FAQ_TEXT = (
GENERAL_PROCESSING = "<i>⏳ Подождите...</i>" GENERAL_PROCESSING = "<i>⏳ Подождите...</i>"
STANDARD_FALLBACK = "<b>❌ Ошибка.</b> Прожмите /start. Если не поможет — значит, мы где-то не доглядели." STANDARD_FALLBACK = (
"<b>❌ Ошибка.</b> Прожмите /start. Если не поможет — значит, мы где-то не доглядели."
)
CALLBACK_FALLBACK = "❌ Что-то пошло не так. Повторите попытку или обратитесь в поддержку." CALLBACK_FALLBACK = "❌ Что-то пошло не так. Повторите попытку или обратитесь в поддержку."
@@ -117,6 +134,18 @@ CONTEXT_REINITIATED = "🌑 Сессия сброшена. Данные могл
UNDEFINED_MESSAGE = "❌ Таких команд у нас нет.\n\n<i>Ищете техподдержку? Вам в /support</i>\n<i>Потерялись? Вам в /start.</i>" UNDEFINED_MESSAGE = "❌ Таких команд у нас нет.\n\n<i>Ищете техподдержку? Вам в /support</i>\n<i>Потерялись? Вам в /start.</i>"
SUBSCRIPTION_INFORMATION = (
"<b>📊 Данные о подписке</b>\n\n"
f"{MALENIA_LINK} Ссылка на подписку: <code>{{link}}</code>\n\n"
"{status_icon} <b>Статус:</b> {status}\n"
+ "📅 <b>Истекает:</b> {expire_date}\n"
+ "⏳ <b>Осталось дней:</b> {days_left}\n"
+ "📊 <b>Израсходовано трафика:</b> {used_traffic}\n"
+ "📱 <b>Количество устройств:</b> {hwid_limit}\n"
)
NO_SUBSCRIPTION = "⚠️ <b>Подписка не найдена.</b>"
# ============================================================ # ============================================================
# РЕФЕРАЛЬНАЯ СИСТЕМА # РЕФЕРАЛЬНАЯ СИСТЕМА
# ============================================================ # ============================================================
@@ -134,50 +163,28 @@ REFERALS_MENU = (
# ОФОРМЛЕНИЕ ПОДПИСКИ # ОФОРМЛЕНИЕ ПОДПИСКИ
# ============================================================ # ============================================================
SUBSCRIPTION_DEVICE_SELECTOR = ( SUBSCRIPTION_DEVICE_SELECTOR = "Сколько устройств вы хотите подключить?\n\nЦена: <b>{price}₽</b>"
"Сколько устройств вы хотите подключить?\n\nЦена: <b>{price}₽</b>"
)
SUBSCRIPTION_DURATION_SELECTOR = "На какой срок хотите оформить подписку?\n\nИтого: <b>{price}₽</b>" SUBSCRIPTION_DURATION_SELECTOR = (
"На какой срок хотите оформить подписку?\n\nИтого: <b>{price}₽</b> <i>(скидка: {discount}%)</i>"
)
WHITELISTS_ALREADY_ON = "Белые списки уже активны." WHITELISTS_ALREADY_ON = "Белые списки уже активны."
CHECKOUT_PAYMENT_CHOICE = ( CHECKOUT_PAYMENT_CHOICE = "Пока что без автоплатежа. Нажмите кнопку ниже и обратитесь в поддержку для оплаты и активации подписки."
"Пока что без автоплатежа. Нажмите кнопку ниже и обратитесь в поддержку для оплаты и активации подписки."
)
# ============================================================ # ============================================================
# ПОДДЕРЖКА # ПОДДЕРЖКА
# ============================================================ # ============================================================
SUPPORT_INIT = ( SUPPORT_INIT = "<b>🎧 Тех. Поддержка Malenia</b> — не здесь. Кнопка ниже."
f"<b>{MALENIA_SUPPORT} Технический отдел</b>\n"
"— если интернет превратился в тыкву или есть идеи по конфигам.\n"
"Пишите по делу, скриншоты приветствуются.\n\n"
"<i>🌑 /cancel — вернуться в меню</i>"
)
SUPPORT_MSG_SENT = "⏳ Приняли ваше сообщение. Мы постараемся ответить на него в ближайшее время.\n<i>Вернуться в меню: /cancel</i>" SUPPORT_MSG_SENT = "⏳ Приняли ваше сообщение. Мы постараемся ответить на него в ближайшее время.\n<i>Вернуться в меню: /cancel</i>"
SUPPORT_SIGNATURE = "<b>📡 Ответ от Malenia:</b>" SUPPORT_SIGNATURE = "<b>📡 Ответ от Malenia:</b>"
# ============================================================
# ШАБЛОНЫ — ОБЩИЕ БЛОКИ
# ============================================================
_SUPPORT_USER_DESCRIPTION = (
"👤 <b>Профиль:</b> {full_name}\n"
"🔗 <b>Alias:</b> {username_display}\n"
"🆔 <b>UID:</b> <code>{telegram_id}</code>\n\n"
"❤️ <b>Referal UID:</b> <code>{referal_id}</code>\n\n"
"💰 <b>Баланс:</b> <code>{balance}</code>"
)
_DIVIDER = "────────────────────────────\n\n"
# ============================================================ # ============================================================
# ШАБЛОНЫ — ЗАЯВКИ И ТИКЕТЫ # ШАБЛОНЫ — ЗАЯВКИ И ТИКЕТЫ
# ============================================================ # ============================================================
@@ -214,7 +221,7 @@ ADMIN_TICKET_WITHOUT_RW = (
+ _SUPPORT_USER_DESCRIPTION + _SUPPORT_USER_DESCRIPTION
+ "\n" + "\n"
+ _DIVIDER + _DIVIDER
+ "⚠️ <b>Remnawave-профиль отсутствует</b>" + NO_SUBSCRIPTION
) )
ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Проверь логи, что-то пошло не по чертежу." ADMIN_STANDARD_FALLBACK = "<b>❌ Ошибка выполнения.</b> Проверь логи, что-то пошло не по чертежу."