Implement buy flow and migrate DTOs to schemas
Move DTO definitions from dto/ to schemas/ and update imports. Add buy handler with FSM state management for device selection, whitelist toggling, and duration choice. Include pricing logic, inline keyboards, and new configuration fields.
This commit is contained in:
@@ -13,6 +13,11 @@ class settings(BaseSettings):
|
|||||||
admin_group_id: int = Field(alias="ADMIN_GROUP_ID")
|
admin_group_id: int = Field(alias="ADMIN_GROUP_ID")
|
||||||
remnawave_url: str = Field(alias="REMNAWAVE_URL")
|
remnawave_url: str = Field(alias="REMNAWAVE_URL")
|
||||||
remnawave_token: str = Field(alias="REMNAWAVE_TOKEN")
|
remnawave_token: str = Field(alias="REMNAWAVE_TOKEN")
|
||||||
|
min_devices: int = Field(3, alias="MIN_DEVICES")
|
||||||
|
max_devices: int = Field(12, alias="MAX_DEVICES")
|
||||||
|
per_device_cost: int = Field(50, alias="PER_DEVICE_COST")
|
||||||
|
whitelist_cost: int = Field(100, alias="WHITELIST_COST")
|
||||||
|
whitelist_device_threshold: int = Field(6, alias="WHITELIST_DEVICE_THRESHOLD")
|
||||||
|
|
||||||
|
|
||||||
settings = settings() # type: ignore
|
settings = settings() # type: ignore
|
||||||
|
|||||||
14
dto/di.py
14
dto/di.py
@@ -1,14 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from repositories.tickets import TicketRepository
|
|
||||||
from repositories.users import UserRepository
|
|
||||||
from services.user_service import UserService
|
|
||||||
from remnawave import RemnawaveSDK
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DependenciesDTO:
|
|
||||||
ticket_repository: TicketRepository
|
|
||||||
user_repository: UserRepository
|
|
||||||
user_service: UserService
|
|
||||||
rw_sdk: RemnawaveSDK
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NewTicketDTO:
|
|
||||||
creator_id: int
|
|
||||||
14
dto/users.py
14
dto/users.py
@@ -1,14 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NewUserDTO:
|
|
||||||
id: int
|
|
||||||
referal: int
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TicketCreator:
|
|
||||||
id: int
|
|
||||||
username: Optional[str] = None
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from .menus import router as menus_router
|
from .menus import router as menus_router
|
||||||
from .support import router as support_router
|
from .support import router as support_router
|
||||||
|
from .buy import router as buy_router
|
||||||
|
|
||||||
routers = [menus_router, support_router]
|
routers = [menus_router, support_router, buy_router]
|
||||||
|
|||||||
151
handlers/buy.py
151
handlers/buy.py
@@ -1,10 +1,153 @@
|
|||||||
from aiogram import Router
|
from typing import Optional
|
||||||
|
from aiogram import Router, F
|
||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
from dto.di import DependenciesDTO
|
from keyboards.client import devices_selector, duration_selector
|
||||||
|
from misc.utils import calculate_price, convert_int_to_duration
|
||||||
|
from schemas.subscriptions import SubscriptionPlan
|
||||||
|
from states.buy import SubscriptionStorage
|
||||||
|
from texts import (
|
||||||
|
CALLBACK_FALLBACK,
|
||||||
|
CONTEXT_REINITIATED,
|
||||||
|
NOTHING_PLACEHOLDER,
|
||||||
|
SUBSCRIPTION_DEVICE_SELECTOR,
|
||||||
|
SUBSCRIPTION_DURATION_SELECTOR,
|
||||||
|
)
|
||||||
|
from config import settings
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
# @router.callback_query()
|
|
||||||
|
@router.callback_query(F.data == "buy")
|
||||||
|
async def buy_init(cb: CallbackQuery, state: FSMContext):
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
ctx = SubscriptionPlan()
|
||||||
|
await cb.message.edit_text(
|
||||||
|
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
|
reply_markup=devices_selector(settings.min_devices),
|
||||||
|
)
|
||||||
|
|
||||||
|
await state.set_state(SubscriptionStorage.devices)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("devices:"))
|
||||||
|
async def device_count_select(cb: CallbackQuery, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
ctx: Optional[SubscriptionPlan] = data.get("ctx", SubscriptionPlan())
|
||||||
|
|
||||||
|
cb_data = cb.data.split(":")
|
||||||
|
devices = int(cb_data[1]) if cb_data[1].isdigit() else settings.min_devices
|
||||||
|
ctx.devices = devices
|
||||||
|
ctx.whitelists = ctx.whitelists or devices >= settings.whitelist_device_threshold
|
||||||
|
|
||||||
|
await cb.message.edit_text(
|
||||||
|
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
|
reply_markup=devices_selector(current=ctx.devices, whitelists=ctx.whitelists),
|
||||||
|
)
|
||||||
|
await state.set_state(SubscriptionStorage.devices)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(SubscriptionStorage.devices, F.data == "whitelist:toggle")
|
||||||
|
async def toggle_whitelists(cb: CallbackQuery, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.set_state(SubscriptionPlan())
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
await cb.answer(CALLBACK_FALLBACK)
|
||||||
|
return
|
||||||
|
ctx.whitelists = (
|
||||||
|
not ctx.whitelists or ctx.devices >= settings.whitelist_device_threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cb.message.edit_reply_markup(
|
||||||
|
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
|
reply_markup=devices_selector(
|
||||||
|
current=ctx.devices, whitelists=ctx.whitelists
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if "message is not modified:" in str(exc):
|
||||||
|
await cb.answer(NOTHING_PLACEHOLDER)
|
||||||
|
else:
|
||||||
|
await cb.answer(CALLBACK_FALLBACK)
|
||||||
|
await state.set_state(SubscriptionStorage.devices)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(SubscriptionStorage.devices, F.data == "confirm")
|
||||||
|
async def select_duration_init(cb: CallbackQuery, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.set_state(SubscriptionPlan())
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
await cb.answer(CALLBACK_FALLBACK)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cb.message.edit_text(
|
||||||
|
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
|
reply_markup=duration_selector(
|
||||||
|
current=ctx.duration, back_cb=f"devices:{ctx.devices}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if "message is not modified:" in str(exc):
|
||||||
|
await cb.answer(NOTHING_PLACEHOLDER)
|
||||||
|
else:
|
||||||
|
await cb.answer(CALLBACK_FALLBACK)
|
||||||
|
raise
|
||||||
|
|
||||||
|
await state.set_state(SubscriptionStorage.duration)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("duration:"))
|
||||||
|
async def select_duration(cb: CallbackQuery, state: FSMContext):
|
||||||
|
data = await state.get_data()
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
ctx: Optional[SubscriptionPlan] = data.get("ctx")
|
||||||
|
if not ctx:
|
||||||
|
await state.set_state()
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
await cb.answer(CONTEXT_REINITIATED)
|
||||||
|
ctx = SubscriptionPlan()
|
||||||
|
|
||||||
|
cb_data = cb.data.split(":")
|
||||||
|
duration = int(cb_data[1]) if cb_data[1].isdigit() else 1
|
||||||
|
ctx.duration = convert_int_to_duration(duration)
|
||||||
|
try:
|
||||||
|
await cb.message.edit_text(
|
||||||
|
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
|
||||||
|
reply_markup=duration_selector(
|
||||||
|
current=ctx.duration, back_cb=f"devices:{ctx.devices}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if "message is not modified:" in str(exc):
|
||||||
|
await cb.answer(NOTHING_PLACEHOLDER)
|
||||||
|
else:
|
||||||
|
await cb.answer(CALLBACK_FALLBACK)
|
||||||
|
raise
|
||||||
|
|
||||||
|
await state.set_state(SubscriptionStorage.duration)
|
||||||
|
await state.set_data({"ctx": ctx})
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(SubscriptionStorage.duration, F.data == "confirm")
|
||||||
|
async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext):
|
||||||
|
await cb.answer("yay!!!! fuck off.")
|
||||||
|
...
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from aiogram.types import CallbackQuery, Message
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from dto.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from keyboards.client import main_menu
|
from keyboards.client import main_menu
|
||||||
from texts import build_main_menu_text
|
from texts import build_main_menu_text
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from aiogram.types import CallbackQuery, Message
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
from dto.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from services.user_service import UserServiceException
|
from services.user_service import UserServiceException
|
||||||
from states.support import SupportStorage
|
from states.support import SupportStorage
|
||||||
from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT
|
from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
from misc.utils import convert_duration_to_int
|
||||||
|
from schemas.subscriptions import SubscriptionDuration
|
||||||
|
|
||||||
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@@ -14,6 +18,71 @@ main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
|||||||
]
|
]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
_return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton(
|
||||||
|
text="⬅️ Назад", callback_data="menu:main"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _return_btn(
|
||||||
|
data: str = "menu:main", text: str = "⬅️ Назад"
|
||||||
|
) -> InlineKeyboardButton:
|
||||||
|
return InlineKeyboardButton(text=text, callback_data=data)
|
||||||
|
|
||||||
|
|
||||||
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||||
[[InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main")]]
|
[[_return_to_menu_btn]]
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def devices_selector(current: int = settings.min_devices, whitelists: bool = False):
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=f"{'✅ ' if i == current else ''}{i}", callback_data=f"devices:{i}"
|
||||||
|
)
|
||||||
|
for i in range(settings.min_devices, settings.max_devices + 1)
|
||||||
|
]
|
||||||
|
builder.add(*buttons).adjust(6)
|
||||||
|
|
||||||
|
wl_icon = "✅ " if whitelists else ""
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=f"{wl_icon}Обход Белых Списков (+100₽)",
|
||||||
|
callback_data="whitelist:toggle",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
builder.row(InlineKeyboardButton(text="🟢", callback_data="confirm"))
|
||||||
|
builder.row(_return_to_menu_btn)
|
||||||
|
|
||||||
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def duration_selector(
|
||||||
|
current: SubscriptionDuration = SubscriptionDuration.MONTH,
|
||||||
|
back_cb: str = "menu:main",
|
||||||
|
) -> InlineKeyboardMarkup:
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
duration_int = convert_duration_to_int(current)
|
||||||
|
|
||||||
|
durations = [
|
||||||
|
(1, "1 Месяц"),
|
||||||
|
(3, "3 Месяца"),
|
||||||
|
(6, "6 Месяцев"),
|
||||||
|
(12, "1 Год"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for val, label in durations:
|
||||||
|
is_selected = val == duration_int
|
||||||
|
|
||||||
|
builder.button(
|
||||||
|
text=f"{'✅ ' if is_selected else ''}{label}",
|
||||||
|
callback_data=f"duration:{val}",
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(text="🛒 Перейти к оплате", callback_data="confirm")
|
||||||
|
)
|
||||||
|
builder.row(_return_btn(back_cb))
|
||||||
|
|
||||||
|
return builder.as_markup()
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -6,7 +6,7 @@ from aiogram import Bot, Dispatcher
|
|||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.client.session.aiohttp import AiohttpSession
|
from aiogram.client.session.aiohttp import AiohttpSession
|
||||||
|
|
||||||
from dto.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from handlers import routers
|
from handlers import routers
|
||||||
from middlewares import DIMiddleware
|
from middlewares import DIMiddleware
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from aiogram import BaseMiddleware
|
|||||||
from aiogram.types import TelegramObject
|
from aiogram.types import TelegramObject
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
|
||||||
from dto.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
|
|
||||||
|
|
||||||
class DIMiddleware(BaseMiddleware):
|
class DIMiddleware(BaseMiddleware):
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from random import choice
|
from random import choice
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from config import settings
|
||||||
|
from schemas.subscriptions import SubscriptionDuration, SubscriptionPlan
|
||||||
import texts
|
import texts
|
||||||
|
|
||||||
QUOTES: list[str] = [
|
QUOTES: list[str] = [
|
||||||
@@ -125,3 +127,45 @@ def format_username(username: Optional[str]) -> str:
|
|||||||
if username:
|
if username:
|
||||||
return "@" + username
|
return "@" + username
|
||||||
return texts.NO_USERNAME
|
return texts.NO_USERNAME
|
||||||
|
|
||||||
|
|
||||||
|
def convert_duration_to_int(duration: SubscriptionDuration) -> int:
|
||||||
|
mapping: dict[str, int] = {
|
||||||
|
SubscriptionDuration.MONTH.value: 1,
|
||||||
|
SubscriptionDuration.THREE_MONTHS.value: 3,
|
||||||
|
SubscriptionDuration.SIX_MONTHS.value: 6,
|
||||||
|
SubscriptionDuration.YEAR.value: 12,
|
||||||
|
}
|
||||||
|
return mapping.get(duration.value, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_int_to_duration(value: int) -> SubscriptionDuration:
|
||||||
|
mapping: dict[int, SubscriptionDuration] = {
|
||||||
|
1: SubscriptionDuration.MONTH,
|
||||||
|
3: SubscriptionDuration.THREE_MONTHS,
|
||||||
|
6: SubscriptionDuration.SIX_MONTHS,
|
||||||
|
12: SubscriptionDuration.YEAR,
|
||||||
|
}
|
||||||
|
return mapping.get(value, SubscriptionDuration.MONTH)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_duration_to_timedelta(duration: SubscriptionDuration) -> timedelta:
|
||||||
|
MONTH_DELTA = timedelta(days=30)
|
||||||
|
mapping: dict[str, timedelta] = {
|
||||||
|
SubscriptionDuration.MONTH.value: MONTH_DELTA,
|
||||||
|
SubscriptionDuration.THREE_MONTHS.value: MONTH_DELTA * 3,
|
||||||
|
SubscriptionDuration.SIX_MONTHS.value: MONTH_DELTA * 6,
|
||||||
|
SubscriptionDuration.YEAR.value: MONTH_DELTA * 12,
|
||||||
|
}
|
||||||
|
return mapping.get(duration.value, MONTH_DELTA)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_price(subscription: SubscriptionPlan):
|
||||||
|
return (
|
||||||
|
(subscription.devices * settings.per_device_cost)
|
||||||
|
+ (
|
||||||
|
int(subscription.whitelists) * settings.whitelist_cost
|
||||||
|
if subscription.devices < settings.whitelist_device_threshold
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
) * convert_duration_to_int(subscription.duration)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.models.tickets import Ticket, TicketStatus
|
from db.models.tickets import Ticket, TicketStatus
|
||||||
from dto.tickets import NewTicketDTO
|
from schemas.tickets import NewTicketDTO
|
||||||
|
|
||||||
|
|
||||||
class TicketRepository:
|
class TicketRepository:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from db.models import User
|
from db.models import User
|
||||||
from dto.users import NewUserDTO
|
from schemas.users import NewUserDTO
|
||||||
|
|
||||||
|
|
||||||
class UserRepository:
|
class UserRepository:
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from remnawave import RemnawaveSDK
|
|||||||
from config import settings
|
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 dto.tickets import NewTicketDTO
|
from schemas.tickets import NewTicketDTO
|
||||||
from dto.users import NewUserDTO, TicketCreator
|
from schemas.users import NewUserDTO, TicketCreator
|
||||||
from repositories import UserRepository
|
from repositories import UserRepository
|
||||||
from repositories.tickets import TicketRepository
|
from repositories.tickets import TicketRepository
|
||||||
from services.rw import RWUserInfo, get_user_by_telegram_id
|
from services.rw import RWUserInfo, get_user_by_telegram_id
|
||||||
@@ -107,7 +107,6 @@ class UserService:
|
|||||||
|
|
||||||
if ticket:
|
if ticket:
|
||||||
try:
|
try:
|
||||||
print(ticket.thread_id)
|
|
||||||
await msg.copy_to(
|
await msg.copy_to(
|
||||||
chat_id=settings.admin_group_id, message_thread_id=ticket.thread_id
|
chat_id=settings.admin_group_id, message_thread_id=ticket.thread_id
|
||||||
)
|
)
|
||||||
|
|||||||
14
texts.py
14
texts.py
@@ -38,8 +38,19 @@ STANDARD_FALLBACK = (
|
|||||||
"<b>❌ Что-то пошло не так</b>, прожмите /start и повторите попытку позже."
|
"<b>❌ Что-то пошло не так</b>, прожмите /start и повторите попытку позже."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CALLBACK_FALLBACK = "❌ Что-то пошло не так"
|
||||||
|
CONTEXT_REINITIATED = "🔴 Контекст данного взаимодействия сброшен. Данные могут отличаться от указанных вами."
|
||||||
|
|
||||||
GENERAL_PROCESSING = "<i>⏳ Выполняю...</i>"
|
GENERAL_PROCESSING = "<i>⏳ Выполняю...</i>"
|
||||||
|
|
||||||
|
SUBSCRIPTION_DEVICE_SELECTOR = (
|
||||||
|
"выбери колво устройств епта. тут ещё цена есть смотри опа: {price}"
|
||||||
|
)
|
||||||
|
WHITELISTS_ALREADY_ON = "уже включены!"
|
||||||
|
SUBSCRIPTION_DURATION_SELECTOR = (
|
||||||
|
"тут короче сколько время. а ещё цена смотри ОПА {price}"
|
||||||
|
)
|
||||||
|
|
||||||
SUPPORT_INIT = f"<b>{MALENIA_SUPPORT} <i>Что-то не работает? Жалоба? Предложение?</i></b>\n— всё сюда, желательно со скриншотами. Чем детальнее описание, тем быстрее ответ.\n\n<i>❌ Для отмены — /cancel</i>"
|
SUPPORT_INIT = f"<b>{MALENIA_SUPPORT} <i>Что-то не работает? Жалоба? Предложение?</i></b>\n— всё сюда, желательно со скриншотами. Чем детальнее описание, тем быстрее ответ.\n\n<i>❌ Для отмены — /cancel</i>"
|
||||||
SUPPORT_MSG_SENT = "⏳ Сообщение отправлено. Для выхода — /cancel, вы можете вернуться сюда из главного меню в любое время."
|
SUPPORT_MSG_SENT = "⏳ Сообщение отправлено. Для выхода — /cancel, вы можете вернуться сюда из главного меню в любое время."
|
||||||
SUPPORT_SIGNATURE = "<b>✍️ Сообщение от технической поддержки:</b>"
|
SUPPORT_SIGNATURE = "<b>✍️ Сообщение от технической поддержки:</b>"
|
||||||
@@ -122,3 +133,6 @@ TRAFFIC_WITH_LIMIT = "{used} / {limit}"
|
|||||||
|
|
||||||
# Шаблон для отображения трафика без лимита: {used}
|
# Шаблон для отображения трафика без лимита: {used}
|
||||||
TRAFFIC_NO_LIMIT = "{used}"
|
TRAFFIC_NO_LIMIT = "{used}"
|
||||||
|
|
||||||
|
|
||||||
|
NOTHING_PLACEHOLDER = "🍃"
|
||||||
|
|||||||
Reference in New Issue
Block a user