Merge branch 'main' into dev

This commit is contained in:
Oleg Kraev
2025-01-24 23:35:19 +03:00
committed by GitHub
20 changed files with 5782 additions and 4444 deletions

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

+2 -2
View File
@@ -6,12 +6,12 @@
- [v1.4](https://github.com/Vladless/Solo_bot/releases/tag/v1.4) — бот для продажи ключей VLESS:
![](preview.jpg)
![](.github/images/preview_v1.jpg)
- [v2.3.1](https://github.com/Vladless/Solo_bot/releases/tag/v2.3.1) — стабильная версия подписок вместо ключей с
кнопками автодобавления в приложение:
![](preview_v_2.jpg)
![](.github/images/preview_v2.jpg)
- [v3.1](https://github.com/Vladless/Solo_bot/releases/tag/v3.1) — версия бота со значительным расширением возможностей.
+7 -7
View File
@@ -46,7 +46,7 @@ async def add_blocked_user(tg_id: int, conn: asyncpg.Connection):
async def init_db(file_path: str = "assets/schema.sql"):
with open(file_path) as file:
with open(file_path, mode="r") as file:
sql_content = file.read()
statements = [stmt.strip() for stmt in sql_content.split(";") if stmt.strip()]
@@ -792,18 +792,18 @@ async def get_referral_stats(referrer_tg_id: int):
WHERE rl.level < {MAX_REFERRAL_LEVELS}
)
SELECT
SUM(p.amount * CASE
{" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])}
ELSE 0
END) AS total_bonus
COALESCE(SUM(p.amount * (
CASE
{" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])}
ELSE 0
END)), 0) AS total_bonus
FROM referral_levels rl
JOIN payments p ON rl.referred_tg_id = p.tg_id
WHERE p.status = 'success'
WHERE p.status = 'success' AND rl.level <= {MAX_REFERRAL_LEVELS}
""",
referrer_tg_id,
)
total_referral_bonus = total_referral_bonus or 0
logger.debug(
f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}"
)
+1
View File
@@ -3,3 +3,4 @@ MY_GIFTS = "🎁 Мои подарки"
PROFILE = "👤 Личный кабинет"
BACK = "🔙 Назад"
GIFTS_ABOUT = "<b>Дарите подарки и следите, чтобы они дошли до адресата! 🎄</b>"
SHARE_GIFT = "🎁 Поделиться подарком"
+195 -21
View File
@@ -20,6 +20,7 @@ from config import (
RENEWAL_PRICES,
SUPPORT_CHAT_URL,
TRIAL_TIME,
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
)
from database import (
@@ -37,7 +38,7 @@ from handlers.buttons.add_subscribe import (
PC_BUTTON,
TV_BUTTON,
)
from handlers.keys.key_utils import create_key_on_cluster
from handlers.keys.key_utils import create_client_on_server, 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
@@ -132,21 +133,21 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
duration_days = int(plan_id) * 30
balance = await get_balance(tg_id)
await save_temporary_data(
session,
tg_id,
"waiting_for_payment",
{
"plan_id": plan_id,
"plan_price": plan_price,
"duration_days": duration_days,
"required_amount": max(0, plan_price - balance),
},
)
if balance < plan_price:
required_amount = plan_price - balance
await save_temporary_data(
session,
tg_id,
"waiting_for_payment",
{
"plan_id": plan_id,
"plan_price": plan_price,
"duration_days": duration_days,
"required_amount": required_amount,
},
)
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
@@ -166,9 +167,9 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
)
return
await update_balance(tg_id, -plan_price)
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
await create_key(tg_id, expiry_time, None, session, callback_query)
await update_balance(tg_id, -plan_price)
async def create_key(
@@ -182,9 +183,65 @@ async def create_key(
moscow_tz = pytz.timezone("Europe/Moscow")
expiry_time = expiry_time.astimezone(moscow_tz)
if USE_COUNTRY_SELECTION:
logger.info("[Country Selection] USE_COUNTRY_SELECTION включен.")
logger.info("[Country Selection] Получение наименее загруженного кластера.")
least_loaded_cluster = await get_least_loaded_cluster()
logger.info(f"[Country Selection] Наименее загруженный кластер: {least_loaded_cluster}")
logger.info(f"[Country Selection] Получение списка серверов для кластера {least_loaded_cluster}.")
servers = await session.fetch(
"SELECT server_name FROM servers WHERE cluster_name = $1",
least_loaded_cluster,
)
countries = [server["server_name"] for server in servers]
logger.info(f"[Country Selection] Список серверов: {countries}")
builder = InlineKeyboardBuilder()
for country in countries:
callback_data = f"select_country|{country}|{expiry_time.isoformat()}"
builder.row(
InlineKeyboardButton(
text=country, callback_data=callback_data
)
)
logger.info(f"[Country Selection] Добавлена кнопка для страны: {country} с callback_data: {callback_data}")
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="profile"))
logger.info("[Country Selection] Добавлена кнопка '🔙 Назад'.")
if isinstance(message_or_query, Message):
logger.info("[Country Selection] Сообщение пользователя - тип Message.")
await message_or_query.answer(
"🌍 Пожалуйста, выберите страну для вашего ключа:",
reply_markup=builder.as_markup(),
)
logger.info("[Country Selection] Сообщение отправлено с выбором страны.")
elif isinstance(message_or_query, CallbackQuery):
logger.info("[Country Selection] Сообщение пользователя - тип CallbackQuery.")
await message_or_query.message.answer(
"🌍 Пожалуйста, выберите страну для вашего ключа:",
reply_markup=builder.as_markup(),
)
logger.info("[Country Selection] Сообщение отправлено с выбором страны.")
elif tg_id is not None:
logger.info("[Country Selection] Использование tg_id для отправки сообщения.")
await bot.send_message(
chat_id=tg_id,
text="🌍 Пожалуйста, выберите страну для вашего ключа:",
reply_markup=builder.as_markup(),
)
logger.info(f"[Country Selection] Сообщение отправлено напрямую в чат {tg_id}.")
else:
logger.error("[Country Selection] Невозможно определить идентификатор чата. Сообщение не отправлено.")
logger.info("[Country Selection] Возврат из функции.")
return
while True:
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
logger.info(f"[Key Generation] Сгенерировано имя ключа: {key_name} для пользователя {tg_id}")
existing_key = await session.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
@@ -193,13 +250,11 @@ async def create_key(
)
if not existing_key:
break
logger.warning(
f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one."
)
logger.warning(f"[Key Generation] Имя ключа {key_name} уже существует. Генерация нового.")
client_id = str(uuid.uuid4())
email = key_name.lower()
expiry_timestamp = int(expiry_time.astimezone(moscow_tz).timestamp() * 1000)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
try:
@@ -218,7 +273,7 @@ async def create_key(
]
await asyncio.gather(*tasks)
logger.info(f"Key created on cluster {least_loaded_cluster} for user {tg_id}.")
logger.info(f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}")
await store_key(
tg_id,
@@ -229,9 +284,10 @@ async def create_key(
least_loaded_cluster,
session,
)
logger.info(f"[Database] Ключ сохранён в базе данных для пользователя {tg_id}")
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id} on cluster: {e}")
logger.error(f"[Error] Ошибка при создании ключа для пользователя {tg_id}: {e}")
error_message = "❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова."
if isinstance(message_or_query, Message):
@@ -271,3 +327,121 @@ async def create_key(
if state:
await state.clear()
logger.info(f"[FSM] Состояние пользователя {tg_id} очищено")
@router.callback_query(F.data.startswith("select_country|"))
async def handle_country_selection(callback_query: CallbackQuery, session: Any):
"""Обработчик выбора страны."""
data = callback_query.data.split("|")
selected_country = data[1]
expiry_time_str = data[2]
tg_id = callback_query.from_user.id
logger.info(f"Пользователь {tg_id} выбрал страну: {selected_country}")
logger.info(f"Получено время истечения: {expiry_time_str}")
try:
expiry_time = datetime.fromisoformat(expiry_time_str)
except ValueError:
logger.error(f"Ошибка преобразования времени истечения: {expiry_time_str}")
await callback_query.message.answer("❌ Некорректное время истечения. Попробуйте снова.")
return
await finalize_key_creation(tg_id, expiry_time, selected_country, None, session, callback_query)
async def finalize_key_creation(
tg_id: int,
expiry_time: datetime,
selected_country: str,
state: FSMContext | None,
session: Any,
callback_query: CallbackQuery,
):
"""Финализирует создание ключа с выбранной страной."""
moscow_tz = pytz.timezone("Europe/Moscow")
expiry_time = expiry_time.astimezone(moscow_tz)
while True:
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
existing_key = await session.fetchrow(
"SELECT * FROM keys WHERE email = $1 AND tg_id = $2",
key_name,
tg_id,
)
if not existing_key:
break
logger.warning(
f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one."
)
client_id = str(uuid.uuid4())
email = key_name.lower()
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
try:
server_info = await session.fetchrow(
"SELECT api_url, inbound_id, server_name FROM servers WHERE server_name = $1",
selected_country,
)
if not server_info:
raise ValueError(f"Сервер {selected_country} не найден.")
semaphore = asyncio.Semaphore(2)
await create_client_on_server(
server_info=server_info,
tg_id=tg_id,
client_id=client_id,
email=email,
expiry_timestamp=expiry_timestamp,
semaphore=semaphore,
)
logger.info(f"Key created on server {selected_country} for user {tg_id}.")
await store_key(
tg_id,
client_id,
email,
expiry_timestamp,
public_link,
selected_country,
session,
)
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id}: {e}")
await callback_query.message.answer("❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова.")
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"),
)
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
remaining_time = expiry_time - datetime.now(moscow_tz)
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
await callback_query.message.answer(key_message, reply_markup=builder.as_markup())
if state:
await state.clear()
+74 -45
View File
@@ -10,9 +10,9 @@ from handlers.utils import get_least_loaded_cluster
from logger import logger
async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_timestamp):
async def create_key_on_cluster(cluster_id: str, tg_id: int, client_id: str, email: str, expiry_timestamp: int):
"""
Создает ключ на всех серверах указанного кластера с одинаковым sub_id и уникальным email при активном SUPERNODE.
Создает ключ на всех серверах указанного кластера.
"""
try:
servers = await get_servers_from_db()
@@ -23,58 +23,87 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time
semaphore = asyncio.Semaphore(2)
async def create_client_on_server(server_info):
async with semaphore:
xui = AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
return
if SUPERNODE:
unique_email = f"{email}_{server_name.lower()}"
sub_id = email
else:
unique_email = email
sub_id = unique_email
await add_client(
xui,
client_id,
unique_email,
tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id
)
if SUPERNODE:
await asyncio.sleep(0.7)
if SUPERNODE:
for server_info in cluster:
await create_client_on_server(server_info)
await create_client_on_server(
server_info,
tg_id,
client_id,
email,
expiry_timestamp,
semaphore,
)
else:
await asyncio.gather(*(create_client_on_server(server) for server in cluster))
await asyncio.gather(
*(
create_client_on_server(
server,
tg_id,
client_id,
email,
expiry_timestamp,
semaphore,
)
for server in cluster
)
)
except Exception as e:
logger.error(f"Ошибка при создании ключа: {e}")
raise e
async def create_client_on_server(
server_info: dict,
tg_id: int,
client_id: str,
email: str,
expiry_timestamp: int,
semaphore: asyncio.Semaphore,
):
"""
Создает клиента на указанном сервере.
"""
async with semaphore:
xui = AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
return
if SUPERNODE:
unique_email = f"{email}_{server_name.lower()}"
sub_id = email
else:
unique_email = email
sub_id = unique_email
await add_client(
xui,
client_id,
unique_email,
tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id,
)
if SUPERNODE:
await asyncio.sleep(0.7)
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
try:
servers = await get_servers_from_db()
+96 -52
View File
@@ -4,6 +4,8 @@ import os
from datetime import datetime, timedelta
from typing import Any
import asyncpg
import pytz
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -13,6 +15,7 @@ from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
DATABASE_URL,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
ENABLE_DELETE_KEY_BUTTON,
@@ -20,6 +23,7 @@ from config import (
PUBLIC_LINK,
RENEWAL_PLANS,
TOTAL_GB,
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
)
from database import (
@@ -77,7 +81,9 @@ async def process_callback_or_message_view_keys(
try:
records = await session.fetch(
"""
SELECT email, client_id FROM keys WHERE tg_id = $1
SELECT email, client_id, expiry_time
FROM keys
WHERE tg_id = $1
""",
chat_id,
)
@@ -95,30 +101,50 @@ async def process_callback_or_message_view_keys(
def build_keys_response(records):
"""
Формирует сообщение и клавиатуру для устройств.
Формирует сообщение и клавиатуру для устройств с указанием срока действия подписки.
"""
builder = InlineKeyboardBuilder()
moscow_tz = pytz.timezone("Europe/Moscow")
if records:
response_message = "<b>🔑 Список ваших подписок:</b>\n\n"
for record in records:
key_name = record["email"]
expiry_time = record.get("expiry_time")
if expiry_time:
expiry_date_full = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz)
formatted_date_full = expiry_date_full.strftime(
"до %d %B %Y года, %H:%M"
).lower()
formatted_date_short = expiry_date_full.strftime("до %d %B").lower()
else:
formatted_date_full = "без срока действия"
formatted_date_short = "без срока действия"
button_text = f"{key_name} ({formatted_date_short})"
builder.row(
InlineKeyboardButton(
text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}"
text=button_text, callback_data=f"view_key|{key_name}"
)
)
response_message += f"• <b>{key_name}</b> ({formatted_date_full})\n"
else:
response_message = (
"<b>🔑 У вас пока нет подписок.</b>\n\n"
"Вы можете создать новую подписку для подключения устройств."
)
builder.row(
InlineKeyboardButton(text=" Добавить подписку", callback_data="create_key")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
inline_keyboard = builder.as_markup()
response_message = (
"<b>🔑 Список ваших подписок</b>\n\n"
"<i>👇 Выберите подписку для управления или добавьте новую для подключения дополнительного устройства:</i>"
)
return inline_keyboard, response_message
@@ -218,7 +244,6 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
),
)
# ✅ Добавлена проверка флага ENABLE_DELETE_KEY_BUTTON
if ENABLE_DELETE_KEY_BUTTON:
builder.row(
InlineKeyboardButton(
@@ -331,38 +356,27 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
callback_data=f"renew_plan|1|{client_id}",
for plan_id, plan_details in RENEWAL_PLANS.items():
months = plan_details["months"]
price = plan_details["price"]
discount = DISCOUNTS.get(plan_id, 0)
button_text = (
f'📅 {months} месяц{"а" if months > 1 else ""} ({price} руб.)'
+ (f' {discount}% скидка' if discount > 0 else "")
)
builder.row(
InlineKeyboardButton(
text=button_text,
callback_data=f"renew_plan|{months}|{client_id}",
)
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.) {DISCOUNTS["3"]}% скидка',
callback_data=f"renew_plan|3|{client_id}",
text="🔙 Назад", callback_data="view_keys"
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.) {DISCOUNTS["6"]}% скидка',
callback_data=f"renew_plan|6|{client_id}",
)
)
builder.row(
InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.) ({DISCOUNTS["12"]}% 🔥)',
callback_data=f"renew_plan|12|{client_id}",
)
)
back_button = InlineKeyboardButton(
text="🔙 Назад", callback_data="view_keys"
)
builder.row(back_button)
balance = await get_balance(tg_id)
response_message = PLAN_SELECTION_MSG.format(
@@ -528,28 +542,58 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
else:
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
servers = await get_servers_from_db()
conn = await asyncpg.connect(DATABASE_URL)
key_info = await conn.fetchrow(
"""
SELECT server_id
FROM keys
WHERE tg_id = $1 AND client_id = $2
""",
tg_id,
client_id,
)
logger.info(f"[RENEW] Запуск продления ключа для пользователя {tg_id} на {plan} мес. на всех серверах.")
if not key_info:
logger.error(f"[RENEW] Ключ с client_id {client_id} для пользователя {tg_id} не найден.")
await conn.close()
return
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)
server_id = key_info["server_id"]
await asyncio.gather(*tasks)
if USE_COUNTRY_SELECTION:
cluster_info = await conn.fetchrow(
"""
SELECT cluster_name
FROM servers
WHERE server_name = $1
""",
server_id,
)
if not cluster_info:
logger.error(f"[RENEW] Сервер {server_id} не найден в таблице servers.")
await conn.close()
return
cluster_id = cluster_info["cluster_name"]
else:
cluster_id = server_id
await conn.close()
logger.info(f"[RENEW] Запуск продления ключа для пользователя {tg_id} на {plan} мес. в кластере {cluster_id}.")
async def renew_key_on_cluster():
await renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
await update_balance(tg_id, -cost)
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await renew_key_on_servers()
await renew_key_on_cluster()
+98 -13
View File
@@ -1,10 +1,12 @@
import asyncio
import os
from datetime import datetime, timedelta
import pytz
import asyncpg
import pytz
from aiogram import Bot, Router, types
from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import BufferedInputFile
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
@@ -15,10 +17,10 @@ from config import (
AUTO_RENEW_KEYS,
DATABASE_URL,
DEV_MODE,
EXPIRED_KEYS_CHECK_INTERVAL,
RENEWAL_PLANS,
TOTAL_GB,
TRIAL_TIME,
EXPIRED_KEYS_CHECK_INTERVAL
)
from database import (
add_blocked_user,
@@ -184,11 +186,22 @@ async def process_10h_record(record, bot, conn):
await conn.execute("UPDATE keys SET notified = TRUE WHERE client_id = $1", record["client_id"])
image_path = os.path.join("img", "notify_10h.jpg")
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_10h.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
else:
await bot.send_message(tg_id, text=KEY_RENEWED.format(email=email), reply_markup=keyboard)
logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
except Exception as e:
@@ -197,6 +210,7 @@ async def process_10h_record(record, bot, conn):
await send_renewal_notification(bot, tg_id, email, message, conn, record["client_id"], "notified")
async def notify_24h_keys(
bot: Bot,
conn: asyncpg.Connection,
@@ -222,7 +236,6 @@ async def notify_24h_keys(
logger.info("Обработка всех уведомлений за 24 часа завершена.")
async def process_24h_record(record, bot, conn):
tg_id = record["tg_id"]
email = record["email"]
@@ -259,11 +272,22 @@ async def process_24h_record(record, bot, conn):
await conn.execute("UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", record["client_id"])
image_path = os.path.join("img", "notify_24h.jpg")
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]]
)
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"),
caption=KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
else:
await bot.send_message(tg_id, text=KEY_RENEWED.format(email=email), reply_markup=keyboard)
logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.")
except Exception as e:
@@ -279,7 +303,19 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id,
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())
image_path = os.path.join("img", "notify_24h.jpg")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"),
caption=message,
reply_markup=keyboard.as_markup(),
)
else:
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)
@@ -293,7 +329,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
inactive_trial_users = await conn.fetch(
"""
SELECT tg_id, username FROM users
SELECT tg_id, username, first_name, last_name FROM users
WHERE tg_id IN (
SELECT tg_id FROM connections
WHERE trial = 0
@@ -306,7 +342,16 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
for user in inactive_trial_users:
tg_id = user["tg_id"]
username = user.get("username", "Пользователь")
username = user["username"]
first_name = user["first_name"]
last_name = user["last_name"]
display_name = (
username
or first_name
or last_name
or "Пользователь"
)
try:
can_notify = await check_notification_time(
@@ -329,7 +374,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
keyboard = builder.as_markup()
message = (
f"👋 Привет, {username}!\n\n"
f"👋 Привет, {display_name}!\n\n"
f"🎉 У тебя есть бесплатный пробный период на {TRIAL_TIME} дней!\n"
"🕒 Не упусти возможность попробовать наш VPN прямо сейчас.\n\n"
"💡 Нажми на кнопку ниже, чтобы активировать пробный доступ."
@@ -365,7 +410,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
expiring_keys = await conn.fetch(
"""
SELECT tg_id, client_id, expiry_time, email FROM keys
SELECT tg_id, client_id, expiry_time, email, server_id FROM keys
WHERE expiry_time <= $1 AND expiry_time > $2
""",
threshold_time,
@@ -380,6 +425,26 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
except Exception as e:
logger.error(f"Ошибка при обработке подписки {record['client_id']}: {e}")
expired_keys = await conn.fetch(
"""
SELECT tg_id, client_id, email, server_id FROM keys
WHERE expiry_time <= $1
""",
current_time,
)
logger.info(f"Найдено {len(expired_keys)} истёкших ключей.")
for record in expired_keys:
try:
await delete_key_from_cluster(record["server_id"], record["email"], record["email"])
await conn.execute(
"DELETE FROM keys WHERE client_id = $1", record["client_id"]
)
logger.info(f"Удалён истёкший ключ {record['client_id']} пользователя {record['tg_id']}.")
except Exception as e:
logger.error(f"Ошибка при удалении истёкшего ключа {record['client_id']}: {e}")
async def process_key(record, bot, conn):
tg_id = record["tg_id"]
@@ -409,6 +474,8 @@ async def process_key(record, bot, conn):
]
)
image_path = os.path.join("img", "notify_expired.jpg")
try:
if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
@@ -433,15 +500,33 @@ async def process_key(record, bot, conn):
logger.info(f"Флаги notified сброшены для клиента {client_id}.")
try:
await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"),
caption = KEY_RENEWED.format(email=email),
reply_markup=keyboard,
)
else:
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:
message_expired = "Ваша подписка истекла. Пополните баланс для продления."
message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления."
try:
await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard)
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
tg_id,
photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"),
caption=message_expired,
reply_markup=keyboard,
)
else:
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}")
+2286 -1396
View File
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -92,12 +92,6 @@ async def process_callback_pay_robokassa(
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму",
callback_data="enter_custom_amount_robokassa",
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
key_count = await get_key_count(tg_id)
@@ -311,4 +305,4 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer
if isinstance(message, types.CallbackQuery):
await message.message.answer(error_message)
else:
await message.answer(error_message)
await message.answer(error_message)
+1632 -1504
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9
View File
@@ -100,6 +100,15 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
)
return await show_start_menu(message, admin, session)
await session.execute(
"""
INSERT INTO connections (tg_id, balance, trial)
VALUES ($1, 0, 1)
ON CONFLICT (tg_id) DO UPDATE SET trial = 1
""",
recipient_tg_id,
)
selected_months = gift_info["selected_months"]
expiry_time = gift_info["expiry_time"]
expiry_time_naive = expiry_time.replace(tzinfo=None)
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB