FlowPayRenew/fixNotify/pay_history/custom/statistic/minorFixes

This commit is contained in:
Vladless
2025-01-14 04:06:55 +03:00
parent c0cbc17c1e
commit 0391bd90e9
30 changed files with 3491 additions and 2372 deletions
+1 -1
View File
@@ -3,8 +3,8 @@ import subprocess
from datetime import datetime
from aiogram.types import BufferedInputFile
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
from logger import logger
+1 -1
View File
@@ -5,8 +5,8 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import ErrorEvent
from config import API_TOKEN
from config import API_TOKEN
from logger import logger
from middlewares.admin import AdminMiddleware
from middlewares.database import DatabaseMiddleware
+1 -1
View File
@@ -1,6 +1,6 @@
import py3xui
from config import LIMIT_IP
from config import LIMIT_IP
from logger import logger
+1 -1
View File
@@ -3,8 +3,8 @@ from datetime import datetime
from typing import Any
import asyncpg
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
+1
View File
@@ -1,5 +1,6 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+2 -2
View File
@@ -10,10 +10,10 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL
from backup import backup_database
from bot import bot
from config import DATABASE_URL
from database import delete_user_data
from filters.admin import IsAdminFilter
from logger import logger
@@ -37,7 +37,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
async def handle_admin_message(message: types.Message, state: FSMContext):
await state.clear()
BOT_VERSION = "4.0.0-preAlpha(9)"
BOT_VERSION = "4.0.0-preAlpha(14)"
builder = InlineKeyboardBuilder()
builder.row(
+11 -14
View File
@@ -6,10 +6,10 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_key_on_cluster
@@ -378,22 +378,19 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery):
)
return
tasks = []
for key in keys_to_sync:
tasks.append(
asyncio.create_task(
create_key_on_cluster(
cluster_name,
key["tg_id"],
key["client_id"],
key["email"],
key["expiry_time"],
)
try:
await create_key_on_cluster(
cluster_name,
key["tg_id"],
key["client_id"],
key["email"],
key["expiry_time"],
)
)
await asyncio.sleep(0.2)
except Exception as e:
logger.error(f"Ошибка при добавлении ключа {key['client_id']} в кластер {cluster_name}: {e}")
await asyncio.gather(*tasks)
await callback_query.message.answer(
f"✅ Ключи успешно синхронизированы для кластера {cluster_name}.",
reply_markup=InlineKeyboardBuilder()
+1 -1
View File
@@ -7,8 +7,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import TOTAL_GB
from config import TOTAL_GB
from database import (
delete_user_data,
get_client_id_by_email,
+4 -4
View File
@@ -1,6 +1,6 @@
DOWNLOAD_IOS_BUTTON = "🍏 Скачать для iOS"
DOWNLOAD_ANDROID_BUTTON = "🤖 Скачать для Android"
IMPORT_IOS = "🍏 Подключить на iOS"
IMPORT_ANDROID = "🤖 Подключить на Android"
DOWNLOAD_IOS_BUTTON = "🍏 Скачать iOS"
DOWNLOAD_ANDROID_BUTTON = "🤖 Скачать Android"
IMPORT_IOS = "🍏 Подключить"
IMPORT_ANDROID = "🤖 Подключить"
PC_BUTTON = "💻 Компьютеры"
TV_BUTTON = "📺 Андроид TV"
+1 -1
View File
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from config import RUB_TO_XTR
from logger import logger
+1 -1
View File
@@ -5,8 +5,8 @@ import asyncpg
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from handlers.texts import (
CONNECT_TV_TEXT,
INSTRUCTION_PC,
+6 -3
View File
@@ -8,6 +8,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
@@ -19,8 +21,6 @@ from config import (
TRIAL_TIME,
USE_NEW_PAYMENT_FLOW,
)
from bot import bot
from database import (
get_balance,
get_trial,
@@ -37,6 +37,7 @@ from handlers.buttons.add_subscribe import (
TV_BUTTON,
)
from handlers.keys.key_utils import create_key_on_cluster
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import DISCOUNTS, key_message_success
from handlers.utils import generate_random_email, get_least_loaded_cluster
@@ -145,8 +146,10 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
if balance < plan_price:
required_amount = plan_price - balance
if USE_NEW_PAYMENT_FLOW:
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
await handle_custom_amount_input(callback_query, session)
else:
builder = InlineKeyboardBuilder()
builder.row(
+1 -1
View File
@@ -1,9 +1,9 @@
import asyncio
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB
from py3xui import AsyncApi
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB
from database import get_servers_from_db
from logger import logger
+105 -72
View File
@@ -7,21 +7,25 @@ from typing import Any
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
ENABLE_DELETE_KEY_BUTTON,
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
PUBLIC_LINK,
RENEWAL_PLANS,
TOTAL_GB,
USE_NEW_PAYMENT_FLOW,
)
from database import (
delete_key,
get_balance,
get_servers_from_db,
save_temporary_data,
store_key,
update_balance,
update_key_expiry,
@@ -40,9 +44,10 @@ from handlers.keys.key_utils import (
renew_key_in_cluster,
update_key_on_cluster,
)
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
DISCOUNTS,
INSUFFICIENT_FUNDS_MSG,
KEY_NOT_FOUND_MSG,
PLAN_SELECTION_MSG,
SUCCESS_RENEWAL_MSG,
@@ -113,7 +118,7 @@ def build_keys_response(records):
inline_keyboard = builder.as_markup()
response_message = (
"<b>🔑 Список ваших подписок</b>\n\n"
"<i>👇 Выберите подписку для управления или добавьте новую (например, для подключения нового устройства):</i>"
"<i>👇 Выберите подписку для управления или добавьте новую для подключения дополнительного устройства:</i>"
)
return inline_keyboard, response_message
@@ -214,14 +219,22 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
),
)
builder.row(
InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
),
InlineKeyboardButton(
text="❌ Удалить", callback_data=f"delete_key|{key_name}"
),
)
# ✅ Добавлена проверка флага ENABLE_DELETE_KEY_BUTTON
if ENABLE_DELETE_KEY_BUTTON:
builder.row(
InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
),
InlineKeyboardButton(
text="❌ Удалить", callback_data=f"delete_key|{key_name}"
),
)
else:
builder.row(
InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{key_name}"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
@@ -480,14 +493,9 @@ async def process_callback_confirm_delete(
@router.callback_query(F.data.startswith("renew_plan|"))
async def process_callback_renew_plan(
callback_query: types.CallbackQuery, session: Any
):
async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
plan, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
)
plan, client_id = callback_query.data.split("|")[1], callback_query.data.split("|")[2]
days_to_extend = 30 * int(plan)
gb_multiplier = {"1": 1, "3": 3, "6": 6, "12": 12}
@@ -505,70 +513,95 @@ async def process_callback_renew_plan(
current_time = datetime.utcnow().timestamp() * 1000
if expiry_time <= current_time:
new_expiry_time = int(
current_time + timedelta(days=days_to_extend).total_seconds() * 1000
)
new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000)
else:
new_expiry_time = int(
expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000
)
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
cost = RENEWAL_PLANS[plan]["price"]
balance = await get_balance(tg_id)
if balance < cost:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить баланс", callback_data="pay")
)
builder.row(
InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
required_amount = cost - balance
logger.info(f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}")
await save_temporary_data(
session,
tg_id,
"waiting_for_renewal_payment",
{
"plan": plan,
"client_id": client_id,
"cost": cost,
"required_amount": required_amount,
"new_expiry_time": new_expiry_time,
"total_gb": total_gb,
"email": email,
},
)
await callback_query.message.answer(
INSUFFICIENT_FUNDS_MSG,
reply_markup=builder.as_markup(),
)
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
logger.info(f"[RENEW] Запуск оплаты через Юкассу для пользователя {tg_id}")
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
logger.info(f"[RENEW] Запуск оплаты через Робокассу для пользователя {tg_id}")
await handle_custom_amount_input(callback_query, session)
else:
logger.info(f"[RENEW] Отправка сообщения о доплате пользователю {tg_id}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Пополните баланс на {required_amount}₽.",
reply_markup=builder.as_markup(),
)
return
response_message = SUCCESS_RENEWAL_MSG.format(
months=RENEWAL_PLANS[plan]["months"]
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await callback_query.message.answer(
response_message, reply_markup=builder.as_markup()
)
servers = await get_servers_from_db()
async def renew_key_on_servers():
tasks = []
for cluster_id in servers:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
await renew_key_on_servers()
logger.info(f"[RENEW] Средств достаточно. Продление ключа для пользователя {tg_id}")
await complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, plan)
else:
await callback_query.message.answer(KEY_NOT_FOUND_MSG)
logger.error(f"[RENEW] Ключ с client_id={client_id} не найден.")
except Exception as e:
logger.error(e)
logger.error(f"[RENEW] Ошибка при продлении ключа для пользователя {tg_id}: {e}")
async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, plan):
response_message = SUCCESS_RENEWAL_MSG.format(months=plan)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if callback_query:
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
else:
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
servers = await get_servers_from_db()
logger.info(f"[RENEW] Запуск продления ключа для пользователя {tg_id} на {plan} мес. на всех серверах.")
async def renew_key_on_servers():
tasks = []
for cluster_id in servers:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await renew_key_on_servers()
+2 -1
View File
@@ -1,11 +1,12 @@
import asyncio
import base64
from datetime import datetime
import aiohttp
import asyncpg
from aiohttp import web
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, TRANSITION_DATE_STR
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, TRANSITION_DATE_STR
from database import get_servers_from_db
from logger import logger
+3 -3
View File
@@ -4,6 +4,9 @@ from datetime import datetime, timedelta
from typing import Any
import pytz
from py3xui import AsyncApi
from client import add_client
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
@@ -12,9 +15,6 @@ from config import (
TOTAL_GB,
TRIAL_TIME,
)
from py3xui import AsyncApi
from client import add_client
from database import get_servers_from_db, store_key, use_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
+141 -270
View File
@@ -5,17 +5,19 @@ import asyncpg
from aiogram import Bot, Router, types
from aiogram.exceptions import TelegramForbiddenError
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
AUTO_DELETE_EXPIRED_KEYS,
AUTO_RENEW_KEYS,
DATABASE_URL,
DEV_MODE,
RENEWAL_PLANS,
TOTAL_GB,
TRIAL_TIME,
)
from py3xui import AsyncApi
from database import (
add_blocked_user,
add_notification,
@@ -124,119 +126,60 @@ async def notify_10h_keys(
logger.info(f"Найдено {len(records)} ключей для уведомления за 10 часов.")
async def process_record(record):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
for record in records:
await process_10h_record(record, bot, conn)
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}"
message = KEY_EXPIRY_10H.format(
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
price=RENEWAL_PLANS["1"]["price"],
)
balance = await get_balance(tg_id)
if balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id,
email,
record["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
""",
record["client_id"],
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
]
]
)
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
else:
try:
keyboard = InlineKeyboardBuilder()
keyboard.button(
text="🔄 Продлить VPN", callback_data=f"renew_key|{email}"
)
keyboard.button(text="💳 Пополнить баланс", callback_data="pay")
keyboard.button(text="👤 Личный кабинет", callback_data="profile")
keyboard.adjust(1)
await bot.send_message(
tg_id, message, reply_markup=keyboard.as_markup()
)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
await conn.execute(
"UPDATE keys SET notified = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(
f"Обновлено поле notified для клиента {record['client_id']}."
)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.debug(
f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
)
await asyncio.gather(*(process_record(record) for record in records))
logger.info("Обработка всех уведомлений за 10 часов завершена.")
async def process_10h_record(record, bot, conn):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
days_left_message = (
"Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}"
)
message = KEY_EXPIRY_10H.format(
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
price=RENEWAL_PLANS["1"]["price"],
)
balance = await get_balance(tg_id)
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
await conn.execute("UPDATE keys SET notified = TRUE WHERE client_id = $1", record["client_id"])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
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 send_renewal_notification(bot, tg_id, email, message, conn, record["client_id"], "notified")
async def notify_24h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -256,127 +199,76 @@ async def notify_24h_keys(
logger.info(f"Найдено {len(records_24h)} ключей для уведомления за 24 часа.")
async def process_record(record):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
for record in records_24h:
await process_24h_record(record, bot, conn)
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}"
message_24h = KEY_EXPIRY_24H.format(
email=email,
days_left_message=days_left_message,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
balance = await get_balance(tg_id)
if balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id,
email,
record["client_id"],
new_expiry_time,
TOTAL_GB,
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
await conn.execute(
"""
UPDATE keys
SET notified_24h = FALSE, notified = FALSE
WHERE client_id = $1
""",
record["client_id"],
)
keyboard = InlineKeyboardBuilder()
keyboard.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="profile"
)
)
await bot.send_message(
tg_id,
text=KEY_RENEWED,
reply_markup=keyboard.as_markup(),
)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
else:
try:
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f"renew_key|{email}",
)
)
builder.row(
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
types.InlineKeyboardButton(
text="👤 Личный кабинет",
callback_data="profile",
)
)
keyboard = builder.as_markup()
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except TelegramForbiddenError:
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}"
)
await conn.execute(
"UPDATE keys SET notified_24h = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.")
await asyncio.gather(*(process_record(record) for record in records_24h))
logger.info("Обработка всех уведомлений за 24 часа завершена.")
async def process_24h_record(record, bot, conn):
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
days_left_message = (
"Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}"
)
message_24h = KEY_EXPIRY_24H.format(
email=email,
days_left_message=days_left_message,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
balance = await get_balance(tg_id)
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
try:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(record["client_id"], new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.")
await conn.execute("UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", record["client_id"])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
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 send_renewal_notification(bot, tg_id, email, message_24h, conn, record["client_id"], "notified_24h")
async def send_renewal_notification(bot, tg_id, email, message, conn, client_id, flag):
try:
keyboard = InlineKeyboardBuilder()
keyboard.row(types.InlineKeyboardButton(text="🔄 Продлить VPN", callback_data=f"renew_key|{email}"))
keyboard.row(types.InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
keyboard.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await bot.send_message(tg_id, message, reply_markup=keyboard.as_markup())
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
await conn.execute(f"UPDATE keys SET {flag} = TRUE WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
logger.info("Проверка пользователей, не активировавших пробный период...")
@@ -461,9 +353,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
current_time,
)
logger.info(
f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает."
)
logger.info(f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает.")
for record in expiring_keys:
try:
@@ -499,22 +389,16 @@ async def process_key(record, bot, conn):
)
try:
if balance >= RENEWAL_PLANS["1"]["price"]:
if AUTO_RENEW_KEYS and 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
)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(client_id, new_expiry_time)
servers = await get_servers_from_db()
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id, email, client_id, new_expiry_time, TOTAL_GB
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
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(
"""
@@ -524,40 +408,27 @@ async def process_key(record, bot, conn):
""",
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}")
logger.info(f"Флаги notified сброшены для клиента {client_id}.")
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
else:
message_expired = "Ваша подписка истекла и была удалена. Получите новую через личный кабинет"
message_expired = "Ваша подписка истекла. Пополните баланс для продления."
await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard)
logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.")
try:
await bot.send_message(
tg_id, text=message_expired, reply_markup=keyboard
)
logger.info(
f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}."
)
except Exception as e:
logger.error(
f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}"
)
if AUTO_DELETE_EXPIRED_KEYS:
servers = await get_servers_from_db()
servers = await get_servers_from_db()
for cluster_id in servers:
await delete_key_from_cluster(cluster_id, email, client_id)
logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.")
for cluster_id in servers:
await delete_key_from_cluster(cluster_id, email, client_id)
logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.")
await delete_key(client_id)
logger.info(f"Ключ для клиента с ID {client_id} удален из базы данных.")
await delete_key(client_id)
logger.info(f"Ключ {client_id} удалён из базы данных.")
else:
logger.info(f"Ключ {client_id} НЕ был удалён (AUTO_DELETE_EXPIRED_KEYS=False).")
except Exception as e:
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
+1
View File
@@ -1,6 +1,7 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
+1
View File
@@ -1,6 +1,7 @@
__all__ = ("router",)
from aiogram import Router
from config import (
CRYPTO_BOT_ENABLE,
ROBOKASSA_ENABLE,
+57 -20
View File
@@ -1,26 +1,29 @@
import hashlib
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from robokassa import HashAlgorithm, Robokassa
from config import (
DATABASE_URL,
ROBOKASSA_ENABLE,
ROBOKASSA_LOGIN,
ROBOKASSA_PASSWORD1,
ROBOKASSA_PASSWORD2,
ROBOKASSA_TEST_MODE,
)
from robokassa import HashAlgorithm, Robokassa
from database import (
add_connection,
add_payment,
check_connection_exists,
get_key_count,
get_temporary_data,
update_balance,
)
from handlers.payments.utils import send_payment_success_notification
@@ -242,17 +245,31 @@ async def process_custom_amount_selection(
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
tg_id = message.chat.id
logger.info(f"User {tg_id} entered custom amount: {message.text}")
async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None):
if isinstance(message, types.CallbackQuery):
tg_id = message.message.chat.id
else:
tg_id = message.chat.id
logger.info(f"User {tg_id} initiated payment through ROBOKASSA")
inv_id = 0
try:
amount = int(message.text)
if amount <= 0:
raise ValueError("Сумма должна быть положительным числом.")
await state.update_data(amount=amount)
conn = await asyncpg.connect(DATABASE_URL)
user_data = await get_temporary_data(conn, tg_id)
await conn.close()
if not user_data:
await message.answer("Данные для оплаты не найдены. Попробуйте снова.")
return
state_type = user_data["state"]
amount = user_data["data"].get("required_amount", 0)
if amount <= 0:
await message.answer("Недостаточная сумма для пополнения.")
return
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
@@ -260,18 +277,38 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Оплатить", url=payment_url)],
[InlineKeyboardButton(text="💳 Оплатить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
]
)
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
)
await state.clear()
except ValueError as e:
logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}")
await message.answer(
text="Введите корректную сумму в рублях (целое положительное число)."
)
if state_type == "waiting_for_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
elif state_type == "waiting_for_renewal_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
else:
await message.answer("Некорректное состояние данных. Попробуйте снова.")
return
if isinstance(message, types.CallbackQuery):
await message.message.answer(
text=message_text,
reply_markup=confirm_keyboard,
)
else:
await message.answer(
text=message_text,
reply_markup=confirm_keyboard,
)
if isinstance(state, FSMContext):
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
error_message = "Произошла ошибка при создании платежа. Попробуйте позже."
if isinstance(message, types.CallbackQuery):
await message.message.answer(error_message)
else:
await message.answer(error_message)
+2074 -1185
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20 -10
View File
@@ -1,13 +1,13 @@
import os
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from typing import Any
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from database import get_balance, get_key_count, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
@@ -56,7 +56,7 @@ async def process_callback_view_profile(
)
if key_count == 0:
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>"
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение</i></pre>"
else:
profile_message += f"\n<pre> <i>{NEWS_MESSAGE}</i></pre>"
@@ -138,16 +138,25 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session:
FROM payments
WHERE tg_id = $1
ORDER BY created_at DESC
LIMIT 3
"""
records = await session.fetch(query, callback_query.from_user.id)
history_text = "📊 История операций с балансом:\n\n"
for record in records:
amount = record['amount']
payment_system = record['payment_system']
status = record['status']
date = record['created_at'].strftime('%Y-%m-%d %H:%M:%S')
history_text += f"<b>Сумма:</b> {amount}\n<b>Способ оплаты:</b> {payment_system}\n<b>Статус:</b> {status}\n<b>Дата:</b> {date}\n\n"
if records:
history_text = "📊 <b>Последние 3 операции с балансом:</b>\n\n"
for record in records:
amount = record['amount']
payment_system = record['payment_system']
status = record['status']
date = record['created_at'].strftime('%Y-%m-%d %H:%M:%S')
history_text += (
f"<b>Сумма:</b> {amount}\n"
f"<b>Способ оплаты:</b> {payment_system}\n"
f"<b>Статус:</b> {status}\n"
f"<b>Дата:</b> {date}\n\n"
)
else:
history_text = "❌ У вас пока нет операций с балансом."
await callback_query.message.answer(
history_text,
@@ -155,6 +164,7 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session:
)
@router.message(F.text == "/tariffs")
@router.callback_query(F.data == "view_tariffs")
async def view_tariffs_handler(callback_query: types.CallbackQuery):
+11 -6
View File
@@ -11,6 +11,7 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CHANNEL_EXISTS,
CHANNEL_URL,
@@ -21,7 +22,6 @@ from config import (
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
)
from database import (
add_connection,
add_referral,
@@ -200,17 +200,22 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL),
)
if CHANNEL_EXISTS:
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL)
)
else:
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)
)
if admin:
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn"))
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
+1 -1
View File
@@ -4,9 +4,9 @@ import re
import aiohttp
import asyncpg
from config import DATABASE_URL
from bot import bot
from config import DATABASE_URL
from database import get_servers_from_db
from logger import logger
+1
View File
@@ -3,6 +3,7 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
+1
View File
@@ -4,6 +4,7 @@ from typing import Any
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+1 -1
View File
@@ -5,10 +5,10 @@ from datetime import datetime, timedelta
import asyncpg
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from ping3 import ping
from bot import bot
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from database import get_servers_from_db
from logger import logger