diff --git a/.gitignore b/.gitignore index 6710dc9d..101388c8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ /database.db /backup_pg.sh /config copy.py -/docker-compose.yml \ No newline at end of file +/docker-compose.yml +__pycache__ diff --git a/bot.py b/bot.py index 2c685583..7dc9c5cf 100644 --- a/bot.py +++ b/bot.py @@ -8,8 +8,10 @@ storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) router = Router() -from handlers import (backup_handler, key_management, keys, notifications, pay, - profile, start, admin, commands) +from handlers.admin import admin +from handlers.keys import key_management, keys +from handlers import (notifications, pay, + profile, start, commands) dp.include_router(commands.router) dp.include_router(start.router) @@ -18,5 +20,4 @@ dp.include_router(keys.router) dp.include_router(key_management.router) dp.include_router(pay.router) dp.include_router(notifications.router) -dp.include_router(backup_handler.router) dp.include_router(admin.router) \ No newline at end of file diff --git a/handlers/__pycache__/commands.cpython-310.pyc b/handlers/__pycache__/commands.cpython-310.pyc index f62c7e61..a093d143 100644 Binary files a/handlers/__pycache__/commands.cpython-310.pyc and b/handlers/__pycache__/commands.cpython-310.pyc differ diff --git a/handlers/__pycache__/pay.cpython-310.pyc b/handlers/__pycache__/pay.cpython-310.pyc index 7a539eca..e1e3da10 100644 Binary files a/handlers/__pycache__/pay.cpython-310.pyc and b/handlers/__pycache__/pay.cpython-310.pyc differ diff --git a/handlers/__pycache__/profile.cpython-310.pyc b/handlers/__pycache__/profile.cpython-310.pyc index cb15fcb8..6ead0c42 100644 Binary files a/handlers/__pycache__/profile.cpython-310.pyc and b/handlers/__pycache__/profile.cpython-310.pyc differ diff --git a/handlers/__pycache__/start.cpython-310.pyc b/handlers/__pycache__/start.cpython-310.pyc index 28aca1a2..4db96f8b 100644 Binary files a/handlers/__pycache__/start.cpython-310.pyc and b/handlers/__pycache__/start.cpython-310.pyc differ diff --git a/handlers/__pycache__/texts.cpython-310.pyc b/handlers/__pycache__/texts.cpython-310.pyc index a93955dd..aa3a631d 100644 Binary files a/handlers/__pycache__/texts.cpython-310.pyc and b/handlers/__pycache__/texts.cpython-310.pyc differ diff --git a/handlers/admin.py b/handlers/admin/admin.py similarity index 93% rename from handlers/admin.py rename to handlers/admin/admin.py index 4bfafe10..86bfa3f1 100644 --- a/handlers/admin.py +++ b/handlers/admin/admin.py @@ -1,6 +1,6 @@ from aiogram import Router, types from aiogram.filters import Command -from database import add_balance_to_client, get_balance, check_connection_exists # Импорт необходимых функций +from database import add_balance_to_client, get_balance, check_connection_exists from config import ADMIN_ID router = Router() diff --git a/handlers/backup_handler.py b/handlers/backup_handler.py deleted file mode 100644 index 0dcbe436..00000000 --- a/handlers/backup_handler.py +++ /dev/null @@ -1,17 +0,0 @@ -from aiogram import Router -from aiogram.filters import Command -from aiogram.types import Message -from config import ADMIN_ID - -router = Router() - -@router.message(Command('backup')) -async def backup_command(message: Message): - if message.from_user.id != ADMIN_ID: - await message.answer("У вас нет прав для выполнения этой команды.") - return - - from backup import backup_database - await message.answer("Запускаю бэкап базы данных...") - await backup_database() - await message.answer("Бэкап завершен и отправлен админу.") \ No newline at end of file diff --git a/handlers/commands.py b/handlers/commands.py index 603febe9..8473d65e 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -6,13 +6,13 @@ import asyncpg from bot import bot from config import ADMIN_ID, DATABASE_URL -from handlers.backup_handler import backup_command from handlers.pay import ReplenishBalanceState, process_custom_amount_input from handlers.profile import process_callback_view_profile from handlers.start import start_command from handlers.texts import TRIAL -from handlers.admin import cmd_add_balance -from handlers.key_management import handle_key_name_input +from handlers.admin.admin import cmd_add_balance +from handlers.keys.key_management import handle_key_name_input +from aiogram.types import Message router = Router() @@ -22,6 +22,17 @@ class Form(StatesGroup): viewing_profile = State() waiting_for_message = State() +@router.message(Command('backup')) +async def backup_command(message: Message): + if message.from_user.id != ADMIN_ID: + await message.answer("У вас нет прав для выполнения этой команды.") + return + + from backup import backup_database + await message.answer("Запускаю бэкап базы данных...") + await backup_database() + await message.answer("Бэкап завершен и отправлен админу.") + @router.message(Command('start')) async def handle_start(message: types.Message, state: FSMContext): await start_command(message) diff --git a/handlers/instructions.jpg b/handlers/instructions/instructions.jpg similarity index 100% rename from handlers/instructions.jpg rename to handlers/instructions/instructions.jpg diff --git a/handlers/instructions.py b/handlers/instructions/instructions.py similarity index 100% rename from handlers/instructions.py rename to handlers/instructions/instructions.py diff --git a/handlers/key_management.py b/handlers/keys/key_management.py similarity index 96% rename from handlers/key_management.py rename to handlers/keys/key_management.py index 86843e95..1badcba6 100644 --- a/handlers/key_management.py +++ b/handlers/keys/key_management.py @@ -1,6 +1,7 @@ import uuid from datetime import datetime, timedelta +from bot import dp import asyncpg from aiogram import F, Router from aiogram.fsm.context import FSMContext @@ -9,12 +10,11 @@ from aiogram.types import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message) from auth import link, login_with_credentials -from bot import bot, dp from client import add_client from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS) from database import add_connection, get_balance, store_key, update_balance -from handlers.instructions import send_instructions +from handlers.instructions.instructions import send_instructions from handlers.profile import process_callback_view_profile from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, key_message_success from handlers.utils import sanitize_key_name @@ -121,7 +121,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): key_name = sanitize_key_name(message.text) if not key_name: - await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите профиль на английском языке.") + await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите ключ устройства на английском языке.") return data = await state.get_data() @@ -164,7 +164,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): if not response.get("success", True): error_msg = response.get("msg", "Неизвестная ошибка.") if "Duplicate email" in error_msg: - await message.bot.send_message(tg_id, "❌ Этот email уже используется. Пожалуйста, выберите другое имя для ключа.") + await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.") await state.set_state(Form.waiting_for_key_name) return else: diff --git a/handlers/keys.py b/handlers/keys/keys.py similarity index 90% rename from handlers/keys.py rename to handlers/keys/keys.py index 275175e6..a7030cfb 100644 --- a/handlers/keys.py +++ b/handlers/keys/keys.py @@ -11,6 +11,7 @@ from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS from database import get_balance, update_balance from handlers.texts import NO_KEYS from handlers.texts import key_message, key_relocated +from handlers.texts import RENEWAL_PLANS, INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, SUCCESS_RENEWAL_MSG, ERROR_RENEWAL_MSG, PLAN_SELECTION_MSG locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8') @@ -159,17 +160,15 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): expiry_time = record['expiry_time'] current_time = datetime.utcnow().timestamp() * 1000 keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='📅 1 месяц (100 руб.)', callback_data=f'renew_plan|1|{client_id}')], - [types.InlineKeyboardButton(text='📅 3 месяца (285 руб.)', callback_data=f'renew_plan|3|{client_id}')], - [types.InlineKeyboardButton(text='📅 6 месяцев (540 руб.)', callback_data=f'renew_plan|6|{client_id}')], - [types.InlineKeyboardButton(text='📅 12 месяцев (1000 руб.)', callback_data=f'renew_plan|12|{client_id}')], + [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 = (f"Выберите план продления:\n\n" - f"💰 Баланс: {balance} руб.\n\n" - f"📅 Текущая дата истечения ключа: {datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}") + response_message = PLAN_SELECTION_MSG.format(balance=balance, expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')) await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") @@ -181,6 +180,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): await callback_query.answer() + @router.callback_query(lambda c: c.data.startswith('confirm_delete|')) async def process_callback_confirm_delete(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id @@ -222,7 +222,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): @router.callback_query(lambda c: c.data.startswith('renew_plan|')) async def process_callback_renew_plan(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - plan, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2] + plan, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2] days_to_extend = 30 * int(plan) try: @@ -241,14 +241,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): else: new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) - if plan == '1': - cost = 100 - elif plan == '3': - cost = 285 - elif plan == '6': - cost = 540 - elif plan == '12': - cost = 1000 + cost = RENEWAL_PLANS[plan]['price'] balance = await get_balance(tg_id) if balance < cost: @@ -256,7 +249,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile') keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [back_button]]) - await bot.edit_message_text("Недостаточно средств для продления ключа.", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) + await bot.edit_message_text(INSUFFICIENT_FUNDS_MSG, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) return session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) @@ -265,14 +258,14 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): if success: await update_balance(tg_id, -cost) await conn.execute('UPDATE keys SET expiry_time = $1 WHERE client_id = $2', new_expiry_time, client_id) - response_message = f"Ваш ключ был успешно продлен на {days_to_extend // 30} месяц(-а)." + 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.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) else: - await bot.edit_message_text("Ошибка при продлении ключа.", chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.edit_message_text(ERROR_RENEWAL_MSG, chat_id=tg_id, message_id=callback_query.message.message_id) else: - await bot.edit_message_text("Ключ не найден.", chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.edit_message_text(KEY_NOT_FOUND_MSG, chat_id=tg_id, message_id=callback_query.message.message_id) finally: await conn.close() @@ -294,7 +287,7 @@ async def process_callback_change_location(callback_query: types.CallbackQuery): try: for server_id, server in SERVERS.items(): count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id) - percent_full = (count / 100) * 100 + percent_full = (count / 60) * 100 if count <= 60 else 100 server_name = f"{server['name']} ({percent_full:.1f}%)" server_buttons.append([types.InlineKeyboardButton(text=server_name, callback_data=f'select_server&{server_id}&{client_id}')]) finally: diff --git a/handlers/trial_key.py b/handlers/keys/trial_key.py similarity index 99% rename from handlers/trial_key.py rename to handlers/keys/trial_key.py index 1d57a599..111ea657 100644 --- a/handlers/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,4 +1,3 @@ -import random import asyncpg import uuid from config import DATABASE_URL, SERVERS, ADMIN_USERNAME, ADMIN_PASSWORD diff --git a/handlers/pay.py b/handlers/pay.py index 2dfefe25..8b81498f 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -13,6 +13,7 @@ from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID from database import (add_connection, check_connection_exists, get_key_count, update_balance) from handlers.profile import process_callback_view_profile +from handlers.texts import PAYMENT_OPTIONS router = Router() @@ -59,10 +60,20 @@ async def process_callback_replenish_balance(callback_query: types.CallbackQuery await add_connection(tg_id, balance=0.0, trial=0) amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='100 RUB', callback_data='amount_100'), InlineKeyboardButton(text='300 RUB', callback_data='amount_300')], - [InlineKeyboardButton(text='600 RUB', callback_data='amount_600'), InlineKeyboardButton(text='1000 RUB', callback_data='amount_1000')], - [InlineKeyboardButton(text='💰 Ввести свою сумму', callback_data='enter_custom_amount')], - [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] + [ + InlineKeyboardButton(text=PAYMENT_OPTIONS[0]['text'], callback_data=PAYMENT_OPTIONS[0]['callback_data']), + InlineKeyboardButton(text=PAYMENT_OPTIONS[1]['text'], callback_data=PAYMENT_OPTIONS[1]['callback_data']) + ], + [ + InlineKeyboardButton(text=PAYMENT_OPTIONS[2]['text'], callback_data=PAYMENT_OPTIONS[2]['callback_data']), + InlineKeyboardButton(text=PAYMENT_OPTIONS[3]['text'], callback_data=PAYMENT_OPTIONS[3]['callback_data']) + ], + [ + InlineKeyboardButton(text=PAYMENT_OPTIONS[4]['text'], callback_data=PAYMENT_OPTIONS[4]['callback_data']) + ], + [ + InlineKeyboardButton(text=PAYMENT_OPTIONS[5]['text'], callback_data=PAYMENT_OPTIONS[5]['callback_data']) + ] ]) await callback_query.message.edit_text( @@ -72,6 +83,7 @@ async def process_callback_replenish_balance(callback_query: types.CallbackQuery await state.set_state(ReplenishBalanceState.choosing_amount) await callback_query.answer() + @router.callback_query(lambda c: c.data == 'back_to_profile') async def back_to_profile_handler(callback_query: types.CallbackQuery, state: FSMContext): await process_callback_view_profile(callback_query, state) @@ -126,9 +138,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F "currency": "RUB" }, "vat_code": 6 - ## Раскоментируйте следующие строки, если у вас регистрация как ИП, а не самозанятость - ## "payment_subject": "payment", - ## "payment_mode": "full_payment", } ] }, @@ -260,7 +269,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) ) else: await message.answer("Ошибка при создании платежа.") + except Exception as e: - await message.answer(f"Произошла ошибка при обработке платежа: {str(e)}") + logging.error(f"Ошибка при создании платежа: {e}") + await message.answer("Произошла ошибка при создании платежа.") else: - await message.answer("Некорректный ввод. Пожалуйста, введите сумму числом:") \ No newline at end of file + await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:") diff --git a/handlers/profile.py b/handlers/profile.py index cdf736c3..7cee93a8 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -5,7 +5,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from bot import bot from database import get_balance, get_key_count, get_referral_stats -from handlers.texts import profile_message_send, invite_message_send +from handlers.texts import profile_message_send, invite_message_send, CHANNEL_LINK, get_referral_link class ReplenishBalanceState(StatesGroup): @@ -13,6 +13,7 @@ class ReplenishBalanceState(StatesGroup): waiting_for_admin_confirmation = State() router = Router() + async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id username = callback_query.from_user.full_name @@ -28,7 +29,7 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta ) profile_message += ( - f"Обязательно подпишитесь на канал здесь\n" + f"Обязательно подпишитесь на канал здесь\n" ) if key_count == 0: @@ -66,7 +67,7 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta @router.callback_query(lambda c: c.data == 'invite') async def invite_handler(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - referral_link = f"https://t.me/SoloNetVPN_bot?start=referral_{tg_id}" + referral_link = get_referral_link(tg_id) referral_stats = await get_referral_stats(tg_id) @@ -90,4 +91,4 @@ async def invite_handler(callback_query: types.CallbackQuery): @router.callback_query(lambda c: c.data == 'view_profile') async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext): - await process_callback_view_profile(callback_query, state) \ No newline at end of file + await process_callback_view_profile(callback_query, state) diff --git a/handlers/start.py b/handlers/start.py index 549822bf..d3d140d8 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -8,7 +8,7 @@ from handlers.texts import ABOUT_VPN, WELCOME_TEXT from bot import bot from config import CHANNEL_URL, SUPPORT_CHAT_URL from database import add_connection, add_referral, check_connection_exists, get_trial -from handlers.trial_key import create_trial_key +from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL router = Router() @@ -65,23 +65,19 @@ async def handle_connect_vpn(callback_query: CallbackQuery): await callback_query.message.delete() user_id = callback_query.from_user.id - # Создаём триальный ключ trial_key_info = await create_trial_key(user_id) if 'error' in trial_key_info: await callback_query.message.answer(trial_key_info['error']) else: - # Формируем сообщение с ключом и инструкциями key_message = ( f"Ваш ключ доступа:\n
{trial_key_info['key']}\n\n"
f"Инструкции:\n{INSTRUCTIONS_TRIAL}"
)
- # Кнопка "В профиль"
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
inline_keyboard_profile = InlineKeyboardMarkup(inline_keyboard=[[button_profile]])
- # Отправляем текст с ключом и инструкциями в виде цитаты
await callback_query.message.answer(
key_message,
parse_mode='HTML',
diff --git a/handlers/texts.py b/handlers/texts.py
index 173bda4f..c840fa45 100644
--- a/handlers/texts.py
+++ b/handlers/texts.py
@@ -1,5 +1,29 @@
-from config import BOT_VERSION
+BOT_VERSION = '1.3.2'
+### Функции образования цен и кнопок продления ключа
+PAYMENT_OPTIONS = [
+ {'text': '100 RUB', 'callback_data': 'amount_100'},
+ {'text': '300 RUB', 'callback_data': 'amount_300'},
+ {'text': '600 RUB', 'callback_data': 'amount_600'},
+ {'text': '1000 RUB', 'callback_data': 'amount_1000'},
+ {'text': '💰 Ввести свою сумму', 'callback_data': 'enter_custom_amount'},
+ {'text': '⬅️ Назад', 'callback_data': 'back_to_profile'},
+]
+
+RENEWAL_PLANS = {
+ '1': {'months': 1, 'price': 100},
+ '3': {'months': 3, 'price': 285},
+ '6': {'months': 6, 'price': 540},
+ '12': {'months': 12, 'price': 1000},
+}
+
+INSUFFICIENT_FUNDS_MSG = "Недостаточно средств для продления ключа."
+KEY_NOT_FOUND_MSG = "Ключ не найден."
+SUCCESS_RENEWAL_MSG = "Ваш ключ был успешно продлен на {months} месяц(-а)."
+ERROR_RENEWAL_MSG = "Ошибка при продлении ключа."
+PLAN_SELECTION_MSG = "Выберите план продления:\n\n💰 Баланс: {balance} руб.\n\n📅 Текущая дата истечения ключа: {expiry_date}"
+
+### Текст главного меню
WELCOME_TEXT = (
"🎉 SoloNet — твой доступ в свободный интернет! 🌐✨\n\n"
"Наши преимущества:\n"
@@ -80,7 +104,6 @@ INSTRUCTIONS_TRIAL = (
"💬 Если у вас возникнут вопросы, не стесняйтесь обращаться в поддержку."
)
-
KEY_EXPIRY_10H = "🔔 Уведомление: Ваш ключ {email} для сервера {server_id} истекает через 10 часов.\n" \
"Дата истечения: {expiry_date}"\
"Перейдите в профиль и пополните баланс, всего 100 рублей на целый месяц"\
@@ -96,6 +119,11 @@ KEY_RENEWAL_FAILED = "Не удалось продлить ключ на пан
KEY_DELETED = "Ваш ключ был удален из-за недостаточного баланса."
KEY_DELETION_FAILED = "Не удалось удалить ключ с панели, обратитесь в поддержку."
+CHANNEL_LINK = "https://t.me/solonet_vpn"
+
+def get_referral_link(user_id):
+ return f"https://t.me/SoloNetVPN_bot?start=referral_{user_id}"
+
def key_message_success(connection_link, remaining_time_message):
key_message = (
"✅ Ключ успешно создан:\n"