refactoring and optimization

This commit is contained in:
Vladless
2026-02-27 22:04:01 +03:00
parent 5709dca63c
commit 18a72f9410
14 changed files with 243 additions and 129 deletions
+6 -1
View File
@@ -24,7 +24,12 @@ async def _get_redis() -> Any | None:
try:
redis_from_url = import_module("redis.asyncio").from_url
client = redis_from_url(REDIS_URL, encoding="utf-8", decode_responses=True)
client = redis_from_url(
REDIS_URL,
encoding="utf-8",
decode_responses=True,
max_connections=64,
)
await client.ping()
_REDIS_CLIENT = client
return _REDIS_CLIENT
+66
View File
@@ -1,3 +1,4 @@
from collections import defaultdict
from datetime import datetime, timedelta
from sqlalchemy import and_, delete, func, select, tuple_
@@ -47,6 +48,43 @@ async def delete_notification(session: AsyncSession, tg_id: int, notification_ty
logger.debug(f"🗑 Уведомление {notification_type} для пользователя {tg_id} удалено")
async def bulk_add_notifications(
session: AsyncSession, items: list[tuple[int, str]], *, commit: bool = False
) -> None:
"""Один запрос: вставка/обновление многих (tg_id, notification_type). Без commit, если commit=False."""
if not items:
return
now = datetime.utcnow()
stmt = insert(Notification).values(
[
{"tg_id": tg_id, "notification_type": ntype, "last_notification_time": now}
for tg_id, ntype in items
]
).on_conflict_do_update(
index_elements=[Notification.tg_id, Notification.notification_type],
set_={"last_notification_time": now},
)
await session.execute(stmt)
if commit:
await session.commit()
logger.info(f"✅ Bulk: добавлено/обновлено {len(items)} уведомлений")
async def bulk_delete_notifications(
session: AsyncSession, items: list[tuple[int, str]], *, commit: bool = False
) -> None:
"""Один запрос: удаление многих (tg_id, notification_type). Без commit, если commit=False."""
if not items:
return
stmt = delete(Notification).where(
tuple_(Notification.tg_id, Notification.notification_type).in_(items)
)
await session.execute(stmt)
if commit:
await session.commit()
logger.debug(f"🗑 Bulk: удалено {len(items)} уведомлений")
async def check_notification_time(session: AsyncSession, tg_id: int, notification_type: str, hours: int = 12) -> bool:
stmt = select(Notification.last_notification_time).where(
Notification.tg_id == tg_id, Notification.notification_type == notification_type
@@ -135,6 +173,34 @@ async def get_last_notification_times_bulk(
return out
_HOT_LEAD_NOTIFICATION_TYPES = (
"hot_lead_step_1",
"hot_lead_step_2",
"hot_lead_step_3",
"hot_lead_step_2_expired",
)
async def get_hot_lead_notification_flags(
session: AsyncSession, tg_ids: list[int]
) -> dict[int, set[str]]:
"""
Один запрос: для каждого tg_id возвращает множество типов уведомлений hot_lead_*,
которые у него уже есть. Используется в notify_hot_leads для устранения N+1.
"""
if not tg_ids:
return {}
stmt = select(Notification.tg_id, Notification.notification_type).where(
Notification.tg_id.in_(tg_ids),
Notification.notification_type.in_(_HOT_LEAD_NOTIFICATION_TYPES),
)
result = await session.execute(stmt)
out = defaultdict(set)
for tg_id, ntype in result.all():
out[tg_id].add(ntype)
return dict(out)
async def check_hot_lead_discount(session: AsyncSession, tg_id: int) -> dict:
try:
result = await session.execute(
+26
View File
@@ -253,6 +253,32 @@ async def check_tariff_exists(session: AsyncSession, tariff_id: int):
return False
async def get_vless_enabled(session: AsyncSession, tariff_id: int | None) -> bool:
"""Возвращает, включён ли VLESS у тарифа (по кэшированному get_tariff_by_id)."""
if not tariff_id:
return False
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
return False
return bool(tariff.get("vless"))
async def get_vless_enabled_batch(
session: AsyncSession, tariff_ids: list[int]
) -> dict[int, bool]:
"""
Один запрос: для списка tariff_id возвращает dict[tariff_id -> vless].
Использовать в списках ключей вместо N вызовов get_vless_enabled.
"""
if not tariff_ids:
return {}
unique_ids = list(dict.fromkeys(tariff_ids))
result = await session.execute(
select(Tariff.id, Tariff.vless).where(Tariff.id.in_(unique_ids))
)
return {row[0]: bool(row[1]) for row in result.all()}
async def get_tariff_sort_order(session: AsyncSession, tariff_id: int) -> int:
try:
result = await session.execute(select(Tariff.sort_order).where(Tariff.id == tariff_id))
+6 -4
View File
@@ -17,7 +17,7 @@ from config import INLINE_MODE, USERNAME_BOT
from database import create_coupon, delete_coupon, get_all_coupons
from filters.admin import IsAdminFilter
from handlers.buttons import BACK
from handlers.utils import format_days
from handlers.utils import format_days, safe_answer_inline_query
from logger import logger
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
@@ -432,7 +432,8 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
coupon = next((c for c in coupons["coupons"] if c["code"] == coupon_code), None)
if not coupon:
await inline_query.answer(
await safe_answer_inline_query(
inline_query,
results=[],
switch_pm_text="Купон не найден",
switch_pm_parameter="coupons",
@@ -442,7 +443,8 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
percent_value = coupon.get("percent")
if percent_value is not None and int(percent_value) > 0:
await inline_query.answer(
await safe_answer_inline_query(
inline_query,
results=[],
switch_pm_text="Процентные купоны не публикуются ссылкой",
switch_pm_parameter="coupons",
@@ -485,4 +487,4 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
reply_markup=builder.as_markup(),
)
await inline_query.answer(results=[result], cache_time=86400, is_personal=True)
await safe_answer_inline_query(inline_query, results=[result], cache_time=86400, is_personal=True)
+3 -3
View File
@@ -33,11 +33,11 @@ from handlers.buttons import (
TV_BUTTON,
)
from handlers.keys.operations import create_key_on_cluster
from database import get_vless_enabled
from handlers.tariffs.tariff_display import (
build_key_created_message,
get_effective_limits_for_key,
resolve_price_to_charge,
resolve_vless_enabled,
)
from handlers.utils import (
edit_or_send_message,
@@ -202,9 +202,9 @@ async def key_cluster_mode(
vless_enabled = False
try:
if plan:
vless_enabled = await resolve_vless_enabled(session, plan)
vless_enabled = await get_vless_enabled(session, plan)
elif key_record.get("tariff_id"):
vless_enabled = await resolve_vless_enabled(session, key_record["tariff_id"])
vless_enabled = await get_vless_enabled(session, key_record["tariff_id"])
except Exception:
vless_enabled = False
+23 -17
View File
@@ -52,6 +52,7 @@ from handlers.buttons import (
TV_BUTTON,
UNFREEZE,
)
from database import get_vless_enabled_batch
from handlers.tariffs.tariff_display import GB, get_key_tariff_addons_state
from handlers.texts import (
DAYS_LEFT_MESSAGE,
@@ -70,6 +71,7 @@ from handlers.utils import (
format_minutes,
get_russian_month,
is_full_remnawave_cluster,
safe_answer_callback,
)
from hooks.hook_buttons import insert_hook_buttons
from hooks.processors import (
@@ -153,6 +155,16 @@ async def build_keys_response(records: list[Key] | None, session: AsyncSession,
end = start + page_size
page_records = records[start:end]
tariff_ids = []
for record in page_records:
tid = getattr(record, "tariff_id", None)
if tid is not None:
try:
tariff_ids.append(int(tid))
except (TypeError, ValueError):
pass
vless_by_tariff = await get_vless_enabled_batch(session, tariff_ids) if tariff_ids else {}
for record in page_records:
alias = record.alias
email = record.email
@@ -167,14 +179,8 @@ async def build_keys_response(records: list[Key] | None, session: AsyncSession,
else:
formatted_date_full = "без срока действия"
is_vless = False
if getattr(record, "tariff_id", None):
try:
from handlers.tariffs.tariff_display import resolve_vless_enabled
is_vless = await resolve_vless_enabled(session, int(record.tariff_id))
except Exception:
is_vless = False
tid = getattr(record, "tariff_id", None)
is_vless = vless_by_tariff.get(int(tid), False) if tid is not None else False
icon = "📶" if is_vless else "🔑"
@@ -232,7 +238,7 @@ async def handle_rename_key(callback: CallbackQuery, state: FSMContext, session:
client_id = callback.data.split("|")[1]
key_row = (await session.execute(select(Key).where(Key.client_id == client_id))).scalar_one_or_none()
if not key_row or key_row.tg_id != callback.from_user.id:
await callback.answer("Доступ запрещён.", show_alert=True)
await safe_answer_callback(callback, "Доступ запрещён.", show_alert=True)
return
await state.set_state(RenameKeyState.waiting_for_new_alias)
await state.update_data(client_id=client_id)
@@ -289,7 +295,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Asyn
key_name = callback_query.data.split("|")[1]
record = await get_key_details(session, key_name)
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
await safe_answer_callback(callback_query, "Доступ запрещён.", show_alert=True)
return
image_path = os.path.join("img", "pic_view.jpg")
await render_key_info(callback_query.message, session, key_name, image_path)
@@ -489,20 +495,20 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: AsyncSession
record = await get_key_details(session, key_name)
if not record:
await callback_query.answer("❌ Ключ не найден.", show_alert=True)
await safe_answer_callback(callback_query, "❌ Ключ не найден.", show_alert=True)
return
if not key_owned_by_user(record, callback_query.from_user.id):
await callback_query.answer("Доступ запрещён.", show_alert=True)
await safe_answer_callback(callback_query, "Доступ запрещён.", show_alert=True)
return
client_id = record.get("client_id")
if not client_id:
await callback_query.answer("❌ У ключа отсутствует client_id.", show_alert=True)
await safe_answer_callback(callback_query, "❌ У ключа отсутствует client_id.", show_alert=True)
return
remna_api_url = await resolve_remnawave_api_url(session, str(record.get("server_id") or ""), fallback_any=True)
if not remna_api_url:
await callback_query.answer("❌ Remnawave-сервер не найден.", show_alert=True)
await safe_answer_callback(callback_query, "❌ Remnawave-сервер не найден.", show_alert=True)
return
async def _reset_devices(api):
@@ -523,7 +529,7 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: AsyncSession
timeout_sec=12.0,
)
if reset_result is None:
await callback_query.answer("❌ Авторизация в Remnawave не удалась.", show_alert=True)
await safe_answer_callback(callback_query, "❌ Авторизация в Remnawave не удалась.", show_alert=True)
return
total, deleted = reset_result
@@ -534,9 +540,9 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: AsyncSession
fallback_any=True,
)
if total == 0:
await callback_query.answer("✅ Устройства не были привязаны.", show_alert=True)
await safe_answer_callback(callback_query, "✅ Устройства не были привязаны.", show_alert=True)
else:
await callback_query.answer(f"✅ Устройства сброшены ({deleted})", show_alert=True)
await safe_answer_callback(callback_query, f"✅ Устройства сброшены ({deleted})", show_alert=True)
if await process_after_hwid_reset(
chat_id=callback_query.from_user.id,
+41 -22
View File
@@ -26,6 +26,8 @@ from config import (
from core.bootstrap import MODES_CONFIG, NOTIFICATIONS_CONFIG
from database import (
add_notification,
bulk_add_notifications,
bulk_delete_notifications,
check_notification_time,
check_notification_time_bulk,
check_notifications_bulk,
@@ -144,34 +146,51 @@ async def preload_notification_data(session: AsyncSession) -> dict[str, Any]:
async def execute_bulk_updates(session: AsyncSession, bulk_updates: dict[str, Any]) -> None:
try:
if bulk_updates["balance_changes"]:
for tg_id, balance_change in bulk_updates["balance_changes"].items():
await session.execute(
text("UPDATE users SET balance = balance + :change WHERE tg_id = :tg_id"),
{"change": balance_change, "tg_id": tg_id},
balance_changes = bulk_updates.get("balance_changes") or {}
if balance_changes:
tg_ids = list(balance_changes.keys())
changes = [balance_changes[tg_id] for tg_id in tg_ids]
await session.execute(
text(
"UPDATE users SET balance = balance + v.change FROM "
"(SELECT unnest(CAST(:tg_ids AS bigint[])) AS tg_id, unnest(CAST(:changes AS double precision[])) AS change) AS v "
"WHERE users.tg_id = v.tg_id"
),
{"tg_ids": tg_ids, "changes": changes},
)
logger.info(f"Bulk: обновлено {len(balance_changes)} балансов")
key_expiry = bulk_updates.get("key_expiry_updates") or []
if key_expiry:
await session.run_sync(
lambda sync_sess: sync_sess.bulk_update_mappings(
Key,
[{"client_id": cid, "expiry_time": exp} for cid, exp in key_expiry],
)
logger.info(f"Bulk: обновлено {len(bulk_updates['balance_changes'])} балансов")
)
logger.info(f"Bulk: обновлено {len(key_expiry)} сроков действия ключей")
if bulk_updates["key_expiry_updates"]:
for client_id, new_expiry in bulk_updates["key_expiry_updates"]:
await session.execute(update(Key).where(Key.client_id == client_id).values(expiry_time=new_expiry))
logger.info(f"Bulk: обновлено {len(bulk_updates['key_expiry_updates'])} сроков действия ключей")
key_tariff = bulk_updates.get("key_tariff_updates") or []
if key_tariff:
await session.run_sync(
lambda sync_sess: sync_sess.bulk_update_mappings(
Key,
[{"client_id": cid, "tariff_id": tid} for cid, tid in key_tariff],
)
)
logger.info(f"Bulk: обновлено {len(key_tariff)} тарифов ключей")
if bulk_updates["key_tariff_updates"]:
for client_id, new_tariff_id in bulk_updates["key_tariff_updates"]:
await session.execute(update(Key).where(Key.client_id == client_id).values(tariff_id=new_tariff_id))
logger.info(f"Bulk: обновлено {len(bulk_updates['key_tariff_updates'])} тарифов ключей")
to_add = bulk_updates.get("notifications_to_add") or []
if to_add:
await bulk_add_notifications(session, to_add, commit=False)
for tg_id, notification_type in bulk_updates["notifications_to_add"]:
await add_notification(session, tg_id, notification_type)
to_delete = bulk_updates.get("notifications_to_delete") or []
if to_delete:
await bulk_delete_notifications(session, to_delete, commit=False)
for tg_id, notification_type in bulk_updates["notifications_to_delete"]:
await delete_notification(session, tg_id, notification_type)
if bulk_updates["notifications_to_add"] or bulk_updates["notifications_to_delete"]:
if to_add or to_delete:
logger.info(
f"Bulk: обработано {len(bulk_updates['notifications_to_add'])} добавлений "
f"и {len(bulk_updates['notifications_to_delete'])} удалений уведомлений"
f"Bulk: обработано {len(to_add)} добавлений и {len(to_delete)} удалений уведомлений"
)
await session.commit()
@@ -1,13 +1,16 @@
from aiogram import Bot
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from config import DISCOUNT_ACTIVE_HOURS, HOT_LEAD_INTERVAL_HOURS
from core.bootstrap import NOTIFICATIONS_CONFIG
from database import add_notification, check_notification_time, get_hot_leads
from database.models import Notification
from database import (
add_notification,
check_notification_time_bulk,
get_hot_lead_notification_flags,
get_hot_leads,
)
from database.tariffs import get_tariffs
from handlers.buttons import MAIN_MENU
from handlers.notifications.notify_kb import build_hot_lead_kb
@@ -28,38 +31,48 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession):
try:
leads = await get_hot_leads(session)
if not leads:
logger.info("Нет горячих лидов для уведомлений.")
return
flags = await get_hot_lead_notification_flags(session, leads)
can_send_after_step1 = await check_notification_time_bulk(
session, [(tid, "hot_lead_step_1") for tid in leads], hot_lead_interval_hours
)
step2_expired_can_send = await check_notification_time_bulk(
session, [(tid, "hot_lead_step_2") for tid in leads], discount_active_hours
)
can_send_after_step2 = await check_notification_time_bulk(
session, [(tid, "hot_lead_step_2") for tid in leads], hot_lead_interval_hours
)
discount_tariffs = await get_tariffs(session, group_code="discounts")
active_discount_tariffs = [t for t in discount_tariffs if t.get("is_active")]
discount_max_tariffs = await get_tariffs(session, group_code="discounts_max")
active_discount_max_tariffs = [t for t in discount_max_tariffs if t.get("is_active")]
notified = 0
for tg_id in leads:
has_step_1 = await session.scalar(
select(select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_1").exists())
)
step_flags = flags.get(tg_id, set())
has_step_1 = "hot_lead_step_1" in step_flags
has_step_2 = "hot_lead_step_2" in step_flags
has_step_3 = "hot_lead_step_3" in step_flags
has_expired_notification = "hot_lead_step_2_expired" in step_flags
if not has_step_1:
await add_notification(session, tg_id, "hot_lead_step_1")
logger.info(f"[HOT LEAD] Шаг 1 — зафиксировано без отправки: {tg_id}")
continue
has_step_2 = await session.scalar(
select(select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_2").exists())
)
if not has_step_2:
can_send = await check_notification_time(
session,
tg_id=tg_id,
notification_type="hot_lead_step_1",
hours=hot_lead_interval_hours,
)
if not can_send:
if (tg_id, "hot_lead_step_1") not in can_send_after_step1:
continue
discount_tariffs = await get_tariffs(session, group_code="discounts")
active_discount_tariffs = [t for t in discount_tariffs if t.get("is_active")]
if not active_discount_tariffs:
logger.warning(
f"[HOT LEAD] Пропуск шага 2 для {tg_id}: нет активных тарифов со скидкой (discounts)"
)
continue
keyboard = build_hot_lead_kb()
result = await send_notification(bot, tg_id, None, HOT_LEAD_MESSAGE, keyboard)
if result:
@@ -68,25 +81,10 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession):
notified += 1
continue
has_step_3 = await session.scalar(
select(select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_3").exists())
)
has_expired_notification = await session.scalar(
select(
select(Notification).filter_by(tg_id=tg_id, notification_type="hot_lead_step_2_expired").exists()
)
)
if not has_step_3 and not has_expired_notification:
expired = await check_notification_time(
session,
tg_id=tg_id,
notification_type="hot_lead_step_2",
hours=discount_active_hours,
)
if expired:
if (tg_id, "hot_lead_step_2") in step2_expired_can_send:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
result = await send_notification(
bot,
tg_id,
@@ -97,26 +95,16 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession):
if result:
await add_notification(session, tg_id, "hot_lead_step_2_expired")
logger.info(f"📭 Скидка упущена — отправлено уведомление: {tg_id}")
continue
continue
if not has_step_3:
can_send = await check_notification_time(
session,
tg_id=tg_id,
notification_type="hot_lead_step_2",
hours=hot_lead_interval_hours,
)
if not can_send:
if (tg_id, "hot_lead_step_2") not in can_send_after_step2:
continue
discount_max_tariffs = await get_tariffs(session, group_code="discounts_max")
active_discount_max_tariffs = [t for t in discount_max_tariffs if t.get("is_active")]
if not active_discount_max_tariffs:
logger.warning(
f"[HOT LEAD] Пропуск шага 3 для {tg_id}: нет активных тарифов с максимальной скидкой (discounts_max)"
)
continue
keyboard = build_hot_lead_kb(final=True)
result = await send_notification(bot, tg_id, None, HOT_LEAD_FINAL_MESSAGE, keyboard)
if result:
+1 -1
View File
@@ -358,7 +358,7 @@ async def prepare_key_expiry_data(key, session: AsyncSession, current_time: int)
device_limit = 0
try:
name, subgroup_title, traffic_limit_gb, device_limit, _ = await get_key_tariff_display(
name, subgroup_title, traffic_limit_gb, device_limit, _, _ = await get_key_tariff_display(
session=session,
key_record=record,
)
+2 -2
View File
@@ -390,11 +390,11 @@ async def fastflow_apply_coupon(message: Message, state: FSMContext, session: An
required_amount_new = int(max(0, ceil(float(new_price) - float(balance_now))))
percent_value = int(getattr(coupon, "percent", 0) or 0)
await create_coupon_usage(session, coupon.id, message.from_user.id)
await update_coupon_usage_count(session, coupon.id)
percent_value = int(getattr(coupon, "percent", 0) or 0)
temp_payload_updated = dict(temp_payload)
temp_payload_updated["required_amount"] = int(required_amount_new)
if "selected_price_rub" in temp_payload_updated:
+3 -3
View File
@@ -43,7 +43,7 @@ from handlers.texts import (
from logger import logger
from .texts import get_referral_link
from .utils import edit_or_send_message, format_days
from .utils import edit_or_send_message, format_days, safe_answer_inline_query
router = Router()
@@ -139,7 +139,7 @@ async def inline_referral_handler(inline_query: InlineQuery, session: AsyncSessi
trial_tariffs = await get_tariffs(session, group_code="trial")
if not trial_tariffs:
await inline_query.answer(results=[], cache_time=0)
await safe_answer_inline_query(inline_query, results=[], cache_time=0)
return
trial_days = trial_tariffs[0]["duration_days"]
@@ -168,7 +168,7 @@ async def inline_referral_handler(inline_query: InlineQuery, session: AsyncSessi
)
)
await inline_query.answer(results=results, cache_time=60, is_personal=True)
await safe_answer_inline_query(inline_query, results=results, cache_time=60, is_personal=True)
@router.callback_query(F.data.startswith("show_referral_qr|"))
+5 -9
View File
@@ -66,7 +66,7 @@ from middlewares.session import release_session_early
from .admin.panel.keyboard import AdminPanelCallback
from .refferal import handle_referral_link
from .utils import edit_or_send_message, extract_user_data
from .utils import edit_or_send_message, extract_user_data, safe_answer_callback
router = Router()
@@ -127,7 +127,7 @@ async def check_subscription_callback(callback: CallbackQuery, state: FSMContext
if member.status not in ["member", "administrator", "creator"]:
await prompt_subscription(callback)
return
await callback.answer(SUBSCRIPTION_CONFIRMED_MSG)
await safe_answer_callback(callback, SUBSCRIPTION_CONFIRMED_MSG)
data = await state.get_data()
original_text = data.get("original_text") or callback.message.text
user_data = data.get("user_data") or extract_user_data(callback.from_user)
@@ -135,7 +135,7 @@ async def check_subscription_callback(callback: CallbackQuery, state: FSMContext
await process_start_logic(callback.message, state, session, admin, original_text, user_data)
except Exception as e:
logger.error(f"[CALLBACK] Ошибка подписки: {e}", exc_info=True)
await callback.answer(SUBSCRIPTION_CHECK_ERROR_MSG, show_alert=True)
await safe_answer_callback(callback, SUBSCRIPTION_CHECK_ERROR_MSG, show_alert=True)
async def process_start_logic(
@@ -229,11 +229,7 @@ async def process_start_logic(
async def handle_coupon_link(part, message, state, session, admin, user_data):
code = part.split("coupons")[1].strip("_")
coupon = await get_coupon_by_code(session, code)
if coupon:
await activate_coupon(message, state, session, code, admin=admin, user_data=user_data)
if getattr(coupon, "days", None):
return
await activate_coupon(message, state, session, code, admin=admin, user_data=user_data)
async def handle_gift(part, message, state, session, user_data):
@@ -268,7 +264,7 @@ async def handle_referral_link_safe(part, message, state, session, user_data):
async def prompt_subscription(callback: CallbackQuery):
await callback.answer(NOT_SUBSCRIBED_YET_MSG, show_alert=True)
await safe_answer_callback(callback, NOT_SUBSCRIBED_YET_MSG, show_alert=True)
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text=SUB_CHANELL, url=CHANNEL_URL))
kb.row(InlineKeyboardButton(text=SUB_CHANELL_DONE, callback_data="check_subscription"))
+9 -18
View File
@@ -18,9 +18,11 @@ async def get_effective_limits_for_key(
tariff_id: int | None,
selected_device_limit: int | None,
selected_traffic_gb: int | None,
tariff: dict | None = None,
) -> tuple[int, int]:
"""Возвращает лимиты устройств и трафика с учётом выбранных значений."""
tariff = await get_tariff_by_id(session, int(tariff_id)) if tariff_id else None
"""Возвращает лимиты устройств и трафика с учётом выбранных значений. tariff опционален — если передан, get_tariff_by_id не вызывается."""
if tariff is None and tariff_id:
tariff = await get_tariff_by_id(session, int(tariff_id))
if tariff:
base_devices = tariff.get("device_limit")
@@ -137,18 +139,6 @@ async def resolve_price_to_charge(session: AsyncSession, state_data: dict[str, A
return int(base_price + device_add_rub + traffic_add_rub)
async def resolve_vless_enabled(session: AsyncSession, tariff_id: int | None) -> bool:
"""Проверяет, включён ли VLESS в тарифе."""
if not tariff_id:
return False
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
return False
return bool(tariff.get("vless"))
async def get_key_tariff_display(
session: AsyncSession,
key_record: dict[str, Any],
@@ -158,8 +148,9 @@ async def get_key_tariff_display(
"""Возвращает отображение тарифа и эффективные лимиты, приоритет — данные панели."""
tariff_id = key_record.get("tariff_id")
if not tariff_id:
return "", "", 0, 0, False
return "", "", 0, 0, False, None
tariff = await get_tariff_by_id(session, int(tariff_id))
selected_device_limit = selected_device_limit_override
selected_traffic_gb = selected_traffic_gb_override
@@ -184,6 +175,7 @@ async def get_key_tariff_display(
tariff_id=int(tariff_id),
selected_device_limit=selected_device_limit,
selected_traffic_gb=selected_traffic_gb,
tariff=tariff,
)
server_cluster_id = key_record.get("server_id")
@@ -216,7 +208,6 @@ async def get_key_tariff_display(
traffic_limit_gb = int(traffic_limit_bytes / GB) if traffic_limit_bytes else 0
tariff = await get_tariff_by_id(session, int(tariff_id))
if tariff:
tariff_name = tariff.get("name", "")
subgroup_title = tariff.get("subgroup_title") or ""
@@ -226,7 +217,7 @@ async def get_key_tariff_display(
subgroup_title = ""
vless_enabled = False
return tariff_name, subgroup_title, traffic_limit_gb, device_limit, vless_enabled
return tariff_name, subgroup_title, traffic_limit_gb, device_limit, vless_enabled, tariff
async def get_key_tariff_addons_state(
@@ -260,6 +251,7 @@ async def get_key_tariff_addons_state(
traffic_limit_gb,
device_limit,
vless_enabled,
tariff,
) = await get_key_tariff_display(
session=session,
key_record=key_record,
@@ -282,7 +274,6 @@ async def get_key_tariff_addons_state(
addons_devices_enabled = False
addons_traffic_enabled = False
tariff = await get_tariff_by_id(session, int(tariff_id))
if tariff and tariff.get("configurable"):
is_tariff_configurable = True
+16 -1
View File
@@ -13,6 +13,7 @@ from aiogram.types import (
BufferedInputFile,
CallbackQuery,
InlineKeyboardMarkup,
InlineQuery,
InputMediaAnimation,
InputMediaPhoto,
InputMediaVideo,
@@ -58,6 +59,19 @@ async def safe_answer_callback(callback_query: CallbackQuery, text: str | None =
raise
async def safe_answer_inline_query(inline_query: InlineQuery, *args: object, **kwargs: object) -> None:
"""
Вызывает inline_query.answer(), не поднимая исключение при устаревшем запросе
(query is too old / response timeout). При нагрузке inline может обрабатываться с задержкой.
"""
try:
await inline_query.answer(*args, **kwargs)
except TelegramBadRequest as e:
msg = str(e).lower()
if not any(phrase in msg for phrase in _CALLBACK_ANSWER_IGNORE):
raise
async def generate_random_email(
length: int = 8,
session: AsyncSession | None = None,
@@ -391,7 +405,8 @@ async def edit_or_send_message(
edit_or_send_message.cache.popitem(last=False)
return
if not force_text and target_message.caption is not None:
caption = getattr(target_message, "caption", None)
if not force_text and caption is not None:
try:
await target_message.edit_caption(caption=text, reply_markup=reply_markup)
return