coupons initial
This commit is contained in:
@@ -10,15 +10,17 @@ storage = MemoryStorage()
|
||||
dp = Dispatcher(bot=bot, storage=storage)
|
||||
router = Router()
|
||||
|
||||
from handlers import commands, notifications, pay, profile, start
|
||||
from handlers.admin import admin_commands, admin_panel, admin_user_editor
|
||||
from handlers import commands, coupons, notifications, pay, profile, start
|
||||
from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor
|
||||
from handlers.keys import key_management, keys
|
||||
from handlers.payments import cryprobot_pay, freekassa_pay, stars_pay, yookassa_pay
|
||||
|
||||
dp.include_router(admin_commands.router)
|
||||
dp.include_router(admin_coupons.router)
|
||||
dp.include_router(admin_panel.router)
|
||||
dp.include_router(admin_user_editor.router)
|
||||
dp.include_router(commands.router)
|
||||
dp.include_router(coupons.router)
|
||||
dp.include_router(start.router)
|
||||
dp.include_router(profile.router)
|
||||
dp.include_router(keys.router)
|
||||
|
||||
+101
-30
@@ -9,6 +9,7 @@ from config import BONUS_PERCENT, DATABASE_URL
|
||||
async def init_db():
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
# Таблица для хранения информации о пользователях
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS connections (
|
||||
@@ -16,9 +17,10 @@ async def init_db():
|
||||
balance REAL NOT NULL DEFAULT 0.0,
|
||||
trial INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
"""
|
||||
)
|
||||
|
||||
# Таблица для хранения ключей
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS keys (
|
||||
@@ -28,54 +30,123 @@ async def init_db():
|
||||
created_at BIGINT NOT NULL,
|
||||
expiry_time BIGINT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
server_id TEXT NOT NULL DEFAULT 'server1', -- поле для идентификатора сервера
|
||||
notified BOOLEAN NOT NULL DEFAULT FALSE, -- новое поле для статуса уведомления
|
||||
server_id TEXT NOT NULL DEFAULT 'cluster1',
|
||||
notified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
notified_24h BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY (tg_id, client_id)
|
||||
)
|
||||
"""
|
||||
"""
|
||||
)
|
||||
|
||||
# Таблица для хранения рефералов
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS referrals (
|
||||
referred_tg_id BIGINT PRIMARY KEY NOT NULL, -- ID приглашенного пользователя
|
||||
referrer_tg_id BIGINT NOT NULL, -- ID пригласившего пользователя
|
||||
reward_issued BOOLEAN DEFAULT FALSE -- Был ли начислен бонус
|
||||
referred_tg_id BIGINT PRIMARY KEY NOT NULL,
|
||||
referrer_tg_id BIGINT NOT NULL,
|
||||
reward_issued BOOLEAN DEFAULT FALSE
|
||||
)
|
||||
"""
|
||||
"""
|
||||
)
|
||||
|
||||
try:
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE keys
|
||||
ADD COLUMN server_id TEXT NOT NULL DEFAULT 'server1'
|
||||
# Таблица для хранения купонов
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS coupons (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
usage_limit INTEGER NOT NULL DEFAULT 1,
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
is_used BOOLEAN NOT NULL DEFAULT FALSE
|
||||
)
|
||||
except asyncpg.exceptions.DuplicateColumnError:
|
||||
pass
|
||||
"""
|
||||
)
|
||||
|
||||
try:
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE keys
|
||||
ADD COLUMN notified BOOLEAN NOT NULL DEFAULT FALSE
|
||||
# Таблица для отслеживания использований купонов пользователями
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS coupon_usages (
|
||||
coupon_id INTEGER NOT NULL REFERENCES coupons(id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL,
|
||||
used_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (coupon_id, user_id)
|
||||
)
|
||||
except asyncpg.exceptions.DuplicateColumnError:
|
||||
pass
|
||||
try:
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE keys
|
||||
ADD COLUMN notified_24h BOOLEAN NOT NULL DEFAULT FALSE
|
||||
"""
|
||||
)
|
||||
except asyncpg.exceptions.DuplicateColumnError:
|
||||
pass
|
||||
)
|
||||
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def create_coupon(coupon_code: str, amount: float, usage_limit: int):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
try:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used)
|
||||
VALUES ($1, $2, $3, 0, FALSE)
|
||||
""",
|
||||
coupon_code,
|
||||
amount,
|
||||
usage_limit,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Ошибка при создании купона: {e}")
|
||||
raise
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def get_all_coupons():
|
||||
"""Получить список всех купонов"""
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
coupons = await conn.fetch(
|
||||
"""
|
||||
SELECT code, amount, usage_limit, usage_count
|
||||
FROM coupons
|
||||
"""
|
||||
)
|
||||
return coupons
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении купонов: {e}")
|
||||
return []
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def delete_coupon_from_db(coupon_code: str):
|
||||
"""Удалить купон из базы данных по его коду"""
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
|
||||
coupon_record = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id FROM coupons WHERE code = $1
|
||||
""",
|
||||
coupon_code,
|
||||
)
|
||||
|
||||
if not coupon_record:
|
||||
return False
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
DELETE FROM coupons WHERE code = $1
|
||||
""",
|
||||
coupon_code,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при удалении купона: {e}")
|
||||
return False
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def restore_trial(tg_id: int):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from loguru import logger
|
||||
|
||||
from database import create_coupon, delete_coupon_from_db, get_all_coupons
|
||||
|
||||
|
||||
class AdminCouponsState(StatesGroup):
|
||||
waiting_for_coupon_data = State()
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "coupons_editor")
|
||||
async def show_coupon_management_menu(callback_query: types.CallbackQuery):
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon")
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons"))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_admin_menu")
|
||||
)
|
||||
|
||||
markup = builder.as_markup()
|
||||
await callback_query.message.answer(
|
||||
"🛠 Меню управления купонами:", reply_markup=markup
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "coupons")
|
||||
async def show_coupon_list(callback_query: types.CallbackQuery):
|
||||
try:
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
coupons = await get_all_coupons()
|
||||
|
||||
if not coupons:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
|
||||
)
|
||||
markup = builder.as_markup()
|
||||
|
||||
await callback_query.message.answer(
|
||||
"❌ На данный момент нет доступных купонов.\n"
|
||||
"Вы можете вернуться в меню управления.",
|
||||
parse_mode="HTML",
|
||||
reply_markup=markup,
|
||||
)
|
||||
await callback_query.answer()
|
||||
return
|
||||
|
||||
coupon_list = "📜 Список всех купонов:\n\n"
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for coupon in coupons:
|
||||
coupon_list += (
|
||||
f"<b>Код:</b> {coupon['code']}\n"
|
||||
f"<b>Сумма:</b> {coupon['amount']} рублей\n"
|
||||
f"<b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
|
||||
f"<b>Использовано:</b> {coupon['usage_count']} раз\n\n"
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f"❌ Удалить {coupon['code']}",
|
||||
callback_data=f"delete_coupon_{coupon['code']}",
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
|
||||
)
|
||||
|
||||
markup = builder.as_markup()
|
||||
await callback_query.message.answer(
|
||||
coupon_list, parse_mode="HTML", reply_markup=markup
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка купонов: {e}")
|
||||
await callback_query.message.answer(
|
||||
f"❌ Произошла ошибка при получении списка купонов: {e}", parse_mode="HTML"
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("delete_coupon_"))
|
||||
async def handle_delete_coupon(callback_query: types.CallbackQuery):
|
||||
coupon_code = callback_query.data[len("delete_coupon_") :]
|
||||
|
||||
try:
|
||||
result = await delete_coupon_from_db(coupon_code)
|
||||
|
||||
if result:
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
await show_coupon_list(callback_query)
|
||||
else:
|
||||
await callback_query.message.answer(
|
||||
f"❌ Купон с кодом <b>{coupon_code}</b> не найден.", parse_mode="HTML"
|
||||
)
|
||||
await show_coupon_list(callback_query)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении купона: {e}")
|
||||
await callback_query.message.answer(
|
||||
f"❌ Произошла ошибка при удалении купона: {e}", parse_mode="HTML"
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "create_coupon")
|
||||
async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
await callback_query.message.answer(
|
||||
"<b>Введите данные для создания купона в формате:</b>\n\n"
|
||||
"<i>код</i> <i>сумма</i> <i>лимит</i>\n\n"
|
||||
"Пример: <b>'COUPON1 50 5'</b>\n\n",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.message(AdminCouponsState.waiting_for_coupon_data)
|
||||
async def process_coupon_data(message: types.Message, state: FSMContext):
|
||||
text = message.text.strip()
|
||||
|
||||
parts = text.split()
|
||||
if len(parts) != 3:
|
||||
await message.answer(
|
||||
"<b>❌ Некорректный формат!</b> Пожалуйста, введите данные в формате:\n"
|
||||
"<b>код</b> <b>сумма</b> <b>лимит</b>\n"
|
||||
"Пример: <b>'COUPON1 50 5'</b>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
coupon_code = parts[0]
|
||||
coupon_amount = float(parts[1])
|
||||
usage_limit = int(parts[2])
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
"<b>⚠️ Проверьте правильность введенных данных.</b>\n"
|
||||
"Сумма должна быть числом, а лимит — целым числом.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await create_coupon(coupon_code, coupon_amount, usage_limit)
|
||||
|
||||
result_message = (
|
||||
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
|
||||
f"Сумма: <b>{coupon_amount} рублей</b>\n"
|
||||
f"Лимит использования: <b>{usage_limit} раз</b>."
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
|
||||
)
|
||||
markup = builder.as_markup()
|
||||
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
await message.answer(result_message, parse_mode="HTML", reply_markup=markup)
|
||||
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании купона: {e}")
|
||||
await message.answer(
|
||||
f"<b>❌ Ошибка при создании купона:</b> {e}", parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back_to_coupons_menu")
|
||||
async def back_to_coupons_menu(callback_query: types.CallbackQuery):
|
||||
"""Возвращаем пользователя в меню управления купонами"""
|
||||
await show_coupon_management_menu(callback_query)
|
||||
@@ -36,6 +36,11 @@ async def handle_admin_command(message: types.Message):
|
||||
text="👥 Управление пользователями", callback_data="user_editor"
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🎟️ Управление купонами", callback_data="coupons_editor"
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")
|
||||
)
|
||||
@@ -164,6 +169,11 @@ async def back_to_admin_menu(callback_query: CallbackQuery):
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🎟️ Управление купонами", callback_data="coupons_editor"
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
from datetime import datetime
|
||||
|
||||
import asyncpg
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from loguru import logger
|
||||
|
||||
from config import DATABASE_URL
|
||||
from database import update_balance
|
||||
|
||||
|
||||
class CouponActivationState(StatesGroup):
|
||||
waiting_for_coupon_code = State()
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "activate_coupon")
|
||||
async def handle_activate_coupon(
|
||||
callback_query: types.CallbackQuery, state: FSMContext
|
||||
):
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
await callback_query.message.answer(
|
||||
"<b>Введите код купона:</b>\n\n"
|
||||
"Пожалуйста, введите действующий код купона, который вы хотите активировать.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await state.set_state(CouponActivationState.waiting_for_coupon_code)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.message(CouponActivationState.waiting_for_coupon_code)
|
||||
async def process_coupon_code(message: types.Message, state: FSMContext):
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
coupon_code = message.text.strip()
|
||||
activation_result = await activate_coupon(message.from_user.id, coupon_code)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
|
||||
)
|
||||
|
||||
markup = builder.as_markup()
|
||||
|
||||
await message.answer(activation_result, reply_markup=markup, parse_mode="HTML")
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def activate_coupon(user_id: int, coupon_code: str):
|
||||
"""Функция для активации купона"""
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
try:
|
||||
coupon_record = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id, usage_limit, usage_count, is_used, amount
|
||||
FROM coupons
|
||||
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
|
||||
""",
|
||||
coupon_code,
|
||||
)
|
||||
|
||||
if not coupon_record:
|
||||
return "<b>❌ Купон не найден</b> или его использование ограничено. Пожалуйста, проверьте код и попробуйте снова."
|
||||
|
||||
usage_exists = await conn.fetchrow(
|
||||
"""
|
||||
SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
|
||||
""",
|
||||
coupon_record["id"],
|
||||
user_id,
|
||||
)
|
||||
|
||||
if usage_exists:
|
||||
return "<b>❌ Вы уже активировали этот купон.</b> Купоны могут быть активированы только один раз."
|
||||
|
||||
coupon_amount = coupon_record["amount"]
|
||||
|
||||
async with conn.transaction():
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE coupons
|
||||
SET usage_count = usage_count + 1,
|
||||
is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
|
||||
WHERE id = $1
|
||||
""",
|
||||
coupon_record["id"],
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO coupon_usages (coupon_id, user_id, used_at)
|
||||
VALUES ($1, $2, $3)
|
||||
""",
|
||||
coupon_record["id"],
|
||||
user_id,
|
||||
datetime.utcnow(),
|
||||
)
|
||||
|
||||
await update_balance(user_id, coupon_amount)
|
||||
return f"<b>✅ Купон успешно активирован!</b>\n\nНа ваш баланс добавлено <b>{coupon_amount} рублей</b>."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при активации купона: {e}")
|
||||
return (
|
||||
"<b>⚠️ Произошла ошибка при активации купона.</b>\nПопробуйте ещё раз позже."
|
||||
)
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
+6
-1
@@ -17,7 +17,7 @@ async def handle_pay(callback_query: CallbackQuery):
|
||||
if YOOKASSA_ENABLE:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="💳 ЯКасса: быстрый перевод",
|
||||
text="💳 ЮКасса: быстрый перевод",
|
||||
callback_data="pay_yookassa",
|
||||
)
|
||||
)
|
||||
@@ -42,6 +42,11 @@ async def handle_pay(callback_query: CallbackQuery):
|
||||
callback_data="pay_stars",
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🎟️ Активировать купон", callback_data="activate_coupon"
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
|
||||
|
||||
@@ -83,13 +83,11 @@ async def process_callback_pay_yookassa(
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="💰 Ввести свою сумму", callback_data="enter_custom_amount"
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")
|
||||
InlineKeyboardButton(
|
||||
text="💰 Ввести свою сумму", callback_data="enter_custom_amount"
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
|
||||
|
||||
key_count = await get_key_count(tg_id)
|
||||
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session):
|
||||
async def handle_about_vpn(callback_query: CallbackQuery):
|
||||
await callback_query.message.delete()
|
||||
|
||||
about_vpn_message = get_about_vpn("3.0.5_preStable")
|
||||
about_vpn_message = get_about_vpn("3.0.6_preStable")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))
|
||||
|
||||
Reference in New Issue
Block a user