Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c61a8475c | |||
| 7b5cbb8629 | |||
| 1e813d506f | |||
| 43a95a7d5a | |||
| 787a1e13c0 | |||
| 6abeae35f8 | |||
| cb94158bd0 | |||
| cf0924959c | |||
| 2ad8627215 | |||
| c2429a0269 | |||
| fd35207f2f | |||
| a9e318136f | |||
| 5f46156907 | |||
| ee7928dda8 | |||
| b39634d4fa | |||
| 23cd7c9e60 | |||
| 9ba1cacdaf | |||
| cc5d47e546 | |||
| 4ba6ef8f0d | |||
| 9003dc5841 | |||
| ba2b2c63a4 | |||
| 4ada912f98 | |||
| 9a10859109 | |||
| 5131c82b75 | |||
| e590c08095 | |||
| e7dcd58314 | |||
| bebd051f47 | |||
| 56babedde6 | |||
| e9e4973dc9 | |||
| 8eb76ff9e4 | |||
| 69de84d430 | |||
| 52b41fe521 |
@@ -2,6 +2,12 @@
|
||||
|
||||
**SoloBot** — ваш идеальный помощник для управления API 3x-UI VPN на протоколе VLESS.
|
||||
|
||||
Две версии — море возможностей:
|
||||
- v1.4 — бот для продажи ключей vless
|
||||
- v2.0 — бот для продажи подписок vless (постоянно обновляется)
|
||||
|
||||
Если вам не хватает функций — направьте их в issue, мы реализуем
|
||||
|
||||
## 📋 Оглавление
|
||||
1. [Описание](#описание)
|
||||
2. [Стек технологий](#стек-технологий)
|
||||
@@ -28,7 +34,9 @@ SoloBot реализует множество функций, включая:
|
||||
- Поддержка нескольких ключей для одного клиента (несколько устройств).
|
||||
- **Реферальная программа** с пригласительной ссылкой.
|
||||
- Доступ к **инструкциям**.
|
||||
- **Пополнение баланса** через сервис Юкасса.
|
||||
- **Пополнение баланса**:
|
||||
* через Юкасса (самозанятость и ИП)
|
||||
* через freekassa (Физические Лица)
|
||||
- Периодические **бэкапы базы данных клиентов**.
|
||||
- Уведомления:
|
||||
- Произвольные сообщения через админку.
|
||||
@@ -37,7 +45,7 @@ SoloBot реализует множество функций, включая:
|
||||
- **Чат поддержки** и канал для связи.
|
||||
- **Автоматическое продление ключа** при наличии достаточного баланса.
|
||||
- **Удобная админка прямо в боте**
|
||||
- **мультисерверность** добавляй сервера в конфига, и они автоматически будут в боте
|
||||
- **мультисерверность** добавляй сервера в конфиг, и они автоматически будут в боте
|
||||
|
||||

|
||||
---
|
||||
@@ -113,7 +121,7 @@ WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
|
||||
SUPPORT_CHAT_URL = ваша ссылка на поддержку
|
||||
|
||||
```
|
||||
**Полная версия конфигурации и файл кастомизации доступны через поддержку нашего бота**
|
||||
**Мы высылаем детальный гайд и недостающие файлы в боте**
|
||||
|
||||
**Все описания в одном файле!** Удобно настроить бот под свой сервис изменив информацию и цены в одном месте
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ router = Router()
|
||||
|
||||
from handlers.admin import admin, admin_panel, user_editor
|
||||
from handlers.keys import key_management, keys
|
||||
from handlers import (notifications, pay,
|
||||
profile, start, commands)
|
||||
from handlers.payment import pay, freekassa
|
||||
from handlers import (notifications, profile, start, commands)
|
||||
|
||||
dp.include_router(admin.router)
|
||||
dp.include_router(admin_panel.router)
|
||||
@@ -22,4 +22,5 @@ dp.include_router(profile.router)
|
||||
dp.include_router(keys.router)
|
||||
dp.include_router(key_management.router)
|
||||
dp.include_router(pay.router)
|
||||
dp.include_router(freekassa.router)
|
||||
dp.include_router(notifications.router)
|
||||
|
||||
@@ -6,6 +6,11 @@ from config import ADMIN_ID, DATABASE_URL
|
||||
import asyncpg
|
||||
from datetime import datetime
|
||||
from bot import bot
|
||||
import subprocess
|
||||
from backup import backup_database
|
||||
from handlers.commands import send_message_to_all_clients
|
||||
from aiogram.types import Message
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
router = Router()
|
||||
|
||||
@@ -21,7 +26,10 @@ async def handle_admin_command(message: types.Message):
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")],
|
||||
[InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")]
|
||||
[InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")],
|
||||
[InlineKeyboardButton(text="Отправить сообщение всем клиентам", callback_data="send_to_alls")],
|
||||
[InlineKeyboardButton(text="Создать бэкап", callback_data="backups")],
|
||||
[InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")]
|
||||
])
|
||||
await message.reply("Панель администратора", reply_markup=keyboard)
|
||||
|
||||
@@ -56,6 +64,28 @@ async def user_stats_menu(callback_query: CallbackQuery):
|
||||
|
||||
await callback_query.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data == "send_to_alls")
|
||||
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()
|
||||
|
||||
@router.callback_query(lambda c: c.data == "backups")
|
||||
async def handle_backup(message: Message):
|
||||
await message.answer("Запускаю бэкап базы данных...")
|
||||
await backup_database()
|
||||
await message.answer("Бэкап завершен и отправлен админу.")
|
||||
|
||||
@router.callback_query(lambda c: c.data == "restart_bot")
|
||||
async def handle_restart(callback_query: CallbackQuery):
|
||||
if callback_query.from_user.id == ADMIN_ID:
|
||||
try:
|
||||
result = subprocess.run(['sudo', 'systemctl', 'restart', 'bot.service'], check=True, capture_output=True, text=True)
|
||||
await callback_query.message.answer("Бот успешно перезапущен.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
await callback_query.message.answer(f"Бот будет перезапущен через 30 секунд {e.stderr}")
|
||||
else:
|
||||
await callback_query.answer("У вас нет доступа к этой команде.", show_alert=True)
|
||||
|
||||
@router.callback_query(lambda c: c.data == "user_editor")
|
||||
async def user_editor_menu(callback_query: CallbackQuery):
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncpg
|
||||
|
||||
from bot import bot
|
||||
from config import ADMIN_ID, DATABASE_URL
|
||||
from handlers.pay import ReplenishBalanceState, process_custom_amount_input
|
||||
from handlers.payment.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
|
||||
@@ -82,8 +82,8 @@ async def handle_send_trial_command(message: types.Message, state: FSMContext):
|
||||
await message.answer(f"Ошибка при отправке сообщений: {e}")
|
||||
|
||||
@router.message(Command('send_to_all'))
|
||||
async def send_message_to_all_clients(message: types.Message, state: FSMContext):
|
||||
if message.from_user.id != ADMIN_ID:
|
||||
async def send_message_to_all_clients(message: types.Message, state: FSMContext, from_panel=False):
|
||||
if not from_panel and message.from_user.id != ADMIN_ID:
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from bot import dp
|
||||
from bot import dp, bot
|
||||
import asyncpg
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -9,10 +9,10 @@ from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
|
||||
InlineKeyboardMarkup, Message)
|
||||
|
||||
from auth import link, login_with_credentials
|
||||
from auth import login_with_credentials, link_subscription
|
||||
from client import add_client
|
||||
from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL,
|
||||
SERVERS)
|
||||
SERVERS, APP_URL)
|
||||
from database import add_connection, get_balance, store_key, update_balance
|
||||
from handlers.instructions.instructions import send_instructions
|
||||
from handlers.profile import process_callback_view_profile
|
||||
@@ -43,16 +43,17 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC
|
||||
await conn.close()
|
||||
|
||||
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='view_profile')
|
||||
server_buttons.append([button_back])
|
||||
server_buttons.append([button_back])
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
"<b>⚙️ Выберите сервер для создания ключа:</b>",
|
||||
await callback_query.message.delete()
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text="<b>⚙️ Выберите сервер для создания ключа:</b>",
|
||||
parse_mode="HTML",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=server_buttons)
|
||||
)
|
||||
|
||||
await state.set_state(Form.waiting_for_server_selection)
|
||||
|
||||
await state.set_state(Form.waiting_for_server_selection)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@@ -124,6 +125,16 @@ async def handle_key_name_input(message: Message, state: FSMContext):
|
||||
await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите ключ устройства на английском языке.")
|
||||
return
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
existing_key = await conn.fetchrow('SELECT * FROM keys WHERE email = $1', key_name.lower())
|
||||
if existing_key:
|
||||
await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.")
|
||||
await state.set_state(Form.waiting_for_key_name)
|
||||
return
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
data = await state.get_data()
|
||||
creating_new_key = data.get('creating_new_key', False)
|
||||
server_id = data.get('selected_server_id')
|
||||
@@ -170,7 +181,7 @@ async def handle_key_name_input(message: Message, state: FSMContext):
|
||||
else:
|
||||
raise Exception(error_msg)
|
||||
|
||||
connection_link = await link(session, server_id, client_id, email)
|
||||
connection_link = await link_subscription(email, server_id)
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
@@ -190,18 +201,24 @@ async def handle_key_name_input(message: Message, state: FSMContext):
|
||||
hours, remainder = divmod(remaining_time.seconds, 3600)
|
||||
minutes, _ = divmod(remainder, 60)
|
||||
|
||||
remaining_time_message = (
|
||||
f"Оставшееся время ключа: {days} день"
|
||||
)
|
||||
remaining_time_message = f"Оставшееся время ключа: {days} день"
|
||||
|
||||
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
|
||||
button_iphone = InlineKeyboardButton(
|
||||
text='🍏IPhone',
|
||||
url=f'{APP_URL}/?url=v2raytun://import/{connection_link}'
|
||||
)
|
||||
button_android = InlineKeyboardButton(
|
||||
text='🤖Android',
|
||||
url=f'{APP_URL}/?url=v2raytun://import-sub?url={connection_link}'
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text='📘 Инструкции по использованию', callback_data='instructions')],
|
||||
[InlineKeyboardButton(text='🔙 Перейти в профиль', callback_data='view_profile')]
|
||||
[button_iphone, button_android],
|
||||
[button_profile]
|
||||
])
|
||||
|
||||
key_message = (
|
||||
key_message_success(connection_link, remaining_time_message)
|
||||
)
|
||||
key_message = key_message_success(connection_link, remaining_time_message)
|
||||
|
||||
await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard)
|
||||
|
||||
|
||||
+47
-17
@@ -4,10 +4,10 @@ from datetime import datetime, timedelta
|
||||
import asyncpg
|
||||
from aiogram import Router, types
|
||||
|
||||
from auth import link, login_with_credentials
|
||||
from auth import login_with_credentials, link_subscription
|
||||
from bot import bot
|
||||
from client import add_client, delete_client, extend_client_key
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS, APP_URL
|
||||
from database import get_balance, update_balance
|
||||
from handlers.texts import NO_KEYS
|
||||
from handlers.texts import key_message, key_relocated
|
||||
@@ -42,20 +42,32 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
|
||||
inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
response_message = (
|
||||
"<b>Это ваши устройства:</b>\n\n"
|
||||
"<i>Нажмите на имя устройства для управления его ключом.</i>" # Добавлено курсивом
|
||||
"<i>Нажмите на имя устройства для управления его ключом.</i>"
|
||||
)
|
||||
|
||||
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=inline_keyboard, parse_mode="HTML")
|
||||
else:
|
||||
response_message = (
|
||||
NO_KEYS
|
||||
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text=response_message,
|
||||
reply_markup=inline_keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
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]])
|
||||
|
||||
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
|
||||
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text=response_message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
@@ -65,7 +77,6 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
|
||||
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.callback_query(lambda c: c.data.startswith('view_key|'))
|
||||
async def process_callback_view_key(callback_query: types.CallbackQuery):
|
||||
tg_id = callback_query.from_user.id
|
||||
@@ -281,7 +292,15 @@ async def handle_error(tg_id, callback_query, message):
|
||||
@router.callback_query(lambda c: c.data.startswith('change_location|'))
|
||||
async def process_callback_change_location(callback_query: types.CallbackQuery):
|
||||
tg_id = callback_query.from_user.id
|
||||
client_id = callback_query.data.split('|')[1]
|
||||
client_id = callback_query.data.split('|')[1]
|
||||
|
||||
instructions_message = (
|
||||
"<b>Перед сменой локации:</b>\n\n"
|
||||
"1. Пожалуйста, отключите ваш VPN.\n"
|
||||
"2. Удалите старый ключ, чтобы избежать конфликтов.\n\n"
|
||||
"Теперь выберите новый сервер для вашего ключа:"
|
||||
)
|
||||
|
||||
server_buttons = []
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
@@ -295,8 +314,7 @@ async def process_callback_change_location(callback_query: types.CallbackQuery):
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=server_buttons)
|
||||
|
||||
response_message = "<b>Выберите новый сервер для вашего ключа:</b>"
|
||||
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
|
||||
await bot.edit_message_text(instructions_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
|
||||
await callback_query.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data.startswith('select_server&'))
|
||||
@@ -331,7 +349,7 @@ async def process_callback_select_server(callback_query: types.CallbackQuery):
|
||||
if not new_client_data:
|
||||
raise Exception("Ошибка при создании клиента на новом сервере.")
|
||||
|
||||
new_key = await link(session_new, server_id, client_id, email)
|
||||
new_key = await link_subscription(email, server_id)
|
||||
|
||||
await conn.execute(
|
||||
'UPDATE keys SET server_id = $1, key = $2 WHERE client_id = $3',
|
||||
@@ -345,9 +363,7 @@ async def process_callback_select_server(callback_query: types.CallbackQuery):
|
||||
if not success_delete:
|
||||
raise Exception(f"Ошибка при удалении клиента с сервера {current_server_id}")
|
||||
|
||||
response_message = (
|
||||
key_relocated(new_key)
|
||||
)
|
||||
response_message = key_relocated(new_key)
|
||||
except Exception as e:
|
||||
response_message = f"Ключ перемещен, но возникла ошибка при удалении клиента с текущего сервера: {e}"
|
||||
|
||||
@@ -355,7 +371,21 @@ async def process_callback_select_server(callback_query: types.CallbackQuery):
|
||||
response_message = "Ключ не найден или уже удален."
|
||||
|
||||
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
|
||||
iphone_button = types.InlineKeyboardButton(
|
||||
text='🍏IPhone',
|
||||
url=f'{APP_URL}/?url=v2raytun://import/{new_key}'
|
||||
)
|
||||
android_button = types.InlineKeyboardButton(
|
||||
text='🤖Android',
|
||||
url=f'{APP_URL}/?url=v2raytun://import-sub?url={new_key}'
|
||||
)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[back_button],
|
||||
[iphone_button, android_button]
|
||||
]
|
||||
)
|
||||
|
||||
await bot.edit_message_text(
|
||||
response_message, chat_id=tg_id, message_id=callback_query.message.message_id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncpg
|
||||
import uuid
|
||||
import uuid
|
||||
from config import DATABASE_URL, SERVERS, ADMIN_USERNAME, ADMIN_PASSWORD
|
||||
from auth import login_with_credentials, link
|
||||
from auth import login_with_credentials, link_subscription
|
||||
from client import add_client
|
||||
from database import store_key, add_connection
|
||||
from handlers.texts import INSTRUCTIONS
|
||||
@@ -19,15 +19,17 @@ async def create_trial_key(tg_id: int):
|
||||
expiry_time = current_time + timedelta(days=1, hours=3)
|
||||
expiry_timestamp = int(expiry_time.timestamp() * 1000)
|
||||
|
||||
client_id = str(uuid.uuid4())
|
||||
email = generate_random_email()
|
||||
client_id = str(uuid.uuid4())
|
||||
email = generate_random_email()
|
||||
response = await add_client(
|
||||
session, server_id, client_id, email, tg_id,
|
||||
limit_ip=1, total_gb=0, expiry_time=expiry_timestamp,
|
||||
enable=True, flow="xtls-rprx-vision"
|
||||
)
|
||||
if response.get("success"):
|
||||
connection_link = await link(session, server_id, client_id, email)
|
||||
|
||||
if response.get("success"):
|
||||
# Генерация ссылки подписки
|
||||
connection_link = await link_subscription(email, server_id)
|
||||
|
||||
existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id)
|
||||
|
||||
|
||||
+81
-36
@@ -5,11 +5,11 @@ from aiogram import Bot, Router
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
import logging
|
||||
from config import DATABASE_URL, ADMIN_USERNAME, ADMIN_PASSWORD, SERVERS
|
||||
from database import get_balance, update_key_expiry, delete_key
|
||||
from database import get_balance, update_key_expiry, delete_key, update_balance
|
||||
from client import extend_client_key, delete_client
|
||||
from auth import login_with_credentials
|
||||
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, KEY_RENEWAL_FAILED, KEY_DELETED, KEY_DELETION_FAILED
|
||||
from aiogram import Router, types
|
||||
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, KEY_RENEWAL_FAILED
|
||||
from aiogram import types
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -25,16 +25,16 @@ async def notify_expiring_keys(bot: Bot):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
logger.info("Подключение к базе данных успешно.")
|
||||
|
||||
current_time = datetime.utcnow().timestamp() * 1000
|
||||
current_time = datetime.utcnow().timestamp() * 1000
|
||||
threshold_time_10h = (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000
|
||||
threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
|
||||
|
||||
logger.info("Начало обработки уведомлений.")
|
||||
|
||||
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
|
||||
await asyncio.sleep(1) # Задержка между уведомлениями за 10 часов и 24 часа
|
||||
await asyncio.sleep(1)
|
||||
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
|
||||
await asyncio.sleep(1) # Задержка перед обработкой истекших ключей
|
||||
await asyncio.sleep(1)
|
||||
await handle_expired_keys(bot, conn, current_time)
|
||||
|
||||
except Exception as e:
|
||||
@@ -64,14 +64,33 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
|
||||
email = record['email']
|
||||
expiry_time = record['expiry_time']
|
||||
server_id = record['server_id']
|
||||
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
message = KEY_EXPIRY_10H.format(server_id=SERVERS[server_id]['name'], email=email, expiry_date=expiry_date)
|
||||
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 = "Ключ истек"
|
||||
elif time_left.days > 0:
|
||||
days_left_message = f"{time_left.days}"
|
||||
else:
|
||||
hours_left = time_left.seconds // 3600
|
||||
days_left_message = f"{hours_left}"
|
||||
|
||||
server_name = SERVERS[server_id]['name']
|
||||
message = KEY_EXPIRY_10H.format(
|
||||
server_id=server_name,
|
||||
email=email,
|
||||
expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
days_left_message=days_left_message
|
||||
)
|
||||
|
||||
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='replenish_balance')],
|
||||
[types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')]
|
||||
])
|
||||
await bot.send_message(tg_id, message, reply_markup=keyboard)
|
||||
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
|
||||
@@ -99,18 +118,31 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
|
||||
expiry_time = record['expiry_time']
|
||||
server_id = record['server_id']
|
||||
|
||||
time_left = (expiry_time / 1000) - datetime.utcnow().timestamp()
|
||||
hours_left = max(0, int(time_left // 3600))
|
||||
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
|
||||
current_date = datetime.utcnow()
|
||||
time_left = expiry_date - current_date
|
||||
|
||||
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
||||
balance = await get_balance(tg_id)
|
||||
if time_left.total_seconds() <= 0:
|
||||
days_left_message = "Ключ истек"
|
||||
elif time_left.days > 0:
|
||||
days_left_message = f"{time_left.days}"
|
||||
else:
|
||||
hours_left = time_left.seconds // 3600
|
||||
days_left_message = f"{hours_left}"
|
||||
|
||||
message_24h = KEY_EXPIRY_24H.format(server_id=SERVERS[server_id]['name'], email=email, hours_left=hours_left, expiry_date=expiry_date, balance=balance)
|
||||
message_24h = KEY_EXPIRY_24H.format(
|
||||
server_id=SERVERS[server_id]['name'],
|
||||
email=email,
|
||||
days_left_message=days_left_message,
|
||||
expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S')
|
||||
)
|
||||
|
||||
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='replenish_balance')],
|
||||
[types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')]
|
||||
])
|
||||
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
|
||||
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
|
||||
@@ -123,17 +155,22 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
|
||||
logger.info("Проверка истекших ключей...")
|
||||
|
||||
current_time = int(current_time)
|
||||
|
||||
current_time = datetime.utcnow().timestamp() * 1000
|
||||
adjusted_current_time = current_time + (3 * 60 * 60 * 1000)
|
||||
|
||||
logger.info(f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}")
|
||||
|
||||
expiring_keys = await conn.fetch('''
|
||||
SELECT tg_id, client_id, expiry_time, server_id, email FROM keys
|
||||
WHERE expiry_time <= $1
|
||||
''', current_time)
|
||||
''', adjusted_current_time)
|
||||
|
||||
logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.")
|
||||
|
||||
|
||||
for record in expiring_keys:
|
||||
tg_id = record['tg_id']
|
||||
client_id = record['client_id']
|
||||
@@ -142,11 +179,28 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
email = record['email']
|
||||
|
||||
logger.info(f"Проверка баланса для клиента {tg_id}: {balance}.")
|
||||
|
||||
expiry_time = record['expiry_time']
|
||||
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
|
||||
current_date = datetime.utcnow()
|
||||
time_left = expiry_date - current_date
|
||||
logger.info(f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}.")
|
||||
|
||||
if time_left.total_seconds() <= 0:
|
||||
days_left_message = "Ключ истек"
|
||||
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>"
|
||||
|
||||
message_expired = f"Ваш ключ {email} для сервера {SERVERS[server_id]['name']} истек и был удален!\n\n Перейдите в профиль для создания нового ключа"
|
||||
|
||||
button_profile = types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[button_profile]])
|
||||
|
||||
if balance >= 100:
|
||||
await update_balance(tg_id, -100)
|
||||
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
|
||||
await update_key_expiry(client_id, new_expiry_time)
|
||||
logger.info(f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}.")
|
||||
@@ -166,22 +220,13 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомления о неудачном продлении ключа пользователю {tg_id}: {e}")
|
||||
else:
|
||||
await delete_key(client_id)
|
||||
logger.info(f"Ключ для клиента {tg_id} удален из-за недостаточного баланса.")
|
||||
|
||||
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
success = await delete_client(session, server_id, client_id)
|
||||
if success:
|
||||
try:
|
||||
await bot.send_message(tg_id, KEY_DELETED, reply_markup=keyboard)
|
||||
logger.info(f"Ключ для пользователя {tg_id} удален.")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомления об удалении ключа пользователю {tg_id}: {e}")
|
||||
else:
|
||||
try:
|
||||
await bot.send_message(tg_id, KEY_DELETION_FAILED, reply_markup=keyboard)
|
||||
logger.error(f"Не удалось удалить ключ для пользователя {tg_id}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомления о неудачном удалении ключа пользователю {tg_id}: {e}")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
try:
|
||||
await bot.send_message(tg_id, message_expired, reply_markup=keyboard)
|
||||
await delete_key(client_id)
|
||||
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
success = await delete_client(session, server_id, client_id)
|
||||
logger.info(f"Ключ для клиента {tg_id} удален из базы данных.")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении ключа для клиента {tg_id}: {e}")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import uuid
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiogram import Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiohttp import web
|
||||
from bot import bot
|
||||
from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID
|
||||
from database import update_balance
|
||||
|
||||
router = Router()
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
class ReplenishBalanceState(StatesGroup):
|
||||
choosing_amount = State()
|
||||
waiting_for_payment_confirmation = State()
|
||||
entering_custom_amount = State()
|
||||
|
||||
def generate_signature(params, api_key):
|
||||
sign_string = ":".join([str(params[k]) for k in sorted(params)]) + api_key
|
||||
return hashlib.md5(sign_string.encode()).hexdigest()
|
||||
|
||||
async def create_payment(user_id, amount, email, ip):
|
||||
payment_id = str(uuid.uuid4())
|
||||
nonce = int(time.time() * 1000)
|
||||
params = {
|
||||
"shopId": FREEKASSA_SHOP_ID,
|
||||
"amount": amount,
|
||||
"currency": "RUB",
|
||||
"paymentId": payment_id,
|
||||
"email": email,
|
||||
"ip": ip,
|
||||
"i": 6,
|
||||
"nonce": nonce
|
||||
}
|
||||
params["signature"] = generate_signature(params, FREEKASSA_API_KEY)
|
||||
|
||||
try:
|
||||
response = requests.post("https://api.freekassa.com/v1/orders/create", json=params)
|
||||
response_data = response.json()
|
||||
|
||||
logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}")
|
||||
|
||||
if response_data.get("type") == "success":
|
||||
return response_data["location"]
|
||||
else:
|
||||
logging.error(f"Ошибка создания платежа: {response_data}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка запроса к FreeKassa: {e}")
|
||||
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}")
|
||||
|
||||
logging.debug(f"Данные вебхука от FreeKassa: {data}")
|
||||
|
||||
if data["status"] == "completed":
|
||||
user_id = data["metadata"]["user_id"]
|
||||
amount = float(data["amount"])
|
||||
|
||||
await update_balance(user_id, amount)
|
||||
await send_payment_success_notification(user_id, amount)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@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
|
||||
|
||||
amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="100 рублей", callback_data="amount|100"),
|
||||
InlineKeyboardButton(text="500 рублей", callback_data="amount|500")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="1000 рублей", callback_data="amount|1000"),
|
||||
InlineKeyboardButton(text="5000 рублей", callback_data="amount|5000")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Введите другую сумму", callback_data="enter_custom_amount")
|
||||
]
|
||||
])
|
||||
|
||||
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text="Выберите сумму пополнения через FreeKassa:",
|
||||
reply_markup=amount_keyboard
|
||||
)
|
||||
|
||||
await state.set_state(ReplenishBalanceState.choosing_amount)
|
||||
await callback_query.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data.startswith('amount|'))
|
||||
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
data = callback_query.data.split('|', 1)
|
||||
amount_str = data[1]
|
||||
try:
|
||||
amount = int(amount_str)
|
||||
except ValueError:
|
||||
await bot.send_message(callback_query.from_user.id, "Некорректная сумма.")
|
||||
return
|
||||
|
||||
user_email = f"{callback_query.from_user.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)
|
||||
|
||||
if payment_url:
|
||||
await bot.send_message(callback_query.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}")
|
||||
else:
|
||||
await bot.send_message(callback_query.from_user.id, "Ошибка при создании платежа. Попробуйте позже.")
|
||||
|
||||
await callback_query.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data == 'enter_custom_amount')
|
||||
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
await callback_query.message.edit_text(
|
||||
text="Введите сумму пополнения:"
|
||||
)
|
||||
await state.set_state(ReplenishBalanceState.entering_custom_amount)
|
||||
await callback_query.answer()
|
||||
|
||||
@router.message(ReplenishBalanceState.entering_custom_amount)
|
||||
async def process_custom_amount_input(message: types.Message, state: FSMContext):
|
||||
if message.text.isdigit():
|
||||
amount = int(message.text)
|
||||
if amount <= 0:
|
||||
await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:")
|
||||
return
|
||||
|
||||
user_email = f"{message.from_user.id}@solo.net"
|
||||
user_ip = message.chat.id
|
||||
payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip)
|
||||
|
||||
if payment_url:
|
||||
await bot.send_message(message.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}")
|
||||
else:
|
||||
await message.answer("Ошибка при создании платежа. Попробуйте позже.")
|
||||
|
||||
else:
|
||||
await message.answer("Пожалуйста, введите корректную сумму.")
|
||||
@@ -73,17 +73,20 @@ async def process_callback_replenish_balance(callback_query: types.CallbackQuery
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=PAYMENT_OPTIONS[5]['text'], callback_data=PAYMENT_OPTIONS[5]['callback_data'])
|
||||
]
|
||||
],
|
||||
])
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text="Выберите сумму пополнения:",
|
||||
reply_markup=amount_keyboard
|
||||
)
|
||||
|
||||
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)
|
||||
+39
-30
@@ -1,11 +1,15 @@
|
||||
import os
|
||||
|
||||
from aiogram import Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, BufferedInputFile
|
||||
|
||||
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, CHANNEL_LINK, get_referral_link
|
||||
from config import PAYMENT_METHOD
|
||||
import logging
|
||||
|
||||
|
||||
class ReplenishBalanceState(StatesGroup):
|
||||
@@ -14,54 +18,59 @@ class ReplenishBalanceState(StatesGroup):
|
||||
|
||||
router = Router()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
|
||||
image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg')
|
||||
|
||||
if not os.path.isfile(image_path):
|
||||
await bot.send_message(tg_id, "Файл изображения не найден.")
|
||||
return
|
||||
|
||||
try:
|
||||
key_count = await get_key_count(tg_id)
|
||||
balance = await get_balance(tg_id)
|
||||
if balance is None:
|
||||
balance = 0
|
||||
|
||||
profile_message = (
|
||||
profile_message_send(username, tg_id, balance, key_count)
|
||||
)
|
||||
|
||||
profile_message = profile_message_send(username, tg_id, balance, key_count)
|
||||
|
||||
profile_message += (
|
||||
f"<b>Обязательно подпишитесь на канал</b> <a href='{CHANNEL_LINK}'>здесь</a>\n"
|
||||
)
|
||||
|
||||
if key_count == 0:
|
||||
profile_message += "\n<i>Нажмите ➕Устройство снизу чтобы добавить устройство в VPN</i>"
|
||||
profile_message += "\n<i>Нажмите ➕Устройство снизу, чтобы добавить устройство в VPN</i>"
|
||||
|
||||
button_create_key = InlineKeyboardButton(text='➕ Устройство', callback_data='create_key')
|
||||
button_view_keys = InlineKeyboardButton(text='📱 Мои устройства', callback_data='view_keys')
|
||||
button_replenish_balance = InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')
|
||||
button_invite = InlineKeyboardButton(text='👥 Пригласить', callback_data='invite')
|
||||
button_instructions = InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions')
|
||||
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[button_create_key, button_view_keys],
|
||||
[button_replenish_balance],
|
||||
[button_invite, button_instructions],
|
||||
[button_back]
|
||||
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text='➕ Устройство', callback_data='create_key'), InlineKeyboardButton(text='📱 Мои устр-ва', callback_data='view_keys')],
|
||||
[InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='pay_freekassa' if PAYMENT_METHOD == 'freekassa' else 'replenish_balance')],
|
||||
[InlineKeyboardButton(text='👥 Пригласить', callback_data='invite'), InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions')],
|
||||
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')]
|
||||
])
|
||||
|
||||
# Попробуем удалить предыдущее сообщение
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщения: {e}") # Логируем ошибку, если удаление не удалось
|
||||
|
||||
with open(image_path, 'rb') as image_file:
|
||||
await bot.send_photo(
|
||||
chat_id=tg_id,
|
||||
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
|
||||
caption=profile_message,
|
||||
parse_mode='HTML',
|
||||
reply_markup=inline_keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
profile_message = f"❗️ Ошибка при получении данных профиля: {e}"
|
||||
keyboard = None
|
||||
|
||||
await callback_query.message.delete()
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text=profile_message,
|
||||
parse_mode='HTML',
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
await bot.send_message(tg_id, f"❗️ Ошибка при получении данных профиля: {e}")
|
||||
|
||||
await callback_query.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data == 'invite')
|
||||
|
||||
+18
-5
@@ -6,7 +6,7 @@ from aiogram.types import (BufferedInputFile, CallbackQuery,
|
||||
InlineKeyboardButton, InlineKeyboardMarkup, Message)
|
||||
from handlers.texts import ABOUT_VPN, WELCOME_TEXT
|
||||
from bot import bot
|
||||
from config import CHANNEL_URL, SUPPORT_CHAT_URL
|
||||
from config import CHANNEL_URL, SUPPORT_CHAT_URL, APP_URL
|
||||
from database import add_connection, add_referral, check_connection_exists, get_trial
|
||||
from handlers.keys.trial_key import create_trial_key
|
||||
from handlers.texts import INSTRUCTIONS_TRIAL
|
||||
@@ -74,16 +74,29 @@ async def handle_connect_vpn(callback_query: CallbackQuery):
|
||||
f"<b>Ваш ключ доступа:</b>\n<pre>{trial_key_info['key']}</pre>\n\n"
|
||||
f"<b>Инструкции:</b>\n{INSTRUCTIONS_TRIAL}"
|
||||
)
|
||||
|
||||
|
||||
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
|
||||
inline_keyboard_profile = InlineKeyboardMarkup(inline_keyboard=[[button_profile]])
|
||||
|
||||
button_iphone = InlineKeyboardButton(
|
||||
text='🍏IPhone',
|
||||
url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}'
|
||||
)
|
||||
button_android = InlineKeyboardButton(
|
||||
text='🤖Android',
|
||||
url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}'
|
||||
)
|
||||
|
||||
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[button_iphone, button_android],
|
||||
[button_profile]
|
||||
])
|
||||
|
||||
await callback_query.message.answer(
|
||||
key_message,
|
||||
parse_mode='HTML',
|
||||
reply_markup=inline_keyboard_profile
|
||||
reply_markup=inline_keyboard
|
||||
)
|
||||
|
||||
|
||||
await callback_query.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data == 'about_vpn')
|
||||
|
||||
@@ -11,7 +11,8 @@ from bot import bot, dp, router
|
||||
from config import WEBAPP_HOST, WEBAPP_PORT, WEBHOOK_PATH, WEBHOOK_URL
|
||||
from database import init_db
|
||||
from handlers.notifications import notify_expiring_keys
|
||||
from handlers.pay import payment_webhook
|
||||
from handlers.payment.pay import payment_webhook
|
||||
from handlers.payment.freekassa import freekassa_webhook
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
@@ -52,6 +53,7 @@ async def main():
|
||||
app.on_startup.append(on_startup)
|
||||
app.on_shutdown.append(on_shutdown)
|
||||
app.router.add_post('/yookassa/webhook', payment_webhook)
|
||||
app.router.add_post('/freekassa/webhook', freekassa_webhook)
|
||||
|
||||
SimpleRequestHandler(dispatcher=dp, bot=bot).register(app, path=WEBHOOK_PATH)
|
||||
setup_application(app, dp, bot=bot)
|
||||
|
||||
Reference in New Issue
Block a user