Merge pull request #100 from izzzzzi/main

Refactoring and Update
This commit is contained in:
Vladislav Lisitsyn
2024-11-24 21:54:53 +03:00
committed by GitHub
33 changed files with 2103 additions and 2482 deletions
+2 -1
View File
@@ -51,4 +51,5 @@ Thumbs.db
nginx.conf
scripts
models.py
models.py
Dockerfile
-23
View File
@@ -1,23 +0,0 @@
FROM python:3.10-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN apt-get update && \
apt-get install -y postgresql-client locales && \
pip install --upgrade pip && pip install -r requirements.txt && \
sed -i '/ru_RU.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen ru_RU.UTF-8
ENV LANG=ru_RU.UTF-8
ENV LANGUAGE=ru_RU:ru
ENV LC_ALL=ru_RU.UTF-8
COPY . .
RUN sed -i "s|DATABASE_URL = .*|DATABASE_URL = '${DATABASE_URL}'|" config.py
CMD ["python", "main.py"]
+1 -1
View File
@@ -2,4 +2,4 @@ formatting:
@echo "Running black..." && black .
@echo "Running isort..." && isort .
@echo "Running flake8..." && flake8 --config .flake8
@echo "Running pylint..." && pylint .
# @echo "Running pylint..." && pylint .
+2 -1
View File
@@ -62,9 +62,10 @@ async def _send_backup_to_admin(bot, backup_file_path):
if isinstance(admin_ids, list):
for id in admin_ids:
await bot.send_document(id, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {id}")
else:
await bot.send_document(admin_ids, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
+24 -5
View File
@@ -1,28 +1,33 @@
import traceback
from aiogram import Bot, Dispatcher, Router
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import ErrorEvent
from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from logger import logger
from middlewares.admin import AdminMiddleware
from middlewares.database import DatabaseMiddleware
from middlewares.delete import DeleteMessageMiddleware
from middlewares.logging import LoggingMiddleware
from middlewares.user import UserMiddleware
bot = Bot(token=API_TOKEN)
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
router = Router()
from handlers import commands, coupons, donate, notifications, pay, profile, start
from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor
from handlers import coupons, donate, notifications, pay, profile, start
from handlers.admin import admin_coupons, admin_panel, admin_user_editor
from handlers.instructions import instructions
from handlers.keys import key_management, keys
from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_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)
@@ -54,3 +59,17 @@ dp.callback_query.middleware(UserMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
dp.message.outer_middleware(DeleteMessageMiddleware())
dp.callback_query.outer_middleware(DeleteMessageMiddleware())
@dp.error()
async def error_handler(event: ErrorEvent):
logger.error(
"Ошибка в боте:\n"
f"Исключение: {event.exception}\n"
f"Тип: {type(event.exception)}\n"
f"Update: {event.update}\n"
f"Трассировка:\n{traceback.format_exc()}"
)
+898 -230
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -10,11 +10,8 @@ class IsAdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
try:
admin_ids: Union[int, list[int]] = ADMIN_ID
if isinstance(admin_ids, list):
return message.from_user.id in admin_ids
return message.from_user.id == admin_ids
except Exception:
return False
-156
View File
@@ -1,156 +0,0 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from bot import bot
from config import DATABASE_URL
from database import add_balance_to_client, check_connection_exists
from filters.admin import IsAdminFilter
from handlers.texts import TRIAL
from logger import logger
router = Router()
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
@router.message(Command("add_balance"), IsAdminFilter())
async def cmd_add_balance(message: types.Message):
try:
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"❌ Клиент с ID {client_id} не найден в базе данных.")
return
await add_balance_to_client(int(client_id), amount)
await message.reply(f"✅ Баланс клиента {client_id} успешно пополнен на {amount}")
except ValueError:
await message.reply(
"❓ Неверный формат команды!\n"
"Пожалуйста, используйте следующий шаблон:\n"
"/add_balance <ID клиента> <сумма пополнения>"
)
except Exception as e:
await message.reply(f"🚨 Произошла непредвиденная ошибка: {e}")
@router.message(Command("backup"), IsAdminFilter())
async def backup_command(message: types.Message):
from backup import backup_database
await message.answer("🔄 Инициализация резервного копирования базы данных...")
await backup_database()
await message.answer("✅ Бэкап базы данных успешно завершен и отправлен администратору.")
@router.message(Command("send_trial"), IsAdminFilter())
async def handle_send_trial_command(message: types.Message, state: FSMContext):
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch(
"""
SELECT tg_id FROM connections WHERE trial = 0
"""
)
if records:
success_count = 0
error_count = 0
blocked_count = 0
for record in records:
tg_id = record["tg_id"]
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
success_count += 1
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
blocked_count += 1
logger.info(f"🚫 Бот заблокирован пользователем с tg_id: {tg_id}")
else:
error_count += 1
logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}")
await message.answer(
f"📊 Результаты рассылки пробных периодов:\n"
f"✅ Успешно отправлено: {success_count}\n"
f"🚫 Заблокировано: {blocked_count}\n"
f"❌ Ошибок: {error_count}"
)
else:
await message.answer("📭 Нет пользователей с неиспользованными пробными ключами.")
finally:
await conn.close()
except Exception as e:
await message.answer(f"❗ Ошибка при отправке сообщений: {e}")
@router.message(Command("send_to_all"), IsAdminFilter())
async def send_message_to_all_clients(message: types.Message, state: FSMContext, from_panel=False):
try:
await message.delete()
except Exception:
pass
if from_panel:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await message.answer(
"✍️ Введите текст сообщения, который вы хотите отправить всем клиентам:",
reply_markup=builder.as_markup(),
)
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message,
state: FSMContext,
):
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch("SELECT tg_id FROM connections")
total_users = len(tg_ids)
success_count = 0
error_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception as e:
error_count += 1
logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}")
await message.answer(
f"📤 Рассылка завершена:\n"
f"👥 Всего пользователей: {total_users}\n"
f"✅ Успешно отправлено: {success_count}\n"
f"❌ Не доставлено: {error_count}"
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await message.answer("❌ Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await state.clear()
+34 -89
View File
@@ -1,3 +1,5 @@
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -18,44 +20,27 @@ router = Router()
@router.callback_query(F.data == "coupons_editor", IsAdminFilter())
async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
finally:
await state.clear()
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=" Создать купон", callback_data="create_coupon"))
builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
markup = builder.as_markup()
await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=markup)
await callback_query.answer()
await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup())
@router.callback_query(F.data == "coupons", IsAdminFilter())
async def show_coupon_list(callback_query: types.CallbackQuery):
async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
try:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
coupons = await get_all_coupons()
coupons = await get_all_coupons(session)
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,
"❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
return
coupon_list = "📜 Список всех купонов:\n\n"
@@ -63,10 +48,10 @@ async def show_coupon_list(callback_query: types.CallbackQuery):
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"
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(
@@ -77,85 +62,60 @@ async def show_coupon_list(callback_query: types.CallbackQuery):
)
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)
await callback_query.message.answer(coupon_list, reply_markup=builder.as_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_"), IsAdminFilter())
async def handle_delete_coupon(callback_query: types.CallbackQuery):
async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any):
coupon_code = callback_query.data[len("delete_coupon_") :]
try:
result = await delete_coupon_from_db(coupon_code)
result = await delete_coupon_from_db(coupon_code, session)
if result:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await show_coupon_list(callback_query)
await show_coupon_list(callback_query, session)
else:
await callback_query.message.answer(
f"❌ Купон с кодом <b>{coupon_code}</b> не найден.",
parse_mode="HTML",
)
await show_coupon_list(callback_query)
await show_coupon_list(callback_query, session)
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", IsAdminFilter())
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}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
markup = builder.as_markup()
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",
reply_markup=markup,
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n",
reply_markup=builder.as_markup(),
)
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
await callback_query.answer()
@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
async def process_coupon_data(message: types.Message, state: FSMContext):
async def process_coupon_data(message: types.Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
markup = builder.as_markup()
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",
reply_markup=markup,
"<b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b> 👈",
reply_markup=builder.as_markup(),
)
return
@@ -165,41 +125,26 @@ async def process_coupon_data(message: types.Message, state: FSMContext):
usage_limit = int(parts[2])
except ValueError:
await message.answer(
"<b>⚠️ Проверьте правильность введенных данных.</b>\n" "Сумма должна быть числом, а лимит — целым числом.",
parse_mode="HTML",
reply_markup=markup,
"⚠️ <b>Проверьте правильность введенных данных!</b>\n"
"💱 Сумма должна быть числом, 🔢 а лимит — целым числом.",
reply_markup=builder.as_markup(),
)
return
try:
await create_coupon(coupon_code, coupon_amount, usage_limit)
await create_coupon(coupon_code, coupon_amount, usage_limit, session)
result_message = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
f"Сумма: <b>{coupon_amount} рублей</b>\n"
f"Лимит использования: <b>{usage_limit} раз</b>."
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 message.answer(result_message, reply_markup=builder.as_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, state: FSMContext):
"""Возвращаем пользователя в меню управления купонами"""
await state.clear()
await show_coupon_management_menu(callback_query)
+65 -48
View File
@@ -1,19 +1,18 @@
from datetime import datetime
import subprocess
from typing import Any
from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from backup import backup_database
from bot import bot
from config import DATABASE_URL
from filters.admin import IsAdminFilter
from handlers.admin.admin_commands import send_message_to_all_clients
from logger import logger
router = Router()
@@ -22,6 +21,7 @@ class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
waiting_for_restart_confirmation = State()
waiting_for_message = State()
@router.callback_query(F.data == "admin", IsAdminFilter())
@@ -33,11 +33,6 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
async def handle_admin_message(message: types.Message, state: FSMContext):
await state.clear()
try:
await message.delete()
except Exception:
pass
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats"))
builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor"))
@@ -45,31 +40,26 @@ async def handle_admin_message(message: types.Message, state: FSMContext):
builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls"))
builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups"))
builder.row(InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot"))
builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile"))
await bot.send_message(
message.chat.id,
"🤖 Панель администратора",
reply_markup=builder.as_markup(),
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer("🤖 Панель администратора", reply_markup=builder.as_markup())
@router.callback_query(F.data == "user_stats", IsAdminFilter())
async def user_stats_menu(callback_query: CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
async def user_stats_menu(callback_query: CallbackQuery, session: Any):
try:
total_users = await conn.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
total_users = await session.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await session.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals")
total_payments_today = await conn.fetchval(
total_payments_today = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE"
)
total_payments_week = await conn.fetchval(
total_payments_week = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
total_payments_all_time = await conn.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments")
total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments")
active_keys = await conn.fetchval(
active_keys = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
@@ -94,22 +84,59 @@ async def user_stats_menu(callback_query: CallbackQuery):
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats"))
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
await callback_query.message.edit_text(stats_message, reply_markup=builder.as_markup(), parse_mode="HTML")
finally:
await conn.close()
await callback_query.message.answer(stats_message, reply_markup=builder.as_markup())
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@router.callback_query(F.data == "send_to_alls", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await send_message_to_all_clients(callback_query.message, state, from_panel=True)
await callback_query.answer()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"✍️ Введите текст сообщения, который вы хотите отправить всем клиентам 📢🌐:",
reply_markup=builder.as_markup(),
)
await state.set_state(UserEditorState.waiting_for_message)
@router.message(UserEditorState.waiting_for_message, IsAdminFilter())
async def process_message_to_all(message: types.Message, state: FSMContext, session: Any):
text_message = message.text
try:
tg_ids = await session.fetch("SELECT tg_id FROM connections")
total_users = len(tg_ids)
success_count = 0
error_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception as e:
error_count += 1
logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}")
await message.answer(
f"📤 Рассылка завершена:\n"
f"👥 Всего пользователей: {total_users}\n"
f"✅ Успешно отправлено: {success_count}\n"
f"❌ Не доставлено: {error_count}"
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await state.clear()
@router.callback_query(F.data == "backups", IsAdminFilter())
async def handle_backup(message: Message):
await message.answer("💾 Инициализация резервного копирования базы данных...")
async def handle_backup(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.answer("💾 Инициализация резервного копирования базы данных...")
await backup_database()
await message.answer("✅ Резервная копия успешно создана и отправлена администратору.")
await callback_query.message.answer("✅ Резервная копия успешно создана и отправлена администратору.")
@router.callback_query(F.data == "restart_bot", IsAdminFilter())
@@ -121,7 +148,7 @@ async def handle_restart(callback_query: CallbackQuery, state: FSMContext):
InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"),
)
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
await callback_query.message.edit_text(
await callback_query.message.answer(
"🤔 Вы уверены, что хотите перезапустить бота?",
reply_markup=builder.as_markup(),
)
@@ -143,16 +170,13 @@ async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext):
text=True,
)
await state.clear()
await callback_query.message.edit_text("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup())
await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup())
except subprocess.CalledProcessError:
await callback_query.message.edit_text("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup())
await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup())
except Exception as e:
await callback_query.message.edit_text(
f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
reply_markup=builder.as_markup(),
await callback_query.message.answer(
f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=builder.as_markup()
)
finally:
await callback_query.answer()
@router.callback_query(F.data == "user_editor", IsAdminFilter())
@@ -167,11 +191,4 @@ async def user_editor_menu(callback_query: CallbackQuery):
builder.row(InlineKeyboardButton(text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id"))
builder.row(InlineKeyboardButton(text="🌐 Поиск по Username", callback_data="search_by_username"))
builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin"))
await callback_query.message.edit_text(
"👇 Выберите способ поиска пользователя:",
reply_markup=builder.as_markup(),
)
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id)
await callback_query.message.answer("👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup())
+284 -406
View File
@@ -1,18 +1,17 @@
import asyncio
from datetime import datetime
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from bot import bot
from config import CLUSTERS, DATABASE_URL, TOTAL_GB
from config import CLUSTERS, TOTAL_GB
from database import get_client_id_by_email, restore_trial, update_key_expiry
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster
from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster
from handlers.utils import sanitize_key_name
from logger import logger
@@ -30,137 +29,126 @@ class UserEditorState(StatesGroup):
@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter())
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("🔍 Введите Telegram ID клиента:")
await callback_query.message.answer("🔍 Введите Telegram ID клиента:")
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.callback_query(F.data == "search_by_username", IsAdminFilter())
async def prompt_username(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("🔍 Введите Username клиента:")
await callback_query.message.answer("🔍 Введите Username клиента:")
await state.set_state(UserEditorState.waiting_for_username)
@router.message(UserEditorState.waiting_for_username, IsAdminFilter())
async def handle_username_input(message: types.Message, state: FSMContext):
async def handle_username_input(message: types.Message, state: FSMContext, session: Any):
username = message.text.strip()
conn = await asyncpg.connect(DATABASE_URL)
try:
user_record = await conn.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
if not user_record:
await message.reply("🔍 Пользователь с указанным username не найден. 🚫")
await state.clear()
return
if not user_record:
await message.answer("🔍 Пользователь с указанным username не найден. 🚫")
await state.clear()
return
tg_id = user_record["tg_id"]
username = await conn.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
tg_id = user_record["tg_id"]
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
await message.reply("Пользователь с указанным tg_id не найден.")
await state.clear()
return
if balance is None:
await message.answer("🚫 Пользователь с указанным tg_id не найден. 🔍")
await state.clear()
return
builder = InlineKeyboardBuilder()
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.reply(user_info, reply_markup=builder.as_markup(), parse_mode="HTML")
await state.set_state(UserEditorState.displaying_user_info)
finally:
await conn.close()
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter())
async def handle_tg_id_input(message: types.Message, state: FSMContext):
async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any):
tg_id = int(message.text)
username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
conn = await asyncpg.connect(DATABASE_URL)
try:
username = await conn.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
await message.answer("❌ Пользователь с указанным tg_id не найден. 🔍")
await state.clear()
return
if balance is None:
await message.reply("❌ Пользователь с указанным tg_id не найден. 🔍")
await state.clear()
return
builder = InlineKeyboardBuilder()
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.reply(user_info, reply_markup=builder.as_markup(), parse_mode="HTML")
await state.set_state(UserEditorState.displaying_user_info)
finally:
await conn.close()
user_info = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter())
async def handle_restore_trial(callback_query: types.CallbackQuery):
async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any):
tg_id = int(callback_query.data.split("_")[2])
await restore_trial(tg_id)
await restore_trial(tg_id, session)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin"))
await callback_query.message.edit_text("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup())
await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup())
@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter())
@@ -168,31 +156,129 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex
tg_id = int(callback_query.data.split("_")[2])
await state.update_data(tg_id=tg_id)
await callback_query.message.edit_text("💸 Введите новую сумму баланса:")
await callback_query.answer()
await callback_query.message.answer("💸 Введите новую сумму баланса:")
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter())
async def handle_new_balance_input(message: types.Message, state: FSMContext):
async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any):
if not message.text.isdigit() or int(message.text) < 0:
await message.reply("❌ Пожалуйста, введите корректную сумму для изменения баланса.")
await message.answer("❌ Пожалуйста, введите корректную сумму для изменения баланса.")
return
new_balance = int(message.text)
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
new_balance,
tg_id,
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
new_balance,
tg_id,
)
response_message = f"✅ Баланс успешно изменен на <b>{new_balance}</b>."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора",
callback_data="admin",
)
)
await message.answer(response_message, reply_markup=builder.as_markup())
await state.clear()
response_message = f"✅ Баланс успешно изменен на <b>{new_balance}</b>."
async def get_key_details(email, session):
record = await session.fetchrow(
"""
SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance
FROM keys k
JOIN connections c ON k.tg_id = c.tg_id
WHERE k.email = $1
""",
email,
)
if not record:
return None
# Определение сервера
server_name = "Неизвестный сервер"
for cluster in CLUSTERS.values():
if record['server_id'] in cluster:
server_name = cluster[record['server_id']].get("name", "Неизвестный сервер")
break
# Расчет времени до истечения
expiry_date = datetime.utcfromtimestamp(record['expiry_time'] / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
return {
'key': record['key'],
'expiry_date': expiry_date.strftime("%d %B %Y года"),
'days_left_message': days_left_message,
'server_name': server_name,
'balance': record['balance'],
'tg_id': record['tg_id'],
}
@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter())
async def process_key_edit(callback_query: CallbackQuery, session: Any):
email = callback_query.data.split("_", 2)[2]
key_details = await get_key_details(email, session)
if not key_details:
await callback_query.message.answer("🔍 <b>Информация о ключе не найдена.</b> 🚫")
return
response_message = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Сервер: <b>{key_details['server_name']}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{email}",
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.answer("🔑 Введите имя ключа:")
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any):
key_name = sanitize_key_name(message.text)
key_details = await get_key_details(key_name, session)
if not key_details:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
@@ -200,202 +286,57 @@ async def handle_new_balance_input(message: types.Message, state: FSMContext):
callback_data="admin",
)
)
await message.reply(
response_message,
await message.answer(
"🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=builder.as_markup(),
parse_mode="HTML",
)
await state.clear()
return
finally:
await conn.close()
response_message = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Сервер: <b>{key_details['server_name']}</b>"
)
await state.clear()
@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter())
async def process_key_edit(callback_query: CallbackQuery):
email = callback_query.data.split("_", 2)[2]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.key, k.expiry_time, k.server_id
FROM keys k
WHERE k.email = $1
""",
email,
)
if record:
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = "Неизвестный сервер"
for cluster in CLUSTERS.values():
if server_id in cluster:
server_name = cluster[server_id].get("name", "Неизвестный сервер")
break
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = (
f"Ключ: <pre>{key}</pre>\n"
f"Дата истечения: <b>{formatted_expiry_date}</b>\n"
f"{days_left_message}\n"
f"Сервер: <b>{server_name}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
),
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{email}",
),
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.edit_text(
response_message,
reply_markup=builder.as_markup(),
parse_mode="HTML",
)
else:
await callback_query.message.edit_text("<b>Информация о ключе не найдена.</b>", parse_mode="HTML")
finally:
await conn.close()
except Exception as e:
logger.error(f"Ошибка при получении информации о ключе: {e}")
await callback_query.answer()
@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("🔑 Введите имя ключа:")
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
async def handle_key_name_input(message: types.Message, state: FSMContext):
key_name = sanitize_key_name(message.text)
conn = await asyncpg.connect(DATABASE_URL)
try:
user_records = await conn.fetch(
"""
SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.email = $1
""",
key_name,
key_buttons = InlineKeyboardBuilder()
key_buttons.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{key_name}",
)
if not user_records:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора",
callback_data="admin",
)
)
await message.reply(
"🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=builder.as_markup(),
)
await state.clear()
return
response_messages = []
key_buttons = InlineKeyboardBuilder()
for record in user_records:
balance = record["balance"]
email = record["email"]
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = "Неизвестный сервер"
for cluster in CLUSTERS.values():
if server_id in cluster:
server_name = cluster[server_id].get("name", "Неизвестный сервер")
break
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%d %B %Y")
response_messages.append(
f"🔑 Ключ: <pre>{key}</pre>\n"
f"⏰ Дата истечения: <b>{expiry_date}</b>\n"
f"💰 Баланс пользователя: <b>{balance}</b>\n"
f"🌐 Сервер: <b>{server_name}</b>"
)
key_buttons.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
)
key_buttons.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{email}",
)
)
key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await message.reply(
"\n".join(response_messages),
reply_markup=key_buttons.as_markup(),
parse_mode="HTML",
)
key_buttons.row(
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{key_name}",
)
)
key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
finally:
await conn.close()
await message.answer(response_message, reply_markup=key_buttons.as_markup())
await state.clear()
@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter())
async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
email = callback_query.data.split("|")[1]
await callback_query.message.edit_text(
f"⏳ Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:",
parse_mode="HTML",
await callback_query.message.answer(
f"⏳ Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:"
)
await state.update_data(email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(message: types.Message, state: FSMContext):
async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any):
user_data = await state.get_data()
email = user_data.get("email")
if not email:
await message.reply("Email не найден в состоянии.")
await message.answer("📧 Email не найден в состоянии. 🚫")
await state.clear()
return
@@ -405,160 +346,97 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext):
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
await message.answer(f"🚫 Клиент с email {email} не найден. 🔍")
await state.clear()
return
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
if not record:
await message.reply("Клиент не найден в базе данных.")
await state.clear()
return
record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
if not record:
await message.answer("🚫 Клиент не найден в базе данных. 🔍")
await state.clear()
return
async def update_key_on_all_servers():
tasks = []
for cluster_id in CLUSTERS:
tasks.append(
asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
expiry_time,
total_gb=TOTAL_GB,
)
async def update_key_on_all_servers():
tasks = []
for cluster_id in CLUSTERS:
tasks.append(
asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
expiry_time,
total_gb=TOTAL_GB,
)
)
await asyncio.gather(*tasks)
)
await asyncio.gather(*tasks)
await update_key_on_all_servers()
await update_key_on_all_servers()
await update_key_expiry(client_id, expiry_time)
await update_key_expiry(client_id, expiry_time)
response_message = (
f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await message.reply(
response_message,
reply_markup=builder.as_markup(),
parse_mode="HTML",
)
finally:
await conn.close()
response_message = (
f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await message.answer(response_message, reply_markup=builder.as_markup())
except ValueError:
await message.reply("❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.")
await message.answer("❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
logger.error(e)
await state.clear()
@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter())
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any):
email = callback_query.data.split("|")[1]
client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email)
conn = await asyncpg.connect(DATABASE_URL)
try:
client_id = await conn.fetchval("SELECT client_id FROM keys WHERE email = $1", email)
if client_id is None:
await bot.edit_message_text(
"Ключ не найден.",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
return
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete_admin|{client_id}",
)
if client_id is None:
await callback_query.message.answer(
"🔍 Ключ не найден. 🚫",
)
builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys"))
await bot.edit_message_text(
"<b>❓ Вы уверены, что хотите удалить ключ?</b>",
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=builder.as_markup(),
parse_mode="HTML",
)
finally:
await conn.close()
return
await callback_query.answer()
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete_admin|{client_id}",
)
)
builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys"))
await callback_query.message.answer(
"<b>❓ Вы уверены, что хотите удалить ключ?</b>",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter())
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
client_id = callback_query.data.split("|")[1]
record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id)
if record:
email = record["email"]
response_message = "✅ Ключ успешно удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id)
async def delete_key_from_servers(email, client_id):
tasks = []
for cluster_id in CLUSTERS:
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
if record:
email = record["email"]
response_message = "✅ Ключ успешно удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
await delete_key_from_servers(email, client_id)
await delete_key_from_db(client_id, session)
async def delete_key_from_servers(email, client_id):
tasks = []
for cluster_id in CLUSTERS:
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
await delete_key_from_servers(email, client_id)
await delete_key_from_db(client_id)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=builder.as_markup(),
)
else:
response_message = "🚫 Ключ не найден или уже удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=builder.as_markup(),
)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(
f"Ошибка при удалении ключа: {e}",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
await callback_query.answer()
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
else:
response_message = "🚫 Ключ не найден или уже удален."
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
-17
View File
@@ -1,17 +0,0 @@
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from handlers.start import start_command
router = Router()
@router.message(Command("start"))
async def handle_start(message: types.Message, state: FSMContext, admin: bool = False):
await start_command(message, admin)
@router.message(Command("menu"))
async def handle_menu(message: types.Message, state: FSMContext, admin: bool = False):
await start_command(message, admin)
+43 -68
View File
@@ -1,15 +1,13 @@
from datetime import datetime
from typing import Any
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
import asyncpg
from config import DATABASE_URL
from database import update_balance
from logger import logger
class CouponActivationState(StatesGroup):
@@ -21,98 +19,75 @@ 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}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
"<b>🎫 Введите код купона:</b>\n\n"
"📝 Пожалуйста, введите действующий код купона, который вы хотите активировать. 🔑",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
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}")
async def process_coupon_code(message: types.Message, state: FSMContext, session: Any):
coupon_code = message.text.strip()
activation_result = await activate_coupon(message.from_user.id, coupon_code)
activation_result = await activate_coupon(message.chat.id, coupon_code, session)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(activation_result, reply_markup=builder.as_markup(), parse_mode="HTML")
await state.clear()
async def activate_coupon(user_id: int, coupon_code: str):
"""Функция для активации купона"""
conn = await asyncpg.connect(DATABASE_URL)
async def activate_coupon(user_id: int, coupon_code: str, session: Any):
coupon_record = await session.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,
)
try:
coupon_record = await conn.fetchrow(
if not coupon_record:
return "<b>❌ Купон не найден</b> 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍"
usage_exists = await session.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 session.transaction():
await session.execute(
"""
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
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_code,
coupon_record["id"],
)
if not coupon_record:
return "<b>❌ Купон не найден</b> 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍"
usage_exists = await conn.fetchrow(
await session.execute(
"""
SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
INSERT INTO coupon_usages (coupon_id, user_id, used_at)
VALUES ($1, $2, $3)
""",
coupon_record["id"],
user_id,
datetime.utcnow(),
)
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()
await update_balance(user_id, coupon_amount)
return f"<b>✅ Купон успешно активирован! 🎉</b>\n\nНа ваш баланс добавлено <b>{coupon_amount} рублей</b> 💰."
+6 -40
View File
@@ -4,7 +4,6 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import RUB_TO_XTR
from logger import logger
@@ -20,12 +19,7 @@ router = Router()
@router.callback_query(F.data == "donate")
async def process_donate(callback_query: types.CallbackQuery, state: FSMContext):
try:
await state.clear()
await callback_query.message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"))
@@ -35,36 +29,26 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext)
callback_data="enter_custom_donate_amount",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await bot.send_message(
chat_id=callback_query.from_user.id,
await callback_query.message.answer(
text="🌟 Поддержите наш проект! 💪\n\n"
"💖 Каждый донат помогает развивать и улучшать сервис. "
"🤝 Мы ценим вашу поддержку и работаем над тем, чтобы сделать наш продукт еще лучше. 🚀💡",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "enter_custom_donate_amount")
async def process_enter_donate_amount(callback_query: types.CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate"))
await callback_query.message.edit_text(f"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup())
await callback_query.message.answer(f"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup())
await state.set_state(DonateState.entering_donate_amount)
await callback_query.answer()
@router.message(DonateState.entering_donate_amount)
async def process_donate_amount_input(message: types.Message, state: FSMContext):
try:
await message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
@@ -91,7 +75,6 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext)
await state.set_state(DonateState.waiting_for_donate_payment)
except Exception as e:
logger.error(f"Ошибка при создании доната: {e}")
await message.answer("Произошла ошибка при создании доната.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
@@ -104,31 +87,14 @@ async def on_pre_checkout_query(pre_checkout_query: PreCheckoutQuery):
@router.message(F.successful_payment, DonateState.waiting_for_donate_payment)
async def on_successful_donate(message: types.Message, state: FSMContext):
try:
user_id = int(message.from_user.id)
amount = float(message.successful_payment.invoice_payload.split("_")[0])
logger.debug(f"Donate succeeded for user_id: {user_id}, amount: {amount}")
state_data = await state.get_data()
previous_message_id = state_data.get("last_message_id")
if previous_message_id:
try:
await bot.delete_message(chat_id=user_id, message_id=previous_message_id)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Вернуться в профиль", callback_data="view_profile"))
sent_message = await bot.send_message(
chat_id=user_id,
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖",
reply_markup=builder.as_markup(),
)
await state.update_data(last_message_id=sent_message.message_id)
await state.clear()
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
except Exception as e:
+26 -76
View File
@@ -1,114 +1,64 @@
import os
from typing import Any
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from bot import bot
from config import CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from config import CONNECT_WINDOWS, SUPPORT_CHAT_URL
from handlers.texts import INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE
from logger import logger
router = Router()
@router.callback_query(F.data == "instructions")
async def send_instructions(callback_query: types.CallbackQuery):
await callback_query.message.delete()
instructions_message = INSTRUCTIONS
image_path = os.path.join("img", "instructions.jpg")
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
await callback_query.answer()
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile"),
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"),
)
with open(image_path, "rb") as image_from_buffer:
await callback_query.message.answer_photo(
BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"),
caption=instructions_message,
parse_mode="Markdown",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
record = await session.fetchrow(
"""
SELECT k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
# Поиск ключа по имени ключа
record = await conn.fetchrow(
"""
SELECT k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
if not record:
await callback_query.message.answer("❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍")
return
if not record:
await bot.send_message(
chat_id=tg_id,
text="<b>Ключ не найден. Проверьте имя ключа.</b>",
parse_mode="HTML",
)
return
key = record["key"]
key_message = KEY_MESSAGE.format(key)
instruction_message = f"{key_message}{INSTRUCTION_PC}"
key = record["key"]
key_message = KEY_MESSAGE.format(key)
instruction_message = f"{key_message}{INSTRUCTION_PC}"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"))
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
connect_windows_button = types.InlineKeyboardButton(
text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"
)
support_button = types.InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}")
back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="view_profile")
inline_keyboard = [
[connect_windows_button],
[support_button],
[back_button],
]
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
await bot.send_message(
tg_id,
instruction_message,
reply_markup=keyboard,
parse_mode="HTML",
)
finally:
await conn.close()
except Exception as e:
logger.error(f"Ошибка при получении ключа: {e}")
await bot.send_message(
chat_id=tg_id,
text="<b>Произошла ошибка. Пожалуйста, повторите попытку позже.</b>",
parse_mode="HTML",
)
await callback_query.answer()
await callback_query.message.answer(instruction_message, reply_markup=builder.as_markup())
+69 -158
View File
@@ -1,27 +1,25 @@
import asyncio
from datetime import datetime, timedelta
from typing import Any
import uuid
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
import asyncpg
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot, dp
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
DATABASE_URL,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
PUBLIC_LINK,
SUPPORT_CHAT_URL,
from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL
from database import (
add_connection,
check_connection_exists,
get_balance,
get_trial,
store_key,
update_balance,
use_trial,
)
from database import add_connection, get_balance, store_key, update_balance
from handlers.instructions.instructions import send_instructions
from handlers.keys.key_utils import create_key_on_cluster
from handlers.profile import process_callback_view_profile
from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, RENEWAL_PLANS, key_message_success
from handlers.utils import get_least_loaded_cluster, sanitize_key_name
from logger import logger
@@ -36,96 +34,57 @@ class Form(StatesGroup):
waiting_for_message = State()
@dp.callback_query(F.data == "create_key")
async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception:
pass
@router.callback_query(F.data == "create_key")
async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext, session: Any):
server_id = "все сервера"
await state.update_data(selected_server_id=server_id)
await select_server(callback_query, state)
await callback_query.answer()
await select_server(callback_query, state, session)
async def select_server(callback_query: CallbackQuery, state: FSMContext):
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1",
callback_query.from_user.id,
)
finally:
await conn.close()
trial_status = existing_connection["trial"] if existing_connection else 0
async def select_server(callback_query: CallbackQuery, state: FSMContext, session: Any):
trial_status = await get_trial(callback_query.message.chat.id, session)
if trial_status == 1:
await bot.send_message(
chat_id=callback_query.from_user.id,
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="✅ Да, подключить новое устройство", callback_data="confirm_create_new_key")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
text=KEY,
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Да, подключить новое устройство",
callback_data="confirm_create_new_key",
)
],
[InlineKeyboardButton(text="↩️ Назад", callback_data="cancel_create_key")],
]
),
reply_markup=builder.as_markup(),
)
await state.update_data(creating_new_key=True)
else:
await bot.send_message(
chat_id=callback_query.from_user.id,
text=KEY_TRIAL,
parse_mode="HTML",
)
await callback_query.message.answer(KEY_TRIAL)
await state.set_state(Form.waiting_for_key_name)
await callback_query.answer()
@dp.callback_query(F.data == "confirm_create_new_key")
@router.callback_query(F.data == "confirm_create_new_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} confirmed creation of a new key.")
balance = await get_balance(tg_id)
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(NULL_BALANCE, reply_markup=builder.as_markup())
await state.clear()
return
logger.info(f"Balance for user {tg_id} is sufficient. Asking for device name.")
await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:")
await callback_query.message.answer("🔑 Пожалуйста, введите имя подключаемого устройства:")
await state.set_state(Form.waiting_for_key_name)
logger.info(f"State set to waiting_for_key_name for user {tg_id}")
await state.update_data(creating_new_key=True)
await callback_query.answer()
@dp.callback_query(F.data == "cancel_create_key")
async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext, admin: bool):
await process_callback_view_profile(callback_query, state, admin)
await callback_query.answer()
@router.message(Form.waiting_for_key_name)
async def handle_key_name_input(message: Message, state: FSMContext):
tg_id = message.from_user.id
async def handle_key_name_input(message: Message, state: FSMContext, session: Any):
tg_id = message.chat.id
key_name = sanitize_key_name(message.text)
logger.info(f"User {tg_id} is attempting to create a key with the name: {key_name}")
@@ -135,38 +94,27 @@ async def handle_key_name_input(message: Message, state: FSMContext):
logger.warning(f"User {tg_id} entered an invalid key name: {key_name}")
return
conn = await asyncpg.connect(DATABASE_URL)
try:
logger.info(f"Checking if key name '{key_name}' already exists for user {tg_id} in the database.")
existing_key = await conn.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
key_name.lower(),
tg_id,
logger.info(f"Checking if key name '{key_name}' already exists for user {tg_id} in the database.")
existing_key = await session.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
key_name.lower(),
tg_id,
)
if existing_key:
await message.answer(
"❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.",
)
if existing_key:
await message.bot.send_message(
tg_id,
"❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.",
)
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}.")
await state.set_state(Form.waiting_for_key_name)
return
finally:
await conn.close()
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}.")
await state.set_state(Form.waiting_for_key_name)
return
client_id = str(uuid.uuid4())
email = key_name.lower()
current_time = datetime.utcnow()
expiry_time = None
conn = await asyncpg.connect(DATABASE_URL)
try:
logger.info(f"Checking trial status for user {tg_id}.")
existing_connection = await conn.fetchrow("SELECT trial FROM connections WHERE tg_id = $1", tg_id)
finally:
await conn.close()
trial_status = existing_connection["trial"] if existing_connection else 0
logger.info(f"Checking trial status for user {tg_id}.")
trial_status = await get_trial(message.chat.id, session)
if trial_status == 0:
expiry_time = current_time + timedelta(days=1, hours=3)
@@ -174,12 +122,11 @@ async def handle_key_name_input(message: Message, state: FSMContext):
else:
balance = await get_balance(tg_id)
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await message.bot.send_message(
tg_id,
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
"💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.",
reply_markup=keyboard,
reply_markup=builder.as_markup(),
)
logger.warning(f"User {tg_id} has insufficient funds for key creation.")
await state.clear()
@@ -194,29 +141,17 @@ async def handle_key_name_input(message: Message, state: FSMContext):
logger.info(f"Generated public link for the key: {public_link}")
button_support = InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL)
button_profile = InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
button_iphone = InlineKeyboardButton(text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}")
button_android = InlineKeyboardButton(
text="🤖 Подключить",
url=f"{CONNECT_ANDROID}{public_link}",
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS),
InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID),
)
button_download_ios = InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS)
button_download_android = InlineKeyboardButton(
text="🤖 Скачать",
url=DOWNLOAD_ANDROID,
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[button_support],
[button_download_ios, button_download_android],
[button_iphone, button_android],
[button_profile],
]
builder.row(
InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}"),
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
remaining_time = expiry_time - current_time
days = remaining_time.days
@@ -224,7 +159,7 @@ async def handle_key_name_input(message: Message, state: FSMContext):
logger.info(f"Sending key message to user {tg_id} with the public link.")
await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard)
await message.answer(key_message, reply_markup=builder.as_markup())
try:
least_loaded_cluster = await get_least_loaded_cluster()
@@ -244,40 +179,16 @@ async def handle_key_name_input(message: Message, state: FSMContext):
await asyncio.gather(*tasks)
conn = await asyncpg.connect(DATABASE_URL)
try:
logger.info(f"Updating trial status for user {tg_id} in the database.")
existing_connection = await conn.fetchrow("SELECT * FROM connections WHERE tg_id = $1", tg_id)
if existing_connection:
await conn.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
else:
await add_connection(tg_id, 0, 1)
finally:
await conn.close()
logger.info(f"Updating trial status for user {tg_id} in the database.")
connection_exists = await check_connection_exists(message.chat.id)
if connection_exists:
await use_trial(message.chat.id, session)
else:
await add_connection(tg_id=tg_id, balance=0, trial=1, session=session)
logger.info(f"Storing key for user {tg_id} in the database.")
await store_key(
tg_id,
client_id,
email,
expiry_timestamp,
public_link,
least_loaded_cluster,
)
await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session)
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id}: {e}")
await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}")
await state.clear()
@dp.callback_query(F.data == "instructions")
async def handle_instructions(callback_query: CallbackQuery):
await send_instructions(callback_query)
@dp.callback_query(F.data == "back_to_main")
async def handle_back_to_main(callback_query: CallbackQuery, state: FSMContext, admin: bool):
await process_callback_view_profile(callback_query, state, admin)
await callback_query.answer()
+2 -6
View File
@@ -79,15 +79,11 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
raise e
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
async def delete_key_from_db(client_id, session):
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
await session.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
async def delete_key_from_cluster(cluster_id, email, client_id):
+310 -494
View File
@@ -2,22 +2,13 @@ import asyncio
from datetime import datetime, timedelta
import locale
import os
from typing import Any
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile
import asyncpg
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CLUSTERS,
CONNECT_ANDROID,
CONNECT_IOS,
DATABASE_URL,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
PUBLIC_LINK,
TOTAL_GB,
)
from config import CLUSTERS, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, TOTAL_GB
from database import delete_key, get_balance, store_key, update_balance, update_key_expiry
from handlers.keys.key_utils import (
delete_key_from_cluster,
@@ -43,207 +34,147 @@ router = Router()
@router.callback_query(F.data == "view_keys")
async def process_callback_view_keys(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_view_keys(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch(
"""
SELECT email, client_id FROM keys WHERE tg_id = $1
""",
tg_id,
records = await session.fetch(
"""
SELECT email, client_id FROM keys WHERE tg_id = $1
""",
tg_id,
)
if records:
builder = InlineKeyboardBuilder()
for record in records:
key_name = record["email"]
builder.row(InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
inline_keyboard = builder.as_markup()
response_message = (
"<b>🔑 Список ваших устройств</b>\n\n" "<i>👇 Выберите устройство для управления подпиской:</i>"
)
if records:
buttons = []
for record in records:
key_name = record["email"]
button = types.InlineKeyboardButton(
text=f"🔑 {key_name}",
callback_data=f"view_key|{key_name}",
image_path = os.path.join("img", "pic_keys.jpg")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"),
caption=response_message,
reply_markup=inline_keyboard,
)
buttons.append([button])
back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile")
buttons.append([back_button])
inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons)
response_message = (
"<b>🔑 Список ваших устройств</b>\n\n" "<i>👇 Выберите устройство для управления подпиской:</i>"
else:
await callback_query.message.answer(
text=response_message,
reply_markup=inline_keyboard,
)
image_path = os.path.join("img", "pic_keys.jpg")
else:
response_message = NO_KEYS
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=" Создать подписку", callback_data="create_key"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
try:
await bot.delete_message(
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
keyboard = builder.as_markup()
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"),
caption=response_message,
parse_mode="HTML",
reply_markup=inline_keyboard,
)
else:
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=inline_keyboard,
parse_mode="HTML",
)
image_path = os.path.join("img", "pic_keys.jpg")
else:
response_message = NO_KEYS
create_key_button = types.InlineKeyboardButton(text=" Создать подписку", callback_data="create_key")
back_button = types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]])
try:
await bot.delete_message(
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
image_path = os.path.join("img", "pic_keys.jpg")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"),
caption=response_message,
parse_mode="HTML",
reply_markup=keyboard,
)
else:
await bot.send_message(
chat_id=tg_id,
text=response_message,
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"),
caption=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
finally:
await conn.close()
else:
await callback_query.message.answer(
text=response_message,
reply_markup=keyboard,
)
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при получении ключей: {e}")
await callback_query.answer()
@router.callback_query(F.data.startswith("view_key|"))
async def process_callback_view_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception:
pass
record = await session.fetchrow(
"""
SELECT k.expiry_time, k.server_id, k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
)
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.expiry_time, k.server_id, k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
key_name,
if record:
key = record["key"]
expiry_time = record["expiry_time"]
server_name = record["server_id"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(key, formatted_expiry_date, days_left_message, server_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS),
InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID),
)
if record:
key = record["key"]
expiry_time = record["expiry_time"]
server_name = record["server_id"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
builder.row(
InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}"),
InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}"),
)
if time_left.total_seconds() <= 0:
days_left_message = "<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}"))
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(key, formatted_expiry_date, days_left_message, server_name)
builder.row(
InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"),
InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"),
)
download_android_button = types.InlineKeyboardButton(text="🤖 Скачать", url=DOWNLOAD_ANDROID)
download_iphone_button = types.InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS)
connect_iphone_button = types.InlineKeyboardButton(text="🍏 Подключить", url=f"{CONNECT_IOS}{key}")
connect_android_button = types.InlineKeyboardButton(text="🤖 Подключить", url=f"{CONNECT_ANDROID}{key}")
connect_pc_button = types.InlineKeyboardButton(
text="💻 Windows/Linux",
callback_data=f"connect_pc|{key_name}",
if not key.startswith(PUBLIC_LINK):
builder.row(
InlineKeyboardButton(text="🔄 Обновить подписку", callback_data=f"update_subscription|{key_name}")
)
renew_button = types.InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}")
delete_button = types.InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}")
back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="view_profile")
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
inline_keyboard = [
[download_iphone_button, download_android_button],
[connect_iphone_button, connect_android_button],
[connect_pc_button],
[renew_button, delete_button],
]
keyboard = builder.as_markup()
if not key.startswith(PUBLIC_LINK):
update_subscription_button = types.InlineKeyboardButton(
text="🔄 Обновить подписку",
callback_data=f"update_subscription|{key_name}",
)
inline_keyboard.append([update_subscription_button])
image_path = os.path.join("img", "pic_view.jpg")
inline_keyboard.append([back_button])
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
return
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
image_path = os.path.join("img", "pic_view.jpg")
if not os.path.isfile(image_path):
await bot.send_message(tg_id, "Файл изображения не найден.")
return
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"),
caption=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
else:
await bot.send_message(
chat_id=tg_id,
text="<b>Информация о подписке не найдена.</b>",
parse_mode="HTML",
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"),
caption=response_message,
reply_markup=keyboard,
)
finally:
await conn.close()
else:
await callback_query.message.answer(
text="<b>Информация о подписке не найдена.</b>",
)
except Exception as e:
await handle_error(
tg_id,
@@ -251,127 +182,85 @@ async def process_callback_view_key(callback_query: types.CallbackQuery):
f"Ошибка при получении информации о ключе: {e}",
)
await callback_query.answer()
@router.callback_query(F.data.startswith("update_subscription|"))
async def process_callback_update_subscription(
callback_query: types.CallbackQuery,
):
tg_id = callback_query.from_user.id
async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
email = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.key, k.expiry_time, k.email, k.server_id, k.client_id
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
email,
)
record = await session.fetchrow(
"""
SELECT k.key, k.expiry_time, k.email, k.server_id, k.client_id
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
email,
)
if record:
expiry_time = record["expiry_time"]
client_id = record["client_id"]
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
if record:
expiry_time = record["expiry_time"]
client_id = record["client_id"]
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
try:
await conn.execute(
"""
DELETE FROM keys
WHERE tg_id = $1 AND email = $2
""",
tg_id,
email,
)
except Exception as delete_error:
await bot.send_message(
tg_id,
f"Ошибка при удалении старой подписки: {delete_error}",
)
return
least_loaded_cluster_id = await get_least_loaded_cluster()
tasks = []
tasks.append(
update_key_on_cluster(
tg_id,
client_id,
email,
expiry_time,
least_loaded_cluster_id,
)
try:
await session.execute(
"""
DELETE FROM keys
WHERE tg_id = $1 AND email = $2
""",
tg_id,
email,
)
except Exception as delete_error:
await callback_query.message.answer(
f"Ошибка при удалении старой подписки: {delete_error}",
)
return
await asyncio.gather(*tasks)
least_loaded_cluster_id = await get_least_loaded_cluster()
await store_key(
tasks = []
tasks.append(
update_key_on_cluster(
tg_id,
client_id,
email,
expiry_time,
public_link,
server_id=least_loaded_cluster_id,
least_loaded_cluster_id,
)
)
try:
await bot.delete_message(
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await asyncio.gather(*tasks)
response_message = f"Ваша подписка {email} обновлена!"
back_button = types.InlineKeyboardButton(text="🔙 Назад в профиль", callback_data="view_profile")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.send_message(
tg_id,
response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
else:
try:
await bot.delete_message(
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await bot.send_message(
tg_id,
"<b>Ключ не найден в базе данных.</b>",
parse_mode="HTML",
)
finally:
await conn.close()
await store_key(
tg_id,
client_id,
email,
expiry_time,
public_link,
server_id=least_loaded_cluster_id,
)
response_message = f"Ваша подписка {email} обновлена!"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
response_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer(
"<b>Ключ не найден в базе данных.</b>",
)
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}")
await callback_query.answer()
@router.callback_query(F.data.startswith("delete_key|"))
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception:
pass
confirmation_keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
@@ -384,177 +273,124 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery):
]
)
await bot.send_message(
chat_id=tg_id,
await callback_query.message.answer(
text="<b>Вы уверены, что хотите удалить ключ?</b>",
reply_markup=confirmation_keyboard,
parse_mode="HTML",
)
except Exception as e:
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при удалении ключа:</b> {e}",
parse_mode="HTML",
)
await callback_query.answer()
logger.error(e)
@router.callback_query(F.data.startswith("renew_key|"))
async def process_callback_renew_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_renew_key(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception:
pass
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT client_id, expiry_time
FROM keys
WHERE email = $1
""",
key_name,
)
if record:
client_id = record["client_id"]
expiry_time = record["expiry_time"]
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
callback_data=f"renew_plan|1|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)',
callback_data=f"renew_plan|3|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)',
callback_data=f"renew_plan|6|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)',
callback_data=f"renew_plan|12|{client_id}",
)
],
[types.InlineKeyboardButton(text="🔙 Назад", callback_data="view_profile")],
]
)
balance = await get_balance(tg_id)
response_message = PLAN_SELECTION_MSG.format(
balance=balance,
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
)
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
else:
# Если ключ не найден
response_message = "<b>Ключ не найден.</b>"
await bot.send_message(chat_id=tg_id, text=response_message, parse_mode="HTML")
finally:
await conn.close()
except Exception as e:
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при выборе плана:</b> {e}",
parse_mode="HTML",
record = await session.fetchrow(
"""
SELECT client_id, expiry_time
FROM keys
WHERE email = $1
""",
key_name,
)
await callback_query.answer()
if record:
client_id = record["client_id"]
expiry_time = record["expiry_time"]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
callback_data=f"renew_plan|1|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)',
callback_data=f"renew_plan|3|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)',
callback_data=f"renew_plan|6|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)',
callback_data=f"renew_plan|12|{client_id}",
)
)
balance = await get_balance(tg_id)
response_message = PLAN_SELECTION_MSG.format(
balance=balance,
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
)
await callback_query.message.answer(
text=response_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer("<b>Ключ не найден.</b>")
except Exception as e:
logger.error(e)
@router.callback_query(F.data.startswith("confirm_delete|"))
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
email = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow("SELECT client_id FROM keys WHERE email = $1", email)
record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email)
if record:
client_id = record["client_id"]
response_message = "Ключ успешно удален."
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
if record:
client_id = record["client_id"]
response_message = "Ключ успешно удален."
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await delete_key(client_id)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
await delete_key(client_id)
await callback_query.message.answer(
response_message,
reply_markup=keyboard,
)
async def delete_key_from_servers():
try:
tasks = []
for cluster_id, cluster in CLUSTERS.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
async def delete_key_from_servers():
try:
tasks = []
for cluster_id, cluster in CLUSTERS.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id}: {e}")
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id}: {e}")
asyncio.create_task(delete_key_from_servers())
asyncio.create_task(delete_key_from_servers())
await delete_key_from_db(client_id)
await delete_key_from_db(client_id, session)
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
finally:
await conn.close()
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(
response_message,
reply_markup=keyboard,
)
except Exception as e:
await bot.edit_message_text(
f"Ошибка при удалении ключа: {e}",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
await callback_query.answer()
logger.error(e)
@router.callback_query(F.data.startswith("renew_plan|"))
async def process_callback_renew_plan(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
plan, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
@@ -565,83 +401,63 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery):
total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
record = await session.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1",
client_id,
)
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1",
client_id,
)
if record:
email = record["email"]
expiry_time = record["expiry_time"]
current_time = datetime.utcnow().timestamp() * 1000
if expiry_time <= current_time:
new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000)
else:
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
cost = RENEWAL_PLANS[plan]["price"]
balance = await get_balance(tg_id)
if balance < cost:
replenish_button = types.InlineKeyboardButton(text="Пополнить баланс", callback_data="pay")
view_profile = types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [view_profile]])
await bot.send_message(
tg_id,
INSUFFICIENT_FUNDS_MSG,
reply_markup=keyboard,
parse_mode="HTML",
)
return
response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"])
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_profile")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.send_message(
tg_id,
response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
async def renew_key_on_servers():
tasks = []
for cluster_id in CLUSTERS:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
await renew_key_on_servers()
if record:
email = record["email"]
expiry_time = record["expiry_time"]
current_time = datetime.utcnow().timestamp() * 1000
if expiry_time <= current_time:
new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000)
else:
await bot.send_message(tg_id, KEY_NOT_FOUND_MSG, parse_mode="HTML")
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
finally:
await conn.close()
cost = RENEWAL_PLANS[plan]["price"]
balance = await get_balance(tg_id)
if balance < cost:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
INSUFFICIENT_FUNDS_MSG,
reply_markup=builder.as_markup(),
)
return
response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
async def renew_key_on_servers():
tasks = []
for cluster_id in CLUSTERS:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
await renew_key_on_servers()
else:
await callback_query.message.answer(KEY_NOT_FOUND_MSG)
except Exception as e:
await bot.send_message(tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML")
await callback_query.answer()
logger.error(e)
+33 -87
View File
@@ -1,107 +1,53 @@
import asyncio
from datetime import datetime, timedelta
from typing import Any
import uuid
import asyncpg
from py3xui import AsyncApi
from client import add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, PUBLIC_LINK, TRIAL_TIME
from database import store_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, PUBLIC_LINK, TRIAL_TIME
from database import store_key, use_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
async def create_trial_key(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
try:
client_id = str(uuid.uuid4())
email = generate_random_email()
async def create_trial_key(tg_id: int, session: Any):
client_id = str(uuid.uuid4())
email = generate_random_email()
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
instructions = INSTRUCTIONS
result = {"key": public_link, "instructions": instructions}
current_time = datetime.utcnow()
expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
instructions = INSTRUCTIONS
least_loaded_cluster = await get_least_loaded_cluster()
for server_id, server in CLUSTERS[least_loaded_cluster].items():
xui = AsyncApi(
CLUSTERS[least_loaded_cluster][server_id]["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
result = {"key": public_link, "instructions": instructions}
asyncio.create_task(generate_and_store_keys(tg_id, client_id, email, public_link))
return result
finally:
await conn.close()
async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public_link: str):
conn = await asyncpg.connect(DATABASE_URL)
try:
current_time = datetime.utcnow()
expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
least_loaded_cluster = await get_least_loaded_cluster()
tasks = []
for server_id, server in CLUSTERS[least_loaded_cluster].items():
task = create_key_on_server(
least_loaded_cluster,
server_id,
client_id,
email,
tg_id,
expiry_timestamp,
)
tasks.append(task)
await asyncio.gather(*tasks)
await store_key(
tg_id,
await add_client(
xui,
client_id,
email,
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
)
await conn.execute(
"""
INSERT INTO connections (tg_id, trial)
VALUES ($1, 1)
ON CONFLICT (tg_id)
DO UPDATE SET trial = 1
""",
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
)
finally:
await conn.close()
async def create_key_on_server(
cluster_id: str,
server_id: str,
client_id: str,
email: str,
tg_id: int,
expiry_timestamp: int,
):
"""Создает ключ на сервере в указанном кластере и возвращает результат."""
xui = AsyncApi(
CLUSTERS[cluster_id][server_id]["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
response = await add_client(
xui,
await store_key(
tg_id,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
)
return response
await use_trial(tg_id, session)
return result
+91 -46
View File
@@ -2,12 +2,20 @@ import asyncio
from datetime import datetime, timedelta
from aiogram import Bot, Router, types
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from py3xui import AsyncApi
from client import delete_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB
from database import delete_key, get_balance, update_balance, update_key_expiry
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME
from database import (
add_notification,
check_notification_time,
delete_key,
get_balance,
update_balance,
update_key_expiry,
)
from handlers.keys.key_utils import renew_key_in_cluster
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS
from logger import logger
@@ -27,6 +35,8 @@ async def notify_expiring_keys(bot: Bot):
logger.info("Начало обработки уведомлений.")
await notify_inactive_trial_users(bot, conn)
await asyncio.sleep(1)
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(1)
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
@@ -95,28 +105,12 @@ async def notify_10h_keys(
if not await is_bot_blocked(bot, tg_id):
try:
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
],
[
types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="view_profile",
)
],
]
)
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{record["client_id"]}')
keyboard.button(text="💳 Пополнить баланс", callback_data="pay")
keyboard.button(text="👤 Личный кабинет", callback_data="profile")
keyboard.adjust(1)
keyboard = keyboard.as_markup()
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
except Exception as e:
@@ -175,28 +169,26 @@ async def notify_24h_keys(
if not await is_bot_blocked(bot, tg_id):
try:
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
],
[
types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="view_profile",
)
],
]
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
)
builder.row(
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="profile",
)
)
keyboard = builder.as_markup()
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except Exception as e:
@@ -212,6 +204,59 @@ async def notify_24h_keys(
await asyncio.sleep(1)
async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
logger.info("Проверка пользователей, не активировавших пробный период...")
inactive_trial_users = await conn.fetch(
"""
SELECT tg_id, username FROM users
WHERE tg_id IN (
SELECT tg_id FROM connections
WHERE trial = 0
) AND tg_id NOT IN (
SELECT DISTINCT tg_id FROM keys
)
"""
)
logger.info(f"Найдено {len(inactive_trial_users)} неактивных пользователей.")
for user in inactive_trial_users:
tg_id = user['tg_id']
username = user.get('username', 'Пользователь')
try:
# Проверяем, можно ли отправить уведомление
can_notify = await check_notification_time(
tg_id, 'inactive_trial', hours=24, session=conn # Уведомление не чаще, чем раз в 24 часа
)
if can_notify and not await is_bot_blocked(bot, tg_id):
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key")
)
builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
keyboard = builder.as_markup()
message = (
f"👋 Привет, {username}!\n\n"
f"🎉 У тебя есть бесплатный пробный период на {TRIAL_TIME} дней!\n"
"🕒 Не упусти возможность попробовать наш VPN прямо сейчас.\n\n"
"💡 Нажми на кнопку ниже, чтобы активировать пробный доступ."
)
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.")
# Добавляем запись о notification
await add_notification(tg_id, 'inactive_trial', session=conn)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}")
await asyncio.sleep(1) # Небольшая задержка между отправками
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
logger.info("Проверка истекших ключей...")
@@ -245,7 +290,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
"💡 Не откладывайте подключение VPN!"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")]]
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
try:
+1 -15
View File
@@ -3,15 +3,12 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from database import get_trial
from handlers.start import send_welcome_message
router = Router()
@router.callback_query(F.data == "pay")
async def handle_pay(callback_query: CallbackQuery):
await callback_query.message.delete()
builder = InlineKeyboardBuilder()
if YOOKASSA_ENABLE:
@@ -51,23 +48,12 @@ async def handle_pay(callback_query: CallbackQuery):
)
builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon"))
builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
"💸 <b>Выберите удобный способ пополнения баланса:</b>\n\n"
"• Быстро и безопасно\n"
"• Поддержка разных платежных систем\n"
"• Моментальное зачисление средств 🚀",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery, admin: bool = False):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status, admin)
await callback_query.answer()
+14 -83
View File
@@ -1,3 +1,5 @@
from typing import Any
from aiocryptopay import AioCryptoPay, Networks
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -6,9 +8,9 @@ from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from bot import bot
from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT
from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
@@ -24,31 +26,9 @@ class ReplenishBalanceState(StatesGroup):
entering_custom_amount_crypto = State()
async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(chat_id=chat_id, message_id=previous_message_id)
sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_cryptobot")
async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
@@ -74,28 +54,19 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st
callback_data="enter_custom_amount_crypto",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
exists = await check_connection_exists(callback_query.message.chat.id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0, session=session)
await callback_query.message.answer(
"Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_crypto)
await callback_query.answer()
@router.callback_query(F.data.startswith("crypto_amount|"))
@@ -103,11 +74,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
data = callback_query.data.split("|", 1)
if len(data) != 2:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Неверные данные для выбора суммы.")
return
@@ -115,11 +81,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
try:
amount = int(amount_str)
except ValueError:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Некорректная сумма.")
return
@@ -127,24 +88,18 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_crypto)
try:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(int(amount // RUB_TO_USDT)),
description=f"Пополнения баланса на {amount} руб",
payload=f"{callback_query.from_user.id}:{int(amount)}",
payload=f"{callback_query.message.chat.id}:{int(amount)}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
await bot.send_message(
chat_id=callback_query.from_user.id,
await callback_query.message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=builder.as_markup(),
)
@@ -152,22 +107,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
await callback_query.message.answer("Ошибка при создании платежа.")
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile"))
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def cryptobot_webhook(request):
@@ -204,9 +143,8 @@ async def process_crypto_payment(payload):
@router.callback_query(F.data == "enter_custom_amount_crypto")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await callback_query.message.answer(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_crypto)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_crypto)
@@ -224,7 +162,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext)
asset="USDT",
amount=str(int(amount // RUB_TO_USDT)),
description=f"Пополнения баланса на {amount} руб",
payload=f"{message.from_user.id}:{amount}",
payload=f"{message.chat.id}:{amount}",
)
if hasattr(invoice, "bot_invoice_url"):
@@ -233,18 +171,11 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await message.message.edit_text(
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=builder.as_markup(),
)
else:
await send_message_with_deletion(
message.from_user.id,
"Ошибка при создании платежа.",
state=state,
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
+10 -31
View File
@@ -12,9 +12,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
import requests
from bot import bot
from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID
from database import add_payment, update_balance
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
router = Router()
@@ -65,16 +65,6 @@ async def create_payment(user_id, amount, email, ip):
return None
async def send_payment_success_notification(user_id, amount):
try:
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def freekassa_webhook(request):
data = await request.json()
logging.debug(f"Получен вебхук от FreeKassa: {data}")
@@ -94,8 +84,6 @@ async def freekassa_webhook(request):
@router.callback_query(lambda c: c.data == "pay_freekassa")
async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
@@ -124,16 +112,12 @@ async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, st
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.send_message(
chat_id=tg_id,
await callback_query.message.answer(
text="Выберите сумму пополнения через FreeKassa:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_freekassa)
await callback_query.answer()
@router.callback_query(F.data.startswith("freekassa_amount|"))
@@ -143,12 +127,12 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
try:
amount = int(amount_str)
except ValueError:
await bot.send_message(callback_query.from_user.id, "Некорректная сумма.")
await callback_query.message.answer("Некорректная сумма.")
return
user_email = f"{callback_query.from_user.id}@solo.net"
user_email = f"{callback_query.message.chat.id}@solo.net"
user_ip = callback_query.message.chat.id
payment_url = await create_payment(callback_query.from_user.id, amount, user_email, user_ip)
payment_url = await create_payment(callback_query.message.chat.id, amount, user_email, user_ip)
if payment_url:
confirm_keyboard = InlineKeyboardMarkup(
@@ -158,25 +142,20 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
]
)
await bot.send_message(
callback_query.from_user.id,
await callback_query.message.answer(
f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:",
reply_markup=confirm_keyboard,
)
else:
await bot.send_message(
callback_query.from_user.id,
await callback_query.message.answer(
"Ошибка при создании платежа. Попробуйте позже.",
)
await callback_query.answer()
@router.callback_query(F.data == "enter_custom_amount_freekassa")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await callback_query.message.answer(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_freekassa)
@@ -187,9 +166,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext)
await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:")
return
user_email = f"{message.from_user.id}@solo.net"
user_email = f"{message.chat.id}@solo.net"
user_ip = message.chat.id
payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip)
payment_url = await create_payment(message.chat.id, amount, user_email, user_ip)
if payment_url:
keyboard = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]])
+16 -68
View File
@@ -1,4 +1,5 @@
import hashlib
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
@@ -8,9 +9,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from robokassa import HashAlgorithm, Robokassa
from bot import bot
from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE
from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
@@ -47,30 +48,9 @@ def generate_payment_link(amount, inv_id, description, tg_id):
return payment_link
async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
logger.debug(f"Deleting previous message with ID: {previous_message_id}")
await bot.delete_message(chat_id=chat_id, message_id=previous_message_id)
sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
await state.update_data({message_key: sent_message.message_id})
logger.debug(f"Sent new message with ID: {sent_message.message_id}")
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Robokassa payment.")
builder = InlineKeyboardBuilder()
@@ -106,23 +86,15 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
await add_connection(tg_id, balance=0.0, trial=0, session=session)
logger.info(f"Created new connection for user {tg_id} with balance 0.0.")
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
logger.debug(f"Deleted message with ID: {callback_query.message.message_id}")
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
await callback_query.message.answer(
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa)
logger.info(f"Displayed amount selection for user {tg_id}.")
await callback_query.answer()
@router.callback_query(F.data.startswith("robokassa_amount|"))
@@ -132,12 +104,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
data = callback_query.data.split("|")
if len(data) != 3 or data[1] != "amount":
logger.error("Ошибка: callback_data не соответствует формату.")
await send_message_with_deletion(
chat_id=callback_query.from_user.id,
text="Неверные данные для выбора суммы.",
state=state,
)
await callback_query.answer("Ошибка: данные повреждены.")
await callback_query.message.answer("Ошибка: данные повреждены.")
return
amount_str = data[2]
@@ -147,22 +114,17 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
raise ValueError("Сумма должна быть положительным числом.")
except ValueError as e:
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
await send_message_with_deletion(
chat_id=callback_query.from_user.id,
text="Некорректная сумма. Попробуйте снова.",
state=state,
)
await callback_query.answer("Некорректная сумма.")
await callback_query.message.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.")
logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.")
inv_id = 0
tg_id = callback_query.from_user.id
tg_id = callback_query.message.chat.id
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}")
logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}")
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
@@ -171,12 +133,11 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
]
)
await callback_query.message.edit_text(
await callback_query.message.answer(
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
)
logger.info(f"Payment link sent to user {callback_query.from_user.id}.")
await callback_query.answer()
logger.info(f"Payment link sent to user {callback_query.message.chat.id}.")
async def robokassa_webhook(request):
@@ -238,31 +199,18 @@ def check_payment_signature(params):
return signature_value.upper() == expected_signature.upper()
async def send_payment_success_notification(user_id: int, amount: float):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile"))
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
logger.info(f"Sent payment success notification to user {user_id}.")
@router.callback_query(F.data == "enter_custom_amount_robokassa")
async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} chose to enter a custom amount.")
await callback_query.message.edit_text(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):")
await callback_query.message.answer(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):")
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
tg_id = message.from_user.id
tg_id = message.chat.id
logger.info(f"User {tg_id} entered custom amount: {message.text}")
inv_id = 0
+9 -44
View File
@@ -1,12 +1,14 @@
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import RUB_TO_XTR
from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
@@ -19,28 +21,9 @@ class ReplenishBalanceState(StatesGroup):
entering_custom_amount_stars = State()
async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(chat_id=chat_id, message_id=previous_message_id)
sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_stars")
async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"))
@@ -77,21 +60,19 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state:
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
await add_connection(tg_id, balance=0.0, trial=0, session=session)
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
await callback_query.message.answer(
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_stars)
await callback_query.answer()
@router.callback_query(F.data.startswith("stars_amount|"))
@@ -146,27 +127,11 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile"))
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
@router.callback_query(F.data == "enter_custom_amount_stars")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await callback_query.message.answer(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_stars)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_stars)
@@ -213,7 +178,7 @@ async def on_successful_payment(
message: types.Message,
):
try:
user_id = int(message.from_user.id)
user_id = int(message.chat.id)
amount = float(message.successful_payment.invoice_payload.split("_")[0])
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await add_payment(int(user_id), float(amount), "stars")
+18
View File
@@ -0,0 +1,18 @@
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from logger import logger
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
+13 -71
View File
@@ -1,3 +1,4 @@
from typing import Any
import uuid
from aiogram import F, Router, types
@@ -8,9 +9,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from yookassa import Configuration, Payment
from bot import bot
from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID
from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
@@ -29,28 +30,9 @@ class ReplenishBalanceState(StatesGroup):
entering_custom_amount_yookassa = State()
async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key="last_message_id"):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(chat_id=chat_id, message_id=previous_message_id)
sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_yookassa")
async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
builder = InlineKeyboardBuilder()
@@ -79,28 +61,21 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta
callback_data="enter_custom_amount_yookassa",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
await add_connection(tg_id, balance=0.0, trial=0, session=session)
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
await callback_query.message.answer(
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_yookassa)
await callback_query.answer()
@router.callback_query(F.data.startswith("yookassa_amount|"))
@@ -108,24 +83,12 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
data = callback_query.data.split("|", 1)
if len(data) != 2:
await send_message_with_deletion(
callback_query.from_user.id,
"Неверные данные для выбора суммы.",
state=state,
message_key="amount_error_message_id",
)
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await send_message_with_deletion(
callback_query.from_user.id,
"Некорректная сумма.",
state=state,
message_key="amount_error_message_id",
)
return
await state.update_data(amount=amount)
@@ -133,7 +96,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
# state_data = await state.get_data()
customer_name = callback_query.from_user.full_name
customer_id = callback_query.from_user.id
customer_id = callback_query.message.chat.id
customer_email = f"{customer_id}@solo.net"
@@ -176,32 +139,12 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
]
)
await callback_query.message.edit_text(
await callback_query.message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard,
)
else:
await send_message_with_deletion(
callback_query.from_user.id,
"Ошибка при создании платежа.",
state=state,
)
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile"))
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
await callback_query.message.answer("Ошибка при создании платежа.")
async def yookassa_webhook(request):
@@ -225,9 +168,8 @@ async def yookassa_webhook(request):
@router.callback_query(F.data == "enter_custom_amount_yookassa")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await callback_query.message.answer(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount_yookassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_yookassa)
@@ -254,7 +196,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext)
"receipt": {
"customer": {
"full_name": message.from_user.full_name,
"email": f"{message.from_user.id}@solo.net",
"email": f"{message.chat.id}@solo.net",
"phone": "79000000000",
},
"items": [
@@ -269,7 +211,7 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext)
}
],
},
"metadata": {"user_id": message.from_user.id},
"metadata": {"user_id": message.chat.id},
},
uuid.uuid4(),
)
+49 -100
View File
@@ -5,91 +5,68 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CHANNEL_URL
from database import get_balance, get_key_count, get_referral_stats
from handlers.texts import RENEWAL_PLANS, get_referral_link, invite_message_send, profile_message_send
from logger import logger
router = Router()
@router.callback_query(F.data == "profile")
async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext, admin: bool):
chat_id = callback_query.from_user.id
chat_id = callback_query.message.chat.id
username = callback_query.from_user.full_name
image_path = os.path.join("img", "pic.jpg")
key_count = await get_key_count(chat_id)
balance = await get_balance(chat_id)
if balance is None:
balance = 0
try:
key_count = await get_key_count(chat_id)
balance = await get_balance(chat_id)
if balance is None:
balance = 0
profile_message = profile_message_send(username, chat_id, balance, key_count)
profile_message = profile_message_send(username, chat_id, balance, key_count)
if key_count == 0:
profile_message += "\n🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i>"
if key_count == 0:
profile_message += "\n🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i>"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL))
builder.row(InlineKeyboardButton(text="💡 Тарифы", callback_data="view_tariffs"))
builder.row(
InlineKeyboardButton(text=" Устройство", callback_data="create_key"),
InlineKeyboardButton(text="📱 Мои устройства", callback_data="view_keys"),
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL))
builder.row(InlineKeyboardButton(text="💡 Тарифы", callback_data="view_tariffs"))
builder.row(
InlineKeyboardButton(text=" Устройство", callback_data="create_key"),
InlineKeyboardButton(text="📱 Мои устройства", callback_data="view_keys"),
)
builder.row(
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
builder.row(
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
InlineKeyboardButton(text="👥 Пригласить друзей", callback_data="invite"),
InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"),
)
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="⬅️ Главное меню", callback_data="back_to_menu"))
)
builder.row(
InlineKeyboardButton(text="👥 Пригласить друзей", callback_data="invite"),
InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"),
)
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="⬅️ Главное меню", callback_data="start"))
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"❗ Ошибка при удалении сообщения: {e}")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=profile_message,
parse_mode="HTML",
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(
chat_id,
f"❗️ Не удалось загрузить профиль. Техническая ошибка: {e}",
else:
await callback_query.message.answer(
text=profile_message,
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "view_tariffs")
async def view_tariffs_handler(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="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
"<b>🚀 Доступные тарифы VPN:</b>\n\n"
@@ -101,15 +78,13 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery):
for months in sorted(RENEWAL_PLANS.keys(), key=int)
]
),
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "invite")
async def invite_handler(callback_query: types.CallbackQuery):
chat_id = callback_query.from_user.id
chat_id = callback_query.message.chat.id
referral_link = get_referral_link(chat_id)
referral_stats = await get_referral_stats(chat_id)
@@ -119,42 +94,16 @@ async def invite_handler(callback_query: types.CallbackQuery):
image_path = os.path.join("img", "pic_invite.jpg")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile"))
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
try:
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"),
caption=invite_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=invite_message,
parse_mode="HTML",
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"),
caption=invite_message,
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(
chat_id=chat_id,
text=f"❗️ Не удалось отправить сообщение. Техническая ошибка: {e}",
parse_mode="HTML",
else:
await callback_query.message.answer(
text=invite_message,
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "view_profile")
async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext, admin: bool = False):
await state.clear()
await process_callback_view_profile(callback_query, state, admin)
+42 -99
View File
@@ -1,134 +1,86 @@
import os
from typing import Any
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from bot import bot
from config import (
CHANNEL_URL,
CONNECT_ANDROID,
CONNECT_IOS,
DATABASE_URL,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
)
from database import add_connection, add_referral, check_connection_exists, get_trial
from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL
from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial
from handlers.keys.trial_key import create_trial_key
from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn
from logger import logger
router = Router()
async def send_welcome_message(chat_id: int, trial_status: int, admin: bool):
@router.callback_query(F.data == "start")
async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool):
await start_command(callback_query.message, state, session, admin)
@router.message(Command("start"))
async def start_command(message: Message, state: FSMContext, session: Any, admin: bool):
if message.text:
try:
referrer_tg_id = int(message.text.split("referral_")[1])
await add_referral(message.chat.id, referrer_tg_id, session)
except (ValueError, IndexError):
pass
connection_exists = await check_connection_exists(message.chat.id)
if not connection_exists:
await add_connection(tg_id=message.chat.id, session=session)
trial_status = await get_trial(message.chat.id, session)
image_path = os.path.join("img", "pic.jpg")
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile"))
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
)
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
await bot.send_photo(
chat_id=chat_id,
await message.answer_photo(
photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
caption=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
await message.answer(
text=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
async def start_command(message: Message, admin: bool = False):
try:
logger.info(f"Получена команда /start. Текст сообщения: {message.text}, user_id: {message.from_user.id}")
if "referral_" in message.text:
logger.info("Обнаружен реферальный код.")
try:
referrer_tg_id = int(message.text.split("referral_")[1])
logger.info(f"ID пригласившего пользователя: {referrer_tg_id}")
except ValueError:
logger.error("Ошибка парсинга реферального ID.")
return
connection_exists = await check_connection_exists(message.from_user.id)
logger.info(f"Результат проверки подключения для user_id {message.from_user.id}: {connection_exists}")
if not connection_exists:
logger.info(f"Добавляем подключение для пользователя: {message.from_user.id}")
await add_connection(message.from_user.id)
logger.info(f"Добавляем реферал для пользователя {message.from_user.id}, приглашённым {referrer_tg_id}")
await add_referral(message.from_user.id, referrer_tg_id)
else:
logger.info(f"Пользователь {message.from_user.id} уже зарегистрирован.")
logger.info(f"Проверяем статус пробного периода для user_id {message.from_user.id}")
trial_status = await get_trial(message.from_user.id)
logger.info(f"Статус пробного периода для user_id {message.from_user.id}: {trial_status}")
logger.info(f"Отправка приветственного сообщения для user_id {message.from_user.id}")
await send_welcome_message(message.chat.id, trial_status, admin)
except Exception as e:
logger.error(f"Ошибка в обработке команды /start для user_id {message.from_user.id}: {e}")
await message.answer("Произошла ошибка. Пожалуйста, попробуйте позже.")
@router.callback_query(F.data == "connect_vpn")
async def handle_connect_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
user_id = callback_query.from_user.id
async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
user_id = callback_query.message.chat.id
trial_key_info = await create_trial_key(user_id)
trial_key_info = await create_trial_key(user_id, session)
if "error" in trial_key_info:
await callback_query.message.answer(trial_key_info["error"])
else:
try:
conn = await asyncpg.connect(DATABASE_URL)
result = await conn.execute(
"""
UPDATE connections SET trial = 1 WHERE tg_id = $1
""",
user_id,
)
logger.info(f"Rows updated: {result}")
await conn.close()
except Exception as e:
logger.error(f"Ошибка при обновлении trial: {e}")
await callback_query.message.answer("Произошла ошибка при обновлении статуса.")
await use_trial(user_id, session)
key_message = (
f"🔑 <b>Ваш персональный ключ доступа:</b>\n"
f"<pre>{trial_key_info['key']}</pre>\n\n"
f"<code>{trial_key_info['key']}</code>\n\n"
f"📋 <b>Быстрая инструкция по подключению:</b>\n{INSTRUCTIONS_TRIAL}"
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS),
InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID),
@@ -144,28 +96,19 @@ async def handle_connect_vpn(callback_query: CallbackQuery):
),
)
await callback_query.message.answer(key_message, parse_mode="HTML", reply_markup=builder.as_markup())
await callback_query.answer()
await callback_query.message.answer(key_message, reply_markup=builder.as_markup())
@router.callback_query(F.data == "about_vpn")
async def handle_about_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
about_vpn_message = get_about_vpn("3.1.1_Stable")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start"))
await callback_query.message.answer(about_vpn_message, parse_mode="HTML", reply_markup=builder.as_markup())
await callback_query.answer()
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery, admin: bool = False):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status, admin)
await callback_query.answer()
await callback_query.message.answer(get_about_vpn("3.1.1_Stable"), reply_markup=builder.as_markup())
+10 -5
View File
@@ -55,23 +55,25 @@ async def on_shutdown(app):
try:
await asyncio.gather(*asyncio.all_tasks(), return_exceptions=True)
except Exception as e:
logger.error(f"Error during shutdown: {e}")
logger.error(f"Ошибка при завершении работы: {e}")
async def shutdown_site(site):
logger.info("Остановка сайта...")
logger.info("Остановка сайт...")
await site.stop()
logger.info("Сервер остановлен.")
logger.info("Сервер сайт.")
async def main():
dp.include_router(router)
if DEV_MODE:
logger.info("Запуск в режиме разработки...")
await bot.delete_webhook()
await init_db()
await dp.start_polling(bot)
else:
logger.info("Запуск в production режиме...")
app = web.Application()
app.on_startup.append(on_startup)
app.on_shutdown.append(on_shutdown)
@@ -96,7 +98,7 @@ async def main():
site = web.TCPSite(runner, host=WEBAPP_HOST, port=WEBAPP_PORT)
await site.start()
logger.info(f"Webhook URL: {WEBHOOK_URL}")
logger.info(f"URL вебхука: {WEBHOOK_URL}")
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
@@ -107,7 +109,10 @@ async def main():
finally:
pending = asyncio.all_tasks()
for task in pending:
task.cancel()
try:
task.cancel()
except Exception as e:
logger.error(e)
await asyncio.gather(*pending, return_exceptions=True)
+1 -4
View File
@@ -4,7 +4,6 @@ from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
from logger import logger
class AdminMiddleware(BaseMiddleware):
@@ -20,10 +19,8 @@ class AdminMiddleware(BaseMiddleware):
def _check_admin_access(self, event: TelegramObject) -> bool:
try:
admin_ids: Union[int, list[int]] = ADMIN_ID
if isinstance(admin_ids, list):
return event.from_user.id in admin_ids
return event.from_user.id == admin_ids
except Exception as e:
logger.error(f"Ошибка проверки администратора: {e}")
except Exception:
return False
+6 -7
View File
@@ -14,10 +14,9 @@ class DatabaseMiddleware(BaseMiddleware):
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
async with await asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as session:
data["session"] = session
try:
return await handler(event, data)
finally:
await pool.release(session)
conn = await asyncpg.connect(DATABASE_URL)
try:
data["session"] = conn
return await handler(event, data)
finally:
await conn.close()
+24
View File
@@ -0,0 +1,24 @@
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
class DeleteMessageMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
if isinstance(event, (Message, CallbackQuery)):
if isinstance(event, Message):
try:
await event.bot.delete_message(event.chat.id, event.message_id - 1)
except Exception:
pass
await event.delete()
elif isinstance(event, CallbackQuery):
await event.answer()
await event.message.delete()
return await handler(event, data)