32 Commits

Author SHA1 Message Date
Vladislav Lisitsyn 9c61a8475c Merge pull request #71 from Vladless/subscriptions-feature
Проверка ключей
2024-11-05 22:02:36 +03:00
Vladless 7b5cbb8629 Проверка ключей 2024-11-05 22:00:32 +03:00
Vladislav Lisitsyn 1e813d506f Merge pull request #70 from Vladless/subscriptions-feature
Админка. Расширение
2024-11-04 01:10:40 +03:00
Vladless 43a95a7d5a Админка. Расширение 2024-11-04 01:10:02 +03:00
Vladislav Lisitsyn 787a1e13c0 Merge pull request #69 from Vladless/subscriptions-feature
Переработка уведомлений
2024-11-03 23:42:07 +03:00
Vladless 6abeae35f8 Переработка уведомлений 2024-11-03 23:41:26 +03:00
Vladislav Lisitsyn cb94158bd0 Merge pull request #68 from Vladless/subscriptions-feature
Профиль. Логи
2024-11-03 21:54:11 +03:00
Vladless cf0924959c Профиль. Логи 2024-11-03 21:52:59 +03:00
Vladislav Lisitsyn 2ad8627215 Merge pull request #67 from Vladless/subscriptions-feature
Уведомления/Профиль. Логгирование
2024-11-03 21:40:41 +03:00
Vladless c2429a0269 Профиль. Логи 2024-11-03 21:39:56 +03:00
Vladless fd35207f2f Уведомления. Правки 2024-11-03 21:38:26 +03:00
Vladislav Lisitsyn a9e318136f Merge pull request #66 from Vladless/subscriptions-feature
Уведомления. Правки
2024-11-03 01:49:54 +03:00
Vladless 5f46156907 Уведомления. Правки 2024-11-03 01:49:02 +03:00
Vladislav Lisitsyn ee7928dda8 Update README.md 2024-11-03 00:34:00 +03:00
Vladislav Lisitsyn b39634d4fa Merge pull request #65 from Vladless/subscriptions-feature
добавление freekassa
2024-11-03 00:32:38 +03:00
Vladless 23cd7c9e60 добавление freekassa 2024-11-03 00:31:57 +03:00
Vladislav Lisitsyn 9ba1cacdaf Update README.md 2024-11-02 04:30:23 +03:00
Vladislav Lisitsyn cc5d47e546 Update README.md 2024-11-02 04:30:01 +03:00
Vladislav Lisitsyn 4ba6ef8f0d Update README.md 2024-11-02 04:29:12 +03:00
Vladislav Lisitsyn 9003dc5841 Merge pull request #64 from Vladless/subscriptions-feature
Подписки. Обновление ключей. Кнопки
2024-11-02 04:25:49 +03:00
Vladislav Lisitsyn ba2b2c63a4 Merge branch 'main' into subscriptions-feature 2024-11-02 04:25:39 +03:00
Vladless 4ada912f98 удобная смена подписки 2024-11-02 04:17:54 +03:00
Vladless 9a10859109 перевод на v2raytun 2024-11-02 03:10:49 +03:00
Vladless 5131c82b75 перевод на v2raytun 2024-11-02 01:17:04 +03:00
Vladless e590c08095 перевод на v2raytun 2024-11-01 23:52:21 +03:00
Vladislav Lisitsyn e7dcd58314 Update README.md 2024-10-31 23:59:20 +03:00
Vladislav Lisitsyn bebd051f47 Merge pull request #63 from Vladless/subscriptions-feature
Кнопка автодобавления конфигурации в клиентское приложение
2024-10-30 20:07:12 +03:00
Vladless 56babedde6 Кнопка автодобавления конфигурации в клиентское приложение 2024-10-30 20:05:38 +03:00
Vladislav Lisitsyn e9e4973dc9 Merge pull request #62 from Vladless/subscriptions-feature
Подписки. Полная версия
2024-10-30 18:03:36 +03:00
Vladless 8eb76ff9e4 Подписки. Полная версия 2024-10-30 18:03:04 +03:00
Vladless 69de84d430 ReadMe 2024-10-30 04:50:53 +03:00
Vladless 52b41fe521 ReadMe 2024-10-30 03:57:27 +03:00
13 changed files with 443 additions and 124 deletions
+11 -3
View File
@@ -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 реализует множество функций, включая:
- **Чат поддержки** и канал для связи.
- **Автоматическое продление ключа** при наличии достаточного баланса.
- **Удобная админка прямо в боте**
- **мультисерверность** добавляй сервера в конфига, и они автоматически будут в боте
- **мультисерверность** добавляй сервера в конфиг, и они автоматически будут в боте
![Основные окна](preview.jpg)
---
@@ -113,7 +121,7 @@ WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
SUPPORT_CHAT_URL = ваша ссылка на поддержку
```
**Полная версия конфигурации и файл кастомизации доступны через поддержку нашего бота**
**Мы высылаем детальный гайд и недостающие файлы в боте**
**Все описания в одном файле!** Удобно настроить бот под свой сервис изменив информацию и цены в одном месте
+3 -2
View File
@@ -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)
+31 -1
View File
@@ -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=[
+3 -3
View File
@@ -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
+34 -17
View File
@@ -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
View File
@@ -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,
+8 -6
View File
@@ -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
View File
@@ -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)
+159
View File
@@ -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("Пожалуйста, введите корректную сумму.")
+6 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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')
+3 -1
View File
@@ -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)