diff --git a/.gitignore b/.gitignore
index 56a3eb02..2e61aaf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,4 +52,5 @@ Thumbs.db
nginx.conf
scripts
models.py
-Dockerfile
\ No newline at end of file
+Dockerfile
+.csv
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py
index ce7466f6..29a2a365 100644
--- a/handlers/admin/admin_panel.py
+++ b/handlers/admin/admin_panel.py
@@ -1,4 +1,5 @@
from datetime import datetime
+from io import BytesIO
import subprocess
from typing import Any
@@ -6,7 +7,7 @@ from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
-from aiogram.types import CallbackQuery, InlineKeyboardButton
+from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from backup import backup_database
@@ -82,6 +83,8 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats"))
+ builder.row(InlineKeyboardButton(text="📥 Выгрузить пользователей в CSV", callback_data="export_users_csv"))
+ builder.row(InlineKeyboardButton(text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv"))
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
await callback_query.message.answer(stats_message, reply_markup=builder.as_markup())
@@ -89,6 +92,98 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any):
logger.error(f"Error in user_stats_menu: {e}")
+@router.callback_query(F.data == "export_users_csv", IsAdminFilter())
+async def export_users_csv(callback_query: CallbackQuery, session: Any):
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats"))
+ try:
+ users = await session.fetch(
+ """
+ SELECT
+ u.tg_id,
+ u.username,
+ u.first_name,
+ u.last_name,
+ u.language_code,
+ u.is_bot,
+ c.balance,
+ c.trial
+ FROM users u
+ LEFT JOIN connections c ON u.tg_id = c.tg_id
+ """
+ )
+
+ if not users:
+ await callback_query.message.answer("📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup())
+ return
+
+ csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" # Заголовки CSV
+ for user in users:
+ csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
+
+ file_name = BytesIO(csv_data.encode("utf-8-sig"))
+ file_name.seek(0)
+
+ file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv")
+
+ await callback_query.message.answer_document(
+ file, caption="📥 Экспорт пользователей в CSV", reply_markup=builder.as_markup()
+ )
+ file_name.close()
+
+ except Exception as e:
+ logger.error(f"Ошибка при экспорте пользователей в CSV: {e}")
+ await callback_query.message.answer(
+ "❗ Произошла ошибка при экспорте пользователей.", reply_markup=builder.as_markup()
+ )
+
+
+@router.callback_query(F.data == "export_payments_csv", IsAdminFilter())
+async def export_payments_csv(callback_query: CallbackQuery, session: Any):
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats"))
+ try:
+ payments = await session.fetch(
+ """
+ SELECT
+ u.tg_id,
+ u.username,
+ u.first_name,
+ u.last_name,
+ p.amount,
+ p.payment_system,
+ p.status,
+ p.created_at
+ FROM users u
+ JOIN payments p ON u.tg_id = p.tg_id
+ """
+ )
+
+ if not payments:
+ await callback_query.message.answer("📭 Нет платежей для экспорта.", reply_markup=builder.as_markup())
+ return
+
+ csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV
+ for payment in payments:
+ csv_data += f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n"
+
+ file_name = BytesIO(csv_data.encode("utf-8-sig"))
+ file_name.seek(0)
+
+ file = BufferedInputFile(file_name.getvalue(), filename="payments_export.csv")
+
+ await callback_query.message.answer_document(
+ file, caption="📥 Экспорт платежей в CSV", reply_markup=builder.as_markup()
+ )
+ file_name.close()
+
+ except Exception as e:
+ logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
+ await callback_query.message.answer(
+ "❗ Произошла ошибка при экспорте платежей.", reply_markup=builder.as_markup()
+ )
+
+
@router.callback_query(F.data == "send_to_alls", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py
index c485963f..a9605053 100644
--- a/handlers/admin/admin_user_editor.py
+++ b/handlers/admin/admin_user_editor.py
@@ -29,13 +29,17 @@ class UserEditorState(StatesGroup):
@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter())
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
- await callback_query.message.answer("🔍 Введите Telegram ID клиента:")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.callback_query(F.data == "search_by_username", IsAdminFilter())
async def prompt_username(callback_query: CallbackQuery, state: FSMContext):
- await callback_query.message.answer("🔍 Введите Username клиента:")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_username)
@@ -45,7 +49,9 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi
user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username)
if not user_record:
- await message.answer("🔍 Пользователь с указанным username не найден. 🚫")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer("🔍 Пользователь с указанным username не найден. 🚫", reply_markup=builder.as_markup())
await state.clear()
return
@@ -56,7 +62,9 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
- await message.answer("🚫 Пользователь с указанным tg_id не найден. 🔍")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer("🚫 Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup())
await state.clear()
return
@@ -102,7 +110,9 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session:
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
if balance is None:
- await message.answer("❌ Пользователь с указанным tg_id не найден. 🔍")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer("❌ Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup())
await state.clear()
return
@@ -155,15 +165,20 @@ async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any
async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
tg_id = int(callback_query.data.split("_")[2])
await state.update_data(tg_id=tg_id)
-
- await callback_query.message.answer("💸 Введите новую сумму баланса:")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter())
async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any):
if not message.text.isdigit() or int(message.text) < 0:
- await message.answer("❌ Пожалуйста, введите корректную сумму для изменения баланса.")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer(
+ "❌ Пожалуйста, введите корректную сумму для изменения баланса.", reply_markup=builder.as_markup()
+ )
return
new_balance = int(message.text)
@@ -239,7 +254,11 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any):
key_details = await get_key_details(email, session)
if not key_details:
- await callback_query.message.answer("🔍 Информация о ключе не найдена. 🚫")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await callback_query.message.answer(
+ "🔍 Информация о ключе не найдена. 🚫", reply_markup=builder.as_markup()
+ )
return
response_message = (
@@ -250,6 +269,12 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any):
)
builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(
+ text="ℹ️ Получить информацию о юзере",
+ callback_data=f"user_info|{key_details['tg_id']}",
+ )
+ )
builder.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
@@ -269,7 +294,9 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
- await callback_query.message.answer("🔑 Введите имя ключа:")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup())
await state.set_state(UserEditorState.waiting_for_key_name)
@@ -280,13 +307,7 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi
if not key_details:
builder = InlineKeyboardBuilder()
- builder.row(
- InlineKeyboardButton(
- text="🔙 Назад в меню администратора",
- callback_data="admin",
- )
- )
-
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(
"🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=builder.as_markup(),
@@ -302,6 +323,12 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi
)
key_buttons = InlineKeyboardBuilder()
+ key_buttons.row(
+ InlineKeyboardButton(
+ text="ℹ️ Получить информацию о юзере",
+ callback_data=f"user_info|{key_details['tg_id']}",
+ )
+ )
key_buttons.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
@@ -314,7 +341,7 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi
callback_data=f"delete_key_admin|{key_name}",
)
)
- key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
+ key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
await message.answer(response_message, reply_markup=key_buttons.as_markup())
await state.clear()
@@ -336,7 +363,9 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se
email = user_data.get("email")
if not email:
- await message.answer("📧 Email не найден в состоянии. 🚫")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup())
await state.clear()
return
@@ -346,13 +375,17 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se
client_id = await get_client_id_by_email(email)
if client_id is None:
- await message.answer(f"🚫 Клиент с email {email} не найден. 🔍")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer(f"🚫 Клиент с email {email} не найден. 🔍", reply_markup=builder.as_markup())
await state.clear()
return
record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
if not record:
- await message.answer("🚫 Клиент не найден в базе данных. 🔍")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer("🚫 Клиент не найден в базе данных. 🔍", reply_markup=builder.as_markup())
await state.clear()
return
@@ -384,7 +417,11 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await message.answer(response_message, reply_markup=builder.as_markup())
except ValueError:
- await message.answer("❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.")
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await message.answer(
+ "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", reply_markup=builder.as_markup()
+ )
except Exception as e:
logger.error(e)
await state.clear()
@@ -396,9 +433,9 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi
client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email)
if client_id is None:
- await callback_query.message.answer(
- "🔍 Ключ не найден. 🚫",
- )
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+ await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup())
return
builder = InlineKeyboardBuilder()
@@ -440,3 +477,44 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
+
+
+@router.callback_query(F.data.startswith("user_info|"), IsAdminFilter())
+async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
+ tg_id = int(callback_query.data.split("|")[1])
+ username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id)
+ balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
+ key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
+ referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
+
+ builder = InlineKeyboardBuilder()
+
+ for (email,) in key_records:
+ builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
+
+ builder.row(
+ InlineKeyboardButton(
+ text="📝 Изменить баланс",
+ callback_data=f"change_balance_{tg_id}",
+ )
+ )
+
+ builder.row(
+ InlineKeyboardButton(
+ text="🔄 Восстановить пробник",
+ callback_data=f"restore_trial_{tg_id}",
+ )
+ )
+
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
+
+ user_info = (
+ f"📊 Информация о пользователе:\n\n"
+ f"🆔 ID пользователя: {tg_id}\n"
+ f"👤 Логин пользователя: @{username}\n"
+ f"💰 Баланс: {balance}\n"
+ f"👥 Количество рефералов: {referral_count}\n"
+ f"🔑 Ключи (для редактирования нажмите на ключ):"
+ )
+ await callback_query.message.answer(user_info, reply_markup=builder.as_markup())
+ await state.set_state(UserEditorState.displaying_user_info)
diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py
index 780a7e71..27859099 100644
--- a/handlers/keys/key_management.py
+++ b/handlers/keys/key_management.py
@@ -9,7 +9,16 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, SUPPORT_CHAT_URL
+from config import (
+ CONNECT_ANDROID,
+ CONNECT_IOS,
+ DOWNLOAD_ANDROID,
+ DOWNLOAD_IOS,
+ PUBLIC_LINK,
+ RENEWAL_PLANS,
+ SUPPORT_CHAT_URL,
+ TRIAL_TIME,
+)
from database import (
add_connection,
check_connection_exists,
@@ -20,7 +29,7 @@ from database import (
use_trial,
)
from handlers.keys.key_utils import create_key_on_cluster
-from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, RENEWAL_PLANS, key_message_success
+from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, key_message_success
from handlers.utils import get_least_loaded_cluster, sanitize_key_name
from logger import logger
@@ -117,7 +126,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An
trial_status = await get_trial(message.chat.id, session)
if trial_status == 0:
- expiry_time = current_time + timedelta(days=1, hours=3)
+ expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
else:
balance = await get_balance(tg_id)
@@ -133,7 +142,7 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An
return
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
- expiry_time = current_time + timedelta(days=30, hours=3)
+ expiry_time = current_time + timedelta(days=30)
logger.info(f"User {tg_id} balance deducted for key creation.")
expiry_timestamp = int(expiry_time.timestamp() * 1000)
diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py
index 0c6d191a..176bba3a 100644
--- a/handlers/keys/keys.py
+++ b/handlers/keys/keys.py
@@ -8,7 +8,16 @@ from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import CLUSTERS, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, TOTAL_GB
+from config import (
+ CLUSTERS,
+ CONNECT_ANDROID,
+ CONNECT_IOS,
+ DOWNLOAD_ANDROID,
+ DOWNLOAD_IOS,
+ PUBLIC_LINK,
+ RENEWAL_PLANS,
+ TOTAL_GB,
+)
from database import delete_key, get_balance, store_key, update_balance, update_key_expiry
from handlers.keys.key_utils import (
delete_key_from_cluster,
@@ -21,7 +30,6 @@ from handlers.texts import (
KEY_NOT_FOUND_MSG,
NO_KEYS,
PLAN_SELECTION_MSG,
- RENEWAL_PLANS,
SUCCESS_RENEWAL_MSG,
key_message,
)
diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py
index 70d5adfe..c8f81b3c 100644
--- a/handlers/keys/trial_key.py
+++ b/handlers/keys/trial_key.py
@@ -18,7 +18,7 @@ async def create_trial_key(tg_id: int, session: Any):
instructions = INSTRUCTIONS
result = {"key": public_link, "instructions": instructions}
current_time = datetime.utcnow()
- expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3)
+ expiry_time = current_time + timedelta(days=TRIAL_TIME)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
least_loaded_cluster = await get_least_loaded_cluster()
diff --git a/handlers/notifications.py b/handlers/notifications.py
index 613f940a..42e11761 100644
--- a/handlers/notifications.py
+++ b/handlers/notifications.py
@@ -7,7 +7,7 @@ import asyncpg
from py3xui import AsyncApi
from client import delete_client
-from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB, TRIAL_TIME
+from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, DEV_MODE, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME
from database import (
add_notification,
check_notification_time,
@@ -17,7 +17,7 @@ from database import (
update_key_expiry,
)
from handlers.keys.key_utils import renew_key_in_cluster
-from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS
+from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED
from logger import logger
router = Router()
@@ -29,13 +29,16 @@ async def notify_expiring_keys(bot: Bot):
conn = await asyncpg.connect(DATABASE_URL)
logger.info("Подключение к базе данных успешно.")
- 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
+ current_time = int(datetime.utcnow().timestamp() * 1000)
+ threshold_time_10h = int((datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000)
+ threshold_time_24h = int((datetime.utcnow() + timedelta(days=1)).timestamp() * 1000)
logger.info("Начало обработки уведомлений.")
- await notify_inactive_trial_users(bot, conn)
+ # TODO
+ # await notify_inactive_trial_users(bot, conn)
+ # await asyncio.sleep(1)
+ await check_online_users()
await asyncio.sleep(1)
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(1)
@@ -53,6 +56,8 @@ async def notify_expiring_keys(bot: Bot):
async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
+ if DEV_MODE:
+ return False
try:
member = await bot.get_chat_member(chat_id, bot.id)
blocked = member.status == "left"
@@ -103,7 +108,7 @@ async def notify_10h_keys(
price=RENEWAL_PLANS["1"]["price"],
)
- if not await is_bot_blocked(bot, tg_id):
+ if not await is_bot_blocked(bot, tg_id) and not DEV_MODE:
try:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{record["client_id"]}')
@@ -167,7 +172,7 @@ async def notify_24h_keys(
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
- if not await is_bot_blocked(bot, tg_id):
+ if not await is_bot_blocked(bot, tg_id) and not DEV_MODE:
try:
builder = InlineKeyboardBuilder()
builder.row(
@@ -259,89 +264,85 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
logger.info("Проверка истекших ключей...")
-
- adjusted_current_time = current_time + (3 * 60 * 60 * 1000)
expiring_keys = await conn.fetch(
"""
SELECT tg_id, client_id, expiry_time, email FROM keys
WHERE expiry_time <= $1
""",
- adjusted_current_time,
+ current_time,
)
+ logger.info(f"current_time {current_time}")
logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.")
- async def process_key(record):
- tg_id = record["tg_id"]
- client_id = record["client_id"]
- email = record["email"]
- balance = await get_balance(tg_id)
- 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}"
- )
-
- message_expired = (
- f"❌ Ваша подписка {email} истекла и была удалена!\n\n"
- "🔍 Перейдите в профиль для создания новой подписки.\n"
- "💡 Не откладывайте подключение VPN!"
- )
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
- )
-
- try:
- if balance >= RENEWAL_PLANS["1"]["price"]:
- await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
- new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
- await update_key_expiry(client_id, new_expiry_time)
-
- for cluster_id in CLUSTERS:
- await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB)
- logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
-
- await conn.execute(
- """
- UPDATE keys
- SET notified = FALSE, notified_24h = FALSE
- WHERE client_id = $1
- """,
- client_id,
- )
- logger.info(f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}.")
- try:
- await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
- logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
- except Exception as e:
- logger.error(f"Ошибка при отправке уведомления клиенту {tg_id}: {e}")
-
- else:
- await safe_send_message(bot, tg_id, message_expired, reply_markup=keyboard)
- await delete_key(client_id)
-
- for cluster_id, cluster in CLUSTERS.items():
- for server_id, server in cluster.items():
- xui = AsyncApi(
- server["API_URL"],
- username=ADMIN_USERNAME,
- password=ADMIN_PASSWORD,
- )
- await delete_client(xui, email, client_id)
-
- except Exception as e:
- logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
-
- await asyncio.gather(*[process_key(record) for record in expiring_keys])
+ await asyncio.gather(*[process_key(record, bot, conn) for record in expiring_keys])
-async def safe_send_message(bot, tg_id, text, reply_markup=None):
+async def process_key(record, bot, conn):
+ tg_id = record["tg_id"]
+ client_id = record["client_id"]
+ email = record["email"]
+ balance = await get_balance(tg_id)
+ 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}"
+ )
+ keyboard = types.InlineKeyboardMarkup(
+ inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
+ )
+
try:
- await bot.send_message(tg_id, text, reply_markup=reply_markup)
- except Exception as e:
- if "chat not found" in str(e):
- logger.warning(f"Чат для клиента {tg_id} не найден.")
+ if balance >= RENEWAL_PLANS["1"]["price"]:
+ await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
+ new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
+ await update_key_expiry(client_id, new_expiry_time)
+
+ for cluster_id in CLUSTERS:
+ await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB)
+ logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
+
+ await conn.execute(
+ """
+ UPDATE keys
+ SET notified = FALSE, notified_24h = FALSE
+ WHERE client_id = $1
+ """,
+ client_id,
+ )
+ logger.info(f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}.")
+ try:
+ await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
+ logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
+ except Exception as e:
+ logger.error(f"Ошибка при отправке уведомления клиенту {tg_id}: {e}")
+
else:
- logger.error(f"Ошибка при отправке сообщения клиенту {tg_id}: {e}")
+ for cluster_id, cluster in CLUSTERS.items():
+ for server_id, server in cluster.items():
+ xui = AsyncApi(
+ server["API_URL"],
+ username=ADMIN_USERNAME,
+ password=ADMIN_PASSWORD,
+ )
+ await delete_client(xui, email, client_id)
+ # await xui.client.delete_depleted(-1)
+ await delete_key(client_id)
+ except Exception as e:
+ logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
+
+
+async def check_online_users():
+ for cluster_id, cluster in CLUSTERS.items():
+ for server_id, server in cluster.items():
+ xui = AsyncApi(server["API_URL"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
+ await xui.login()
+ try:
+ online_users = len(await xui.client.online())
+ logger.info(
+ f"Сервер '{server['name']}' доступен, текущее количество активных пользователей: {online_users}."
+ )
+ except Exception as e:
+ logger.error(f"Не удалось проверить пользователей на сервере {server_id}: {e}")
diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py
index 67cdd3c6..9a612586 100644
--- a/handlers/payments/cryprobot_pay.py
+++ b/handlers/payments/cryprobot_pay.py
@@ -54,9 +54,7 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st
callback_data="enter_custom_amount_crypto",
)
)
-
- builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
-
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(callback_query.message.chat.id)
if key_count == 0:
exists = await check_connection_exists(callback_query.message.chat.id)
diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py
index b3c26790..7c423129 100644
--- a/handlers/payments/freekassa_pay.py
+++ b/handlers/payments/freekassa_pay.py
@@ -110,7 +110,7 @@ async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, st
callback_data="enter_custom_amount_freekassa",
)
)
- builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
await callback_query.message.answer(
text="Выберите сумму пополнения через FreeKassa:",
diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py
index f9248aec..f2e50906 100644
--- a/handlers/payments/robokassa_pay.py
+++ b/handlers/payments/robokassa_pay.py
@@ -79,7 +79,7 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st
callback_data="enter_custom_amount_robokassa",
)
)
- builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(tg_id)
diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py
index edc9f071..7d073a55 100644
--- a/handlers/payments/yookassa_pay.py
+++ b/handlers/payments/yookassa_pay.py
@@ -61,7 +61,7 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta
callback_data="enter_custom_amount_yookassa",
)
)
- builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
+ builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(tg_id)
diff --git a/handlers/profile.py b/handlers/profile.py
index e4d67c20..9029aa9f 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -5,9 +5,9 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import CHANNEL_URL
+from config import CHANNEL_URL, RENEWAL_PLANS
from database import get_balance, get_key_count, get_referral_stats
-from handlers.texts import RENEWAL_PLANS, get_referral_link, invite_message_send, profile_message_send
+from handlers.texts import get_referral_link, invite_message_send, profile_message_send
router = Router()
diff --git a/handlers/start.py b/handlers/start.py
index 2c6a7799..c5e240e8 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -22,6 +22,7 @@ async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMC
@router.message(Command("start"))
async def start_command(message: Message, state: FSMContext, session: Any, admin: bool):
+ await state.clear()
if message.text:
try:
referrer_tg_id = int(message.text.split("referral_")[1])
@@ -80,7 +81,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
)
builder = InlineKeyboardBuilder()
- builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
+ builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS),
InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID),
@@ -95,6 +96,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
url=f'{CONNECT_ANDROID}{trial_key_info["key"]}',
),
)
+ builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(key_message, reply_markup=builder.as_markup())
diff --git a/main.py b/main.py
index 54d2efd0..6c23e88d 100644
--- a/main.py
+++ b/main.py
@@ -71,6 +71,7 @@ async def main():
logger.info("Запуск в режиме разработки...")
await bot.delete_webhook()
await init_db()
+ asyncio.create_task(periodic_notifications())
await dp.start_polling(bot)
else:
logger.info("Запуск в production режиме...")
diff --git a/middlewares/delete.py b/middlewares/delete.py
index e3911cf8..ae3d7198 100644
--- a/middlewares/delete.py
+++ b/middlewares/delete.py
@@ -13,11 +13,12 @@ class DeleteMessageMiddleware(BaseMiddleware):
) -> Any:
if isinstance(event, (Message, CallbackQuery)):
if isinstance(event, Message):
- try:
- await event.bot.delete_message(event.chat.id, event.message_id - 1)
- except Exception:
- pass
- await event.delete()
+ if not event.entities[0].type == "bot_command" and event.text == "/start":
+ try:
+ await event.bot.delete_message(event.chat.id, event.message_id - 1)
+ except Exception:
+ pass
+ await event.delete()
elif isinstance(event, CallbackQuery):
await event.answer()
await event.message.delete()