tariff customization/traffic for each tariff/ruff
This commit is contained in:
@@ -193,6 +193,20 @@ CREATE TABLE IF NOT EXISTS tracking_sources (
|
||||
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS source_code TEXT REFERENCES tracking_sources (code);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tariffs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
group_code TEXT NOT NULL,
|
||||
duration_days INTEGER NOT NULL CHECK (duration_days > 0),
|
||||
price_rub INTEGER NOT NULL CHECK (price_rub >= 0),
|
||||
traffic_limit BIGINT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE servers ADD COLUMN IF NOT EXISTS tariff_group TEXT;
|
||||
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
|
||||
+85
@@ -1504,3 +1504,88 @@ async def get_tracking_source_stats(code: str, session) -> dict:
|
||||
code,
|
||||
)
|
||||
return dict(result) if result else {}
|
||||
|
||||
|
||||
async def get_tariffs(session, tariff_id: int = None, group_code: str = None):
|
||||
"""
|
||||
Возвращает список всех тарифов или конкретный тариф по ID или группе.
|
||||
"""
|
||||
query = "SELECT * FROM tariffs"
|
||||
params = []
|
||||
|
||||
if tariff_id is not None:
|
||||
query += " WHERE id = $1"
|
||||
params = [tariff_id]
|
||||
elif group_code is not None:
|
||||
query += " WHERE group_code = $1"
|
||||
params = [group_code]
|
||||
|
||||
result = await session.fetch(query, *params) if params else await session.fetch(query)
|
||||
return [dict(record) for record in result]
|
||||
|
||||
|
||||
async def update_tariff(session, tariff_id: int, updates: dict):
|
||||
"""
|
||||
Обновляет указанные поля в тарифе по его ID.
|
||||
Пример updates: {"price_rub": 199, "is_active": False}
|
||||
"""
|
||||
if not updates:
|
||||
return False
|
||||
|
||||
set_clause = ", ".join(f"{key} = ${i + 2}" for i, key in enumerate(updates))
|
||||
values = list(updates.values())
|
||||
|
||||
query = f"""
|
||||
UPDATE tariffs
|
||||
SET {set_clause}, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"""
|
||||
await session.execute(query, tariff_id, *values)
|
||||
return True
|
||||
|
||||
|
||||
async def create_tariff(session, data: dict):
|
||||
"""
|
||||
Создаёт новый тариф. Ожидает словарь с полями таблицы (без id, created_at).
|
||||
"""
|
||||
keys = ", ".join(data.keys())
|
||||
values_placeholders = ", ".join(f"${i + 1}" for i in range(len(data)))
|
||||
values = list(data.values())
|
||||
|
||||
query = f"""
|
||||
INSERT INTO tariffs ({keys}, created_at, updated_at)
|
||||
VALUES ({values_placeholders}, NOW(), NOW())
|
||||
RETURNING *
|
||||
"""
|
||||
result = await session.fetchrow(query, *values)
|
||||
return dict(result) if result else None
|
||||
|
||||
|
||||
async def delete_tariff(session, tariff_id: int):
|
||||
"""
|
||||
Удаляет тариф по ID.
|
||||
"""
|
||||
query = "DELETE FROM tariffs WHERE id = $1"
|
||||
await session.execute(query, tariff_id)
|
||||
return True
|
||||
|
||||
|
||||
async def get_tariffs_for_cluster(session, cluster_name: str) -> list[dict]:
|
||||
row = await session.fetchrow(
|
||||
"SELECT tariff_group FROM servers WHERE cluster_name = $1 LIMIT 1",
|
||||
cluster_name,
|
||||
)
|
||||
if not row or not row["tariff_group"]:
|
||||
return []
|
||||
|
||||
group_code = row["tariff_group"]
|
||||
rows = await session.fetch(
|
||||
"SELECT * FROM tariffs WHERE group_code = $1 AND is_active = TRUE ORDER BY duration_days",
|
||||
group_code,
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def get_tariff_by_id(session, tariff_id: int) -> dict | None:
|
||||
row = await session.fetchrow("SELECT * FROM tariffs WHERE id = $1", tariff_id)
|
||||
return dict(row) if row else None
|
||||
|
||||
@@ -13,6 +13,7 @@ from .restart import router as restart_router
|
||||
from .sender import router as sender_router
|
||||
from .servers import router as servers_router
|
||||
from .stats import router as stats_router
|
||||
from .tariffs import router as tariffs_router
|
||||
from .users import router as users_router
|
||||
|
||||
|
||||
@@ -31,4 +32,5 @@ router.include_routers(
|
||||
restart_router,
|
||||
bans_router,
|
||||
ads_router,
|
||||
tariffs_router,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
@@ -18,7 +18,6 @@ from config import (
|
||||
DATABASE_URL,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
TOTAL_GB,
|
||||
USE_COUNTRY_SELECTION,
|
||||
)
|
||||
from database import check_unique_server_name, get_servers, update_key_expiry
|
||||
@@ -41,6 +40,7 @@ from .keyboard import (
|
||||
build_manage_cluster_kb,
|
||||
build_panel_type_kb,
|
||||
build_sync_cluster_kb,
|
||||
build_tariff_group_selection_kb,
|
||||
)
|
||||
|
||||
|
||||
@@ -563,9 +563,32 @@ async def handle_days_input(message: Message, state: FSMContext, session: Any):
|
||||
user_data = await state.get_data()
|
||||
cluster_name = user_data.get("cluster_name")
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
add_ms = days * 86400 * 1000
|
||||
total_gb = int((days / 30) * TOTAL_GB * 1024**3)
|
||||
|
||||
row = await session.fetchrow("SELECT tariff_group FROM servers WHERE cluster_name = $1 LIMIT 1", cluster_name)
|
||||
if not row or not row["tariff_group"]:
|
||||
await message.answer("❌ Не удалось определить тарифную группу для этого кластера.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
group_code = row["tariff_group"]
|
||||
|
||||
tariff = await session.fetchrow(
|
||||
"""
|
||||
SELECT * FROM tariffs
|
||||
WHERE group_code = $1 AND is_active = TRUE AND duration_days >= $2
|
||||
ORDER BY duration_days ASC
|
||||
LIMIT 1
|
||||
""",
|
||||
group_code,
|
||||
days,
|
||||
)
|
||||
if not tariff:
|
||||
await message.answer("❌ Нет активных тарифов, подходящих по сроку.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
total_gb = tariff["traffic_limit"] or 0
|
||||
|
||||
keys = await session.fetch(
|
||||
"SELECT tg_id, client_id, email, expiry_time FROM keys WHERE server_id = $1",
|
||||
@@ -578,7 +601,7 @@ async def handle_days_input(message: Message, state: FSMContext, session: Any):
|
||||
return
|
||||
|
||||
for key in keys:
|
||||
new_expiry = (key["expiry_time"] or now) + add_ms
|
||||
new_expiry = key["expiry_time"] + add_ms
|
||||
await renew_key_in_cluster(
|
||||
cluster_name,
|
||||
email=key["email"],
|
||||
@@ -588,14 +611,15 @@ async def handle_days_input(message: Message, state: FSMContext, session: Any):
|
||||
)
|
||||
await update_key_expiry(key["client_id"], new_expiry, session)
|
||||
|
||||
logger.info(f"[Cluster Extend] {key['email']} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}")
|
||||
|
||||
await message.answer(
|
||||
f"✅ Время подписки продлено на <b>{days} дней</b> всем пользователям в кластере <b>{cluster_name}</b>."
|
||||
)
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число дней.")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении дней: {e}")
|
||||
logger.error(f"[Cluster Extend] Ошибка при добавлении дней: {e}")
|
||||
await message.answer("❌ Произошла ошибка при продлении времени.")
|
||||
finally:
|
||||
await state.clear()
|
||||
@@ -855,3 +879,37 @@ async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMConte
|
||||
finally:
|
||||
await conn.close()
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.callback_query(AdminClusterCallback.filter(F.action == "set_tariff"), IsAdminFilter())
|
||||
async def show_tariff_group_selection(callback: CallbackQuery, callback_data: AdminClusterCallback, session):
|
||||
cluster_name = callback_data.data
|
||||
rows = await session.fetch(
|
||||
"SELECT DISTINCT group_code FROM tariffs WHERE group_code IS NOT NULL ORDER BY group_code"
|
||||
)
|
||||
groups = [r["group_code"] for r in rows]
|
||||
|
||||
if not groups:
|
||||
await callback.message.edit_text("❌ Нет доступных тарифных групп.")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"<b>💸 Выберите тарифную группу для кластера <code>{cluster_name}</code>:</b>",
|
||||
reply_markup=build_tariff_group_selection_kb(cluster_name, groups),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(AdminClusterCallback.filter(F.action == "apply_tariff_group"), IsAdminFilter())
|
||||
async def apply_tariff_group(callback: CallbackQuery, callback_data: AdminClusterCallback, session):
|
||||
try:
|
||||
cluster_name, group_code = callback_data.data.split("|", 1)
|
||||
except ValueError:
|
||||
await callback.message.edit_text("❌ Неверные данные.")
|
||||
return
|
||||
|
||||
await session.execute("UPDATE servers SET tariff_group = $1 WHERE cluster_name = $2", group_code, cluster_name)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✅ Для кластера <code>{cluster_name}</code> установлена тарифная группа: <b>{group_code}</b>",
|
||||
reply_markup=build_cluster_management_kb(cluster_name),
|
||||
)
|
||||
|
||||
@@ -2,8 +2,6 @@ from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from handlers.buttons import BACK
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
||||
from ..servers.keyboard import AdminServerCallback
|
||||
|
||||
@@ -96,6 +94,12 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
|
||||
callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(),
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="💸 Тариф: [установить/изменить]",
|
||||
callback_data=AdminClusterCallback(action="set_tariff", data=cluster_name).pack(),
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack()))
|
||||
|
||||
return builder.as_markup()
|
||||
@@ -130,3 +134,19 @@ def build_panel_type_kb() -> InlineKeyboardMarkup:
|
||||
builder.button(text="🌀 Remnawave", callback_data=AdminClusterCallback(action="panel_remnawave").pack())
|
||||
builder.row(build_admin_back_btn("clusters"))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_tariff_group_selection_kb(cluster_name: str, groups: list[str]) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
for group in groups:
|
||||
builder.button(
|
||||
text=group,
|
||||
callback_data=AdminClusterCallback(action="apply_tariff_group", data=f"{cluster_name}|{group}").pack(),
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад", callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack()
|
||||
)
|
||||
)
|
||||
builder.adjust(2, 1)
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -29,12 +29,13 @@ def build_panel_kb() -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()),
|
||||
InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="💸 Тарифы", callback_data=AdminPanelCallback(action="tariffs").pack()))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data=AdminPanelCallback(action="stats").pack()),
|
||||
InlineKeyboardButton(text="📈 Аналитика", callback_data=AdminPanelCallback(action="ads").pack()),
|
||||
)
|
||||
builder.button(text=MAIN_MENU, callback_data="profile")
|
||||
builder.adjust(1, 1, 2, 2, 2, 1)
|
||||
builder.adjust(1, 1, 2, 2, 1, 2, 1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from .tariffs_handler import router
|
||||
@@ -0,0 +1,85 @@
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback
|
||||
|
||||
|
||||
class AdminTariffCallback(CallbackData, prefix="tariff"):
|
||||
action: str
|
||||
|
||||
|
||||
def build_tariff_menu_kb() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text="🆕 Новый тариф", callback_data=AdminTariffCallback(action="create").pack()))
|
||||
builder.row(InlineKeyboardButton(text="📋 Мои тарифы", callback_data=AdminTariffCallback(action="list").pack()))
|
||||
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminPanelCallback(action="admin").pack()))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_cancel_kb() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[[InlineKeyboardButton(text="❌ Отменить", callback_data="cancel_tariff_creation")]]
|
||||
)
|
||||
|
||||
|
||||
def build_tariff_groups_kb(groups: list[str]) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
for group in groups:
|
||||
builder.button(text=group, callback_data=AdminTariffCallback(action=f"group|{group}").pack())
|
||||
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminPanelCallback(action="tariffs").pack()))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
if tariffs:
|
||||
group_code = tariffs[0]["group_code"]
|
||||
else:
|
||||
group_code = "unknown"
|
||||
|
||||
for t in tariffs:
|
||||
title = f"{t['name']} — {t['price_rub']}₽"
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=title, callback_data=AdminTariffCallback(action=f"view|{t['id']}").pack())
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="➕ Добавить тариф", callback_data=AdminTariffCallback(action=f"create|{group_code}").pack()
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminTariffCallback(action="list").pack()))
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_single_tariff_kb(tariff_id: int) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="✏️ Редактировать", callback_data=AdminTariffCallback(action=f"edit|{tariff_id}").pack()
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="🗑 Удалить", callback_data=AdminTariffCallback(action=f"delete|{tariff_id}").pack()
|
||||
),
|
||||
],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminTariffCallback(action="list").pack())],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def build_edit_tariff_fields_kb(tariff_id: int) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📝 Название", callback_data=f"edit_field|{tariff_id}|name")],
|
||||
[InlineKeyboardButton(text="📅 Длительность", callback_data=f"edit_field|{tariff_id}|duration_days")],
|
||||
[InlineKeyboardButton(text="💰 Цена", callback_data=f"edit_field|{tariff_id}|price_rub")],
|
||||
[InlineKeyboardButton(text="📦 Трафик (ГБ или 0)", callback_data=f"edit_field|{tariff_id}|traffic_limit")],
|
||||
[InlineKeyboardButton(text="🔘 Активность", callback_data=f"toggle_active|{tariff_id}")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view|{tariff_id}")],
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,329 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||
|
||||
from database import create_tariff
|
||||
from filters.admin import IsAdminFilter
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback
|
||||
from .keyboard import (
|
||||
AdminTariffCallback,
|
||||
build_cancel_kb,
|
||||
build_edit_tariff_fields_kb,
|
||||
build_single_tariff_kb,
|
||||
build_tariff_groups_kb,
|
||||
build_tariff_list_kb,
|
||||
build_tariff_menu_kb,
|
||||
)
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class TariffCreateState(StatesGroup):
|
||||
group = State()
|
||||
name = State()
|
||||
duration = State()
|
||||
price = State()
|
||||
traffic = State()
|
||||
confirm_more = State()
|
||||
|
||||
|
||||
class TariffEditState(StatesGroup):
|
||||
choosing_field = State()
|
||||
editing_value = State()
|
||||
|
||||
|
||||
@router.callback_query(AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter())
|
||||
async def handle_tariff_menu(callback_query: CallbackQuery):
|
||||
text = (
|
||||
"<b>💸 Управление тарифами</b>\n\n"
|
||||
"Здесь вы можете:\n"
|
||||
"• 🆕 Создать новый тариф (длительность, цена, лимит трафика)\n"
|
||||
"• 📋 Просмотреть и редактировать существующие тарифы"
|
||||
)
|
||||
await callback_query.message.edit_text(text=text, reply_markup=build_tariff_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action == "create"), IsAdminFilter())
|
||||
async def start_tariff_creation(callback: CallbackQuery, state: FSMContext):
|
||||
await state.set_state(TariffCreateState.group)
|
||||
await callback.message.edit_text(
|
||||
"📁 Введите <b>код группы</b>, в которую вы хотите добавить тариф.\n\n"
|
||||
"Например: <code>basic</code>, <code>vip</code>, <code>business</code>",
|
||||
reply_markup=build_cancel_kb(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(TariffCreateState.group, IsAdminFilter())
|
||||
async def process_tariff_group(message: Message, state: FSMContext):
|
||||
group_code = message.text.strip().lower()
|
||||
await state.update_data(group_code=group_code)
|
||||
await state.set_state(TariffCreateState.name)
|
||||
await message.answer(
|
||||
"📝 Введите <b>название тарифа</b>\n\n"
|
||||
"Например: <i>30 дней</i> или <i>1 месяц</i>\n\n"
|
||||
"<i>Это название будет отображаться пользователю при выборе тарифа</i>",
|
||||
reply_markup=build_cancel_kb(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(TariffCreateState.name, IsAdminFilter())
|
||||
async def process_tariff_name(message: Message, state: FSMContext):
|
||||
await state.update_data(name=message.text.strip())
|
||||
await state.set_state(TariffCreateState.duration)
|
||||
await message.answer(
|
||||
"📅 Введите <b>длительность тарифа в днях</b> (например: <i>30</i>):", reply_markup=build_cancel_kb()
|
||||
)
|
||||
|
||||
|
||||
@router.message(TariffCreateState.duration, IsAdminFilter())
|
||||
async def process_tariff_duration(message: Message, state: FSMContext):
|
||||
try:
|
||||
days = int(message.text.strip())
|
||||
if days <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное количество дней (целое число больше 0):")
|
||||
return
|
||||
|
||||
await state.update_data(duration_days=days)
|
||||
await state.set_state(TariffCreateState.price)
|
||||
await message.answer(
|
||||
"💰 Введите <b>цену тарифа в рублях</b> (например: <i>150</i>)\n\n"
|
||||
"<i>Будет показано клиенту при выборе тарифа</i>",
|
||||
reply_markup=build_cancel_kb(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(TariffCreateState.price, IsAdminFilter())
|
||||
async def process_tariff_price(message: Message, state: FSMContext):
|
||||
try:
|
||||
price = int(message.text.strip())
|
||||
if price < 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректную цену (целое число 0 или больше):")
|
||||
return
|
||||
|
||||
await state.update_data(price_rub=price)
|
||||
await state.set_state(TariffCreateState.traffic)
|
||||
await message.answer(
|
||||
"📦 Введите <b>лимит трафика в ГБ</b> (например: <i>100</i>, 0 — безлимит):", reply_markup=build_cancel_kb()
|
||||
)
|
||||
|
||||
|
||||
@router.message(TariffCreateState.traffic, IsAdminFilter())
|
||||
async def process_tariff_traffic(message: Message, state: FSMContext, session):
|
||||
try:
|
||||
traffic = int(message.text.strip())
|
||||
if traffic < 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректный лимит трафика (целое число 0 или больше):")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
data["traffic_limit"] = traffic * 1024**3 if traffic > 0 else None
|
||||
|
||||
new_tariff = await create_tariff(
|
||||
session,
|
||||
{
|
||||
"name": data["name"],
|
||||
"group_code": data["group_code"],
|
||||
"duration_days": data["duration_days"],
|
||||
"price_rub": data["price_rub"],
|
||||
"traffic_limit": data["traffic_limit"],
|
||||
},
|
||||
)
|
||||
|
||||
await state.set_state(TariffCreateState.confirm_more)
|
||||
await message.answer(
|
||||
f"✅ Тариф <b>{new_tariff['name']}</b> добавлен в группу <code>{data['group_code']}</code>.\n\n"
|
||||
"➕ Хотите добавить ещё один тариф в эту группу?",
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да", callback_data="add_more_tariff"),
|
||||
InlineKeyboardButton(text="❌ Нет", callback_data="done_tariff_group"),
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "add_more_tariff", IsAdminFilter())
|
||||
async def handle_add_more_tariff(callback: CallbackQuery, state: FSMContext):
|
||||
await state.set_state(TariffCreateState.name)
|
||||
await callback.message.edit_text("📝 Введите <b>название следующего тарифа</b>:", reply_markup=build_cancel_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "done_tariff_group", IsAdminFilter())
|
||||
async def handle_done_tariff_group(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
await callback.message.edit_text("✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "cancel_tariff_creation", IsAdminFilter())
|
||||
async def cancel_tariff_creation(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
await callback.message.edit_text("❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action == "list"), IsAdminFilter())
|
||||
async def show_tariff_groups(callback: CallbackQuery, session):
|
||||
rows = await session.fetch(
|
||||
"SELECT DISTINCT group_code FROM tariffs WHERE group_code IS NOT NULL ORDER BY group_code"
|
||||
)
|
||||
groups = [r["group_code"] for r in rows]
|
||||
|
||||
if not groups:
|
||||
await callback.message.edit_text("❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb())
|
||||
return
|
||||
|
||||
await callback.message.edit_text("<b>📋 Выберите тарифную группу:</b>", reply_markup=build_tariff_groups_kb(groups))
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter())
|
||||
async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTariffCallback, session):
|
||||
group_code = callback_data.action.split("|", 1)[1]
|
||||
rows = await session.fetch("SELECT * FROM tariffs WHERE group_code = $1 ORDER BY duration_days", group_code)
|
||||
tariffs = [dict(r) for r in rows]
|
||||
|
||||
if not tariffs:
|
||||
await callback.message.edit_text("❌ В этой группе пока нет тарифов.")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"<b>📦 Тарифы группы: {group_code}</b>", reply_markup=build_tariff_list_kb(tariffs)
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter())
|
||||
async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, session):
|
||||
tariff_id = int(callback_data.action.split("|", 1)[1])
|
||||
tariff = await session.fetchrow("SELECT * FROM tariffs WHERE id = $1", tariff_id)
|
||||
|
||||
if not tariff:
|
||||
await callback.message.edit_text("❌ Тариф не найден.")
|
||||
return
|
||||
|
||||
t = dict(tariff)
|
||||
traffic_text = f"{t['traffic_limit'] // 1024**3} ГБ" if t["traffic_limit"] else "Безлимит"
|
||||
|
||||
text = (
|
||||
f"<b>📄 Тариф: {t['name']}</b>\n\n"
|
||||
f"📁 Группа: <code>{t['group_code']}</code>\n"
|
||||
f"📅 Длительность: <b>{t['duration_days']} дней</b>\n"
|
||||
f"💰 Стоимость: <b>{t['price_rub']}₽</b>\n"
|
||||
f"📦 Трафик: <b>{traffic_text}</b>\n"
|
||||
f"{'✅ Активен' if t['is_active'] else '⛔ Отключен'}"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=build_single_tariff_kb(tariff_id))
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter())
|
||||
async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback):
|
||||
tariff_id = int(callback_data.action.split("|", 1)[1])
|
||||
await callback.message.edit_text(
|
||||
"⚠️ Вы уверены, что хотите <b>удалить</b> этот тариф?",
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"),
|
||||
InlineKeyboardButton(text="❌ Отмена", callback_data=f"view|{tariff_id}"),
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_delete_tariff|"), IsAdminFilter())
|
||||
async def delete_tariff(callback: CallbackQuery, session):
|
||||
tariff_id = int(callback.data.split("|", 1)[1])
|
||||
await session.execute("DELETE FROM tariffs WHERE id = $1", tariff_id)
|
||||
await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter())
|
||||
async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext):
|
||||
tariff_id = int(callback_data.action.split("|")[1])
|
||||
await state.update_data(tariff_id=tariff_id)
|
||||
await state.set_state(TariffEditState.choosing_field)
|
||||
await callback.message.edit_text(
|
||||
"<b>✏️ Что вы хотите изменить?</b>", reply_markup=build_edit_tariff_fields_kb(tariff_id)
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("edit_field|"), IsAdminFilter())
|
||||
async def ask_new_value(callback: CallbackQuery, state: FSMContext):
|
||||
_, _tariff_id, field = callback.data.split("|")
|
||||
await state.update_data(field=field)
|
||||
await state.set_state(TariffEditState.editing_value)
|
||||
|
||||
field_names = {
|
||||
"name": "название тарифа",
|
||||
"duration_days": "длительность в днях",
|
||||
"price_rub": "цену в рублях",
|
||||
"traffic_limit": "лимит трафика в ГБ (0 — безлимит)",
|
||||
}
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✏️ Введите новое значение для <b>{field_names.get(field, field)}</b>:", reply_markup=build_cancel_kb()
|
||||
)
|
||||
|
||||
|
||||
@router.message(TariffEditState.editing_value, IsAdminFilter())
|
||||
async def apply_edit(message: Message, state: FSMContext, session):
|
||||
data = await state.get_data()
|
||||
tariff_id = data["tariff_id"]
|
||||
field = data["field"]
|
||||
value = message.text.strip()
|
||||
|
||||
if field in ["duration_days", "price_rub", "traffic_limit"]:
|
||||
try:
|
||||
num = int(value)
|
||||
if num < 0:
|
||||
raise ValueError
|
||||
if field == "traffic_limit":
|
||||
value = num * 1024**3 if num > 0 else None
|
||||
else:
|
||||
value = num
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число.")
|
||||
return
|
||||
|
||||
await session.execute(f"UPDATE tariffs SET {field} = $1, updated_at = NOW() WHERE id = $2", value, tariff_id)
|
||||
await state.clear()
|
||||
await message.answer("✅ Тариф успешно обновлён.")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("toggle_active|"), IsAdminFilter())
|
||||
async def toggle_tariff_status(callback: CallbackQuery, session):
|
||||
tariff_id = int(callback.data.split("|")[1])
|
||||
row = await session.fetchrow("SELECT is_active FROM tariffs WHERE id = $1", tariff_id)
|
||||
|
||||
if not row:
|
||||
await callback.message.edit_text("❌ Тариф не найден.")
|
||||
return
|
||||
|
||||
new_status = not row["is_active"]
|
||||
await session.execute("UPDATE tariffs SET is_active = $1, updated_at = NOW() WHERE id = $2", new_status, tariff_id)
|
||||
|
||||
status_text = "✅ Тариф активирован." if new_status else "⛔ Тариф отключён."
|
||||
await callback.message.edit_text(status_text)
|
||||
|
||||
|
||||
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter())
|
||||
async def start_tariff_creation_existing_group(
|
||||
callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext
|
||||
):
|
||||
group_code = callback_data.action.split("|", 1)[1]
|
||||
await state.update_data(group_code=group_code)
|
||||
await state.set_state(TariffCreateState.name)
|
||||
await callback.message.edit_text(
|
||||
f"📦 Добавление нового тарифа в группу <code>{group_code}</code>\n\n📝 Введите <b>название тарифа</b>:",
|
||||
reply_markup=build_cancel_kb(),
|
||||
)
|
||||
@@ -1,11 +1,13 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import asyncpg
|
||||
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import RENEWAL_PRICES, TOTAL_GB, HWID_RESET_BUTTON
|
||||
from database import get_clusters
|
||||
from config import DATABASE_URL, HWID_RESET_BUTTON
|
||||
from database import get_clusters, get_tariffs, get_tariffs_for_cluster
|
||||
from handlers.buttons import BACK
|
||||
|
||||
from ..panel.keyboard import build_admin_back_btn
|
||||
@@ -77,29 +79,50 @@ def build_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup:
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup:
|
||||
async def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
for month, amount in RENEWAL_PRICES.items():
|
||||
builder.button(
|
||||
text=f"+ {amount}Р ({month} мес.)",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=amount).pack(),
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
tariffs = await get_tariffs(conn)
|
||||
for tariff in tariffs:
|
||||
months = tariff["duration_days"] // 30
|
||||
if months < 1:
|
||||
continue
|
||||
price = tariff["price_rub"]
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f"+ {price}₽ ({months} мес.)",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=price).pack(),
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=f"- {price}₽ ({months} мес.)",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=-price).pack(),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="💵 Добавить",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id).pack(),
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="💵 Вычесть",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_take", tg_id=tg_id).pack(),
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="💵 Установить баланс",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_set", tg_id=tg_id).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text=f"- {amount}Р ({month} мес.)",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=-amount).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="💵 Добавить", callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id).pack()
|
||||
)
|
||||
builder.button(
|
||||
text="💵 Вычесть", callback_data=AdminUserEditorCallback(action="users_balance_take", tg_id=tg_id).pack()
|
||||
)
|
||||
builder.button(
|
||||
text="💵 Установить баланс",
|
||||
callback_data=AdminUserEditorCallback(action="users_balance_set", tg_id=tg_id).pack(),
|
||||
)
|
||||
|
||||
builder.row(build_editor_back_btn(tg_id, True))
|
||||
builder.adjust(2, 2, 2, 2, 2, 1)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -112,33 +135,69 @@ def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
|
||||
async def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
for month in RENEWAL_PRICES.keys():
|
||||
month = int(month)
|
||||
builder.button(
|
||||
text=f"+ {month} мес.",
|
||||
callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=month).pack(),
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
record = await conn.fetchrow("SELECT server_id FROM keys WHERE email = $1", email)
|
||||
if not record:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Сервер не найден",
|
||||
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
|
||||
)
|
||||
)
|
||||
return builder.as_markup()
|
||||
|
||||
server_id = record["server_id"]
|
||||
|
||||
tariffs = await get_tariffs_for_cluster(conn, server_id)
|
||||
for tariff in tariffs:
|
||||
months = tariff["duration_days"] // 30
|
||||
if months < 1:
|
||||
continue
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f"+ {months} мес.",
|
||||
callback_data=AdminUserKeyEditorCallback(
|
||||
action="add", tg_id=tg_id, data=email, month=months
|
||||
).pack(),
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=f"- {months} мес.",
|
||||
callback_data=AdminUserKeyEditorCallback(
|
||||
action="add", tg_id=tg_id, data=email, month=-months
|
||||
).pack(),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⏳ Добавить дни",
|
||||
callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack(),
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="⏳ Вычесть дни",
|
||||
callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack(),
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⏳ Установить дату истечения",
|
||||
callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text=f"- {month} мес.",
|
||||
callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=-month).pack(),
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=BACK,
|
||||
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="⏳ Добавить дни", callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack()
|
||||
)
|
||||
builder.button(
|
||||
text="⏳ Вычесть дни", callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack()
|
||||
)
|
||||
builder.button(
|
||||
text="⏳ Установить дату истечения",
|
||||
callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text=BACK, # todo: fix magic text was set
|
||||
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
|
||||
)
|
||||
builder.adjust(2, 2, 2, 2, 2, 1)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -182,13 +241,12 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
|
||||
text="📊 Трафик",
|
||||
callback_data=AdminUserEditorCallback(action="users_traffic", data=email, tg_id=key_details["tg_id"]).pack(),
|
||||
)
|
||||
if TOTAL_GB > 0:
|
||||
builder.button(
|
||||
text="♻️ Сбросить трафик",
|
||||
callback_data=AdminUserEditorCallback(
|
||||
action="users_reset_traffic", data=email, tg_id=key_details["tg_id"]
|
||||
).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="♻️ Сбросить трафик",
|
||||
callback_data=AdminUserEditorCallback(
|
||||
action="users_reset_traffic", data=email, tg_id=key_details["tg_id"]
|
||||
).pack(),
|
||||
)
|
||||
if HWID_RESET_BUTTON:
|
||||
builder.button(
|
||||
text="💻 HWID",
|
||||
@@ -205,15 +263,11 @@ def build_hwid_menu_kb(email: str, tg_id: int) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(
|
||||
text="♻️ Сбросить HWID",
|
||||
callback_data=AdminUserEditorCallback(
|
||||
action="users_hwid_reset", data=email, tg_id=tg_id
|
||||
).pack(),
|
||||
callback_data=AdminUserEditorCallback(action="users_hwid_reset", data=email, tg_id=tg_id).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="🔙 Назад",
|
||||
callback_data=AdminUserEditorCallback(
|
||||
action="users_key_edit", data=email, tg_id=tg_id
|
||||
).pack(),
|
||||
callback_data=AdminUserEditorCallback(action="users_key_edit", data=email, tg_id=tg_id).pack(),
|
||||
)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -5,6 +5,7 @@ import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router, types
|
||||
@@ -14,7 +15,7 @@ from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import RENEWAL_PRICES, TOTAL_GB, USE_COUNTRY_SELECTION, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
|
||||
from config import DATABASE_URL, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, USE_COUNTRY_SELECTION
|
||||
from database import (
|
||||
delete_key,
|
||||
delete_user_data,
|
||||
@@ -22,6 +23,7 @@ from database import (
|
||||
get_client_id_by_email,
|
||||
get_key_details,
|
||||
get_servers,
|
||||
get_tariffs_for_cluster,
|
||||
set_user_balance,
|
||||
update_balance,
|
||||
update_key_expiry,
|
||||
@@ -38,8 +40,8 @@ from handlers.keys.key_utils import (
|
||||
)
|
||||
from handlers.utils import generate_random_email, sanitize_key_name
|
||||
from logger import logger
|
||||
from utils.csv_export import export_referrals_csv
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
from utils.csv_export import export_referrals_csv
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn, build_admin_back_kb
|
||||
from .keyboard import (
|
||||
@@ -47,6 +49,7 @@ from .keyboard import (
|
||||
AdminUserKeyEditorCallback,
|
||||
build_cluster_selection_kb,
|
||||
build_editor_kb,
|
||||
build_hwid_menu_kb,
|
||||
build_key_delete_kb,
|
||||
build_key_edit_kb,
|
||||
build_user_delete_kb,
|
||||
@@ -55,7 +58,6 @@ from .keyboard import (
|
||||
build_users_balance_kb,
|
||||
build_users_key_expiry_kb,
|
||||
build_users_key_show_kb,
|
||||
build_hwid_menu_kb
|
||||
)
|
||||
|
||||
|
||||
@@ -156,7 +158,9 @@ async def handle_hwid_reset(callback_query: CallbackQuery, callback_data: AdminU
|
||||
|
||||
devices = await api.get_user_hwid_devices(client_id)
|
||||
if not devices:
|
||||
await callback_query.message.edit_text("ℹ️ У пользователя нет привязанных устройств.", reply_markup=build_editor_kb(tg_id, True))
|
||||
await callback_query.message.edit_text(
|
||||
"ℹ️ У пользователя нет привязанных устройств.", reply_markup=build_editor_kb(tg_id, True)
|
||||
)
|
||||
return
|
||||
|
||||
deleted = 0
|
||||
@@ -331,7 +335,7 @@ async def handle_balance_change(callback_query: CallbackQuery, callback_data: Ad
|
||||
else:
|
||||
text += "\n <i>🚫 Отсутствуют</i>"
|
||||
|
||||
await callback_query.message.edit_text(text=text, reply_markup=build_users_balance_kb(tg_id))
|
||||
await callback_query.message.edit_text(text=text, reply_markup=await build_users_balance_kb(tg_id))
|
||||
|
||||
|
||||
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter())
|
||||
@@ -450,14 +454,10 @@ async def handle_key_edit(
|
||||
text += f"\n🏷️ Имя ключа: <b>{alias}</b>"
|
||||
|
||||
if not update or not callback_data.edit:
|
||||
await callback_query.message.edit_text(
|
||||
text=text,
|
||||
reply_markup=build_key_edit_kb(key_details, email)
|
||||
)
|
||||
await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_details, email))
|
||||
else:
|
||||
await callback_query.message.edit_text(
|
||||
text=text,
|
||||
reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email)
|
||||
text=text, reply_markup=await build_users_key_expiry_kb(callback_data.tg_id, email)
|
||||
)
|
||||
|
||||
|
||||
@@ -466,7 +466,7 @@ async def handle_change_expiry(callback_query: CallbackQuery, callback_data: Adm
|
||||
tg_id = callback_data.tg_id
|
||||
email = callback_data.data
|
||||
|
||||
await callback_query.message.edit_reply_markup(reply_markup=build_users_key_expiry_kb(tg_id, email))
|
||||
await callback_query.message.edit_reply_markup(reply_markup=await build_users_key_expiry_kb(tg_id, email))
|
||||
|
||||
|
||||
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter())
|
||||
@@ -763,14 +763,30 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
|
||||
if client_id is None:
|
||||
return ValueError(f"User with email {email} was not found")
|
||||
|
||||
server_id = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
|
||||
record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id)
|
||||
if not record:
|
||||
return ValueError(f"Key with client_id {client_id} was not found")
|
||||
|
||||
if not server_id:
|
||||
return ValueError(f"User with client_id {server_id} was not found")
|
||||
server_id = record["server_id"]
|
||||
servers = await session.fetch(
|
||||
"SELECT tariff_group FROM servers WHERE server_name = $1 OR cluster_name = $1 LIMIT 1", server_id
|
||||
)
|
||||
if not servers or not servers[0]["tariff_group"]:
|
||||
return ValueError(f"Tariff group not found for server_id={server_id}")
|
||||
|
||||
tariff_group = servers[0]["tariff_group"]
|
||||
tariffs = await session.fetch(
|
||||
"SELECT duration_days, traffic_limit FROM tariffs WHERE group_code = $1 AND is_active = TRUE ORDER BY duration_days",
|
||||
tariff_group,
|
||||
)
|
||||
if not tariffs:
|
||||
return ValueError(f"No tariffs found for group {tariff_group}")
|
||||
|
||||
added_days = max((expiry_time - int(time.time() * 1000)) / (1000 * 86400), 1)
|
||||
closest_tariff = min(tariffs, key=lambda t: abs(t["duration_days"] - added_days))
|
||||
total_gb = closest_tariff["traffic_limit"] or 0
|
||||
|
||||
clusters = await get_servers()
|
||||
added_days = max((expiry_time - int(time.time() * 1000)) / (1000 * 86400), 1)
|
||||
total_gb = int((added_days / 30) * TOTAL_GB * 1024**3)
|
||||
|
||||
async def update_key_on_all_servers():
|
||||
tasks = [
|
||||
@@ -785,7 +801,6 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
|
||||
)
|
||||
for cluster_name in clusters
|
||||
]
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
await update_key_on_all_servers()
|
||||
@@ -953,8 +968,28 @@ async def handle_create_key_country(callback_query: CallbackQuery, state: FSMCon
|
||||
await state.set_state(UserEditorState.selecting_duration)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for months, _ in RENEWAL_PRICES.items():
|
||||
builder.button(text=f"{months} мес.", callback_data=str(months))
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
row = await conn.fetchrow("SELECT cluster_name FROM servers WHERE server_name = $1", country)
|
||||
|
||||
if not row:
|
||||
await callback_query.message.edit_text("❌ Сервер не найден.")
|
||||
return
|
||||
|
||||
cluster_name = row["cluster_name"]
|
||||
await state.update_data(cluster_name=cluster_name)
|
||||
|
||||
tariffs = await get_tariffs_for_cluster(conn, cluster_name)
|
||||
|
||||
for tariff in tariffs:
|
||||
months = tariff["duration_days"] // 30
|
||||
if months < 1:
|
||||
continue
|
||||
builder.button(text=f"{months} мес.", callback_data=str(months))
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
builder.adjust(1)
|
||||
builder.row(build_admin_back_btn())
|
||||
|
||||
@@ -971,8 +1006,18 @@ async def handle_create_key_cluster(callback_query: CallbackQuery, state: FSMCon
|
||||
await state.set_state(UserEditorState.selecting_duration)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for months, _ in RENEWAL_PRICES.items():
|
||||
builder.button(text=f"{months} мес.", callback_data=str(months))
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
tariffs = await get_tariffs_for_cluster(conn, cluster_name)
|
||||
for tariff in tariffs:
|
||||
months = tariff["duration_days"] // 30
|
||||
if months < 1:
|
||||
continue
|
||||
builder.button(text=f"{months} мес.", callback_data=str(months))
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
builder.adjust(1)
|
||||
builder.row(build_admin_back_btn())
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ from aiogram import F, Router
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
TOTAL_GB,
|
||||
)
|
||||
from database import (
|
||||
get_key_details,
|
||||
)
|
||||
@@ -83,6 +80,7 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
|
||||
leftover = 0
|
||||
|
||||
new_expiry_time = now_ms + leftover
|
||||
|
||||
await session.execute(
|
||||
"""
|
||||
UPDATE keys
|
||||
@@ -95,8 +93,24 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
|
||||
record["tg_id"],
|
||||
client_id,
|
||||
)
|
||||
tariff = await session.fetchrow(
|
||||
"""
|
||||
SELECT t.traffic_limit
|
||||
FROM tariffs t
|
||||
JOIN servers s ON s.tariff_group = t.tariff_group
|
||||
WHERE s.server_name = $1
|
||||
ORDER BY t.duration_days DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
cluster_id,
|
||||
)
|
||||
|
||||
if not tariff or not tariff["traffic_limit"]:
|
||||
raise ValueError("Не удалось определить тариф для сервера")
|
||||
|
||||
base_bytes = int(tariff["traffic_limit"])
|
||||
added_days = max(leftover / (1000 * 86400), 0.01)
|
||||
total_gb = int((added_days / 30) * TOTAL_GB * 1024**3)
|
||||
total_gb = int((added_days / 30) * base_bytes)
|
||||
|
||||
await renew_key_in_cluster(
|
||||
cluster_id=cluster_id,
|
||||
|
||||
@@ -9,12 +9,7 @@ from aiogram.types import CallbackQuery, FSInputFile, InlineKeyboardButton, Mess
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot import bot
|
||||
from config import (
|
||||
CONNECT_PHONE_BUTTON,
|
||||
RENEWAL_PRICES,
|
||||
SUPPORT_CHAT_URL,
|
||||
DEFAULT_HWID_LIMIT
|
||||
)
|
||||
from config import CONNECT_PHONE_BUTTON, DEFAULT_HWID_LIMIT, SUPPORT_CHAT_URL
|
||||
from database import (
|
||||
get_key_details,
|
||||
get_trial,
|
||||
@@ -90,9 +85,10 @@ async def key_cluster_mode(
|
||||
if trial_status in [0, -1]:
|
||||
await update_trial(tg_id, 1, session)
|
||||
|
||||
if data.get("plan_id"):
|
||||
plan_price = RENEWAL_PRICES.get(data["plan_id"])
|
||||
await update_balance(tg_id, -plan_price, session)
|
||||
if data.get("tariff_id"):
|
||||
row = await session.fetchrow("SELECT price_rub FROM tariffs WHERE id = $1", data["tariff_id"])
|
||||
if row:
|
||||
await update_balance(tg_id, -row["price_rub"], session)
|
||||
|
||||
logger.info(f"[Database] Баланс обновлён для пользователя {tg_id}")
|
||||
|
||||
|
||||
@@ -18,12 +18,11 @@ from config import (
|
||||
ADMIN_USERNAME,
|
||||
CONNECT_PHONE_BUTTON,
|
||||
DATABASE_URL,
|
||||
DEFAULT_HWID_LIMIT,
|
||||
PUBLIC_LINK,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
RENEWAL_PRICES,
|
||||
SUPPORT_CHAT_URL,
|
||||
DEFAULT_HWID_LIMIT
|
||||
)
|
||||
from database import (
|
||||
add_user,
|
||||
@@ -427,9 +426,10 @@ async def finalize_key_creation(
|
||||
trial_status = await get_trial(tg_id, session)
|
||||
if trial_status in [0, -1]:
|
||||
await update_trial(tg_id, 1, session)
|
||||
if data.get("plan_id"):
|
||||
plan_price = RENEWAL_PRICES.get(data["plan_id"])
|
||||
await update_balance(tg_id, -plan_price, session)
|
||||
if data.get("tariff_id"):
|
||||
row = await session.fetchrow("SELECT price_rub FROM tariffs WHERE id = $1", data["tariff_id"])
|
||||
if row:
|
||||
await update_balance(tg_id, -row["price_rub"], session)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Key Finalize] Ошибка при создании ключа для пользователя {tg_id}: {e}")
|
||||
|
||||
@@ -10,19 +10,12 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
NOTIFY_EXTRA_DAYS,
|
||||
RENEWAL_PRICES,
|
||||
TRIAL_TIME,
|
||||
TRIAL_TIME_DISABLE,
|
||||
USE_COUNTRY_SELECTION,
|
||||
USE_NEW_PAYMENT_FLOW,
|
||||
)
|
||||
from database import (
|
||||
add_user,
|
||||
check_user_exists,
|
||||
create_temporary_data,
|
||||
get_balance,
|
||||
get_trial,
|
||||
)
|
||||
from database import add_user, check_user_exists, create_temporary_data, get_balance, get_tariffs_for_cluster, get_trial
|
||||
from handlers.buttons import (
|
||||
MAIN_MENU,
|
||||
PAYMENT,
|
||||
@@ -31,11 +24,10 @@ from handlers.payments.robokassa_pay import handle_custom_amount_input
|
||||
from handlers.payments.yookassa_pay import process_custom_amount_input
|
||||
from handlers.texts import (
|
||||
CREATING_CONNECTION_MSG,
|
||||
DISCOUNTS,
|
||||
INSUFFICIENT_FUNDS_MSG,
|
||||
SELECT_TARIFF_PLAN_MSG,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message
|
||||
from handlers.utils import edit_or_send_message, get_least_loaded_cluster
|
||||
from logger import logger
|
||||
|
||||
from .key_cluster_mode import key_cluster_mode
|
||||
@@ -81,61 +73,66 @@ async def handle_key_creation(
|
||||
await create_key(tg_id, expiry_time, state, session, message_or_query)
|
||||
return
|
||||
|
||||
cluster_name = await get_least_loaded_cluster()
|
||||
tariffs = await get_tariffs_for_cluster(session, cluster_name)
|
||||
|
||||
if not tariffs:
|
||||
await edit_or_send_message(
|
||||
target_message=message_or_query if isinstance(message_or_query, Message) else message_or_query.message,
|
||||
text="❌ Нет доступных тарифов для выбранного кластера.",
|
||||
reply_markup=None,
|
||||
)
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for index, (plan_id, price) in enumerate(RENEWAL_PRICES.items()):
|
||||
discount_text = ""
|
||||
if DISCOUNTS and plan_id in DISCOUNTS:
|
||||
discount_percentage = DISCOUNTS[plan_id]
|
||||
discount_text = f" ({discount_percentage}% скидка)"
|
||||
if index == len(RENEWAL_PRICES) - 1:
|
||||
discount_text = f" ({discount_percentage}% 🔥)"
|
||||
for t in tariffs:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f"📅 {plan_id} мес. - {price}₽{discount_text}",
|
||||
callback_data=f"select_plan_{plan_id}",
|
||||
text=f"{t['name']} — {t['price_rub']}₽",
|
||||
callback_data=f"select_tariff_plan|{t['id']}",
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
if isinstance(message_or_query, CallbackQuery):
|
||||
target_message = message_or_query.message
|
||||
else:
|
||||
target_message = message_or_query
|
||||
|
||||
target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text=SELECT_TARIFF_PLAN_MSG,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
)
|
||||
|
||||
await state.update_data(tg_id=tg_id)
|
||||
await state.set_state(Form.waiting_for_server_selection)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("select_plan_"))
|
||||
@router.callback_query(F.data.startswith("select_tariff_plan|"))
|
||||
async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state: FSMContext):
|
||||
tg_id = callback_query.message.chat.id
|
||||
plan_id = callback_query.data.split("_")[-1]
|
||||
plan_price = RENEWAL_PRICES.get(plan_id)
|
||||
if plan_price is None:
|
||||
await callback_query.message.answer("🚫 Неверный тарифный план.")
|
||||
tariff_id = int(callback_query.data.split("|")[1])
|
||||
|
||||
row = await session.fetchrow("SELECT * FROM tariffs WHERE id = $1", tariff_id)
|
||||
if not row:
|
||||
await callback_query.message.edit_text("❌ Указанный тариф не найден.")
|
||||
return
|
||||
duration_days = int(plan_id) * 30
|
||||
|
||||
tariff = dict(row)
|
||||
duration_days = tariff["duration_days"]
|
||||
price_rub = tariff["price_rub"]
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
if balance < plan_price:
|
||||
required_amount = plan_price - balance
|
||||
if balance < price_rub:
|
||||
required_amount = price_rub - balance
|
||||
await create_temporary_data(
|
||||
session,
|
||||
tg_id,
|
||||
"waiting_for_payment",
|
||||
{
|
||||
"plan_id": plan_id,
|
||||
"plan_price": plan_price,
|
||||
"tariff_id": tariff_id,
|
||||
"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":
|
||||
@@ -148,12 +145,11 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
|
||||
target_message=callback_query.message,
|
||||
text=INSUFFICIENT_FUNDS_MSG.format(required_amount=required_amount),
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
)
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text="⏳ Подождите...", callback_data="creating_key"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=CREATING_CONNECTION_MSG,
|
||||
@@ -161,8 +157,8 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
|
||||
)
|
||||
|
||||
expiry_time = datetime.now(moscow_tz) + timedelta(days=duration_days)
|
||||
await state.update_data(plan_id=plan_id)
|
||||
await create_key(tg_id, expiry_time, state, session, callback_query, plan=int(plan_id))
|
||||
await state.update_data(tariff_id=tariff_id)
|
||||
await create_key(tg_id, expiry_time, state, session, callback_query, plan=tariff_id)
|
||||
|
||||
|
||||
async def create_key(
|
||||
|
||||
+175
-150
@@ -10,8 +10,6 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from bot import bot
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
RENEWAL_PRICES,
|
||||
TOTAL_GB,
|
||||
USE_COUNTRY_SELECTION,
|
||||
USE_NEW_PAYMENT_FLOW,
|
||||
)
|
||||
@@ -35,7 +33,6 @@ from handlers.keys.key_utils import (
|
||||
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_RENEWAL_MSG,
|
||||
KEY_NOT_FOUND_MSG,
|
||||
PLAN_SELECTION_MSG,
|
||||
@@ -54,173 +51,201 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any
|
||||
key_name = callback_query.data.split("|")[1]
|
||||
try:
|
||||
record = await get_key_details(key_name, session)
|
||||
if record:
|
||||
client_id = record["client_id"]
|
||||
expiry_time = record["expiry_time"]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for plan_id, price in RENEWAL_PRICES.items():
|
||||
months = int(plan_id)
|
||||
|
||||
discount = DISCOUNTS.get(plan_id, 0) if isinstance(DISCOUNTS, dict) else 0
|
||||
|
||||
button_text = f"📅 {format_months(months)} ({price} руб.)"
|
||||
if discount > 0:
|
||||
button_text += f" {discount}% скидка"
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"renew_plan|{months}|{client_id}",
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record['email']}"))
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
|
||||
response_message = PLAN_SELECTION_MSG.format(
|
||||
balance=balance,
|
||||
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=response_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
)
|
||||
else:
|
||||
if not record:
|
||||
await callback_query.message.answer("<b>Ключ не найден.</b>")
|
||||
return
|
||||
|
||||
client_id = record["client_id"]
|
||||
expiry_time = record["expiry_time"]
|
||||
server_id = record["server_id"]
|
||||
|
||||
logger.info(f"[RENEW] Получение тарифной группы для server_id={server_id}")
|
||||
|
||||
row = await session.fetchrow(
|
||||
"""
|
||||
SELECT tariff_group FROM servers
|
||||
WHERE id::text = $1 OR server_name = $1 OR cluster_name = $1
|
||||
LIMIT 1
|
||||
""",
|
||||
server_id,
|
||||
)
|
||||
|
||||
if not row or not row["tariff_group"]:
|
||||
logger.warning(f"[RENEW] Тарифная группа не найдена для server_id={server_id}")
|
||||
await callback_query.message.answer("❌ Не удалось определить тарифную группу.")
|
||||
return
|
||||
|
||||
tariff_group = row["tariff_group"]
|
||||
logger.info(f"[RENEW] Найдена тарифная группа '{tariff_group}' для server_id={server_id}")
|
||||
|
||||
tariffs = await session.fetch(
|
||||
"""
|
||||
SELECT * FROM tariffs
|
||||
WHERE group_code = $1 AND is_active = TRUE
|
||||
ORDER BY duration_days
|
||||
""",
|
||||
tariff_group,
|
||||
)
|
||||
|
||||
if not tariffs:
|
||||
logger.warning(f"[RENEW] Нет активных тарифов для группы '{tariff_group}'")
|
||||
await callback_query.message.answer("❌ Нет доступных тарифов для этой группы.")
|
||||
return
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for t in tariffs:
|
||||
button_text = f"📅 {t['name']} — {t['price_rub']}₽"
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"renew_plan|{t['id']}|{client_id}",
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record['email']}"))
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
response_message = PLAN_SELECTION_MSG.format(
|
||||
balance=balance,
|
||||
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=response_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в process_callback_renew_key: {e}")
|
||||
logger.error(f"[RENEW] Ошибка в process_callback_renew_key для tg_id={tg_id}: {e}")
|
||||
await callback_query.message.answer("❌ Произошла ошибка при обработке. Попробуйте позже.")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("renew_plan|"))
|
||||
async def process_callback_renew_plan(callback_query: CallbackQuery, session: Any):
|
||||
tg_id = callback_query.message.chat.id
|
||||
plan, client_id = callback_query.data.split("|")[1], callback_query.data.split("|")[2]
|
||||
days_to_extend = 30 * int(plan)
|
||||
|
||||
total_gb = int((int(plan) or 1) * TOTAL_GB * 1024**3)
|
||||
tariff_id, client_id = callback_query.data.split("|")[1:]
|
||||
tariff_id = int(tariff_id)
|
||||
|
||||
try:
|
||||
tariff = await session.fetchrow("SELECT * FROM tariffs WHERE id = $1 AND is_active = TRUE", tariff_id)
|
||||
if not tariff:
|
||||
await callback_query.message.answer("❌ Тариф не найден или отключён.")
|
||||
return
|
||||
|
||||
duration_days = tariff["duration_days"]
|
||||
cost = tariff["price_rub"]
|
||||
total_gb = tariff["traffic_limit"] or 0
|
||||
|
||||
record = await get_key_by_server(tg_id, client_id, session)
|
||||
|
||||
if record:
|
||||
email = record["email"]
|
||||
expiry_time = record["expiry_time"]
|
||||
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)
|
||||
else:
|
||||
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
|
||||
|
||||
cost = RENEWAL_PRICES.get(plan)
|
||||
if cost is None:
|
||||
await callback_query.message.answer("❌ Неверный тарифный план.")
|
||||
return
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
|
||||
if balance < cost:
|
||||
required_amount = cost - balance
|
||||
|
||||
logger.info(
|
||||
f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}"
|
||||
)
|
||||
|
||||
await create_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,
|
||||
},
|
||||
)
|
||||
|
||||
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=PAYMENT, callback_data="pay"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount),
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
)
|
||||
return
|
||||
|
||||
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:
|
||||
if not record:
|
||||
await callback_query.message.answer(KEY_NOT_FOUND_MSG)
|
||||
logger.error(f"[RENEW] Ключ с client_id={client_id} не найден.")
|
||||
return
|
||||
|
||||
email = record["email"]
|
||||
expiry_time = record["expiry_time"]
|
||||
current_time = datetime.utcnow().timestamp() * 1000
|
||||
|
||||
if expiry_time <= current_time:
|
||||
new_expiry_time = int(current_time + timedelta(days=duration_days).total_seconds() * 1000)
|
||||
else:
|
||||
new_expiry_time = int(expiry_time + timedelta(days=duration_days).total_seconds() * 1000)
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
if balance < cost:
|
||||
required_amount = cost - balance
|
||||
logger.info(f"[RENEW] Недостаточно средств: {required_amount}₽")
|
||||
|
||||
await create_temporary_data(
|
||||
session,
|
||||
tg_id,
|
||||
"waiting_for_renewal_payment",
|
||||
{
|
||||
"tariff_id": tariff_id,
|
||||
"client_id": client_id,
|
||||
"cost": cost,
|
||||
"required_amount": required_amount,
|
||||
"new_expiry_time": new_expiry_time,
|
||||
"total_gb": total_gb,
|
||||
"email": email,
|
||||
},
|
||||
)
|
||||
|
||||
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(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"[RENEW] Продление ключа для пользователя {tg_id} на {duration_days} дней")
|
||||
await complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, tariff_id)
|
||||
|
||||
except Exception as 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):
|
||||
logger.info(f"[Info] Продление ключа {client_id} на {plan} мес. (Start)")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
response_message = SUCCESS_RENEWAL_MSG.format(months_formatted=format_months(int(plan)))
|
||||
|
||||
if callback_query:
|
||||
try:
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=response_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[Error] Ошибка при редактировании сообщения: {e}")
|
||||
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())
|
||||
|
||||
async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, tariff_id):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
key_info = await get_key_details(email, conn)
|
||||
if not key_info:
|
||||
logger.error(f"[Error] Ключ с client_id={client_id} не найден в БД.")
|
||||
await conn.close()
|
||||
return
|
||||
|
||||
server_id = key_info["server_id"]
|
||||
try:
|
||||
logger.info(f"[Info] Продление ключа {client_id} по тарифу ID={tariff_id} (Start)")
|
||||
|
||||
if USE_COUNTRY_SELECTION:
|
||||
cluster_info = await check_server_name_by_cluster(server_id, conn)
|
||||
if not cluster_info:
|
||||
logger.error(f"[Error] Сервер {server_id} не найден в таблице servers.")
|
||||
await conn.close()
|
||||
tariff = await conn.fetchrow("SELECT * FROM tariffs WHERE id = $1", tariff_id)
|
||||
if not tariff:
|
||||
logger.error(f"[Error] Тариф с id={tariff_id} не найден.")
|
||||
return
|
||||
cluster_id = cluster_info["cluster_name"]
|
||||
else:
|
||||
cluster_id = server_id
|
||||
|
||||
await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb)
|
||||
await update_key_expiry(client_id, new_expiry_time, conn)
|
||||
await update_balance(tg_id, -cost, conn)
|
||||
await conn.close()
|
||||
months_formatted = format_months(tariff["duration_days"] // 30)
|
||||
|
||||
logger.info(f"[Info] Продление ключа {client_id} завершено успешно (User: {tg_id})")
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
response_message = SUCCESS_RENEWAL_MSG.format(months_formatted=months_formatted)
|
||||
|
||||
if callback_query:
|
||||
try:
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=response_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[Error] Ошибка при редактировании сообщения: {e}")
|
||||
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())
|
||||
|
||||
key_info = await get_key_details(email, conn)
|
||||
if not key_info:
|
||||
logger.error(f"[Error] Ключ с client_id={client_id} не найден в БД.")
|
||||
return
|
||||
|
||||
server_id = key_info["server_id"]
|
||||
if USE_COUNTRY_SELECTION:
|
||||
cluster_info = await check_server_name_by_cluster(server_id, conn)
|
||||
if not cluster_info:
|
||||
logger.error(f"[Error] Сервер {server_id} не найден в таблице servers.")
|
||||
return
|
||||
cluster_id = cluster_info["cluster_name"]
|
||||
else:
|
||||
cluster_id = server_id
|
||||
|
||||
await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb)
|
||||
await update_key_expiry(client_id, new_expiry_time, conn)
|
||||
await update_balance(tg_id, -cost, conn)
|
||||
|
||||
logger.info(f"[Info] Продление ключа {client_id} завершено успешно (User: {tg_id})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Error] Ошибка в complete_key_renewal: {e}")
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
+55
-13
@@ -1,20 +1,18 @@
|
||||
import asyncio
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
from bot import bot
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
DEFAULT_HWID_LIMIT,
|
||||
LIMIT_IP,
|
||||
PUBLIC_LINK,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
SUPERNODE,
|
||||
TOTAL_GB,
|
||||
DEFAULT_HWID_LIMIT
|
||||
)
|
||||
from database import delete_notification, get_servers, store_key
|
||||
from handlers.utils import check_server_key_limit, get_least_loaded_cluster
|
||||
@@ -40,7 +38,7 @@ async def create_key_on_cluster(
|
||||
plan: int = None,
|
||||
session=None,
|
||||
remnawave_link: str = None,
|
||||
hwid_limit: Optional[int] = DEFAULT_HWID_LIMIT,
|
||||
hwid_limit: int | None = DEFAULT_HWID_LIMIT,
|
||||
):
|
||||
try:
|
||||
servers = await get_servers(include_enabled=True)
|
||||
@@ -66,12 +64,21 @@ async def create_key_on_cluster(
|
||||
|
||||
async with asyncpg.create_pool(DATABASE_URL) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
traffic_limit_bytes = None
|
||||
if plan is not None:
|
||||
tariff = await conn.fetchrow("SELECT traffic_limit FROM tariffs WHERE id = $1", plan)
|
||||
if not tariff:
|
||||
raise ValueError(f"Тариф с id={plan} не найден.")
|
||||
traffic_limit_bytes = int(tariff["traffic_limit"])
|
||||
|
||||
remnawave_servers = [
|
||||
s for s in enabled_servers
|
||||
s
|
||||
for s in enabled_servers
|
||||
if s.get("panel_type", "3x-ui").lower() == "remnawave" and await check_server_key_limit(s, conn)
|
||||
]
|
||||
xui_servers = [
|
||||
s for s in enabled_servers
|
||||
s
|
||||
for s in enabled_servers
|
||||
if s.get("panel_type", "3x-ui").lower() == "3x-ui" and await check_server_key_limit(s, conn)
|
||||
]
|
||||
|
||||
@@ -96,7 +103,6 @@ async def create_key_on_cluster(
|
||||
if not inbound_ids:
|
||||
logger.warning("Нет inbound_id у серверов Remnawave")
|
||||
else:
|
||||
traffic_limit_bytes = int((plan or 1) * TOTAL_GB * 1024**3)
|
||||
short_uuid = None
|
||||
if remnawave_link and "/" in remnawave_link:
|
||||
short_uuid = remnawave_link.rstrip("/").split("/")[-1]
|
||||
@@ -104,12 +110,14 @@ async def create_key_on_cluster(
|
||||
user_data = {
|
||||
"username": email,
|
||||
"trafficLimitStrategy": "NO_RESET",
|
||||
"trafficLimitBytes": traffic_limit_bytes,
|
||||
"expireAt": expire_at,
|
||||
"telegramId": tg_id,
|
||||
"activeUserInbounds": inbound_ids,
|
||||
}
|
||||
|
||||
if traffic_limit_bytes and traffic_limit_bytes > 0:
|
||||
user_data["trafficLimitBytes"] = traffic_limit_bytes
|
||||
|
||||
if short_uuid:
|
||||
user_data["shortUuid"] = short_uuid
|
||||
if hwid_limit is not None:
|
||||
@@ -171,7 +179,7 @@ async def create_client_on_server(
|
||||
plan: int = None,
|
||||
):
|
||||
"""
|
||||
Создает клиента на указанном сервере.
|
||||
Создает клиента на указанном 3x-ui сервере с лимитом по тарифу.
|
||||
"""
|
||||
async with semaphore:
|
||||
xui = await get_xui_instance(server_info["api_url"])
|
||||
@@ -190,7 +198,14 @@ async def create_client_on_server(
|
||||
unique_email = email
|
||||
sub_id = unique_email
|
||||
|
||||
total_gb_value = int((plan or 1) * TOTAL_GB * 1024**3)
|
||||
total_gb_value = 0
|
||||
if plan is not None:
|
||||
async with asyncpg.create_pool(DATABASE_URL) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
tariff = await conn.fetchrow("SELECT traffic_limit FROM tariffs WHERE id = $1", plan)
|
||||
if not tariff:
|
||||
raise ValueError(f"Тариф с id={plan} не найден.")
|
||||
total_gb_value = int(tariff["traffic_limit"])
|
||||
|
||||
await add_client(
|
||||
xui,
|
||||
@@ -212,7 +227,9 @@ async def create_client_on_server(
|
||||
await asyncio.sleep(0.7)
|
||||
|
||||
|
||||
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb, hwid_device_limit=DEFAULT_HWID_LIMIT):
|
||||
async def renew_key_in_cluster(
|
||||
cluster_id, email, client_id, new_expiry_time, total_gb, hwid_device_limit=DEFAULT_HWID_LIMIT
|
||||
):
|
||||
try:
|
||||
servers = await get_servers()
|
||||
cluster = servers.get(cluster_id)
|
||||
@@ -434,6 +451,18 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
|
||||
"activeUserInbounds": inbound_ids,
|
||||
}
|
||||
|
||||
async with asyncpg.create_pool(DATABASE_URL) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
group_id = remnawave_servers[0].get("tariff_group")
|
||||
if group_id is None:
|
||||
raise ValueError("У Remnawave-сервера отсутствует tariff_group")
|
||||
tariff = await conn.fetchrow(
|
||||
"SELECT traffic_limit FROM tariffs WHERE tariff_group = $1 ORDER BY traffic_limit_gb DESC LIMIT 1",
|
||||
group_id,
|
||||
)
|
||||
if tariff:
|
||||
user_data["trafficLimitBytes"] = int(tariff["traffic_limit"] * 1024**3)
|
||||
|
||||
result = await remna.create_user(user_data)
|
||||
if result:
|
||||
remnawave_client_id = result.get("uuid")
|
||||
@@ -464,12 +493,25 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
|
||||
sub_id = email
|
||||
unique_email = email
|
||||
|
||||
total_gb_bytes = 0
|
||||
async with asyncpg.create_pool(DATABASE_URL) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
group_id = server_info.get("tariff_group")
|
||||
if group_id is None:
|
||||
raise ValueError(f"У сервера {server_name} отсутствует tariff_group")
|
||||
tariff = await conn.fetchrow(
|
||||
"SELECT traffic_limit FROM tariffs WHERE tariff_group = $1 ORDER BY traffic_limit_gb DESC LIMIT 1",
|
||||
group_id,
|
||||
)
|
||||
if tariff:
|
||||
total_gb_bytes = int(tariff["traffic_limit_gb"] * 1024**3)
|
||||
|
||||
config = ClientConfig(
|
||||
client_id=remnawave_client_id,
|
||||
email=unique_email,
|
||||
tg_id=tg_id,
|
||||
limit_ip=LIMIT_IP,
|
||||
total_gb=TOTAL_GB,
|
||||
total_gb=total_gb_bytes,
|
||||
expiry_time=expiry_time,
|
||||
enable=True,
|
||||
flow="xtls-rprx-vision",
|
||||
|
||||
@@ -17,16 +17,12 @@ from config import (
|
||||
CONNECT_PHONE_BUTTON,
|
||||
ENABLE_DELETE_KEY_BUTTON,
|
||||
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
|
||||
HWID_RESET_BUTTON,
|
||||
QRCODE,
|
||||
TOGGLE_CLIENT,
|
||||
USE_COUNTRY_SELECTION,
|
||||
HWID_RESET_BUTTON
|
||||
)
|
||||
from database import (
|
||||
get_key_details,
|
||||
get_keys,
|
||||
get_servers
|
||||
)
|
||||
from database import get_key_details, get_keys, get_servers
|
||||
from handlers.buttons import (
|
||||
ADD_SUB,
|
||||
ALIAS,
|
||||
@@ -36,6 +32,7 @@ from handlers.buttons import (
|
||||
CONNECT_PHONE,
|
||||
DELETE,
|
||||
FREEZE,
|
||||
HWID_BUTTON,
|
||||
MAIN_MENU,
|
||||
PC_BUTTON,
|
||||
QR,
|
||||
@@ -43,7 +40,6 @@ from handlers.buttons import (
|
||||
RENEW_FULL,
|
||||
TV_BUTTON,
|
||||
UNFREEZE,
|
||||
HWID_BUTTON
|
||||
)
|
||||
from handlers.texts import (
|
||||
FROZEN_SUBSCRIPTION_MSG,
|
||||
@@ -196,7 +192,7 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
|
||||
return
|
||||
|
||||
is_frozen = record["is_frozen"]
|
||||
email = record["email"]
|
||||
record["email"]
|
||||
client_id = record.get("client_id")
|
||||
remnawave_link = record.get("remnawave_link")
|
||||
key = record.get("key")
|
||||
@@ -326,9 +322,7 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
|
||||
return
|
||||
|
||||
servers = await get_servers()
|
||||
remna_server = next(
|
||||
(srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None
|
||||
)
|
||||
remna_server = next((srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None)
|
||||
if not remna_server:
|
||||
await callback_query.answer("❌ Remnawave-сервер не найден.", show_alert=True)
|
||||
return
|
||||
|
||||
@@ -16,8 +16,6 @@ from config import (
|
||||
NOTIFY_MAXPRICE,
|
||||
NOTIFY_RENEW,
|
||||
NOTIFY_RENEW_EXPIRED,
|
||||
RENEWAL_PRICES,
|
||||
TOTAL_GB,
|
||||
TRIAL_TIME_DISABLE,
|
||||
)
|
||||
from database import (
|
||||
@@ -29,6 +27,7 @@ from database import (
|
||||
get_all_keys,
|
||||
get_balance,
|
||||
get_last_notification_time,
|
||||
get_tariffs_for_cluster,
|
||||
update_balance,
|
||||
update_key_expiry,
|
||||
)
|
||||
@@ -297,25 +296,22 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
|
||||
|
||||
|
||||
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: int, keys: list):
|
||||
"""
|
||||
Обрабатывает истекшие ключи, проверяя продление или удаление.
|
||||
"""
|
||||
logger.info("Начало обработки истекших ключей.")
|
||||
|
||||
expired_keys = [key for key in keys if key.get("expiry_time") and key.get("expiry_time") < current_time]
|
||||
expired_keys = [key for key in keys if key.get("expiry_time") and key["expiry_time"] < current_time]
|
||||
logger.info(f"Найдено {len(expired_keys)} истекших ключей.")
|
||||
|
||||
tg_ids = [key["tg_id"] for key in expired_keys]
|
||||
emails = [key.get("email", "") for key in expired_keys]
|
||||
|
||||
users = await check_notifications_bulk("key_expired", 0, conn, tg_ids=tg_ids, emails=emails)
|
||||
|
||||
messages = []
|
||||
|
||||
for key in expired_keys:
|
||||
tg_id = key["tg_id"]
|
||||
email = key.get("email", "")
|
||||
client_id = key.get("client_id")
|
||||
server_id = key.get("server_id")
|
||||
email = key["email"]
|
||||
client_id = key["client_id"]
|
||||
server_id = key["server_id"]
|
||||
notification_id = f"{email}_key_expired"
|
||||
|
||||
last_notification_time = await get_last_notification_time(tg_id, notification_id, session=conn)
|
||||
@@ -323,20 +319,17 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
if NOTIFY_RENEW_EXPIRED:
|
||||
try:
|
||||
balance = await get_balance(tg_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения баланса для пользователя {tg_id}: {e}")
|
||||
continue
|
||||
|
||||
renewal_period_months = 1
|
||||
renewal_cost = RENEWAL_PRICES[str(renewal_period_months)]
|
||||
tariffs = await get_tariffs_for_cluster(conn, server_id)
|
||||
tariff = tariffs[0] if tariffs else None
|
||||
|
||||
if balance >= renewal_cost:
|
||||
try:
|
||||
if tariff and balance >= tariff["price_rub"]:
|
||||
await process_auto_renew_or_notify(
|
||||
bot, conn, key, notification_id, 1, "notify_expired.jpg", KEY_RENEWED_TEMP_MSG
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка авто-продления для пользователя {tg_id}: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка авто-продления для пользователя {tg_id}: {e}")
|
||||
continue
|
||||
|
||||
if NOTIFY_DELETE_KEY:
|
||||
@@ -375,16 +368,12 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
if NOTIFY_DELETE_DELAY > 0:
|
||||
hours = NOTIFY_DELETE_DELAY // 60
|
||||
minutes = NOTIFY_DELETE_DELAY % 60
|
||||
|
||||
if hours > 0:
|
||||
if minutes > 0:
|
||||
delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format(
|
||||
email=email, hours_formatted=format_hours(hours), minutes_formatted=format_minutes(minutes)
|
||||
)
|
||||
else:
|
||||
delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format(
|
||||
email=email, hours_formatted=format_hours(hours)
|
||||
)
|
||||
if hours > 0 and minutes > 0:
|
||||
delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format(
|
||||
email=email, hours_formatted=format_hours(hours), minutes_formatted=format_minutes(minutes)
|
||||
)
|
||||
elif hours > 0:
|
||||
delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format(email=email, hours_formatted=format_hours(hours))
|
||||
else:
|
||||
delay_message = KEY_EXPIRED_DELAY_MINUTES_MSG.format(
|
||||
email=email, minutes_formatted=format_minutes(minutes)
|
||||
@@ -405,16 +394,15 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
results = await send_messages_with_limit(bot, messages, conn=conn)
|
||||
sent_count = 0
|
||||
for msg, result in zip(messages, results, strict=False):
|
||||
tg_id = msg["tg_id"]
|
||||
email = msg["email"]
|
||||
if result:
|
||||
await add_notification(tg_id, msg["notification_id"], session=conn)
|
||||
await add_notification(msg["tg_id"], msg["notification_id"], session=conn)
|
||||
sent_count += 1
|
||||
logger.info(f"📢 Отправлено уведомление об истекшем ключе для подписки {email} пользователю {tg_id}.")
|
||||
logger.info(f"📢 Уведомление об истекшем ключе {msg['email']} отправлено пользователю {msg['tg_id']}.")
|
||||
else:
|
||||
logger.warning(
|
||||
f"📢 Не удалось отправить уведомление об истекшем ключе для подписки {email} пользователю {tg_id}."
|
||||
f"📢 Не удалось отправить уведомление об истекшем ключе {msg['email']} пользователю {msg['tg_id']}."
|
||||
)
|
||||
|
||||
logger.info(f"Отправлено {sent_count} уведомлений об истекших ключах.")
|
||||
|
||||
logger.info("Обработка истекших ключей завершена.")
|
||||
@@ -424,10 +412,6 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
async def process_auto_renew_or_notify(
|
||||
bot, conn, key: dict, notification_id: str, renewal_period_months: int, standard_photo: str, standard_caption: str
|
||||
):
|
||||
"""
|
||||
Если баланс пользователя позволяет, продлевает ключ на максимальный возможный срок и списывает средства;
|
||||
иначе отправляет стандартное уведомление.
|
||||
"""
|
||||
tg_id = key.get("tg_id")
|
||||
email = key.get("email", "")
|
||||
renew_notification_id = f"{email}_renew"
|
||||
@@ -441,65 +425,58 @@ async def process_auto_renew_or_notify(
|
||||
return
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения данных для пользователя {tg_id}: {e}")
|
||||
return
|
||||
|
||||
if NOTIFY_MAXPRICE:
|
||||
renewal_period_months = max(
|
||||
(int(months) for months, price in RENEWAL_PRICES.items() if balance >= price), default=None
|
||||
)
|
||||
else:
|
||||
renewal_period_months = 1 if balance >= RENEWAL_PRICES["1"] else None
|
||||
|
||||
if renewal_period_months:
|
||||
renewal_period_months = int(renewal_period_months)
|
||||
renewal_cost = RENEWAL_PRICES[str(renewal_period_months)]
|
||||
client_id = key.get("client_id")
|
||||
server_id = key.get("server_id")
|
||||
current_expiry = key.get("expiry_time")
|
||||
new_expiry_time = current_expiry + renewal_period_months * 30 * 24 * 3600 * 1000
|
||||
|
||||
formatted_expiry_date = datetime.fromtimestamp(new_expiry_time / 1000, moscow_tz).strftime("%d %B %Y, %H:%M")
|
||||
total_gb = int(renewal_period_months * TOTAL_GB * 1024**3)
|
||||
tariffs = await get_tariffs_for_cluster(conn, server_id)
|
||||
if not tariffs:
|
||||
logger.warning(f"⛔ Нет доступных тарифов для продления подписки {email} (сервер: {server_id})")
|
||||
return
|
||||
|
||||
if NOTIFY_MAXPRICE:
|
||||
suitable_tariffs = [t for t in tariffs if balance >= t["price_rub"]]
|
||||
selected_tariff = suitable_tariffs[-1] if suitable_tariffs else None
|
||||
else:
|
||||
selected_tariff = tariffs[0] if balance >= tariffs[0]["price_rub"] else None
|
||||
|
||||
if not selected_tariff:
|
||||
keyboard = build_notification_kb(email)
|
||||
await add_notification(tg_id, notification_id, session=conn)
|
||||
await send_notification(bot, tg_id, standard_photo, standard_caption, keyboard)
|
||||
return
|
||||
|
||||
client_id = key.get("client_id")
|
||||
current_expiry = key.get("expiry_time")
|
||||
duration_days = selected_tariff["duration_days"]
|
||||
renewal_cost = selected_tariff["price_rub"]
|
||||
traffic_limit = selected_tariff["traffic_limit"]
|
||||
total_gb = traffic_limit if traffic_limit else 0
|
||||
|
||||
new_expiry_time = (
|
||||
current_expiry
|
||||
if current_expiry > datetime.utcnow().timestamp() * 1000
|
||||
else datetime.utcnow().timestamp() * 1000
|
||||
) + duration_days * 24 * 60 * 60 * 1000
|
||||
|
||||
formatted_expiry_date = datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz).strftime("%d %B %Y, %H:%M")
|
||||
|
||||
logger.info(
|
||||
f"Продление подписки {email} на {renewal_period_months} мес. для пользователя {tg_id}. Баланс: {balance}, списываем: {renewal_cost}"
|
||||
)
|
||||
|
||||
try:
|
||||
await renew_key_in_cluster(server_id, email, client_id, new_expiry_time, total_gb)
|
||||
await update_balance(tg_id, -renewal_cost, session=conn)
|
||||
await update_key_expiry(client_id, new_expiry_time, conn)
|
||||
await renew_key_in_cluster(server_id, email, client_id, int(new_expiry_time), total_gb)
|
||||
await update_balance(tg_id, -renewal_cost, session=conn)
|
||||
await update_key_expiry(client_id, int(new_expiry_time), conn)
|
||||
await add_notification(tg_id, renew_notification_id, session=conn)
|
||||
await delete_notification(tg_id, notification_id, session=conn)
|
||||
|
||||
await add_notification(tg_id, renew_notification_id, session=conn)
|
||||
await delete_notification(tg_id, notification_id, session=conn)
|
||||
renewed_message = KEY_RENEWED.format(email=email, months=duration_days // 30, expiry_date=formatted_expiry_date)
|
||||
|
||||
logger.info(
|
||||
f"✅ Ключ {client_id} продлён на {renewal_period_months} мес. для пользователя {tg_id}. Списано {renewal_cost}."
|
||||
)
|
||||
|
||||
renewed_message = KEY_RENEWED.format(
|
||||
email=email, months=renewal_period_months, expiry_date=formatted_expiry_date
|
||||
)
|
||||
|
||||
keyboard = build_notification_expired_kb()
|
||||
result = await send_notification(bot, tg_id, "notify_expired.jpg", renewed_message, keyboard)
|
||||
if result:
|
||||
logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.")
|
||||
else:
|
||||
logger.warning(
|
||||
f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}."
|
||||
)
|
||||
except KeyError as e:
|
||||
logger.error(f"❌ Ошибка форматирования сообщения KEY_RENEWED: отсутствует ключ {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при продлении ключа {client_id} для пользователя {tg_id}: {e}")
|
||||
else:
|
||||
keyboard = build_notification_kb(email)
|
||||
await add_notification(tg_id, notification_id, session=conn)
|
||||
result = await send_notification(bot, tg_id, standard_photo, standard_caption, keyboard)
|
||||
keyboard = build_notification_expired_kb()
|
||||
result = await send_notification(bot, tg_id, "notify_expired.jpg", renewed_message, keyboard)
|
||||
if result:
|
||||
logger.info(f"📢 Отправлено уведомление об истекающей подписке {email} пользователю {tg_id}.")
|
||||
logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.")
|
||||
else:
|
||||
logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.")
|
||||
logger.warning(f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка в process_auto_renew_or_notify: {e}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
-25
@@ -105,7 +105,7 @@ async def check_subscription_callback(callback_query: CallbackQuery, state: FSMC
|
||||
session=session,
|
||||
admin=admin,
|
||||
text_to_process=original_text,
|
||||
user_data=user_data
|
||||
user_data=user_data,
|
||||
)
|
||||
logger.info(f"[CALLBACK] Завершен вызов process_start_logic для пользователя {user_id}")
|
||||
except Exception as e:
|
||||
@@ -113,23 +113,6 @@ async def check_subscription_callback(callback_query: CallbackQuery, state: FSMC
|
||||
await callback_query.answer(SUBSCRIPTION_CHECK_ERROR_MSG, show_alert=True)
|
||||
|
||||
|
||||
async def process_start_logic(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
session: Any,
|
||||
admin: bool,
|
||||
text_to_process: str = None,
|
||||
user_data: dict | None = None,
|
||||
):
|
||||
text = text_to_process or message.text or message.caption
|
||||
user_data = user_data or {
|
||||
"tg_id": (message.from_user or message.chat).id,
|
||||
"username": getattr(message.from_user, "username", None),
|
||||
"first_name": getattr(message.from_user, "first_name", None),
|
||||
"last_name": getattr(message.from_user, "last_name", None),
|
||||
"language_code": getattr(message.from_user, "language_code", None),
|
||||
"is_bot": getattr(message.from_user, "is_bot", False),
|
||||
}
|
||||
|
||||
async def process_start_logic(
|
||||
message: Message,
|
||||
@@ -139,10 +122,6 @@ async def process_start_logic(
|
||||
text_to_process: str = None,
|
||||
user_data: dict | None = None,
|
||||
):
|
||||
from config import CHANNEL_EXISTS, CHANNEL_REQUIRED, CHANNEL_ID, CHANNEL_URL
|
||||
from handlers.texts import SUBSCRIPTION_REQUIRED_MSG
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
|
||||
user_data = user_data or {
|
||||
"tg_id": (message.from_user or message.chat).id,
|
||||
@@ -229,7 +208,7 @@ async def process_start_logic(
|
||||
logger.info(f"[UTM] Обнаружена ссылка на UTM: {utm_code}")
|
||||
await handle_utm_link(utm_code, message, state, session, user_data=user_data)
|
||||
continue
|
||||
|
||||
|
||||
await state.clear()
|
||||
if gift_detected:
|
||||
return
|
||||
@@ -247,7 +226,7 @@ async def process_start_logic(
|
||||
await show_start_menu(message, admin, session)
|
||||
else:
|
||||
await show_start_menu(message, admin, session)
|
||||
|
||||
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
@@ -342,4 +321,4 @@ async def handle_about_vpn(callback_query: CallbackQuery, session: Any):
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=image_path,
|
||||
force_text=False,
|
||||
)
|
||||
)
|
||||
|
||||
Binary file not shown.
+3
-1
@@ -1,9 +1,11 @@
|
||||
from aiohttp.web_urldispatcher import UrlDispatcher
|
||||
|
||||
import bot
|
||||
|
||||
from config import TBLOCKER_WEBHOOK_PATH
|
||||
|
||||
from .tblocker import tblocker_webhook
|
||||
|
||||
|
||||
async def register_web_routes(router: UrlDispatcher) -> None:
|
||||
dp = bot.dp
|
||||
router.add_post(TBLOCKER_WEBHOOK_PATH, tblocker_webhook)
|
||||
|
||||
+33
-52
@@ -1,34 +1,30 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from datetime import datetime
|
||||
|
||||
import asyncpg
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
||||
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiohttp import web
|
||||
|
||||
from bot import bot
|
||||
from config import BLOCK_DURATION, DATABASE_URL, SERVER_COUNTRIES, TIMESTAMP_TTL
|
||||
from database import get_key_details
|
||||
from logger import logger
|
||||
from handlers.buttons import MAIN_MENU
|
||||
from logger import logger
|
||||
|
||||
|
||||
last_unblock_data = {}
|
||||
|
||||
|
||||
|
||||
def get_country_from_server(server: str) -> str:
|
||||
"""
|
||||
Определяет страну сервера по его имени или домену.
|
||||
Ищет совпадение части домена в полных доменах.
|
||||
"""
|
||||
server_part = server.split('.')[0]
|
||||
server_part = server.split(".")[0]
|
||||
|
||||
for full_domain, country in SERVER_COUNTRIES.items():
|
||||
if server_part in full_domain:
|
||||
@@ -52,17 +48,18 @@ def handle_telegram_errors(func):
|
||||
tg_id = kwargs.get("tg_id") or args[1]
|
||||
logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}")
|
||||
return False
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@handle_telegram_errors
|
||||
async def send_notification(tg_id: int, username: str, ip: str, server: str, action: str, timestamp: str):
|
||||
country = get_country_from_server(server)
|
||||
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
if action == 'block':
|
||||
|
||||
if action == "block":
|
||||
message = (
|
||||
f"⚠️ <b>Замечено использование торрентов</b> ⚠️\n\n"
|
||||
f"<b>Уважаемый пользователь, мы обнаружили использование торрент-трафика в вашей подписке.</b>\n\n"
|
||||
@@ -92,13 +89,8 @@ async def send_notification(tg_id: int, username: str, ip: str, server: str, act
|
||||
f"• Пожалуйста, воздержитесь от использования торрентов\n"
|
||||
f"• Убедитесь, что торрент-клиент полностью выключен"
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text=message,
|
||||
parse_mode='HTML',
|
||||
reply_markup=builder.as_markup()
|
||||
)
|
||||
|
||||
await bot.send_message(chat_id=tg_id, text=message, parse_mode="HTML", reply_markup=builder.as_markup())
|
||||
logger.info(f"Отправлено уведомление пользователю {tg_id} о {action} для подписки {username}")
|
||||
return True
|
||||
|
||||
@@ -107,13 +99,13 @@ async def tblocker_webhook(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
logger.info(f"Получен запрос от tblocker: {data}")
|
||||
|
||||
username = data.get('username')
|
||||
ip = data.get('ip')
|
||||
server = data.get('server')
|
||||
action = data.get('action')
|
||||
timestamp = data.get('timestamp')
|
||||
|
||||
|
||||
username = data.get("username")
|
||||
ip = data.get("ip")
|
||||
server = data.get("server")
|
||||
action = data.get("action")
|
||||
timestamp = data.get("timestamp")
|
||||
|
||||
if not all([username, ip, server, action, timestamp]):
|
||||
logger.error("Неполные данные в вебхуке")
|
||||
return web.json_response({"error": "Missing required fields"}, status=400)
|
||||
@@ -122,45 +114,34 @@ async def tblocker_webhook(request):
|
||||
current_time = datetime.now().timestamp()
|
||||
|
||||
last_unblock_data = {
|
||||
k: v for k, v in last_unblock_data.items()
|
||||
if current_time - v['received_at'] <= TIMESTAMP_TTL
|
||||
k: v for k, v in last_unblock_data.items() if current_time - v["received_at"] <= TIMESTAMP_TTL
|
||||
}
|
||||
|
||||
cache_key = f"{username}:{server}"
|
||||
if action == 'unblock' and cache_key in last_unblock_data:
|
||||
if timestamp == last_unblock_data[cache_key]['timestamp']:
|
||||
if action == "unblock" and cache_key in last_unblock_data:
|
||||
if timestamp == last_unblock_data[cache_key]["timestamp"]:
|
||||
return web.json_response({"status": "ok", "message": "duplicate unblock skipped"})
|
||||
|
||||
if action == 'unblock':
|
||||
last_unblock_data[cache_key] = {
|
||||
'timestamp': timestamp,
|
||||
'received_at': current_time
|
||||
}
|
||||
|
||||
if action == "unblock":
|
||||
last_unblock_data[cache_key] = {"timestamp": timestamp, "received_at": current_time}
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
key_info = await get_key_details(username, conn)
|
||||
|
||||
|
||||
if not key_info:
|
||||
logger.error(f"Ключ не найден для email {username}")
|
||||
return web.json_response({"error": "Key not found"}, status=404)
|
||||
|
||||
success = await send_notification(
|
||||
key_info['tg_id'],
|
||||
username,
|
||||
ip,
|
||||
server,
|
||||
action,
|
||||
timestamp
|
||||
)
|
||||
|
||||
|
||||
success = await send_notification(key_info["tg_id"], username, ip, server, action, timestamp)
|
||||
|
||||
if not success:
|
||||
logger.warning(f"Не удалось отправить уведомление пользователю {key_info['tg_id']}")
|
||||
|
||||
|
||||
return web.json_response({"status": "ok"})
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке вебхука: {str(e)}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user