diff --git a/bot.py b/bot.py
index e0488422..c01c2d8d 100644
--- a/bot.py
+++ b/bot.py
@@ -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)
diff --git a/database.py b/database.py
index 1459cecc..c1fcc078 100644
--- a/database.py
+++ b/database.py
@@ -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:
diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py
new file mode 100644
index 00000000..00764539
--- /dev/null
+++ b/handlers/admin/admin_coupons.py
@@ -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"Код: {coupon['code']}\n"
+ f"Сумма: {coupon['amount']} рублей\n"
+ f"Лимит использования: {coupon['usage_limit']} раз\n"
+ f"Использовано: {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"❌ Купон с кодом {coupon_code} не найден.", 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(
+ "Введите данные для создания купона в формате:\n\n"
+ "код сумма лимит\n\n"
+ "Пример: 'COUPON1 50 5'\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(
+ "❌ Некорректный формат! Пожалуйста, введите данные в формате:\n"
+ "код сумма лимит\n"
+ "Пример: 'COUPON1 50 5'",
+ parse_mode="HTML",
+ )
+ return
+
+ try:
+ coupon_code = parts[0]
+ coupon_amount = float(parts[1])
+ usage_limit = int(parts[2])
+ except ValueError:
+ await message.answer(
+ "⚠️ Проверьте правильность введенных данных.\n"
+ "Сумма должна быть числом, а лимит — целым числом.",
+ parse_mode="HTML",
+ )
+ return
+
+ try:
+ await create_coupon(coupon_code, coupon_amount, usage_limit)
+
+ result_message = (
+ f"✅ Купон с кодом {coupon_code} успешно создан!\n"
+ f"Сумма: {coupon_amount} рублей\n"
+ f"Лимит использования: {usage_limit} раз."
+ )
+
+ 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"❌ Ошибка при создании купона: {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)
diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py
index 08e5c907..c840524f 100644
--- a/handlers/admin/admin_panel.py
+++ b/handlers/admin/admin_panel.py
@@ -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")
)
diff --git a/handlers/coupons.py b/handlers/coupons.py
new file mode 100644
index 00000000..86aed0e4
--- /dev/null
+++ b/handlers/coupons.py
@@ -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(
+ "Введите код купона:\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 "❌ Купон не найден или его использование ограничено. Пожалуйста, проверьте код и попробуйте снова."
+
+ 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 "❌ Вы уже активировали этот купон. Купоны могут быть активированы только один раз."
+
+ 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"✅ Купон успешно активирован!\n\nНа ваш баланс добавлено {coupon_amount} рублей."
+
+ except Exception as e:
+ logger.error(f"Ошибка при активации купона: {e}")
+ return (
+ "⚠️ Произошла ошибка при активации купона.\nПопробуйте ещё раз позже."
+ )
+
+ finally:
+ await conn.close()
diff --git a/handlers/pay.py b/handlers/pay.py
index c0a21de2..42ebc494 100644
--- a/handlers/pay.py
+++ b/handlers/pay.py
@@ -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")
diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py
index 08346329..1bd35f3a 100644
--- a/handlers/payments/yookassa_pay.py
+++ b/handlers/payments/yookassa_pay.py
@@ -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)
diff --git a/handlers/start.py b/handlers/start.py
index 37ffe227..a06d602d 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -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"))